Repository: spyder-ide/spyder

Issue:
Selected text unselected after "Replace selection"
<!--- **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] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)

## Problem Description
When selecting a text and doing "Replace selection", the selection is gone. This is a bit inconvenient when wanting to make several replacements within the same selection as you need to select the text again.

After checking the code, the "Replace selection", removes the old text, hence, the selections are gone missing, and then inserts the new text. Hence, to solve this, one would need to perform a new selection in the code (or perform the replacement in a different way).
https://github.com/spyder-ide/spyder/blob/1c795a749b73ab39184fc7f54188502c967526ab/spyder/widgets/findreplace.py#L573-L579
This holds for all versions I've tested 3.3.2, 3.3.4, and the latest master. (And since the behaviour is expected from the code, I'm pretty certain it is valid.)

(I've also noted some issues with not being able to do "Replace all" when there is some text selected, but that I just noticed so I cannot really say how consistent that issue is.)

### What steps reproduce the problem?

1. Select a block of text
2. Perform "Replace selection"

### What is the expected output? What do you see instead?
The resulting text block is still selected.




Discussion:
Could you open a pull request to try to fix this? Thanks!
I'll see what I can do. Should it go to both 3.x and master? It seems like PRs aimed at 3.x are later merged into master, so maybe it is better to try to add this to 3.x? (I am not updated on the time plan, but it seems like there will be at least one more 3.x release considering you are still merging things there.)
> Should it go to both 3.x and master?

Master because the 3.x branch is in maintenance mode and it's only receiving critical or very simple bug fixes.

Resolving patch:
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -571,12 +571,20 @@ def replace_find_selection(self, focus_replace_text=False):
             replacement = re_pattern.sub(replace_text, selected_text)
             if replacement != selected_text:
                 cursor = self.editor.textCursor()
+                start_pos = cursor.selectionStart()
                 cursor.beginEditBlock()
                 cursor.removeSelectedText()
                 if not self.re_button.isChecked():
                     replacement = re.sub(r'\\(?![nrtf])(.)', r'\1', replacement)
                 cursor.insertText(replacement)
+                # Restore selection
+                self.editor.set_cursor_position(start_pos)
+                newl_cnt = replacement.count(self.editor.get_line_separator())
+                sel_len = len(replacement) - newl_cnt
+                for c in range(sel_len):
+                    self.editor.extend_selection_to_next('character', 'right')
                 cursor.endEditBlock()
+
             if focus_replace_text:
                 self.replace_text.setFocus()
             else:


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_editor.py b/spyder/plugins/editor/widgets/tests/test_editor.py
--- a/spyder/plugins/editor/widgets/tests/test_editor.py
+++ b/spyder/plugins/editor/widgets/tests/test_editor.py
@@ -419,8 +419,10 @@ def test_selection_escape_characters(editor_find_replace_bot, qtbot):
     cursor.select(QTextCursor.LineUnderCursor)
     assert cursor.selection().toPlainText() == "\\n \\t escape characters"
 
-    #replace
+    # Replace
     finder.replace_find_selection()
+    # Test that selection is correct
+    assert cursor.selection().toPlainText() == "\\n \\t some escape characters"
     assert editor.toPlainText() == expected_new_text