Repository: spyder-ide/spyder

Issue:
Find in files plugin does not forward results to editor properly
<!--- **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

* [ ] Searched the [issues page](https://github.com/spyder-ide/spyder/issues?q=is%3Aissue) for similar reports
* [ ] Read the relevant sections of the [Spyder Troubleshooting Guide](https://github.com/spyder-ide/spyder/wiki/Troubleshooting-Guide-and-FAQ) and followed its advice
* [ ] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [ ] Could not reproduce inside ``jupyter qtconsole`` (if console-related)
* [ ] Tried basic troubleshooting (if a bug/error)
    * [ ] Restarted Spyder
    * [ ] Reset preferences with ``spyder --reset``
    * [ ] Reinstalled the latest version of [Anaconda](https://www.anaconda.com/download/)
    * [ ] Tried the other applicable steps from the Troubleshooting Guide
* [x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
When clicking on a result in the find in files plugin, the editor doesn't always goes to the right result.


### What steps reproduce the problem?

1. Create a new file in an empty folder
2. Paste: 
```
we Weee wee
We
wee
```
3. Save the file, then set the current working directory to the partent folder
4. Go to the Find plugin
5. Search for `we+` with regexp enabled and no case sensitivity
6. Click on one of the 5 results
7. Only the first one is properly selected
 
3. 

### What is the expected output? What do you see instead?
I would expect to go to the bold text, instead it sometimes goes there, sometimes it just go to the line, sometimes it goes to the first result, event if there is multiple results in the line.




## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->
Spyder 4.3.0.dev0
Python 3.7.6 64-bit | Qt 5.12.8 | PyQt5 5.12.3 | Windows 10 

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
PASTE DEPENDENCIES HERE
```



Discussion:

Resolving patch:
diff --git a/spyder/plugins/editor/plugin.py b/spyder/plugins/editor/plugin.py
--- a/spyder/plugins/editor/plugin.py
+++ b/spyder/plugins/editor/plugin.py
@@ -1903,7 +1903,7 @@ def change_max_recent_files(self):
     @Slot(str, int, str, object)
     def load(self, filenames=None, goto=None, word='',
              editorwindow=None, processevents=True, start_column=None,
-             set_focus=True, add_where='end'):
+             end_column=None, set_focus=True, add_where='end'):
         """
         Load a text file
         editorwindow: load in this editorwindow (useful when clicking on
@@ -1911,6 +1911,10 @@ def load(self, filenames=None, goto=None, word='',
         processevents: determines if processEvents() should be called at the
         end of this method (set to False to prevent keyboard events from
         creeping through to the editor during debugging)
+        If goto is not none it represent a line to go to. start_column is
+        the start position in this line and end_column the length
+        (So that the end position is start_column + end_column)
+        Alternatively, the first match of word is used as a position.
         """
         # Switch to editor before trying to load a file
         try:
@@ -2048,7 +2052,8 @@ def _convert(fname):
                 self.__add_recent_file(filename)
             if goto is not None: # 'word' is assumed to be None as well
                 current_editor.go_to_line(goto[index], word=word,
-                                          start_column=start_column)
+                                          start_column=start_column,
+                                          end_column=end_column)
             current_editor.clearFocus()
             current_editor.setFocus()
             current_editor.window().raise_()
diff --git a/spyder/plugins/findinfiles/plugin.py b/spyder/plugins/findinfiles/plugin.py
--- a/spyder/plugins/findinfiles/plugin.py
+++ b/spyder/plugins/findinfiles/plugin.py
@@ -174,7 +174,8 @@ def register_plugin(self):
         """Register plugin in Spyder's main window"""
         self.add_dockwidget()
         self.findinfiles.result_browser.sig_edit_goto.connect(
-                                                         self.main.editor.load)
+            lambda filename, lineno, colno, colend: self.main.editor.load(
+                filename, lineno, start_column=colno, end_column=colend))
         self.findinfiles.find_options.redirect_stdio.connect(
                                         self.main.redirect_internalshell_stdio)
         self.main.workingdirectory.refresh_findinfiles.connect(self.refreshdir)
diff --git a/spyder/plugins/findinfiles/widgets.py b/spyder/plugins/findinfiles/widgets.py
--- a/spyder/plugins/findinfiles/widgets.py
+++ b/spyder/plugins/findinfiles/widgets.py
@@ -206,10 +206,18 @@ def find_string_in_file(self, fname):
                             if self.stopped:
                                 return False
                         self.total_matches += 1
+                        bstart, bend = match.start(), match.end()
+                        try:
+                            # Go from binary position to utf8 position
+                            start = len(line[:bstart].decode(enc))
+                            end = start + len(line[bstart:bend].decode(enc))
+                        except UnicodeDecodeError:
+                            start = bstart
+                            end = bend
                         self.partial_results.append((osp.abspath(fname),
                                                      lineno + 1,
-                                                     match.start(),
-                                                     match.end(),
+                                                     start,
+                                                     end,
                                                      line_dec))
                         if len(self.partial_results) > (2**self.power):
                             self.process_results()
@@ -222,10 +230,18 @@ def find_string_in_file(self, fname):
                             if self.stopped:
                                 return False
                         self.total_matches += 1
+                        try:
+                            # Go from binary position to utf8 position
+                            start = len(line[:found].decode(enc))
+                            end = start + len(text.decode(enc))
+                        except UnicodeDecodeError:
+                            start = found
+                            end = found + len(text)
+
                         self.partial_results.append((osp.abspath(fname),
                                                      lineno + 1,
-                                                     found,
-                                                     found + len(text),
+                                                     start,
+                                                     end,
                                                      line_dec))
                         if len(self.partial_results) > (2**self.power):
                             self.process_results()
@@ -266,7 +282,7 @@ def process_results(self):
                 self.num_files += 1
 
             line = self.truncate_result(line, colno, match_end)
-            item = (filename, lineno, colno, line)
+            item = (filename, lineno, colno, line, match_end)
             items.append(item)
 
         # Process title
@@ -941,7 +957,7 @@ def sizeHint(self, option, index):
 
 
 class ResultsBrowser(OneColumnTree):
-    sig_edit_goto = Signal(str, int, str)
+    sig_edit_goto = Signal(str, int, int, int)
     sig_max_results_reached = Signal()
 
     def __init__(self, parent, text_color=None, max_results=1000):
@@ -978,8 +994,8 @@ def activated(self, item):
         """Double-click event."""
         itemdata = self.data.get(id(self.currentItem()))
         if itemdata is not None:
-            filename, lineno, colno = itemdata
-            self.sig_edit_goto.emit(filename, lineno, self.search_text)
+            filename, lineno, colno, colend = itemdata
+            self.sig_edit_goto.emit(filename, lineno, colno, colend - colno)
 
     def set_sorting(self, flag):
         """Enable result sorting after search is complete."""
@@ -1032,12 +1048,12 @@ def append_result(self, items, title):
         self.setUpdatesEnabled(False)
         self.set_title(title)
         for item in items:
-            filename, lineno, colno, line = item
+            filename, lineno, colno, line, match_end = item
             file_item = self.files.get(filename, None)
             if file_item:
                 item = LineMatchItem(file_item, lineno, colno, line,
                                      self.font, self.text_color)
-                self.data[id(item)] = (filename, lineno, colno)
+                self.data[id(item)] = (filename, lineno, colno, match_end)
         self.setUpdatesEnabled(True)
 
     def set_max_results(self, value):


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -3867,5 +3867,52 @@ def test_print_comms(main_window, qtbot):
             in control.toPlainText())
 
 
+@pytest.mark.slow
+@flaky(max_runs=3)
+@pytest.mark.skipif(os.name == 'nt', reason="UTF8 on Windows")
+def test_goto_find(main_window, qtbot, tmpdir):
+    """Test find goes to the right place."""
+    # Use UTF8 only character to make sure positions are respected
+    code = "we Weee wee\nWe\n🚫 wee"
+    match_positions = [
+        (0, 2),
+        (3, 7),
+        (8, 11),
+        (12, 14),
+        (18, 21)
+    ]
+    subdir = tmpdir.mkdir("find-sub")
+    p = subdir.join("find-test.py")
+    p.write(code)
+    main_window.editor.load(to_text_string(p))
+    code_editor = main_window.editor.get_focus_widget()
+
+    main_window.explorer.chdir(str(subdir))
+
+    findinfiles = main_window.findinfiles.findinfiles
+    findinfiles.set_search_text("we+")
+    findinfiles.find_options.edit_regexp.setChecked(
+        True)
+    findinfiles.find_options.case_button.setChecked(
+        False)
+    with qtbot.waitSignal(findinfiles.sig_finished, timeout=SHELL_TIMEOUT):
+        findinfiles.find()
+
+    results = findinfiles.result_browser.data
+    assert len(results) == 5
+    assert len(findinfiles.result_browser.files) == 1
+
+    file_item = list(findinfiles.result_browser.files.values())[0]
+    assert file_item.childCount() == 5
+
+    for i in range(5):
+        item = file_item.child(i)
+        findinfiles.result_browser.setCurrentItem(item)
+        findinfiles.result_browser.activated(item)
+        cursor = code_editor.textCursor()
+        position = (cursor.selectionStart(), cursor.selectionEnd())
+        assert position == match_positions[i]
+
+
 if __name__ == "__main__":
     pytest.main()
diff --git a/spyder/plugins/findinfiles/tests/test_widgets.py b/spyder/plugins/findinfiles/tests/test_widgets.py
--- a/spyder/plugins/findinfiles/tests/test_widgets.py
+++ b/spyder/plugins/findinfiles/tests/test_widgets.py
@@ -38,7 +38,7 @@ def process_search_results(results):
     """
     matches = {}
     for result in results.values():
-        file, line, col = result
+        file, line, col, __ = result
         filename = osp.basename(file)
         if filename not in matches:
             matches[filename] = []