Skip to content

Commit

Permalink
Don't restart servers when userprefs change (#2448)
Browse files Browse the repository at this point in the history
Co-authored-by: Rafał Chłodnicki <[email protected]>
  • Loading branch information
jwortmann and rchl authored Oct 2, 2024
1 parent 856f2c2 commit 93ce8fd
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 53 deletions.
21 changes: 16 additions & 5 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,6 @@
from .url import parse_uri
from .url import unparse_uri
from .version import __version__
from .views import DiagnosticSeverityData
from .views import extract_variables
from .views import get_storage_path
from .views import get_uri_and_range_from_location
Expand Down Expand Up @@ -577,9 +576,7 @@ def has_capability_async(self, capability_path: str) -> bool:
def shutdown_async(self) -> None:
...

def present_diagnostics_async(
self, is_view_visible: bool, data_per_severity: dict[tuple[int, bool], DiagnosticSeverityData]
) -> None:
def present_diagnostics_async(self, is_view_visible: bool) -> None:
...

def on_request_started_async(self, request_id: int, request: Request) -> None:
Expand All @@ -606,6 +603,9 @@ def set_code_lenses_pending_refresh(self, needs_refresh: bool = True) -> None:
def reset_show_definitions(self) -> None:
...

def on_userprefs_changed_async(self) -> None:
...


class SessionBufferProtocol(Protocol):

Expand Down Expand Up @@ -653,6 +653,9 @@ def get_capability(self, capability_path: str) -> Any | None:
def has_capability(self, capability_path: str) -> bool:
...

def on_userprefs_changed_async(self) -> None:
...

def on_diagnostics_async(
self, raw_diagnostics: list[Diagnostic], version: int | None, visible_session_views: set[SessionViewProtocol]
) -> None:
Expand Down Expand Up @@ -1344,8 +1347,11 @@ def set_config_status_async(self, message: str) -> None:
:param message: The message
"""
self.config_status_message = message.strip()
self._redraw_config_status_async()

def _redraw_config_status_async(self) -> None:
for sv in self.session_views_async():
self.config.set_view_status(sv.view, message)
self.config.set_view_status(sv.view, self.config_status_message)

def set_window_status_async(self, key: str, message: str) -> None:
self._status_messages[key] = message
Expand Down Expand Up @@ -1480,6 +1486,11 @@ def on_file_event_async(self, events: list[FileWatcherEvent]) -> None:

# --- misc methods -------------------------------------------------------------------------------------------------

def on_userprefs_changed_async(self) -> None:
self._redraw_config_status_async()
for sb in self.session_buffers_async():
sb.on_userprefs_changed_async()

def markdown_language_id_to_st_syntax_map(self) -> MarkdownLangMap | None:
return self._plugin.markdown_language_id_to_st_syntax_map() if self._plugin is not None else None

Expand Down
53 changes: 39 additions & 14 deletions plugin/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,49 @@
from .types import read_dict_setting
from .types import Settings
from .types import SettingsRegistration
from typing import Any, Callable
from abc import ABCMeta
from abc import abstractmethod
from typing import Any
import json
import os
import sublime


class LspSettingsChangeListener(metaclass=ABCMeta):

@abstractmethod
def on_client_config_updated(self, config_name: str | None = None) -> None:
raise NotImplementedError()

@abstractmethod
def on_userprefs_updated(self) -> None:
raise NotImplementedError()


class ClientConfigs:

def __init__(self) -> None:
self.all: dict[str, ClientConfig] = {}
self.external: dict[str, ClientConfig] = {}
self._listener: Callable[[str | None], None] | None = None
self._listener: LspSettingsChangeListener | None = None
self._clients_hash: int | None = None

def _notify_clients_listener(self, config_name: str | None = None) -> None:
if self._listener:
self._listener.on_client_config_updated(config_name)

def _notify_listener(self, config_name: str | None = None) -> None:
if callable(self._listener):
self._listener(config_name)
def _notify_userprefs_listener(self) -> None:
if self._listener:
self._listener.on_userprefs_updated()

def add_for_testing(self, config: ClientConfig) -> None:
assert config.name not in self.all
self.all[config.name] = config
self._notify_listener()
self._notify_clients_listener()

def remove_for_testing(self, config: ClientConfig) -> None:
self.all.pop(config.name)
self._notify_listener()
self._notify_clients_listener()

def add_external_config(self, name: str, s: sublime.Settings, file: str, notify_listener: bool) -> bool:
if name in self.external:
Expand All @@ -49,13 +68,13 @@ def add_external_config(self, name: str, s: sublime.Settings, file: str, notify_
# That causes many calls to WindowConfigManager.match_view, which is relatively speaking an expensive
# operation. To ensure that this dance is done only once, we delay notifying the WindowConfigManager until
# all plugins have done their `register_plugin` call.
debounced(lambda: self._notify_listener(name), 200, lambda: len(self.external) == size)
debounced(lambda: self._notify_clients_listener(name), 200, lambda: len(self.external) == size)
return True

def remove_external_config(self, name: str) -> None:
self.external.pop(name, None)
if self.all.pop(name, None):
self._notify_listener()
self._notify_clients_listener()

def update_external_config(self, name: str, s: sublime.Settings, file: str) -> None:
try:
Expand All @@ -66,20 +85,26 @@ def update_external_config(self, name: str, s: sublime.Settings, file: str) -> N
return
self.external[name] = config
self.all[name] = config
self._notify_listener(name)
self._notify_clients_listener(name)

def update_configs(self) -> None:
global _settings_obj
if _settings_obj is None:
return
clients_dict = read_dict_setting(_settings_obj, "clients", {})
_clients_hash = hash(json.dumps(clients_dict, sort_keys=True))
if _clients_hash == self._clients_hash:
self._notify_userprefs_listener()
return
self._clients_hash = _clients_hash
clients = DottedDict(read_dict_setting(_settings_obj, "default_clients", {}))
clients.update(read_dict_setting(_settings_obj, "clients", {}))
clients.update(clients_dict)
self.all.clear()
self.all.update({name: ClientConfig.from_dict(name, d) for name, d in clients.get().items()})
self.all.update(self.external)
debug("enabled configs:", ", ".join(sorted(c.name for c in self.all.values() if c.enabled)))
debug("disabled configs:", ", ".join(sorted(c.name for c in self.all.values() if not c.enabled)))
self._notify_listener()
self._notify_clients_listener()

def _set_enabled(self, config_name: str, is_enabled: bool) -> None:
from .sessions import get_plugin
Expand All @@ -104,8 +129,8 @@ def enable(self, config_name: str) -> None:
def disable(self, config_name: str) -> None:
self._set_enabled(config_name, False)

def set_listener(self, recipient: Callable[[str | None], None]) -> None:
self._listener = recipient
def set_listener(self, listener: LspSettingsChangeListener) -> None:
self._listener = listener


_settings_obj: sublime.Settings | None = None
Expand Down
2 changes: 2 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,8 @@ def set_view_status(self, view: sublime.View, message: str) -> None:
if sublime.load_settings("LSP.sublime-settings").get("show_view_status"):
status = f"{self.name} ({message})" if message else self.name
view.set_status(self.status_key, status)
else:
self.erase_view_status(view)

def erase_view_status(self, view: sublime.View) -> None:
view.erase_status(self.status_key)
Expand Down
16 changes: 13 additions & 3 deletions plugin/core/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from .sessions import Manager
from .sessions import Session
from .settings import client_configs
from .settings import LspSettingsChangeListener
from .settings import userprefs
from .transports import create_transport
from .types import ClientConfig
Expand Down Expand Up @@ -518,16 +519,25 @@ def on_configs_changed(self, config_name: str | None = None) -> None:
sublime.set_timeout_async(lambda: self.restart_sessions_async(config_name))


class WindowRegistry:
class WindowRegistry(LspSettingsChangeListener):
def __init__(self) -> None:
self._enabled = False
self._windows: dict[int, WindowManager] = {}
client_configs.set_listener(self._on_client_config_updated)
client_configs.set_listener(self)

def _on_client_config_updated(self, config_name: str | None = None) -> None:
def on_client_config_updated(self, config_name: str | None = None) -> None:
for wm in self._windows.values():
wm.get_config_manager().update(config_name)

def on_userprefs_updated(self) -> None:
sublime.set_timeout_async(self._on_userprefs_updated_async)

def _on_userprefs_updated_async(self) -> None:
for wm in self._windows.values():
wm.on_diagnostics_updated()
for session in wm.get_sessions():
session.on_userprefs_changed_async()

def enable(self) -> None:
self._enabled = True
# Initialize manually at plugin_loaded as we'll miss out on "on_new_window_async" events.
Expand Down
17 changes: 8 additions & 9 deletions plugin/inlay_hint.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,23 @@
class LspToggleInlayHintsCommand(LspWindowCommand):
capability = 'inlayHintProvider'

def __init__(self, window: sublime.Window) -> None:
super().__init__(window)
window.settings().set('lsp_show_inlay_hints', userprefs().show_inlay_hints)

def run(self, enable: bool | None = None) -> None:
window_settings = self.window.settings()
if not isinstance(enable, bool):
enable = not self.are_enabled(self.window)
self.window.settings().set('lsp_show_inlay_hints', enable)
enable = not bool(window_settings.get('lsp_show_inlay_hints'))
window_settings.set('lsp_show_inlay_hints', enable)
status = 'on' if enable else 'off'
sublime.status_message(f'Inlay Hints are {status}')
for session in self.sessions():
for sv in session.session_views_async():
sv.session_buffer.do_inlay_hints_async(sv.view)

def is_checked(self) -> bool:
return self.are_enabled(self.window)

@classmethod
def are_enabled(cls, window: sublime.Window | None) -> bool:
if not window:
return userprefs().show_inlay_hints
return bool(window.settings().get('lsp_show_inlay_hints', userprefs().show_inlay_hints))
return bool(self.window.settings().get('lsp_show_inlay_hints'))


class LspInlayHintClickCommand(LspTextCommand):
Expand Down
41 changes: 32 additions & 9 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
from .core.views import text_document_identifier
from .core.views import will_save
from .inlay_hint import inlay_hint_to_phantom
from .inlay_hint import LspToggleInlayHintsCommand
from .semantic_highlighting import SemanticToken
from functools import partial
from typing import Any, Callable, Iterable, List, Protocol
Expand Down Expand Up @@ -118,6 +117,7 @@ def __init__(self, session_view: SessionViewProtocol, buffer_id: int, uri: Docum
self._id = buffer_id
self._pending_changes: PendingChanges | None = None
self.diagnostics: list[tuple[Diagnostic, sublime.Region]] = []
self.diagnostics_data_per_severity: dict[tuple[int, bool], DiagnosticSeverityData] = {}
self.diagnostics_version = -1
self.diagnostics_flags = 0
self._diagnostics_are_visible = False
Expand Down Expand Up @@ -379,6 +379,15 @@ def on_post_save_async(self, view: sublime.View, new_uri: DocumentUri) -> None:
self._has_changed_during_save = False
self._on_after_change_async(view, view.change_count())

def on_userprefs_changed_async(self) -> None:
self._redraw_document_links_async()
if userprefs().semantic_highlighting:
self.semantic_tokens.needs_refresh = True
else:
self._clear_semantic_tokens_async()
for sv in self.session_views:
sv.on_userprefs_changed_async()

def some_view(self) -> sublime.View | None:
if not self.session_views:
return None
Expand Down Expand Up @@ -428,14 +437,20 @@ def _do_document_link_async(self, view: sublime.View, version: int) -> None:

def _on_document_link_async(self, view: sublime.View, response: list[DocumentLink] | None) -> None:
self._document_links = response or []
self._redraw_document_links_async()

def _redraw_document_links_async(self) -> None:
if self._document_links and userprefs().link_highlight_style == "underline":
view.add_regions(
"lsp_document_link",
[range_to_region(link["range"], view) for link in self._document_links],
scope="markup.underline.link.lsp",
flags=DOCUMENT_LINK_FLAGS)
view = self.some_view()
if not view:
return
regions = [range_to_region(link["range"], view) for link in self._document_links]
for sv in self.session_views:
sv.view.add_regions(
"lsp_document_link", regions, scope="markup.underline.link.lsp", flags=DOCUMENT_LINK_FLAGS)
else:
view.erase_regions("lsp_document_link")
for sv in self.session_views:
sv.view.erase_regions("lsp_document_link")

def get_document_link_at_point(self, view: sublime.View, point: int) -> DocumentLink | None:
for link in self._document_links:
Expand Down Expand Up @@ -539,13 +554,14 @@ def on_diagnostics_async(
else:
data.regions.append(region)
diagnostics.append((diagnostic, region))
self.diagnostics_data_per_severity = data_per_severity

def present() -> None:
self.diagnostics_version = diagnostics_version
self.diagnostics = diagnostics
self._diagnostics_are_visible = bool(diagnostics)
for sv in self.session_views:
sv.present_diagnostics_async(sv in visible_session_views, data_per_severity)
sv.present_diagnostics_async(sv in visible_session_views)

self._diagnostics_debouncer_async.cancel_pending()
if self._diagnostics_are_visible:
Expand Down Expand Up @@ -707,12 +723,19 @@ def set_semantic_tokens_pending_refresh(self, needs_refresh: bool = True) -> Non
def get_semantic_tokens(self) -> list[SemanticToken]:
return self.semantic_tokens.tokens

def _clear_semantic_tokens_async(self) -> None:
for sv in self.session_views:
self._clear_semantic_token_regions(sv.view)

# --- textDocument/inlayHint ----------------------------------------------------------------------------------

def do_inlay_hints_async(self, view: sublime.View) -> None:
if not self.has_capability("inlayHintProvider"):
return
if not LspToggleInlayHintsCommand.are_enabled(view.window()):
window = view.window()
if not window:
return
if not window.settings().get('lsp_show_inlay_hints'):
self.remove_all_inlay_hints()
return
params: InlayHintParams = {
Expand Down
Loading

0 comments on commit 93ce8fd

Please sign in to comment.