Add interactive search to diff view

Add a simple interactive search to the diff view.  This is bound
to C-s by default.  It highlights text, but does not yet navigate.

Change-Id: Ic795bb5d18911590237b6595f812e10fd4baa1ce
This commit is contained in:
James E. Blair 2015-10-20 21:11:58 -07:00
parent c8d81b7693
commit 0d0f0f6dcd
6 changed files with 100 additions and 3 deletions

View File

@ -73,6 +73,7 @@ TOGGLE_SUBSCRIBED = 'toggle subscribed'
SELECT_PATCHSETS = 'select patchsets'
NEXT_SELECTABLE = 'next selectable'
PREV_SELECTABLE = 'prev selectable'
INTERACTIVE_SEARCH = 'interactive search'
DEFAULT_KEYMAP = {
REDRAW_SCREEN: 'ctrl l',
@ -129,7 +130,8 @@ DEFAULT_KEYMAP = {
SELECT_PATCHSETS: 'p',
NEXT_SELECTABLE: 'tab',
PREV_SELECTABLE: 'shift tab',
}
INTERACTIVE_SEARCH: 'ctrl s',
}
URWID_COMMANDS = frozenset((
urwid.REDRAW_SCREEN,

View File

@ -193,6 +193,52 @@ class YesNoDialog(ButtonDialog):
return None
return r
class SearchableText(urwid.Text):
def set_text(self, markup):
self._markup = markup
super(SearchableText, self).set_text(markup)
def search(self, search, attribute):
if not search:
self.set_text(self._markup)
return
(text, attrs) = urwid.util.decompose_tagmarkup(self._markup)
last = 0
while True:
start = text.find(search, last)
if start < 0:
break
end = start + len(search)
i = 0
newattrs = []
for attr, al in attrs:
if i + al <= start:
i += al
newattrs.append((attr, al))
continue
if i >= end:
i += al
newattrs.append((attr, al))
continue
before = max(start - i, 0)
after = max(i + al - end, 0)
if before:
newattrs.append((attr, before))
newattrs.append((attribute, len(search)))
if after:
newattrs.append((attr, after))
i += al
if i < start:
newattrs.append((None, start-i))
i += start-i
if i < end:
newattrs.append((attribute, len(search)))
last = start + 1
attrs = newattrs
self._text = text
self._attrib = attrs
self._invalidate()
class HyperText(urwid.Text):
_selectable = True

View File

@ -47,6 +47,7 @@ DEFAULT_PALETTE={
'comment-name': ['white', 'dark gray'],
'line-number': ['dark gray', ''],
'focused-line-number': ['dark gray,standout', ''],
'search-result': ['default,standout', ''],
# Change view
'change-data': ['dark cyan', ''],
'focused-change-data': ['light cyan', ''],

View File

@ -106,10 +106,16 @@ class BaseDiffLine(urwid.Button):
def selectable(self):
return True
def search(self, search, attribute):
pass
class BaseFileHeader(urwid.Button):
def selectable(self):
return True
def search(self, search, attribute):
pass
class BaseFileReminder(urwid.WidgetWrap):
pass
@ -170,6 +176,7 @@ class BaseDiffView(urwid.WidgetWrap):
def _init(self):
del self._w.contents[:]
self.search = None
with self.app.db.getSession() as session:
new_revision = session.getRevision(self.new_revision_key)
old_comments = []
@ -427,7 +434,26 @@ class BaseDiffView(urwid.WidgetWrap):
context = item.context
return context
def search_valid_char(self, ch):
return urwid.util.is_wide_char(ch, 0) or (len(ch) == 1 and ord(ch) >= 32)
def keypress(self, size, key):
if self.search is not None:
if self.search_valid_char(key) or key == 'backspace':
if key == 'backspace':
self.search = self.search[:-1]
else:
self.search += key
self.interactiveSearch(self.search)
return None
else:
self.app.status.update(title=self.title)
if not self.search:
self.interactiveSearch(None)
self.search = None
if key in ['enter', 'esc']:
return None
old_focus = self.listbox.focus
r = super(BaseDiffView, self).keypress(size, key)
new_focus = self.listbox.focus
@ -446,6 +472,10 @@ class BaseDiffView(urwid.WidgetWrap):
if keymap.SELECT_PATCHSETS in commands:
self.openPatchsetDialog()
return None
if keymap.INTERACTIVE_SEARCH in commands:
self.search = ''
self.interactiveSearch(self.search)
return None
return r
def mouse_event(self, size, event, button, x, y, focus):
@ -515,3 +545,10 @@ class BaseDiffView(urwid.WidgetWrap):
self.app.backScreen()
self.old_revision_key, self.new_revision_key = dialog.getSelected()
self._init()
def interactiveSearch(self, search):
if search is not None:
self.app.status.update(title=("Search: " + search))
for line in self.listbox.body:
if hasattr(line, 'search'):
line.search(search, 'search-result')

View File

@ -82,6 +82,7 @@ class SideDiffLine(BaseDiffLine):
def __init__(self, app, context, old, new, callback=None):
super(SideDiffLine, self).__init__('', on_press=callback)
self.context = context
self.text_widgets = []
columns = []
for (ln, action, line) in (old, new):
if ln is None:
@ -90,7 +91,9 @@ class SideDiffLine(BaseDiffLine):
ln = '%*i' % (LN_COL_WIDTH-1, ln)
ln_col = urwid.Text(('line-number', ln))
ln_col.set_wrap_mode('clip')
line_col = urwid.Text(line)
line_col = mywid.SearchableText(line)
self.text_widgets.append(line_col)
if action == '':
line_col = urwid.AttrMap(line_col, 'nonexistent')
columns += [(LN_COL_WIDTH, ln_col), line_col]
@ -105,6 +108,10 @@ class SideDiffLine(BaseDiffLine):
}
self._w = urwid.AttrMap(col, None, focus_map=map)
def search(self, search, attribute):
for w in self.text_widgets:
w.search(search, attribute)
class SideFileHeader(BaseFileHeader):
def __init__(self, app, context, old, new, callback=None):
super(SideFileHeader, self).__init__('', on_press=callback)

View File

@ -73,7 +73,8 @@ class UnifiedDiffLine(BaseDiffLine):
columns = [(LN_COL_WIDTH, urwid.Text(u'')), (LN_COL_WIDTH, new_ln_col)]
if new_action == ' ':
columns = [(LN_COL_WIDTH, old_ln_col), (LN_COL_WIDTH, new_ln_col)]
line_col = urwid.Text(line)
line_col = mywid.SearchableText(line)
self.text_widget = line_col
if action == '':
line_col = urwid.AttrMap(line_col, 'nonexistent')
columns += [line_col]
@ -88,6 +89,9 @@ class UnifiedDiffLine(BaseDiffLine):
}
self._w = urwid.AttrMap(col, None, focus_map=map)
def search(self, search, attribute):
self.text_widget.search(search, attribute)
class UnifiedFileHeader(BaseFileHeader):
def __init__(self, app, context, oldnew, old, new, callback=None):
super(UnifiedFileHeader, self).__init__('', on_press=callback)