Repository: spyder-ide/spyder

Issue:
Find in Files should have ignore case flag
Similar to the editor search dialog, Find in Files should have an option to ignore case when searching. Like the editor search dialog, the button should be located to the right of the regex toggle.

This would be more very useful and make the UI more consistent.



Discussion:
@andfoy, please work on this one.

Resolving patch:
diff --git a/spyder/config/main.py b/spyder/config/main.py
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -276,6 +276,7 @@
               'search_text_samples': [codeanalysis.TASKS_PATTERN],
               'in_python_path': False,
               'more_options': False,
+              'case_sensitive': True
               }),
             ('workingdir',
              {
diff --git a/spyder/plugins/findinfiles.py b/spyder/plugins/findinfiles.py
--- a/spyder/plugins/findinfiles.py
+++ b/spyder/plugins/findinfiles.py
@@ -52,11 +52,13 @@ def __init__(self, parent=None):
         exclude_regexp = self.get_option('exclude_regexp')
         in_python_path = self.get_option('in_python_path')
         more_options = self.get_option('more_options')
+        case_sensitive = self.get_option('case_sensitive')
         FindInFilesWidget.__init__(self, parent,
-                                   search_text, search_text_regexp, search_path,
-                                   exclude, exclude_idx, exclude_regexp,
-                                   supported_encodings,
-                                   in_python_path, more_options)
+                                   search_text, search_text_regexp,
+                                   search_path, exclude, exclude_idx,
+                                   exclude_regexp, supported_encodings,
+                                   in_python_path, more_options,
+                                   case_sensitive)
         SpyderPluginMixin.__init__(self, parent)
         
         # Initialize plugin
@@ -156,9 +158,9 @@ def closing_plugin(self, cancelable=False):
         self.closing_widget()  # stop search thread and clean-up
         options = self.find_options.get_options(all=True)
         if options is not None:
-            search_text, text_re, search_path, \
-            exclude, exclude_idx, exclude_re, \
-            in_python_path, more_options = options
+            (search_text, text_re, search_path,
+             exclude, exclude_idx, exclude_re,
+             in_python_path, more_options, case_sensitive) = options
             hist_limit = 15
             search_text = search_text[:hist_limit]
             search_path = search_path[:hist_limit]
@@ -171,6 +173,7 @@ def closing_plugin(self, cancelable=False):
             self.set_option('exclude_regexp', exclude_re)
             self.set_option('in_python_path', in_python_path)
             self.set_option('more_options', more_options)
+            self.set_option('case_sensitive', case_sensitive)
         return True
 
 
diff --git a/spyder/widgets/findinfiles.py b/spyder/widgets/findinfiles.py
--- a/spyder/widgets/findinfiles.py
+++ b/spyder/widgets/findinfiles.py
@@ -34,10 +34,10 @@
 from spyder.config.base import _
 from spyder.py3compat import getcwd, to_text_string
 from spyder.utils import icon_manager as ima
-from spyder.utils.qthelpers import create_toolbutton
 from spyder.utils.encoding import is_text_file
 from spyder.widgets.comboboxes import PatternComboBox
 from spyder.widgets.onecolumntree import OneColumnTree
+from spyder.utils.qthelpers import create_toolbutton, get_icon
 
 from spyder.config.gui import get_font
 from spyder.widgets.waitingspinner import QWaitingSpinner
@@ -70,12 +70,14 @@ def __init__(self, parent):
         self.texts = None
         self.text_re = None
         self.completed = None
+        self.case_sensitive = True
         self.get_pythonpath_callback = None
         self.results = {}
         self.total_matches = 0
         self.is_file = False
 
-    def initialize(self, path, is_file, exclude, texts, text_re):
+    def initialize(self, path, is_file, exclude,
+                   texts, text_re, case_sensitive):
         self.rootpath = path
         self.python_path = False
         self.hg_manifest = False
@@ -85,6 +87,7 @@ def initialize(self, path, is_file, exclude, texts, text_re):
         self.is_file = is_file
         self.stopped = False
         self.completed = False
+        self.case_sensitive = case_sensitive
 
     def run(self):
         try:
@@ -136,18 +139,23 @@ def find_string_in_file(self, fname):
         try:
             for lineno, line in enumerate(open(fname, 'rb')):
                 for text, enc in self.texts:
+                    line_search = line
+                    if not self.case_sensitive:
+                        line_search = line_search.lower()
                     if self.text_re:
-                        found = re.search(text, line)
+                        found = re.search(text, line_search)
                         if found is not None:
                             break
                     else:
-                        found = line.find(text)
+                        found = line_search.find(text)
                         if found > -1:
                             break
                 try:
                     line_dec = line.decode(enc)
                 except UnicodeDecodeError:
                     line_dec = line
+                if not self.case_sensitive:
+                    line = line.lower()
                 if self.text_re:
                     for match in re.finditer(text, line):
                         self.total_matches += 1
@@ -186,7 +194,8 @@ class FindOptions(QWidget):
 
     def __init__(self, parent, search_text, search_text_regexp, search_path,
                  exclude, exclude_idx, exclude_regexp,
-                 supported_encodings, in_python_path, more_options):
+                 supported_encodings, in_python_path, more_options,
+                 case_sensitive):
         QWidget.__init__(self, parent)
 
         if search_path is None:
