Repository: spyder-ide/spyder

Issue:
Hovers and calltips broken in the Editor
@dalthviz, I just found out that our hovers and calltips are broken in the Editor. These are some examples:

![Selección_016](https://user-images.githubusercontent.com/365293/60034196-58cdcd80-96aa-11e9-8a94-3ff542efabee.png)

![Selección_017](https://user-images.githubusercontent.com/365293/60034239-6f742480-96aa-11e9-974d-a2c47f6a5926.png)

![Selección_020](https://user-images.githubusercontent.com/365293/60034677-79e2ee00-96ab-11e9-9dfa-cb76b26cd71d.png)

It seems adjusted things too much for the IPython console in PR #9585.

----

Please solve this one ASAP because we need to include it in beta3.


Discussion:

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
@@ -2020,6 +2020,7 @@ def show_code_analysis_results(self, line_number, block_data):
         )
 
         msglist = []
+        max_lines_msglist = 25
         sorted_code_analysis = sorted(code_analysis, key=lambda i: i[2])
         for src, code, sev, msg in sorted_code_analysis:
             if '[' in msg and ']' in msg:
@@ -2028,15 +2029,49 @@ def show_code_analysis_results(self, line_number, block_data):
 
             msg = msg.strip()
             # Avoid messing TODO, FIXME
-            msg = msg[0].upper() + msg[1:]
-            msg = textwrap.wrap(msg, width=self._DEFAULT_MAX_WIDTH)
+            # Prevent error if msg only has one element
             if len(msg) > 1:
-                msg = '<br>'.join(msg) + '<br>'
+                msg = msg[0].upper() + msg[1:]
+
+            # Get individual lines following paragraph format and handle
+            # symbols like '<' and '>' to not mess with br tags
+            msg = msg.replace('<', '&lt;').replace('>', '&gt;')
+            paragraphs = msg.splitlines()
+            new_paragraphs = []
+            long_paragraphs = 0
+            lines_per_message = 6
+            for paragraph in paragraphs:
+                new_paragraph = textwrap.wrap(
+                    paragraph,
+                    width=self._DEFAULT_MAX_HINT_WIDTH)
+                if lines_per_message > 2:
+                    if len(new_paragraph) > 1:
+                        new_paragraph = '<br>'.join(new_paragraph[:2]) + '...'
+                        long_paragraphs += 1
+                        lines_per_message -= 2
+                    else:
+                        new_paragraph = '<br>'.join(new_paragraph)
+                        lines_per_message -= 1
+                    new_paragraphs.append(new_paragraph)
+
+            if len(new_paragraphs) > 1:
+                # Define max lines taking into account that in the same
+                # tooltip you can find multiple warnings and messages
+                # and each one can have multiple lines
+                if long_paragraphs != 0:
+                    max_lines = 3
+                    max_lines_msglist -= max_lines * 2
+                else:
+                    max_lines = 5
+                    max_lines_msglist -= max_lines
+                msg = '<br>'.join(new_paragraphs[:max_lines]) + '<br>'
             else:
-                msg = '<br>'.join(msg)
+                msg = '<br>'.join(new_paragraphs)
+
             base_64 = ima.base64_from_icon(icons[sev], size, size)
-            msglist.append(template.format(base_64, msg, src,
-                                           code, size=size))
+            if max_lines_msglist >= 0:
+                msglist.append(template.format(base_64, msg, src,
+                                               code, size=size))
 
         if msglist:
             self.show_tooltip(
diff --git a/spyder/plugins/ipythonconsole/widgets/help.py b/spyder/plugins/ipythonconsole/widgets/help.py
--- a/spyder/plugins/ipythonconsole/widgets/help.py
+++ b/spyder/plugins/ipythonconsole/widgets/help.py
@@ -48,19 +48,21 @@ def get_documentation(self, content):
                 signature = self.get_signature(content).split('(')[-1]
 
                 # Base value for the documentation
-                documentation = text.split('Docstring:')[-1].split('Type:')[0]
+                documentation = (text.split('Docstring:')[-1].
+                                 split('Type:')[0].split('File:')[0])
 
                 if signature:
                     # Check if the signature is in the Docstring
                     doc_from_signature = documentation.split(signature)
                     if len(doc_from_signature) > 1:
                         return (doc_from_signature[-1].split('Docstring:')[-1].
-                                split('Type:')[0])
+                                split('Type:')[0].
+                                split('File:')[0]).strip('\r\n')
 
-                return documentation
+                return documentation.strip('\r\n')
             else:
                 text = re.compile(ANSI_PATTERN).sub('', text)
-                return text
+                return text.strip('\r\n')
         else:
             return ''
 
@@ -115,10 +117,10 @@ def get_signature(self, content):
                 # recognizing the signature in the logic above.
                 signature = ANSI_OR_SPECIAL_PATTERN.sub('', signature)
 
-                return signature
+                return signature.strip('\r\n')
             else:
                 text = re.compile(ANSI_PATTERN).sub('', text)
-                return text
+                return text.strip('\r\n')
         else:
             return ''
 
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -57,7 +57,7 @@ class BaseEditMixin(object):
     _DEFAULT_MAX_LINES = 10
     _DEFAULT_MAX_WIDTH = 60
     _DEFAULT_MAX_HINT_LINES = 20
-    _DEFAULT_MAX_HINT_WIDTH = 120
+    _DEFAULT_MAX_HINT_WIDTH = 85
 
     def __init__(self):
         self.eol_chars = None
@@ -191,6 +191,7 @@ def _format_text(self, title=None, signature=None, text=None,
                 template += '<hr>'
 
         if signature:
+            signature = signature.strip('\r\n')
             template += BASE_TEMPLATE.format(
                 font_family=font_family,
                 size=text_size,
@@ -199,15 +200,32 @@ def _format_text(self, title=None, signature=None, text=None,
             )
 
         # Documentation/text handling
-        if not text:
-            text = '\n<i>No documentation available</i>'
+        if (text is None or not text.strip() or
+                text.strip() == '<no docstring>'):
+            text = '<i>No documentation available</i>'
+        else:
+            text = text.strip()
 
         if not with_html_format:
-            # Wrap text
-            lines = textwrap.wrap(text, width=max_width)
+            paragraphs = text.splitlines()
+            for paragraph in paragraphs:
+                # Wrap text
+                paragraph = textwrap.wrap(text, width=max_width)
+
+                # Remove empty lines at the beginning
+                paragraph = [l for l in paragraph if l.strip()]
+
+                # Merge paragraph text
+                paragraph = '\n'.join(paragraph)
 
-            # Remove empty lines at the beginning
-            lines = [l for l in lines if l.strip()]
+            # Join paragraphs and split in lines for max_lines check
+            paragraphs = '\n'.join(paragraphs)
+            paragraphs = paragraphs.strip('\r\n')
+            lines = paragraphs.splitlines()
+
+            # Check that the first line is not empty
+            if len(lines) > 0 and not lines[0].strip():
+                lines = lines[1:]
         else:
             lines = [l for l in text.split('\n') if l.strip()]
 
@@ -217,7 +235,7 @@ def _format_text(self, title=None, signature=None, text=None,
                 text = '\n'.join(lines[:max_lines]) + ' ...'
 
         text = text.replace('\n', '<br>')
-        if text_new_line:
+        if text_new_line and signature:
             text = '<br>' + text
 
         template += BASE_TEMPLATE.format(
@@ -368,7 +386,7 @@ def handle_sub(matchobj):
                 title = title_template
             new_signatures.append(title)
 
-        return '<br><br>'.join(new_signatures)
+        return '<br>'.join(new_signatures)
 
     def _check_signature_and_format(self, signature_or_text, parameter=None,
                                     max_width=_DEFAULT_MAX_WIDTH,
@@ -503,7 +521,9 @@ def show_tooltip(self, title=None, signature=None, text=None,
                      at_line=None, at_point=None, display_link=False,
                      max_lines=_DEFAULT_MAX_LINES,
                      max_width=_DEFAULT_MAX_WIDTH,
-                     cursor=None, with_html_format=False):
+                     cursor=None,
+                     with_html_format=False,
+                     text_new_line=True):
         """Show tooltip."""
         # Find position of calltip
         point = self._calculate_position(
@@ -521,7 +541,8 @@ def show_tooltip(self, title=None, signature=None, text=None,
             display_link=display_link,
             max_lines=max_lines,
             max_width=max_width,
-            with_html_format=with_html_format
+            with_html_format=with_html_format,
+            text_new_line=text_new_line
         )
 
         self._update_stylesheet(self.tooltip_widget)
@@ -531,7 +552,8 @@ def show_tooltip(self, title=None, signature=None, text=None,
 
     def show_hint(self, text, inspect_word, at_point,
                   max_lines=_DEFAULT_MAX_HINT_LINES,
-                  max_width=_DEFAULT_MAX_HINT_WIDTH):
+                  max_width=_DEFAULT_MAX_HINT_WIDTH,
+                  text_new_line=True):
         """Show code hint and crop text as needed."""
         # Check if signature and format
         res = self._check_signature_and_format(text)
@@ -546,7 +568,8 @@ def show_hint(self, text, inspect_word, at_point,
         self.show_tooltip(signature=html_signature, text=extra_text,
                           at_point=point, inspect_word=inspect_word,
                           display_link=True, max_lines=max_lines,
-                          max_width=max_width, cursor=cursor)
+                          max_width=max_width, cursor=cursor,
+                          text_new_line=text_new_line)
 
     def hide_tooltip(self):
         """


Test patch:
diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
--- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
+++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
@@ -152,8 +152,22 @@ def close_console():
 # =============================================================================
 @pytest.mark.slow
 @flaky(max_runs=3)
+@pytest.mark.parametrize(
+        "function,signature,documentation",
+        [("arange",
+          ["start", "stop"],
+          ["Return evenly spaced values within a given interval.<br>",
+           "returns an ndarray rather than a list.<br>"]),
+         ("vectorize",
+          ["pyfunc", "otype", "signature"],
+          ["Generalized function class.<br>",
+           "numpy array or a tuple of numpy ..."]),
+         ("absolute",
+          ["x", "/", "out"],
+          ["Parameters<br>", "x : array_like ..."])]
+        )
 @pytest.mark.skipif(sys.platform == 'darwin', reason="Times out on macOS")
-def test_get_calltips(ipyconsole, qtbot):
+def test_get_calltips(ipyconsole, qtbot, function, signature, documentation):
     """Test that calltips show the documentation."""
     shell = ipyconsole.get_current_shellwidget()
     control = shell._control
@@ -167,16 +181,22 @@ def test_get_calltips(ipyconsole, qtbot):
     # Write an object in the console that should generate a calltip
     # and wait for the kernel to send its response.
     with qtbot.waitSignal(shell.kernel_client.shell_channel.message_received):
-        qtbot.keyClicks(control, 'np.abs(')
+        qtbot.keyClicks(control, 'np.' + function + '(')
 
     # Wait a little bit for the calltip to appear
     qtbot.wait(500)
 
     # Assert we displayed a calltip
     assert control.calltip_widget.isVisible()
-    assert 'Calculate the absolute value' in control.calltip_widget.text()
+
     # Hide the calltip to avoid focus problems on Linux
     control.calltip_widget.hide()
+    
+    # Check spected elements for signature and documentation
+    for element in signature:
+        assert element in control.calltip_widget.text()
+    for element in documentation:
+        assert element in control.calltip_widget.text()
 
 
 @pytest.mark.slow