Repository: spyder-ide/spyder

Issue:
"Replace Selection" Erroneously Removing/Interpreting Escape Sequences
## Description of your problem

When performing a find and replace using the basic Ctrl-R in Spyder, the "Replace Selection" option will remove and parse escape sequences present in the selected portion of the file regardless of the actual find query, silently deleting line continuation "\"s and rendering "\n"s, "\r"s etc. as newlines, etc, and likely similar for others. This almost invariably breaks the code and requires careful diagnosis and manual inspection to fix, and there appears to be no obvious workaround other than using "Replace all" or going through result by result, which does not trigger the issue.

I've tested this across multiple machines across various versions of Python and Spyder since installing Anaconda between 1-2 months ago.

**What steps will reproduce the problem?**
Minimal example code:
`test_string = "foo"`
`long_string = "foo" + "\n" + "bar"`
`cap_string = long_string\`
`               .capitalize()`
`print(cap_string)`

1. Select above code
2. Enter any string that matches something in above, e.g. "string", and any replacement e.g. "str"
3. Click "Replace Selection" with default settings

**What is the expected output? What do you see instead?**
Expected:
`test_str= "foo"`
`long_str= "foo" + "\n" + "bar"`
`cap_str= long_str\`
`               .capitalize()`
`print(cap_str)`

Actual:
`test_str= "foo"`
`long_str= "foo" + "`
`" + "bar"`
`cap_str= long_str`
`               .capitalize()`
`print(cap_str)`


## Versions and main components

* Spyder Version: 3.2.3
* Python Version: 3.6.1 – 3.6.3 x64
* Qt Version: 5.6.2
* PyQt Version: 5.6
* Operating system: Win8.1 Pro x64, Win10 Ent x64


## Dependencies

IPython >=4.0    :  5.3.0 (OK)
cython >=0.21    :  0.25.2 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
nbconvert >=4.0  :  5.1.1 (OK)
numpy >=1.7      :  1.12.1 (OK)
pandas >=0.13.1  :  0.20.1 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pyflakes >=0.6.0 :  1.5.0 (OK)
pygments >=2.0   :  2.2.0 (OK)
pylint >=0.25    :  1.7.2 (OK)
qtconsole >=4.2.0:  4.3.0 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
sphinx >=0.6.6   :  1.5.6 (OK)
sympy >=0.7.3    :  1.0 (OK)




Discussion:
Thanks for reporting. We'll fix it in one of our next releases.

@rlaverde, please take a look at this one.
Thanks very much. On a related note, I've been consistently bothered with the find and replace dialog that there appears to be no evident way to move between the textbox where one enters the string to search for, and the one to replace it with, i.e. TAB doesn't work as it should (and does everywhere else); again on multiple Windows machines with the above traits. My only recourse is to manually click around with the mouse, and googling and the keyboard shortcuts dialog hasn't revealed any obvious solutions. Is this in fact a bug (the field trapping TAB?), or am I missing something? If the former, I guess I'll open a new issue? Thanks.

Resolving patch:
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -29,14 +29,6 @@
 from spyder.widgets.comboboxes import PatternComboBox
 
 
-CONTROL_CHARACTERS = {
-    '\\n': '\n',
-    '\\r': '\r',
-    '\\t': '\t',
-    '\\f': '\f'
-}
-
-
 def is_position_sup(pos1, pos2):
     """Return True is pos1 > pos2"""
     return pos1 > pos2
@@ -565,10 +557,8 @@ def replace_find_selection(self, focus_replace_text=False):
                 cursor = self.editor.textCursor()
                 cursor.beginEditBlock()
                 cursor.removeSelectedText()
-                for plain_char in CONTROL_CHARACTERS:
-                    replacement = replacement.replace(
-                        plain_char, CONTROL_CHARACTERS[plain_char])
-                replacement = re.sub(r'\\(.)', r'\1', replacement)
+                if not self.re_button.isChecked():
+                    replacement = re.sub(r'\\(?![nrtf])(.)', r'\1', replacement)
                 cursor.insertText(replacement)
                 cursor.endEditBlock()
             if focus_replace_text:


Test patch:
diff --git a/spyder/widgets/tests/test_editor.py b/spyder/widgets/tests/test_editor.py
--- a/spyder/widgets/tests/test_editor.py
+++ b/spyder/widgets/tests/test_editor.py
@@ -320,19 +320,17 @@ def test_replace_enter_press(editor_find_replace_bot):
     qtbot.keyPress(finder.search_text, Qt.Key_Return, modifier=Qt.ShiftModifier)
     assert editor.get_cursor_line_column() == (3,4)
 
-def test_selection_replace_plain_regex(editor_find_replace_bot):
+def test_replace_plain_regex(editor_find_replace_bot):
     """Test that regex reserved characters are displayed as plain text."""
     editor_stack, editor, finder, qtbot = editor_find_replace_bot
     expected_new_text = ('.\\[()]*test bacon\n'
                          'spam sausage\n'
                          'spam egg')
-    old_text = editor.toPlainText()
     finder.show()
     finder.show_replace()
     qtbot.keyClicks(finder.search_text, 'spam')
     qtbot.keyClicks(finder.replace_text, '.\[()]*test')
     qtbot.keyPress(finder.replace_text, Qt.Key_Return)
-    text = editor.toPlainText()[0:-1]
     assert editor.toPlainText()[0:-1] == expected_new_text
 
 def test_replace_invalid_regex(editor_find_replace_bot):
@@ -352,6 +350,30 @@ def test_replace_invalid_regex(editor_find_replace_bot):
     qtbot.mouseClick(finder.replace_all_button, Qt.LeftButton)
     assert editor.toPlainText() == old_text
 
+
+def test_selection_escape_characters(editor_find_replace_bot):
+    editor_stack, editor, finder, qtbot = editor_find_replace_bot
+    expected_new_text = ('spam bacon\n'
+                         'spam sausage\n'
+                         'spam egg\n'
+                         '\\n \\t some escape characters')
+    qtbot.keyClicks(editor, '\\n \\t escape characters')
+
+    finder.show()
+    finder.show_replace()
+    qtbot.keyClicks(finder.search_text, 'escape')
+    qtbot.keyClicks(finder.replace_text, 'some escape')
+
+    # Select last line
+    cursor = editor.textCursor()
+    cursor.select(QTextCursor.LineUnderCursor)
+    assert cursor.selection().toPlainText() == "\\n \\t escape characters"
+
+    #replace
+    finder.replace_find_selection()
+    assert editor.toPlainText() == expected_new_text
+
+
 def test_advance_cell(editor_cells_bot):
     editor_stack, editor, qtbot = editor_cells_bot