Skip to content

Commit

Permalink
Send completion request to multiple sessions (#1582)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
predragnikolic authored Mar 7, 2021
1 parent 3245597 commit 551127a
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 53 deletions.
28 changes: 17 additions & 11 deletions plugin/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,38 @@
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
from .core.views import range_to_region
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)

Expand All @@ -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:
Expand Down
9 changes: 7 additions & 2 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:

Expand Down
13 changes: 9 additions & 4 deletions plugin/core/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
7 changes: 4 additions & 3 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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
Expand Down
98 changes: 66 additions & 32 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 ---------------------------------------------------------------------------------------

Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 551127a

Please sign in to comment.