Repository: spyder-ide/spyder

Issue:
Wrap text in warnings and hover tips
<!--- **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

* [x] 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
* [x] Reproduced the issue after updating with ``conda update spyder`` (or ``pip``, if not using Anaconda)
* [x] Completed the **Problem Description**, **Steps to Reproduce** and **Version** sections below


## Problem Description
Hover tips and warnings extend beyond Spyder screen




Discussion:
@dalthviz, please take a look at this one. It shouldn't be that hard to solve.

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
@@ -25,6 +25,7 @@
 import re
 import sre_constants
 import sys
+import textwrap
 import time
 from unicodedata import category
 
@@ -2002,6 +2003,11 @@ 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)
+            if len(msg) > 1:
+                msg = '<br>'.join(msg) + '<br>'
+            else:
+                msg = '<br>'.join(msg)
             base_64 = ima.base64_from_icon(icons[sev], size, size)
             msglist.append(template.format(base_64, msg, src,
                                            code, size=size))
@@ -2012,6 +2018,7 @@ def show_code_analysis_results(self, line_number, block_data):
                 text='\n'.join(msglist),
                 title_color='#129625',
                 at_line=line_number,
+                with_html_format=True
             )
             self.highlight_line_warning(block_data)
 
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
@@ -42,47 +42,83 @@ def get_documentation(self, content):
         data = content.get('data', {})
         text = data.get('text/plain', '')
         if text:
-            text = re.compile(ANSI_PATTERN).sub('', text)
-            signature = self.get_signature(content).split('(')[-1]
-            if signature:
-                documentation = text.split(signature)
-                if len(documentation) > 1:
-                    return documentation[-1].split('Type:')[0]
+            if (self.language_name is not None
+                    and self.language_name == 'python'):
+                text = re.compile(ANSI_PATTERN).sub('', text)
+                signature = self.get_signature(content).split('(')[-1]
+
+                # Base value for the documentation
+                documentation = text.split('Docstring:')[-1].split('Type:')[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])
+
+                return documentation
             else:
-                return text.split('Docstring:')[-1].split('Type:')[0]
+                text = re.compile(ANSI_PATTERN).sub('', text)
+                return text
         else:
             return ''
 
+    def _get_signature(self, name, text):
+        """Get signature from text using a given function name."""
+        signature = ''
+        argspec = getargspecfromtext(text)
+        if argspec:
+            # This covers cases like np.abs, whose docstring is
+            # the same as np.absolute and because of that a proper
+            # signature can't be obtained correctly
+            signature = name + argspec
+        else:
+            signature = getsignaturefromtext(text, name)
+        return signature
+
     def get_signature(self, content):
         """Get signature from inspect reply content"""
         data = content.get('data', {})
         text = data.get('text/plain', '')
         if text:
-            text = ANSI_OR_SPECIAL_PATTERN.sub('', text)
-            self._control.current_prompt_pos = self._prompt_pos
-            line = self._control.get_current_line_to_cursor()
-            name = line[:-1].split('(')[-1]   # Take last token after a (
-            name = name.split('.')[-1]        # Then take last token after a .
-            # Clean name from invalid chars
-            try:
-                name = self.clean_invalid_var_chars(name)
-            except:
-                pass
-
-            docstring_text = text.split('Docstring:')[-1]
-            argspec = getargspecfromtext(docstring_text)
-            if argspec:
-                # This covers cases like np.abs, whose docstring is
-                # the same as np.absolute and because of that a proper
-                # signature can't be obtained correctly
-                signature = name + argspec
+            if (self.language_name is not None
+                    and self.language_name == 'python'):
+                self._control.current_prompt_pos = self._prompt_pos
+                line = self._control.get_current_line_to_cursor()
+                name = line[:-1].split('(')[-1]   # Take last token after a (
+                name = name.split('.')[-1]   # Then take last token after a .
+
+                # Clean name from invalid chars
+                try:
+                    name = self.clean_invalid_var_chars(name)
+                except Exception:
+                    pass
+
+                text = text.split('Docstring:')
+
+                # Try signature from text before 'Docstring:'
+                before_text = text[0]
+                before_signature = self._get_signature(name, before_text)
+
+                # Try signature from text after 'Docstring:'
+                after_text = text[-1]
+                after_signature = self._get_signature(name, after_text)
+
+                # Stay with the longest signature
+                if len(before_signature) > len(after_signature):
+                    signature = before_signature
+                else:
+                    signature = after_signature
+
+                # Prevent special characters. Applied here to ensure
+                # recognizing the signature in the logic above.
+                signature = ANSI_OR_SPECIAL_PATTERN.sub('', signature)
+
+                return signature
             else:
-                signature = getsignaturefromtext(docstring_text, name)
-
-            # Remove docstring for uniformity with editor
-            signature = signature.split('Docstring:')[0]
-
-            return signature
+                text = re.compile(ANSI_PATTERN).sub('', text)
+                return text
         else:
             return ''
 
@@ -147,9 +183,12 @@ def _handle_inspect_reply(self, rep):
             if content.get('status') == 'ok' and content.get('found', False):
                 signature = self.get_signature(content)
                 documentation = self.get_documentation(content)
+                new_line = (self.language_name is not None
+                            and self.language_name == 'python')
                 self._control.show_calltip(
                     signature,
                     documentation=documentation,
                     language=self.language_name,
-                    max_lines=7
+                    max_lines=7,
+                    text_new_line=new_line
                 )
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -54,6 +54,10 @@ class BaseEditMixin(object):
     _CHAR_HIGHLIGHT_COLOR = 'red'
     _DEFAULT_TEXT_COLOR = '#999999'
     _DEFAULT_LANGUAGE = 'python'
+    _DEFAULT_MAX_LINES = 10
+    _DEFAULT_MAX_WIDTH = 60
+    _DEFAULT_MAX_HINT_LINES = 20
+    _DEFAULT_MAX_HINT_WIDTH = 120
 
     def __init__(self):
         self.eol_chars = None
@@ -142,7 +146,8 @@ def _get_inspect_shortcut(self):
 
     def _format_text(self, title=None, signature=None, text=None,
                      inspect_word=None, title_color=None, max_lines=None,
-                     display_link=False):
+                     max_width=_DEFAULT_MAX_WIDTH, display_link=False,
+                     text_new_line=False,  with_html_format=False):
         """
         Create HTML template for calltips and tooltips.
 
@@ -195,18 +200,26 @@ def _format_text(self, title=None, signature=None, text=None,
 
         # Documentation/text handling
         if not text:
-            text = '\n<i>No documentation available</i>\n'
+            text = '\n<i>No documentation available</i>'
 
-        # Remove empty lines at the beginning
-        lines = [l for l in text.split('\n') if l.strip()]
+        if not with_html_format:
+            # Wrap text
+            lines = textwrap.wrap(text, width=max_width)
+
+            # Remove empty lines at the beginning
+            lines = [l for l in lines if l.strip()]
+        else:
+            lines = [l for l in text.split('\n') if l.strip()]
 
         # Limit max number of text displayed
         if max_lines:
-            lines = text.split('\n')
             if len(lines) > max_lines:
                 text = '\n'.join(lines[:max_lines]) + ' ...'
 
         text = text.replace('\n', '<br>')
+        if text_new_line:
+            text = '<br>' + text
+
         template += BASE_TEMPLATE.format(
             font_family=font_family,
             size=text_size,
@@ -263,6 +276,7 @@ def _format_text(self, title=None, signature=None, text=None,
         return template
 
     def _format_signature(self, signatures, parameter=None,
+                          max_width=_DEFAULT_MAX_WIDTH,
                           parameter_color=_PARAMETER_HIGHLIGHT_COLOR,
                           char_color=_CHAR_HIGHLIGHT_COLOR,
                           language=_DEFAULT_LANGUAGE):
@@ -318,7 +332,8 @@ def handle_sub(matchobj):
             formatted_lines = []
             name = signature.split('(')[0]
             indent = ' ' * (len(name) + 1)
-            rows = textwrap.wrap(signature, width=60, subsequent_indent=indent)
+            rows = textwrap.wrap(signature, width=max_width,
+                                 subsequent_indent=indent)
             for row in rows:
                 if parameter:
                     # Add template to highlight the active parameter
@@ -356,6 +371,7 @@ def handle_sub(matchobj):
         return '<br><br>'.join(new_signatures)
 
     def _check_signature_and_format(self, signature_or_text, parameter=None,
+                                    max_width=_DEFAULT_MAX_WIDTH,
                                     language=_DEFAULT_LANGUAGE):
         """
         LSP hints might provide docstrings instead of signatures.
