diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 0932574f0..e1dfe9f0e 100644 --- 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 index 52f0f2c6b..71d2f4c26 100644 --- a/plugin/core/signature_help.py +++ b/plugin/core/signature_help.py @@ -1,236 +1,164 @@ -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: + 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 = '

{} of {} overloads ' + \ + "(use ↑ ↓ to navigate, press Esc to hide):

" + 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
class and the extra
 are copied from mdpopups' HTML output. When mdpopups changes
         # its output style, we must update this literal string accordingly.
         formatted.append('
')
-        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("
") + 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: + formatted.append('
') + formatted.append(docs) + formatted.append('
') + 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("

{}

".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("

{}: {}

".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 '{}'.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 = "
    " 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 index f743aa66e..bda0cb1f9 100644 --- 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 '{}'.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: diff --git a/stubs/sublime.pyi b/stubs/sublime.pyi index da84b3bb5..8abbc431e 100644 --- a/stubs/sublime.pyi +++ b/stubs/sublime.pyi @@ -958,6 +958,9 @@ class View: def transform_region_from(self, region: Region, change_id: Any) -> Region: ... + def style_for_scope(self, scope: str) -> Dict[str, str]: + ... + class Buffer: buffer_id = ... # type: int diff --git a/tests/test_signature_help.py b/tests/test_signature_help.py index 9c812847f..03e3f9a6a 100644 --- a/tests/test_signature_help.py +++ b/tests/test_signature_help.py @@ -1,307 +1,247 @@ -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.protocol import SignatureHelp +from LSP.plugin.core.signature_help import SigHelp from LSP.plugin.core.typing import Union, Dict +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 = """
-
-foo_bar
-(
-value: int
-) -> None
-
-

{'value': 'The default function for foobaring'}

-

value: {'value': 'A number to foobar on'}

""" - -MISSING_LABEL_SIGNATURE = """
-
-
-
""" - -OVERLOADS_FIRST = """**1** of **2** overloads (use the ↑ ↓ keys to navigate, press ESC to hide): - -
-
-foo_bar
-(
-value: int
-) -> None
-
-

{'value': 'The default function for foobaring'}

-

value: {'value': 'A number to foobar on'}

""" - - -OVERLOADS_SECOND = """**2** of **2** overloads (use the ↑ ↓ keys to navigate, press ESC to hide): - -
-
-foo_bar
-(
-value: int,\
- \nmultiplier: int
-) -> None
-
-

{'value': 'Foobaring with a multiplier'}

-

value: {'value': 'A number to foobar on'}

""" - -OVERLOADS_SECOND_SECOND_PARAMETER = """**2** of **2** overloads (use the ↑ ↓ keys to navigate, press ESC to hide): - -
-
-foo_bar
-(
-value: int,\
- \nmultiplier: int
-) -> None
-
-

{'value': 'Foobaring with a multiplier'}

-

multiplier: Change foobar to work on larger increments

""" - - -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, "\nfoobar()") - - 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, """ -foobar -( -foo,\ - \nfoo -)""") - - 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, """ -foobar -(self,\ - \nfoo: str,\ - \nfoo: i32 -)""") - - 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, """ -f -(\ -\nx: str,\ - \nt -)""") - - 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, """ -foobar -( -foo,\ - \nfoo -)""") - - 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, """ -foobar\ - \nfoo\ - \nfoo""") - - 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, """ -do_the_foo_bar_if_correct_with_optional_bar_and_uppercase_option -( -takes_a_mandatory_foo: int,\ -
    \nbar_if_needed: Optional[str],\ -
    \nin_uppercase: Optional[bool] -) -> Optional[str]
""") - - -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 assert_render(self, input: SignatureHelp, regex: str) -> None: + help = SigHelp(input) + 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''' +
+            f\(
+            x
+            \)
+            
+

must be in the frobnicate range

+

f does interesting things

+ ''' + ) + + 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''' +
+            f\(
+            x
+            \)
+            
+

must be in the frobnicate range

+

f does interesting things

+ ''' + ) + + 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''' +
+            f\(
+            x
+            , 
+            y
+            \)
+            
+

hello there

+ ''' + ) + + 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''' +
+            f\(
+            x
+            , 
+            y
+            \)
+            
+

hello there

+ ''' + ) + + 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''' +

+

+ 2 of 2 overloads \(use ↑ ↓ to navigate, press Esc to hide\): +
+

+
f\(
+            x
+            , 
+            a
+            , 
+            b
+            \)
+            
+ ''' + ) + + 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''' +
+            RUN 
+            \[
+             
+            "command"
+             
+            "parameters"
+            , 
+            \.\.\.
+             
+            \]
+            
+ ''' + )