From cfecfdc87c0e4ee8b34fe52d8feaa610ab51e12b Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 13 Feb 2021 20:38:59 +0100 Subject: [PATCH 01/30] send completion request to multiple sessions --- plugin/documents.py | 53 ++++++++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index dec65eb47..e187c9b1a 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -2,6 +2,7 @@ from .code_actions import CodeActionsByConfigName from .completion import LspResolveDocsCommand from .core.logging import debug +from .core.promise import Promise, ResolveFunc from .core.protocol import CodeLens from .core.protocol import Command from .core.protocol import Diagnostic @@ -19,7 +20,7 @@ from .core.types import basescope2languageid from .core.types import debounced from .core.types import FEATURES_TIMEOUT -from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple, Union +from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple, Union, cast from .core.views import DIAGNOSTIC_SEVERITY from .core.views import document_color_params from .core.views import format_completion @@ -53,7 +54,7 @@ } ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], int], None] - +ResolvedCompletion = Tuple[List[sublime.CompletionItem], int] def is_regular_view(v: sublime.View) -> bool: # Not from the quick panel (CTRL+P), must have a filename on-disk, and not a special view like a console, @@ -606,20 +607,38 @@ def render_highlights_on_main_thread() -> None: # --- textDocument/complete ---------------------------------------------------------------------------------------- - def _on_query_completions_async(self, resolve: ResolveCompletionsFn, location: int) -> None: - session = self.session('completionProvider', location) - if not session: - resolve([], 0) + def _on_query_completions_async(self, resolve_completion_list: ResolveCompletionsFn, location: int) -> None: + sessions = self.sessions('completionProvider') + if not sessions: + resolve_completion_list([], 0) return self.purge_changes_async() - can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) - config_name = session.config.name - session.send_request_async( - Request.complete(text_document_position_params(self.view, location), self.view), - lambda res: self._on_complete_result(res, resolve, can_resolve_completion_items, config_name), - lambda res: self._on_complete_error(res, resolve)) - - def _on_complete_result(self, response: Optional[Union[dict, List]], resolve: ResolveCompletionsFn, + + completion_promises = [] # List[Promise] + for session in sessions: + can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) + config_name = session.config.name + + def completion_request() -> Promise: + return Promise(lambda resolve: session.send_request_async( + Request.complete(text_document_position_params(self.view, location), self.view), + lambda res: self._on_complete_result(res, resolve, can_resolve_completion_items, config_name), + lambda res: self._on_complete_error(res, resolve))) + + completion_promises.append(completion_request) + + def combine_responses(responses): + responses = cast(List[ResolvedCompletion], responses) + items = [] # List[CompletionItem] + flags = 0 # int + for r in responses: + items.extend(r[0]) + flags |= r[1] + resolve_completion_list(items, flags) + + Promise.all(completion_promises).then(combine_responses) + + def _on_complete_result(self, response: Optional[Union[dict, List]], resolve_promise: ResolveFunc[ResolvedCompletion], can_resolve_completion_items: bool, session_name: str) -> None: response_items = [] # type: List[Dict] flags = 0 @@ -640,10 +659,10 @@ def _on_complete_result(self, response: Optional[Union[dict, List]], resolve: Re for index, response_item in enumerate(response_items)] if items: flags |= sublime.INHIBIT_REORDER - resolve(items, flags) + resolve_promise((items, flags)) - def _on_complete_error(self, error: dict, resolve: ResolveCompletionsFn) -> None: - resolve([], 0) + def _on_complete_error(self, error: dict, resolve_promise: ResolveFunc[ResolvedCompletion]) -> None: + resolve_promise(([], 0)) LspResolveDocsCommand.completions = [] sublime.status_message('Completion error: ' + str(error.get('message'))) From ebd124a0677138d903b0bdf3063089f68bab9e75 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 13 Feb 2021 20:54:24 +0100 Subject: [PATCH 02/30] must call the function to return the promise :) --- plugin/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index e187c9b1a..d5d62b361 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -625,7 +625,7 @@ def completion_request() -> Promise: lambda res: self._on_complete_result(res, resolve, can_resolve_completion_items, config_name), lambda res: self._on_complete_error(res, resolve))) - completion_promises.append(completion_request) + completion_promises.append(completion_request()) def combine_responses(responses): responses = cast(List[ResolvedCompletion], responses) From 20dd3e7b474060d3bfe3af9a9634e06655895dc2 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 14 Feb 2021 00:16:59 +0100 Subject: [PATCH 03/30] add types for CompletionItem and CompletionList --- plugin/completion.py | 4 ++-- plugin/core/protocol.py | 37 +++++++++++++++++++++++++++++++++++-- plugin/core/views.py | 6 +++--- plugin/documents.py | 17 +++++++++++------ 4 files changed, 51 insertions(+), 13 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 51245c8a1..b77d69070 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -3,7 +3,7 @@ import webbrowser from .core.logging import debug from .core.edit import parse_text_edit -from .core.protocol import Request, InsertTextFormat, Range +from .core.protocol import Request, InsertTextFormat, Range, CompletionItem from .core.registry import LspTextCommand from .core.typing import Any, List, Dict, Optional, Generator, Union from .core.views import FORMAT_STRING, FORMAT_MARKUP_CONTENT, minihtml @@ -14,7 +14,7 @@ class LspResolveDocsCommand(LspTextCommand): - completions = [] # type: List[Dict[str, Any]] + completions = [] # type: List[CompletionItem] def run(self, edit: sublime.Edit, index: int, event: Optional[dict] = None) -> None: item = self.completions[index] diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 92a8ed50b..b0439f4c9 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -151,13 +151,11 @@ class SignatureHelpTriggerKind: 'targetSelectionRange': Dict[str, Any] }, total=False) - DiagnosticRelatedInformation = TypedDict('DiagnosticRelatedInformation', { 'location': Location, 'message': str }, total=False) - Diagnostic = TypedDict('Diagnostic', { 'range': RangeLsp, 'severity': int, @@ -169,6 +167,41 @@ class SignatureHelpTriggerKind: 'relatedInformation': List[DiagnosticRelatedInformation] }, total=False) +TextEdit = TypedDict('TextEdit', { + 'range': RangeLsp, + 'newText': str +}, total=True) + +InsertReplaceEdit = TypedDict('InsertReplaceEdit', { + 'replace': RangeLsp, + 'insert': RangeLsp, + 'newText': str +}, total=True) + +CompletionItem = TypedDict('CompletionItem', { + 'label': str, + 'kind': Optional[int], + 'tags': Optional[List[int]], + 'detail': Optional[str], + 'documentation': Optional[str], + 'deprecated': Optional[bool], + 'preselect': Optional[bool], + 'sortText': Optional[str], + 'filterText': Optional[str], + 'insertText': Optional[str], + 'insertTextFormat': Optional[int], + 'insertTextMode': Optional[int], + 'textEdit': Optional[Union[TextEdit, InsertReplaceEdit]], + 'additionalTextEdits': Optional[List[TextEdit]], + 'commitCharacters': Optional[List[str]], + 'command': Optional[Command], + 'data': Any +}, total=True) + +CompletionList = TypedDict('CompletionList', { + 'isIncomplete': bool, + 'items': List[CompletionItem], +}, total=True) class Request: diff --git a/plugin/core/views.py b/plugin/core/views.py index 70db506da..909879b40 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1,5 +1,5 @@ from .css import css as lsp_css -from .protocol import CompletionItemTag +from .protocol import CompletionItem, CompletionItemTag from .protocol import Diagnostic from .protocol import DiagnosticRelatedInformation from .protocol import DiagnosticSeverity @@ -672,7 +672,7 @@ def format_diagnostic_for_html(view: sublime.View, diagnostic: Diagnostic, base_ return "".join(formatted) -def _is_completion_item_deprecated(item: dict) -> bool: +def _is_completion_item_deprecated(item: CompletionItem) -> bool: if item.get("deprecated", False): return True tags = item.get("tags") @@ -682,7 +682,7 @@ def _is_completion_item_deprecated(item: dict) -> bool: def format_completion( - item: dict, index: int, can_resolve_completion_items: bool, session_name: str + item: CompletionItem, index: int, can_resolve_completion_items: bool, session_name: str ) -> sublime.CompletionItem: # This is a hot function. Don't do heavy computations or IO in this function. item_kind = item.get("kind") diff --git a/plugin/documents.py b/plugin/documents.py index d5d62b361..3046927c0 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -2,8 +2,11 @@ from .code_actions import CodeActionsByConfigName from .completion import LspResolveDocsCommand from .core.logging import debug -from .core.promise import Promise, ResolveFunc +from .core.promise import Promise +from .core.promise import ResolveFunc from .core.protocol import CodeLens +from .core.protocol import CompletionItem +from .core.protocol import CompletionList from .core.protocol import Command from .core.protocol import Diagnostic from .core.protocol import DocumentHighlightKind @@ -53,8 +56,10 @@ DocumentHighlightKind.Write: "write" } -ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], int], None] -ResolvedCompletion = Tuple[List[sublime.CompletionItem], int] +Flags = int +ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], Flags], None] +ResolvedCompletion = Tuple[List[sublime.CompletionItem], Flags] +CompletionResponse = Union[List[CompletionItem], CompletionList, None] def is_regular_view(v: sublime.View) -> bool: # Not from the quick panel (CTRL+P), must have a filename on-disk, and not a special view like a console, @@ -614,7 +619,7 @@ def _on_query_completions_async(self, resolve_completion_list: ResolveCompletion return self.purge_changes_async() - completion_promises = [] # List[Promise] + completion_promises = [] # type: List[Promise[ResolvedCompletion]] for session in sessions: can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) config_name = session.config.name @@ -638,9 +643,9 @@ def combine_responses(responses): Promise.all(completion_promises).then(combine_responses) - def _on_complete_result(self, response: Optional[Union[dict, List]], resolve_promise: ResolveFunc[ResolvedCompletion], + def _on_complete_result(self, response: CompletionResponse, resolve_promise: ResolveFunc[ResolvedCompletion], can_resolve_completion_items: bool, session_name: str) -> None: - response_items = [] # type: List[Dict] + response_items = [] # type: List[CompletionItem] flags = 0 prefs = userprefs() if prefs.inhibit_snippet_completions: From 633d9ca645b71ef49134e46b2252ff531a8268ae Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 14 Feb 2021 01:36:30 +0100 Subject: [PATCH 04/30] changes needed to fix LspResolveDocsCommand.completions I needed to reaorganize the code to make the LspResolveDocsCommand.completions work properly --- plugin/documents.py | 76 +++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 3046927c0..0c513c5bf 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -58,8 +58,9 @@ Flags = int ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], Flags], None] -ResolvedCompletion = Tuple[List[sublime.CompletionItem], Flags] + CompletionResponse = Union[List[CompletionItem], CompletionList, None] +CompletionResponseWithSession = Tuple[CompletionResponse, Session] def is_regular_view(v: sublime.View) -> bool: # Not from the quick panel (CTRL+P), must have a filename on-disk, and not a special view like a console, @@ -619,57 +620,58 @@ def _on_query_completions_async(self, resolve_completion_list: ResolveCompletion return self.purge_changes_async() - completion_promises = [] # type: List[Promise[ResolvedCompletion]] + completion_promises = [] # type: List[Promise[CompletionResponseWithSession]] for session in sessions: - can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) - config_name = session.config.name def completion_request() -> Promise: return Promise(lambda resolve: session.send_request_async( Request.complete(text_document_position_params(self.view, location), self.view), - lambda res: self._on_complete_result(res, resolve, can_resolve_completion_items, config_name), - lambda res: self._on_complete_error(res, resolve))) + lambda res: self._on_complete_result(res, resolve, session), + lambda res: self._on_complete_error(res, resolve, session))) completion_promises.append(completion_request()) - def combine_responses(responses): - responses = cast(List[ResolvedCompletion], responses) - items = [] # List[CompletionItem] - flags = 0 # int - for r in responses: - items.extend(r[0]) - flags |= r[1] - resolve_completion_list(items, flags) - - Promise.all(completion_promises).then(combine_responses) - - def _on_complete_result(self, response: CompletionResponse, resolve_promise: ResolveFunc[ResolvedCompletion], - can_resolve_completion_items: bool, session_name: str) -> None: - response_items = [] # type: List[CompletionItem] - flags = 0 + LspResolveDocsCommand.completions = [] + Promise.all(completion_promises).then( + lambda responses: self._on_all_settled(responses, resolve_completion_list)) + + def _on_complete_result(self, response: CompletionResponse, resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: + resolve_promise((response, session)) + + def _on_complete_error(self, error: dict, resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: + resolve_promise((None, session)) + sublime.status_message('Completion error: ' + str(error.get('message'))) + + def _on_all_settled(self, + responses: Optional[List[CompletionResponseWithSession]], + resolve_completion_list: ResolveCompletionsFn + ): + if not responses: + resolve_completion_list([], 0) + items = [] # type: List[sublime.CompletionItem] + flags = 0 # int prefs = userprefs() if prefs.inhibit_snippet_completions: flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS if prefs.inhibit_word_completions: flags |= sublime.INHIBIT_WORD_COMPLETIONS - if isinstance(response, dict): - response_items = response["items"] or [] - if response.get("isIncomplete", False): - flags |= sublime.DYNAMIC_COMPLETIONS - elif isinstance(response, list): - response_items = response - response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) - LspResolveDocsCommand.completions = response_items - items = [format_completion(response_item, index, can_resolve_completion_items, session_name) - for index, response_item in enumerate(response_items)] + + for response, session in responses: + response_items = [] # type: List[CompletionItem] + if isinstance(response, dict): + response_items = response["items"] or [] + if response.get("isIncomplete", False): + flags |= sublime.DYNAMIC_COMPLETIONS + elif isinstance(response, list): + response_items = response + response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) + LspResolveDocsCommand.completions.extend(response_items) + can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) + items.extend([format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) + for index, response_item in enumerate(response_items)]) if items: flags |= sublime.INHIBIT_REORDER - resolve_promise((items, flags)) - - def _on_complete_error(self, error: dict, resolve_promise: ResolveFunc[ResolvedCompletion]) -> None: - resolve_promise(([], 0)) - LspResolveDocsCommand.completions = [] - sublime.status_message('Completion error: ' + str(error.get('message'))) + resolve_completion_list(items, flags) # --- Public utility methods --------------------------------------------------------------------------------------- From 66d06660d38dc71845707bc072af4af8c54527a5 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 14 Feb 2021 11:57:37 +0100 Subject: [PATCH 05/30] mypy and flake fixes --- plugin/core/protocol.py | 1 + plugin/documents.py | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index b0439f4c9..c50262629 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -203,6 +203,7 @@ class SignatureHelpTriggerKind: 'items': List[CompletionItem], }, total=True) + class Request: __slots__ = ('method', 'params', 'view', 'progress') diff --git a/plugin/documents.py b/plugin/documents.py index 0c513c5bf..36b1bfefa 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -23,7 +23,7 @@ from .core.types import basescope2languageid from .core.types import debounced from .core.types import FEATURES_TIMEOUT -from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple, Union, cast +from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple, Union from .core.views import DIAGNOSTIC_SEVERITY from .core.views import document_color_params from .core.views import format_completion @@ -62,6 +62,7 @@ CompletionResponse = Union[List[CompletionItem], CompletionList, None] CompletionResponseWithSession = Tuple[CompletionResponse, Session] + def is_regular_view(v: sublime.View) -> bool: # Not from the quick panel (CTRL+P), must have a filename on-disk, and not a special view like a console, # output panel or find-in-files panels. @@ -635,19 +636,23 @@ def completion_request() -> Promise: Promise.all(completion_promises).then( lambda responses: self._on_all_settled(responses, resolve_completion_list)) - def _on_complete_result(self, response: CompletionResponse, resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: + def _on_complete_result(self, response: CompletionResponse, + resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: resolve_promise((response, session)) - def _on_complete_error(self, error: dict, resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: + def _on_complete_error(self, error: dict, + resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: resolve_promise((None, session)) sublime.status_message('Completion error: ' + str(error.get('message'))) - def _on_all_settled(self, + def _on_all_settled( + self, responses: Optional[List[CompletionResponseWithSession]], resolve_completion_list: ResolveCompletionsFn - ): + ) -> None: if not responses: resolve_completion_list([], 0) + return items = [] # type: List[sublime.CompletionItem] flags = 0 # int prefs = userprefs() @@ -667,8 +672,9 @@ def _on_all_settled(self, response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) LspResolveDocsCommand.completions.extend(response_items) can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) - items.extend([format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) - for index, response_item in enumerate(response_items)]) + items.extend( + [format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) + for index, response_item in enumerate(response_items)]) if items: flags |= sublime.INHIBIT_REORDER resolve_completion_list(items, flags) From 6c6837d128bffbf2cc6a516def1d55ce191a4311 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 14 Feb 2021 12:12:28 +0100 Subject: [PATCH 06/30] fix failing test - I expect the popup to not be visible if there are no completion items this test was failing even before this PR --- tests/test_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_completion.py b/tests/test_completion.py index ff22b3cae..5561c9969 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -78,7 +78,7 @@ def verify(self, *, completion_items: List[Dict[str, Any]], insert_text: str, ex def test_none(self) -> 'Generator': self.set_response("textDocument/completion", None) self.view.run_command('auto_complete') - yield lambda: self.view.is_auto_complete_visible() + yield lambda: self.view.is_auto_complete_visible() == False def test_simple_label(self) -> 'Generator': yield from self.verify( From 9074230d2e273b304c97bf0e454e573f74db6358 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sun, 14 Feb 2021 12:38:04 +0100 Subject: [PATCH 07/30] fix flake --- tests/test_completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_completion.py b/tests/test_completion.py index 5561c9969..9ad0bf59c 100644 --- a/tests/test_completion.py +++ b/tests/test_completion.py @@ -78,7 +78,7 @@ def verify(self, *, completion_items: List[Dict[str, Any]], insert_text: str, ex def test_none(self) -> 'Generator': self.set_response("textDocument/completion", None) self.view.run_command('auto_complete') - yield lambda: self.view.is_auto_complete_visible() == False + yield lambda: self.view.is_auto_complete_visible() is False def test_simple_label(self) -> 'Generator': yield from self.verify( From 08c4f3dae1e625c8bdd9697ec70d6de48086f6e1 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 00:04:49 +0100 Subject: [PATCH 08/30] not all keys are required when constructing a CompletionItem --- plugin/core/protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index c50262629..965e5b8bb 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -196,7 +196,7 @@ class SignatureHelpTriggerKind: 'commitCharacters': Optional[List[str]], 'command': Optional[Command], 'data': Any -}, total=True) +}, total=False) CompletionList = TypedDict('CompletionList', { 'isIncomplete': bool, From f8dc0ea6ff0f0d9fd1a927af307a4722c0720f15 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 00:09:39 +0100 Subject: [PATCH 09/30] add markup content --- plugin/core/protocol.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 965e5b8bb..a8ee58391 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -58,6 +58,10 @@ class SignatureHelpTriggerKind: 'href': str }, total=True) +MarkupContent = TypedDict('MarkupContent', { + 'kind': str, + 'value': str +}, total=True) ExecuteCommandParams = TypedDict('ExecuteCommandParams', { 'command': str, @@ -183,7 +187,7 @@ class SignatureHelpTriggerKind: 'kind': Optional[int], 'tags': Optional[List[int]], 'detail': Optional[str], - 'documentation': Optional[str], + 'documentation': Optional[Union[str, MarkupContent]], 'deprecated': Optional[bool], 'preselect': Optional[bool], 'sortText': Optional[str], From c1d19486312bb61747bfeddebc6b559b2ddd208f Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 00:12:22 +0100 Subject: [PATCH 10/30] separate lines to be consistent --- plugin/core/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 909879b40..3afc6a987 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1,5 +1,6 @@ from .css import css as lsp_css -from .protocol import CompletionItem, CompletionItemTag +from .protocol import CompletionItem +from .protocol import CompletionItemTag from .protocol import Diagnostic from .protocol import DiagnosticRelatedInformation from .protocol import DiagnosticSeverity From 14c0e41f02c480352ffed4d54dc6c1d6ad07b8d3 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 17:05:24 +0100 Subject: [PATCH 11/30] resolve earlier --- plugin/documents.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 36b1bfefa..1fc485a93 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -627,7 +627,7 @@ def _on_query_completions_async(self, resolve_completion_list: ResolveCompletion def completion_request() -> Promise: return Promise(lambda resolve: session.send_request_async( Request.complete(text_document_position_params(self.view, location), self.view), - lambda res: self._on_complete_result(res, resolve, session), + lambda res: resolve((res, session)), lambda res: self._on_complete_error(res, resolve, session))) completion_promises.append(completion_request()) @@ -636,10 +636,6 @@ def completion_request() -> Promise: Promise.all(completion_promises).then( lambda responses: self._on_all_settled(responses, resolve_completion_list)) - def _on_complete_result(self, response: CompletionResponse, - resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: - resolve_promise((response, session)) - def _on_complete_error(self, error: dict, resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: resolve_promise((None, session)) From c0a93c7e0c7b35fc2173f84d0fb187c9f4cbd699 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 17:24:16 +0100 Subject: [PATCH 12/30] move sessions, and session_by_name methods to functions for easier reusability --- plugin/core/registry.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 915c539e4..0924d0f9a 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -29,6 +29,17 @@ def best_session(view: sublime.View, sessions: Iterable[Session], point: Optiona return None +def sessions(view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]: + yield from sessions_for_view(view, capability) + + +def session_by_name(view: sublime.View, session_name: str) -> Optional[Session]: + for session in sessions(view): + if session.config.name == session_name: + return session + return None + + configs = ConfigManager(client_configs.all) client_configs.set_listener(configs.update) windows = WindowRegistry(configs) @@ -68,7 +79,7 @@ def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) return False if not self.capability and not self.session_name: # Any session will do. - return any(self.sessions()) + return any(sessions(self.view)) return True def want_event(self) -> bool: @@ -80,13 +91,7 @@ def best_session(self, capability: str, point: Optional[int] = None) -> Optional def session_by_name(self, name: Optional[str] = None) -> Optional[Session]: target = name if name else self.session_name - for session in self.sessions(): - if session.config.name == target: - return session - return None - - def sessions(self, capability: Optional[str] = None) -> Generator[Session, None, None]: - yield from sessions_for_view(self.view, capability) + return session_by_name(self.view, target) class LspRestartClientCommand(sublime_plugin.TextCommand): From 64e3e29532f5bf8c3bab28457de52fd4ca023273 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 17:46:57 +0100 Subject: [PATCH 13/30] use session_name to get the session --- plugin/completion.py | 11 ++++++----- plugin/core/views.py | 5 +++-- plugin/documents.py | 24 +++++++++++++++--------- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index b77d69070..d29210eed 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -3,8 +3,9 @@ import webbrowser from .core.logging import debug from .core.edit import parse_text_edit -from .core.protocol import Request, InsertTextFormat, Range, CompletionItem +from .core.protocol import Request, InsertTextFormat, Range, CompletionItem, MarkupContent from .core.registry import LspTextCommand +from .core.registry import session_by_name from .core.typing import Any, List, Dict, Optional, Generator, Union from .core.views import FORMAT_STRING, FORMAT_MARKUP_CONTENT, minihtml from .core.views import range_to_region @@ -16,7 +17,7 @@ class LspResolveDocsCommand(LspTextCommand): completions = [] # type: List[CompletionItem] - def run(self, edit: sublime.Edit, index: int, event: Optional[dict] = None) -> None: + def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional[dict] = None) -> None: item = self.completions[index] detail = self.format_documentation(item.get('detail') or "") documentation = self.format_documentation(item.get("documentation") or "") @@ -25,14 +26,14 @@ def run(self, edit: sublime.Edit, index: int, event: Optional[dict] = None) -> N if not detail or not documentation: # To make sure that the detail or documentation fields doesn't exist we need to resove the completion item. # If those fields appear after the item is resolved we show them in the popup. - session = self.best_session('completionProvider.resolveProvider') - if session: + session = session_by_name(self.view, session_name) + if session and session.has_capability('completionProvider.resolveProvider'): session.send_request(Request.resolveCompletionItem(item, self.view), self.handle_resolve_response) return minihtml_content = self.get_content(documentation, detail) self.show_popup(minihtml_content) - def format_documentation(self, content: Union[str, Dict[str, str]]) -> str: + def format_documentation(self, content: Union[str, MarkupContent]) -> str: return minihtml(self.view, content, allowed_formats=FORMAT_STRING | FORMAT_MARKUP_CONTENT) def get_content(self, documentation: str, detail: str) -> str: diff --git a/plugin/core/views.py b/plugin/core/views.py index 3afc6a987..5a90198dc 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -8,6 +8,7 @@ from .protocol import Location from .protocol import LocationLink from .protocol import Notification +from .protocol import MarkupContent from .protocol import Point from .protocol import Range from .protocol import Request @@ -392,7 +393,7 @@ def update_lsp_popup(view: sublime.View, contents: str, md: bool = False, css: O FORMAT_MARKUP_CONTENT = 0x4 -def minihtml(view: sublime.View, content: Union[str, Dict[str, str], list], allowed_formats: int) -> str: +def minihtml(view: sublime.View, content: Union[str, MarkupContent, list], allowed_formats: int) -> str: """ Formats provided input content into markup accepted by minihtml. @@ -708,7 +709,7 @@ def format_completion( st_details = "" if can_resolve_completion_items or item.get("documentation"): - st_details += make_command_link("lsp_resolve_docs", "More", {"index": index}) + st_details += make_command_link("lsp_resolve_docs", "More", {"index": index, "session_name": session_name}) st_details += " | " if lsp_detail else "" st_details += "

{}

".format(lsp_detail) diff --git a/plugin/documents.py b/plugin/documents.py index 1fc485a93..6a69ec97c 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -16,6 +16,7 @@ from .core.protocol import SignatureHelp from .core.registry import best_session from .core.registry import LspTextCommand +from .core.registry import session_by_name from .core.registry import windows from .core.sessions import Session from .core.settings import userprefs @@ -59,8 +60,9 @@ Flags = int ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], Flags], None] +SessionName = str CompletionResponse = Union[List[CompletionItem], CompletionList, None] -CompletionResponseWithSession = Tuple[CompletionResponse, Session] +ResolvedCompletions = Tuple[CompletionResponse, SessionName] def is_regular_view(v: sublime.View) -> bool: @@ -621,14 +623,14 @@ def _on_query_completions_async(self, resolve_completion_list: ResolveCompletion return self.purge_changes_async() - completion_promises = [] # type: List[Promise[CompletionResponseWithSession]] + completion_promises = [] # type: List[Promise[ResolvedCompletions]] for session in sessions: def completion_request() -> Promise: return Promise(lambda resolve: session.send_request_async( Request.complete(text_document_position_params(self.view, location), self.view), - lambda res: resolve((res, session)), - lambda res: self._on_complete_error(res, resolve, session))) + lambda res: resolve((res, session.config.name)), + lambda res: self._on_complete_error(res, resolve, session.config.name))) completion_promises.append(completion_request()) @@ -637,13 +639,13 @@ def completion_request() -> Promise: lambda responses: self._on_all_settled(responses, resolve_completion_list)) def _on_complete_error(self, error: dict, - resolve_promise: ResolveFunc[CompletionResponseWithSession], session: Session) -> None: - resolve_promise((None, session)) + resolve_promise: ResolveFunc[ResolvedCompletions], session_name: str) -> None: + resolve_promise((None, session_name)) sublime.status_message('Completion error: ' + str(error.get('message'))) def _on_all_settled( self, - responses: Optional[List[CompletionResponseWithSession]], + responses: Optional[List[ResolvedCompletions]], resolve_completion_list: ResolveCompletionsFn ) -> None: if not responses: @@ -657,7 +659,11 @@ def _on_all_settled( if prefs.inhibit_word_completions: flags |= sublime.INHIBIT_WORD_COMPLETIONS - for response, session in responses: + for response, session_name in responses: + session = session_by_name(self.view, session_name) + if not session: + continue + response_items = [] # type: List[CompletionItem] if isinstance(response, dict): response_items = response["items"] or [] @@ -667,7 +673,7 @@ def _on_all_settled( response_items = response response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) LspResolveDocsCommand.completions.extend(response_items) - can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider')) + can_resolve_completion_items = session.has_capability('completionProvider.resolveProvider') items.extend( [format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) for index, response_item in enumerate(response_items)]) From 4432bc98084b5a27b266328c888f07cff95b092e Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 22:03:20 +0100 Subject: [PATCH 14/30] remove promise if statement --- plugin/documents.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 6a69ec97c..840e3d2b9 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -645,12 +645,9 @@ def _on_complete_error(self, error: dict, def _on_all_settled( self, - responses: Optional[List[ResolvedCompletions]], + responses: List[ResolvedCompletions], resolve_completion_list: ResolveCompletionsFn ) -> None: - if not responses: - resolve_completion_list([], 0) - return items = [] # type: List[sublime.CompletionItem] flags = 0 # int prefs = userprefs() From f4ba0329fa9fb780024e49a7631810b8f74cd6e4 Mon Sep 17 00:00:00 2001 From: Predrag Date: Tue, 16 Feb 2021 22:27:28 +0100 Subject: [PATCH 15/30] make mypy happy make me sad because of unnecessary casts: plugin/core/views.py:453 plugin/core/views.py:458 --- plugin/core/protocol.py | 10 ++++++++-- plugin/core/views.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index a8ee58391..1f5535081 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -58,6 +58,12 @@ class SignatureHelpTriggerKind: 'href': str }, total=True) +MarkedStringDict = TypedDict('MarkedStringDict', { + 'language': str, + 'value': str +}, total=True) +MarkedString = Union[str, MarkedStringDict] + MarkupContent = TypedDict('MarkupContent', { 'kind': str, 'value': str @@ -97,13 +103,13 @@ class SignatureHelpTriggerKind: ParameterInformation = TypedDict('ParameterInformation', { 'label': Union[str, List[int]], - 'documentation': Union[str, Dict[str, str]] + 'documentation': Union[str, MarkupContent] }, total=False) SignatureInformation = TypedDict('SignatureInformation', { 'label': str, - 'documentation': Union[str, Dict[str, str]], + 'documentation': Union[str, MarkupContent], 'parameters': List[ParameterInformation] }, total=False) diff --git a/plugin/core/views.py b/plugin/core/views.py index 0194096f4..c0313145c 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -8,6 +8,8 @@ from .protocol import Location from .protocol import LocationLink from .protocol import Notification +from .protocol import MarkedString +from .protocol import MarkedStringDict from .protocol import MarkupContent from .protocol import Point from .protocol import Range @@ -392,7 +394,7 @@ def update_lsp_popup(view: sublime.View, contents: str, md: bool = False, css: O FORMAT_MARKUP_CONTENT = 0x4 -def minihtml(view: sublime.View, content: Union[str, MarkupContent, list], allowed_formats: int) -> str: +def minihtml(view: sublime.View, content: Union[str, MarkupContent, MarkedString, list], allowed_formats: int) -> str: """ Formats provided input content into markup accepted by minihtml. @@ -446,15 +448,15 @@ def minihtml(view: sublime.View, content: Union[str, MarkupContent, list], allow result = "\n".join(formatted) if (parse_marked_string or parse_markup_content) and isinstance(content, dict): # MarkupContent or MarkedString (dict) - language = content.get("language") - kind = content.get("kind") value = content.get("value") or "" - if parse_markup_content and kind: - # MarkupContent + if parse_markup_content and 'kind' in content: + content = cast(MarkupContent, content) # cast is not necessary for pyright, but required by mypy + kind = content.get("kind") is_plain_text = kind != "markdown" result = value - if parse_marked_string and language: - # MarkedString (dict) + if parse_marked_string and 'language' in content: + content = cast(MarkedStringDict, content) # cast is not necessary for pyright, but required by mypy + language = content.get("language") is_plain_text = False result = "```{}\n{}\n```\n".format(language, value) if is_plain_text: From 1f608ec0aa8382bcb7f4066305f796c5a6c3d2d9 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 17:51:17 +0100 Subject: [PATCH 16/30] remove global session_by_name function and add it as a method to DocumentSyncListener --- plugin/completion.py | 3 +-- plugin/core/registry.py | 20 +++++++------------- plugin/documents.py | 9 +++++++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index b003dbe66..92e28dce2 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -5,7 +5,6 @@ from .core.edit import parse_text_edit from .core.protocol import Request, InsertTextFormat, Range, CompletionItem, MarkupContent from .core.registry import LspTextCommand -from .core.registry import session_by_name from .core.typing import Any, List, Dict, Optional, Generator, Union from .core.views import FORMAT_STRING, FORMAT_MARKUP_CONTENT, minihtml from .core.views import range_to_region @@ -26,7 +25,7 @@ def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional if not detail or not documentation: # To make sure that the detail or documentation fields doesn't exist we need to resove the completion item. # If those fields appear after the item is resolved we show them in the popup. - session = session_by_name(self.view, session_name) + session = self.session_by_name(session_name) if session and session.has_capability('completionProvider.resolveProvider'): session.send_request(Request.resolveCompletionItem(item, self.view), self.handle_resolve_response) return diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 0924d0f9a..bbf072a84 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -29,17 +29,6 @@ def best_session(view: sublime.View, sessions: Iterable[Session], point: Optiona return None -def sessions(view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]: - yield from sessions_for_view(view, capability) - - -def session_by_name(view: sublime.View, session_name: str) -> Optional[Session]: - for session in sessions(view): - if session.config.name == session_name: - return session - return None - - configs = ConfigManager(client_configs.all) client_configs.set_listener(configs.update) windows = WindowRegistry(configs) @@ -79,7 +68,7 @@ def is_enabled(self, event: Optional[dict] = None, point: Optional[int] = None) return False if not self.capability and not self.session_name: # Any session will do. - return any(sessions(self.view)) + return any(self.sessions()) return True def want_event(self) -> bool: @@ -91,8 +80,13 @@ def best_session(self, capability: str, point: Optional[int] = None) -> Optional def session_by_name(self, name: Optional[str] = None) -> Optional[Session]: target = name if name else self.session_name - return session_by_name(self.view, target) + for session in self.sessions(): + if session.config.name == target: + return session + return None + def sessions(self, capability: Optional[str] = None) -> Generator[Session, None, None]: + yield from sessions_for_view(self.view, capability) class LspRestartClientCommand(sublime_plugin.TextCommand): def run(self, edit: Any) -> None: diff --git a/plugin/documents.py b/plugin/documents.py index 840e3d2b9..bc1a80dd7 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -16,7 +16,6 @@ from .core.protocol import SignatureHelp from .core.registry import best_session from .core.registry import LspTextCommand -from .core.registry import session_by_name from .core.registry import windows from .core.sessions import Session from .core.settings import userprefs @@ -657,7 +656,7 @@ def _on_all_settled( flags |= sublime.INHIBIT_WORD_COMPLETIONS for response, session_name in responses: - session = session_by_name(self.view, session_name) + session = self.session_by_name(session_name) if not session: continue @@ -696,6 +695,12 @@ def sessions(self, capability: Optional[str]) -> Generator[Session, None, None]: def session(self, capability: str, point: Optional[int] = None) -> Optional[Session]: return best_session(self.view, self.sessions(capability), point) + def session_by_name(self, name: Optional[str] = None) -> Optional[Session]: + for sb in self.session_buffers_async(): + if sb.session.config.name == name: + return sb.session + return None + def get_capability_async(self, session: Session, capability_path: str) -> Optional[Any]: for sv in self.session_views_async(): if sv.session == session: From 0183d43fc4789a543823e68edd50da5355d15e6b Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 17:52:07 +0100 Subject: [PATCH 17/30] fix pyflake --- plugin/core/registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/core/registry.py b/plugin/core/registry.py index bbf072a84..915c539e4 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -88,6 +88,7 @@ def session_by_name(self, name: Optional[str] = None) -> Optional[Session]: def sessions(self, capability: Optional[str] = None) -> Generator[Session, None, None]: yield from sessions_for_view(self.view, capability) + class LspRestartClientCommand(sublime_plugin.TextCommand): def run(self, edit: Any) -> None: window = self.view.window() From d19284c801fc98e107609e795c9d4e9f6a4b5929 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 18:12:42 +0100 Subject: [PATCH 18/30] use send_request_task --- plugin/documents.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index bc1a80dd7..6c949d538 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -10,6 +10,7 @@ from .core.protocol import Command from .core.protocol import Diagnostic from .core.protocol import DocumentHighlightKind +from .core.protocol import Error from .core.protocol import Notification from .core.protocol import Range from .core.protocol import Request @@ -61,7 +62,7 @@ SessionName = str CompletionResponse = Union[List[CompletionItem], CompletionList, None] -ResolvedCompletions = Tuple[CompletionResponse, SessionName] +ResolvedCompletions = Tuple[Union[CompletionResponse, Error], SessionName] def is_regular_view(v: sublime.View) -> bool: @@ -625,11 +626,10 @@ def _on_query_completions_async(self, resolve_completion_list: ResolveCompletion completion_promises = [] # type: List[Promise[ResolvedCompletions]] for session in sessions: - def completion_request() -> Promise: - return Promise(lambda resolve: session.send_request_async( - Request.complete(text_document_position_params(self.view, location), self.view), - lambda res: resolve((res, session.config.name)), - lambda res: self._on_complete_error(res, resolve, session.config.name))) + def completion_request() -> Promise[ResolvedCompletions]: + return session.send_request_task( + Request.complete(text_document_position_params(self.view, location), self.view) + ).then(lambda response: (response, session.config.name)) completion_promises.append(completion_request()) @@ -637,11 +637,6 @@ def completion_request() -> Promise: Promise.all(completion_promises).then( lambda responses: self._on_all_settled(responses, resolve_completion_list)) - def _on_complete_error(self, error: dict, - resolve_promise: ResolveFunc[ResolvedCompletions], session_name: str) -> None: - resolve_promise((None, session_name)) - sublime.status_message('Completion error: ' + str(error.get('message'))) - def _on_all_settled( self, responses: List[ResolvedCompletions], @@ -656,6 +651,10 @@ def _on_all_settled( flags |= sublime.INHIBIT_WORD_COMPLETIONS for response, session_name in responses: + if isinstance(response, Error): + sublime.status_message('Completion error: '.format(response)) + continue + session = self.session_by_name(session_name) if not session: continue From 8f0d43e13d664d5e442337c4498c2b18a247a526 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 18:49:23 +0100 Subject: [PATCH 19/30] remove types from protocol, but leave some I left `CompletionItem = Dict[str, Any]` because it gives me one place later to transform it from a Dict to a TypedDict. Will be done in a separate PR. --- plugin/completion.py | 4 ++-- plugin/core/protocol.py | 45 +++-------------------------------------- plugin/core/views.py | 17 +++++++--------- plugin/documents.py | 2 +- 4 files changed, 13 insertions(+), 55 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 92e28dce2..f748d45ca 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -3,7 +3,7 @@ import webbrowser from .core.logging import debug from .core.edit import parse_text_edit -from .core.protocol import Request, InsertTextFormat, Range, CompletionItem, MarkupContent +from .core.protocol import Request, InsertTextFormat, Range, CompletionItem from .core.registry import LspTextCommand from .core.typing import Any, List, Dict, Optional, Generator, Union from .core.views import FORMAT_STRING, FORMAT_MARKUP_CONTENT, minihtml @@ -32,7 +32,7 @@ def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional minihtml_content = self.get_content(documentation, detail) self.show_popup(minihtml_content) - def format_documentation(self, content: Union[str, MarkupContent]) -> str: + def format_documentation(self, content: Union[str, Dict[str, str]]) -> str: return minihtml(self.view, content, allowed_formats=FORMAT_STRING | FORMAT_MARKUP_CONTENT) def get_content(self, documentation: str, detail: str) -> str: diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 1f5535081..cc6df6831 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -58,16 +58,6 @@ class SignatureHelpTriggerKind: 'href': str }, total=True) -MarkedStringDict = TypedDict('MarkedStringDict', { - 'language': str, - 'value': str -}, total=True) -MarkedString = Union[str, MarkedStringDict] - -MarkupContent = TypedDict('MarkupContent', { - 'kind': str, - 'value': str -}, total=True) ExecuteCommandParams = TypedDict('ExecuteCommandParams', { 'command': str, @@ -103,13 +93,13 @@ class SignatureHelpTriggerKind: ParameterInformation = TypedDict('ParameterInformation', { 'label': Union[str, List[int]], - 'documentation': Union[str, MarkupContent] + 'documentation': Union[str, Dict[str, str]] }, total=False) SignatureInformation = TypedDict('SignatureInformation', { 'label': str, - 'documentation': Union[str, MarkupContent], + 'documentation': Union[str, Dict[str, str]], 'parameters': List[ParameterInformation] }, total=False) @@ -177,36 +167,7 @@ class SignatureHelpTriggerKind: 'relatedInformation': List[DiagnosticRelatedInformation] }, total=False) -TextEdit = TypedDict('TextEdit', { - 'range': RangeLsp, - 'newText': str -}, total=True) - -InsertReplaceEdit = TypedDict('InsertReplaceEdit', { - 'replace': RangeLsp, - 'insert': RangeLsp, - 'newText': str -}, total=True) - -CompletionItem = TypedDict('CompletionItem', { - 'label': str, - 'kind': Optional[int], - 'tags': Optional[List[int]], - 'detail': Optional[str], - 'documentation': Optional[Union[str, MarkupContent]], - 'deprecated': Optional[bool], - 'preselect': Optional[bool], - 'sortText': Optional[str], - 'filterText': Optional[str], - 'insertText': Optional[str], - 'insertTextFormat': Optional[int], - 'insertTextMode': Optional[int], - 'textEdit': Optional[Union[TextEdit, InsertReplaceEdit]], - 'additionalTextEdits': Optional[List[TextEdit]], - 'commitCharacters': Optional[List[str]], - 'command': Optional[Command], - 'data': Any -}, total=False) +CompletionItem = Dict[str, Any] CompletionList = TypedDict('CompletionList', { 'isIncomplete': bool, diff --git a/plugin/core/views.py b/plugin/core/views.py index c0313145c..7f41d9d20 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -8,9 +8,6 @@ from .protocol import Location from .protocol import LocationLink from .protocol import Notification -from .protocol import MarkedString -from .protocol import MarkedStringDict -from .protocol import MarkupContent from .protocol import Point from .protocol import Range from .protocol import Request @@ -394,7 +391,7 @@ def update_lsp_popup(view: sublime.View, contents: str, md: bool = False, css: O FORMAT_MARKUP_CONTENT = 0x4 -def minihtml(view: sublime.View, content: Union[str, MarkupContent, MarkedString, list], allowed_formats: int) -> str: +def minihtml(view: sublime.View, content: Union[str, Dict[str, str], list], allowed_formats: int) -> str: """ Formats provided input content into markup accepted by minihtml. @@ -448,15 +445,15 @@ def minihtml(view: sublime.View, content: Union[str, MarkupContent, MarkedString result = "\n".join(formatted) if (parse_marked_string or parse_markup_content) and isinstance(content, dict): # MarkupContent or MarkedString (dict) + language = content.get("language") + kind = content.get("kind") value = content.get("value") or "" - if parse_markup_content and 'kind' in content: - content = cast(MarkupContent, content) # cast is not necessary for pyright, but required by mypy - kind = content.get("kind") + if parse_markup_content and kind: + # MarkupContent is_plain_text = kind != "markdown" result = value - if parse_marked_string and 'language' in content: - content = cast(MarkedStringDict, content) # cast is not necessary for pyright, but required by mypy - language = content.get("language") + if parse_marked_string and language: + # MarkedString (dict) is_plain_text = False result = "```{}\n{}\n```\n".format(language, value) if is_plain_text: diff --git a/plugin/documents.py b/plugin/documents.py index 6c949d538..438c8087f 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -652,7 +652,7 @@ def _on_all_settled( for response, session_name in responses: if isinstance(response, Error): - sublime.status_message('Completion error: '.format(response)) + sublime.status_message('Completion error: {}'.format(response)) continue session = self.session_by_name(session_name) From fa61e6a87101d144fac7b3ceb48c41c04af0a74d Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 18:49:39 +0100 Subject: [PATCH 20/30] remove unused import --- plugin/documents.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index 438c8087f..31dea5bb1 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -3,7 +3,6 @@ from .completion import LspResolveDocsCommand from .core.logging import debug from .core.promise import Promise -from .core.promise import ResolveFunc from .core.protocol import CodeLens from .core.protocol import CompletionItem from .core.protocol import CompletionList From 369c895a483072175daab5b60792dc269eb5ea20 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 21:08:15 +0100 Subject: [PATCH 21/30] fix import order --- plugin/documents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index 31dea5bb1..28b48c439 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -4,9 +4,9 @@ from .core.logging import debug from .core.promise import Promise from .core.protocol import CodeLens +from .core.protocol import Command from .core.protocol import CompletionItem from .core.protocol import CompletionList -from .core.protocol import Command from .core.protocol import Diagnostic from .core.protocol import DocumentHighlightKind from .core.protocol import Error From d2deb69dac795c0d6c4d9b68ee2f4524598893b1 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 21:09:27 +0100 Subject: [PATCH 22/30] remove empty lines --- plugin/documents.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 28b48c439..119e11a62 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -621,7 +621,6 @@ def _on_query_completions_async(self, resolve_completion_list: ResolveCompletion resolve_completion_list([], 0) return self.purge_changes_async() - completion_promises = [] # type: List[Promise[ResolvedCompletions]] for session in sessions: @@ -631,7 +630,6 @@ def completion_request() -> Promise[ResolvedCompletions]: ).then(lambda response: (response, session.config.name)) completion_promises.append(completion_request()) - LspResolveDocsCommand.completions = [] Promise.all(completion_promises).then( lambda responses: self._on_all_settled(responses, resolve_completion_list)) From 04345e223e9522b79241751db82fd30464f83319 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 21:10:58 +0100 Subject: [PATCH 23/30] omit "[" and "]" --- plugin/documents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 119e11a62..0b0dfc5a9 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -667,8 +667,8 @@ def _on_all_settled( LspResolveDocsCommand.completions.extend(response_items) can_resolve_completion_items = session.has_capability('completionProvider.resolveProvider') items.extend( - [format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) - for index, response_item in enumerate(response_items)]) + format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) + for index, response_item in enumerate(response_items)) if items: flags |= sublime.INHIBIT_REORDER resolve_completion_list(items, flags) From a6f7189e389d1b44a05b03d443c4dd3b0f13c8f0 Mon Sep 17 00:00:00 2001 From: Predrag Date: Sat, 20 Feb 2021 21:20:05 +0100 Subject: [PATCH 24/30] fix possible error message overlaping, instead show all error messages --- plugin/documents.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 0b0dfc5a9..f0cd5c1f1 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -640,22 +640,20 @@ def _on_all_settled( resolve_completion_list: ResolveCompletionsFn ) -> None: items = [] # type: List[sublime.CompletionItem] + errors = [] # type: List[Error] flags = 0 # int prefs = userprefs() if prefs.inhibit_snippet_completions: flags |= sublime.INHIBIT_EXPLICIT_COMPLETIONS if prefs.inhibit_word_completions: flags |= sublime.INHIBIT_WORD_COMPLETIONS - for response, session_name in responses: if isinstance(response, Error): - sublime.status_message('Completion error: {}'.format(response)) + errors.append(response) continue - session = self.session_by_name(session_name) if not session: continue - response_items = [] # type: List[CompletionItem] if isinstance(response, dict): response_items = response["items"] or [] @@ -671,6 +669,9 @@ def _on_all_settled( for index, response_item in enumerate(response_items)) if items: flags |= sublime.INHIBIT_REORDER + if errors: + error_messages = ", ".join(str(error) for error in errors) + sublime.status_message('Completion error: {}'.format(error_messages)) resolve_completion_list(items, flags) # --- Public utility methods --------------------------------------------------------------------------------------- From 60b88699b23ef5331409ffb17dbb8625a3cf7d74 Mon Sep 17 00:00:00 2001 From: Predrag Date: Wed, 24 Feb 2021 23:59:14 +0100 Subject: [PATCH 25/30] fix: mutability bug format_completion index for the first completion item will be 0, second 2, third 4 after this PR the index will be 0, second 1, third 2 --- plugin/documents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index a79b6abe2..f7a1dc925 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -674,8 +674,9 @@ def _on_all_settled( response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) LspResolveDocsCommand.completions.extend(response_items) can_resolve_completion_items = session.has_capability('completionProvider.resolveProvider') + items_count = len(items) items.extend( - format_completion(response_item, len(items) + index, can_resolve_completion_items, session.config.name) + format_completion(response_item, items_count + index, can_resolve_completion_items, session.config.name) for index, response_item in enumerate(response_items)) if items: flags |= sublime.INHIBIT_REORDER From 01a98131d403ef89c79d442e5b9f36eb28e0c3b7 Mon Sep 17 00:00:00 2001 From: Predrag Nikolic Date: Tue, 2 Mar 2021 22:00:49 +0100 Subject: [PATCH 26/30] save recieved completions in a dict grouped by session name, instead of saving all results in one list --- plugin/completion.py | 5 +++-- plugin/documents.py | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index f748d45ca..61356d4a9 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -11,13 +11,14 @@ from .core.views import show_lsp_popup from .core.views import update_lsp_popup +SessionName = str class LspResolveDocsCommand(LspTextCommand): - completions = [] # type: List[CompletionItem] + completions = {} # type: Dict[SessionName, List[CompletionItem]] def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional[dict] = None) -> None: - item = self.completions[index] + item = self.completions[session_name][index] detail = self.format_documentation(item.get('detail') or "") documentation = self.format_documentation(item.get("documentation") or "") # don't show the detail in the cooperate AC popup if it is already shown in the AC details filed. diff --git a/plugin/documents.py b/plugin/documents.py index f7a1dc925..ea8a3f08a 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -640,7 +640,7 @@ def completion_request() -> Promise[ResolvedCompletions]: ).then(lambda response: (response, session.config.name)) completion_promises.append(completion_request()) - LspResolveDocsCommand.completions = [] + LspResolveDocsCommand.completions = {} Promise.all(completion_promises).then( lambda responses: self._on_all_settled(responses, resolve_completion_list)) @@ -672,11 +672,10 @@ def _on_all_settled( elif isinstance(response, list): response_items = response response_items = sorted(response_items, key=lambda item: item.get("sortText") or item["label"]) - LspResolveDocsCommand.completions.extend(response_items) + LspResolveDocsCommand.completions[session_name] = response_items can_resolve_completion_items = session.has_capability('completionProvider.resolveProvider') - items_count = len(items) items.extend( - format_completion(response_item, items_count + index, can_resolve_completion_items, session.config.name) + format_completion(response_item, index, can_resolve_completion_items, session.config.name) for index, response_item in enumerate(response_items)) if items: flags |= sublime.INHIBIT_REORDER From 934db7adeac915a078476d413bede14b460d9168 Mon Sep 17 00:00:00 2001 From: Predrag Nikolic Date: Tue, 2 Mar 2021 22:22:16 +0100 Subject: [PATCH 27/30] fix flake --- plugin/completion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/completion.py b/plugin/completion.py index 61356d4a9..3ef0ee08a 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -13,6 +13,7 @@ SessionName = str + class LspResolveDocsCommand(LspTextCommand): completions = {} # type: Dict[SessionName, List[CompletionItem]] From 64a60e82a65f386dbdc1e02bb0cad4372102f22d Mon Sep 17 00:00:00 2001 From: Predrag Nikolic Date: Wed, 3 Mar 2021 20:55:53 +0100 Subject: [PATCH 28/30] move the clearing of completions to on_all_settled --- plugin/documents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index ea8a3f08a..647fd0a91 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -640,7 +640,7 @@ def completion_request() -> Promise[ResolvedCompletions]: ).then(lambda response: (response, session.config.name)) completion_promises.append(completion_request()) - LspResolveDocsCommand.completions = {} + Promise.all(completion_promises).then( lambda responses: self._on_all_settled(responses, resolve_completion_list)) @@ -649,6 +649,7 @@ def _on_all_settled( responses: List[ResolvedCompletions], resolve_completion_list: ResolveCompletionsFn ) -> None: + LspResolveDocsCommand.completions = {} items = [] # type: List[sublime.CompletionItem] errors = [] # type: List[Error] flags = 0 # int From 1a02f822fe25aad1a64c89393cbebe8ef2faf6db Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 7 Mar 2021 19:00:17 +0100 Subject: [PATCH 29/30] Account for view-specific capabilities --- plugin/completion.py | 18 +++++++++++------- plugin/core/registry.py | 13 +++++++++---- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index 3ef0ee08a..a2acf15b6 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -25,12 +25,16 @@ def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional # don't show the detail in the cooperate AC popup if it is already shown in the AC details filed. self.is_detail_shown = bool(detail) if not detail or not documentation: - # To make sure that the detail or documentation fields doesn't exist we need to resove the completion item. - # If those fields appear after the item is resolved we show them in the popup. - session = self.session_by_name(session_name) - if session and session.has_capability('completionProvider.resolveProvider'): - session.send_request(Request.resolveCompletionItem(item, self.view), self.handle_resolve_response) - return + + def run_async() -> None: + # To make sure that the detail or documentation fields doesn't exist we need to resove the completion + # item. If those fields appear after the item is resolved we show them in the popup. + session = self.session_by_name(session_name, 'completionProvider.resolveProvider') + if session: + request = Request.resolveCompletionItem(item, self.view) + session.send_request_async(request, self.handle_resolve_response_async) + + sublime.set_timeout_async(run_async) minihtml_content = self.get_content(documentation, detail) self.show_popup(minihtml_content) @@ -56,7 +60,7 @@ def show_popup(self, minihtml_content: str) -> None: def on_navigate(self, url: str) -> None: webbrowser.open(url) - def handle_resolve_response(self, item: Optional[dict]) -> None: + def handle_resolve_response_async(self, item: Optional[dict]) -> None: detail = "" documentation = "" if item: diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 4139f7c9b..7b398c9c1 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -81,11 +81,16 @@ def best_session(self, capability: str, point: Optional[int] = None) -> Optional listener = windows.listener_for_view(self.view) return listener.session(capability, point) if listener else None - def session_by_name(self, name: Optional[str] = None) -> Optional[Session]: + def session_by_name(self, name: Optional[str] = None, capability_path: Optional[str] = None) -> Optional[Session]: target = name if name else self.session_name - for session in self.sessions(): - if session.config.name == target: - return session + listener = windows.listener_for_view(self.view) + if listener: + for sv in listener.session_views_async(): + if sv.session.config.name == target: + if capability_path is None or sv.has_capability_async(capability_path): + return sv.session + else: + return None return None def sessions(self, capability: Optional[str] = None) -> Generator[Session, None, None]: From 4f0d59151bbc7e0627abde5fea797c39fffff9ad Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Sun, 7 Mar 2021 20:01:51 +0100 Subject: [PATCH 30/30] Forgot a return statement --- plugin/completion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/completion.py b/plugin/completion.py index a2acf15b6..67fb83465 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -34,7 +34,7 @@ def run_async() -> None: request = Request.resolveCompletionItem(item, self.view) session.send_request_async(request, self.handle_resolve_response_async) - sublime.set_timeout_async(run_async) + return sublime.set_timeout_async(run_async) minihtml_content = self.get_content(documentation, detail) self.show_popup(minihtml_content)