From 551127a68353cc527a25b746550e44e7268d3ea6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D1=80=D0=B0=D0=B3=20=D0=9D=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=D0=BB=D0=B8=D1=9B=20/=20Predrag=20Nikolic?= Date: Sun, 7 Mar 2021 20:09:51 +0100 Subject: [PATCH] Send completion request to multiple sessions (#1582) * changes needed to fix LspResolveDocsCommand.completions I needed to reaorganize the code to make the LspResolveDocsCommand.completions work properly * fix failing test - I expect the popup to not be visible if there are no completion items * save Received completions in a dict grouped by session name, instead of saving all results in one list --- plugin/completion.py | 28 +++++++----- plugin/core/protocol.py | 9 +++- plugin/core/registry.py | 13 ++++-- plugin/core/views.py | 7 +-- plugin/documents.py | 98 +++++++++++++++++++++++++++------------- tests/test_completion.py | 2 +- 6 files changed, 104 insertions(+), 53 deletions(-) diff --git a/plugin/completion.py b/plugin/completion.py index e9b5267ec..67fb83465 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 @@ -11,24 +11,30 @@ from .core.views import show_lsp_popup from .core.views import update_lsp_popup +SessionName = str + class LspResolveDocsCommand(LspTextCommand): - completions = [] # type: List[Dict[str, Any]] + completions = {} # type: Dict[SessionName, List[CompletionItem]] - def run(self, edit: sublime.Edit, index: int, event: Optional[dict] = None) -> None: - item = self.completions[index] + def run(self, edit: sublime.Edit, index: int, session_name: str, event: Optional[dict] = None) -> None: + 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. 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.best_session('completionProvider.resolveProvider') - if session: - 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) + + return sublime.set_timeout_async(run_async) minihtml_content = self.get_content(documentation, detail) self.show_popup(minihtml_content) @@ -54,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/protocol.py b/plugin/core/protocol.py index 362ff68a1..d0dd06eb0 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -150,13 +150,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, @@ -168,6 +166,13 @@ class SignatureHelpTriggerKind: 'relatedInformation': List[DiagnosticRelatedInformation] }, total=False) +CompletionItem = Dict[str, Any] + +CompletionList = TypedDict('CompletionList', { + 'isIncomplete': bool, + 'items': List[CompletionItem], +}, total=True) + class Request: 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]: diff --git a/plugin/core/views.py b/plugin/core/views.py index 4082d7e31..2e80c949d 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1,4 +1,5 @@ from .css import css as lsp_css +from .protocol import CompletionItem from .protocol import CompletionItemTag from .protocol import Diagnostic from .protocol import DiagnosticRelatedInformation @@ -672,7 +673,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 +683,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") @@ -700,7 +701,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}) if lsp_filter_text and lsp_filter_text != lsp_label: st_trigger = lsp_filter_text diff --git a/plugin/documents.py b/plugin/documents.py index 6bb722af0..15b54fcf9 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -2,10 +2,14 @@ from .code_actions import CodeActionsByConfigName from .completion import LspResolveDocsCommand 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 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 @@ -57,7 +61,12 @@ DocumentHighlightKind.Write: "region.yellowish markup.highlight.write.lsp" } -ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], int], None] +Flags = int +ResolveCompletionsFn = Callable[[List[sublime.CompletionItem], Flags], None] + +SessionName = str +CompletionResponse = Union[List[CompletionItem], CompletionList, None] +ResolvedCompletions = Tuple[Union[CompletionResponse, Error], SessionName] def is_regular_view(v: sublime.View) -> bool: @@ -629,46 +638,65 @@ 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, - can_resolve_completion_items: bool, session_name: str) -> None: - response_items = [] # type: List[Dict] - flags = 0 + completion_promises = [] # type: List[Promise[ResolvedCompletions]] + for session in sessions: + + 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()) + + Promise.all(completion_promises).then( + lambda responses: self._on_all_settled(responses, resolve_completion_list)) + + def _on_all_settled( + self, + responses: List[ResolvedCompletions], + resolve_completion_list: ResolveCompletionsFn + ) -> None: + LspResolveDocsCommand.completions = {} + 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 - 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_name in responses: + if isinstance(response, Error): + 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 [] + 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[session_name] = response_items + can_resolve_completion_items = session.has_capability('completionProvider.resolveProvider') + items.extend( + 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 - resolve(items, flags) - - def _on_complete_error(self, error: dict, resolve: ResolveCompletionsFn) -> None: - resolve([], 0) - LspResolveDocsCommand.completions = [] - sublime.status_message('Completion error: ' + str(error.get('message'))) + 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 --------------------------------------------------------------------------------------- @@ -688,6 +716,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: diff --git a/tests/test_completion.py b/tests/test_completion.py index ff22b3cae..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() + yield lambda: self.view.is_auto_complete_visible() is False def test_simple_label(self) -> 'Generator': yield from self.verify(