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

New server configuration key: auto_complete_selector #1408

Merged
merged 14 commits into from
Nov 5, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
3 changes: 2 additions & 1 deletion LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@
"document_selector": "source.objc++",
"languageId": "objective-cpp"
},
]
],
"auto_complete_selector": "punctuation.accessor | (meta.preprocessor.include string - punctuation.definition.string.end)",
},
"dart": {
"command": [
Expand Down
15 changes: 12 additions & 3 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,10 @@ class SessionViewProtocol(Protocol):
listener = None # type: Any
session_buffer = None # type: Any

def on_capability_added_async(self, capability_path: str, options: Dict[str, Any]) -> None:
def on_capability_added_async(self, registration_id: str, capability_path: str, options: Dict[str, Any]) -> None:
...

def on_capability_removed_async(self, discarded_capabilities: Dict[str, Any]) -> None:
def on_capability_removed_async(self, registration_id: str, discarded_capabilities: Dict[str, Any]) -> None:
...

def has_capability_async(self, capability_path: str) -> bool:
Expand Down Expand Up @@ -971,6 +971,10 @@ def m_client_registerCapability(self, params: Any, request_id: Any) -> None:
else:
# The registration applies globally to all buffers.
self.capabilities.register(registration_id, capability_path, registration_path, options)
# We must inform our SessionViews of the new capabilities, in case it's for instance a hoverProvider
# or a completionProvider for trigger characters.
for sv in self.session_views_async():
sv.on_capability_added_async(registration_id, capability_path, options)
self.send_response(Response(request_id, None))

def m_client_unregisterCapability(self, params: Any, request_id: Any) -> None:
Expand All @@ -986,7 +990,12 @@ def m_client_unregisterCapability(self, params: Any, request_id: Any) -> None:
self.send_error_response(request_id, Error(ErrorCode.InvalidParams, message))
return
elif not data.selector:
self.capabilities.unregister(registration_id, capability_path, registration_path)
discarded = self.capabilities.unregister(registration_id, capability_path, registration_path)
# We must inform our SessionViews of the removed capabilities, in case it's for instance a hoverProvider
# or a completionProvider for trigger characters.
if isinstance(discarded, dict):
for sv in self.session_views_async():
sv.on_capability_removed_async(registration_id, discarded)
self.send_response(Response(request_id, None))

def m_window_workDoneProgress_create(self, params: Any, request_id: Any) -> None:
Expand Down
10 changes: 10 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ def __init__(self,
command: Optional[List[str]] = None,
binary_args: Optional[List[str]] = None, # DEPRECATED
tcp_port: Optional[int] = None,
auto_complete_selector: Optional[str] = None,
ignore_server_triggers: bool = False,
rwols marked this conversation as resolved.
Show resolved Hide resolved
enabled: bool = True,
init_options: DottedDict = DottedDict(),
settings: DottedDict = DottedDict(),
Expand All @@ -519,6 +521,8 @@ def __init__(self,
self.command = binary_args
self.languages = languages
self.tcp_port = tcp_port
self.auto_complete_selector = auto_complete_selector
self.ignore_server_triggers = ignore_server_triggers
self.enabled = enabled
self.init_options = init_options
self.settings = settings
Expand All @@ -538,6 +542,8 @@ def from_sublime_settings(cls, name: str, s: sublime.Settings, file: str) -> "Cl
command=read_list_setting(s, "command", []),
languages=_read_language_configs(s),
tcp_port=s.get("tcp_port"),
auto_complete_selector=s.get("auto_complete_selector"),
ignore_server_triggers=bool(s.get("ignore_server_triggers", False)),
# Default to True, because an LSP plugin is enabled iff it is enabled as a Sublime package.
enabled=bool(s.get("enabled", True)),
init_options=init_options,
Expand All @@ -553,6 +559,8 @@ def from_dict(cls, name: str, d: Dict[str, Any]) -> "ClientConfig":
command=d.get("command", []),
languages=_read_language_configs(d),
tcp_port=d.get("tcp_port"),
auto_complete_selector=d.get("auto_complete_selector"),
ignore_server_triggers=bool(d.get("ignore_server_triggers", False)),
enabled=d.get("enabled", False),
init_options=DottedDict(d.get("initializationOptions")),
settings=DottedDict(d.get("settings")),
Expand All @@ -569,6 +577,8 @@ def update(self, override: Dict[str, Any]) -> "ClientConfig":
command=override.get("command", self.command),
languages=languages,
tcp_port=override.get("tcp_port", self.tcp_port),
auto_complete_selector=override.get("auto_complete_selector", self.auto_complete_selector),
ignore_server_triggers=bool(override.get("ignore_server_triggers", self.ignore_server_triggers)),
enabled=override.get("enabled", self.enabled),
init_options=DottedDict.from_base_and_override(self.init_options, override.get("initializationOptions")),
settings=DottedDict.from_base_and_override(self.settings, override.get("settings")),
Expand Down
21 changes: 0 additions & 21 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,6 @@ def _on_query_completions_async(self, promise: sublime.CompletionList, location:
if not session:
resolve(promise, [])
return
self._apply_view_settings(session)
rwols marked this conversation as resolved.
Show resolved Hide resolved
self.purge_changes_async()
can_resolve_completion_items = bool(session.get_capability('completionProvider.resolveProvider'))
config_name = session.config.name
Expand All @@ -531,26 +530,6 @@ def _on_query_completions_async(self, promise: sublime.CompletionList, location:
lambda res: self._on_complete_result(res, promise, can_resolve_completion_items, config_name),
lambda res: self._on_complete_error(res, promise))

def _apply_view_settings(self, session: Session) -> None:
settings = self.view.settings()
completion_triggers = settings.get('auto_complete_triggers') or [] # type: List[Dict[str, str]]
if any(trigger.get('server', None) == session.config.name for trigger in completion_triggers):
return
# This is to make ST match with labels that have a weird prefix like a space character.
settings.set('auto_complete_preserve_order', 'none')
trigger_chars = session.get_capability('completionProvider.triggerCharacters') or []
if trigger_chars:
completion_triggers.append({
'characters': "".join(trigger_chars),
# Heuristics: Don't auto-complete in comments, and don't trigger auto-complete when we're at the
# end of a string. We *do* want to trigger auto-complete in strings because of languages like
# Bash and some language servers are allowing the user to auto-complete file-system files in
# things like import statements. We may want to move this to the LSP.sublime-settings.
'selector': "- comment - punctuation.definition.string.end",
'server': session.config.name
})
settings.set('auto_complete_triggers', completion_triggers)

def _on_complete_result(self, response: Optional[Union[dict, List]], completion_list: sublime.CompletionList,
can_resolve_completion_items: bool, session_name: str) -> None:
response_items = [] # type: List[Dict]
Expand Down
4 changes: 2 additions & 2 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def register_capability_async(
self.capabilities.register(registration_id, capability_path, registration_path, options)
view = None # type: Optional[sublime.View]
for sv in self.session_views:
sv.on_capability_added_async(capability_path, options)
sv.on_capability_added_async(registration_id, capability_path, options)
if view is None:
view = sv.view
if view is not None:
Expand All @@ -141,7 +141,7 @@ def unregister_capability_async(
if discarded is None:
return
for sv in self.session_views:
sv.on_capability_removed_async(discarded)
sv.on_capability_removed_async(registration_id, discarded)

def get_capability(self, capability_path: str) -> Optional[Any]:
value = self.capabilities.get(capability_path)
Expand Down
78 changes: 75 additions & 3 deletions plugin/session_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class SessionView:
SHOW_DEFINITIONS_KEY = "show_definitions"
HOVER_PROVIDER_KEY = "hoverProvider"
HOVER_PROVIDER_COUNT_KEY = "lsp_hover_provider_count"
AC_TRIGGERS_KEY = "auto_complete_triggers"
COMPLETION_PROVIDER_KEY = "completionProvider"
TRIGGER_CHARACTERS_KEY = "completionProvider.triggerCharacters"

_session_buffers = WeakValueDictionary() # type: WeakValueDictionary[Tuple[str, int], SessionBuffer]

Expand Down Expand Up @@ -54,16 +57,22 @@ def __init__(self, listener: AbstractViewListener, session: Session) -> None:
session.config.set_view_status(self.view, "")
if self.session.has_capability(self.HOVER_PROVIDER_KEY):
self._increment_hover_count()
self._clear_auto_complete_triggers(settings)
self._setup_auto_complete_triggers(settings)
# This is to make ST match with labels that have a weird prefix like a space character.
# TODO: Maybe remove this?
settings.set('auto_complete_preserve_order', 'none')

def __del__(self) -> None:
settings = self.view.settings() # type: sublime.Settings
self._clear_auto_complete_triggers(settings)
if self.session.has_capability(self.HOVER_PROVIDER_KEY):
self._decrement_hover_count()
# If the session is exiting then there's no point in sending textDocument/didClose and there's also no point
# in unregistering ourselves from the session.
if not self.session.exiting:
self.session.unregister_session_view_async(self)
self.session.config.erase_view_status(self.view)
settings = self.view.settings() # type: sublime.Settings
# TODO: Language ID must be UNIQUE!
languages = settings.get(self.LANGUAGE_ID_KEY)
if isinstance(languages, dict):
Expand All @@ -75,6 +84,63 @@ def __del__(self) -> None:
for severity in range(1, len(DIAGNOSTIC_SEVERITY) + 1):
self.view.erase_regions(self.diagnostics_key(severity))

def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None:
'''Remove all of our modifications to the view's "auto_complete_triggers"'''
triggers = settings.get(self.AC_TRIGGERS_KEY)
if isinstance(triggers, list):
triggers = [t for t in triggers if self.session.config.name != t.get("server", "")]
settings.set(self.AC_TRIGGERS_KEY, triggers)

def _setup_auto_complete_triggers(self, settings: sublime.Settings) -> None:
"""Register trigger characters from static capabilities of the server."""
trigger_chars = self.session.get_capability(self.TRIGGER_CHARACTERS_KEY)
if isinstance(trigger_chars, list):
self._apply_auto_complete_triggers(settings, trigger_chars)

def _register_auto_complete_triggers(self, registration_id: str, trigger_chars: List[str]) -> None:
"""Register trigger characters from a dynamic server registration."""
self._apply_auto_complete_triggers(self.view.settings(), trigger_chars, registration_id)

def _unregister_auto_complete_triggers(self, registration_id: str) -> None:
"""Remove trigger characters that were previously dynamically registered."""
settings = self.view.settings()
triggers = settings.get(self.AC_TRIGGERS_KEY)
if isinstance(triggers, list):
new_triggers = [] # type: List[Dict[str, str]]
name = self.session.config.name
for trigger in triggers:
if trigger.get("server", "") == name and trigger.get("registration_id", "") == registration_id:
continue
new_triggers.append(trigger)
settings.set(self.AC_TRIGGERS_KEY, triggers)

def _apply_auto_complete_triggers(
self,
settings: sublime.Settings,
trigger_chars: List[str],
registration_id: Optional[str] = None
) -> None:
"""This method actually modifies the auto_complete_triggers entries for the view."""
selector = self.session.config.auto_complete_selector
if not selector:
# If the user did not set up an auto_complete_selector for this server configuration, fallback to the
# "global" auto_complete_selector of the view.
selector = str(settings.get("auto_complete_selector"))
rchl marked this conversation as resolved.
Show resolved Hide resolved
trigger = {
"selector": selector,
# This key is not used by Sublime, but is used as a "breadcrumb" to figure out what needs to be removed
# from the auto_complete_triggers array once the session is stopped.
"server": self.session.config.name
}
if not self.session.config.ignore_server_triggers:
trigger["characters"] = "".join(trigger_chars)
if isinstance(registration_id, str):
# This key is not used by Sublime, but is used as a "breadcrumb" as well, for dynamic registrations.
trigger["registration_id"] = registration_id
triggers = settings.get(self.AC_TRIGGERS_KEY) or [] # type: List[Dict[str, str]]
triggers.append(trigger)
settings.set(self.AC_TRIGGERS_KEY, triggers)

def _increment_hover_count(self) -> None:
settings = self.view.settings()
count = settings.get(self.HOVER_PROVIDER_COUNT_KEY, 0)
Expand All @@ -99,13 +165,19 @@ def has_capability(self, capability_path: str) -> bool:
value = self.session_buffer.get_capability(capability_path)
return isinstance(value, dict) or bool(value)

def on_capability_added_async(self, capability_path: str, options: Dict[str, Any]) -> None:
def on_capability_added_async(self, registration_id: str, capability_path: str, options: Dict[str, Any]) -> None:
if capability_path == self.HOVER_PROVIDER_KEY:
self._increment_hover_count()
elif capability_path.startswith(self.COMPLETION_PROVIDER_KEY):
trigger_chars = options.get("triggerCharacters")
if isinstance(trigger_chars, list):
self._register_auto_complete_triggers(registration_id, trigger_chars)

def on_capability_removed_async(self, discarded: Dict[str, Any]) -> None:
def on_capability_removed_async(self, registration_id: str, discarded: Dict[str, Any]) -> None:
if self.HOVER_PROVIDER_KEY in discarded:
self._decrement_hover_count()
elif self.COMPLETION_PROVIDER_KEY in discarded:
self._unregister_auto_complete_triggers(registration_id)

def has_capability_async(self, capability_path: str) -> bool:
return self.session_buffer.has_capability(capability_path)
Expand Down
Loading