@@ -406,6 +422,7 @@ def _check_signature_and_format(self, signature_or_text, parameter=None,
                 new_signature = self._format_signature(
                     signatures=signature,
                     parameter=parameter,
+                    max_width=max_width
                 )
         elif has_multisignature:
             signature = signature_or_text.replace(name_plus_char,
@@ -421,6 +438,7 @@ def _check_signature_and_format(self, signature_or_text, parameter=None,
             new_signature = self._format_signature(
                 signatures=signatures,
                 parameter=parameter,
+                max_width=max_width
             )
             extra_text = None
         else:
@@ -430,7 +448,8 @@ def _check_signature_and_format(self, signature_or_text, parameter=None,
         return new_signature, extra_text, inspect_word
 
     def show_calltip(self, signature, parameter=None, documentation=None,
-                     language=_DEFAULT_LANGUAGE, max_lines=10):
+                     language=_DEFAULT_LANGUAGE, max_lines=_DEFAULT_MAX_LINES,
+                     max_width=_DEFAULT_MAX_WIDTH, text_new_line=True):
         """
         Show calltip.
 
@@ -460,7 +479,8 @@ def show_calltip(self, signature, parameter=None, documentation=None,
 
         # Format
         res = self._check_signature_and_format(signature, parameter,
-                                               language=language)
+                                               language=language,
+                                               max_width=max_width)
         new_signature, text, inspect_word = res
         text = self._format_text(
             signature=new_signature,
@@ -468,6 +488,8 @@ def show_calltip(self, signature, parameter=None, documentation=None,
             display_link=False,
             text=documentation,
             max_lines=max_lines,
+            max_width=max_width,
+            text_new_line=text_new_line
         )
 
         self._update_stylesheet(self.calltip_widget)
@@ -479,7 +501,9 @@ def show_calltip(self, signature, parameter=None, documentation=None,
     def show_tooltip(self, title=None, signature=None, text=None,
                      inspect_word=None, title_color=_DEFAULT_TITLE_COLOR,
                      at_line=None, at_point=None, display_link=False,
-                     max_lines=10, cursor=None):
+                     max_lines=_DEFAULT_MAX_LINES,
+                     max_width=_DEFAULT_MAX_WIDTH,
+                     cursor=None, with_html_format=False):
         """Show tooltip."""
         # Find position of calltip
         point = self._calculate_position(
@@ -496,6 +520,8 @@ def show_tooltip(self, title=None, signature=None, text=None,
             inspect_word=inspect_word,
             display_link=display_link,
             max_lines=max_lines,
+            max_width=max_width,
+            with_html_format=with_html_format
         )
 
         self._update_stylesheet(self.tooltip_widget)
@@ -503,7 +529,9 @@ def show_tooltip(self, title=None, signature=None, text=None,
         # Display tooltip
         self.tooltip_widget.show_tip(point, tiptext, cursor=cursor)
 
-    def show_hint(self, text, inspect_word, at_point):
+    def show_hint(self, text, inspect_word, at_point,
+                  max_lines=_DEFAULT_MAX_HINT_LINES,
+                  max_width=_DEFAULT_MAX_HINT_WIDTH):
         """Show code hint and crop text as needed."""
         # Check if signature and format
         res = self._check_signature_and_format(text)
@@ -517,7 +545,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=10, cursor=cursor)
+                          display_link=True, max_lines=max_lines,
+                          max_width=max_width, cursor=cursor)
 
     def hide_tooltip(self):
         """


Test patch: