From 27c01d1df2a57940c0359a5ffe9334bbc75414ab Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 21 Sep 2022 23:15:48 +0200 Subject: [PATCH 1/6] Fix issues with diagnostics panel toggling on save --- plugin/core/diagnostics_manager.py | 114 +++++++---------------------- plugin/core/diagnostics_storage.py | 105 ++++++++++++++++++++++++++ plugin/core/registry.py | 4 +- plugin/core/sessions.py | 34 ++++----- plugin/core/windows.py | 30 ++++---- plugin/documents.py | 61 ++++++++++----- plugin/goto_diagnostic.py | 13 ++-- plugin/session_buffer.py | 48 +----------- 8 files changed, 218 insertions(+), 191 deletions(-) create mode 100644 plugin/core/diagnostics_storage.py diff --git a/plugin/core/diagnostics_manager.py b/plugin/core/diagnostics_manager.py index 96e7b8cf5..3ec626c9f 100644 --- a/plugin/core/diagnostics_manager.py +++ b/plugin/core/diagnostics_manager.py @@ -1,102 +1,42 @@ -from .protocol import Diagnostic, DiagnosticSeverity, DocumentUri -from .typing import Callable, Iterator, List, Tuple, TypeVar +from .sessions import Manager +from .protocol import Diagnostic, DocumentUri +from .typing import List, Optional, Tuple from .url import parse_uri -from .views import diagnostic_severity -from collections import OrderedDict -import functools -ParsedUri = Tuple[str, str] -T = TypeVar('T') +class DiagnosticsManager(): + """Per-window diagnostics manager that gives access to combined diagnostics from all active sessions.""" -class DiagnosticsManager(OrderedDict): - # From the specs: - # - # When a file changes it is the server’s responsibility to re-compute - # diagnostics and push them to the client. If the computed set is empty - # it has to push the empty array to clear former diagnostics. Newly - # pushed diagnostics always replace previously pushed diagnostics. There - # is no merging that happens on the client side. - # - # https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics + def __init__(self, manager: Manager) -> None: + self._manager = manager - def add_diagnostics_async(self, document_uri: DocumentUri, diagnostics: List[Diagnostic]) -> None: + def has_diagnostics(self, document_uri: Optional[DocumentUri]) -> bool: """ - Add `diagnostics` for `document_uri` to the store, replacing previously received `diagnoscis` - for this `document_uri`. If `diagnostics` is the empty list, `document_uri` is removed from - the store. The item received is moved to the end of the store. + Returns true if there are any diagnostics (optionally filtered by `document_uri`). """ - uri = parse_uri(document_uri) - if not diagnostics: - # received "clear diagnostics" message for this uri - self.pop(uri, None) - return - self[uri] = diagnostics - self.move_to_end(uri) # maintain incoming order - - def filter_map_diagnostics_async(self, pred: Callable[[Diagnostic], bool], - f: Callable[[ParsedUri, Diagnostic], T]) -> Iterator[Tuple[ParsedUri, List[T]]]: - """ - Yields `(uri, results)` items with `results` being a list of `f(diagnostic)` for each - diagnostic for this `uri` with `pred(diagnostic) == True`, filtered by `bool(f(diagnostic))`. - Only `uri`s with non-empty `results` are returned. Each `uri` is guaranteed to be yielded - not more than once. Items and results are ordered as they came in from the server. - """ - for uri, diagnostics in self.items(): - results = list(filter(None, map(functools.partial(f, uri), filter(pred, diagnostics)))) # type: List[T] - if results: - yield uri, results - - def filter_map_diagnostics_flat_async(self, pred: Callable[[Diagnostic], bool], - f: Callable[[ParsedUri, Diagnostic], T]) -> Iterator[Tuple[ParsedUri, T]]: - """ - Flattened variant of `filter_map_diagnostics_async()`. Yields `(uri, result)` items for each - of the `result`s per `uri` instead. Each `uri` can be yielded more than once. Items are - grouped by `uri` and each `uri` group is guaranteed to appear not more than once. Items are - ordered as they came in from the server. - """ - for uri, results in self.filter_map_diagnostics_async(pred, f): - for result in results: - yield uri, result - - def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]: - """ - Returns `(total_errors, total_warnings)` count of all diagnostics currently in store. - """ - return ( - sum(map(severity_count(DiagnosticSeverity.Error), self.values())), - sum(map(severity_count(DiagnosticSeverity.Warning), self.values())), - ) + parsed_uri = parse_uri(document_uri) if document_uri else None + for session in self._manager.get_sessions(): + if (parsed_uri and parsed_uri in session.diagnostics) or session.diagnostics: + return True + return False def diagnostics_by_document_uri(self, document_uri: DocumentUri) -> List[Diagnostic]: """ Returns possibly empty list of diagnostic for `document_uri`. """ - return self.get(parse_uri(document_uri), []) + diagnostics = [] # type: List[Diagnostic] + for session in self._manager.get_sessions(): + diagnostics.extend(session.diagnostics.diagnostics_by_document_uri(document_uri)) + return diagnostics - def diagnostics_by_parsed_uri(self, uri: ParsedUri) -> List[Diagnostic]: + def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]: """ - Returns possibly empty list of diagnostic for `uri`. + Returns `(total_errors, total_warnings)` count of all diagnostics for all sessions. """ - return self.get(uri, []) - - -def severity_count(severity: int) -> Callable[[List[Diagnostic]], int]: - def severity_count(diagnostics: List[Diagnostic]) -> int: - return len(list(filter(has_severity(severity), diagnostics))) - - return severity_count - - -def has_severity(severity: int) -> Callable[[Diagnostic], bool]: - def has_severity(diagnostic: Diagnostic) -> bool: - return diagnostic_severity(diagnostic) == severity - - return has_severity - - -def is_severity_included(max_severity: int) -> Callable[[Diagnostic], bool]: - def severity_included(diagnostic: Diagnostic) -> bool: - return diagnostic_severity(diagnostic) <= max_severity - - return severity_included + errors = 0 + warnings = 0 + for session in self._manager.get_sessions(): + session_errors, session_warnings = session.diagnostics.sum_total_errors_and_warnings_async() + errors += session_errors + warnings += session_warnings + return (errors, warnings) diff --git a/plugin/core/diagnostics_storage.py b/plugin/core/diagnostics_storage.py new file mode 100644 index 000000000..f7bedc4db --- /dev/null +++ b/plugin/core/diagnostics_storage.py @@ -0,0 +1,105 @@ +from .protocol import Diagnostic, DiagnosticSeverity, DocumentUri +from .typing import Callable, Iterator, List, Optional, Tuple, TypeVar +from .url import parse_uri +from .views import diagnostic_severity +from collections import OrderedDict +import functools + +ParsedUri = Tuple[str, str] +VersionedDiagnostics = Tuple[List[Diagnostic], Optional[int]] +T = TypeVar('T') + + +# NOTE: OrderedDict can only be properly typed in Python >=3.8. +class DiagnosticsStorage(OrderedDict): + # From the specs: + # + # When a file changes it is the server’s responsibility to re-compute + # diagnostics and push them to the client. If the computed set is empty + # it has to push the empty array to clear former diagnostics. Newly + # pushed diagnostics always replace previously pushed diagnostics. There + # is no merging that happens on the client side. + # + # https://microsoft.github.io/language-server-protocol/specification#textDocument_publishDiagnostics + + def add_diagnostics_async(self, document_uri: DocumentUri, diagnostics: List[Diagnostic]) -> None: + """ + Add `diagnostics` for `document_uri` to the store, replacing previously received `diagnoscis` + for this `document_uri`. If `diagnostics` is the empty list, `document_uri` is removed from + the store. The item received is moved to the end of the store. + """ + uri = parse_uri(document_uri) + if not diagnostics: + # received "clear diagnostics" message for this uri + self.pop(uri, None) + return + self[uri] = diagnostics + self.move_to_end(uri) # maintain incoming order + + def filter_map_diagnostics_async( + self, pred: Callable[[Diagnostic], bool], f: Callable[[ParsedUri, Diagnostic], T] + ) -> Iterator[Tuple[ParsedUri, List[T]]]: + """ + Yields `(uri, results)` items with `results` being a list of `f(diagnostic)` for each + diagnostic for this `uri` with `pred(diagnostic) == True`, filtered by `bool(f(diagnostic))`. + Only `uri`s with non-empty `results` are returned. Each `uri` is guaranteed to be yielded + not more than once. Items and results are ordered as they came in from the server. + """ + for uri, diagnostics in self.items(): + results = list(filter(None, map(functools.partial(f, uri), filter(pred, diagnostics)))) # type: List[T] + if results: + yield uri, results + + def filter_map_diagnostics_flat_async(self, pred: Callable[[Diagnostic], bool], + f: Callable[[ParsedUri, Diagnostic], T]) -> Iterator[Tuple[ParsedUri, T]]: + """ + Flattened variant of `filter_map_diagnostics_async()`. Yields `(uri, result)` items for each + of the `result`s per `uri` instead. Each `uri` can be yielded more than once. Items are + grouped by `uri` and each `uri` group is guaranteed to appear not more than once. Items are + ordered as they came in from the server. + """ + for uri, results in self.filter_map_diagnostics_async(pred, f): + for result in results: + yield uri, result + + def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]: + """ + Returns `(total_errors, total_warnings)` count of all diagnostics currently in store. + """ + return ( + sum(map(severity_count(DiagnosticSeverity.Error), self.values())), + sum(map(severity_count(DiagnosticSeverity.Warning), self.values())), + ) + + def diagnostics_by_document_uri(self, document_uri: DocumentUri) -> List[Diagnostic]: + """ + Returns possibly empty list of diagnostic for `document_uri`. + """ + return self.get(parse_uri(document_uri), []) + + def diagnostics_by_parsed_uri(self, uri: ParsedUri) -> List[Diagnostic]: + """ + Returns possibly empty list of diagnostic for `uri`. + """ + return self.get(uri, []) + + +def severity_count(severity: int) -> Callable[[List[Diagnostic]], int]: + def severity_count(diagnostics: List[Diagnostic]) -> int: + return len(list(filter(has_severity(severity), diagnostics))) + + return severity_count + + +def has_severity(severity: int) -> Callable[[Diagnostic], bool]: + def has_severity(diagnostic: Diagnostic) -> bool: + return diagnostic_severity(diagnostic) == severity + + return has_severity + + +def is_severity_included(max_severity: int) -> Callable[[Diagnostic], bool]: + def severity_included(diagnostic: Diagnostic) -> bool: + return diagnostic_severity(diagnostic) <= max_severity + + return severity_included diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 0f1cfe7d2..18b33954a 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -178,9 +178,7 @@ def navigate_diagnostics(view: sublime.View, point: Optional[int], forward: bool window = view.window() if not window: return - diagnostics = [] # type: List[Diagnostic] - for session in windows.lookup(window).get_sessions(): - diagnostics.extend(session.diagnostics_manager.diagnostics_by_document_uri(uri)) + diagnostics = windows.lookup(window).diagnostics_manager.diagnostics_by_document_uri(uri) if not diagnostics: return # Sort diagnostics by location diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index a9d559f7a..9e08aed6f 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -1,5 +1,5 @@ from .collections import DottedDict -from .diagnostics_manager import DiagnosticsManager +from .diagnostics_storage import DiagnosticsStorage from .edit import apply_edits from .edit import parse_workspace_edit from .edit import TextEditTuple @@ -14,8 +14,6 @@ from .open import center_selection from .open import open_externally from .open import open_file -from .panels import is_panel_open -from .panels import PanelName from .progress import WindowProgressReporter from .promise import PackagedTask from .promise import Promise @@ -43,6 +41,7 @@ from .protocol import LSPObject from .protocol import MarkupKind from .protocol import Notification +from .protocol import PublishDiagnosticsParams from .protocol import Range from .protocol import Request from .protocol import Response @@ -152,6 +151,13 @@ def window(self) -> sublime.Window: """ pass + @abstractmethod + def get_sessions(self) -> 'Generator[Session, None, None]': + """ + Iterate over all sessions stored in this manager. + """ + pass + @abstractmethod def sessions(self, view: sublime.View, capability: Optional[str] = None) -> 'Generator[Session, None, None]': """ @@ -186,7 +192,7 @@ def start_async(self, configuration: ClientConfig, initiating_view: sublime.View pass @abstractmethod - def update_diagnostics_panel_async(self) -> None: + def on_diagnostics_updated(self) -> None: pass @abstractmethod @@ -613,12 +619,6 @@ def on_session_initialized_async(self, session: 'Session') -> None: def on_session_shutdown_async(self, session: 'Session') -> None: raise NotImplementedError() - @abstractmethod - def diagnostics_async( - self - ) -> Iterable[Tuple['SessionBufferProtocol', List[Tuple[Diagnostic, sublime.Region]]]]: - raise NotImplementedError() - @abstractmethod def diagnostics_intersecting_region_async( self, @@ -1140,6 +1140,7 @@ def __init__(self, manager: Manager, logger: Logger, workspace_folders: List[Wor self.window = manager.window() self.state = ClientStates.STARTING self.capabilities = Capabilities() + self.diagnostics = DiagnosticsStorage() self.exiting = False self._registrations = {} # type: Dict[str, _RegistrationData] self._init_callback = None # type: Optional[InitCallback] @@ -1155,7 +1156,6 @@ def __init__(self, manager: Manager, logger: Logger, workspace_folders: List[Wor self._plugin_class = plugin_class self._plugin = None # type: Optional[AbstractPlugin] self._status_messages = {} # type: Dict[str, str] - self.diagnostics_manager = DiagnosticsManager() self._semantic_tokens_map = get_semantic_tokens_map(config.semantic_tokens) def __getattr__(self, name: str) -> Any: @@ -1692,21 +1692,21 @@ def m_workspace_inlayHint_refresh(self, params: None, request_id: Any) -> None: for sv in not_visible_session_views: sv.session_buffer.set_inlay_hints_pending_refresh() - def m_textDocument_publishDiagnostics(self, params: Any) -> None: + def m_textDocument_publishDiagnostics(self, params: PublishDiagnosticsParams) -> None: """handles the textDocument/publishDiagnostics notification""" - uri = params["uri"] mgr = self.manager() if not mgr: return + uri = params["uri"] reason = mgr.should_present_diagnostics(uri) if isinstance(reason, str): return debug("ignoring unsuitable diagnostics for", uri, "reason:", reason) - self.diagnostics_manager.add_diagnostics_async(uri, params["diagnostics"]) - if is_panel_open(self.window, PanelName.Diagnostics): - mgr.update_diagnostics_panel_async() + diagnostics = params["diagnostics"] + self.diagnostics.add_diagnostics_async(uri, diagnostics) + mgr.on_diagnostics_updated() sb = self.get_session_buffer_for_uri_async(uri) if sb: - sb.on_diagnostics_async(params["diagnostics"], params.get("version")) + sb.on_diagnostics_async(diagnostics, params.get("version")) def m_client_registerCapability(self, params: Any, request_id: Any) -> None: """handles the client/registerCapability request""" diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 2f2896e71..6df145d10 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -2,7 +2,8 @@ from .configurations import ConfigManager from .configurations import WindowConfigManager from .diagnostics import ensure_diagnostics_panel -from .diagnostics_manager import is_severity_included +from .diagnostics_manager import DiagnosticsManager +from .diagnostics_storage import is_severity_included from .logging import debug from .logging import exception_log from .message_request_handler import MessageRequestHandler @@ -80,8 +81,8 @@ def __init__( self._listeners = WeakSet() # type: WeakSet[AbstractViewListener] self._new_listener = None # type: Optional[AbstractViewListener] self._new_session = None # type: Optional[Session] - self._diagnostic_phantom_set = None # type: Optional[sublime.PhantomSet] self._panel_code_phantoms = None # type: Optional[sublime.PhantomSet] + self.diagnostics_manager = DiagnosticsManager(self) self.total_error_count = 0 self.total_warning_count = 0 sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, [])) @@ -90,9 +91,6 @@ def __init__( def get_config_manager(self) -> WindowConfigManager: return self._configs - def get_sessions(self) -> Generator[Session, None, None]: - yield from self._sessions - def on_load_project_async(self) -> None: self.update_workspace_folders_async() self._configs.update() @@ -197,6 +195,9 @@ def _publish_sessions_to_listener_async(self, listener: AbstractViewListener) -> def window(self) -> sublime.Window: return self._window + def get_sessions(self) -> Generator[Session, None, None]: + yield from self._sessions + def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]: inside_workspace = self._workspace.contains(view) sessions = list(self._sessions) @@ -413,21 +414,24 @@ def handle_stderr_log(self, session: Session, message: str) -> None: def handle_show_message(self, session: Session, params: Any) -> None: sublime.status_message("{}: {}".format(session.config.name, extract_message(params))) + def on_diagnostics_updated(self) -> None: + errors, warnings = self.diagnostics_manager.sum_total_errors_and_warnings_async() + self.total_error_count = errors + self.total_warning_count = warnings + for listener in list(self._listeners): + set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count) + if is_panel_open(self._window, PanelName.Diagnostics): + self.update_diagnostics_panel_async() + def update_diagnostics_panel_async(self) -> None: to_render = [] # type: List[str] - self.total_error_count = 0 - self.total_warning_count = 0 - listeners = list(self._listeners) prephantoms = [] # type: List[Tuple[int, int, str, str]] row = 0 max_severity = userprefs().diagnostics_panel_include_severity_level contributions = OrderedDict( ) # type: OrderedDict[str, List[Tuple[str, Optional[int], Optional[str], Optional[str]]]] for session in self._sessions: - local_errors, local_warnings = session.diagnostics_manager.sum_total_errors_and_warnings_async() - self.total_error_count += local_errors - self.total_warning_count += local_warnings - for (_, path), contribution in session.diagnostics_manager.filter_map_diagnostics_async( + for (_, path), contribution in session.diagnostics.filter_map_diagnostics_async( is_severity_included(max_severity), lambda _, diagnostic: format_diagnostic_for_panel(diagnostic)): seen = path in contributions contributions.setdefault(path, []).extend(contribution) @@ -443,8 +447,6 @@ def update_diagnostics_panel_async(self) -> None: row += content.count("\n") + 1 to_render.append("") # add spacing between filenames row += 1 - for listener in listeners: - set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count) characters = "\n".join(to_render) if not characters: characters = _NO_DIAGNOSTICS_PLACEHOLDER diff --git a/plugin/documents.py b/plugin/documents.py index fb1bd8dea..7b05bfa80 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -59,6 +59,11 @@ import webbrowser +class StaleDiagnosticsException(Exception): + def __init__(self) -> None: + super().__init__('Stale diagnostics') + + SUBLIME_WORD_MASK = 515 Flags = int @@ -157,6 +162,7 @@ def on_change() -> None: else: self.set_uri(view_to_uri(view)) self._auto_complete_triggered_manually = False + self._re_evaluate_if_toggle_diagnostics_panel = False self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._setup() @@ -234,14 +240,17 @@ def on_session_shutdown_async(self, session: Session) -> None: # SessionView was likely not created for this config so remove status here. session.config.erase_view_status(self.view) - def diagnostics_async( - self + def _diagnostics_async( + self, throw_if_stale: bool = False ) -> Generator[Tuple[SessionBufferProtocol, List[Tuple[Diagnostic, sublime.Region]]], None, None]: change_count = self.view.change_count() for sb in self.session_buffers_async(): - # do not provide stale diagnostics - if sb.diagnostics_version == change_count: - yield sb, sb.diagnostics + print(sb.session.config.name, sb.diagnostics_version, change_count) + if sb.diagnostics_version != change_count: + if throw_if_stale: + raise StaleDiagnosticsException() + continue + yield sb, sb.diagnostics def diagnostics_intersecting_region_async( self, @@ -249,7 +258,7 @@ def diagnostics_intersecting_region_async( ) -> Tuple[List[Tuple[SessionBufferProtocol, List[Diagnostic]]], sublime.Region]: covering = sublime.Region(region.begin(), region.end()) result = [] # type: List[Tuple[SessionBufferProtocol, List[Diagnostic]]] - for sb, diagnostics in self.diagnostics_async(): + for sb, diagnostics in self._diagnostics_async(): intersections = [] # type: List[Diagnostic] for diagnostic, candidate in diagnostics: # Checking against points is inclusive unlike checking whether region intersects another @@ -268,7 +277,7 @@ def diagnostics_touching_point_async( ) -> Tuple[List[Tuple[SessionBufferProtocol, List[Diagnostic]]], sublime.Region]: covering = sublime.Region(pt, pt) result = [] # type: List[Tuple[SessionBufferProtocol, List[Diagnostic]]] - for sb, diagnostics in self.diagnostics_async(): + for sb, diagnostics in self._diagnostics_async(): intersections = [] # type: List[Diagnostic] for diagnostic, candidate in diagnostics: severity = diagnostic_severity(diagnostic) @@ -286,6 +295,9 @@ def on_diagnostics_updated_async(self) -> None: if userprefs().show_code_actions: self._do_code_actions() self._update_diagnostic_in_status_bar_async() + if self._re_evaluate_if_toggle_diagnostics_panel: + self._re_evaluate_if_toggle_diagnostics_panel = False + self._toggle_diagnostics_panel_if_needed_async() def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: @@ -387,19 +399,32 @@ def on_post_save_async(self) -> None: # The URI scheme has changed. This means we need to re-determine whether any language servers should # be attached to the view. sublime.set_timeout(self._reset) - window = self.view.window() - if window and userprefs().show_diagnostics_panel_on_save > 0 and is_panel_open(window, PanelName.Diagnostics): - self._hide_diagnostics_panel_if_empty() + self._toggle_diagnostics_panel_if_needed_async() - def _hide_diagnostics_panel_if_empty(self) -> None: + def _toggle_diagnostics_panel_if_needed_async(self) -> None: severity_threshold = userprefs().show_diagnostics_panel_on_save - hide_panel = True - for _, diagnostics in self.diagnostics_async(): - if any(diagnostic_severity(diagnostic) <= severity_threshold for diagnostic, _ in diagnostics): - hide_panel = False - break - if hide_panel and self._manager: - self._manager.hide_diagnostics_panel_async() + if severity_threshold == 0: + return + window = self.view.window() + if not window or not self._manager: + return + has_relevant_diagnostcs = False + try: + for _, diagnostics in self._diagnostics_async(throw_if_stale=True): + if any(diagnostic_severity(diagnostic) <= severity_threshold for diagnostic, _ in diagnostics): + has_relevant_diagnostcs = True + break + except StaleDiagnosticsException: + # Don't make decision if there are stale diagnostics (which there usually are after saving changes). + # Re-evaluate next time diagnostics are updated. + self._re_evaluate_if_toggle_diagnostics_panel = True + return + if is_panel_open(window, PanelName.Diagnostics): + if not has_relevant_diagnostcs: + self._manager.hide_diagnostics_panel_async() + else: + if has_relevant_diagnostcs: + self._manager.show_diagnostics_panel_async() def on_close(self) -> None: if self._registered and self._manager: diff --git a/plugin/goto_diagnostic.py b/plugin/goto_diagnostic.py index b5d7ef182..49dcb6eca 100644 --- a/plugin/goto_diagnostic.py +++ b/plugin/goto_diagnostic.py @@ -1,4 +1,4 @@ -from .core.diagnostics_manager import ParsedUri, is_severity_included +from .core.diagnostics_storage import ParsedUri, is_severity_included from .core.protocol import Diagnostic, DocumentUri, DiagnosticSeverity, Location from .core.registry import windows from .core.sessions import Session @@ -46,10 +46,7 @@ def is_enabled(self, uri: Optional[DocumentUri] = None, diagnostic: Optional[dic uri = uri_from_view(view) except MissingUriError: return False - if uri: - parsed_uri = parse_uri(uri) - return any(parsed_uri in session.diagnostics_manager for session in get_sessions(self.window)) - return any(bool(session.diagnostics_manager) for session in get_sessions(self.window)) + return windows.lookup(self.window).diagnostics_manager.has_diagnostics(uri) def input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]: uri, diagnostic = args.get("uri"), args.get("diagnostic") @@ -88,12 +85,12 @@ def list_items(self) -> Tuple[List[sublime.ListInputItem], int]: severities_per_path = OrderedDict() # type: OrderedDict[ParsedUri, List[DiagnosticSeverity]] self.first_locations = dict() # type: Dict[ParsedUri, Tuple[Session, Location]] for session in get_sessions(self.window): - for parsed_uri, severity in session.diagnostics_manager.filter_map_diagnostics_flat_async( + for parsed_uri, severity in session.diagnostics.filter_map_diagnostics_flat_async( is_severity_included(max_severity), lambda _, diagnostic: diagnostic_severity(diagnostic)): severities_per_path.setdefault(parsed_uri, []).append(severity) if parsed_uri not in self.first_locations: severities_per_path.move_to_end(parsed_uri) - diagnostics = session.diagnostics_manager.diagnostics_by_parsed_uri(parsed_uri) + diagnostics = session.diagnostics.diagnostics_by_parsed_uri(parsed_uri) if diagnostics: self.first_locations[parsed_uri] = session, diagnostic_location(parsed_uri, diagnostics[0]) # build items @@ -174,7 +171,7 @@ def list_items(self) -> List[sublime.ListInputItem]: max_severity = userprefs().diagnostics_panel_include_severity_level for i, session in enumerate(self.sessions): for diagnostic in filter(is_severity_included(max_severity), - session.diagnostics_manager.diagnostics_by_parsed_uri(self.parsed_uri)): + session.diagnostics.diagnostics_by_parsed_uri(self.parsed_uri)): lines = diagnostic["message"].splitlines() first_line = lines[0] if lines else "" if len(lines) > 1: diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 84f88057e..6caeed7ac 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -1,7 +1,6 @@ from .core.panels import is_panel_open from .core.panels import PanelName from .core.protocol import Diagnostic -from .core.protocol import DiagnosticSeverity from .core.protocol import DocumentLink from .core.protocol import DocumentUri from .core.protocol import InlayHint @@ -112,9 +111,6 @@ def __init__(self, session_view: SessionViewProtocol, buffer_id: int, uri: Docum self.diagnostics_flags = 0 self.diagnostics_are_visible = False self.last_text_change_time = 0.0 - self.total_errors = 0 - self.total_warnings = 0 - self.should_show_diagnostics_panel = False self.diagnostics_debouncer = Debouncer() self.color_phantoms = sublime.PhantomSet(view, "lsp_color") self.document_links = [] # type: List[DocumentLink] @@ -129,7 +125,7 @@ def __init__(self, session_view: SessionViewProtocol, buffer_id: int, uri: Docum def __del__(self) -> None: mgr = self.session.manager() if mgr and is_panel_open(mgr.window(), PanelName.Diagnostics): - mgr.update_diagnostics_panel_async() + mgr.on_diagnostics_updated() self.color_phantoms.update([]) # 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. @@ -323,10 +319,6 @@ def on_post_save_async(self, view: sublime.View, new_uri: DocumentUri) -> None: if send_did_save: self.purge_changes_async(view) self.session.send_notification(did_save(view, include_text, self.last_known_uri)) - if self.should_show_diagnostics_panel: - mgr = self.session.manager() - if mgr: - mgr.show_diagnostics_panel_async() def some_view(self) -> Optional[sublime.View]: for sv in self.session_views: @@ -396,9 +388,6 @@ def update_document_link(self, new_link: DocumentLink) -> None: def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optional[int]) -> None: data_per_severity = {} # type: Dict[Tuple[int, bool], DiagnosticSeverityData] - total_errors = 0 - total_warnings = 0 - should_show_diagnostics_panel = False view = self.some_view() if view is None: return @@ -423,40 +412,17 @@ def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optio else: data.regions.append(region) diagnostics.append((diagnostic, region)) - if severity == DiagnosticSeverity.Error: - total_errors += 1 - elif severity == DiagnosticSeverity.Warning: - total_warnings += 1 - if severity <= userprefs().show_diagnostics_panel_on_save: - should_show_diagnostics_panel = True - self._publish_diagnostics_to_session_views( - diagnostics_version, - diagnostics, - data_per_severity, - total_errors, - total_warnings, - should_show_diagnostics_panel - ) + self._publish_diagnostics_to_session_views(diagnostics_version, diagnostics, data_per_severity) def _publish_diagnostics_to_session_views( self, diagnostics_version: int, diagnostics: List[Tuple[Diagnostic, sublime.Region]], data_per_severity: Dict[Tuple[int, bool], DiagnosticSeverityData], - total_errors: int, - total_warnings: int, - should_show_diagnostics_panel: bool ) -> None: def present() -> None: - self._present_diagnostics_async( - diagnostics_version, - diagnostics, - data_per_severity, - total_errors, - total_warnings, - should_show_diagnostics_panel - ) + self._present_diagnostics_async(diagnostics_version, diagnostics, data_per_severity) self.diagnostics_debouncer.cancel_pending() @@ -486,17 +452,11 @@ def _present_diagnostics_async( diagnostics_version: int, diagnostics: List[Tuple[Diagnostic, sublime.Region]], data_per_severity: Dict[Tuple[int, bool], DiagnosticSeverityData], - total_errors: int, - total_warnings: int, - should_show_diagnostics_panel: bool ) -> None: self.diagnostics_version = diagnostics_version self.diagnostics = diagnostics self.data_per_severity = data_per_severity self.diagnostics_are_visible = bool(diagnostics) - self.total_errors = total_errors - self.total_warnings = total_warnings - self.should_show_diagnostics_panel = should_show_diagnostics_panel for sv in self.session_views: sv.present_diagnostics_async() @@ -563,7 +523,7 @@ def _on_semantic_tokens_delta_async(self, response: Optional[Dict]) -> None: return self._draw_semantic_tokens_async() - def _on_semantic_tokens_error_async(self, error: dict) -> None: + def _on_semantic_tokens_error_async(self, _: dict) -> None: self.semantic_tokens.pending_response = None self.semantic_tokens.result_id = None From c742f3447a5fa990f0c4a66a0ba88b763cfaa726 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 21 Sep 2022 23:42:45 +0200 Subject: [PATCH 2/6] unused imports --- plugin/core/registry.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 18b33954a..902cb9689 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -1,10 +1,9 @@ from .configurations import ConfigManager -from .protocol import Diagnostic from .protocol import Point from .sessions import AbstractViewListener from .sessions import Session from .settings import client_configs -from .typing import Optional, Any, Generator, Iterable, List +from .typing import Optional, Any, Generator, Iterable from .views import first_selection_region from .views import MissingUriError from .views import point_to_offset From 16b88c801f0cf3cf9094d6f5bc79c31c37620ca6 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 21 Sep 2022 23:50:17 +0200 Subject: [PATCH 3/6] revert DiagnosticsManager --- plugin/core/diagnostics_manager.py | 42 ------------------------------ plugin/core/registry.py | 7 +++-- plugin/core/sessions.py | 7 ----- plugin/core/windows.py | 17 ++++++------ plugin/documents.py | 1 - plugin/goto_diagnostic.py | 5 +++- 6 files changed, 18 insertions(+), 61 deletions(-) delete mode 100644 plugin/core/diagnostics_manager.py diff --git a/plugin/core/diagnostics_manager.py b/plugin/core/diagnostics_manager.py deleted file mode 100644 index 3ec626c9f..000000000 --- a/plugin/core/diagnostics_manager.py +++ /dev/null @@ -1,42 +0,0 @@ -from .sessions import Manager -from .protocol import Diagnostic, DocumentUri -from .typing import List, Optional, Tuple -from .url import parse_uri - - -class DiagnosticsManager(): - """Per-window diagnostics manager that gives access to combined diagnostics from all active sessions.""" - - def __init__(self, manager: Manager) -> None: - self._manager = manager - - def has_diagnostics(self, document_uri: Optional[DocumentUri]) -> bool: - """ - Returns true if there are any diagnostics (optionally filtered by `document_uri`). - """ - parsed_uri = parse_uri(document_uri) if document_uri else None - for session in self._manager.get_sessions(): - if (parsed_uri and parsed_uri in session.diagnostics) or session.diagnostics: - return True - return False - - def diagnostics_by_document_uri(self, document_uri: DocumentUri) -> List[Diagnostic]: - """ - Returns possibly empty list of diagnostic for `document_uri`. - """ - diagnostics = [] # type: List[Diagnostic] - for session in self._manager.get_sessions(): - diagnostics.extend(session.diagnostics.diagnostics_by_document_uri(document_uri)) - return diagnostics - - def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]: - """ - Returns `(total_errors, total_warnings)` count of all diagnostics for all sessions. - """ - errors = 0 - warnings = 0 - for session in self._manager.get_sessions(): - session_errors, session_warnings = session.diagnostics.sum_total_errors_and_warnings_async() - errors += session_errors - warnings += session_warnings - return (errors, warnings) diff --git a/plugin/core/registry.py b/plugin/core/registry.py index 902cb9689..b228c0f23 100644 --- a/plugin/core/registry.py +++ b/plugin/core/registry.py @@ -1,9 +1,10 @@ from .configurations import ConfigManager +from .protocol import Diagnostic from .protocol import Point from .sessions import AbstractViewListener from .sessions import Session from .settings import client_configs -from .typing import Optional, Any, Generator, Iterable +from .typing import Optional, Any, Generator, Iterable, List from .views import first_selection_region from .views import MissingUriError from .views import point_to_offset @@ -177,7 +178,9 @@ def navigate_diagnostics(view: sublime.View, point: Optional[int], forward: bool window = view.window() if not window: return - diagnostics = windows.lookup(window).diagnostics_manager.diagnostics_by_document_uri(uri) + diagnostics = [] # type: List[Diagnostic] + for session in windows.lookup(window).get_sessions(): + diagnostics.extend(session.diagnostics.diagnostics_by_document_uri(uri)) if not diagnostics: return # Sort diagnostics by location diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 9e08aed6f..4efa757c0 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -151,13 +151,6 @@ def window(self) -> sublime.Window: """ pass - @abstractmethod - def get_sessions(self) -> 'Generator[Session, None, None]': - """ - Iterate over all sessions stored in this manager. - """ - pass - @abstractmethod def sessions(self, view: sublime.View, capability: Optional[str] = None) -> 'Generator[Session, None, None]': """ diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 6df145d10..823cb927f 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -2,7 +2,6 @@ from .configurations import ConfigManager from .configurations import WindowConfigManager from .diagnostics import ensure_diagnostics_panel -from .diagnostics_manager import DiagnosticsManager from .diagnostics_storage import is_severity_included from .logging import debug from .logging import exception_log @@ -82,7 +81,6 @@ def __init__( self._new_listener = None # type: Optional[AbstractViewListener] self._new_session = None # type: Optional[Session] self._panel_code_phantoms = None # type: Optional[sublime.PhantomSet] - self.diagnostics_manager = DiagnosticsManager(self) self.total_error_count = 0 self.total_warning_count = 0 sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, [])) @@ -91,6 +89,9 @@ def __init__( def get_config_manager(self) -> WindowConfigManager: return self._configs + def get_sessions(self) -> Generator[Session, None, None]: + yield from self._sessions + def on_load_project_async(self) -> None: self.update_workspace_folders_async() self._configs.update() @@ -195,9 +196,6 @@ def _publish_sessions_to_listener_async(self, listener: AbstractViewListener) -> def window(self) -> sublime.Window: return self._window - def get_sessions(self) -> Generator[Session, None, None]: - yield from self._sessions - def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Generator[Session, None, None]: inside_workspace = self._workspace.contains(view) sessions = list(self._sessions) @@ -415,9 +413,12 @@ def handle_show_message(self, session: Session, params: Any) -> None: sublime.status_message("{}: {}".format(session.config.name, extract_message(params))) def on_diagnostics_updated(self) -> None: - errors, warnings = self.diagnostics_manager.sum_total_errors_and_warnings_async() - self.total_error_count = errors - self.total_warning_count = warnings + self.total_error_count = 0 + self.total_warning_count = 0 + for session in self._sessions: + local_errors, local_warnings = session.diagnostics.sum_total_errors_and_warnings_async() + self.total_error_count += local_errors + self.total_warning_count += local_warnings for listener in list(self._listeners): set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count) if is_panel_open(self._window, PanelName.Diagnostics): diff --git a/plugin/documents.py b/plugin/documents.py index 7b05bfa80..efbb34a60 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -245,7 +245,6 @@ def _diagnostics_async( ) -> Generator[Tuple[SessionBufferProtocol, List[Tuple[Diagnostic, sublime.Region]]], None, None]: change_count = self.view.change_count() for sb in self.session_buffers_async(): - print(sb.session.config.name, sb.diagnostics_version, change_count) if sb.diagnostics_version != change_count: if throw_if_stale: raise StaleDiagnosticsException() diff --git a/plugin/goto_diagnostic.py b/plugin/goto_diagnostic.py index 49dcb6eca..7050c0600 100644 --- a/plugin/goto_diagnostic.py +++ b/plugin/goto_diagnostic.py @@ -46,7 +46,10 @@ def is_enabled(self, uri: Optional[DocumentUri] = None, diagnostic: Optional[dic uri = uri_from_view(view) except MissingUriError: return False - return windows.lookup(self.window).diagnostics_manager.has_diagnostics(uri) + if uri: + parsed_uri = parse_uri(uri) + return any(parsed_uri in session.diagnostics for session in get_sessions(self.window)) + return any(bool(session.diagnostics) for session in get_sessions(self.window)) def input(self, args: dict) -> Optional[sublime_plugin.CommandInputHandler]: uri, diagnostic = args.get("uri"), args.get("diagnostic") From c5d9bcdea9b8f894d56f0471325e6a7225b757df Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 21 Sep 2022 23:53:40 +0200 Subject: [PATCH 4/6] update mock impl --- tests/test_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_session.py b/tests/test_session.py index 81a9cb518..1d4ed96ee 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -40,7 +40,7 @@ def start_async(self, configuration: ClientConfig, initiating_view: sublime.View def on_post_exit_async(self, session: Session, exit_code: int, exception: Optional[Exception]) -> None: pass - def update_diagnostics_panel_async(self) -> None: + def on_diagnostics_updated(self) -> None: pass def show_diagnostics_panel_async(self) -> None: From 9e171550a45ac60d78c6730cc646ed6f6457102c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 21 Sep 2022 23:55:21 +0200 Subject: [PATCH 5/6] unused --- plugin/core/diagnostics_storage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugin/core/diagnostics_storage.py b/plugin/core/diagnostics_storage.py index f7bedc4db..776f8937d 100644 --- a/plugin/core/diagnostics_storage.py +++ b/plugin/core/diagnostics_storage.py @@ -1,12 +1,11 @@ from .protocol import Diagnostic, DiagnosticSeverity, DocumentUri -from .typing import Callable, Iterator, List, Optional, Tuple, TypeVar +from .typing import Callable, Iterator, List, Tuple, TypeVar from .url import parse_uri from .views import diagnostic_severity from collections import OrderedDict import functools ParsedUri = Tuple[str, str] -VersionedDiagnostics = Tuple[List[Diagnostic], Optional[int]] T = TypeVar('T') From 84eab004931cf9154980e441422b6e41e757fae1 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 24 Sep 2022 14:43:13 +0200 Subject: [PATCH 6/6] better approach? --- plugin/documents.py | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index efbb34a60..ab562a5ce 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -59,11 +59,6 @@ import webbrowser -class StaleDiagnosticsException(Exception): - def __init__(self) -> None: - super().__init__('Stale diagnostics') - - SUBLIME_WORD_MASK = 515 Flags = int @@ -162,7 +157,7 @@ def on_change() -> None: else: self.set_uri(view_to_uri(view)) self._auto_complete_triggered_manually = False - self._re_evaluate_if_toggle_diagnostics_panel = False + self._change_count_on_last_save = -1 self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._setup() @@ -241,15 +236,12 @@ def on_session_shutdown_async(self, session: Session) -> None: session.config.erase_view_status(self.view) def _diagnostics_async( - self, throw_if_stale: bool = False + self, allow_stale: bool = False ) -> Generator[Tuple[SessionBufferProtocol, List[Tuple[Diagnostic, sublime.Region]]], None, None]: change_count = self.view.change_count() for sb in self.session_buffers_async(): - if sb.diagnostics_version != change_count: - if throw_if_stale: - raise StaleDiagnosticsException() - continue - yield sb, sb.diagnostics + if sb.diagnostics_version == change_count or allow_stale: + yield sb, sb.diagnostics def diagnostics_intersecting_region_async( self, @@ -294,8 +286,7 @@ def on_diagnostics_updated_async(self) -> None: if userprefs().show_code_actions: self._do_code_actions() self._update_diagnostic_in_status_bar_async() - if self._re_evaluate_if_toggle_diagnostics_panel: - self._re_evaluate_if_toggle_diagnostics_panel = False + if self.view.change_count() == self._change_count_on_last_save: self._toggle_diagnostics_panel_if_needed_async() def _update_diagnostic_in_status_bar_async(self) -> None: @@ -398,6 +389,7 @@ def on_post_save_async(self) -> None: # The URI scheme has changed. This means we need to re-determine whether any language servers should # be attached to the view. sublime.set_timeout(self._reset) + self._change_count_on_last_save = self.view.change_count() self._toggle_diagnostics_panel_if_needed_async() def _toggle_diagnostics_panel_if_needed_async(self) -> None: @@ -408,16 +400,10 @@ def _toggle_diagnostics_panel_if_needed_async(self) -> None: if not window or not self._manager: return has_relevant_diagnostcs = False - try: - for _, diagnostics in self._diagnostics_async(throw_if_stale=True): - if any(diagnostic_severity(diagnostic) <= severity_threshold for diagnostic, _ in diagnostics): - has_relevant_diagnostcs = True - break - except StaleDiagnosticsException: - # Don't make decision if there are stale diagnostics (which there usually are after saving changes). - # Re-evaluate next time diagnostics are updated. - self._re_evaluate_if_toggle_diagnostics_panel = True - return + for _, diagnostics in self._diagnostics_async(allow_stale=True): + if any(diagnostic_severity(diagnostic) <= severity_threshold for diagnostic, _ in diagnostics): + has_relevant_diagnostcs = True + break if is_panel_open(window, PanelName.Diagnostics): if not has_relevant_diagnostcs: self._manager.hide_diagnostics_panel_async()