From b43210b5a57f7449e158610488e0cd43eb071042 Mon Sep 17 00:00:00 2001 From: David Aguilar Date: Mon, 13 Mar 2023 03:18:57 -0700 Subject: [PATCH] diff: add a "Search in diff" feature Teach the diff editor to search within the diff text when the ctrl-f and ctrl-g hotkeys are used. Closes: #1116 Suggested-by: Giovanni Martins @GiovanniSM20 via github.com Suggested-by: Dorian Marchal @dorian-marchal via github.com Signed-off-by: David Aguilar --- CHANGES.rst | 3 ++ cola/hotkeys.py | 7 ++-- cola/icons.py | 14 ++++++-- cola/widgets/diff.py | 79 ++++++++++++++++++++++++++++++++++++++++++- cola/widgets/text.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/hotkeys.html | 17 +++++++++- docs/hotkeys_de.html | 17 +++++++++- docs/hotkeys_zh_CN.html | 17 +++++++++- docs/hotkeys_zh_TW.html | 17 +++++++++- 9 files changed, 251 insertions(+), 9 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 9cd776c6..aa236e13 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -106,6 +106,9 @@ Usability, bells and whistles files have been selected. (`#697 `_) +* The Diff Editor learned to search within the diff text using the standard ctrl-f hotkey. + (`#1116 `_) + * The Diff Editor now uses an easier-to-see *block cursor* by default. Disable `cola.blockcursor `_ to continue using original *line cursor*. diff --git a/cola/hotkeys.py b/cola/hotkeys.py index fa25cb45..43e5a152 100644 --- a/cola/hotkeys.py +++ b/cola/hotkeys.py @@ -25,11 +25,11 @@ EDIT = hotkey(Qt.CTRL | Qt.Key_E) EDIT_SECONDARY = hotkey(Qt.CTRL | Qt.SHIFT | Qt.Key_E) EXPORT = hotkey(Qt.ALT | Qt.SHIFT | Qt.Key_E) FIT = hotkey(Qt.Key_F) -FETCH = hotkey(Qt.CTRL | Qt.Key_F) +FETCH = hotkey(Qt.CTRL | Qt.SHIFT | Qt.Key_F) FILTER = hotkey(Qt.CTRL | Qt.SHIFT | Qt.Key_F) GOTO_END = hotkey(Qt.SHIFT | Qt.Key_G) GOTO_START = hotkey(Qt.Key_G, Qt.Key_G) # gg -GREP = hotkey(Qt.CTRL | Qt.Key_G) +GREP = hotkey(Qt.ALT | Qt.Key_G) # H-P MOVE_LEFT = hotkey(Qt.Key_H) MOVE_LEFT_SHIFT = hotkey(Qt.SHIFT | Qt.Key_H) @@ -62,6 +62,9 @@ REFRESH_SECONDARY = hotkey(Qt.Key_F5) REFRESH_HOTKEYS = (REFRESH, REFRESH_SECONDARY) STAGE_DIFF = hotkey(Qt.Key_S) EDIT_AND_STAGE_DIFF = hotkey(Qt.CTRL | Qt.SHIFT | Qt.Key_S) +SEARCH = hotkey(Qt.CTRL | Qt.Key_F) +SEARCH_NEXT = hotkey(Qt.CTRL | Qt.Key_G) +SEARCH_PREV = hotkey(Qt.CTRL | Qt.SHIFT | Qt.Key_G) STAGE_DIFF_ALT = hotkey(Qt.SHIFT | Qt.Key_S) STAGE_SELECTION = hotkey(Qt.CTRL | Qt.Key_S) STAGE_ALL = hotkey(Qt.CTRL | Qt.SHIFT | Qt.Key_S) diff --git a/cola/icons.py b/cola/icons.py index 220413a1..48cc5c79 100644 --- a/cola/icons.py +++ b/cola/icons.py @@ -320,12 +320,12 @@ def modified_name(): def move_down(): """Move down icon""" - return from_theme('go-previous', fallback='arrow-down.svg') + return from_theme('go-next', fallback='arrow-up.svg') def move_up(): """Move up icon""" - return from_theme('go-next', fallback='arrow-up.svg') + return from_theme('go-previous', fallback='arrow-down.svg') def new(): @@ -343,6 +343,16 @@ def open_directory(): return from_theme('folder', fallback='folder.svg') +def previous(): + """Previous icon""" + return icon('arrow-up.svg') + + +def next(): + """Go to next item icon""" + return icon('arrow-down.svg') + + def partial_name(): """Partial icon name""" return name_from_basename('partial.svg') diff --git a/cola/widgets/diff.py b/cola/widgets/diff.py index 149f1b65..710ef626 100644 --- a/cola/widgets/diff.py +++ b/cola/widgets/diff.py @@ -27,6 +27,7 @@ from .. import utils from .. import qtutils from .text import TextDecorator from .text import VimHintedPlainTextEdit +from .text import TextSearchWidget from . import defs from . import imageview @@ -439,6 +440,8 @@ class DiffLineNumbers(TextDecorator): class Viewer(QtWidgets.QFrame): """Text and image diff viewers""" + INDEX_TEXT = 0 + INDEX_IMAGE = 1 def __init__(self, context, parent=None): super(Viewer, self).__init__(parent) @@ -451,12 +454,19 @@ class Viewer(QtWidgets.QFrame): self.text = DiffEditor(context, options, self) self.image = imageview.ImageView(parent=self) self.image.setFocusPolicy(Qt.NoFocus) + self.search_widget = TextSearchWidget(self) + self.search_widget.hide() stack = self.stack = QtWidgets.QStackedWidget(self) stack.addWidget(self.text) stack.addWidget(self.image) - self.main_layout = qtutils.vbox(defs.no_margin, defs.no_spacing, self.stack) + self.main_layout = qtutils.vbox( + defs.no_margin, + defs.no_spacing, + self.stack, + self.search_widget, + ) self.setLayout(self.main_layout) # Observe images @@ -474,6 +484,65 @@ class Viewer(QtWidgets.QFrame): self.setFocusProxy(self.text) + self.search_widget.search_text.connect(self.search_text) + + self.search_action = qtutils.add_action( + self, + N_('Search in Diff'), + self.show_search_diff, + hotkeys.SEARCH, + ) + self.search_next_action = qtutils.add_action( + self, + N_('Find next item'), + self.search_widget.search, + hotkeys.SEARCH_NEXT, + ) + self.search_prev_action = qtutils.add_action( + self, + N_('Find previous item'), + self.search_widget.search_backwards, + hotkeys.SEARCH_PREV, + ) + + def show_search_diff(self): + """Show a dialog for searching diffs""" + # The diff search is only active in text mode. + if self.stack.currentIndex() != self.INDEX_TEXT: + return + if not self.search_widget.isVisible(): + self.search_widget.show() + self.search_widget.setFocus(True) + + def search_text(self, text, backwards): + """Search the diff text for the given text""" + cursor = self.text.textCursor() + if cursor.hasSelection(): + selected_text = cursor.selectedText() + case_sensitive = self.search_widget.is_case_sensitive() + if text_matches(case_sensitive, selected_text, text): + if backwards: + position = cursor.selectionStart() + else: + position = cursor.selectionEnd() + else: + if backwards: + position = cursor.selectionEnd() + else: + position = cursor.selectionStart() + cursor.setPosition(position) + self.text.setTextCursor(cursor) + + flags = self.search_widget.find_flags(backwards) + if not self.text.find(text, flags): + if backwards: + location = QtGui.QTextCursor.End + else: + location = QtGui.QTextCursor.Start + cursor.movePosition(location, QtGui.QTextCursor.MoveAnchor) + self.text.setTextCursor(cursor) + self.text.find(text, flags) + def export_state(self, state): state['show_diff_line_numbers'] = self.options.show_line_numbers.isChecked() state['image_diff_mode'] = self.options.image_mode.currentIndex() @@ -501,6 +570,7 @@ class Viewer(QtWidgets.QFrame): self.options.set_diff_type(diff_type) if diff_type == main.Types.IMAGE: self.stack.setCurrentWidget(self.image) + self.search_widget.hide() self.render() else: self.stack.setCurrentWidget(self.text) @@ -635,6 +705,13 @@ def create_image(width, height): return image +def text_matches(case_sensitive, a, b): + """Compare text with case sensitivity taken into account""" + if case_sensitive: + return a == b + return a.lower() == b.lower() + + def create_painter(image): painter = QtGui.QPainter(image) painter.fillRect(image.rect(), Qt.transparent) diff --git a/cola/widgets/text.py b/cola/widgets/text.py index db051904..e638305c 100644 --- a/cola/widgets/text.py +++ b/cola/widgets/text.py @@ -27,6 +27,7 @@ def get_stripped(widget): class LineEdit(QtWidgets.QLineEdit): cursor_changed = Signal(int, int) + esc_pressed = Signal() def __init__(self, parent=None, row=1, get_value=None, clear_button=False): QtWidgets.QLineEdit.__init__(self, parent) @@ -62,6 +63,12 @@ class LineEdit(QtWidgets.QLineEdit): self.setText(value) self.setCursorPosition(pos) + def keyPressEvent(self, event): + key = event.key() + if key == Qt.Key_Escape: + self.esc_pressed.emit() + super(LineEdit, self).keyPressEvent(event) + class LineEditCursorPosition(object): """Translate cursorPositionChanged(int,int) into cursorPosition(int,int)""" @@ -370,6 +377,88 @@ class PlainTextEdit(QtWidgets.QPlainTextEdit): self.ext.context_menu_event(event) +class TextSearchWidget(QtWidgets.QWidget): + """The search dialog that displays over a text edit field""" + search_text = Signal(object, bool) + + def __init__(self, parent): + super(TextSearchWidget, self).__init__(parent) + self.setAutoFillBackground(True) + self._parent = parent + + self.text = HintedDefaultLineEdit(N_('Find in diff'), parent=self) + + self.prev_button = qtutils.create_action_button( + tooltip=N_('Find the previous occurrence of the phrase'), + icon=icons.previous() + ) + + self.next_button = qtutils.create_action_button( + tooltip=N_('Find the next occurrence of the phrase'), + icon=icons.next() + ) + + self.match_case_checkbox = qtutils.checkbox(N_('Match Case')) + self.whole_words_checkbox = qtutils.checkbox(N_('Whole Words')) + + self.close_button = qtutils.create_action_button( + tooltip=N_('Close the find bar'), + icon=icons.close() + ) + + layout = qtutils.hbox( + defs.margin, + defs.button_spacing, + self.text, + self.prev_button, + self.next_button, + self.match_case_checkbox, + self.whole_words_checkbox, + qtutils.STRETCH, + self.close_button, + ) + self.setLayout(layout) + self.setFocusProxy(self.text) + + self.text.esc_pressed.connect(self.hide_search) + self.text.returnPressed.connect(self.search) + self.text.textChanged.connect(self.search) + + qtutils.connect_button(self.next_button, self.search) + qtutils.connect_button(self.prev_button, self.search_backwards) + qtutils.connect_button(self.close_button, self.hide_search) + qtutils.connect_checkbox(self.match_case_checkbox, lambda _: self.search()) + qtutils.connect_checkbox(self.whole_words_checkbox, lambda _: self.search()) + + def search(self): + """Emit a signal with the current search text""" + self.search_text.emit(self.text.get(), False) + + def search_backwards(self): + """Emit a signal with the current search text for a backwards search""" + self.search_text.emit(self.text.get(), True) + + def hide_search(self): + """Hide the search window""" + self.hide() + self._parent.setFocus(True) + + def find_flags(self, backwards): + """Return QTextDocument.FindFlags for the current search options""" + flags = QtGui.QTextDocument.FindFlags() + if backwards: + flags = flags | QtGui.QTextDocument.FindBackward + if self.match_case_checkbox.isChecked(): + flags = flags | QtGui.QTextDocument.FindCaseSensitively + if self.whole_words_checkbox.isChecked(): + flags = flags | QtGui.QTextDocument.FindWholeWords + return flags + + def is_case_sensitive(self): + """Are we searching using a case-insensitive search?""" + return self.match_case_checkbox.isChecked() + + class TextEditExtension(BaseTextEditExtension): def init(self): widget = self.widget diff --git a/docs/hotkeys.html b/docs/hotkeys.html index df7fe726..4e083793 100644 --- a/docs/hotkeys.html +++ b/docs/hotkeys.html @@ -137,7 +137,7 @@ span.title { Find files - Ctrl + G + Alt + G : Grep @@ -287,6 +287,21 @@ span.title { View diff using `git difftool` + Ctrl + F + : + Search diff for matching text + + + Ctrl + G + : + Search diff for the next text match + + + Ctrl + Shift + G + : + Search diff for the previous text match + + Alt + Shift + C : Copy Diff to clipboard (strips diff +/- prefixes) diff --git a/docs/hotkeys_de.html b/docs/hotkeys_de.html index 859b43a1..7eb16da1 100644 --- a/docs/hotkeys_de.html +++ b/docs/hotkeys_de.html @@ -141,7 +141,7 @@ span.title { Find files - Ctrl + G + Alt + G : Suchen @@ -267,6 +267,21 @@ span.title { Mithilfe von `git difftool` ansehen + Ctrl + F + : + Search diff for matching text + + + Ctrl + G + : + Search diff for the next text match + + + Ctrl + Shift + G + : + Search diff for the previous text match + + Alt + Shift + C : Copy Diff to clipboard (strips diff +/- prefixes) diff --git a/docs/hotkeys_zh_CN.html b/docs/hotkeys_zh_CN.html index 6f98d2f5..c6926d64 100644 --- a/docs/hotkeys_zh_CN.html +++ b/docs/hotkeys_zh_CN.html @@ -140,7 +140,7 @@ span.title { Find files - Ctrl + G + Alt + G : 搜索 @@ -265,6 +265,21 @@ span.title { 使用「git difftool」命令查看内容差异 + Ctrl + F + : + Search diff for matching text + + + Ctrl + G + : + Search diff for the next text match + + + Ctrl + Shift + G + : + Search diff for the previous text match + + Alt + Shift + C : Copy Diff to clipboard (strips diff +/- prefixes) diff --git a/docs/hotkeys_zh_TW.html b/docs/hotkeys_zh_TW.html index 3d217721..652f938a 100644 --- a/docs/hotkeys_zh_TW.html +++ b/docs/hotkeys_zh_TW.html @@ -138,7 +138,7 @@ span.title { 尋找檔案 - Ctrl + G + Alt + G : 搜尋 @@ -265,6 +265,21 @@ span.title { 使用「git difftool」命令檢視內容差異 + Ctrl + F + : + Search diff for matching text + + + Ctrl + G + : + Search diff for the next text match + + + Ctrl + Shift + G + : + Search diff for the previous text match + + Alt + Shift + C : Copy Diff to clipboard (strips diff +/- prefixes) -- 2.11.4.GIT