From ef4b666b868fe1afe0485148bcff24d1922980ce Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Thu, 10 Dec 2020 22:38:17 +0100 Subject: [PATCH 1/2] Clickable links in the diagnostics panel --- Syntaxes/Diagnostics.sublime-syntax | 2 +- plugin/core/views.py | 28 ++++++++++++++---- plugin/core/windows.py | 46 ++++++++++++++++++++++------- plugin/documents.py | 4 +-- plugin/session_buffer.py | 2 +- 5 files changed, 62 insertions(+), 20 deletions(-) diff --git a/Syntaxes/Diagnostics.sublime-syntax b/Syntaxes/Diagnostics.sublime-syntax index a9bb7780b..012d51c95 100644 --- a/Syntaxes/Diagnostics.sublime-syntax +++ b/Syntaxes/Diagnostics.sublime-syntax @@ -54,7 +54,7 @@ contexts: expect-source-and-code: - include: pop-at-end - - match: ([^:\]]+)((:)(\S+))? + - match: ([^:\]]+)((:)(\S+)?)? captures: 1: comment.line.source.lsp 3: punctuation.separator.lsp diff --git a/plugin/core/views.py b/plugin/core/views.py index 6b10d0508..4b7bc1ac8 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -8,7 +8,7 @@ from .protocol import Point from .protocol import Range from .protocol import Request -from .typing import Optional, Dict, Any, Iterable, List, Union, Callable +from .typing import Optional, Dict, Any, Iterable, List, Union, Callable, Tuple from .url import filename_to_uri from .url import uri_to_filename import html @@ -542,10 +542,26 @@ def format_severity(severity: int) -> str: return "???" -def format_diagnostic_for_panel(diagnostic: Diagnostic) -> str: +def format_diagnostic_for_panel(diagnostic: Diagnostic) -> Tuple[str, Optional[int], Optional[str], Optional[str]]: + """ + Turn an LSP diagnostic into a string suitable for an output panel. + + :param diagnostic: The diagnostic + :returns: Tuple of (content, optional offset, optional code, optional href) + When the last three elements are optional, don't show an inline phantom + When the last three elemenst are not optional, show an inline phantom + using the information given. + """ formatted = [diagnostic.source if diagnostic.source else "unknown-source"] - if diagnostic.code: - formatted.extend((":", str(diagnostic.code))) + offset = None + href = None + code = str(diagnostic.code) if diagnostic.code else None + if code: + formatted.append(":") + if diagnostic.code_description: + href = diagnostic.code_description["href"] + else: + formatted.append(code) lines = diagnostic.message.splitlines() or [""] # \u200B is the zero-width space result = "{:>4}:{:<4}{:<8}{} \u200B{}".format( @@ -555,9 +571,11 @@ def format_diagnostic_for_panel(diagnostic: Diagnostic) -> str: lines[0], "".join(formatted) ) + if href: + offset = len(result) for line in itertools.islice(lines, 1, None): result += "\n" + 17 * " " + line - return result + return result, offset, code, href def _format_diagnostic_related_info(info: DiagnosticRelatedInformation, base_dir: Optional[str] = None) -> str: diff --git a/plugin/core/windows.py b/plugin/core/windows.py index 89b07b00e..ff39df103 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -11,10 +11,11 @@ from .protocol import Diagnostic from .protocol import Error from .protocol import Point -from .sessions import SessionBufferProtocol, get_plugin from .sessions import Logger from .sessions import Manager from .sessions import Session +from .sessions import SessionBufferProtocol +from .sessions import get_plugin from .sessions import SessionViewProtocol from .settings import userprefs from .transports import create_transport @@ -22,6 +23,7 @@ from .typing import Optional, Any, Dict, Deque, List, Generator, Tuple, Iterable from .views import diagnostic_to_phantom from .views import extract_variables +from .views import make_link from .workspace import disable_in_project from .workspace import enable_in_project from .workspace import ProjectFolders @@ -67,7 +69,7 @@ def diagnostics_async(self) -> Iterable[Tuple[SessionBufferProtocol, List[Diagno raise NotImplementedError() @abstractmethod - def diagnostics_panel_contribution_async(self) -> List[str]: + def diagnostics_panel_contribution_async(self) -> List[Tuple[str, Optional[int], Optional[str], Optional[str]]]: raise NotImplementedError() @abstractmethod @@ -122,6 +124,7 @@ def __init__( self._new_session = None # type: Optional[Session] self._cursor = DiagnosticsCursor(userprefs().show_diagnostics_severity_level) self._diagnostic_phantom_set = None # type: Optional[sublime.PhantomSet] + self._panel_code_phantoms = None # type: Optional[sublime.PhantomSet] self.total_error_count = 0 self.total_warning_count = 0 @@ -407,14 +410,13 @@ def handle_show_message(self, session: Session, params: Any) -> None: sublime.status_message("{}: {}".format(session.config.name, extract_message(params))) def update_diagnostics_panel_async(self) -> None: - panel = ensure_diagnostics_panel(self._window) - if not panel: - return 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() self.total_error_count += local_errors @@ -426,14 +428,36 @@ def update_diagnostics_panel_async(self) -> None: 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)) - to_render.extend(contribution) - 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": "\n".join(to_render)}) + row += 1 + for content, offset, code, href in contribution: + row += 1 + 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") for listener in listeners: set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count) + characters = "\n".join(to_render) + + def update() -> 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") + phantoms = [] # type: List[sublime.Phantom] + for row, col, code, href in prephantoms: + point = panel.text_point(row, col) + region = sublime.Region(point, point) + phantoms.append(sublime.Phantom(region, make_link(href, code), sublime.LAYOUT_INLINE)) + self._panel_code_phantoms.update(phantoms) + + sublime.set_timeout(update) def _can_manipulate_diagnostics_panel(self) -> bool: active_panel = self._window.active_panel() diff --git a/plugin/documents.py b/plugin/documents.py index f87b5ca75..db10589d5 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -172,8 +172,8 @@ 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[str]: - result = [] # type: List[str] + 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(): diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 07ab489e2..e3eaecaf6 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -42,7 +42,7 @@ class DiagnosticSeverityData: def __init__(self, severity: int) -> None: self.regions = [] # type: List[sublime.Region] self.annotations = [] # type: List[str] - self.panel_contribution = [] # 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 From 7bc382f8bad4909ccb910485d6f4c9eae20c7d3b Mon Sep 17 00:00:00 2001 From: Raoul Wols Date: Thu, 10 Dec 2020 23:17:51 +0100 Subject: [PATCH 2/2] Fixes --- Syntaxes/Diagnostics.sublime-syntax | 2 +- plugin/core/windows.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Syntaxes/Diagnostics.sublime-syntax b/Syntaxes/Diagnostics.sublime-syntax index 012d51c95..32ab4e871 100644 --- a/Syntaxes/Diagnostics.sublime-syntax +++ b/Syntaxes/Diagnostics.sublime-syntax @@ -11,7 +11,7 @@ contexts: - include: line file: - - match: ^(.*)(:)$ + - match: ^(?!\s*\d+:\d+)(.*)(:)$ captures: 0: meta.diagnostic.preamble.lsp 1: string.unquoted.lsp diff --git a/plugin/core/windows.py b/plugin/core/windows.py index ff39df103..f0b39f18b 100644 --- a/plugin/core/windows.py +++ b/plugin/core/windows.py @@ -430,11 +430,10 @@ def update_diagnostics_panel_async(self) -> None: to_render.append("{}:".format(file_path)) row += 1 for content, offset, code, href in contribution: - row += 1 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") + row += content.count("\n") + 1 for listener in listeners: set_diagnostics_count(listener.view, self.total_error_count, self.total_warning_count) characters = "\n".join(to_render)