Repository: spyder-ide/spyder

Issue:
RuntimeError after closing a split editor
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

1. open and close a split editor
2. ?
3. Get the error

### Traceback
```python-traceback
    self.process_code_analysis(params['params'])
    self.cleanup_code_analysis()
    self.setUpdatesEnabled(False)
RuntimeError: wrapped C/C++ object of type CodeEditor has been deleted

Traceback (most recent call last):
    msg = self.format(record)
    return fmt.format(record)
    record.message = record.getMessage()
    msg = msg % self.args
ValueError: incomplete format
Call stack:
    start.main()
    mainwindow.main()
    mainwindow = run_spyder(app, options, args)
    app.exec_()
    handler(resp['params'])
    {'params': diagnostics})
    handler(params)
    self.log_lsp_handle_errors("Error when processing linting")
    logger.error('%', 1, stack_info=True)
Arguments: (1,)
Traceback (most recent call last):
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/lsp/client.py", line 249, in on_msg_received
    handler(resp['params'])
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/lsp/providers/document.py", line 51, in process_document_diagnostics
    {'params': diagnostics})
  File "/Users/quentinpeter/pyscripts/spyder/spyder/plugins/editor/widgets/codeeditor.py", line 805, in handle_response
    self.lsp_response_signal.emit(method, params)
RuntimeError: wrapped C/C++ object of type CodeEditor has been deleted
```

## Versions

* Spyder version: 4.0.0.dev0 45efb0945
* Python version: 3.7.3
* Qt version: 5.9.1
* PyQt5 version: 5.9
* Operating System: Darwin 18.5.0

### Dependencies

```
pygments >=2.0     :  2.3.1 (OK)
qdarkstyle >=2.6.4 :  2.6.5 (OK)
sphinx >=0.6.6     :  1.8.5 (OK)
psutil >=0.3       :  5.6.1 (OK)
pyls >=0.19.0;<0.25:  0.25.0 (NOK)
nbconvert >=4.0    :  5.4.1 (OK)
pandas >=0.13.1    :  0.24.2 (OK)
numpy >=1.7        :  1.16.2 (OK)
sympy >=0.7.3      :  1.3 (OK)
cython >=0.21      :  0.29.6 (OK)
qtconsole >=4.2.0  :  4.5.0.dev0 (OK)
IPython >=4.0      :  7.3.0 (OK)
matplotlib >=2.0.0 :  3.0.3 (OK)
pylint >=0.25      :  2.3.1 (OK)
```



Discussion:
Ok, I understand the error now. It happens when you close the splitted panel before linting results arrive from the PyLS.

@andfoy, please take a look at this one. We should unregister the CodeEditor instance that gets removed, right? (well, you know better).

Please focus on solving this one as soon as possible so we can have it as part of beta2.
@CAM-Gerlach, I think issue #9325 should be merged with this one.
@ccordoba12 Yes, it would seem one of them should be merged with the other. #9325 has much more extensive detail and documentation of the problem, including all relevant logs and environment information, and has seniority, but this issue has a longer actual traceback and is more focused on what the actual problem appears to be. Your call which to keep as the canonical one.
BTW, thanks @impact27 for helping narrow this tricky issue down.

Resolving patch:
diff --git a/spyder/plugins/editor/lsp/providers/document.py b/spyder/plugins/editor/lsp/providers/document.py
--- a/spyder/plugins/editor/lsp/providers/document.py
+++ b/spyder/plugins/editor/lsp/providers/document.py
@@ -246,6 +246,8 @@ def document_did_close(self, params):
             params[ClientConstants.CANCEL] = True
         else:
             editors = self.watched_files[filename]
+            if len(editors) > 1:
+                params[ClientConstants.CANCEL] = True
             idx = -1
             for i, editor in enumerate(editors):
                 if id(codeeditor) == id(editor):
@@ -256,4 +258,5 @@ def document_did_close(self, params):
 
             if len(editors) == 0:
                 self.watched_files.pop(filename)
+
         return params
diff --git a/spyder/plugins/editor/widgets/editor.py b/spyder/plugins/editor/widgets/editor.py
--- a/spyder/plugins/editor/widgets/editor.py
+++ b/spyder/plugins/editor/widgets/editor.py
@@ -819,6 +819,8 @@ def closeEvent(self, event):
             for finfo in self.data:
                 self.outlineexplorer.remove_editor(finfo.editor.oe_proxy)
 
+        for finfo in self.data:
+            finfo.editor.notify_close()
         QWidget.closeEvent(self, event)
 
     def clone_editor_from(self, other_finfo, set_current):
@@ -2647,7 +2649,7 @@ def editorstack_closed(self):
             self.unregister_editorstack_cb(self.editorstack)
             self.editorstack = None
             close_splitter = self.count() == 1
-        except RuntimeError:
+        except (RuntimeError, AttributeError):
             # editorsplitter has been destroyed (happens when closing a
             # EditorMainWindow instance)
             return


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_editorsplitter.py b/spyder/plugins/editor/widgets/tests/test_editorsplitter.py
--- a/spyder/plugins/editor/widgets/tests/test_editorsplitter.py
+++ b/spyder/plugins/editor/widgets/tests/test_editorsplitter.py
@@ -11,8 +11,14 @@
 # Standard library imports
 try:
     from unittest.mock import Mock
+    import pathlib
 except ImportError:
     from mock import Mock  # Python 2
+    import pathlib2 as pathlib
+
+import os
+import os.path as osp
+from functools import partial
 
 # Third party imports
 import pytest
@@ -40,6 +46,60 @@ def editor_splitter_bot(qtbot):
     return es
 
 
+@pytest.fixture
+def editor_splitter_lsp(qtbot_module, lsp_manager, request):
+    text = """
+    import sys
+    """
+
+    def report_file_open(options):
+        filename = options['filename']
+        language = options['language']
+        callback = options['codeeditor']
+        lsp_manager.register_file(
+            language.lower(), filename, callback)
+        settings = lsp_manager.main.editor.lsp_editor_settings['python']
+        callback.start_lsp_services(settings)
+
+        with qtbot_module.waitSignal(
+                callback.lsp_response_signal, timeout=30000):
+            callback.document_did_open()
+
+    def register_editorstack(editorstack):
+        editorstack.perform_lsp_request.connect(lsp_manager.send_request)
+        editorstack.sig_open_file.connect(report_file_open)
+        settings = lsp_manager.main.editor.lsp_editor_settings['python']
+        editorstack.notify_server_ready('python', settings)
+
+    def clone(editorstack, template=None):
+        # editorstack.clone_from(template)
+        editor_stack = EditorStack(None, [])
+        editor_stack.set_find_widget(Mock())
+        editor_stack.set_io_actions(Mock(), Mock(), Mock(), Mock())
+        # Emulate "cloning"
+        editorsplitter.editorstack.new('test.py', 'utf-8', text)
+
+    mock_plugin = Mock()
+    editorsplitter = EditorSplitter(
+        None, mock_plugin, [], register_editorstack_cb=register_editorstack)
+
+    editorsplitter.editorstack.set_find_widget(Mock())
+    editorsplitter.editorstack.set_io_actions(Mock(), Mock(), Mock(), Mock())
+    editorsplitter.editorstack.new('test.py', 'utf-8', text)
+
+    mock_plugin.clone_editorstack.side_effect = partial(
+        clone, template=editorsplitter.editorstack)
+    qtbot_module.addWidget(editorsplitter)
+    editorsplitter.show()
+
+    def teardown():
+        editorsplitter.hide()
+        editorsplitter.close()
+
+    request.addfinalizer(teardown)
+    return editorsplitter, lsp_manager
+
+
 @pytest.fixture
 def editor_splitter_layout_bot(editor_splitter_bot):
     """Create editor splitter for testing layouts."""
@@ -64,7 +124,6 @@ def clone(editorstack):
 
 
 # ---- Tests
-
 def test_init(editor_splitter_bot):
     """"Test __init__."""
     es = editor_splitter_bot
@@ -332,6 +391,29 @@ def test_set_layout_settings_goto(editor_splitter_layout_bot):
                                              (False, 'foo.py', [1, 1, 1])]
 
 
+@pytest.mark.slow
+@pytest.mark.first
+@pytest.mark.skipif(os.name == 'nt',
+                    reason="Makes other tests fail on Windows")
+def test_lsp_splitter_close(editor_splitter_lsp):
+    """Test for issue #9341"""
+    editorsplitter, lsp_manager = editor_splitter_lsp
+
+    editorsplitter.split()
+    lsp_files = lsp_manager.clients['python']['instance'].watched_files
+    editor = editorsplitter.editorstack.get_current_editor()
+    path = pathlib.Path(osp.abspath(editor.filename)).as_uri()
+    assert len(lsp_files[path]) == 2
+
+    editorstacks = editorsplitter.iter_editorstacks()
+    assert len(editorstacks) == 2
+
+    last_editorstack = editorstacks[0][0]
+    last_editorstack.close()
+    lsp_files = lsp_manager.clients['python']['instance'].watched_files
+    assert len(lsp_files[path]) == 1
+
+
 if __name__ == "__main__":
     import os.path as osp
     pytest.main(['-x', osp.basename(__file__), '-v', '-rw'])