Repository: spyder-ide/spyder

Issue:
Find/Replace widget: add option to find/replace in selection only
_From [stream.n...@gmx.de](https://code.google.com/u/104425159064108662638/) on 2010-09-13T03:06:25Z_

What steps will reproduce the problem?

 At the moment the Find-Replace dialog allows only to replace single tokens or all of them in the current document. In addition I would find it very useful to have a checkbox like "Replace in selection only" and "Replace All" means then that all of the occurrences in the selection only are replaced. What version of the product are you using? On what operating system? 2.0.0 Beta 3

_Original issue: http://code.google.com/p/spyderlib/issues/detail?id=358_



Discussion:
_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2010-09-25T02:10:27Z_

**Summary:** Find/Replace widget: add checkbox "replace in selection only"  
  **Status:** Accepted  
  **Labels:** -Type-Defect -Priority-Medium Type-Enhancement Priority-Low  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2011-05-16T01:11:21Z_

**Labels:** Cat-Editor  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2011-10-29T04:37:22Z_

issue #303 has been merged into this issue.

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2011-10-29T04:37:56Z_

**Summary:** Find/Replace widget: add option to find or replace in selection only  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2011-10-29T04:38:13Z_

**Labels:** Milestone-Release2.2  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2012-02-21T01:07:21Z_

issue #943 has been merged into this issue.

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2012-03-18T13:46:18Z_

**Labels:** -Milestone-Release2.2 Milestone-v2.2  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2012-03-18T13:56:22Z_

**Labels:** -Milestone-v2.2 MS-v2.2  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2012-03-18T14:15:12Z_

**Labels:** -Type-Enhancement Type-Enh  

_From [ccordoba12](https://code.google.com/u/ccordoba12/) on 2013-02-11T08:59:07Z_

**Labels:** -MS-v2.2 MS-v2.3  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2013-05-11T05:32:21Z_

**Labels:** -MS-v2.3 MS-v2.4  

_From [fsald...@gmail.com](https://code.google.com/u/104402591291906531590/) on 2013-09-07T07:48:01Z_

This would be a very nice feature to add. I see it has been requested in 2010. Any progress?

We consider this issue is very specific, not that useful and really hard to implement. For all those reasons, we decline to implement it.
> We consider this issue is very specific, not that useful and really hard to implement. For all those reasons, we decline to implement it.

I use this feature all of the time in other editors. Copy some chunk of code and then regex replace a bunch of stuff to refactor it into something else without affecting the rest of the document.

How hard could it be to get the current selection then replace one-by-one or replace all in that scope?
Then you're welcome to help us to implement it. Sorry, but we won't spend cycles on this one.
Not tested (at all) but...

https://github.com/spyder-ide/spyder/compare/master...spillz:patch-1
Great!! Please create a pull request for that so we can review it and give you feedback.
Now here: https://github.com/spillz/spyder/tree/spillz-replace-selection-patch

I can't test now so won't PR until I can.
Ok, but if you want this to be included in our next release, please try to hurry up a bit (we are one week left to release 3.2, I'd say).

Resolving patch:
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -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)
@@ -184,7 +197,7 @@ def create_shortcuts(self, parent):
                                    name='Find previous', parent=parent)
         togglefind = config_shortcut(self.show, context='_',
                                      name='Find text', parent=parent)
-        togglereplace = config_shortcut(self.toggle_replace_widgets,
+        togglereplace = config_shortcut(self.show_replace,
                                         context='_', name='Replace text',
                                         parent=parent)
         hide = config_shortcut(self.hide, context='_', name='hide find and replace',
@@ -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,7 +334,8 @@ 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
@@ -322,15 +343,16 @@ def find_next(self):
     @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,52 @@ 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):
+        """Replace and find in the current selection"""
+        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
+                try:
+                    re.compile(pattern)
+                except re.error:
+                    return #if the pattern won't compile cancel the find/replace
+                word_pattern = r'\b{pattern}\b'.format(pattern = pattern)
+                replacement = re.sub(word_pattern, replace_text, seltxt, flags=re_flags)
+            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()


Test patch: