Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for pull diagnostics #2221

Merged
merged 9 commits into from
Mar 27, 2023
Merged
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
8 changes: 8 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
26 changes: 24 additions & 2 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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):

Expand Down Expand Up @@ -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))
Copy link
Member

@rchl rchl Mar 25, 2023

Choose a reason for hiding this comment

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

It's perhaps a bit unclear in the LSP spec whether the response should be sent after or before the related requests are finished but I guess we are currently consistent with ourselves at least.

Copy link
Member Author

@jwortmann jwortmann Mar 27, 2023

Choose a reason for hiding this comment

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

I would think that if it was expected to postpone the response until the other requests were sent, then this would explicitly be mentioned in the specs. So I think the requests are independent and it should be fine to immediately send the response here.

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"]
Expand Down
2 changes: 1 addition & 1 deletion plugin/core/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
80 changes: 76 additions & 4 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down