Repository: spyder-ide/spyder

Issue:
Ctrl+F should show the number of appearances in the file as #current/#total
For example:

a e i o u **a** e a e i  

Ctrl+F -> "a" 2/3


Discussion:
I don't follow exactly what you mean. Could you post a screenshot to see how this looks in other editors/IDEs?
@ccordoba12 Yeah, absolutely, something like the red circle:

![image](https://cloud.githubusercontent.com/assets/3608776/22127516/28a00650-de6a-11e6-9f1f-af87f8dadb9f.png)

Do you want to work on this?
Yes, I'm assigning it to myself now

Resolving patch:
diff --git a/spyder/widgets/browser.py b/spyder/widgets/browser.py
--- a/spyder/widgets/browser.py
+++ b/spyder/widgets/browser.py
@@ -7,6 +7,8 @@
 """Simple web browser widget"""
 
 # Standard library imports
+import re
+import sre_constants
 import sys
 
 # Third party imports
@@ -61,6 +63,7 @@ def __init__(self, parent):
         if WEBENGINE:
             web_page = WebPage(self)
             self.setPage(web_page)
+            self.source_text = ''
 
     def find_text(self, text, changed=True,
                   forward=True, case=False, words=False,
@@ -81,7 +84,37 @@ def find_text(self, text, changed=True,
     def get_selected_text(self):
         """Return text selected by current text cursor"""
         return self.selectedText()
-        
+
+    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):
+        """Get the number of matches for the searched text."""
+        pattern = to_text_string(pattern)
+        if not pattern:
+            return 0
+        if not source_text:
+            if WEBENGINE:
+                self.page().toPlainText(self.set_source_text)
+                source_text = to_text_string(self.source_text)
+            else:
+                source_text = to_text_string(
+                        self.page().mainFrame().toPlainText())
+        try:
+            if case:
+                regobj = re.compile(pattern)
+            else:
+                regobj = re.compile(pattern, re.IGNORECASE)
+        except sre_constants.error:
+            return
+
+        number_matches = 0
+        for match in regobj.finditer(source_text):
+            number_matches += 1
+
+        return number_matches
+
     def set_font(self, font, fixed_font=None):
         font = QFontInfo(font)
         settings = self.page().settings()
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -17,7 +17,7 @@
 # Third party imports
 from qtpy.QtCore import Qt, QTimer, Signal, Slot, QEvent
 from qtpy.QtGui import QTextCursor
-from qtpy.QtWidgets import (QCheckBox, QGridLayout, QHBoxLayout, QLabel,
+from qtpy.QtWidgets import (QGridLayout, QHBoxLayout, QLabel,
                             QSizePolicy, QWidget)
 
 # Local imports
@@ -77,7 +77,8 @@ def __init__(self, parent, enable_replace=False):
 
         self.search_text.lineEdit().textEdited.connect(
                                                      self.text_has_been_edited)
-        
+
+        self.number_matches_text = QLabel(self)
         self.previous_button = create_toolbutton(self,
                                              triggered=self.find_previous,
                                              icon=ima.icon('ArrowUp'))
@@ -112,9 +113,9 @@ def __init__(self, parent, enable_replace=False):
 
         hlayout = QHBoxLayout()
         self.widgets = [self.close_button, self.search_text,
-                        self.previous_button, self.next_button,
-                        self.re_button, self.case_button, self.words_button,
-                        self.highlight_button]
+                        self.number_matches_text, self.previous_button,
+                        self.next_button, self.re_button, self.case_button,
+                        self.words_button, self.highlight_button]
         for widget in self.widgets[1:]:
             hlayout.addWidget(widget)
         glayout.addLayout(hlayout, 0, 1)
@@ -244,6 +245,7 @@ def show(self, hide_replace=True):
         """Overrides Qt Method"""
         QWidget.show(self)
         self.visibility_changed.emit(True)
+        self.change_number_matches()
         if self.editor is not None:
             if hide_replace:
                 if self.replace_widgets[0].isVisible():
@@ -382,6 +384,7 @@ def find(self, changed=True, forward=True,
             if not self.is_code_editor:
                 # Clears the selection for WebEngine
                 self.editor.find_text('')
+            self.change_number_matches()
             return None
         else:
             case = self.case_button.isChecked()
@@ -399,6 +402,14 @@ def find(self, changed=True, forward=True,
                         self.highlight_matches()
             else:
                 self.clear_matches()
+
+            number_matches = self.editor.get_number_matches(text, case=case)
+            if hasattr(self.editor, 'get_match_number'):
+                match_number = self.editor.get_match_number(text, case=case)
+            else:
+                match_number = 0
+            self.change_number_matches(current_match=match_number,
+                                       total_matches=number_matches)
             return found
 
     @Slot()
@@ -521,3 +532,15 @@ def replace_find_selection(self, focus_replace_text=False):
                 self.replace_text.setFocus()
             else:
                 self.editor.setFocus()
+
+    def change_number_matches(self, current_match=0, total_matches=0):
+        """Change number of match and total matches."""
+        if current_match and total_matches:
+            matches_string = "{} {} {}".format(current_match, _("of"),
+                                               total_matches)
+            self.number_matches_text.setText(matches_string)
+        elif total_matches:
+            matches_string = "{} {}".format(total_matches, _("matches"))
+            self.number_matches_text.setText(matches_string)
+        else:
+            self.number_matches_text.setText(_("no matches"))
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -523,6 +523,36 @@ 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):
+        """Get the number of matches for the searched text."""
+        pattern = to_text_string(pattern)
+        if not pattern:
+            return 0
+        if not source_text:
+            source_text = to_text_string(self.toPlainText())
+        try:
+            if case:
+                regobj = re.compile(pattern)
+            else:
+                regobj = re.compile(pattern, re.IGNORECASE)
+        except sre_constants.error:
+            return
+
+        number_matches = 0
+        for match in regobj.finditer(source_text):
+            number_matches += 1
+
+        return number_matches
+
+    def get_match_number(self, pattern, case=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)
+        return match_number
+
     # --- Numpy matrix/array helper / See 'spyder/widgets/arraybuilder.py'
     def enter_array_inline(self):
         """ """


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
@@ -81,8 +81,31 @@ def editor_cells_bot(base_editor_bot):
     qtbot.addWidget(editor_stack)
     return editor_stack, finfo.editor, qtbot
 
+
 # Tests
 #-------------------------------
+def test_find_number_matches(qtbot):
+    """Test for number matches in find/replace."""
+    editor_stack, editor = setup_editor(qtbot)
+    editor_stack.find_widget.case_button.setChecked(True)
+    text = ' test \nTEST \nTest \ntesT '
+    editor.set_text(text)
+
+    editor_stack.find_widget.search_text.add_text('test')
+    editor_stack.find_widget.find(changed=False, forward=True,
+                                  rehighlight=False,
+                                  multiline_replace_check=False)
+    editor_text = editor_stack.find_widget.number_matches_text.text()
+    assert editor_text == '1 of 1'
+
+    editor_stack.find_widget.search_text.add_text('fail')
+    editor_stack.find_widget.find(changed=False, forward=True,
+                                  rehighlight=False,
+                                  multiline_replace_check=False)
+    editor_text = editor_stack.find_widget.number_matches_text.text()
+    assert editor_text == 'no matches'
+
+
 def test_move_current_line_up(editor_bot):
     editor_stack, editor, qtbot = editor_bot