diff --git a/gertty/keymap.py b/gertty/keymap.py index 660adb7..b51f805 100644 --- a/gertty/keymap.py +++ b/gertty/keymap.py @@ -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, diff --git a/gertty/mywid.py b/gertty/mywid.py index 478fe88..63c1695 100644 --- a/gertty/mywid.py +++ b/gertty/mywid.py @@ -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 diff --git a/gertty/palette.py b/gertty/palette.py index fae8756..3656565 100644 --- a/gertty/palette.py +++ b/gertty/palette.py @@ -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', ''], diff --git a/gertty/view/diff.py b/gertty/view/diff.py index 976445a..7f671a4 100644 --- a/gertty/view/diff.py +++ b/gertty/view/diff.py @@ -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') diff --git a/gertty/view/side_diff.py b/gertty/view/side_diff.py index d1f1072..6298505 100644 --- a/gertty/view/side_diff.py +++ b/gertty/view/side_diff.py @@ -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) diff --git a/gertty/view/unified_diff.py b/gertty/view/unified_diff.py index 15f0734..4e63832 100644 --- a/gertty/view/unified_diff.py +++ b/gertty/view/unified_diff.py @@ -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)