Skip to content

Commit

Permalink
Add option to show diagnostics as annotations (#1702)
Browse files Browse the repository at this point in the history
  • Loading branch information
rchl authored Jun 21, 2023
1 parent a074944 commit d37244f
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 9 deletions.
10 changes: 10 additions & 0 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@
// hint: 4
"show_diagnostics_severity_level": 4,

// Show diagnostics as annotations with level equal to or less than:
// none: 0 (never show)
// error: 1
// warning: 2
// info: 3
// hint: 4
// When enabled, it's recommended not to use the "annotation" value for the
// `show_code_actions` option as it's impossible to enforce which one gets shown first.
"show_diagnostics_annotations_severity_level": 0,

// Open the diagnostics panel automatically on save when diagnostics level is
// equal to or less than:
// none: 0 (never open the panel automatically)
Expand Down
16 changes: 16 additions & 0 deletions annotations.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.lsp_annotation {
margin: 0;
border-width: 0;
}
.lsp_annotation .errors {
color: color(var(--redish) alpha(0.85));
}
.lsp_annotation .warnings {
color: color(var(--yellowish) alpha(0.85));
}
.lsp_annotation .info {
color: color(var(--bluish) alpha(0.85));
}
.lsp_annotation .hints {
color: color(var(--bluish) alpha(0.85));
}
2 changes: 2 additions & 0 deletions plugin/core/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def __init__(self) -> None:
self.sheets = sublime.load_resource("Packages/LSP/sheets.css")
self.sheets_classname = "lsp_sheet"
self.inlay_hints = sublime.load_resource("Packages/LSP/inlay_hints.css")
self.annotations = sublime.load_resource("Packages/LSP/annotations.css")
self.annotations_classname = "lsp_annotation"


_css = None # type: Optional[CSS]
Expand Down
2 changes: 1 addition & 1 deletion plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ def additional_variables(cls) -> Optional[Dict[str, str]]:
def storage_path(cls) -> str:
"""
The storage path. Use this as your base directory to install server files. Its path is '$DATA/Package Storage'.
You should have an additional subdirectory preferrably the same name as your plugin. For instance:
You should have an additional subdirectory preferably the same name as your plugin. For instance:
```python
from LSP.plugin import AbstractPlugin
Expand Down
8 changes: 5 additions & 3 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def debounced(f: Callable[[], Any], timeout_ms: int = 0, condition: Callable[[],
:param f: The function to possibly run. Its return type is discarded.
:param timeout_ms: The time in milliseconds after which to possibly to run the function
:param condition: The condition that must evaluate to True in order to run the funtion
:param condition: The condition that must evaluate to True in order to run the function
:param async_thread: If true, run the function on the async worker thread, otherwise run the function on the
main thread
"""
Expand Down Expand Up @@ -142,7 +142,7 @@ class DebouncerNonThreadSafe:
the callback function will only be called once, after `timeout_ms` since the last call.
This implementation is not thread safe. You must ensure that `debounce()` is called from the same thread as
was choosen during initialization through the `async_thread` argument.
was chosen during initialization through the `async_thread` argument.
"""

def __init__(self, async_thread: bool) -> None:
Expand All @@ -158,7 +158,7 @@ def debounce(
:param f: The function to possibly run
:param timeout_ms: The time in milliseconds after which to possibly to run the function
:param condition: The condition that must evaluate to True in order to run the funtion
:param condition: The condition that must evaluate to True in order to run the function
"""

def run(debounce_id: int) -> None:
Expand Down Expand Up @@ -214,6 +214,7 @@ class Settings:
show_code_lens = cast(str, None)
show_inlay_hints = cast(bool, None)
show_code_actions_in_hover = cast(bool, None)
show_diagnostics_annotations_severity_level = cast(int, None)
show_diagnostics_count_in_view_status = cast(bool, None)
show_multiline_diagnostics_highlights = cast(bool, None)
show_multiline_document_highlights = cast(bool, None)
Expand Down Expand Up @@ -254,6 +255,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None:
r("show_code_lens", "annotation")
r("show_inlay_hints", False)
r("show_code_actions_in_hover", True)
r("show_diagnostics_annotations_severity_level", 0)
r("show_diagnostics_count_in_view_status", False)
r("show_diagnostics_in_view_status", True)
r("show_multiline_diagnostics_highlights", True)
Expand Down
19 changes: 18 additions & 1 deletion plugin/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -872,14 +872,31 @@ def diagnostic_severity(diagnostic: Diagnostic) -> DiagnosticSeverity:
return diagnostic.get("severity", DiagnosticSeverity.Error)


def format_diagnostics_for_annotation(
diagnostics: List[Diagnostic], severity: DiagnosticSeverity, view: sublime.View
) -> Tuple[List[str], str]:
css_class = DIAGNOSTIC_SEVERITY[severity - 1][1]
scope = DIAGNOSTIC_SEVERITY[severity - 1][2]
color = view.style_for_scope(scope).get('foreground') or 'red'
annotations = []
for diagnostic in diagnostics:
message = text2html(diagnostic.get('message') or '')
source = diagnostic.get('source')
line = "[{}] {}".format(text2html(source), message) if source else message
content = '<body id="annotation" class="{1}"><style>{0}</style><div class="{2}">{3}</div></body>'.format(
lsp_css().annotations, lsp_css().annotations_classname, css_class, line)
annotations.append(content)
return (annotations, color)


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
When the last three elements are not optional, show an inline phantom
using the information given.
"""
formatted, code, href = diagnostic_source_and_code(diagnostic)
Expand Down
43 changes: 43 additions & 0 deletions plugin/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from .core.protocol import Diagnostic
from .core.protocol import DiagnosticSeverity
from .core.settings import userprefs
from .core.typing import List, Tuple
from .core.views import DIAGNOSTIC_KINDS
from .core.views import diagnostic_severity
from .core.views import format_diagnostics_for_annotation
import sublime


class DiagnosticsAnnotationsView():

def __init__(self, view: sublime.View, config_name: str) -> None:
self._view = view
self._config_name = config_name

def initialize_region_keys(self) -> None:
r = [sublime.Region(0, 0)]
for severity in DIAGNOSTIC_KINDS.keys():
self._view.add_regions(self._annotation_region_key(severity), r)

def _annotation_region_key(self, severity: DiagnosticSeverity) -> str:
return 'lsp_da-{}-{}'.format(severity, self._config_name)

def draw(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None:
flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE
max_severity_level = userprefs().show_diagnostics_annotations_severity_level
# To achieve the correct order of annotations (most severe having priority) we have to add regions from the
# most to the least severe.
for severity in DIAGNOSTIC_KINDS.keys():
if severity <= max_severity_level:
matching_diagnostics = ([], []) # type: Tuple[List[Diagnostic], List[sublime.Region]]
for diagnostic, region in diagnostics:
if diagnostic_severity(diagnostic) != severity:
continue
matching_diagnostics[0].append(diagnostic)
matching_diagnostics[1].append(region)
annotations, color = format_diagnostics_for_annotation(matching_diagnostics[0], severity, self._view)
self._view.add_regions(
self._annotation_region_key(severity), matching_diagnostics[1], flags=flags,
annotations=annotations, annotation_color=color)
else:
self._view.erase_regions(self._annotation_region_key(severity))
6 changes: 3 additions & 3 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ def diagnostics_intersecting_region_async(
for diagnostic, candidate in diagnostics:
# Checking against points is inclusive unlike checking whether region intersects another region
# which is exclusive (at region end) and we want an inclusive behavior in this case.
if region.contains(candidate.a) or region.contains(candidate.b):
if region.intersects(candidate) or region.contains(candidate.a) or region.contains(candidate.b):
covering = covering.cover(candidate)
intersections.append(diagnostic)
if intersections:
Expand Down Expand Up @@ -354,7 +354,7 @@ def on_activated_async(self) -> None:
sb.do_inlay_hints_async(self.view)

def on_selection_modified_async(self) -> None:
first_region, any_different = self._update_stored_selection_async()
first_region, _ = self._update_stored_selection_async()
if first_region is None:
return
if not self._is_in_higlighted_region(first_region.b):
Expand Down Expand Up @@ -862,7 +862,7 @@ def _register_async(self) -> None:
def _on_view_updated_async(self) -> None:
self._code_lenses_debouncer_async.debounce(
self._do_code_lenses_async, timeout_ms=self.code_lenses_debounce_time)
first_region, any_different = self._update_stored_selection_async()
first_region, _ = self._update_stored_selection_async()
if first_region is None:
return
self._clear_highlight_regions()
Expand Down
2 changes: 1 addition & 1 deletion plugin/goto_diagnostic.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def name(self) -> str:
return "diagnostic"

def list_items(self) -> List[sublime.ListInputItem]:
list_items = []
list_items = [] # type: 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),
Expand Down
7 changes: 7 additions & 0 deletions plugin/session_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .core.views import DIAGNOSTIC_SEVERITY
from .core.views import DiagnosticSeverityData
from .core.views import text_document_identifier
from .diagnostics import DiagnosticsAnnotationsView
from .session_buffer import SessionBuffer
from weakref import ref
from weakref import WeakValueDictionary
Expand Down Expand Up @@ -51,6 +52,7 @@ class SessionView:
def __init__(self, listener: AbstractViewListener, session: Session, uri: DocumentUri) -> None:
self._view = listener.view
self._session = session
self._diagnostic_annotations = DiagnosticsAnnotationsView(self._view, session.config.name)
self._initialize_region_keys()
self._active_requests = {} # type: Dict[int, ActiveRequest]
self._listener = ref(listener)
Expand Down Expand Up @@ -96,6 +98,9 @@ def on_before_remove(self) -> None:
self.view.erase_regions("{}_underline".format(self.diagnostics_key(severity, True)))
self.view.erase_regions("lsp_document_link")
self.session_buffer.remove_session_view(self)
listener = self.listener()
if listener:
listener.on_diagnostics_updated_async(False)

@property
def session(self) -> Session:
Expand Down Expand Up @@ -156,6 +161,7 @@ def _initialize_region_keys(self) -> None:
self.view.add_regions("lsp_highlight_{}{}".format(kind, mode), r)
if hover_highlight_style in ("underline", "stippled"):
self.view.add_regions(HOVER_HIGHLIGHT_KEY, r)
self._diagnostic_annotations.initialize_region_keys()

def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None:
'''Remove all of our modifications to the view's "auto_complete_triggers"'''
Expand Down Expand Up @@ -293,6 +299,7 @@ def present_diagnostics_async(
data_per_severity, sev, level, flags[sev - 1] or DIAGNOSTIC_SEVERITY[sev - 1][4], multiline=False)
self._draw_diagnostics(
data_per_severity, sev, level, multiline_flags or DIAGNOSTIC_SEVERITY[sev - 1][5], multiline=True)
self._diagnostic_annotations.draw(self.session_buffer.diagnostics)
listener = self.listener()
if listener:
listener.on_diagnostics_updated_async(is_view_visible)
Expand Down
18 changes: 18 additions & 0 deletions sublime-package.json
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,24 @@
"maximum": 4,
"markdownDescription": "Show highlights and gutter markers in the file views for diagnostics with level equal to or less than:\n\n- _none_: `0`,\n- _error_: `1`,\n- _warning_: `2`,\n- _info_: `3`,\n- _hint_: `4`"
},
"show_diagnostics_annotations_severity_level": {
"default": 0,
"enum": [
0,
1,
2,
3,
4
],
"markdownDescription": "Show diagnostics as annotations with level equal to or less than given value.\n\nWhen enabled, it's recommended not to use the `\"annotation\"` value for the `show_code_actions` option as it's impossible to enforce which one gets shown first.",
"markdownEnumDescriptions": [
"never show",
"error",
"warning",
"info",
"hint"
]
},
"diagnostics_panel_include_severity_level": {
"type": "integer",
"minimum": 1,
Expand Down

0 comments on commit d37244f

Please sign in to comment.