Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send completion request to multiple sessions #1582

Merged
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cfecfdc
send completion request to multiple sessions
predragnikolic Feb 13, 2021
ebd124a
must call the function to return the promise :)
predragnikolic Feb 13, 2021
20dd3e7
add types for CompletionItem and CompletionList
predragnikolic Feb 13, 2021
633d9ca
changes needed to fix LspResolveDocsCommand.completions
predragnikolic Feb 14, 2021
66d0666
mypy and flake fixes
predragnikolic Feb 14, 2021
6c6837d
fix failing test - I expect the popup to not be visible if there are …
predragnikolic Feb 14, 2021
9074230
fix flake
predragnikolic Feb 14, 2021
08c4f3d
not all keys are required when constructing a CompletionItem
predragnikolic Feb 15, 2021
f8dc0ea
add markup content
predragnikolic Feb 15, 2021
c1d1948
separate lines to be consistent
predragnikolic Feb 15, 2021
14c0e41
resolve earlier
predragnikolic Feb 16, 2021
c0a93c7
move sessions, and session_by_name methods to functions for easier re…
predragnikolic Feb 16, 2021
64e3e29
use session_name to get the session
predragnikolic Feb 16, 2021
adda5e4
Merge branch 'st4000-exploration' into send_completion_request_to_mul…
predragnikolic Feb 16, 2021
4432bc9
remove promise if statement
predragnikolic Feb 16, 2021
f4ba032
make mypy happy
predragnikolic Feb 16, 2021
1f608ec
remove global session_by_name function and add it as a method to Docu…
predragnikolic Feb 20, 2021
0183d43
fix pyflake
predragnikolic Feb 20, 2021
d19284c
use send_request_task
predragnikolic Feb 20, 2021
8f0d43e
remove types from protocol, but leave some
predragnikolic Feb 20, 2021
fa61e6a
remove unused import
predragnikolic Feb 20, 2021
369c895
fix import order
predragnikolic Feb 20, 2021
d2deb69
remove empty lines
predragnikolic Feb 20, 2021
04345e2
omit "[" and "]"
predragnikolic Feb 20, 2021
a6f7189
fix possible error message overlaping, instead show all error messages
predragnikolic Feb 20, 2021
4281463
Merge branch 'st4000-exploration' into send_completion_request_to_mul…
predragnikolic Feb 20, 2021
60b8869
fix: mutability bug
predragnikolic Feb 24, 2021
01a9813
save recieved completions in a dict grouped by session name, instead …
predrag-codetribe Mar 2, 2021
934db7a
fix flake
predrag-codetribe Mar 2, 2021
64a60e8
move the clearing of completions to on_all_settled
predrag-codetribe Mar 3, 2021
d6897e7
Merge branch 'st4000-exploration' into send_completion_request_to_mul…
rwols Mar 7, 2021
1a02f82
Account for view-specific capabilities
rwols Mar 7, 2021
4f0d591
Forgot a return statement
rwols Mar 7, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)

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())

predragnikolic marked this conversation as resolved.
Show resolved Hide resolved
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"])
rwols marked this conversation as resolved.
Show resolved Hide resolved
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
rwols marked this conversation as resolved.
Show resolved Hide resolved

def test_simple_label(self) -> 'Generator':
yield from self.verify(
Expand Down