Repository: sublimelsp/LSP

Issue:
The signature help "parentheses" detection needs more work
![Screenshot from 2020-04-29 23-05-29](https://user-images.githubusercontent.com/2431823/80647025-12858f80-8a6e-11ea-88b9-8cec2005d75c.png)

We've seen that for e.g. a dockerfile language server the parens detection doesn't work at all.
For gopls, parentheses are used after the function ends. The right decision might be to abandon this parsing. There's nothing in the spec that guarantees us parentheses of any kind.


Discussion:
Here are some example signature responses (for my own information mostly).
```json
{
    "signatures": [
        {
            "label": "toggleExpand(memberId: number): void",
            "parameters": [
                {
                    "documentation": "",
                    "label": "memberId: number"
                }
            ]
        }
    ],
    "activeParameter": 0,
    "activeSignature": 0
}
```

```json
{
    "signatures": [
        {
            "documentation": "Copy new files and directories to the image's filesystem.",
            "label": "COPY [flags] source ... dest",
            "parameters": [
                {
                    "documentation": "Optional flags to configure this instruction.",
                    "label": "[flags]"
                },
                {
                    "documentation": "The resource to copy.",
                    "label": "source"
                },
                {
                    "documentation": "The resource to copy.",
                    "label": "..."
                },
                {
                    "documentation": "The name of the destination file or folder.",
                    "label": "dest"
                }
            ]
        }
    ],
    "activeParameter": 1,
    "activeSignature": 0
}
```
I haven't checked what we are parsing them for. Is that to highlight active parameter?
there's an issue i noticed before (i don't know if it's still there): when you are inside the parameter section of a function and and click "(" (e.g to create a tuple), signatureHelp is invoked.
```python
f(1, 2, (|))
```
> there's an issue i noticed before (i don't know if it's still there): when you are inside the parameter section of a function and and click "(" (e.g to create a tuple), signatureHelp is invoked.

Yes, there's not much we can do about that. The signature help feature works with trigger characters. Trigger characters are blunt and don't know anything about ST scopes.
Maybe we can add a customization point in AbstractPlugin for this
That sounds fine to me.

What form would the customization have? A regexp (or a series of)?
Just another selector in the client config would suffice I think?

Resolving patch:
diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py
--- a/plugin/core/protocol.py
+++ b/plugin/core/protocol.py
@@ -10,7 +10,7 @@
 TextDocumentSyncKindIncremental = 2
 
 
-class DiagnosticSeverity(object):
+class DiagnosticSeverity:
     Error = 1
     Warning = 2
     Information = 3
@@ -26,13 +26,19 @@ class InsertTextFormat:
     Snippet = 2
 
 
-class DocumentHighlightKind(object):
+class DocumentHighlightKind:
     Unknown = 0
     Text = 1
     Read = 2
     Write = 3
 
 
+class SignatureHelpTriggerKind:
+    Invoked = 1
+    TriggerCharacter = 2
+    ContentChange = 3
+
+
 ExecuteCommandParams = TypedDict('ExecuteCommandParams', {
     'command': str,
     'arguments': Optional[List[Any]],
@@ -56,6 +62,34 @@ class DocumentHighlightKind(object):
 })
 
 
+ParameterInformation = TypedDict('ParameterInformation', {
+    'label': Union[str, List[int]],
+    'documentation': Union[str, Dict[str, str]]
+}, total=False)
+
+
+SignatureInformation = TypedDict('SignatureInformation', {
+    'label': str,
+    'documentation': Union[str, Dict[str, str]],
+    'parameters': List[ParameterInformation]
+}, total=False)
+
+
+SignatureHelp = TypedDict('SignatureHelp', {
+    'signatures': List[SignatureInformation],
+    'activeSignature': int,
+    'activeParameter': int,
+}, total=False)
+
+
+SignatureHelpContext = TypedDict('SignatureHelpContext', {
+    'triggerKind': int,
+    'triggerCharacter': str,
+    'isRetrigger': bool,
+    'activeSignatureHelp': SignatureHelp
+}, total=False)
+
+
 class Request:
 
     __slots__ = ('method', 'params', 'view')
diff --git a/plugin/core/signature_help.py b/plugin/core/signature_help.py
--- a/plugin/core/signature_help.py
+++ b/plugin/core/signature_help.py
@@ -1,236 +1,166 @@
-import html
 from .logging import debug
-from .typing import Tuple, Optional, List, Protocol, Union, Dict
-
-
-class ScopeRenderer(Protocol):
-
-    def function(self, content: str, escape: bool = True) -> str:
-        ...
-
-    def punctuation(self, content: str) -> str:
-        ...
-
-    def parameter(self, content: str, emphasize: bool = False) -> str:
-        ...
-
-    def markup(self, content: Union[str, Dict[str, str]]) -> str:
-        ...
-
-
-class ParameterInformation(object):
-
-    def __init__(self, label: Optional[str], label_range: Optional[Tuple[int, int]],
-                 documentation: Optional[str]) -> None:
-        self.label = label
-        self.range = label_range
-        self.documentation = documentation
-
-
-class SignatureInformation(object):
-
-    def __init__(self, label: str, documentation: Optional[str], paren_bounds: Tuple[int, int],
-                 parameters: List[ParameterInformation] = []) -> None:
-        self.label = label
-        self.documentation = documentation
-        self.parameters = parameters
-        [self.open_paren_index, self.close_paren_index] = paren_bounds
-
-
-def parse_signature_label(signature_label: str, parameters: List[ParameterInformation]) -> Tuple[int, int]:
-    current_index = -1
-
-    # assumption - if there are parens, the first paren starts arguments
-    # (note: self argument in pyls-returned method calls not included in params!)
-    # if no parens, start detecting from first parameter instead.
-    # if paren, extract and highlight
-    open_paren_index = signature_label.find('(')
-    params_start_index = open_paren_index + 1
-    current_index = params_start_index
-    has_commas = signature_label.find(',') > -1
-
-    for parameter in parameters:
-
-        if parameter.label:
-            range_start = signature_label.find(parameter.label, current_index)
-            if range_start > -1:
-                range_end = range_start + len(parameter.label)
-                parameter.range = (range_start, range_end)
-        elif parameter.range:
-            parameter.label = signature_label[parameter.range[0]:parameter.range[1]]
-
-        if parameter.range:
-            current_index = parameter.range[1]
-
-        # if server said param was "x" while signature was "f(x: str, ...)"
-        # then we should fast-forward to avoid matching the next parameter too early.
-        if has_commas:
-            try:
-                if signature_label[current_index] != ',':
-                    next_comma_index = signature_label.find(',', current_index)
-                    if next_comma_index > -1:
-                        # print('Found {} instead of comma at index {}, fast-forwarded to {}'.format(
-                        #     signature_label[current_index], current_index, next_comma_index))
-                        current_index = next_comma_index
-            except IndexError:
-                # bail
-                return (-1, -1)
-
-    close_paren_index = signature_label.find(')', current_index)
-
-    return open_paren_index, close_paren_index
-
-
-def parse_parameter_information(parameter: dict) -> ParameterInformation:
-    label_or_range = parameter['label']
-    label_range = None
-    label = None
-    if isinstance(label_or_range, str):
-        label = label_or_range
-    else:
-        label_range = label_or_range
-    return ParameterInformation(label, label_range, parameter.get('documentation'))
-
-
-def parse_signature_information(signature: dict) -> SignatureInformation:
-    signature_label = signature['label']
-    param_infos = []  # type: 'List[ParameterInformation]'
-    parameters = signature.get('parameters')
-    paren_bounds = (-1, -1)
-    if parameters:
-        param_infos = list(parse_parameter_information(param) for param in parameters)
-        paren_bounds = parse_signature_label(signature_label, param_infos)
-
-    return SignatureInformation(signature_label, signature.get('documentation'), paren_bounds, param_infos)
-
-
-class SignatureHelp(object):
+from .protocol import SignatureHelp
+from .protocol import SignatureHelpContext
+from .protocol import SignatureInformation
+from .typing import Optional, List
+from .views import FORMAT_MARKUP_CONTENT
+from .views import FORMAT_STRING
+from .views import minihtml
+import html
+import sublime
+
+
+class SigHelp:
+    """
+    A quasi state-machine object that maintains which signature (a.k.a. overload) is active. The active signature is
+    determined by what the end-user is doing.
+    """
+
+    def __init__(self, state: SignatureHelp) -> None:
+        self._state = state
+        self._signatures = self._state["signatures"]
+        self._active_signature_index = self._state.get("activeSignature") or 0
+        self._active_parameter_index = self._state.get("activeParameter") or 0
+
+    @classmethod
+    def from_lsp(cls, sighelp: Optional[SignatureHelp]) -> "Optional[SigHelp]":
+        """Create a SigHelp state object from a server's response to textDocument/signatureHelp."""
+        if sighelp is None or not sighelp.get("signatures"):
+            return None
+        return cls(sighelp)
+
+    def render(self, view: sublime.View) -> str:
+        """Render the signature help content as minihtml."""
+        try:
+            signature = self._signatures[self._active_signature_index]
+        except IndexError:
+            return ""
+        formatted = []  # type: List[str]
+        intro = self._render_intro()
+        if intro:
+            formatted.append(intro)
+        formatted.extend(self._render_label(view, signature))
+        formatted.extend(self._render_docs(view, signature))
+        return "".join(formatted)
+
+    def context(self, trigger_kind: int, trigger_character: str, is_retrigger: bool) -> SignatureHelpContext:
+        """
+        Extract the state out of this state machine to send back to the language server.
+
+        XXX: Currently unused. Revisit this some time in the future.
+        """
+        self._state["activeSignature"] = self._active_signature_index
+        return {
+            "triggerKind": trigger_kind,
+            "triggerCharacter": trigger_character,
+            "isRetrigger": is_retrigger,
+            "activeSignatureHelp": self._state
+        }
 
-    def __init__(self, signatures: List[SignatureInformation],
-                 active_signature: int = 0, active_parameter: int = 0) -> None:
-        self._signatures = signatures
-        self._active_signature_index = active_signature
-        self._active_parameter_index = active_parameter
+    def has_multiple_signatures(self) -> bool:
+        """Does the current signature help state contain more than one overload?"""
+        return len(self._signatures) > 1
 
-    def build_popup_content(self, renderer: ScopeRenderer) -> str:
-        parameter_documentation = None  # type: Optional[str]
+    def select_signature(self, direction: int) -> None:
+        """Increment or decrement the active overload; purely chosen by the end-user."""
+        new_index = self._active_signature_index + direction
+        self._active_signature_index = max(0, min(new_index, len(self._signatures) - 1))
 
-        formatted = []
+    def active_signature(self) -> SignatureInformation:
+        return self._signatures[self._active_signature_index]
 
+    def _render_intro(self) -> Optional[str]:
         if len(self._signatures) > 1:
-            formatted.append(self._build_overload_selector())
-
-        signature = self._signatures[self._active_signature_index]  # type: SignatureInformation
+            fmt = '<p><div style="font-size: 0.9rem"><b>{}</b> of <b>{}</b> overloads ' + \
+                  "(use ↑ ↓ to navigate, press Esc to hide):</div></p>"
+            return fmt.format(
+                self._active_signature_index + 1,
+                len(self._signatures),
+            )
+        return None
 
-        # Write the active signature and give special treatment to the active parameter (if found).
+    def _render_label(self, view: sublime.View, signature: SignatureInformation) -> List[str]:
+        formatted = []  # type: List[str]
         # Note that this <div> class and the extra <pre> are copied from mdpopups' HTML output. When mdpopups changes
         # its output style, we must update this literal string accordingly.
         formatted.append('<div class="highlight"><pre>')
-        formatted.append(render_signature_label(renderer, signature, self._active_parameter_index))
+        label = signature["label"]
+        parameters = signature.get("parameters")
+        if parameters:
+            prev, start, end = 0, 0, 0
+            for i, param in enumerate(parameters):
+                rawlabel = param["label"]
+                if isinstance(rawlabel, list):
+                    # TODO: UTF-16 offsets
+                    start, end = rawlabel[0], rawlabel[1]
+                else:
+                    # Note that this route is from an earlier spec. It is a bad way of doing things because this
+                    # route relies on the client being smart enough to figure where the parameter is inside of
+                    # the signature label. The above case where the label is a tuple of (start, end) positions is much
+                    # more robust.
+                    start = label[prev:].find(rawlabel)
+                    if start == -1:
+                        debug("no match found for {}".format(rawlabel))
+                        continue
+                    start += prev
+                    end = start + len(rawlabel)
+                if prev < start:
+                    formatted.append(_function(view, label[prev:start]))
+                formatted.append(_parameter(view, label[start:end], i == self._active_parameter_index))
+                prev = end
+            if end < len(label):
+                formatted.append(_function(view, label[end:]))
+        else:
+            formatted.append(_function(view, label))
         formatted.append("</pre></div>")
+        return formatted
+
+    def _render_docs(self, view: sublime.View, signature: SignatureInformation) -> List[str]:
+        formatted = []  # type: List[str]
+        docs = self._parameter_documentation(view, signature)
+        if docs:
+            formatted.append(docs)
+        docs = _signature_documentation(view, signature)
+        if docs:
+            if formatted:
+                formatted.append("<hr/>")
+            formatted.append('<div style="font-size: 0.9rem">')
+            formatted.append(docs)
+            formatted.append('</div>')
+        return formatted
+
+    def _parameter_documentation(self, view: sublime.View, signature: SignatureInformation) -> Optional[str]:
+        parameters = signature.get("parameters")
+        if not parameters:
+            return None
+        try:
+            parameter = parameters[self._active_parameter_index]
+        except IndexError:
+            return None
+        documentation = parameter.get("documentation")
+        if documentation:
+            return minihtml(view, documentation, allowed_formats=FORMAT_STRING | FORMAT_MARKUP_CONTENT)
+        return None
 
-        if signature.documentation:
-            formatted.append("<p>{}</p>".format(renderer.markup(signature.documentation)))
 
-        if signature.parameters and self._active_parameter_index in range(0, len(signature.parameters)):
-            parameter = signature.parameters[self._active_parameter_index]
-            parameter_label = html.escape(parameter.label, quote=False) if parameter.label else ""
-            parameter_documentation = parameter.documentation
-            if parameter_documentation:
-                formatted.append("<p><b>{}</b>: {}</p>".format(
-                    parameter_label,
-                    renderer.markup(parameter_documentation)))
+def _function(view: sublime.View, content: str) -> str:
+    return _wrap_with_scope_style(view, content, "entity.name.function", False)
 
-        return "\n".join(formatted)
 
-    def has_multiple_signatures(self) -> bool:
-        return len(self._signatures) > 1
+def _parameter(view: sublime.View, content: str, emphasize: bool) -> str:
+    return _wrap_with_scope_style(view, content, "variable.parameter", emphasize)
 
-    def select_signature(self, direction: int) -> None:
-        new_index = self._active_signature_index + direction
 
-        # clamp signature index
-        self._active_signature_index = max(0, min(new_index, len(self._signatures) - 1))
-
-    def active_signature(self) -> SignatureInformation:
-        return self._signatures[self._active_signature_index]
-
-    def _build_overload_selector(self) -> str:
-        return "**{}** of **{}** overloads (use the ↑ ↓ keys to navigate, press ESC to hide):\n".format(
-            str(self._active_signature_index + 1), str(len(self._signatures)))
+def _wrap_with_scope_style(view: sublime.View, content: str, scope: str, emphasize: bool) -> str:
+    return '<span style="color: {}{}">{}</span>'.format(
+        view.style_for_scope(scope)["foreground"],
+        '; font-weight: bold; text-decoration: underline' if emphasize else '',
+        html.escape(content, quote=False)
+    )
 
 
-def create_signature_help(response: Optional[dict]) -> Optional[SignatureHelp]:
-    if response is None:
-        return None
-    signatures = response.get("signatures") or []
-    signatures = [parse_signature_information(signature) for signature in signatures]
-    if signatures:
-        active_signature = response.get("activeSignature", -1)
-        active_parameter = response.get("activeParameter", -1)
-        if not 0 <= active_signature < len(signatures):
-            debug("activeSignature {} not a valid index for signatures length {}".format(
-                active_signature, len(signatures)))
-            active_signature = 0
-        return SignatureHelp(signatures, active_signature, active_parameter)
+def _signature_documentation(view: sublime.View, signature: SignatureInformation) -> Optional[str]:
+    documentation = signature.get("documentation")
+    if documentation:
+        return minihtml(view, documentation, allowed_formats=FORMAT_STRING | FORMAT_MARKUP_CONTENT)
     return None
-
-
-def find_params_to_split_at(label: str) -> List[int]:
-    max_length = 80
-    params_to_newline_before = []  # type: List[int]
-    if len(label) > max_length and '\n' not in label:
-        param_index = 0
-        last_comma_offset = 0
-        line_offset = 0
-        while True:
-            comma_offset = label.find(',', last_comma_offset)
-            if comma_offset == -1:
-                if len(label) - line_offset > max_length:
-                    # If no more commas, but remainder is too long, add a split
-                    params_to_newline_before.append(param_index)
-                break
-            param_index += 1
-            if comma_offset - line_offset > max_length:
-                params_to_newline_before.append(param_index)
-                line_offset = last_comma_offset
-                if max_length == 80:
-                    max_length = 76  # account for 4-space indent.
-            last_comma_offset = comma_offset + 1
-    return params_to_newline_before
-
-
-def render_signature_label(renderer: ScopeRenderer, sig_info: SignatureInformation,
-                           active_parameter_index: int = -1) -> str:
-    if sig_info.parameters:
-        label = sig_info.label
-        params_to_newline_before = find_params_to_split_at(label)
-
-        # replace with styled spans in reverse order
-
-        if sig_info.close_paren_index > -1:
-            start = sig_info.close_paren_index
-            end = start+1
-            label = label[:start] + renderer.punctuation(label[start:end]) + html.escape(label[end:], quote=False)
-
-        max_param_index = len(sig_info.parameters) - 1
-        for reverse_index, param in enumerate(reversed(sig_info.parameters)):
-            if param.range:
-                start, end = param.range
-                forward_index = max_param_index - reverse_index
-                is_current = active_parameter_index == forward_index
-                rendered_param = renderer.parameter(content=label[start:end], emphasize=is_current)
-                maybe_newline = "<br>&nbsp;&nbsp;&nbsp;&nbsp;" if forward_index in params_to_newline_before else ""
-                label = label[:start] + maybe_newline + rendered_param + label[end:]
-
-                # todo: highlight commas between parameters as punctuation.
-
-        if sig_info.open_paren_index > -1:
-            start = sig_info.open_paren_index
-            end = start+1
-            label = html.escape(label[:start], quote=False) + renderer.punctuation(label[start:end]) + label[end:]
-
-        # todo: only render up to first parameter as function scope.
-        return renderer.function(label, escape=False)
-    else:
-        return renderer.function(sig_info.label)
diff --git a/plugin/documents.py b/plugin/documents.py
--- a/plugin/documents.py
+++ b/plugin/documents.py
@@ -8,12 +8,12 @@
 from .core.protocol import DocumentHighlightKind
 from .core.protocol import Range
 from .core.protocol import Request
+from .core.protocol import SignatureHelp
 from .core.registry import best_session
 from .core.registry import windows
 from .core.sessions import Session
 from .core.settings import userprefs
-from .core.signature_help import create_signature_help
-from .core.signature_help import SignatureHelp
+from .core.signature_help import SigHelp
 from .core.types import basescope2languageid
 from .core.types import debounced
 from .core.types import FEATURES_TIMEOUT
@@ -21,11 +21,8 @@
 from .core.views import DIAGNOSTIC_SEVERITY
 from .core.views import document_color_params
 from .core.views import format_completion
-from .core.views import FORMAT_MARKUP_CONTENT
-from .core.views import FORMAT_STRING
 from .core.views import lsp_color_to_phantom
 from .core.views import make_command_link
-from .core.views import minihtml
 from .core.views import range_to_region
 from .core.views import region_to_range
 from .core.views import text_document_position_params
@@ -37,7 +34,6 @@
 from .session_view import SessionView
 from weakref import WeakSet
 from weakref import WeakValueDictionary
-import html
 import mdpopups
 import sublime
 import sublime_plugin
@@ -67,32 +63,6 @@ def previous_non_whitespace_char(view: sublime.View, pt: int) -> str:
     return prev
 
 
-class ColorSchemeScopeRenderer:
-    def __init__(self, view: sublime.View) -> None:
-        self._scope_styles = {}  # type: dict
-        self._view = view
-        for scope in ["entity.name.function", "variable.parameter", "punctuation"]:
-            self._scope_styles[scope] = mdpopups.scope2style(view, scope)
-
-    def function(self, content: str, escape: bool = True) -> str:
-        return self._wrap_with_scope_style(content, "entity.name.function", escape=escape)
-
-    def punctuation(self, content: str) -> str:
-        return self._wrap_with_scope_style(content, "punctuation")
-
-    def parameter(self, content: str, emphasize: bool = False) -> str:
-        return self._wrap_with_scope_style(content, "variable.parameter", emphasize)
-
-    def markup(self, content: Union[str, Dict[str, str]]) -> str:
-        return minihtml(self._view, content, allowed_formats=FORMAT_STRING | FORMAT_MARKUP_CONTENT)
-
-    def _wrap_with_scope_style(self, content: str, scope: str, emphasize: bool = False, escape: bool = True) -> str:
-        color = self._scope_styles[scope]["color"]
-        additional_styles = 'font-weight: bold; text-decoration: underline;' if emphasize else ''
-        content = html.escape(content, quote=False) if escape else content
-        return '<span style="color: {};{}">{}</span>'.format(color, additional_styles, content)
-
-
 class TextChangeListener(sublime_plugin.TextChangeListener):
 
     ids_to_listeners = WeakValueDictionary()  # type: WeakValueDictionary[int, TextChangeListener]
@@ -157,8 +127,7 @@ def __init__(self, view: sublime.View) -> None:
         self._session_views = {}  # type: Dict[str, SessionView]
         self._stored_region = sublime.Region(-1, -1)
         self._color_phantoms = sublime.PhantomSet(self.view, "lsp_color")
-        self._sighelp = None  # type: Optional[SignatureHelp]
-        self._sighelp_renderer = ColorSchemeScopeRenderer(self.view)
+        self._sighelp = None  # type: Optional[SigHelp]
         self._language_id = ""
         self._registered = False
 
@@ -318,7 +287,7 @@ def on_query_context(self, key: str, operator: int, operand: Any, match_all: boo
             elif self._sighelp and self._sighelp.has_multiple_signatures() and not self.view.is_auto_complete_visible():
                 # We use the "operand" for the number -1 or +1. See the keybindings.
                 self._sighelp.select_signature(operand)
-                self._update_sighelp_popup(self._sighelp.build_popup_content(self._sighelp_renderer))
+                self._update_sighelp_popup(self._sighelp.render(self.view))
                 return True  # We handled this keybinding.
         return False
 
@@ -374,10 +343,10 @@ def _do_signature_help(self, manual: bool) -> None:
             self.view.hide_popup()
             self._sighelp = None
 
-    def _on_signature_help(self, response: Optional[Dict], point: int) -> None:
-        self._sighelp = create_signature_help(response)
+    def _on_signature_help(self, response: Optional[SignatureHelp], point: int) -> None:
+        self._sighelp = SigHelp.from_lsp(response)
         if self._sighelp:
-            content = self._sighelp.build_popup_content(self._sighelp_renderer)
+            content = self._sighelp.render(self.view)
 
             def render_sighelp_on_main_thread() -> None:
                 if self.view.is_popup_visible():
@@ -397,7 +366,7 @@ def _show_sighelp_popup(self, content: str, point: int) -> None:
         mdpopups.show_popup(self.view,
                             content,
                             css=css().popups,
-                            md=True,
+                            md=False,
                             flags=flags,
                             location=point,
                             wrapper_class=css().popups_classname,
@@ -410,7 +379,7 @@ def _update_sighelp_popup(self, content: str) -> None:
         mdpopups.update_popup(self.view,
                               content,
                               css=css().popups,
-                              md=True,
+                              md=False,
                               wrapper_class=css().popups_classname)
 
     def _on_sighelp_hide(self) -> None:


Test patch:
diff --git a/tests/test_signature_help.py b/tests/test_signature_help.py
--- a/tests/test_signature_help.py
+++ b/tests/test_signature_help.py
@@ -1,307 +1,253 @@
-from LSP.plugin.core.signature_help import create_signature_help
-from LSP.plugin.core.signature_help import parse_signature_information
-from LSP.plugin.core.signature_help import render_signature_label
-from LSP.plugin.core.signature_help import ScopeRenderer
-from LSP.plugin.core.signature_help import SignatureHelp
-from LSP.plugin.core.typing import Union, Dict
+from LSP.plugin.core.protocol import SignatureHelp
+from LSP.plugin.core.signature_help import SigHelp
+import sublime
 import unittest
 
-signature = {
-    'label': 'foo_bar(value: int) -> None',
-    'documentation': {'value': 'The default function for foobaring'},
-    'parameters': [{
-        'label': 'value',
-        'documentation': {
-            'value': 'A number to foobar on'
-        }
-    }]
-}  # type: dict
 
-signature_overload = {
-    'label': 'foo_bar(value: int, multiplier: int) -> None',
-    'documentation': {'value': 'Foobaring with a multiplier'},
-    'parameters': [{
-        'label': 'value',
-        'documentation': {
-            'value': 'A number to foobar on'
-        }
-    }, {
-        'label': 'multiplier',
-        'documentation': 'Change foobar to work on larger increments'
-    }]
-}  # type: dict
-
-signature_missing_label = {
-    'documentation': '',
-    'parameters': [{
-        'documentation': None,
-        'label': 'verbose_name'
-    }, {
-        'documentation': None,
-        'label': 'name'
-    }, {
-        'documentation': None,
-        'label': 'primary_key'
-    }, {
-        'documentation': None,
-        'label': 'max_length'
-    }],
-    'label': ''
-}
-
-signature_information = parse_signature_information(signature)
-signature_overload_information = parse_signature_information(signature_overload)
-signature_missing_label_information = parse_signature_information(signature_missing_label)
-
-SINGLE_SIGNATURE = """<div class="highlight"><pre>
-
-<entity.name.function>foo_bar
-<punctuation>(</punctuation>
-<variable.parameter emphasize>value</variable.parameter>: int
-<punctuation>)</punctuation> -&gt; None</entity.name.function>
-</pre></div>
-<p>{'value': 'The default function for foobaring'}</p>
-<p><b>value</b>: {'value': 'A number to foobar on'}</p>"""
-
-MISSING_LABEL_SIGNATURE = """<div class="highlight"><pre>
-
-<entity.name.function></entity.name.function>
-</pre></div>"""
-
-OVERLOADS_FIRST = """**1** of **2** overloads (use the ↑ ↓ keys to navigate, press ESC to hide):
-
-<div class="highlight"><pre>
-
-<entity.name.function>foo_bar
-<punctuation>(</punctuation>
-<variable.parameter emphasize>value</variable.parameter>: int
-<punctuation>)</punctuation> -&gt; None</entity.name.function>
-</pre></div>
-<p>{'value': 'The default function for foobaring'}</p>
-<p><b>value</b>: {'value': 'A number to foobar on'}</p>"""
-
-
-OVERLOADS_SECOND = """**2** of **2** overloads (use the ↑ ↓ keys to navigate, press ESC to hide):
-
-<div class="highlight"><pre>
-
-<entity.name.function>foo_bar
-<punctuation>(</punctuation>
-<variable.parameter emphasize>value</variable.parameter>: int,\
- \n<variable.parameter>multiplier</variable.parameter>: int
-<punctuation>)</punctuation> -&gt; None</entity.name.function>
-</pre></div>
-<p>{'value': 'Foobaring with a multiplier'}</p>
-<p><b>value</b>: {'value': 'A number to foobar on'}</p>"""
-
-OVERLOADS_SECOND_SECOND_PARAMETER = """**2** of **2** overloads (use the ↑ ↓ keys to navigate, press ESC to hide):
-
-<div class="highlight"><pre>
-
-<entity.name.function>foo_bar
-<punctuation>(</punctuation>
-<variable.parameter>value</variable.parameter>: int,\
- \n<variable.parameter emphasize>multiplier</variable.parameter>: int
-<punctuation>)</punctuation> -&gt; None</entity.name.function>
-</pre></div>
-<p>{'value': 'Foobaring with a multiplier'}</p>
-<p><b>multiplier</b>: Change foobar to work on larger increments</p>"""
-
-
-JSON_STRINGIFY = """"""
-
-
-def create_signature(label: str, *param_labels, **kwargs) -> dict:
-    raw = dict(label=label, parameters=list(dict(label=param_label) for param_label in param_labels))
-    raw.update(kwargs)
-    return raw
-
-
-class MockRenderer(ScopeRenderer):
-
-    def function(self, content: str, escape: bool = True) -> str:
-        return self._wrap_with_scope_style(content, "entity.name.function")
-
-    def punctuation(self, content: str) -> str:
-        return self._wrap_with_scope_style(content, "punctuation")
-
-    def parameter(self, content: str, emphasize: bool = False) -> str:
-        return self._wrap_with_scope_style(content, "variable.parameter", emphasize)
-
-    def _wrap_with_scope_style(self, content: str, scope: str, emphasize: bool = False) -> str:
-        return '\n<{}{}>{}</{}>'.format(scope, " emphasize" if emphasize else "", content, scope)
-
-    def markup(self, content: Union[str, Dict[str, str]]) -> str:
-        return content
-
-
-renderer = MockRenderer()
-
-
-class CreateSignatureHelpTests(unittest.TestCase):
-
-    def test_none(self):
-        self.assertIsNone(create_signature_help(None))
-
-    def test_empty(self):
-        self.assertIsNone(create_signature_help({}))
-
-    def test_default_indices(self):
-
-        help = create_signature_help({"signatures": [signature]})
-
-        self.assertIsNotNone(help)
-        if help:
-            self.assertEqual(help._active_signature_index, 0)
-            self.assertEqual(help._active_parameter_index, -1)
-
-    def test_dockerfile_signature_help(self):
-        info = parse_signature_information({
-            'label': 'RUN [ "command" "parameters", ... ]',
-            'parameters': [
-                {'label': '['},
-                {'label': '"command"'},
-                {'label': '"parameters"'},
-                {'label': '...'},
-                {'label': ']'}
-            ]})
-        self.assertEqual(info.label, 'RUN [ "command" "parameters", ... ]')
-        self.assertEqual(len(info.parameters), 5)
-        self.assertEqual(info.parameters[0].label, '[')
-        self.assertEqual(info.parameters[1].label, '"command"')
-        self.assertEqual(info.parameters[2].label, '"parameters"')
-        self.assertEqual(info.parameters[3].label, '...')
-        self.assertEqual(info.parameters[4].label, ']')
-        # There are no open and close parentheses.
-        self.assertEqual(info.open_paren_index, -1)
-        self.assertEqual(info.close_paren_index, -1)
-
-
-class RenderSignatureLabelTests(unittest.TestCase):
-
-    def test_no_parameters(self):
-        sig = create_signature("foobar()")
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 0)
-            self.assertEqual(label, "\n<entity.name.function>foobar()</entity.name.function>")
-
-    def test_params(self):
-        sig = create_signature("foobar(foo, foo)", "foo", "foo", activeParameter=1)
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 1)
-            self.assertEqual(label, """
-<entity.name.function>foobar
-<punctuation>(</punctuation>
-<variable.parameter>foo</variable.parameter>,\
- \n<variable.parameter emphasize>foo</variable.parameter>
-<punctuation>)</punctuation></entity.name.function>""")
-
-    def test_params_are_substrings(self):
-        sig = create_signature("foobar(self, foo: str, foo: i32)", "foo", "foo", activeParameter=1)
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 1)
-            self.assertEqual(label, """
-<entity.name.function>foobar
-<punctuation>(</punctuation>self,\
- \n<variable.parameter>foo</variable.parameter>: str,\
- \n<variable.parameter emphasize>foo</variable.parameter>: i32
-<punctuation>)</punctuation></entity.name.function>""")
-
-    def test_params_are_substrings_before_comma(self):
-        sig = create_signature("f(x: str, t)", "x", "t")
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 0)
-            self.assertEqual(label, """
-<entity.name.function>f
-<punctuation>(</punctuation>\
-\n<variable.parameter emphasize>x</variable.parameter>: str,\
- \n<variable.parameter>t</variable.parameter>
-<punctuation>)</punctuation></entity.name.function>""")
-
-    def test_params_with_range(self):
-        sig = create_signature("foobar(foo, foo)", [7, 10], [12, 15], activeParameter=1)
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 1)
-            self.assertEqual(label, """
-<entity.name.function>foobar
-<punctuation>(</punctuation>
-<variable.parameter>foo</variable.parameter>,\
- \n<variable.parameter emphasize>foo</variable.parameter>
-<punctuation>)</punctuation></entity.name.function>""")
-
-    def test_params_no_parens(self):
-        # note: will not work without ranges: first "foo" param will match "foobar"
-        sig = create_signature("foobar foo foo", [7, 10], [11, 14], activeParameter=1)
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 1)
-            self.assertEqual(label, """
-<entity.name.function>foobar\
- \n<variable.parameter>foo</variable.parameter>\
- \n<variable.parameter emphasize>foo</variable.parameter></entity.name.function>""")
-
-    def test_long_signature(self):
-        # self.maxDiff = None
-        sig = create_signature(
-            """do_the_foo_bar_if_correct_with_optional_bar_and_uppercase_option(takes_a_mandatory_foo: int, \
-bar_if_needed: Optional[str], in_uppercase: Optional[bool]) -> Optional[str]""",
-            "takes_a_mandatory_foo",
-            "bar_if_needed",
-            "in_uppercase",
-            activeParameter=1)
-        help = create_signature_help(dict(signatures=[sig]))
-        if help:
-            label = render_signature_label(renderer, help.active_signature(), 1)
-            self.assertEqual(label, """
-<entity.name.function>do_the_foo_bar_if_correct_with_optional_bar_and_uppercase_option
-<punctuation>(</punctuation>
-<variable.parameter>takes_a_mandatory_foo</variable.parameter>: int,\
- <br>&nbsp;&nbsp;&nbsp;&nbsp;\n<variable.parameter emphasize>bar_if_needed</variable.parameter>: Optional[str],\
- <br>&nbsp;&nbsp;&nbsp;&nbsp;\n<variable.parameter>in_uppercase</variable.parameter>: Optional[bool]
-<punctuation>)</punctuation> -&gt; Optional[str]</entity.name.function>""")
-
-
-class SignatureHelpTests(unittest.TestCase):
-
-    def test_single_signature(self):
-        help = SignatureHelp([signature_information])
-        self.assertIsNotNone(help)
-        if help:
-            content = help.build_popup_content(renderer)
-            self.assertFalse(help.has_multiple_signatures())
-            self.assertEqual(content, SINGLE_SIGNATURE)
-
-    def test_signature_missing_label(self):
-        help = SignatureHelp([signature_missing_label_information])
-        self.assertIsNotNone(help)
-        if help:
-            content = help.build_popup_content(renderer)
-            self.assertFalse(help.has_multiple_signatures())
-            self.assertEqual(content, MISSING_LABEL_SIGNATURE)
-
-    def test_overload(self):
-        help = SignatureHelp([signature_information, signature_overload_information])
-        self.assertIsNotNone(help)
-        if help:
-            content = help.build_popup_content(renderer)
-            self.assertTrue(help.has_multiple_signatures())
-            self.assertEqual(content, OVERLOADS_FIRST)
-
-            help.select_signature(1)
-            help.select_signature(1)  # verify we don't go out of bounds,
-            content = help.build_popup_content(renderer)
-            self.assertEqual(content, OVERLOADS_SECOND)
-
-    def test_active_parameter(self):
-        help = SignatureHelp([signature_information, signature_overload_information], active_signature=1,
-                             active_parameter=1)
-        self.assertIsNotNone(help)
-        if help:
-            content = help.build_popup_content(renderer)
-            self.assertTrue(help.has_multiple_signatures())
-            self.assertEqual(content, OVERLOADS_SECOND_SECOND_PARAMETER)
+class SignatureHelpTest(unittest.TestCase):
+
+    def setUp(self) -> None:
+        self.view = sublime.active_window().active_view()
+
+    def test_no_signature(self) -> None:
+        help = SigHelp.from_lsp(None)
+        self.assertIsNone(help)
+
+    def test_empty_signature_list(self) -> None:
+        help = SigHelp.from_lsp({"signatures": []})
+        self.assertIsNone(help)
+
+    def assert_render(self, input: SignatureHelp, regex: str) -> None:
+        help = SigHelp(input)
+        assert self.view
+        self.assertRegex(help.render(self.view), regex.replace("\n", "").replace("            ", ""))
+
+    def test_signature(self) -> None:
+        self.assert_render(
+            {
+                "signatures":
+                [
+                    {
+                        "label": "f(x)",
+                        "documentation": "f does interesting things",
+                        "parameters":
+                        [
+                            {
+                                "label": "x",
+                                "documentation": "must be in the frobnicate range"
+                            }
+                        ]
+                    }
+                ],
+                "activeSignature": 0,
+                "activeParameter": 0
+            },
+            r'''
+            <div class="highlight"><pre>
+            <span style="color: #\w{6}">f\(</span>
+            <span style="color: #\w{6}; font-weight: bold; text-decoration: underline">x</span>
+            <span style="color: #\w{6}">\)</span>
+            </pre></div>
+            <p>must be in the frobnicate range</p>
+            <hr/>
+            <div style="font-size: 0\.9rem"><p>f does interesting things</p></div>
+            '''
+        )
+
+    def test_markdown(self) -> None:
+        self.assert_render(
+            {
+                "signatures":
+                [
+                    {
+                        "label": "f(x)",
+                        "documentation":
+                        {
+                            "value": "f does _interesting_ things",
+                            "kind": "markdown"
+                        },
+                        "parameters":
+                        [
+                            {
+                                "label": "x",
+                                "documentation":
+                                {
+                                    "value": "must be in the **frobnicate** range",
+                                    "kind": "markdown"
+                                }
+                            }
+                        ]
+                    }
+                ],
+                "activeSignature": 0,
+                "activeParameter": 0
+            },
+            r'''
+            <div class="highlight"><pre>
+            <span style="color: #\w{6}">f\(</span>
+            <span style="color: #\w{6}; font-weight: bold; text-decoration: underline">x</span>
+            <span style="color: #\w{6}">\)</span>
+            </pre></div>
+            <p>must be in the <strong>frobnicate</strong> range</p>
+            <hr/>
+            <div style="font-size: 0\.9rem"><p>f does <em>interesting</em> things</p></div>
+            '''
+        )
+
+    def test_second_parameter(self) -> None:
+        self.assert_render(
+            {
+                "signatures":
+                [
+                    {
+                        "label": "f(x, y)",
+                        "parameters":
+                        [
+                            {
+                                "label": "x"
+                            },
+                            {
+                                "label": "y",
+                                "documentation": "hello there"
+                            }
+                        ]
+                    }
+                ],
+                "activeSignature": 0,
+                "activeParameter": 1
+            },
+            r'''
+            <div class="highlight"><pre>
+            <span style="color: #\w{6}">f\(</span>
+            <span style="color: #\w{6}">x</span>
+            <span style="color: #\w{6}">, </span>
+            <span style="color: #\w{6}; font-weight: bold; text-decoration: underline">y</span>
+            <span style="color: #\w{6}">\)</span>
+            </pre></div>
+            <p>hello there</p>
+            '''
+        )
+
+    def test_parameter_ranges(self) -> None:
+        self.assert_render(
+            {
+                "signatures":
+                [
+                    {
+                        "label": "f(x, y)",
+                        "parameters":
+                        [
+                            {
+                                "label": [2, 3],
+                            },
+                            {
+                                "label": [5, 6],
+                                "documentation": "hello there"
+                            }
+                        ]
+                    }
+                ],
+                "activeSignature": 0,
+                "activeParameter": 1
+            },
+            r'''
+            <div class="highlight"><pre>
+            <span style="color: #\w{6}">f\(</span>
+            <span style="color: #\w{6}">x</span>
+            <span style="color: #\w{6}">, </span>
+            <span style="color: #\w{6}; font-weight: bold; text-decoration: underline">y</span>
+            <span style="color: #\w{6}">\)</span>
+            </pre></div>
+            <p>hello there</p>
+            '''
+        )
+
+    def test_overloads(self) -> None:
+        self.assert_render(
+            {
+                "signatures":
+                [
+                    {
+                        "label": "f(x, y)",
+                        "parameters":
+                        [
+                            {
+                                "label": [2, 3]
+                            },
+                            {
+                                "label": [5, 6],
+                                "documentation": "hello there"
+                            }
+                        ]
+                    },
+                    {
+                        "label": "f(x, a, b)",
+                        "parameters":
+                        [
+                            {
+                                "label": [2, 3]
+                            },
+                            {
+                                "label": [5, 6]
+                            },
+                            {
+                                "label": [8, 9]
+                            }
+                        ]
+                    }
+                ],
+                "activeSignature": 1,
+                "activeParameter": 0
+            },
+            r'''
+            <p>
+            <div style="font-size: 0\.9rem">
+            <b>2</b> of <b>2</b> overloads \(use ↑ ↓ to navigate, press Esc to hide\):
+            </div>
+            </p>
+            <div class="highlight"><pre><span style="color: #52fa7c">f\(</span>
+            <span style="color: #\w{6}; font-weight: bold; text-decoration: underline">x</span>
+            <span style="color: #\w{6}">, </span>
+            <span style="color: #\w{6}">a</span>
+            <span style="color: #\w{6}">, </span>
+            <span style="color: #\w{6}">b</span>
+            <span style="color: #\w{6}">\)</span>
+            </pre></div>
+            '''
+        )
+
+    def test_dockerfile_signature(self) -> None:
+        self.assert_render(
+            {
+                "signatures":
+                [
+                    {
+                        "label": 'RUN [ "command" "parameters", ... ]',
+                        "parameters":
+                        [
+                            {'label': '['},
+                            {'label': '"command"'},
+                            {'label': '"parameters"'},
+                            {'label': '...'},
+                            {'label': ']'}
+                        ]
+                    }
+                ],
+                "activeSignature": 0,
+                "activeParameter": 2
+            },
+            r'''
+            <div class="highlight"><pre>
+            <span style="color: #\w{6}">RUN </span>
+            <span style="color: #\w{6}">\[</span>
+            <span style="color: #\w{6}"> </span>
+            <span style="color: #\w{6}">"command"</span>
+            <span style="color: #\w{6}"> </span>
+            <span style="color: #\w{6}; font-weight: bold; text-decoration: underline">"parameters"</span>
+            <span style="color: #\w{6}">, </span>
+            <span style="color: #\w{6}">\.\.\.</span>
+            <span style="color: #\w{6}"> </span>
+            <span style="color: #\w{6}">\]</span>
+            </pre></div>
+            '''
+        )