@@ -212,6 +221,11 @@ def __init__(self, parent, search_text, search_text_regexp, search_path,
         self.edit_regexp = create_toolbutton(self,
                                              icon=ima.icon('advanced'),
                                              tip=_('Regular expression'))
+        self.case_button = create_toolbutton(self,
+                                             icon=get_icon("upper_lower.png"),
+                                             tip=_("Case Sensitive"))
+        self.case_button.setCheckable(True)
+        self.case_button.setChecked(case_sensitive)
         self.edit_regexp.setCheckable(True)
         self.edit_regexp.setChecked(search_text_regexp)
         self.more_widgets = ()
@@ -233,7 +247,7 @@ def __init__(self, parent, search_text, search_text_regexp, search_path,
                                              tip=_("Stop search"),
                                              text_beside_icon=True)
         self.stop_button.setEnabled(False)
-        for widget in [self.search_text, self.edit_regexp,
+        for widget in [self.search_text, self.edit_regexp, self.case_button,
                        self.ok_button, self.stop_button, self.more_options]:
             hlayout1.addWidget(widget)
 
@@ -340,8 +354,12 @@ def get_options(self, all=False):
         text_re = self.edit_regexp.isChecked()
         exclude = to_text_string(self.exclude_pattern.currentText())
         exclude_re = self.exclude_regexp.isChecked()
+        case_sensitive = self.case_button.isChecked()
         python_path = False
 
+        if not case_sensitive:
+            texts = [(text[0].lower(), text[1]) for text in texts]
+
         global_path_search = self.global_path_search.isChecked()
         project_search = self.project_search.isChecked()
         file_search = self.file_search.isChecked()
@@ -380,9 +398,9 @@ def get_options(self, all=False):
             more_options = self.more_options.isChecked()
             return (search_text, text_re, [],
                     exclude, exclude_idx, exclude_re,
-                    python_path, more_options)
+                    python_path, more_options, case_sensitive)
         else:
-            return (path, file_search, exclude, texts, text_re)
+            return (path, file_search, exclude, texts, text_re, case_sensitive)
 
     @Slot()
     def select_directory(self):
@@ -716,7 +734,8 @@ def __init__(self, parent,
                  exclude=r"\.pyc$|\.orig$|\.hg|\.svn", exclude_idx=None,
                  exclude_regexp=True,
                  supported_encodings=("utf-8", "iso-8859-1", "cp1252"),
-                 in_python_path=False, more_options=False):
+                 in_python_path=False, more_options=False,
+                 case_sensitive=True):
         QWidget.__init__(self, parent)
 
         self.setWindowTitle(_('Find in files'))
@@ -731,7 +750,7 @@ def __init__(self, parent,
                                         search_path,
                                         exclude, exclude_idx, exclude_regexp,
                                         supported_encodings, in_python_path,
-                                        more_options)
+                                        more_options, case_sensitive)
         self.find_options.find.connect(self.find)
         self.find_options.stop.connect(self.stop_and_reset_thread)
 


Test patch:
diff --git a/spyder/widgets/tests/data/ham.txt b/spyder/widgets/tests/data/ham.txt
new file mode 100644
--- /dev/null
+++ b/spyder/widgets/tests/data/ham.txt
@@ -0,0 +1,10 @@
+ham(N) -> ham(N, 1, 1).
+
+ham(0, F1, _) -> F1;
+ham(N, F1, F2) when N > 0 ->
+    ham(N - 1, F2, F2 + F1).
+
+Eggs = 10.
+Sausage = atom.
+HaM = false.
+Ham = Eggs + Sausage.
\ No newline at end of file
diff --git a/spyder/widgets/tests/test_findinfiles.py b/spyder/widgets/tests/test_findinfiles.py
--- a/spyder/widgets/tests/test_findinfiles.py
+++ b/spyder/widgets/tests/test_findinfiles.py
@@ -55,6 +55,13 @@ def expected_results():
     return results
 
 
+def expected_case_unsensitive_results():
+    results = {'spam.txt': [(1, 10)],
+               'ham.txt': [(1, 0), (1, 10), (3, 0), (4, 0),
+                           (5, 4), (9, 0), (10, 0)]}
+    return results
+
+
 def test_findinfiles(qtbot):
     """Run find in files widget."""
     find_in_files = setup_findinfiles(qtbot)
@@ -99,5 +106,29 @@ def test_exclude_extension(qtbot):
     assert files_filtered
 
 
+def test_case_unsensitive_search(qtbot):
+    find_in_files = setup_findinfiles(qtbot, case_sensitive=False)
+    find_in_files.set_search_text('ham')
+    find_in_files.find_options.set_directory(osp.join(LOCATION, "data"))
+    find_in_files.find()
+    blocker = qtbot.waitSignal(find_in_files.sig_finished)
+    blocker.wait()
+    matches = process_search_results(find_in_files.result_browser.data)
+    print(matches)
+    assert expected_case_unsensitive_results() == matches
+
+
+def test_case_sensitive_search(qtbot):
+    find_in_files = setup_findinfiles(qtbot)
+    find_in_files.set_search_text('HaM')
+    find_in_files.find_options.set_directory(osp.join(LOCATION, "data"))
+    find_in_files.find()
+    blocker = qtbot.waitSignal(find_in_files.sig_finished)
+    blocker.wait()
+    matches = process_search_results(find_in_files.result_browser.data)
+    print(matches)
+    assert matches == {'ham.txt': [(9, 0)]}
+
+
 if __name__ == "__main__":
     pytest.main()