Skip to content
60 changes: 60 additions & 0 deletions plugin/core/diagnostics_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from .protocol import Diagnostic, DiagnosticSeverity
from .settings import userprefs
from .typing import Callable, Iterator, List, Optional, Tuple
from .url import parse_uri
from .views import diagnostic_severity, format_diagnostic_for_panel
from collections import OrderedDict


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 add_diagnostics_async(self, uri: str, diagnostics: List[Diagnostic]) -> None:
_, path = parse_uri(uri)
if not diagnostics:
# received "clear diagnostics" message for this path
self.pop(path, None)
return
max_severity = userprefs().diagnostics_panel_include_severity_level
self[path] = (
list(
filter(
None,
(
format_diagnostic_for_panel(diagnostic)
for diagnostic in diagnostics
if diagnostic_severity(diagnostic) <= max_severity
),
)
),
len(list(filter(has_severity(DiagnosticSeverity.Error), diagnostics))),
len(list(filter(has_severity(DiagnosticSeverity.Warning), diagnostics))),
)
self.move_to_end(path) # maintain incoming order

def diagnostics_panel_contributions_async(
self,
) -> Iterator[Tuple[str, List[Tuple[str, Optional[int], Optional[str], Optional[str]]]]]:
for path, (contribution, _, _) in self.items():
if contribution:
yield path, contribution

def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]:
return (
sum(errors for _, errors, _ in self.values()),
sum(warnings for _, _, warnings in self.values()),
)


def has_severity(severity: int) -> Callable[[Diagnostic], bool]:
def has_severity(diagnostic: Diagnostic) -> bool:
return diagnostic_severity(diagnostic) == severity
return has_severity
4 changes: 4 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .collections import DottedDict
from .diagnostics_manager import DiagnosticsManager
from .edit import apply_workspace_edit
from .edit import parse_workspace_edit
from .file_watcher import DEFAULT_KIND
Expand Down Expand Up @@ -886,6 +887,7 @@ 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()

def __getattr__(self, name: str) -> Any:
"""
Expand Down Expand Up @@ -1342,6 +1344,8 @@ def m_textDocument_publishDiagnostics(self, params: Any) -> None:
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"])
mgr.update_diagnostics_panel_async()
sb = self.get_session_buffer_for_uri_async(uri)
if sb:
sb.on_diagnostics_async(params["diagnostics"], params.get("version"))
Expand Down
50 changes: 15 additions & 35 deletions plugin/core/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
from weakref import WeakSet
import functools
import json
import os
import sublime
import threading
import urllib.parse
Expand Down Expand Up @@ -102,14 +101,6 @@ def diagnostics_intersecting_async(
else:
return self.diagnostics_intersecting_region_async(region_or_point)

@abstractmethod
def diagnostics_panel_contribution_async(self) -> Sequence[Tuple[str, Optional[int], Optional[str], Optional[str]]]:
raise NotImplementedError()

@abstractmethod
def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]:
raise NotImplementedError()

@abstractmethod
def on_diagnostics_updated_async(self) -> None:
raise NotImplementedError()
Expand Down Expand Up @@ -176,7 +167,7 @@ def __init__(
self._panel_code_phantoms = None # type: Optional[sublime.PhantomSet]
self.total_error_count = 0
self.total_warning_count = 0
sublime.set_timeout(functools.partial(self._update_panel_main_thread, None, _NO_DIAGNOSTICS_PLACEHOLDER, []))
sublime.set_timeout(functools.partial(self._update_panel_main_thread, _NO_DIAGNOSTICS_PLACEHOLDER, []))

def get_config_manager(self) -> WindowConfigManager:
return self._configs
Expand Down Expand Up @@ -508,47 +499,36 @@ def handle_show_message(self, session: Session, params: Any) -> None:

def update_diagnostics_panel_async(self) -> None:
to_render = [] # type: List[str]
base_dir = None
self.total_error_count = 0
self.total_warning_count = 0
listeners = list(self._listeners)
prephantoms = [] # type: List[Tuple[int, int, str, str]]
row = 0
for listener in listeners:
local_errors, local_warnings = listener.sum_total_errors_and_warnings_async()
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
contribution = listener.diagnostics_panel_contribution_async()
if not contribution:
continue
file_path = listener.view.file_name() or ""
base_dir = self.get_project_path(file_path) # What about different base dirs for multiple folders?
file_path = os.path.relpath(file_path, base_dir) if base_dir else file_path
to_render.append("{}:".format(file_path))
row += 1
for content, offset, code, href in contribution:
to_render.append(content)
if offset is not None and code is not None and href is not None:
prephantoms.append((row, offset, code, href))
row += content.count("\n") + 1
to_render.append("") # add spacing between filenames
row += 1
for path, contribution in session.diagnostics_manager.diagnostics_panel_contributions_async():
to_render.append("{}:".format(path))
row += 1
for content, offset, code, href in contribution:
to_render.append(content)
if offset is not None and code is not None and href is not None:
prephantoms.append((row, offset, code, href))
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
sublime.set_timeout(functools.partial(self._update_panel_main_thread, base_dir, characters, prephantoms))
sublime.set_timeout(functools.partial(self._update_panel_main_thread, characters, prephantoms))

def _update_panel_main_thread(self, base_dir: Optional[str], characters: str,
prephantoms: List[Tuple[int, int, str, str]]) -> None:
def _update_panel_main_thread(self, characters: str, prephantoms: List[Tuple[int, int, str, str]]) -> None:
panel = ensure_diagnostics_panel(self._window)
if not panel or not panel.is_valid():
return
if isinstance(base_dir, str):
panel.settings().set("result_base_dir", base_dir)
else:
panel.settings().erase("result_base_dir")
panel.run_command("lsp_update_panel", {"characters": characters})
if self._panel_code_phantoms is None:
self._panel_code_phantoms = sublime.PhantomSet(panel, "hrefs")
Expand Down
23 changes: 0 additions & 23 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple, Union
from .core.url import parse_uri
from .core.url import view_to_uri
from .core.views import DIAGNOSTIC_SEVERITY
from .core.views import diagnostic_severity
from .core.views import document_color_params
from .core.views import first_selection_region
Expand Down Expand Up @@ -238,20 +237,6 @@ 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_panel_contribution_async(self) -> List[Tuple[str, Optional[int], Optional[str], Optional[str]]]:
result = [] # type: List[Tuple[str, Optional[int], Optional[str], Optional[str]]]
# Sort by severity
for severity in range(1, len(DIAGNOSTIC_SEVERITY) + 1):
for sb in self.session_buffers_async():
data = sb.data_per_severity.get((severity, False))
if data:
result.extend(data.panel_contribution)
data = sb.data_per_severity.get((severity, True))
if data:
result.extend(data.panel_contribution)
# sort the result by asc line number
return sorted(result)

def diagnostics_async(
self
) -> Generator[Tuple[SessionBuffer, List[Tuple[Diagnostic, sublime.Region]]], None, None]:
Expand Down Expand Up @@ -739,14 +724,6 @@ def trigger_on_pre_save_async(self) -> None:
for sv in self.session_views_async():
sv.on_pre_save_async()

def sum_total_errors_and_warnings_async(self) -> Tuple[int, int]:
errors = 0
warnings = 0
for sb in self.session_buffers_async():
errors += sb.total_errors
warnings += sb.total_warnings
return errors, warnings

def revert_async(self) -> None:
if self.view.is_primary():
for sv in self.session_views_async():
Expand Down
9 changes: 1 addition & 8 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from .core.views import did_close
from .core.views import did_open
from .core.views import did_save
from .core.views import format_diagnostic_for_panel
from .core.views import MissingUriError
from .core.views import range_to_region
from .core.views import will_save
Expand All @@ -41,13 +40,12 @@ def update(self, version: int, changes: Iterable[sublime.TextChange]) -> None:

class DiagnosticSeverityData:

__slots__ = ('regions', 'regions_with_tag', 'annotations', 'panel_contribution', 'scope', 'icon')
__slots__ = ('regions', 'regions_with_tag', 'annotations', 'scope', 'icon')

def __init__(self, severity: int) -> None:
self.regions = [] # type: List[sublime.Region]
self.regions_with_tag = {} # type: Dict[int, List[sublime.Region]]
self.annotations = [] # type: List[str]
self.panel_contribution = [] # type: List[Tuple[str, Optional[int], Optional[str], Optional[str]]]
_, _, self.scope, self.icon, _, _ = DIAGNOSTIC_SEVERITY[severity - 1]
if userprefs().diagnostics_gutter_marker != "sign":
self.icon = userprefs().diagnostics_gutter_marker
Expand Down Expand Up @@ -300,8 +298,6 @@ def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optio
total_errors += 1
elif severity == DiagnosticSeverity.Warning:
total_warnings += 1
if severity <= userprefs().diagnostics_panel_include_severity_level:
data.panel_contribution.append(format_diagnostic_for_panel(diagnostic))
if severity <= userprefs().show_diagnostics_panel_on_save:
should_show_diagnostics_panel = True
self._publish_diagnostics_to_session_views(
Expand Down Expand Up @@ -374,9 +370,6 @@ def _present_diagnostics_async(
self.should_show_diagnostics_panel = should_show_diagnostics_panel
for sv in self.session_views:
sv.present_diagnostics_async()
mgr = self.session.manager()
if mgr:
mgr.update_diagnostics_panel_async()

def __str__(self) -> str:
return '{}:{}:{}'.format(self.session.config.name, self.id, self.get_uri())