Repository: spyder-ide/spyder

Issue:
Bug: Find and Replace indicate "no matches" when "[", "(", or ")" included in the search string
## Description

When using both the "Find" and the "Replace" feature, I noticed that whenever my search string included a "[", "(" or ")" (but not a "]", "{", or "}"), the UI will claim "no matches" even though it can still actually find them. I've tried this in large (1,000 line) files with hundreds of these characters, and in no circumstances will it display any matches, though it will still find them (making this less critical). 

**What steps will reproduce the problem?**

1. Ensure focus is on a file with these characters (minimal example:)
```
{foo}
{bar}
[foo]
[bar]
(foo)
(bar)
```
2. Bring up Find or Replace dialogs
3. Search for, e.g. "[", "[foo]", "(bar)"; observe match count

**What is the expected output? What do you see instead?**
For e.g. "[" on the above, expected: 1 of 2 , actual: no matches;
For "(foo)", expected: 1 of 1, actual: no matches.
Etc. for any string containing "(", "[", and/or ")" regardless of what other characters it contains.


**Please provide any additional information below**

Reproduced on multiple spyder installs, machines and OS platforms.


## Version and main components

* Spyder Version: 3.2.4 
* Python Version: 3.6.3
* Qt Versions: 5.6.2, PyQt5 5.6 on Windows 8.1 Pro x64 and Win 10 Ent x64

## Dependencies
```
pyflakes >=0.6.0 :  1.6.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.20.3 (OK)
numpy >=1.7      :  1.13.3 (OK)
sphinx >=0.6.6   :  1.6.3 (OK)
rope >=0.9.4     :  0.10.5 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
nbconvert >=4.0  :  5.3.1 (OK)
sympy >=0.7.3    :  1.1.1 (OK)
cython >=0.21    :  0.26.1 (OK)
qtconsole >=4.2.0:  4.3.1 (OK)
IPython >=4.0    :  6.1.0 (OK)
pylint >=0.25    :  1.7.4 (OK)

```



Discussion:
Thanks for reporting. We'll take a a look at it in a future release.

Resolving patch:
diff --git a/spyder/widgets/browser.py b/spyder/widgets/browser.py
--- a/spyder/widgets/browser.py
+++ b/spyder/widgets/browser.py
@@ -90,11 +90,14 @@ def set_source_text(self, source_text):
         """Set source text of the page. Callback for QWebEngineView."""
         self.source_text = source_text
 
-    def get_number_matches(self, pattern, source_text='', case=False):
+    def get_number_matches(self, pattern, source_text='', case=False,
+                           regexp=False):
         """Get the number of matches for the searched text."""
         pattern = to_text_string(pattern)
         if not pattern:
             return 0
+        if not regexp:
+            pattern = re.escape(pattern)
         if not source_text:
             if WEBENGINE:
                 self.page().toPlainText(self.set_source_text)
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -435,9 +435,11 @@ def regexp_error_msg(pattern):
             else:
                 self.clear_matches()
 
-            number_matches = self.editor.get_number_matches(text, case=case)
+            number_matches = self.editor.get_number_matches(text, case=case,
+                                                            regexp=regexp)
             if hasattr(self.editor, 'get_match_number'):
-                match_number = self.editor.get_match_number(text, case=case)
+                match_number = self.editor.get_match_number(text, case=case,
+                                                            regexp=regexp)
             else:
                 match_number = 0
             self.change_number_matches(current_match=match_number,
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -523,11 +523,14 @@ def is_editor(self):
         """Needs to be overloaded in the codeeditor where it will be True"""
         return False
 
-    def get_number_matches(self, pattern, source_text='', case=False):
+    def get_number_matches(self, pattern, source_text='', case=False,
+                           regexp=False):
         """Get the number of matches for the searched text."""
         pattern = to_text_string(pattern)
         if not pattern:
             return 0
+        if not regexp:
+            pattern = re.escape(pattern)
         if not source_text:
             source_text = to_text_string(self.toPlainText())
         try:
@@ -536,7 +539,7 @@ def get_number_matches(self, pattern, source_text='', case=False):
             else:
                 regobj = re.compile(pattern, re.IGNORECASE)
         except sre_constants.error:
-            return
+            return None
 
         number_matches = 0
         for match in regobj.finditer(source_text):
@@ -544,13 +547,13 @@ def get_number_matches(self, pattern, source_text='', case=False):
 
         return number_matches
 
-    def get_match_number(self, pattern, case=False):
+    def get_match_number(self, pattern, case=False, regexp=False):
         """Get number of the match for the searched text."""
         position = self.textCursor().position()
         source_text = self.get_text(position_from='sof', position_to=position)
         match_number = self.get_number_matches(pattern,
                                                source_text=source_text,
-                                               case=case)
+                                               case=case, regexp=regexp)
         return match_number
 
     # --- Numpy matrix/array helper / See 'spyder/widgets/arraybuilder.py'


Test patch:
diff --git a/spyder/widgets/tests/test_mixins.py b/spyder/widgets/tests/test_mixins.py
new file mode 100644
--- /dev/null
+++ b/spyder/widgets/tests/test_mixins.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright © Spyder Project Contributors
+# Licensed under the terms of the MIT License
+#
+
+# Third party imports
+from qtpy.QtWidgets import QPlainTextEdit
+
+import pytest
+
+# Local imports
+from spyder.widgets import mixins
+
+
+class BaseWidget(QPlainTextEdit, mixins.BaseEditMixin):
+    pass
+
+# --- Fixtures
+# -----------------------------------------------------------------------------
+
+
+@pytest.fixture
+def mixinsbot(qtbot):
+    widget = BaseWidget()
+    qtbot.addWidget(widget)
+    widget.show()
+    return qtbot, widget
+
+# --- Tests
+# -----------------------------------------------------------------------------
+
+
+def test_get_number_matches(mixinsbot):
+    # Test get_number_matches().
+    qtbot, widget = mixinsbot
+    get = widget.get_number_matches
+
+    code = ('class C():\n'
+            '    def __init__(self):\n'
+            '        pass\n'
+            '    def f(self, a, b):\n'
+            '        pass\n')
+
+    # Empty pattern.
+    assert get('') == 0
+
+    # No case, no regexp.
+    assert get('self', source_text=code) == 2
+    assert get('c', source_text=code) == 2
+
+    # Case, no regexp.
+    assert get('self', source_text=code, case=True) == 2
+    assert get('c', source_text=code, case=True) == 1
+
+    # No case, regexp.
+    assert get('e[a-z]?f', source_text=code, regexp=True) == 4
+    assert get('e[A-Z]?f', source_text=code, regexp=True) == 4
+
+    # Case, regexp.
+    assert get('e[a-z]?f', source_text=code, case=True, regexp=True) == 4
+    assert get('e[A-Z]?f', source_text=code, case=True, regexp=True) == 2
+
+    # Issue 5680.
+    assert get('(', source_text=code) == 3
+    assert get('(', source_text=code, case=True) == 3
+    assert get('(', source_text=code, regexp=True) is None
+    assert get('(', source_text=code, case=True, regexp=True) is None
+
+
+def test_get_match_number(mixinsbot):
+    # Test get_match_number().
+    qtbot, widget = mixinsbot
+    get = widget.get_match_number
+
+    code = ('class C():\n'
+            '    def __init__(self):\n'
+            '        pass\n'
+            '    def f(self, a, b):\n'
+            '        pass\n')
+    widget.setPlainText(code)
+    cursor = widget.textCursor()
+    cursor.setPosition(widget.get_position('sof'))
+
+    # Empty pattern.
+    assert get('') == 0
+
+    # Issue 5680.
+    widget.find_text('(')
+    assert get('(') == 1
+
+    # First occurrence.
+    widget.find_text('self')
+    assert get('self') == 1
+    assert get('self', case=True) == 1
+
+    # Second occurrence.
+    widget.find_text('pass')
+    widget.find_text('self')
+    assert get('self') == 2
+    assert get('self', case=True) == 2