From 93ce8fdc30fff54d39972bf2aa5f03d37da8b691 Mon Sep 17 00:00:00 2001 From: jwortmann Date: Wed, 2 Oct 2024 21:17:03 +0200 Subject: [PATCH] Don't restart servers when userprefs change (#2448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafał Chłodnicki --- plugin/core/sessions.py | 21 ++++++++++++---- plugin/core/settings.py | 53 +++++++++++++++++++++++++++++----------- plugin/core/types.py | 2 ++ plugin/core/windows.py | 16 +++++++++--- plugin/inlay_hint.py | 17 ++++++------- plugin/session_buffer.py | 41 ++++++++++++++++++++++++------- plugin/session_view.py | 26 ++++++++++---------- 7 files changed, 123 insertions(+), 53 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index ccb30b429..d84d0f923 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -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 @@ -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: @@ -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): @@ -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: @@ -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 @@ -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 diff --git a/plugin/core/settings.py b/plugin/core/settings.py index eb171be2a..0b90dfe76 100644 --- a/plugin/core/settings.py +++ b/plugin/core/settings.py @@ -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: @@ -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: @@ -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 @@ -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 diff --git a/plugin/core/types.py b/plugin/core/types.py index eef8d040e..0fc7105b7 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -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) diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 2c678756f..728dda9b5 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -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 @@ -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. diff --git a/plugin/inlay_hint.py b/plugin/inlay_hint.py index 819e7a2b8..3b633da69 100644 --- a/plugin/inlay_hint.py +++ b/plugin/inlay_hint.py @@ -19,10 +19,15 @@ 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(): @@ -30,13 +35,7 @@ def run(self, enable: bool | None = None) -> None: 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): diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index cf432e6af..959bfad39 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -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 @@ -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 @@ -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 @@ -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: @@ -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: @@ -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 = { diff --git a/plugin/session_view.py b/plugin/session_view.py index 94296b6cc..64e5a7e05 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -18,7 +18,6 @@ from .core.sessions import Session from .core.settings import userprefs from .core.views import DIAGNOSTIC_SEVERITY -from .core.views import DiagnosticSeverityData from .core.views import text_document_identifier from .diagnostics import DiagnosticsAnnotationsView from .session_buffer import SessionBuffer @@ -299,25 +298,23 @@ def diagnostics_tag_scope(self, tag: int) -> str | None: return f'markup.{k.lower()}.lsp' return 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: + self._redraw_diagnostics_async() + listener = self.listener() + if listener: + listener.on_diagnostics_updated_async(is_view_visible) + + def _redraw_diagnostics_async(self) -> None: flags = userprefs().diagnostics_highlight_style_flags() # for single lines multiline_flags = None if userprefs().show_multiline_diagnostics_highlights else sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.NO_UNDO # noqa: E501 level = userprefs().show_diagnostics_severity_level for sev in reversed(range(1, len(DIAGNOSTIC_SEVERITY) + 1)): - self._draw_diagnostics( - data_per_severity, sev, level, flags[sev - 1] or DIAGNOSTIC_SEVERITY[sev - 1][4], multiline=False) - self._draw_diagnostics( - data_per_severity, sev, level, multiline_flags or DIAGNOSTIC_SEVERITY[sev - 1][5], multiline=True) + self._draw_diagnostics(sev, level, flags[sev - 1] or DIAGNOSTIC_SEVERITY[sev - 1][4], multiline=False) + self._draw_diagnostics(sev, level, multiline_flags or DIAGNOSTIC_SEVERITY[sev - 1][5], multiline=True) self._diagnostic_annotations.draw(self.session_buffer.diagnostics) - listener = self.listener() - if listener: - listener.on_diagnostics_updated_async(is_view_visible) def _draw_diagnostics( self, - data_per_severity: dict[tuple[int, bool], DiagnosticSeverityData], severity: int, max_severity_level: int, flags: int, @@ -326,7 +323,7 @@ def _draw_diagnostics( ICON_FLAGS = sublime.HIDE_ON_MINIMAP | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.NO_UNDO key = self.diagnostics_key(severity, multiline) tags = {tag: TagData(f'{key}_tags_{tag}') for tag in DIAGNOSTIC_TAG_VALUES} - data = data_per_severity.get((severity, multiline)) + data = self._session_buffer.diagnostics_data_per_severity.get((severity, multiline)) if data and severity <= max_severity_level: non_tag_regions = data.regions for tag, regions in data.regions_with_tag.items(): @@ -378,6 +375,9 @@ def on_pre_save_async(self) -> None: def on_post_save_async(self, new_uri: DocumentUri) -> None: self.session_buffer.on_post_save_async(self.view, new_uri) + def on_userprefs_changed_async(self) -> None: + self._redraw_diagnostics_async() + # --- textDocument/codeLens ---------------------------------------------------------------------------------------- def start_code_lenses_async(self) -> None: