diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 9aa4178a9..29b9345c4 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -5969,6 +5969,14 @@ def selectionRange(cls, params: SelectionRangeParams) -> 'Request': def workspaceSymbol(cls, params: WorkspaceSymbolParams) -> 'Request': return Request("workspace/symbol", params, None, progress=True) + @classmethod + def documentDiagnostic(cls, params: DocumentDiagnosticParams, view: sublime.View) -> 'Request': + return Request('textDocument/diagnostic', params, view) + + @classmethod + def workspaceDiagnostic(cls, params: WorkspaceDiagnosticParams) -> 'Request': + return Request('workspace/diagnostic', params) + @classmethod def shutdown(cls) -> 'Request': return Request("shutdown") diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index d87ed97e3..b065c6bcc 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -175,7 +175,7 @@ def get_project_path(self, file_path: str) -> Optional[str]: raise NotImplementedError() @abstractmethod - def should_present_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: + def should_ignore_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: """ Should the diagnostics for this URI be shown in the view? Return a reason why not """ @@ -370,6 +370,10 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor "codeDescriptionSupport": True, "dataSupport": True }, + "diagnostic": { + "dynamicRegistration": True, + "relatedDocumentSupport": True + }, "selectionRange": { "dynamicRegistration": True }, @@ -435,6 +439,9 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor }, "semanticTokens": { "refreshSupport": True + }, + "diagnostics": { + "refreshSupport": True } } # type: WorkspaceClientCapabilities window_capabilities = { @@ -604,6 +611,12 @@ def set_inlay_hints_pending_refresh(self, needs_refresh: bool = True) -> None: def remove_inlay_hint_phantom(self, phantom_uuid: str) -> None: ... + def do_document_diagnostic_async(self, view: sublime.View, version: Optional[int] = None) -> None: + ... + + def set_document_diagnostic_pending_refresh(self, needs_refresh: bool = True) -> None: + ... + class AbstractViewListener(metaclass=ABCMeta): @@ -1745,13 +1758,22 @@ 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_workspace_diagnostic_refresh(self, params: None, request_id: Any) -> None: + """handles the workspace/diagnostic/refresh request""" + self.send_response(Response(request_id, None)) + visible_session_views, not_visible_session_views = self.session_views_by_visibility() + for sv in visible_session_views: + sv.session_buffer.do_document_diagnostic_async(sv.view) + for sv in not_visible_session_views: + sv.session_buffer.set_document_diagnostic_pending_refresh() + def m_textDocument_publishDiagnostics(self, params: PublishDiagnosticsParams) -> None: """handles the textDocument/publishDiagnostics notification""" mgr = self.manager() if not mgr: return uri = params["uri"] - reason = mgr.should_present_diagnostics(uri, self.config) + reason = mgr.should_ignore_diagnostics(uri, self.config) if isinstance(reason, str): return debug("ignoring unsuitable diagnostics for", uri, "reason:", reason) diagnostics = params["diagnostics"] diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 1f28dc9f3..986e8b079 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -344,7 +344,7 @@ def get_project_path(self, file_path: str) -> Optional[str]: candidate = folder return candidate - def should_present_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: + def should_ignore_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: scheme, path = parse_uri(uri) if scheme != "file": return None diff --git a/plugin/documents.py b/plugin/documents.py index cfe350fa4..27d977888 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -341,14 +341,17 @@ def on_activated_async(self) -> None: self._do_code_actions_async() for sv in self.session_views_async(): if sv.code_lenses_needs_refresh: - sv.set_code_lenses_pending_refresh(False) + sv.set_code_lenses_pending_refresh(needs_refresh=False) sv.start_code_lenses_async() for sb in self.session_buffers_async(): + if sb.document_diagnostic_needs_refresh: + sb.set_document_diagnostic_pending_refresh(needs_refresh=False) + sb.do_document_diagnostic_async(self.view) if sb.semantic_tokens.needs_refresh: - sb.set_semantic_tokens_pending_refresh(False) + sb.set_semantic_tokens_pending_refresh(needs_refresh=False) sb.do_semantic_tokens_async(self.view) if sb.inlay_hints_needs_refresh: - sb.set_inlay_hints_pending_refresh(False) + sb.set_inlay_hints_pending_refresh(needs_refresh=False) sb.do_inlay_hints_async(self.view) def on_selection_modified_async(self) -> None: diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 174cf8ee8..0f4f58ddd 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -1,10 +1,17 @@ from .core.protocol import ColorInformation from .core.protocol import Diagnostic +from .core.protocol import DiagnosticServerCancellationData from .core.protocol import DiagnosticSeverity +from .core.protocol import DocumentDiagnosticParams +from .core.protocol import DocumentDiagnosticReport +from .core.protocol import DocumentDiagnosticReportKind from .core.protocol import DocumentLink from .core.protocol import DocumentUri +from .core.protocol import Error +from .core.protocol import FullDocumentDiagnosticReport from .core.protocol import InlayHint from .core.protocol import InlayHintParams +from .core.protocol import LSPErrorCodes from .core.protocol import Request from .core.protocol import SemanticTokensDeltaParams from .core.protocol import SemanticTokensParams @@ -18,7 +25,7 @@ from .core.types import debounced from .core.types import DebouncerNonThreadSafe from .core.types import FEATURES_TIMEOUT -from .core.typing import Any, Callable, Iterable, Optional, List, Set, Dict, Tuple, Union +from .core.typing import Any, Callable, Iterable, Optional, List, Protocol, Set, Dict, Tuple, TypeGuard, Union from .core.typing import cast from .core.views import DIAGNOSTIC_SEVERITY from .core.views import diagnostic_severity @@ -49,6 +56,19 @@ HUGE_FILE_SIZE = 50000 +class CallableWithOptionalArguments(Protocol): + def __call__(self, *args: Any) -> None: + ... + + +def is_diagnostic_server_cancellation_data(data: Any) -> TypeGuard[DiagnosticServerCancellationData]: + return isinstance(data, dict) and 'retriggerRequest' in data + + +def is_full_document_diagnostic_report(response: DocumentDiagnosticReport) -> TypeGuard[FullDocumentDiagnosticReport]: + return response['kind'] == DocumentDiagnosticReportKind.Full + + class PendingChanges: __slots__ = ('version', 'changes') @@ -115,6 +135,8 @@ def __init__(self, session_view: SessionViewProtocol, buffer_id: int, uri: Docum self.diagnostics_version = -1 self.diagnostics_flags = 0 self.diagnostics_are_visible = False + self.document_diagnostic_result_id = None # type: Optional[str] + self.document_diagnostic_needs_refresh = False self.last_text_change_time = 0.0 self.diagnostics_debouncer_async = DebouncerNonThreadSafe(async_thread=True) self.color_phantoms = sublime.PhantomSet(view, "lsp_color") @@ -144,11 +166,13 @@ def _check_did_open(self, view: sublime.View) -> None: return self.session.send_notification(did_open(view, language_id)) self.opened = True - self._do_color_boxes_async(view, view.change_count()) + version = view.change_count() + self._do_color_boxes_async(view, version) + self.do_document_diagnostic_async(view, version) self.do_semantic_tokens_async(view, view.size() > HUGE_FILE_SIZE) self.do_inlay_hints_async(view) if userprefs().link_highlight_style in ("underline", "none"): - self._do_document_link_async(view, view.change_count()) + self._do_document_link_async(view, version) self.session.notify_plugin_on_session_buffer_change(self) def _check_did_close(self) -> None: @@ -218,6 +242,8 @@ def register_capability_async( if view is not None: if capability_path.startswith("textDocumentSync."): self._check_did_open(view) + elif capability_path.startswith("diagnosticProvider"): + self.do_document_diagnostic_async(view, view.change_count()) def unregister_capability_async( self, @@ -314,6 +340,7 @@ def _on_after_change_async(self, view: sublime.View, version: int) -> None: self._has_changed_during_save = True return self._do_color_boxes_async(view, version) + self.do_document_diagnostic_async(view, version) self.do_semantic_tokens_async(view) if userprefs().link_highlight_style in ("underline", "none"): self._do_document_link_async(view, version) @@ -352,7 +379,7 @@ def some_view(self) -> Optional[sublime.View]: for sv in self.session_views: return sv.view - def _if_view_unchanged(self, f: Callable[[sublime.View, Any], None], version: int) -> Callable[[Any], None]: + def _if_view_unchanged(self, f: Callable[[sublime.View, Any], None], version: int) -> CallableWithOptionalArguments: """ Ensures that the view is at the same version when we were called, before calling the `f` function. """ @@ -414,6 +441,51 @@ def update_document_link(self, new_link: DocumentLink) -> None: self.document_links.append(new_link) break + # --- textDocument/diagnostic -------------------------------------------------------------------------------------- + + def do_document_diagnostic_async(self, view: sublime.View, version: Optional[int] = None) -> None: + mgr = self.session.manager() + if not mgr: + return + if mgr.should_ignore_diagnostics(self.last_known_uri, self.session.config): + return + if version is None: + version = view.change_count() + if self.has_capability("diagnosticProvider"): + params = {'textDocument': text_document_identifier(view)} # type: DocumentDiagnosticParams + identifier = self.get_capability("diagnosticProvider.identifier") + if identifier: + params['identifier'] = identifier + if self.document_diagnostic_result_id: + params['previousResultId'] = self.document_diagnostic_result_id + self.session.send_request_async( + Request.documentDiagnostic(params, view), + self._if_view_unchanged(self.on_document_diagnostic, version), + self._if_view_unchanged(self._on_document_diagnostic_error, version) + ) + + def on_document_diagnostic(self, view: Optional[sublime.View], response: DocumentDiagnosticReport) -> None: + self.document_diagnostic_result_id = response.get('resultId') + if is_full_document_diagnostic_report(response): + self.session.m_textDocument_publishDiagnostics( + {'uri': self.last_known_uri, 'diagnostics': response['items']}) + for uri, diagnostic_report in response.get('relatedDocuments', {}): + sb = self.session.get_session_buffer_for_uri_async(uri) + if sb: + cast(SessionBuffer, sb).on_document_diagnostic(None, cast(DocumentDiagnosticReport, diagnostic_report)) + + def _on_document_diagnostic_error(self, view: sublime.View, error: Error) -> None: + if error.code == LSPErrorCodes.ServerCancelled and is_diagnostic_server_cancellation_data(error.data) and \ + error.data['retriggerRequest']: + # Retrigger the request after a short delay, but only if there were no additional changes to the buffer (in + # that case the request will be retriggered automatically anyway) + version = view.change_count() + sublime.set_timeout_async( + lambda: self._if_view_unchanged(self.do_document_diagnostic_async, version)(version), 500) + + def set_document_diagnostic_pending_refresh(self, needs_refresh: bool = True) -> None: + self.document_diagnostic_needs_refresh = needs_refresh + # --- textDocument/publishDiagnostics ------------------------------------------------------------------------------ def on_diagnostics_async( diff --git a/tests/test_session.py b/tests/test_session.py index 3b298d066..615f71546 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -31,7 +31,7 @@ def sessions(self, view: sublime.View, capability: Optional[str] = None) -> Gene def get_project_path(self, file_name: str) -> Optional[str]: return None - def should_present_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: + def should_ignore_diagnostics(self, uri: DocumentUri, configuration: ClientConfig) -> Optional[str]: return None def start_async(self, configuration: ClientConfig, initiating_view: sublime.View) -> None: