-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PR: Implement "Replace in selection" in the Editor #4638
Changes from 4 commits
d22d024
2e904b3
2ad8ed6
ef6698d
9ee947f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,11 +67,13 @@ def __init__(self, parent, enable_replace=False): | |
|
||
self.return_shift_pressed.connect( | ||
lambda: | ||
self.find(changed=False, forward=False, rehighlight=False)) | ||
self.find(changed=False, forward=False, rehighlight=False, | ||
multiline_replace_check = False)) | ||
|
||
self.return_pressed.connect( | ||
lambda: | ||
self.find(changed=False, forward=True, rehighlight=False)) | ||
self.find(changed=False, forward=True, rehighlight=False, | ||
multiline_replace_check = False)) | ||
|
||
self.search_text.lineEdit().textEdited.connect( | ||
self.text_has_been_edited) | ||
|
@@ -124,18 +126,29 @@ def __init__(self, parent, enable_replace=False): | |
self.replace_text.valid.connect( | ||
lambda _: self.replace_find(focus_replace_text=True)) | ||
self.replace_button = create_toolbutton(self, | ||
text=_('Replace/find'), | ||
text=_('Replace/find next'), | ||
icon=ima.icon('DialogApplyButton'), | ||
triggered=self.replace_find, | ||
text_beside_icon=True) | ||
self.replace_button.clicked.connect(self.update_replace_combo) | ||
self.replace_button.clicked.connect(self.update_search_combo) | ||
|
||
self.all_check = QCheckBox(_("Replace all")) | ||
self.replace_sel_button = create_toolbutton(self, | ||
text=_('Replace selection'), | ||
icon=ima.icon('DialogApplyButton'), | ||
triggered=self.replace_find_selection, | ||
text_beside_icon=True) | ||
self.replace_sel_button.clicked.connect(self.update_replace_combo) | ||
self.replace_sel_button.clicked.connect(self.update_search_combo) | ||
|
||
self.replace_all_button = create_toolbutton(self, | ||
text=_('Replace all'), | ||
icon=ima.icon('DialogApplyButton'), | ||
triggered=self.replace_find_all, | ||
text_beside_icon=True) | ||
self.replace_all_button.clicked.connect(self.update_replace_combo) | ||
self.replace_all_button.clicked.connect(self.update_search_combo) | ||
|
||
self.replace_layout = QHBoxLayout() | ||
widgets = [replace_with, self.replace_text, self.replace_button, | ||
self.all_check] | ||
self.replace_sel_button, self.replace_all_button] | ||
for widget in widgets: | ||
self.replace_layout.addWidget(widget) | ||
glayout.addLayout(self.replace_layout, 1, 1) | ||
|
@@ -215,7 +228,8 @@ def toggle_replace_widgets(self): | |
self.hide() | ||
else: | ||
self.show_replace() | ||
self.replace_text.setFocus() | ||
if len(to_text_string(self.search_text.currentText()))>0: | ||
self.replace_text.setFocus() | ||
|
||
@Slot(bool) | ||
def toggle_highlighting(self, state): | ||
|
@@ -226,32 +240,38 @@ def toggle_highlighting(self, state): | |
else: | ||
self.clear_matches() | ||
|
||
def show(self): | ||
def show(self, hide_replace=True): | ||
"""Overrides Qt Method""" | ||
QWidget.show(self) | ||
self.visibility_changed.emit(True) | ||
if self.editor is not None: | ||
if hide_replace: | ||
if self.replace_widgets[0].isVisible(): | ||
self.hide_replace() | ||
text = self.editor.get_selected_text() | ||
highlighted = True | ||
# If no text is highlighted for search, use whatever word is under | ||
# the cursor | ||
if not text: | ||
highlighted = False | ||
try: | ||
cursor = self.editor.textCursor() | ||
cursor.select(QTextCursor.WordUnderCursor) | ||
text = to_text_string(cursor.selectedText()) | ||
except AttributeError: | ||
# We can't do this for all widgets, e.g. WebView's | ||
pass | ||
|
||
# Now that text value is sorted out, use it for the search | ||
if text and not self.search_text.currentText() or highlighted: | ||
self.search_text.setEditText(text) | ||
self.search_text.lineEdit().selectAll() | ||
self.refresh() | ||
else: | ||
self.search_text.lineEdit().selectAll() | ||
# When selecting several lines, and replace box is activated the | ||
# text won't be replaced for the selection | ||
if hide_replace or len(text.splitlines())<=1: | ||
highlighted = True | ||
# If no text is highlighted for search, use whatever word is | ||
# under the cursor | ||
if not text: | ||
highlighted = False | ||
try: | ||
cursor = self.editor.textCursor() | ||
cursor.select(QTextCursor.WordUnderCursor) | ||
text = to_text_string(cursor.selectedText()) | ||
except AttributeError: | ||
# We can't do this for all widgets, e.g. WebView's | ||
pass | ||
|
||
# Now that text value is sorted out, use it for the search | ||
if text and not self.search_text.currentText() or highlighted: | ||
self.search_text.setEditText(text) | ||
self.search_text.lineEdit().selectAll() | ||
self.refresh() | ||
else: | ||
self.search_text.lineEdit().selectAll() | ||
self.search_text.setFocus() | ||
|
||
@Slot() | ||
|
@@ -267,7 +287,7 @@ def hide(self): | |
|
||
def show_replace(self): | ||
"""Show replace widgets""" | ||
self.show() | ||
self.show(hide_replace=False) | ||
for widget in self.replace_widgets: | ||
widget.show() | ||
|
||
|
@@ -314,23 +334,25 @@ def set_editor(self, editor, refresh=True): | |
@Slot() | ||
def find_next(self): | ||
"""Find next occurrence""" | ||
state = self.find(changed=False, forward=True, rehighlight=False) | ||
state = self.find(changed=False, forward=True, rehighlight=False, | ||
multiline_replace_check=False) | ||
self.editor.setFocus() | ||
self.search_text.add_current_text() | ||
return state | ||
|
||
@Slot() | ||
def find_previous(self): | ||
"""Find previous occurrence""" | ||
state = self.find(changed=False, forward=False, rehighlight=False) | ||
state = self.find(changed=False, forward=False, rehighlight=False, | ||
multiline_replace_check=False) | ||
self.editor.setFocus() | ||
return state | ||
|
||
def text_has_been_edited(self, text): | ||
"""Find text has been edited (this slot won't be triggered when | ||
setting the search pattern combo box text programmatically""" | ||
setting the search pattern combo box text programmatically)""" | ||
self.find(changed=True, forward=True, start_highlight_timer=True) | ||
|
||
def highlight_matches(self): | ||
"""Highlight found results""" | ||
if self.is_code_editor and self.highlight_button.isChecked(): | ||
|
@@ -339,15 +361,21 @@ def highlight_matches(self): | |
regexp = self.re_button.isChecked() | ||
self.editor.highlight_found_results(text, words=words, | ||
regexp=regexp) | ||
|
||
def clear_matches(self): | ||
"""Clear all highlighted matches""" | ||
if self.is_code_editor: | ||
self.editor.clear_found_results() | ||
|
||
def find(self, changed=True, forward=True, | ||
rehighlight=True, start_highlight_timer=False): | ||
rehighlight=True, start_highlight_timer=False, multiline_replace_check=True): | ||
"""Call the find function""" | ||
# When several lines are selected in the editor and replace box is activated, | ||
# dynamic search is deactivated to prevent changing the selection. Otherwise | ||
# we show matching items. | ||
if multiline_replace_check and self.replace_widgets[0].isVisible() and \ | ||
len(to_text_string(self.editor.get_selected_text()).splitlines())>1: | ||
return None | ||
text = self.search_text.currentText() | ||
if len(text) == 0: | ||
self.search_text.lineEdit().setStyleSheet("") | ||
|
@@ -374,7 +402,7 @@ def find(self, changed=True, forward=True, | |
return found | ||
|
||
@Slot() | ||
def replace_find(self, focus_replace_text=False): | ||
def replace_find(self, focus_replace_text=False, replace_all=False): | ||
"""Replace and find""" | ||
if (self.editor is not None): | ||
replace_text = to_text_string(self.replace_text.currentText()) | ||
|
@@ -444,10 +472,60 @@ def replace_find(self, focus_replace_text=False): | |
QTextCursor.KeepAnchor) | ||
else: | ||
break | ||
if not self.all_check.isChecked(): | ||
if not replace_all: | ||
break | ||
self.all_check.setCheckState(Qt.Unchecked) | ||
if cursor is not None: | ||
cursor.endEditBlock() | ||
if focus_replace_text: | ||
self.replace_text.setFocus() | ||
|
||
@Slot() | ||
def replace_find_all(self, focus_replace_text=False): | ||
"""Replace and find all matching occurrences""" | ||
self.replace_find(focus_replace_text, replace_all=True) | ||
|
||
|
||
@Slot() | ||
def replace_find_selection(self, focus_replace_text=False): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if using a custom search is the best way, because the other searching methods use the findText function of Although I'm not sure if this could be implemented using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True that there is a small risk that there are different results for the two regex implementations. I think the risk is relatively small because we don't highlight find results when the user chooses a selection. And there's always UNDO... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually a bigger inconsistency is that I didn't implement the whole word restriction. DOH! And it might ultimately be simpler to use the QT implementation because python's re doesn't handle whole words. Let me give this some thought over the weekend. |
||
"""Replace and find in the current selection""" | ||
def _word_replacer(match): | ||
"""Used to replace only match objects that are whole | ||
words when the user has selected whole word search""" | ||
unchanged = match.group(0) | ||
if match.start(0)>0 and re.search(r'\w', seltxt[match.start(0)-1], flags=word_flags): | ||
return unchanged | ||
if match.end(0)<len(seltxt) and re.search(r'\w', seltxt[match.end(0)+1], flags=word_flags): | ||
return unchanged | ||
if re.search(r'\W', unchanged, flags=word_flags): | ||
return unchanged | ||
return match.expand(replace_text) | ||
|
||
if (self.editor is not None): | ||
replace_text = to_text_string(self.replace_text.currentText()) | ||
search_text = to_text_string(self.search_text.currentText()) | ||
pattern = search_text if self.re_button.isChecked() else None | ||
case = self.case_button.isChecked() | ||
words = self.words_button.isChecked() | ||
re_flags = re.MULTILINE if case else re.IGNORECASE|re.MULTILINE | ||
|
||
cursor = self.editor.textCursor() | ||
cursor.beginEditBlock() | ||
seltxt = to_text_string(self.editor.get_selected_text()) | ||
if not pattern: | ||
pattern = re.escape(search_text) | ||
replace_text = re.escape(replace_text) | ||
if words: | ||
#If whole words is checked we need to check that each match | ||
#is actually a whole word before replacing | ||
word_flags = 0 if case else re.IGNORECASE | ||
replacement = re.sub(pattern, _word_replacer, seltxt, flags=re_flags) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why
|
||
else: | ||
replacement = re.sub(pattern, replace_text, seltxt, flags=re_flags) | ||
if replacement != seltxt: | ||
cursor.removeSelectedText() | ||
cursor.insertText(replacement) | ||
cursor.endEditBlock() | ||
if focus_replace_text: | ||
self.replace_text.setFocus() | ||
else: | ||
self.editor.setFocus() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe a comment for explaining this behavior is missing: "When selecting several lines, and replace box is activated the text won't be replaced for the selection"