Repository: sublimelsp/LSP

Issue:
Non-ideal rendering of links in text content in documentation popup
Link includes `>` and is not working:
![Screenshot 2020-05-13 at 20 07 52](https://user-images.githubusercontent.com/153197/81848458-72436680-9555-11ea-87b5-35cb528b2f04.png)


Discussion:

Resolving patch:
diff --git a/plugin/completion.py b/plugin/completion.py
--- a/plugin/completion.py
+++ b/plugin/completion.py
@@ -67,12 +67,12 @@ def get_content(self, documentation: str, detail: str) -> str:
         return content
 
     def show_popup(self, minihtml_content: str) -> None:
+        viewport_width = self.view.viewport_extent()[0]
         mdpopups.show_popup(
             self.view,
             minihtml_content,
             flags=sublime.COOPERATE_WITH_AUTO_COMPLETE,
-            max_width=480,
-            max_height=410,
+            max_width=viewport_width,
             allow_code_wrap=True,
             on_navigate=self.on_navigate
         )
diff --git a/plugin/core/views.py b/plugin/core/views.py
--- a/plugin/core/views.py
+++ b/plugin/core/views.py
@@ -1,4 +1,3 @@
-import html
 import linecache
 import mdpopups
 import re
@@ -193,19 +192,34 @@ def minihtml(view: sublime.View, content: Union[str, dict]) -> str:
         return ''
 
 
-def text2html(content: str) -> str:
-    content = html.escape(content).replace('\n', '<br>').replace('\t', '&nbsp;&nbsp;&nbsp;&nbsp;')
+REPLACEMENT_MAP = {
+    "&": "&amp;",
+    "<": "&lt;",
+    ">": "&gt;",
+    "\t": 4 * "&nbsp;",
+    "\n": "<br>",
+    "\xa0": "&nbsp;",  # non-breaking space
+    "\xc2": "&nbsp;",  # control character
+}
+
+PATTERNS = [
+    r'(?P<special>[{}])'.format(''.join(REPLACEMENT_MAP.keys())),
+    r'(?P<url>https?://(?:[\w\d:#@%/;$()~_?\+\-=\\\.&](?:#!)?)*)',
+    r'(?P<multispace> {2,})',
+]
 
-    def replace_nbsp(match: Any) -> str:
-        spaces = match.group(0)
-        return "&nbsp;" * len(spaces)
+REPLACEMENT_RE = re.compile('|'.join(PATTERNS), flags=re.IGNORECASE)
 
-    # if there are 2 or more spaces, replace them with &nbsp;
-    content = re.sub(r"( {2,})", replace_nbsp, content)
 
-    def replace_url_with_link(match: Any) -> str:
-        url = match.group(0)
+def _replace_match(match: Any) -> str:
+    special_match = match.group('special')
+    if special_match:
+        return REPLACEMENT_MAP[special_match]
+    url = match.group('url')
+    if url:
         return "<a href='{}'>{}</a>".format(url, url)
+    return len(match.group('multispace')) * '&nbsp;'
 
-    FIND_URL = re.compile(r'(https?://(?:[\w\d:#@%/;$()~_?\+\-=\\\.&](?:#!)?)*)', flags=re.IGNORECASE)
-    return re.sub(FIND_URL, replace_url_with_link, content)
+
+def text2html(content: str) -> str:
+    return re.sub(REPLACEMENT_RE, _replace_match, content)


Test patch:
diff --git a/tests/test_views.py b/tests/test_views.py
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -117,6 +117,9 @@ def test_point_to_offset_utf16(self) -> None:
     def test_text2html_replaces_tabs_with_br(self) -> None:
         self.assertEqual(text2html("Hello,\t world "), "Hello,&nbsp;&nbsp;&nbsp;&nbsp; world ")
 
+    def test_text2html_non_breaking_space_and_control_char_with_entity(self) -> None:
+        self.assertEqual(text2html("no\xc2\xa0breaks"), "no&nbsp;&nbsp;breaks")
+
     def test_text2html_replaces_two_or_more_spaces_with_nbsp(self) -> None:
         content = " One  Two   Three One    Four"
         expect = " One&nbsp;&nbsp;Two&nbsp;&nbsp;&nbsp;Three One&nbsp;&nbsp;&nbsp;&nbsp;Four"
@@ -126,9 +129,25 @@ def test_text2html_does_not_replace_one_space_with_nbsp(self) -> None:
         content = " John has one apple "
         self.assertEqual(text2html(content), content)
 
-    def test_text2html_replaces_tabs_parses_links(self) -> None:
-        content = "Here is a url https://github.com/sublimelsp/LSP\n" \
-            "Issues: https://github.com/sublimelsp/LSP/issues ."
-        expect = "Here is a url <a href='https://github.com/sublimelsp/LSP'>https://github.com/sublimelsp/LSP</a><br>" \
-            "Issues: <a href='https://github.com/sublimelsp/LSP/issues'>https://github.com/sublimelsp/LSP/issues</a> ."
+    def test_text2html_replaces_newlines_with_br(self) -> None:
+        self.assertEqual(text2html("a\nb"), "a<br>b")
+
+    def test_text2html_parses_link_simple(self) -> None:
+        content = "https://github.com/sublimelsp/LSP"
+        expect = "<a href='https://github.com/sublimelsp/LSP'>https://github.com/sublimelsp/LSP</a>"
+        self.assertEqual(text2html(content), expect)
+
+    def test_text2html_parses_link_in_angle_brackets(self) -> None:
+        content = "<https://github.com/sublimelsp/LSP>"
+        expect = "&lt;<a href='https://github.com/sublimelsp/LSP'>https://github.com/sublimelsp/LSP</a>&gt;"
+        self.assertEqual(text2html(content), expect)
+
+    def test_text2html_parses_link_in_double_quotes(self) -> None:
+        content = "\"https://github.com/sublimelsp/LSP\""
+        expect = "\"<a href='https://github.com/sublimelsp/LSP'>https://github.com/sublimelsp/LSP</a>\""
+        self.assertEqual(text2html(content), expect)
+
+    def test_text2html_parses_link_in_single_quotes(self) -> None:
+        content = "'https://github.com/sublimelsp/LSP'"
+        expect = "'<a href='https://github.com/sublimelsp/LSP'>https://github.com/sublimelsp/LSP</a>'"
         self.assertEqual(text2html(content), expect)