Repository: spyder-ide/spyder

Issue:
PyLS hangs if definition cannot be located
## Description

### What steps will reproduce the problem?

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

1.  Find a line of code with at least two levels of dot operators and no typehints, such as `self.encoding_changed.emit(finfo.encoding)`, where  the type of encoding_changed is not known
2.  Highlight the last level of dot operators (in my example, `emit`)
3.  Attempt to go to definition, either via context menu or shortcut  (Cursor should not advance to the definition)
4.  Make some change that should trigger diagnostic warning, like add whitespace to the end of a line
5.  Warning will fail to appear for the code style violation

This PyLS hang will persist until the file is closed.



## Versions

* Spyder version: 4.0.0.dev0 
* Python version: 3.7.0
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

```
pygments >=2.0    :  2.2.0 (OK)
sphinx >=0.6.6    :  2.0.0 (OK)
pyls >=0.19.0     :  0+unknown (OK)
nbconvert >=4.0   :  5.4.0 (OK)
pandas >=0.13.1   :  0.23.4 (OK)
numpy >=1.7       :  1.15.2 (OK)
sympy >=0.7.3     :  1.3 (OK)
cython >=0.21     :  0.28.5 (OK)
qtconsole >=4.2.0 :  4.4.2 (OK)
IPython >=4.0     :  7.0.1 (OK)
matplotlib >=2.0.0:  3.0.1 (OK)
pylint >=0.25     :  2.1.1 (OK)
```



Discussion:
The more I think about this, it could be more of a PyLS issue.  But maybe Spyder can work around it by timing out on requests from PyLS and restart the thread if it never returns in a `response_required` request.

I'll wait to hear what you guys think before I submit an issue on PyLS.
What PyLS version are you using?
Oh, sorry, I checked out the 0.19.0 branch of the PyLS git repo.

Resolving patch:
diff --git a/spyder/plugins/editor/widgets/codeeditor.py b/spyder/plugins/editor/widgets/codeeditor.py
--- a/spyder/plugins/editor/widgets/codeeditor.py
+++ b/spyder/plugins/editor/widgets/codeeditor.py
@@ -19,12 +19,13 @@
 
 # Standard library imports
 from __future__ import division
+
+import logging
 import os.path as osp
 import re
 import sre_constants
 import sys
 import time
-import logging
 from unicodedata import category
 
 # Third party imports
@@ -45,7 +46,7 @@
 
 # Local imports
 from spyder.api.panel import Panel
-from spyder.config.base import _
+from spyder.config.base import _, get_debug_level
 from spyder.config.gui import get_shortcut, config_shortcut
 from spyder.config.main import (CONF, RUN_CELL_SHORTCUT,
                                 RUN_CELL_AND_ADVANCE_SHORTCUT)
@@ -720,6 +721,23 @@ def emit_request(self, method, params, requires_response):
         self.sig_perform_lsp_request.emit(
             self.language.lower(), method, params)
 
+    def log_lsp_handle_errors(self, message):
+        """
+        Log errors when handling LSP responses.
+
+        This works when debugging is on or off.
+        """
+        if get_debug_level() > 0:
+            # We log the error normally when running on debug mode.
+            logger.error(message, exc_info=True)
+        else:
+            # We need this because logger.error activates our error
+            # report dialog but it doesn't show the entire traceback
+            # there. So we intentionally leave an error in this call
+            # to get the entire stack info generated by it, which
+            # gives the info we need from users.
+            logger.error('%', 1, stack_info=True)
+
     # ------------- LSP: Configuration and protocol start/end ----------------
     def start_lsp_services(self, config):
         """Start LSP integration if it wasn't done before."""
@@ -758,7 +776,7 @@ def parse_lsp_config(self, config):
 
     @request(method=LSPRequestTypes.DOCUMENT_DID_OPEN, requires_response=False)
     def document_did_open(self):
-        """Send textDocument/didOpen request to server."""
+        """Send textDocument/didOpen request to the server."""
         self.document_opened = True
         params = {
             'file': self.filename,
@@ -773,7 +791,7 @@ def document_did_open(self):
     @request(
         method=LSPRequestTypes.DOCUMENT_DID_CHANGE, requires_response=False)
     def document_did_change(self, text=None):
-        """Send textDocument/didChange request to server."""
+        """Send textDocument/didChange request to the server."""
         self.text_version += 1
         params = {
             'file': self.filename,
@@ -784,12 +802,16 @@ def document_did_change(self, text=None):
 
     @handles(LSPRequestTypes.DOCUMENT_PUBLISH_DIAGNOSTICS)
     def linting_diagnostics(self, params):
-        self.process_code_analysis(params['params'])
+        """Handle linting response."""
+        try:
+            self.process_code_analysis(params['params'])
+        except Exception:
+            self.log_lsp_handle_errors("Error when processing linting")
 
     # ------------- LSP: Completion ---------------------------------------
     @request(method=LSPRequestTypes.DOCUMENT_COMPLETION)
     def do_completion(self, automatic=False):
-        """Trigger completion"""
+        """Trigger completion."""
         self.document_did_change('')
         line, column = self.get_cursor_line_column()
         params = {
@@ -803,15 +825,20 @@ def do_completion(self, automatic=False):
     @handles(LSPRequestTypes.DOCUMENT_COMPLETION)
     def process_completion(self, params):
         """Handle completion response."""
-        completions = params['params']
-        if completions is not None and len(completions) > 0:
-            completion_list = sorted(completions, key=lambda x: x['sortText'])
-            position = self.last_completion_position
-            self.completion_widget.show_list(completion_list, position)
+        try:
+            completions = params['params']
+            if completions is not None and len(completions) > 0:
+                completion_list = sorted(completions,
+                                         key=lambda x: x['sortText'])
+                position = self.last_completion_position
+                self.completion_widget.show_list(completion_list, position)
+        except Exception:
+            self.log_lsp_handle_errors('Error when processing completions')
 
     # ------------- LSP: Signature Hints ------------------------------------
     @request(method=LSPRequestTypes.DOCUMENT_SIGNATURE)
     def request_signature(self):
+        """Ask for signature."""
         self.document_did_change('')
         line, column = self.get_cursor_line_column()
         params = {
@@ -823,43 +850,48 @@ def request_signature(self):
 
     @handles(LSPRequestTypes.DOCUMENT_SIGNATURE)
     def process_signatures(self, params):
-        signature = params['params']
-        if (signature is not None and
-                'activeParameter' in signature):
-            self.sig_signature_invoked.emit()
-            line, _ = self.get_cursor_line_column()
-            active_parameter_idx = signature['activeParameter']
-            signature = signature['signatures']
-            func_doc = signature['documentation']
-            func_signature = signature['label']
-            parameters = signature['parameters']
-            parameter = parameters[active_parameter_idx]
-
-            font = self.font()
-            size = font.pointSize()
-            family = font.family()
-
-            parameter_str = ''
-            color_change_str = ('<span style=\'font-family: "{0}"; '
-                                'font-size: {1}pt; color: {2}\'>')
-            if (parameter['documentation'] is not None and
-                    len(parameter['documentation']) > 0):
-                parameter_fmt = ('{0}<b>{1}</b></span>: {2}\n')
-                parameter_str = parameter_fmt.format(
-                    color_change_str.format(family, size, '#daa520'),
-                    parameter['label'], parameter['documentation'])
-
-            title = func_signature.replace(
-                parameter['label'], '{0}{1}</span>'.format(
-                    color_change_str.format(family, size, '#daa520'),
-                    parameter['label']))
-            tooltip_text = "{0}{1}".format(parameter_str, func_doc)
-            self.show_calltip(
-                title, tooltip_text, color='#999999')
+        """Handle signature response."""
+        try:
+            signature = params['params']
+            if (signature is not None and
+                    'activeParameter' in signature):
+                self.sig_signature_invoked.emit()
+                line, _ = self.get_cursor_line_column()
+                active_parameter_idx = signature['activeParameter']
+                signature = signature['signatures']
+                func_doc = signature['documentation']
+                func_signature = signature['label']
+                parameters = signature['parameters']
+                parameter = parameters[active_parameter_idx]
+
+                font = self.font()
+                size = font.pointSize()
+                family = font.family()
+
+                parameter_str = ''
+                color_change_str = ('<span style=\'font-family: "{0}"; '
+                                    'font-size: {1}pt; color: {2}\'>')
+                if (parameter['documentation'] is not None and
+                        len(parameter['documentation']) > 0):
+                    parameter_fmt = ('{0}<b>{1}</b></span>: {2}\n')
+                    parameter_str = parameter_fmt.format(
+                        color_change_str.format(family, size, '#daa520'),
+                        parameter['label'], parameter['documentation'])
+
+                title = func_signature.replace(
+                    parameter['label'], '{0}{1}</span>'.format(
+                        color_change_str.format(family, size, '#daa520'),
+                        parameter['label']))
+                tooltip_text = "{0}{1}".format(parameter_str, func_doc)
+                self.show_calltip(
+                    title, tooltip_text, color='#999999')
+        except Exception:
+            self.log_lsp_handle_errors("Error when processing signature")
 
     # ------------- LSP: Hover ---------------------------------------
     @request(method=LSPRequestTypes.DOCUMENT_HOVER)
     def request_hover(self, line, col):
+        """Request hover information."""
         params = {
             'file': self.filename,
             'line': line,
@@ -869,16 +901,20 @@ def request_hover(self, line, col):
 
     @handles(LSPRequestTypes.DOCUMENT_HOVER)
     def handle_hover_response(self, contents):
-        text = contents['params']
-        self.sig_display_signature.emit(text)
-        self.show_calltip(_("Hint"), text)
-        # QTimer.singleShot(20000, lambda: QToolTip.hideText())
+        """Handle hover response."""
+        try:
+            text = contents['params']
+            self.sig_display_signature.emit(text)
+            self.show_calltip(_("Hint"), text)
+            # QTimer.singleShot(20000, lambda: QToolTip.hideText())
+        except Exception:
+            self.log_lsp_handle_errors("Error when processing hover")
 
     # ------------- LSP: Go To Definition ----------------------------
     @Slot()
     @request(method=LSPRequestTypes.DOCUMENT_DEFINITION)
     def go_to_definition_from_cursor(self, cursor=None):
-        """Go to definition from cursor instance (QTextCursor)"""
+        """Go to definition from cursor instance (QTextCursor)."""
         if (not self.go_to_definition_enabled or
                 self.in_comment_or_string()):
             return
@@ -899,21 +935,30 @@ def go_to_definition_from_cursor(self, cursor=None):
 
     @handles(LSPRequestTypes.DOCUMENT_DEFINITION)
     def handle_go_to_definition(self, position):
-        position = position['params']
-        if position is not None:
-            def_range = position['range']
-            start = def_range['start']
-            if self.filename == position['file']:
-                self.go_to_line(start['line'] + 1, start['character'],
-                                None, word=None)
-            else:
-                self.go_to_definition.emit(position['file'], start['line'] + 1,
-                                           start['character'])
+        """Handle go to definition response."""
+        try:
+            position = position['params']
+            if position is not None:
+                def_range = position['range']
+                start = def_range['start']
+                if self.filename == position['file']:
+                    self.go_to_line(start['line'] + 1,
+                                    start['character'],
+                                    None,
+                                    word=None)
+                else:
+                    self.go_to_definition.emit(position['file'],
+                                               start['line'] + 1,
+                                               start['character'])
+        except Exception:
+            self.log_lsp_handle_errors(
+                "Error when processing go to definition")
 
     # ------------- LSP: Save/close file -----------------------------------
     @request(method=LSPRequestTypes.DOCUMENT_DID_SAVE,
              requires_response=False)
     def notify_save(self):
+        """Send save request."""
         # self.document_did_change()
         params = {'file': self.filename}
         if self.save_include_text:
@@ -923,6 +968,7 @@ def notify_save(self):
     @request(method=LSPRequestTypes.DOCUMENT_DID_CLOSE,
              requires_response=False)
     def notify_close(self):
+        """Send close request."""
         if self.lsp_ready:
             params = {
                 'file': self.filename,


Test patch: