Repository: spyder-ide/spyder

Issue:
Find and replace "Replace all occurences" ignores "Only search for whole words"
<!--- **PLEASE READ:** When submitting here, please ensure you've completed the following checklist and checked the boxes to confirm. Issue reports without it may be closed. Thanks! --->

### Issue Report Checklist

* [x] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [x] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [x] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [ ] Tried basic troubleshooting (if a bug/error)
    * [x] Restarted Spyder
    * [ ] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [ ] Tried the other applicable steps from the Troubleshooting Guide
* [ ] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
find and replace ignores "Only search for whole words" when doing "Replace all occurences". Only the correct occurences are highlighted and if I replace them one by one then all is fine. The problem is just with "Replace all occurences".


### What steps reproduce the problem?

1. Write in a new file
```
aaa
aaa2
```
2. Open find and replace. Write `aaa` in the find line. Write `aaa3` in the replace line.
3. Check "Only search for whole words". Only `aaa` is highlighted.
4. Press "Replace all occurences".

### What is the expected output? What do you see instead?
Expected:
```
aaa3
aaa2
```
I get
```
aaa3
aaa32
```

## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 5.4.1 (although I'm quite sure it was 5.3.x before updating as requested in the issue guidelines)
* Python version: 3.10.8 64-bit
* Qt version: 5.15.6
* PyQt version: 5.15.7
* Operating System name/version: Linux 5.4.0-137-generic 

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
# Mandatory:
atomicwrites >=1.2.0          :  1.4.1 (OK)
chardet >=2.0.0               :  5.1.0 (OK)
cloudpickle >=0.5.0           :  2.2.0 (OK)
cookiecutter >=1.6.0          :  2.1.1 (OK)
diff_match_patch >=20181111   :  20200713 (OK)
intervaltree >=3.0.2          :  3.0.2 (OK)
IPython >=7.31.1;<9.0.0       :  8.8.0 (OK)
jedi >=0.17.2;<0.19.0         :  0.18.2 (OK)
jellyfish >=0.7               :  0.9.0 (OK)
jsonschema >=3.2.0            :  4.17.3 (OK)
keyring >=17.0.0              :  23.13.1 (OK)
nbconvert >=4.0               :  7.2.8 (OK)
numpydoc >=0.6.0              :  1.5.0 (OK)
parso >=0.7.0;<0.9.0          :  0.8.3 (OK)
pexpect >=4.4.0               :  4.8.0 (OK)
pickleshare >=0.4             :  0.7.5 (OK)
psutil >=5.3                  :  5.9.4 (OK)
pygments >=2.0                :  2.14.0 (OK)
pylint >=2.5.0;<3.0           :  2.15.10 (OK)
pylint_venv >=2.1.1           :  2.3.0 (OK)
pyls_spyder >=0.4.0           :  0.4.0 (OK)
pylsp >=1.7.0;<1.8.0          :  1.7.1 (OK)
pylsp_black >=1.2.0           :  1.2.1 (OK)
qdarkstyle >=3.0.2;<3.1.0     :  3.0.3 (OK)
qstylizer >=0.2.2             :  0.2.2 (OK)
qtawesome >=1.2.1             :  1.2.2 (OK)
qtconsole >=5.4.0;<5.5.0      :  5.4.0 (OK)
qtpy >=2.1.0                  :  2.3.0 (OK)
rtree >=0.9.7                 :  1.0.1 (OK)
setuptools >=49.6.0           :  66.0.0 (OK)
sphinx >=0.6.6                :  6.1.3 (OK)
spyder_kernels >=2.4.1;<2.5.0 :  2.4.2 (OK)
textdistance >=4.2.0          :  4.5.0 (OK)
three_merge >=0.1.1           :  0.1.1 (OK)
watchdog >=0.10.3             :  2.2.1 (OK)
xdg >=0.26                    :  0.28 (OK)
zmq >=22.1.0                  :  25.0.0 (OK)

# Optional:
cython >=0.21                 :  None (NOK)
matplotlib >=3.0.0            :  3.6.2 (OK)
numpy >=1.7                   :  1.24.1 (OK)
pandas >=1.1.1                :  1.5.2 (OK)
scipy >=0.17.0                :  1.10.0 (OK)
sympy >=0.7.3                 :  None (NOK)```



Discussion:
Hey @yuvalwas, thanks for reporting and for the detailed instructions to reproduce this problem. I can confirm it and we'll try to fix it in a future release.

Resolving patch:
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -649,7 +649,6 @@ def replace_find(self, focus_replace_text=False):
                 cursor.setPosition(found_cursor.selectionEnd(),
                                    QTextCursor.KeepAnchor)
 
-
         if cursor is not None:
             cursor.endEditBlock()
 
@@ -666,24 +665,35 @@ def replace_find_all(self):
         """Replace and find all matching occurrences"""
         if self.editor is None:
             return
+
         replace_text = to_text_string(self.replace_text.currentText())
         search_text = to_text_string(self.search_text.currentText())
-        re_pattern = None
         case = self.case_button.isChecked()
+        word = self.words_button.isChecked()
         re_flags = re.MULTILINE if case else re.IGNORECASE | re.MULTILINE
-        re_enabled = self.re_button.isChecked()
-        # Check regexp before proceeding
-        if re_enabled:
-            try:
-                re_pattern = re.compile(search_text, flags=re_flags)
-                # Check if replace_text can be substituted in re_pattern
-                # Fixes spyder-ide/spyder#7177.
-                re_pattern.sub(replace_text, '')
-            except re.error:
-                # Do nothing with an invalid regexp
-                return
+
+        if self.re_button.isChecked():
+            pattern = search_text
         else:
-            re_pattern = re.compile(re.escape(search_text), flags=re_flags)
+            pattern = re.escape(search_text)
+            # re.sub processes backslashes so they must be escaped
+            replace_text = replace_text.replace('\\', r'\\')
+
+        # Match whole words only
+        if word:
+            pattern = r'\b{pattern}\b'.format(pattern=pattern)
+
+        # Check regexp before proceeding
+        re_pattern = None
+        try:
+            re_pattern = re.compile(pattern, flags=re_flags)
+
+            # Check if replace_text can be substituted in re_pattern
+            # Fixes spyder-ide/spyder#7177.
+            re_pattern.sub(replace_text, '')
+        except re.error:
+            # Do nothing with an invalid regexp
+            return
 
         cursor = self.editor._select_text("sof", "eof")
         text = self.editor.toPlainText()
@@ -709,6 +719,7 @@ def replace_find_selection(self, focus_replace_text=False):
                 pattern = search_text
             else:
                 pattern = re.escape(search_text)
+                # re.sub processes backslashes so they must be escaped
                 replace_text = replace_text.replace('\\', r'\\')
             if word:  # match whole words only
                 pattern = r'\b{pattern}\b'.format(pattern=pattern)


Test patch:
diff --git a/spyder/widgets/tests/test_findreplace.py b/spyder/widgets/tests/test_findreplace.py
--- a/spyder/widgets/tests/test_findreplace.py
+++ b/spyder/widgets/tests/test_findreplace.py
@@ -100,6 +100,51 @@ def test_replace_selection(findreplace_editor, qtbot):
     assert len(editor.get_selected_text()) == len(expected)
 
 
+def test_replace_all(findreplace_editor, qtbot):
+    """
+    Test find replace final selection in the editor.
+
+    Regression test for spyder-ide/spyder#20403
+    """
+    editor = findreplace_editor.editor
+    findreplace = findreplace_editor.findreplace
+    findreplace.show_replace()
+
+    findreplace.search_text.setCurrentText('a')
+    findreplace.replace_text.setCurrentText('x')
+
+    # Replace all
+    editor.set_text('a\naa')
+    expected = 'x\nxx'
+    qtbot.wait(500)
+    findreplace.replace_find_all()
+    qtbot.wait(500)
+    assert editor.toPlainText() == expected
+
+    # Replace all with whole word matching
+    editor.set_text('a\naa')
+    expected = 'x\naa'
+    qtbot.wait(500)
+    qtbot.mouseClick(findreplace.words_button, Qt.LeftButton)
+    findreplace.replace_find_all()
+    qtbot.wait(500)
+    qtbot.mouseClick(findreplace.words_button, Qt.LeftButton)
+    assert editor.toPlainText() == expected
+
+    findreplace.search_text.setCurrentText(r'a(\d+)a')
+    findreplace.replace_text.setCurrentText(r'b\1b')
+
+    # Replace all with regex
+    editor.set_text('a123a\nabca')
+    expected = 'b123b\nabca'
+    qtbot.wait(500)
+    qtbot.mouseClick(findreplace.re_button, Qt.LeftButton)
+    findreplace.replace_find_all()
+    qtbot.wait(500)
+    qtbot.mouseClick(findreplace.re_button, Qt.LeftButton)
+    assert editor.toPlainText() == expected
+
+
 def test_messages_action(findreplace_editor, qtbot):
     """
     Test that we set the right icons and tooltips on messages_action.