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 all 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
11 changes: 11 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_trigger_chars: bool = False,
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_trigger_chars = ignore_server_trigger_chars
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_trigger_chars=bool(s.get("ignore_server_trigger_chars", 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_trigger_chars=bool(d.get("ignore_server_trigger_chars", False)),
enabled=d.get("enabled", False),
init_options=DottedDict(d.get("initializationOptions")),
settings=DottedDict(d.get("settings")),
Expand All @@ -569,6 +577,9 @@ 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_trigger_chars=bool(
override.get("ignore_server_trigger_chars", self.ignore_server_trigger_chars)),
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_trigger_chars:
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
33 changes: 27 additions & 6 deletions sublime-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
},
"markdownDescription": "The command to start the language server."
},
"ClientAutoCompleteSelector": {
"type": "string",
"markdownDescription": "When specified, this [selector](https://www.sublimetext.com/docs/3/selectors.html) is used as the `\"selector\"` key in an entry of the `\"auto_complete_triggers\"` of the view applicable to this configuration. You don't have to necessarily provide this value. Because your language server registers so-called trigger characters in any case. However, this selector allows you to fine-tune the auto-complete behavior if the registered trigger characters of the language server result in an unpleasent auto-complete experience. Note that the behavior of this selector will depend on the .sublime-syntax in use.\n\nThis value is _not_ applied to the **global** `\"auto_complete_selector\"` setting of the view."
},
"ClientIgnoreServerTriggerChars": {
"type": "boolean",
"default": false,
"markdownDescription": "A language server may register a list of characters that, when typed, trigger the auto-complete. These days, there are Sublime syntaxes that are so good that it is possible to detect via a selector when the auto-complete should be triggered. This can result in a much better editor experience. When this is the case for your syntax, choose an appropriate selector for the `\"auto_complete_selector\"` and set this setting to `\"true\"`.\n\n**Hint.** You may look at `view.settings().get(\"auto_complete_triggers\")` in the Console to verify your settings."
},
"ClientInitializationOptions": {
"type": "object",
"markdownDescription": "The initializationOptions that are passed to the language server process during the _initialize_ phase. This is a rather free-form dictionary of key-value pairs and is different per language server. Look up documentation of your specific langauge server to see what the possible key-value pairs can be."
Expand Down Expand Up @@ -118,6 +127,12 @@
"default": 0,
"markdownDescription": "When specified, the TCP port to use to connect to the language server process. If not specified, STDIO is used as the transport. When set to zero, a free TCP port is chosen. You can use that free TCP port number as a template variable, i.e. as `${tcp_port}` in the `\"command\"`."
},
"auto_complete_selector": {
"$ref": "#/definitions/ClientAutoCompleteSelector"
},
"ignore_server_trigger_chars": {
"$ref": "#/definitions/ClientIgnoreServerTriggerChars"
},
rwols marked this conversation as resolved.
Show resolved Hide resolved
"languages": {
"$ref": "#/definitions/ClientLanguages"
},
Expand All @@ -128,7 +143,7 @@
"$ref": "#/definitions/ClientSettings"
},
"experimental_capabilities": {
"$ref": "#/definitions/ClientExperimentalCapabilities",
"$ref": "#/definitions/ClientExperimentalCapabilities"
},
"env": {
"$ref": "#/definitions/ClientEnv"
Expand Down Expand Up @@ -435,8 +450,8 @@
"markdownDescription": "The dictionary of your configured language servers or overrides for existing configurations. The keys of this dictionary are free-form. They give a humany-friendly name to the server configuration. They are shown in the bottom-left corner in the status bar once attached to a view (unless you have `\"show_view_status\"` set to `false`).",
"additionalProperties": {
"$ref": "sublime://settings/LSP#/definitions/ClientConfig"
},
},
}
}
}
}
}
Expand All @@ -459,14 +474,20 @@
"$ref": "sublime://settings/LSP#/definitions/ClientSettings"
},
"experimental_capabilities": {
"$ref": "sublime://settings/LSP#/definitions/ClientExperimentalCapabilities",
"$ref": "sublime://settings/LSP#/definitions/ClientExperimentalCapabilities"
},
"env": {
"$ref": "sublime://settings/LSP#/definitions/ClientEnv"
},
},
"auto_complete_selector": {
"$ref": "sublime://settings/LSP#/definitions/ClientAutoCompleteSelector"
},
"ignore_server_trigger_chars": {
"$ref": "sublime://settings/LSP#/definitions/ClientIgnoreServerTriggerChars"
}
}
}
},
}
]
}
}
Loading