Repository: spyder-ide/spyder

Issue:
Segmentation fault in paintEvent
<!--- **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
* [x] 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
* [ ] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
I had a segmentation fault during a `paintEvent` indicating that a C++ object was freed too early.


### What steps reproduce the problem?

1. Search and replace + ????
2. Crash

### What is the expected output? What do you see instead?
No Crash


### Paste Traceback/Error Below (if applicable)
<!--- Copy from error dialog or View > Panes > Internal Console --->

```python-traceback
Current thread 0x0000000110f0ed40 (most recent call first):
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/widgets/codeeditor.py", line 4082 in paintEvent
  File "/Users/quentinpeter/pyscripts/spyder/spyder/widgets/helperwidgets.py", line 260 in update_status
  File "/Users/quentinpeter/pyscripts/spyder/spyder/widgets/comboboxes.py", line 90 in selected
  File "/Users/quentinpeter/pyscripts/spyder/spyder/widgets/comboboxes.py", line 174 in selected
  File "/Users/quentinpeter/pyscripts/spyder/spyder/widgets/comboboxes.py", line 359 in selected
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/pylint/widgets/pylintgui.py", line 249 in set_filename
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/pylint/plugin.py", line 145 in set_filename
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/widgets/editor.py", line 2160 in focus_changed
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/widgets/base.py", line 983 in focusInEvent
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/widgets/codeeditor.py", line 3429 in event
  File "/Users/quentinpeter/pyscripts/spyder/spyder/widgets/findreplace.py", line 359 in find_next
  File "/Users/quentinpeter/pyscripts/spyder/spyder/widgets/findreplace.py", line 527 in replace_find
  File "/Users/quentinpeter/pyscripts/spyder/spyder/app/mainwindow.py", line 3551 in run_spyder
  File "/Users/quentinpeter/pyscripts/spyder/spyder/app/mainwindow.py", line 3668 in main
  File "/Users/quentinpeter/pyscripts/spyder/spyder/app/start.py", line 209 in main
  File "/Applications/Spyder.app/Contents/MacOS/Spyder", line 7 in <module>
```

Head of C traceback:
```
Thread 0 Crashed:: ZMQbg/105  Dispatch queue: com.apple.main-thread
0   libsystem_kernel.dylib        	0x00007fff6f0e747a __pthread_kill + 10
1   libsystem_pthread.dylib       	0x00007fff6f1a4707 pthread_kill + 384
2   libsystem_c.dylib             	0x00007fff6efff31e raise + 26
3   libsystem_platform.dylib      	0x00007fff6f199b1d _sigtramp + 29
4   ???                           	000000000000000000 0 + 0
5   QtWidgets                     	0x0000000123f2771e QPlainTextEdit::paintEvent(QPaintEvent*) + 1598
6   QtWidgets.so                  	0x000000012399a2db meth_QPlainTextEdit_paintEvent(_object*, _object*) + 139
7   org.python.python             	0x0000000106b6c42b _PyMethodDef_RawFastCallKeywords + 235
8   org.python.python             	0x0000000106b6bab2 _PyCFunction_FastCallKeywords + 44
9   org.python.python             	0x0000000106c00e82 call_function + 636
```

Probably not relevant, but another thread was doing:
```
Thread 0x00007000016d6000 (most recent call first):
  File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/genericpath.py", line 19 in exists
  File "/usr/local/lib/python3.7/site-packages/requests/utils.py", line 185 in get_netrc_auth
  File "/usr/local/lib/python3.7/site-packages/requests/sessions.py", line 449 in prepare_request
  File "/usr/local/lib/python3.7/site-packages/requests/sessions.py", line 519 in request
  File "/usr/local/lib/python3.7/site-packages/requests/sessions.py", line 546 in get
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/completion/kite/client.py", line 114 in perform_http_request
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/completion/kite/client.py", line 98 in _get_status
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/completion/kite/client.py", line 103 in get_status
```

## 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 version: Spyder 4.0.0.dev0 (Commit: cbbcf69ae) 
* Python version: Python 3.7.4 64-bit
* Qt version: Qt 5.12.4
* PyQt version: PyQt5 5.12.3
* Operating System name/version: Darwin 19.0.0 

### Dependencies
<!--- Please go to the menu entry Help > Dependencies,
press the Copy to clipboard button and paste below --->

```
cloudpickle >=0.5.0          :  0.8.0 (OK)
pygments >=2.0               :  2.3.1 (OK)
qtconsole >=4.5.5            :  4.6.0.dev0 (OK)
nbconvert >=4.0              :  5.4.1 (OK)
sphinx >=0.6.6               :  1.8.5 (OK)
pylint >=0.25                :  2.3.1 (OK)
psutil >=0.3                 :  5.6.1 (OK)
qtawesome >=0.5.7            :  0.5.7 (OK)
qtpy >=1.5.0                 :  1.7.1 (OK)
pickleshare >=0.4            :  0.7.5 (OK)
zmq >=17                     :  18.0.1 (OK)
chardet >=2.0.0              :  3.0.4 (OK)
numpydoc >=0.6.0             :  0.8.0 (OK)
spyder_kernels >=1.5.0;<2.0.0:  1.7.0.dev0 (OK)
qdarkstyle >=2.7             :  2.7 (OK)
atomicwrites >=1.2.0         :  1.3.0 (OK)
diff_match_patch >=20181111  :  20181111 (OK)
watchdog                     :  None (OK)
keyring                      :  None (OK)
pexpect >=4.4.0              :  4.6.0 (OK)
pympler                      :  None (OK)
sympy >=0.7.3                :  1.3 (OK)
cython >=0.21                :  0.29.6 (OK)
IPython >=4.0                :  7.8.0 (OK)
matplotlib >=2.0.0           :  3.1.1 (OK)
pandas >=0.13.1              :  0.24.2 (OK)
numpy >=1.7                  :  1.16.2 (OK)
scipy >=0.17.0               :  1.2.1 (OK)
pyls >=0.28.2;<0.29.0        :  0.28.3 (OK)
applaunchservices >=0.1.7    :  0.2.0 (OK)
```



Discussion:
> Search and replace + ????

What do you mean by this? Where you searching `????` or what?
What I mean is that most of the time search and replace works fine. When spyder crashes something else must happen, but I don't know what.
Usually (It happened twice) it happens if I replace the same text multiple times in a row.
Ok, but we need a reproducible (or almost reproducible) case to understand how to fix this.
Paste in a file:
```
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa, aaa
```
search `aaa` and replace by `bbb_cccccccc` repetitively
This crashes for me after pressing ~ 10 times on `Research/find next`
Also somehow spyder becomes super slow with this code! Try saving this file, loading it, and clicking on a `aaa` so it is highlighed.
@goanpeca, please take a look at this one.
>Also somehow spyder becomes super slow with this code! Try saving this file, loading it, and clicking on a aaa so it is highlighed.

This I notice and can try to work with this.


>This crashes for me after pressing ~ 10 times on Research/find next

I cannot reproduce this @impact27 🤷‍♂ 
Well now I can't reproduce either, but it was very reproducible when it was happening. :/
It seems having many extra selections is making the widget performance sluggish. Not sure of any real solution to this.
> It seems having many extra selections is making the widget performance sluggish

What if we repaint every certain time with a QTimer? I mean, unless users start writing in the editor again?
The problem is already evident with scrilling which triggers repaint anyways
Scrolling with any file content? Or in this case with `aaa` selected?
>Scrolling with any file content? Or in this case with aaa selected?

With aaa selected

Resolving patch:
diff --git a/spyder/widgets/findreplace.py b/spyder/widgets/findreplace.py
--- a/spyder/widgets/findreplace.py
+++ b/spyder/widgets/findreplace.py
@@ -369,20 +369,22 @@ def set_editor(self, editor, refresh=True):
             self.clear_matches()
 
     @Slot()
-    def find_next(self):
+    def find_next(self, set_focus=True):
         """Find next occurrence"""
         state = self.find(changed=False, forward=True, rehighlight=False,
                           multiline_replace_check=False)
-        self.editor.setFocus()
+        if set_focus:
+            self.editor.setFocus()
         self.search_text.add_current_text()
         return state
 
     @Slot()
-    def find_previous(self):
+    def find_previous(self, set_focus=True):
         """Find previous occurrence"""
         state = self.find(changed=False, forward=False, rehighlight=False,
                           multiline_replace_check=False)
-        self.editor.setFocus()
+        if set_focus:
+            self.editor.setFocus()
         return state
 
     def text_has_been_edited(self, text):
@@ -469,99 +471,101 @@ def find(self, changed=True, forward=True, rehighlight=True,
     @Slot()
     def replace_find(self, focus_replace_text=False, replace_all=False):
         """Replace and find"""
-        if (self.editor is not None):
-            replace_text = to_text_string(self.replace_text.currentText())
-            search_text = to_text_string(self.search_text.currentText())
-            re_pattern = None
-            case = self.case_button.isChecked()
-            re_flags = re.MULTILINE if case else re.IGNORECASE | re.MULTILINE
-            # Check regexp before proceeding
-            if self.re_button.isChecked():
-                try:
-                    re_pattern = re.compile(search_text, flags=re_flags)
-                    # Check if replace_text can be substituted in re_pattern
-                    # Fixes spyder-ide/spyder#7177.
-                    re_pattern.sub(replace_text, '')
-                except re.error:
-                    # Do nothing with an invalid regexp
-                    return
-
-            first = True
-            cursor = None
-            while True:
-                if first:
-                    # First found
-                    seltxt = to_text_string(self.editor.get_selected_text())
-                    cmptxt1 = search_text if case else search_text.lower()
-                    cmptxt2 = seltxt if case else seltxt.lower()
-                    if re_pattern is None:
-                        has_selected = self.editor.has_selected_text()
-                        if has_selected and cmptxt1 == cmptxt2:
-                            # Text was already found, do nothing
-                            pass
-                        else:
-                            if not self.find(changed=False, forward=True,
-                                             rehighlight=False):
-                                break
+        if self.editor is None:
+            return
+        replace_text = to_text_string(self.replace_text.currentText())
+        search_text = to_text_string(self.search_text.currentText())
+        re_pattern = None
+        case = self.case_button.isChecked()
+        re_flags = re.MULTILINE if case else re.IGNORECASE | re.MULTILINE
+        # Check regexp before proceeding
+        if self.re_button.isChecked():
+            try:
+                re_pattern = re.compile(search_text, flags=re_flags)
+                # Check if replace_text can be substituted in re_pattern
+                # Fixes spyder-ide/spyder#7177.
+                re_pattern.sub(replace_text, '')
+            except re.error:
+                # Do nothing with an invalid regexp
+                return
+
+        first = True
+        cursor = None
+        while True:
+            if first:
+                # First found
+                seltxt = to_text_string(self.editor.get_selected_text())
+                cmptxt1 = search_text if case else search_text.lower()
+                cmptxt2 = seltxt if case else seltxt.lower()
+                if re_pattern is None:
+                    has_selected = self.editor.has_selected_text()
+                    if has_selected and cmptxt1 == cmptxt2:
+                        # Text was already found, do nothing
+                        pass
                     else:
-                        if len(re_pattern.findall(cmptxt2)) > 0:
-                            pass
-                        else:
-                            if not self.find(changed=False, forward=True,
-                                             rehighlight=False):
-                                break
-                    first = False
-                    wrapped = False
-                    position = self.editor.get_position('cursor')
-                    position0 = position
-                    cursor = self.editor.textCursor()
-                    cursor.beginEditBlock()
+                        if not self.find(changed=False, forward=True,
+                                         rehighlight=False):
+                            break
                 else:
-                    position1 = self.editor.get_position('cursor')
-                    if is_position_inf(position1,
-                                       position0 + len(replace_text) -
-                                       len(search_text) + 1):
-                        # Identify wrapping even when the replace string
-                        # includes part of the search string
-                        wrapped = True
-                    if wrapped:
-                        if position1 == position or \
-                           is_position_sup(position1, position):
-                            # Avoid infinite loop: replace string includes
-                            # part of the search string
+                    if len(re_pattern.findall(cmptxt2)) > 0:
+                        pass
+                    else:
+                        if not self.find(changed=False, forward=True,
+                                         rehighlight=False):
                             break
-                    if position1 == position0:
-                        # Avoid infinite loop: single found occurrence
+                first = False
+                wrapped = False
+                position = self.editor.get_position('cursor')
+                position0 = position
+                cursor = self.editor.textCursor()
+                cursor.beginEditBlock()
+            else:
+                position1 = self.editor.get_position('cursor')
+                if is_position_inf(position1,
+                                   position0 + len(replace_text) -
+                                   len(search_text) + 1):
+                    # Identify wrapping even when the replace string
+                    # includes part of the search string
+                    wrapped = True
+                if wrapped:
+                    if position1 == position or \
+                       is_position_sup(position1, position):
+                        # Avoid infinite loop: replace string includes
+                        # part of the search string
                         break
-                    position0 = position1
-                if re_pattern is None:
-                    cursor.removeSelectedText()
-                    cursor.insertText(replace_text)
-                else:
-                    seltxt = to_text_string(cursor.selectedText())
-                    cursor.removeSelectedText()
-                    cursor.insertText(re_pattern.sub(replace_text, seltxt))
-                if self.find_next():
-                    found_cursor = self.editor.textCursor()
-                    cursor.setPosition(found_cursor.selectionStart(),
-                                       QTextCursor.MoveAnchor)
-                    cursor.setPosition(found_cursor.selectionEnd(),
-                                       QTextCursor.KeepAnchor)
-                else:
+                if position1 == position0:
+                    # Avoid infinite loop: single found occurrence
                     break
-                if not replace_all:
-                    break
-            if cursor is not None:
-                cursor.endEditBlock()
-            if focus_replace_text:
-                self.replace_text.setFocus()
+                position0 = position1
+            if re_pattern is None:
+                cursor.removeSelectedText()
+                cursor.insertText(replace_text)
+            else:
+                seltxt = to_text_string(cursor.selectedText())
+                cursor.removeSelectedText()
+                cursor.insertText(re_pattern.sub(replace_text, seltxt))
+            if self.find_next(set_focus=False):
+                found_cursor = self.editor.textCursor()
+                cursor.setPosition(found_cursor.selectionStart(),
+                                   QTextCursor.MoveAnchor)
+                cursor.setPosition(found_cursor.selectionEnd(),
+                                   QTextCursor.KeepAnchor)
+            else:
+                break
+            if not replace_all:
+                break
+        if cursor is not None:
+            cursor.endEditBlock()
+        if focus_replace_text:
+            self.replace_text.setFocus()
+        else:
+            self.editor.setFocus()
 
     @Slot()
     def replace_find_all(self, focus_replace_text=False):
         """Replace and find all matching occurrences"""
         self.replace_find(focus_replace_text, replace_all=True)
 
-
     @Slot()
     def replace_find_selection(self, focus_replace_text=False):
         """Replace and find in the current selection"""


Test patch: