Skip to content
Open
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
2 changes: 2 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .plugin.core.css import load as load_css
from .plugin.core.open import opening_files
from .plugin.core.panels import PanelName
from .plugin.core.registry import LspCheckApplicableCommand
from .plugin.core.registry import LspNextDiagnosticCommand
from .plugin.core.registry import LspOpenLocationCommand
from .plugin.core.registry import LspPrevDiagnosticCommand
Expand Down Expand Up @@ -94,6 +95,7 @@
"LspApplyDocumentEditCommand",
"LspApplyWorkspaceEditCommand",
"LspCallHierarchyCommand",
"LspCheckApplicableCommand",
"LspClearLogPanelCommand",
"LspClearPanelCommand",
"LspCodeActionsCommand",
Expand Down
3 changes: 3 additions & 0 deletions plugin/core/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def __init__(self, window: sublime.Window, global_configs: dict[str, ClientConfi
def add_change_listener(self, listener: WindowConfigChangeListener) -> None:
self._change_listeners.add(listener)

def get_config(self, config_name: str) -> ClientConfig | None:
return self.all.get(config_name)

def get_configs(self) -> list[ClientConfig]:
return sorted(self.all.values(), key=lambda config: config.name)

Expand Down
37 changes: 35 additions & 2 deletions plugin/core/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from ...protocol import Diagnostic
from ...protocol import Location
from ...protocol import LocationLink
from .logging import debug
from .sessions import AbstractViewListener
from .sessions import Session
from .url import parse_uri
from .views import first_selection_region
from .views import get_uri_and_position_from_location
from .views import MissingUriError
Expand All @@ -12,7 +14,7 @@
from .windows import WindowManager
from .windows import WindowRegistry
from functools import partial
from typing import Any, Generator, Iterable
from typing import Generator, Iterable
import operator
import sublime
import sublime_plugin
Expand Down Expand Up @@ -201,7 +203,7 @@ def _handle_continuation(self, location: Location | LocationLink, success: bool)

class LspRestartServerCommand(LspTextCommand):

def run(self, edit: Any, config_name: str | None = None) -> None:
def run(self, edit: sublime.Edit, config_name: str | None = None) -> None:
wm = windows.lookup(self.view.window())
if not wm:
return
Expand All @@ -226,6 +228,37 @@ def run_async() -> None:
sublime.set_timeout_async(run_async)


class LspCheckApplicableCommand(sublime_plugin.TextCommand):

def run(self, edit: sublime.Edit, session_name: str) -> None:
sublime.set_timeout_async(lambda: self._run_async(session_name))

def _run_async(self, session_name: str) -> None:
if wm := windows.lookup(self.view.window()):
config = wm.get_config_manager().get_config(session_name)
if not config:
debug(f'Configuration with name {session_name} does not exist')
return
listener = windows.listener_for_view(self.view)
if not listener:
debug(f'No listener for view {self.view}')
return
scheme, _ = parse_uri(uri_from_view(self.view))
is_applicable = config.match_view(self.view, scheme)
if session := wm.get_session(session_name, self.view.file_name() or ''):
session_view = session.session_view_for_view_async(self.view)
if is_applicable and not session_view:
listener.on_session_initialized_async(session)
elif not is_applicable and session_view:
session.shutdown_session_view_async(session_view)
elif is_applicable:
wm.start_async(config, self.view)
if wm._new_session:
wm._sessions.add(wm._new_session)
listener.on_session_initialized_async(wm._new_session)
wm._new_session = None
Comment on lines +256 to +259
Copy link
Member Author

@jwortmann jwortmann Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is pretty ugly, but the public WindowManager.start_async method is not actually sufficient to start a new session. That method sets an internal variable at

self._new_session = session
and it expects some more code to be run after that, in order to properly handle the start of a new session.

I won't touch the recursive

def _dequeue_listener_async(self) -> None:
method because it is a riddle to me how that works...

Starting a new session when triggered from the lsp_check_applicable TextCommand seems to work correctly now though.



def navigate_diagnostics(view: sublime.View, point: int | None, forward: bool = True) -> None:
try:
uri = uri_from_view(view)
Expand Down
43 changes: 29 additions & 14 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
from .views import get_uri_and_range_from_location
from .views import kind_contains_other_kind
from .views import MarkdownLangMap
from .views import uri_from_view
from .workspace import is_subpath_of
from .workspace import WorkspaceFolder
from abc import ABCMeta
Expand Down Expand Up @@ -917,13 +918,31 @@ def configuration(cls) -> tuple[sublime.Settings, str]:
return sublime.load_settings(basename), filepath

@classmethod
def selector(cls, view: sublime.View, config: ClientConfig) -> str:
def is_applicable(cls, view: sublime.View, config: ClientConfig) -> bool:
"""
Override the default selector used to determine whether server should run on the given view.
Determine whether the server should run on the given view.

The default implementation checks whether the URI scheme and the syntax scope match against the schemes and
selector from the settings file. You can override this method for example to dynamically evaluate the applicable
selector, or to ignore certain views even when those would match the static config. Please note that no document
syncronization messages (textDocument/didOpen, textDocument/didChange, textDocument/didClose, etc.) are sent to
the server for ignored views.

This method is called when the view gets opened. To manually trigger this method again, run the
`lsp_check_applicable` TextCommand for the given view and with a `session_name` keyword argument.

:param view: The view
:param config: The config
"""
if (syntax := view.syntax()) and (selector := cls.selector(view, config).strip()):
# TODO replace `cls.selector(view, config)` with `config.selector` after the next release
scheme, _ = parse_uri(uri_from_view(view))
return scheme in config.schemes and sublime.score_selector(syntax.scope, selector) > 0
return False

@classmethod
@deprecated("Use `is_applicable(view, config)` instead.")
def selector(cls, view: sublime.View, config: ClientConfig) -> str:
return config.selector

@classmethod
Expand Down Expand Up @@ -1024,15 +1043,8 @@ def on_post_start(cls, window: sublime.Window, initiating_view: sublime.View,
pass

@classmethod
@deprecated("Use `is_applicable(view, config)` instead.")
def should_ignore(cls, view: sublime.View) -> bool:
"""
Exclude a view from being handled by the language server, even if it matches the URI scheme(s) and selector from
the configuration. This can be used to, for example, ignore certain file patterns which are listed in a
configuration file (e.g. .gitignore). Please note that this also means that no document syncronization
notifications (textDocument/didOpen, textDocument/didChange, textDocument/didClose, etc.) are sent to the server
for ignored views, when they are opened in the editor. Therefore this method should be used with caution for
language servers which index all files in the workspace.
"""
return False

@classmethod
Expand Down Expand Up @@ -1504,7 +1516,7 @@ def compare_by_string(sb: SessionBufferProtocol | None) -> bool:
def can_handle(self, view: sublime.View, scheme: str, capability: str | None, inside_workspace: bool) -> bool:
if not self.state == ClientStates.READY:
return False
if self._plugin and self._plugin.should_ignore(view):
if self._plugin and self._plugin.should_ignore(view): # TODO remove after next release
debug(view, "ignored by plugin", self._plugin.__class__.__name__)
return False
if scheme == "file":
Expand Down Expand Up @@ -2410,9 +2422,7 @@ def end_async(self) -> None:
self._plugin.on_session_end_async(None, None)
self._plugin = None
for sv in self.session_views_async():
for status_key in self._status_messages.keys():
sv.view.erase_status(status_key)
sv.shutdown_async()
self.shutdown_session_view_async(sv)
self.capabilities.clear()
self._registrations.clear()
for watcher in self._static_file_watchers:
Expand All @@ -2424,6 +2434,11 @@ def end_async(self) -> None:
self.state = ClientStates.STOPPING
self.send_request_async(Request.shutdown(), self._handle_shutdown_result, self._handle_shutdown_result)

def shutdown_session_view_async(self, session_view: SessionViewProtocol) -> None:
for status_key in self._status_messages.keys():
session_view.view.erase_status(status_key)
session_view.shutdown_async()

def _handle_shutdown_result(self, _: Any) -> None:
self.exit()

Expand Down
13 changes: 5 additions & 8 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,14 +862,11 @@ def erase_view_status(self, view: sublime.View) -> None:

def match_view(self, view: sublime.View, scheme: str) -> bool:
from .sessions import get_plugin
syntax = view.syntax()
if not syntax:
return False
plugin = get_plugin(self.name)
selector = plugin.selector(view, self).strip() if plugin else self.selector.strip()
if not selector:
return False
return scheme in self.schemes and sublime.score_selector(syntax.scope, selector) > 0
if plugin := get_plugin(self.name):
return plugin.is_applicable(view, self)
if (syntax := view.syntax()) and (selector := self.selector.strip()):
return scheme in self.schemes and sublime.score_selector(syntax.scope, selector) > 0
return False

def map_client_path_to_server_uri(self, path: str) -> str:
if self.path_maps:
Expand Down
8 changes: 5 additions & 3 deletions plugin/core/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,11 @@ def _needed_config(self, view: sublime.View) -> ClientConfig | None:
handled = True
break
if not handled:
plugin = get_plugin(config.name)
if plugin and plugin.should_ignore(view):
debug(view, "ignored by plugin", plugin.__name__)
if plugin := get_plugin(config.name):
if plugin.should_ignore(view): # TODO remove after next release
debug(view, "ignored by plugin", plugin.__name__)
elif plugin.is_applicable(view, config):
return config
else:
return config
return None
Expand Down