From 603ece5561e94d97e501d2b1bfe87a424d5cf3ff Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 29 May 2021 22:40:25 +0200 Subject: [PATCH 01/55] Show all diagnostics as annotations --- LSP.sublime-settings | 3 +++ annotations.css | 16 ++++++++++++++++ plugin/core/css.py | 2 ++ plugin/core/types.py | 6 ++++-- plugin/core/views.py | 20 ++++++++++++++++++++ plugin/documents.py | 34 ++++++++++++++++++++++++++++++++-- plugin/session_buffer.py | 6 +++++- plugin/session_view.py | 10 +++++++++- sublime-package.json | 14 ++++++++++++++ 9 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 annotations.css diff --git a/LSP.sublime-settings b/LSP.sublime-settings index cef118333..29f7f5e0f 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -47,6 +47,9 @@ // under the cursor in status bar if available. "show_diagnostics_in_view_status": true, + // Show the diagnostics as inline annotations. + "show_diagnostics_inline": "off", + // Show highlights and gutter markers in the file views for diagnostics // with level equal to or less than: // none: 0 (never show) diff --git a/annotations.css b/annotations.css new file mode 100644 index 000000000..e20ed8866 --- /dev/null +++ b/annotations.css @@ -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)); +} diff --git a/plugin/core/css.py b/plugin/core/css.py index ec8828489..205744458 100644 --- a/plugin/core/css.py +++ b/plugin/core/css.py @@ -10,6 +10,8 @@ def __init__(self) -> None: self.notification_classname = "notification" self.sheets = sublime.load_resource("Packages/LSP/sheets.css") self.sheets_classname = "lsp_sheet" + self.annotations = sublime.load_resource("Packages/LSP/annotations.css") + self.annotations_classname = "lsp_annotation" _css = None # type: Optional[CSS] diff --git a/plugin/core/types.py b/plugin/core/types.py index 8f2b3c5cf..658c297bc 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -2,8 +2,8 @@ from .file_watcher import FileWatcherEventType from .logging import debug, set_debug_logging from .protocol import TextDocumentSyncKindNone -from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Union, Set, Tuple, TypedDict, TypeVar -from .typing import cast +from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Literal, Union, Set, Tuple +from .typing import cast, TypedDict, TypeVar from .url import filename_to_uri from .url import parse_uri from threading import RLock @@ -209,6 +209,7 @@ class Settings: show_diagnostics_count_in_view_status = None # type: bool show_multiline_diagnostics_highlights = None # type: bool show_diagnostics_in_view_status = None # type: bool + show_diagnostics_inline = None # type: Literal["all", "at-cursor", "off"] show_diagnostics_panel_on_save = None # type: int show_diagnostics_severity_level = None # type: int show_references_in_quick_panel = None # type: bool @@ -244,6 +245,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("show_code_actions_in_hover", True) r("show_diagnostics_count_in_view_status", False) r("show_diagnostics_in_view_status", True) + r("show_diagnostics_inline", "none") r("show_multiline_diagnostics_highlights", True) r("show_diagnostics_panel_on_save", 2) r("show_diagnostics_severity_level", 2) diff --git a/plugin/core/views.py b/plugin/core/views.py index 89fdafcc8..45c54fd44 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -703,6 +703,26 @@ def diagnostic_source(diagnostic: Diagnostic) -> str: return diagnostic.get("source", "unknown-source") +def format_diagnostics_for_annotation( + diagnostics: List[Diagnostic], view: sublime.View +) -> Tuple[List[sublime.Region], List[str]]: + regions = [] + annotations = [] + for diagnostic in diagnostics: + lsp_range = diagnostic.get('range') + if not lsp_range: + continue + message = text2html(diagnostic.get('message') or '') + source = diagnostic.get('source') + css_class = DIAGNOSTIC_SEVERITY[diagnostic_severity(diagnostic) - 1][1] + line = "[{}] {}".format(source, message) if source else message + content = '
{3}
'.format( + lsp_css().annotations, lsp_css().annotations_classname, css_class, line) + regions.append(range_to_region(Range.from_lsp(lsp_range), view)) + annotations.append(content) + return (regions, annotations) + + 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. diff --git a/plugin/documents.py b/plugin/documents.py index 63f515bad..767080902 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -27,9 +27,11 @@ 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 first_selection_region from .core.views import format_completion +from .core.views import format_diagnostics_for_annotation from .core.views import make_command_link from .core.views import MarkdownLangMap from .core.views import range_to_region @@ -257,7 +259,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: @@ -289,6 +291,7 @@ def on_diagnostics_updated_async(self) -> None: if userprefs().show_code_actions: self._do_code_actions() self._update_diagnostic_in_status_bar_async() + self._update_inline_diagnostic_async() def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: @@ -304,6 +307,30 @@ def _update_diagnostic_in_status_bar_async(self) -> None: return self.view.erase_status(self.ACTIVE_DIAGNOSTIC) + def _update_inline_diagnostic_async(self) -> None: + region_key = "lsp_d-a" + self.view.erase_regions(region_key) + if userprefs().show_diagnostics_inline != 'at-cursor': + return + r = first_selection_region(self.view) + if r is None: + return + sorted_diagnostics = [] # type: List[Diagnostic] + session_buffer_diagnostics, _ = self.diagnostics_intersecting_region_async(r) + for _, diagnostics in session_buffer_diagnostics: + sorted_diagnostics.extend(diagnostics) + if sorted_diagnostics: + sorted_diagnostics = sorted(sorted_diagnostics, key=lambda d: d.get('severity', 1)) + first_diagnostic = sorted_diagnostics[0] + lsp_range = first_diagnostic.get('range') + if lsp_range: + scope = DIAGNOSTIC_SEVERITY[first_diagnostic.get('severity', 1) - 1][2] + icon = "" + flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE + annotation_color = self.view.style_for_scope(scope).get('foreground') or 'red' + regions, annotations = format_diagnostics_for_annotation(sorted_diagnostics, self.view) + self.view.add_regions(region_key, regions, scope, icon, flags, annotations, annotation_color) + def session_views_async(self) -> Generator[SessionView, None, None]: yield from self._session_views.values() @@ -364,6 +391,7 @@ def on_selection_modified_async(self) -> None: self._when_selection_remains_stable_async(self._do_code_actions, current_region, after_ms=self.code_actions_debounce_time) self._update_diagnostic_in_status_bar_async() + self._update_inline_diagnostic_async() self._resolve_visible_code_lenses_async() def on_post_save_async(self) -> None: @@ -555,7 +583,9 @@ def _on_code_actions(self, responses: CodeActionsByConfigName) -> None: flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE annotations = [] annotation_color = "" - if userprefs().show_code_actions == 'bulb': + if userprefs().show_code_actions == 'bulb' or ( + userprefs().show_code_actions == 'annotation' and userprefs().show_diagnostics_inline == 'at-cursor' + ): scope = 'region.yellowish lightbulb.lsp' icon = 'Packages/LSP/icons/lightbulb.png' self._lightbulb_line = self.view.rowcol(regions[0].begin())[0] diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 0192cd317..d31d14e9b 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -47,9 +47,11 @@ def update(self, version: int, changes: Iterable[sublime.TextChange]) -> None: class DiagnosticSeverityData: - __slots__ = ('regions', 'regions_with_tag', 'annotations', 'scope', 'icon') + __slots__ = ('region_diagnostics', 'tag_diagnostics', 'regions', 'regions_with_tag', 'annotations', 'scope', 'icon') def __init__(self, severity: int) -> None: + self.region_diagnostics = [] # type: List[Diagnostic] + self.tag_diagnostics = [] # type: List[Diagnostic] self.regions = [] # type: List[sublime.Region] self.regions_with_tag = {} # type: Dict[int, List[sublime.Region]] self.annotations = [] # type: List[str] @@ -361,8 +363,10 @@ def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optio if tags: for tag in tags: data.regions_with_tag.setdefault(tag, []).append(region) + data.tag_diagnostics.append(diagnostic) else: data.regions.append(region) + data.region_diagnostics.append(diagnostic) diagnostics.append((diagnostic, region)) if severity == DiagnosticSeverity.Error: total_errors += 1 diff --git a/plugin/session_view.py b/plugin/session_view.py index 9cb7907f3..ca49ab8a1 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -278,10 +278,18 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, tag_scope = self.diagnostics_tag_scope(tag) # Trick to only add tag regions if there is a corresponding color scheme scope defined. if tag_scope and 'background' in self.view.style_for_scope(tag_scope): + # annotations = [format_diagnostic_for_annotation(diag) for diag in data.tag_diagnostics] + # annotation_color = self.view.style_for_scope(tag_scope)['foreground'] + # self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE, + # annotations=annotations, annotation_color=annotation_color) self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE) else: non_tag_regions.extend(regions) - self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags) + # annotations = [format_diagnostic_for_annotation(diag) for diag in data.region_diagnostics] + # annotation_color = self.view.style_for_scope(data.scope)['foreground'] + # self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags | sublime.DRAW_EMPTY, + # annotations, annotation_color) + self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags | sublime.DRAW_EMPTY) else: self.view.erase_regions(key) diff --git a/sublime-package.json b/sublime-package.json index c1eedc347..fa8669563 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -285,6 +285,20 @@ "default": true, "markdownDescription": "Show the diagnostics description of the code under the cursor in status bar if available." }, + "show_diagnostics_inline": { + "default": "off", + "enum": [ + "all", + "at-cursor", + "off" + ], + "markdownEnumDescriptions": [ + "Show all existing diagnostics as inline annotations.", + "Only show those diagnostics inline that intersect with current selection / cursor. Enabling this option forces the `show_code_actions` option to `\"bulb\"` if set to `\"annotation\"`.", + "Don't show diagnostics inline." + ], + "markdownDescription": "Show the diagnostics as inline annotations." + }, "show_diagnostics_severity_level": { "type": "integer", "default": 4, From 2563f4aa682daa9f377cbf8cab02444a840cdb13 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 14 Jul 2022 23:13:45 +0200 Subject: [PATCH 02/55] remove "all" option --- LSP.sublime-settings | 5 ++++- plugin/core/types.py | 2 +- plugin/documents.py | 6 ++---- plugin/session_view.py | 8 -------- sublime-package.json | 8 +++----- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index d940575ea..abe3228d4 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -48,7 +48,10 @@ "show_diagnostics_in_view_status": true, // Show the diagnostics as inline annotations. - "show_diagnostics_inline": "off", + // When using the "at-cursor" value it's recommended to not use the "annotation" + // value for the "show_code_actions" option as then the code action annotations will + // show over the diagnostic annotations. + "show_diagnostics_inline": "none", // Show highlights and gutter markers in the file views for diagnostics // with level equal to or less than: diff --git a/plugin/core/types.py b/plugin/core/types.py index f53c280cd..842e49fa1 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -210,7 +210,7 @@ class Settings: show_diagnostics_count_in_view_status = None # type: bool show_multiline_diagnostics_highlights = None # type: bool show_diagnostics_in_view_status = None # type: bool - show_diagnostics_inline = None # type: Literal["all", "at-cursor", "off"] + show_diagnostics_inline = None # type: Literal["all", "at-cursor", "none"] show_diagnostics_panel_on_save = None # type: int show_diagnostics_severity_level = None # type: int show_references_in_quick_panel = None # type: bool diff --git a/plugin/documents.py b/plugin/documents.py index 767080902..5d1d31c69 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -308,7 +308,7 @@ def _update_diagnostic_in_status_bar_async(self) -> None: self.view.erase_status(self.ACTIVE_DIAGNOSTIC) def _update_inline_diagnostic_async(self) -> None: - region_key = "lsp_d-a" + region_key = "lsp_d-ann" self.view.erase_regions(region_key) if userprefs().show_diagnostics_inline != 'at-cursor': return @@ -583,9 +583,7 @@ def _on_code_actions(self, responses: CodeActionsByConfigName) -> None: flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE annotations = [] annotation_color = "" - if userprefs().show_code_actions == 'bulb' or ( - userprefs().show_code_actions == 'annotation' and userprefs().show_diagnostics_inline == 'at-cursor' - ): + if userprefs().show_code_actions == 'bulb': scope = 'region.yellowish lightbulb.lsp' icon = 'Packages/LSP/icons/lightbulb.png' self._lightbulb_line = self.view.rowcol(regions[0].begin())[0] diff --git a/plugin/session_view.py b/plugin/session_view.py index 5795201c4..4b4d806eb 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -280,17 +280,9 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, tag_scope = self.diagnostics_tag_scope(tag) # Trick to only add tag regions if there is a corresponding color scheme scope defined. if tag_scope and 'background' in self.view.style_for_scope(tag_scope): - # annotations = [format_diagnostic_for_annotation(diag) for diag in data.tag_diagnostics] - # annotation_color = self.view.style_for_scope(tag_scope)['foreground'] - # self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE, - # annotations=annotations, annotation_color=annotation_color) self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE) else: non_tag_regions.extend(regions) - # annotations = [format_diagnostic_for_annotation(diag) for diag in data.region_diagnostics] - # annotation_color = self.view.style_for_scope(data.scope)['foreground'] - # self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags | sublime.DRAW_EMPTY, - # annotations, annotation_color) self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags | sublime.DRAW_EMPTY) else: self.view.erase_regions(key) diff --git a/sublime-package.json b/sublime-package.json index 3b6c74347..43dcc50cc 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -286,15 +286,13 @@ "markdownDescription": "Show the diagnostics description of the code under the cursor in status bar if available." }, "show_diagnostics_inline": { - "default": "off", + "default": "none", "enum": [ - "all", "at-cursor", - "off" + "none" ], "markdownEnumDescriptions": [ - "Show all existing diagnostics as inline annotations.", - "Only show those diagnostics inline that intersect with current selection / cursor. Enabling this option forces the `show_code_actions` option to `\"bulb\"` if set to `\"annotation\"`.", + "Show diagnostics that intersect with the current selection / cursor inline. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as then the code action annotations will show over the diagnostic annotations.", "Don't show diagnostics inline." ], "markdownDescription": "Show the diagnostics as inline annotations." From 69a750d98ee8f6a030c2754061f2f3997a8cc91e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 14 Jul 2022 23:26:10 +0200 Subject: [PATCH 03/55] handle multiple regions --- plugin/documents.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 5d1d31c69..15a8e7993 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -312,13 +312,11 @@ def _update_inline_diagnostic_async(self) -> None: self.view.erase_regions(region_key) if userprefs().show_diagnostics_inline != 'at-cursor': return - r = first_selection_region(self.view) - if r is None: - return sorted_diagnostics = [] # type: List[Diagnostic] - session_buffer_diagnostics, _ = self.diagnostics_intersecting_region_async(r) - for _, diagnostics in session_buffer_diagnostics: - sorted_diagnostics.extend(diagnostics) + for r in self.view.sel(): + session_buffer_diagnostics, _ = self.diagnostics_intersecting_region_async(r) + for _, diagnostics in session_buffer_diagnostics: + sorted_diagnostics.extend(diagnostics) if sorted_diagnostics: sorted_diagnostics = sorted(sorted_diagnostics, key=lambda d: d.get('severity', 1)) first_diagnostic = sorted_diagnostics[0] From b11799683fb1099f0972459e85e817ab4780e815 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 14 Jul 2022 23:43:23 +0200 Subject: [PATCH 04/55] remove unused code --- plugin/session_buffer.py | 6 +----- plugin/session_view.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 62022a345..023d274c6 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -49,11 +49,9 @@ def update(self, version: int, changes: Iterable[sublime.TextChange]) -> None: class DiagnosticSeverityData: - __slots__ = ('region_diagnostics', 'tag_diagnostics', 'regions', 'regions_with_tag', 'annotations', 'scope', 'icon') + __slots__ = ('regions', 'regions_with_tag', 'annotations', 'scope', 'icon') def __init__(self, severity: int) -> None: - self.region_diagnostics = [] # type: List[Diagnostic] - self.tag_diagnostics = [] # type: List[Diagnostic] self.regions = [] # type: List[sublime.Region] self.regions_with_tag = {} # type: Dict[int, List[sublime.Region]] self.annotations = [] # type: List[str] @@ -405,10 +403,8 @@ def on_diagnostics_async(self, raw_diagnostics: List[Diagnostic], version: Optio if tags: for tag in tags: data.regions_with_tag.setdefault(tag, []).append(region) - data.tag_diagnostics.append(diagnostic) else: data.regions.append(region) - data.region_diagnostics.append(diagnostic) diagnostics.append((diagnostic, region)) if severity == DiagnosticSeverity.Error: total_errors += 1 diff --git a/plugin/session_view.py b/plugin/session_view.py index 4b4d806eb..48f5c5b36 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -283,7 +283,7 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, self.view.add_regions(key_tags[tag], regions, tag_scope, flags=sublime.DRAW_NO_OUTLINE) else: non_tag_regions.extend(regions) - self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags | sublime.DRAW_EMPTY) + self.view.add_regions(key, non_tag_regions, data.scope, data.icon, flags) else: self.view.erase_regions(key) From 846e21e2202ce9b09919c74bf4b1cda567489997 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 15 Jul 2022 00:00:31 +0200 Subject: [PATCH 05/55] fixes --- plugin/core/types.py | 2 +- plugin/documents.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index 842e49fa1..ac6b07171 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -210,7 +210,7 @@ class Settings: show_diagnostics_count_in_view_status = None # type: bool show_multiline_diagnostics_highlights = None # type: bool show_diagnostics_in_view_status = None # type: bool - show_diagnostics_inline = None # type: Literal["all", "at-cursor", "none"] + show_diagnostics_inline = None # type: Literal["at-cursor", "none"] show_diagnostics_panel_on_save = None # type: int show_diagnostics_severity_level = None # type: int show_references_in_quick_panel = None # type: bool diff --git a/plugin/documents.py b/plugin/documents.py index 15a8e7993..154c9c31e 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -308,7 +308,7 @@ def _update_diagnostic_in_status_bar_async(self) -> None: self.view.erase_status(self.ACTIVE_DIAGNOSTIC) def _update_inline_diagnostic_async(self) -> None: - region_key = "lsp_d-ann" + region_key = "lsp_d-annotations" self.view.erase_regions(region_key) if userprefs().show_diagnostics_inline != 'at-cursor': return From 4e674129a5cb8941e119968afe8b710af17fd4cb Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 30 Aug 2022 00:37:12 +0200 Subject: [PATCH 06/55] move handling to SessionView to get instant feedback on restarting server --- plugin/core/sessions.py | 3 +++ plugin/documents.py | 27 +++++++-------------------- plugin/session_view.py | 27 +++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 4c5ee436e..3a054abbf 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -481,6 +481,9 @@ def shutdown_async(self) -> None: def present_diagnostics_async(self) -> None: ... + def update_inline_diagnostics_async(self, diagnostics: List[Diagnostic]) -> None: + ... + def on_request_started_async(self, request_id: int, request: Request) -> None: ... diff --git a/plugin/documents.py b/plugin/documents.py index 6d6ce3ed7..77deafd83 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -284,7 +284,7 @@ def on_diagnostics_updated_async(self) -> None: if userprefs().show_code_actions: self._do_code_actions() self._update_diagnostic_in_status_bar_async() - self._update_inline_diagnostic_async() + self._update_inline_diagnostics_async() def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: @@ -300,27 +300,14 @@ def _update_diagnostic_in_status_bar_async(self) -> None: return self.view.erase_status(self.ACTIVE_DIAGNOSTIC) - def _update_inline_diagnostic_async(self) -> None: - region_key = "lsp_d-annotations" - self.view.erase_regions(region_key) - if userprefs().show_diagnostics_inline != 'at-cursor': - return - sorted_diagnostics = [] # type: List[Diagnostic] + def _update_inline_diagnostics_async(self) -> None: + diagnostics = [] # type: List[Diagnostic] for r in self.view.sel(): session_buffer_diagnostics, _ = self.diagnostics_intersecting_region_async(r) for _, diagnostics in session_buffer_diagnostics: - sorted_diagnostics.extend(diagnostics) - if sorted_diagnostics: - sorted_diagnostics = sorted(sorted_diagnostics, key=lambda d: d.get('severity', 1)) - first_diagnostic = sorted_diagnostics[0] - lsp_range = first_diagnostic.get('range') - if lsp_range: - scope = DIAGNOSTIC_SEVERITY[first_diagnostic.get('severity', 1) - 1][2] - icon = "" - flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE - annotation_color = self.view.style_for_scope(scope).get('foreground') or 'red' - regions, annotations = format_diagnostics_for_annotation(sorted_diagnostics, self.view) - self.view.add_regions(region_key, regions, scope, icon, flags, annotations, annotation_color) + diagnostics.extend(diagnostics) + for sv in self.session_views_async(): + sv.update_inline_diagnostics_async(diagnostics) def session_views_async(self) -> Generator[SessionView, None, None]: yield from self._session_views.values() @@ -389,7 +376,7 @@ def on_selection_modified_async(self) -> None: self._when_selection_remains_stable_async(self._do_code_actions, current_region, after_ms=self.code_actions_debounce_time) self._update_diagnostic_in_status_bar_async() - self._update_inline_diagnostic_async() + self._update_inline_diagnostics_async() self._resolve_visible_code_lenses_async() def on_post_save_async(self) -> None: diff --git a/plugin/session_view.py b/plugin/session_view.py index 7923ba992..7f812743c 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -2,6 +2,7 @@ from .core.progress import ViewProgressReporter from .core.promise import Promise from .core.protocol import CodeLens +from .core.protocol import Diagnostic from .core.protocol import DiagnosticTag from .core.protocol import DocumentUri from .core.protocol import Notification @@ -12,6 +13,7 @@ from .core.types import debounced from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY +from .core.views import format_diagnostics_for_annotation from .core.views import text_document_identifier from .session_buffer import SessionBuffer from weakref import ref @@ -82,6 +84,7 @@ def on_before_remove(self) -> None: self.view.erase_regions(self.diagnostics_key(severity, False)) self.view.erase_regions(self.diagnostics_key(severity, True)) self.view.erase_regions("lsp_document_link") + self._clear_inline_diagnostics_regions() self.session_buffer.remove_session_view(self) @property @@ -294,6 +297,30 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, else: self.view.erase_regions(key) + def update_inline_diagnostics_async(self, diagnostics: List[Diagnostic]) -> None: + region_key = self._inline_diagnostics_region_key() + self.view.erase_regions(region_key) + if userprefs().show_diagnostics_inline != 'at-cursor': + return + if diagnostics: + sorted_diagnostics = sorted(diagnostics, key=lambda d: d.get('severity', 1)) + first_diagnostic = sorted_diagnostics[0] + lsp_range = first_diagnostic.get('range') + if lsp_range: + scope = DIAGNOSTIC_SEVERITY[first_diagnostic.get('severity', 1) - 1][2] + icon = "" + flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE + annotation_color = self.view.style_for_scope(scope).get('foreground') or 'red' + regions, annotations = format_diagnostics_for_annotation(sorted_diagnostics, self.view) + self.view.add_regions(region_key, regions, scope, icon, flags, annotations, annotation_color) + + def _inline_diagnostics_region_key(self) -> str: + return "lsp_d-annotations" + + def _clear_inline_diagnostics_regions(self) -> None: + region_key = self._inline_diagnostics_region_key() + self.view.erase_regions(region_key) + def on_request_started_async(self, request_id: int, request: Request) -> None: self.active_requests[request_id] = request if request.progress: From ec3dfb96a9d596b759ce995d2b7c85559a01ef8f Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 30 Aug 2022 22:52:03 +0200 Subject: [PATCH 07/55] lint --- plugin/documents.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 77deafd83..601353ab8 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -29,13 +29,11 @@ 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_HIGHLIGHT_KIND_SCOPES from .core.views import DOCUMENT_HIGHLIGHT_KINDS from .core.views import first_selection_region from .core.views import format_completion -from .core.views import format_diagnostics_for_annotation from .core.views import make_command_link from .core.views import MarkdownLangMap from .core.views import range_to_region From 33a0ef0324ed650770cecb1f122361c03757c959 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 11 Sep 2022 23:22:15 +0200 Subject: [PATCH 08/55] maybe I'll get it right eventually --- plugin/core/sessions.py | 3 --- plugin/documents.py | 24 ++++++++++++++++++++---- plugin/session_view.py | 30 +++--------------------------- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 87d0cc7b7..28647c062 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -486,9 +486,6 @@ def shutdown_async(self) -> None: def present_diagnostics_async(self) -> None: ... - def update_inline_diagnostics_async(self, diagnostics: List[Diagnostic]) -> None: - ... - def on_request_started_async(self, request_id: int, request: Request) -> None: ... diff --git a/plugin/documents.py b/plugin/documents.py index 6c77a32b9..4a4e14841 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -32,11 +32,13 @@ 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_HIGHLIGHT_KIND_SCOPES from .core.views import DOCUMENT_HIGHLIGHT_KINDS from .core.views import first_selection_region from .core.views import format_code_actions_for_quick_panel +from .core.views import format_diagnostics_for_annotation from .core.views import format_completion from .core.views import make_command_link from .core.views import MarkdownLangMap @@ -130,6 +132,7 @@ def __repr__(self) -> str: class DocumentSyncListener(sublime_plugin.ViewEventListener, AbstractViewListener): ACTIVE_DIAGNOSTIC = "lsp_active_diagnostic" + INLINE_DIAGNOSTIC_REGION_KEY = "lsp_d-annotations" code_actions_debounce_time = FEATURES_TIMEOUT color_boxes_debounce_time = FEATURES_TIMEOUT highlights_debounce_time = FEATURES_TIMEOUT @@ -303,13 +306,26 @@ def _update_diagnostic_in_status_bar_async(self) -> None: self.view.erase_status(self.ACTIVE_DIAGNOSTIC) def _update_inline_diagnostics_async(self) -> None: - diagnostics = [] # type: List[Diagnostic] + selections_diagnostics = [] # type: List[Diagnostic] for r in self.view.sel(): session_buffer_diagnostics, _ = self.diagnostics_intersecting_region_async(r) for _, diagnostics in session_buffer_diagnostics: - diagnostics.extend(diagnostics) - for sv in self.session_views_async(): - sv.update_inline_diagnostics_async(diagnostics) + selections_diagnostics.extend(diagnostics) + self.view.erase_regions(self.INLINE_DIAGNOSTIC_REGION_KEY) + if userprefs().show_diagnostics_inline != 'at-cursor': + return + if selections_diagnostics: + sorted_diagnostics = sorted(selections_diagnostics, key=lambda d: d.get('severity', 1)) + first_diagnostic = sorted_diagnostics[0] + lsp_range = first_diagnostic.get('range') + if lsp_range: + scope = DIAGNOSTIC_SEVERITY[first_diagnostic.get('severity', 1) - 1][2] + icon = "" + flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE + annotation_color = self.view.style_for_scope(scope).get('foreground') or 'red' + regions, annotations = format_diagnostics_for_annotation(sorted_diagnostics, self.view) + self.view.add_regions( + self.INLINE_DIAGNOSTIC_REGION_KEY, regions, scope, icon, flags, annotations, annotation_color) def session_views_async(self) -> Generator[SessionView, None, None]: yield from self._session_views.values() diff --git a/plugin/session_view.py b/plugin/session_view.py index 7f812743c..9710ce5cc 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -2,7 +2,6 @@ from .core.progress import ViewProgressReporter from .core.promise import Promise from .core.protocol import CodeLens -from .core.protocol import Diagnostic from .core.protocol import DiagnosticTag from .core.protocol import DocumentUri from .core.protocol import Notification @@ -13,7 +12,6 @@ from .core.types import debounced from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY -from .core.views import format_diagnostics_for_annotation from .core.views import text_document_identifier from .session_buffer import SessionBuffer from weakref import ref @@ -84,8 +82,10 @@ def on_before_remove(self) -> None: self.view.erase_regions(self.diagnostics_key(severity, False)) self.view.erase_regions(self.diagnostics_key(severity, True)) self.view.erase_regions("lsp_document_link") - self._clear_inline_diagnostics_regions() self.session_buffer.remove_session_view(self) + listener = self.listener() + if listener: + listener.on_diagnostics_updated_async() @property def session(self) -> Session: @@ -297,30 +297,6 @@ def _draw_diagnostics(self, severity: int, max_severity_level: int, flags: int, else: self.view.erase_regions(key) - def update_inline_diagnostics_async(self, diagnostics: List[Diagnostic]) -> None: - region_key = self._inline_diagnostics_region_key() - self.view.erase_regions(region_key) - if userprefs().show_diagnostics_inline != 'at-cursor': - return - if diagnostics: - sorted_diagnostics = sorted(diagnostics, key=lambda d: d.get('severity', 1)) - first_diagnostic = sorted_diagnostics[0] - lsp_range = first_diagnostic.get('range') - if lsp_range: - scope = DIAGNOSTIC_SEVERITY[first_diagnostic.get('severity', 1) - 1][2] - icon = "" - flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE - annotation_color = self.view.style_for_scope(scope).get('foreground') or 'red' - regions, annotations = format_diagnostics_for_annotation(sorted_diagnostics, self.view) - self.view.add_regions(region_key, regions, scope, icon, flags, annotations, annotation_color) - - def _inline_diagnostics_region_key(self) -> str: - return "lsp_d-annotations" - - def _clear_inline_diagnostics_regions(self) -> None: - region_key = self._inline_diagnostics_region_key() - self.view.erase_regions(region_key) - def on_request_started_async(self, request_id: int, request: Request) -> None: self.active_requests[request_id] = request if request.progress: From 70cad6ac9e00bac13437497bb80244446c702492 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 24 Sep 2022 10:01:16 +0200 Subject: [PATCH 09/55] Filter out non-quickfix actions in view --- plugin/code_actions.py | 392 ++++++++++++++++++----------------------- plugin/core/typing.py | 4 + plugin/core/views.py | 6 +- plugin/documents.py | 52 +++--- plugin/hover.py | 43 ++--- 5 files changed, 229 insertions(+), 268 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 62d3a4901..44170b866 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -7,9 +7,11 @@ from .core.protocol import Request from .core.registry import LspTextCommand from .core.registry import windows +from .core.sessions import AbstractViewListener +from .core.sessions import Session from .core.sessions import SessionBufferProtocol from .core.settings import userprefs -from .core.typing import Any, List, Dict, Callable, Optional, Tuple, Union, cast +from .core.typing import Any, List, Dict, Callable, Optional, Tuple, TypeGuard, Union, cast from .core.views import entire_content_region from .core.views import first_selection_region from .core.views import format_code_actions_for_quick_panel @@ -17,217 +19,169 @@ from .save_command import LspSaveCommand, SaveTask import sublime +ConfigName = str CodeActionOrCommand = Union[CodeAction, Command] -CodeActionsResponse = Optional[List[CodeActionOrCommand]] -CodeActionsByConfigName = Dict[str, List[CodeActionOrCommand]] +CodeActionsByConfigName = Tuple[ConfigName, List[CodeActionOrCommand]] -class CodeActionsCollector: - """ - Collects code action responses from multiple sessions. Calls back the "on_complete_handler" with - results when all responses are received. - - Usage example: - - with CodeActionsCollector() as collector: - actions_manager.request_with_diagnostics(collector.create_collector('test_config')) - actions_manager.request_with_diagnostics(collector.create_collector('another_config')) - - The "create_collector()" must only be called within the "with" context. Once the context is - exited, the "on_complete_handler" will be called once all the created collectors receive the - response (are called). - """ - - def __init__(self, on_complete_handler: Callable[[CodeActionsByConfigName], None]): - self._on_complete_handler = on_complete_handler - self._commands_by_config = {} # type: CodeActionsByConfigName - self._request_count = 0 - self._response_count = 0 - self._all_requested = False - - def __enter__(self) -> 'CodeActionsCollector': - return self - - def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: - self._all_requested = True - self._notify_if_all_finished() - - def create_collector(self, config_name: str) -> Callable[[CodeActionsResponse], None]: - self._request_count += 1 - return lambda actions: self._collect_response(config_name, actions) - - def _collect_response(self, config_name: str, actions: CodeActionsResponse) -> None: - self._response_count += 1 - self._commands_by_config[config_name] = self._get_enabled_actions(actions or []) - self._notify_if_all_finished() - - def _get_enabled_actions(self, actions: List[CodeActionOrCommand]) -> List[CodeActionOrCommand]: - return [action for action in actions if not action.get('disabled')] - - def _notify_if_all_finished(self) -> None: - if self._all_requested and self._request_count == self._response_count: - # Call back on Sublime's async thread - sublime.set_timeout_async(lambda: self._on_complete_handler(self._commands_by_config)) - - def get_actions(self) -> CodeActionsByConfigName: - return self._commands_by_config +def is_command(action: CodeActionOrCommand) -> TypeGuard[Command]: + return 'command' in action class CodeActionsManager: """Manager for per-location caching of code action responses.""" def __init__(self) -> None: - self._response_cache = None # type: Optional[Tuple[str, CodeActionsCollector]] + self._response_cache = None # type: Optional[Tuple[str, Promise[List[CodeActionsByConfigName]]]] def request_for_region_async( self, view: sublime.View, region: sublime.Region, session_buffer_diagnostics: List[Tuple[SessionBufferProtocol, List[Diagnostic]]], - actions_handler: Callable[[CodeActionsByConfigName], None], only_kinds: Optional[List[CodeActionKind]] = None, manual: bool = False, - ) -> None: + ) -> Promise[List[CodeActionsByConfigName]]: """ Requests code actions with provided diagnostics and specified region. If there are no diagnostics for given session, the request will be made with empty diagnostics list. """ - self._request_async( - view, - region, - session_buffer_diagnostics, - only_with_diagnostics=False, - actions_handler=actions_handler, - on_save_actions=None, - only_kinds=only_kinds, - manual=manual) - - def request_on_save( - self, - view: sublime.View, - actions_handler: Callable[[CodeActionsByConfigName], None], - on_save_actions: Dict[str, bool] - ) -> None: - """ - Requests code actions on save. - """ - listener = windows.listener_for_view(view) - if not listener: - return - region = entire_content_region(view) - session_buffer_diagnostics, _ = listener.diagnostics_intersecting_region_async(region) - self._request_async( - view, - region, - session_buffer_diagnostics, - only_with_diagnostics=False, - actions_handler=actions_handler, - on_save_actions=on_save_actions, - only_kinds=None, - manual=False) - - def _request_async( - self, - view: sublime.View, - region: sublime.Region, - session_buffer_diagnostics: List[Tuple[SessionBufferProtocol, List[Diagnostic]]], - only_with_diagnostics: bool, - actions_handler: Callable[[CodeActionsByConfigName], None], - on_save_actions: Optional[Dict[str, bool]] = None, - only_kinds: Optional[List[CodeActionKind]] = None, - manual: bool = False, - ) -> None: listener = windows.listener_for_view(view) if not listener: - return + return Promise.resolve([]) location_cache_key = None - use_cache = on_save_actions is None and not manual + use_cache = not manual if use_cache: - location_cache_key = "{}#{}:{}:{}".format( - view.buffer_id(), view.change_count(), region, only_with_diagnostics) + location_cache_key = "{}#{}:{}".format(view.buffer_id(), view.change_count(), region) if self._response_cache: - cache_key, cache_collector = self._response_cache + cache_key, task = self._response_cache if location_cache_key == cache_key: - sublime.set_timeout(lambda: actions_handler(cache_collector.get_actions())) - return + return task else: self._response_cache = None - collector = CodeActionsCollector(actions_handler) - with collector: - for session in listener.sessions_async('codeActionProvider'): - diagnostics = [] # type: List[Diagnostic] - for sb, diags in session_buffer_diagnostics: - if sb.session == session: - diagnostics = diags - break - if on_save_actions is not None: - supported_kinds = session.get_capability('codeActionProvider.codeActionKinds') # type: Optional[List[CodeActionKind]] # noqa: E501 - matching_kinds = get_matching_kinds(on_save_actions, supported_kinds or []) - if matching_kinds: - params = text_document_code_action_params(view, region, diagnostics, matching_kinds, manual) - request = Request.codeAction(params, view) - session.send_request_async( - request, *filtering_collector(session.config.name, matching_kinds, collector)) - else: - if only_with_diagnostics and not diagnostics: - continue - params = text_document_code_action_params(view, region, diagnostics, only_kinds, manual) - request = Request.codeAction(params, view) - session.send_request_async(request, collector.create_collector(session.config.name)) - if location_cache_key: - self._response_cache = (location_cache_key, collector) - - -def filtering_collector( - config_name: str, - kinds: List[CodeActionKind], - actions_collector: CodeActionsCollector -) -> Tuple[Callable[[CodeActionsResponse], None], Callable[[Any], None]]: - """ - Filters actions returned from the session so that only matching kinds are collected. - Since older servers don't support the "context.only" property, these will return all - actions that need to be filtered. - """ + def request_factory(session: Session) -> Optional[Request]: + diagnostics = [] # type: List[Diagnostic] + for sb, diags in session_buffer_diagnostics: + if sb.session == session: + diagnostics = diags + break + params = text_document_code_action_params(view, region, diagnostics, only_kinds, manual) + return Request.codeAction(params, view) + + def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> List[CodeActionOrCommand]: + # Filter out non "quickfix" code actions unless "only_kinds" is provided. + if only_kinds: + return [a for a in actions if not is_command(a) and is_kinds_include_kind(only_kinds, a.get('kind'))] + return [ + a for a in actions if is_command(a) or is_kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) + ] + + task = self._collect_code_actions_async(listener, request_factory, response_filter) + if location_cache_key: + self._response_cache = (location_cache_key, task) + return task - def actions_filter(actions: CodeActionsResponse) -> List[CodeActionOrCommand]: - return [a for a in (actions or []) if a.get('kind') in kinds] + def request_on_save_async( + self, view: sublime.View, on_save_actions: Dict[str, bool] + ) -> Promise[List[CodeActionsByConfigName]]: + listener = windows.listener_for_view(view) + if not listener: + return Promise.resolve([]) + region = entire_content_region(view) + session_buffer_diagnostics, _ = listener.diagnostics_intersecting_region_async(region) - collector = actions_collector.create_collector(config_name) - return ( - lambda actions: collector(actions_filter(actions)), - lambda error: collector([]) - ) + def request_factory(session: Session) -> Optional[Request]: + matching_kinds = get_matching_on_save_kinds(session, on_save_actions) + if not matching_kinds: + return None + diagnostics = [] # type: List[Diagnostic] + for sb, diags in session_buffer_diagnostics: + if sb.session == session: + diagnostics = diags + break + params = text_document_code_action_params(view, region, diagnostics, matching_kinds, manual=False) + return Request.codeAction(params, view) + + def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> List[CodeActionOrCommand]: + # Filter actions returned from the session so that only matching kinds are collected. + # Since older servers don't support the "context.only" property, those will return all + # actions that need to be then manually filtered. + matching_kinds = get_matching_on_save_kinds(session, on_save_actions) + return [a for a in actions if a.get('kind') in matching_kinds] + + return self._collect_code_actions_async(listener, request_factory, response_filter) + + def _collect_code_actions_async( + self, + listener: AbstractViewListener, + request_factory: Callable[[Session], Optional[Request]], + response_filter: Optional[Callable[[Session, List[CodeActionOrCommand]], List[CodeActionOrCommand]]] = None, + ) -> Promise[List[CodeActionsByConfigName]]: + + def on_response( + session: Session, response: Union[Error, Optional[List[CodeActionOrCommand]]] + ) -> CodeActionsByConfigName: + if isinstance(response, Error): + actions = [] + else: + actions = [action for action in (response or []) if not action.get('disabled')] + if actions and response_filter: + actions = response_filter(session, actions) + return (session.config.name, actions) + + tasks = [] # type: List[Promise[CodeActionsByConfigName]] + for session in listener.sessions_async('codeActionProvider'): + request = request_factory(session) + if request: + task = session.send_request_task(request) # type: Promise[Optional[List[CodeActionOrCommand]]] + tasks.append(task.then(lambda response: on_response(session, response))) + # Return only results for non-empty lists. + return Promise.all(tasks).then(lambda sessions: list(filter(lambda session: len(session[1]), sessions))) actions_manager = CodeActionsManager() -def get_matching_kinds(user_actions: Dict[str, bool], session_actions: List[CodeActionKind]) -> List[CodeActionKind]: +def get_matching_on_save_kinds(session: Session, user_actions: Dict[str, bool]) -> List[CodeActionKind]: """ - Filters user-enabled or disabled actions so that only ones matching the session actions - are returned. Returned actions are those that are enabled and are not overridden by more - specific, disabled actions. + Filters user-enabled or disabled actions so that only ones matching the session kinds + are returned. Returned kinds are those that are enabled and are not overridden by more + specific, disabled kinds. - Filtering only returns actions that exactly match the ones supported by given session. + Filtering only returns kinds that exactly match the ones supported by given session. If user has enabled a generic action that matches more specific session action (for example user's a.b matching session's a.b.c), then the more specific (a.b.c) must be - returned as servers must receive only actions that they advertise support for. + returned as servers must receive only kinds that they advertise support for. """ + session_kinds = session.get_capability('codeActionProvider.codeActionKinds') # type: Optional[List[CodeActionKind]] # noqa: E501 + if not session_kinds: + return [] matching_kinds = [] - for session_action in session_actions: + for session_kind in session_kinds: enabled = False - action_parts = cast(str, session_action).split('.') + action_parts = cast(str, session_kind).split('.') for i in range(len(action_parts)): current_part = '.'.join(action_parts[0:i + 1]) user_value = user_actions.get(current_part, None) if isinstance(user_value, bool): enabled = user_value if enabled: - matching_kinds.append(session_action) + matching_kinds.append(session_kind) return matching_kinds +def is_kinds_include_kind(kinds: List[CodeActionKind], kind: Optional[CodeActionKind]) -> bool: + """ + The "kinds" list includes "kind" if any of the "kinds" matches "kind" exactly or the "kind" prefix matches one + of the "kinds". + """ + if not kind: + return False + kind_parts = cast(str, kind).split('.') + return any(part for part in kind_parts if part in kinds) + + class CodeActionOnSaveTask(SaveTask): """ The main task that requests code actions from sessions and runs them. @@ -241,7 +195,7 @@ def is_applicable(cls, view: sublime.View) -> bool: @classmethod def _get_code_actions_on_save(cls, view: sublime.View) -> Dict[str, bool]: - view_code_actions = view.settings().get('lsp_code_actions_on_save') or {} + view_code_actions = cast(Dict[str, bool], view.settings().get('lsp_code_actions_on_save') or {}) code_actions = userprefs().lsp_code_actions_on_save.copy() code_actions.update(view_code_actions) allowed_code_actions = dict() @@ -257,20 +211,17 @@ def run_async(self) -> None: def _request_code_actions_async(self) -> None: self._purge_changes_async() on_save_actions = self._get_code_actions_on_save(self._task_runner.view) - actions_manager.request_on_save(self._task_runner.view, self._handle_response_async, on_save_actions) + actions_manager.request_on_save_async(self._task_runner.view, on_save_actions).then(self._handle_response_async) - def _handle_response_async(self, responses: CodeActionsByConfigName) -> None: + def _handle_response_async(self, responses: List[CodeActionsByConfigName]) -> None: if self._cancelled: return document_version = self._task_runner.view.change_count() tasks = [] # type: List[Promise] - for config_name, code_actions in responses.items(): - if code_actions: - for session in self._task_runner.sessions('codeActionProvider'): - if session.config.name == config_name: - for code_action in code_actions: - tasks.append(session.run_code_action_async(code_action, progress=False)) - break + for config_name, code_actions in responses: + session = self._task_runner.session_by_name(config_name, 'codeActionProvider') + if session: + tasks.extend([session.run_code_action_async(action, progress=False) for action in code_actions]) Promise.all(tasks).then(lambda _: self._on_code_actions_completed(document_version)) def _on_code_actions_completed(self, previous_document_version: int) -> None: @@ -293,65 +244,60 @@ def run( edit: sublime.Edit, event: Optional[dict] = None, only_kinds: Optional[List[CodeActionKind]] = None, - commands_by_config: Optional[CodeActionsByConfigName] = None + code_actions_by_config: Optional[List[CodeActionsByConfigName]] = None ) -> None: - self.commands = [] # type: List[Tuple[str, CodeActionOrCommand]] - self.commands_by_config = {} # type: CodeActionsByConfigName - if commands_by_config: - self.handle_responses_async(commands_by_config, run_first=True) - else: - view = self.view - region = first_selection_region(view) - if region is None: - return - listener = windows.listener_for_view(view) - if not listener: - return - session_buffer_diagnostics, covering = listener.diagnostics_intersecting_async(region) - actions_manager.request_for_region_async( - view, covering, session_buffer_diagnostics, self.handle_responses_async, only_kinds, manual=True) - - def handle_responses_async(self, responses: CodeActionsByConfigName, run_first: bool = False) -> None: - self.commands_by_config = responses - self.commands = self.combine_commands() - if len(self.commands) == 1 and run_first: - self.handle_select(0) - else: - self.show_code_actions() - - def combine_commands(self) -> 'List[Tuple[str, CodeActionOrCommand]]': - results = [] - for config, commands in self.commands_by_config.items(): - for command in commands: - results.append((config, command)) - return results - - def show_code_actions(self) -> None: - if len(self.commands) > 0: - window = self.view.window() - if window: - items, selected_index = format_code_actions_for_quick_panel([command[1] for command in self.commands]) - window.show_quick_panel( - items, - self.handle_select, - selected_index=selected_index, - placeholder="Code action") + if code_actions_by_config: + self._handle_code_actions(code_actions_by_config, run_first=True) + return + self._run_async(only_kinds) + + def _run_async(self, only_kinds: Optional[List[CodeActionKind]] = None) -> None: + view = self.view + region = first_selection_region(view) + if region is None: + return + listener = windows.listener_for_view(view) + if not listener: + return + session_buffer_diagnostics, covering = listener.diagnostics_intersecting_async(region) + actions_manager \ + .request_for_region_async(view, covering, session_buffer_diagnostics, only_kinds, manual=True) \ + .then(lambda actions: sublime.set_timeout(lambda: self._handle_code_actions(actions))) + + def _handle_code_actions(self, response: List[CodeActionsByConfigName], run_first: bool = False) -> None: + # Flatten response to a list of (config_name, code_action) tuples. + actions = [] # type: List[Tuple[ConfigName, CodeActionOrCommand]] + for config_name, session_actions in response: + actions.extend([(config_name, action) for action in session_actions]) + if len(actions) == 1 and run_first: + self._handle_select(0, actions) else: - self.view.show_popup('No actions available', sublime.HIDE_ON_MOUSE_MOVE_AWAY) + self._show_code_actions(actions) - def handle_select(self, index: int) -> None: - if index > -1: + def _show_code_actions(self, actions: List[Tuple[ConfigName, CodeActionOrCommand]]) -> None: + window = self.view.window() + if not window: + return + items, selected_index = format_code_actions_for_quick_panel(actions) + window.show_quick_panel( + items, + lambda i: self._handle_select(i, actions), + selected_index=selected_index, + placeholder="Code action") + + def _handle_select(self, index: int, actions: List[Tuple[ConfigName, CodeActionOrCommand]]) -> None: + if index == -1: + return - def run_async() -> None: - selected = self.commands[index] - session = self.session_by_name(selected[0]) - if session: - name = session.config.name - session.run_code_action_async(selected[1], progress=True).then( - lambda resp: self.handle_response_async(name, resp)) + def run_async() -> None: + config_name, action = actions[index] + session = self.session_by_name(config_name) + if session: + session.run_code_action_async(action, progress=True) \ + .then(lambda response: self._handle_response_async(config_name, response)) - sublime.set_timeout_async(run_async) + sublime.set_timeout_async(run_async) - def handle_response_async(self, session_name: str, response: Any) -> None: + def _handle_response_async(self, session_name: str, response: Any) -> None: if isinstance(response, Error): sublime.error_message("{}: {}".format(session_name, str(response))) diff --git a/plugin/core/typing.py b/plugin/core/typing.py index fd3cd8398..98c0cfae3 100644 --- a/plugin/core/typing.py +++ b/plugin/core/typing.py @@ -25,6 +25,7 @@ from typing import Tuple from typing import Type from typing import TypedDict + from typing import TypeGuard from typing import TypeVar from typing import Union @@ -57,6 +58,9 @@ class TypedDict(Type, dict): # type: ignore def __init__(*args, **kwargs) -> None: # type: ignore pass + class TypeGuard(Type): # type: ignore + pass + class Enum(Type): # type: ignore pass diff --git a/plugin/core/views.py b/plugin/core/views.py index 53e0cb26e..ebf87ee4d 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -1049,15 +1049,15 @@ def format_completion( def format_code_actions_for_quick_panel( - code_actions: List[Union[CodeAction, Command]] + session_actions: Iterable[Tuple[str, Union[CodeAction, Command]]] ) -> Tuple[List[sublime.QuickPanelItem], int]: items = [] # type: List[sublime.QuickPanelItem] selected_index = -1 - for idx, code_action in enumerate(code_actions): + for idx, (config_name, code_action) in enumerate(session_actions): lsp_kind = code_action.get("kind", "") first_kind_component = cast(CodeActionKind, str(lsp_kind).split(".")[0]) kind = CODE_ACTION_KINDS.get(first_kind_component, sublime.KIND_AMBIGUOUS) - items.append(sublime.QuickPanelItem(code_action["title"], kind=kind)) + items.append(sublime.QuickPanelItem(code_action["title"], annotation=config_name, kind=kind)) if code_action.get('isPreferred', False): selected_index = idx return items, selected_index diff --git a/plugin/documents.py b/plugin/documents.py index 3a5219107..0c31ef7e9 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -176,7 +176,7 @@ def _setup(self) -> None: self._stored_region = sublime.Region(-1, -1) self._sighelp = None # type: Optional[SigHelp] self._lightbulb_line = None # type: Optional[int] - self._actions_by_config = {} # type: Dict[str, List[CodeActionOrCommand]] + self._actions_by_config = [] # type: List[CodeActionsByConfigName] self._registered = False def _cleanup(self) -> None: @@ -284,7 +284,7 @@ def diagnostics_touching_point_async( def on_diagnostics_updated_async(self) -> None: self._clear_code_actions_annotation() if userprefs().show_code_actions: - self._do_code_actions() + self._do_code_actions_async() self._update_diagnostic_in_status_bar_async() window = self.view.window() is_active_view = window and window.active_view() == self.view @@ -369,7 +369,7 @@ def on_selection_modified_async(self) -> None: after_ms=self.highlights_debounce_time) self._clear_code_actions_annotation() if userprefs().show_code_actions: - self._when_selection_remains_stable_async(self._do_code_actions, current_region, + self._when_selection_remains_stable_async(self._do_code_actions_async, current_region, after_ms=self.code_actions_debounce_time) self._update_diagnostic_in_status_bar_async() self._resolve_visible_code_lenses_async() @@ -590,13 +590,17 @@ def _on_sighelp_navigate(self, href: str) -> None: # --- textDocument/codeAction -------------------------------------------------------------------------------------- - def _do_code_actions(self) -> None: + def _do_code_actions_async(self) -> None: diagnostics_by_config, covering = self.diagnostics_intersecting_async(self._stored_region) - actions_manager.request_for_region_async( - self.view, covering, diagnostics_by_config, self._on_code_actions, manual=False) - - def _on_code_actions(self, responses: CodeActionsByConfigName) -> None: - action_count = sum(map(len, responses.values())) + actions_manager \ + .request_for_region_async(self.view, covering, diagnostics_by_config, manual=False) \ + .then(self._on_code_actions) + + def _on_code_actions(self, responses: List[CodeActionsByConfigName]) -> None: + # flatten list + action_lists = [actions for _, actions in responses if len(actions)] + all_actions = [action for actions in action_lists for action in actions] + action_count = len(all_actions) if action_count == 0: return regions = [sublime.Region(self._stored_region.b, self._stored_region.a)] @@ -614,7 +618,7 @@ def _on_code_actions(self, responses: CodeActionsByConfigName) -> None: if action_count > 1: title = '{} code actions'.format(action_count) else: - title = next(itertools.chain.from_iterable(responses.values()))['title'] + title = all_actions[0]['title'] title = "
".join(textwrap.wrap(title, width=30)) code_actions_link = make_command_link('lsp_code_actions', title, {"commands_by_config": responses}) annotations = ["
{}
".format(code_actions_link)] @@ -628,26 +632,30 @@ def _clear_code_actions_annotation(self) -> None: def _on_navigate(self, href: str, point: int) -> None: if href.startswith('code-actions:'): _, config_name = href.split(":") - actions = self._actions_by_config[config_name] + actions = next(actions for name, actions in self._actions_by_config if name == config_name) if len(actions) > 1: window = self.view.window() if window: - items, selected_index = format_code_actions_for_quick_panel(actions) + items, selected_index = format_code_actions_for_quick_panel( + map(lambda action: (config_name, action), actions)) window.show_quick_panel( items, - lambda i: self.handle_code_action_select(config_name, i), + lambda i: self.handle_code_action_select(config_name, actions, i), selected_index=selected_index, placeholder="Code actions") else: - self.handle_code_action_select(config_name, 0) - - def handle_code_action_select(self, config_name: str, index: int) -> None: - if index > -1: - def run_async() -> None: - session = self.session_by_name(config_name) - if session: - session.run_code_action_async(self._actions_by_config[config_name][index], progress=True) - sublime.set_timeout_async(run_async) + self.handle_code_action_select(config_name, actions, 0) + + def handle_code_action_select(self, config_name: str, actions: List[CodeActionOrCommand], index: int) -> None: + if index == -1: + return + + def run_async() -> None: + session = self.session_by_name(config_name) + if session: + session.run_code_action_async(actions[index], progress=True) + + sublime.set_timeout_async(run_async) # --- textDocument/codeLens ---------------------------------------------------------------------------------------- diff --git a/plugin/hover.py b/plugin/hover.py index 04fbb60cd..36cbfa3d7 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -1,5 +1,6 @@ from .code_actions import actions_manager from .code_actions import CodeActionOrCommand +from .code_actions import CodeActionsByConfigName from .core.open import open_file_uri from .core.open import open_in_browser from .core.promise import Promise @@ -79,9 +80,9 @@ def link(self, point: int, view: sublime.View) -> str: ] -def code_actions_content(actions_by_config: Dict[str, List[CodeActionOrCommand]]) -> str: +def code_actions_content(actions_by_config: List[CodeActionsByConfigName]) -> str: formatted = [] - for config_name, actions in actions_by_config.items(): + for config_name, actions in actions_by_config: action_count = len(actions) if action_count > 0: href = "{}:{}".format('code-actions', config_name) @@ -122,7 +123,7 @@ def run( self._base_dir = wm.get_project_path(self.view.file_name() or "") self._hover_responses = [] # type: List[Tuple[Hover, Optional[MarkdownLangMap]]] self._document_link = ('', False, None) # type: Tuple[str, bool, Optional[sublime.Region]] - self._actions_by_config = {} # type: Dict[str, List[CodeActionOrCommand]] + self._actions_by_config = [] # type: List[CodeActionsByConfigName] self._diagnostics_by_config = [] # type: Sequence[Tuple[SessionBufferProtocol, Sequence[Diagnostic]]] # TODO: For code actions it makes more sense to use the whole selection under mouse (if available) # rather than just the hover point. @@ -140,9 +141,9 @@ def run_async() -> None: if self._diagnostics_by_config: self.show_hover(listener, hover_point, only_diagnostics) if not only_diagnostics and userprefs().show_code_actions_in_hover: - actions_manager.request_for_region_async( - self.view, covering, self._diagnostics_by_config, - functools.partial(self.handle_code_actions, listener, hover_point), manual=False) + actions_manager \ + .request_for_region_async(self.view, covering, self._diagnostics_by_config, manual=False) \ + .then(lambda results: self._handle_code_actions(listener, hover_point, results)) sublime.set_timeout_async(run_async) @@ -230,11 +231,11 @@ def _on_all_document_links_resolved( self._document_link = ('
'.join(contents) if contents else '', link_has_standard_tooltip, link_range) self.show_hover(listener, point, only_diagnostics=False) - def handle_code_actions( + def _handle_code_actions( self, listener: AbstractViewListener, point: int, - responses: Dict[str, List[CodeActionOrCommand]] + responses: List[CodeActionsByConfigName] ) -> None: self._actions_by_config = responses self.show_hover(listener, point, only_diagnostics=False) @@ -329,20 +330,21 @@ def _on_navigate(self, href: str, point: int) -> None: if window: open_file_uri(window, href) elif href.startswith('code-actions:'): - _, config_name = href.split(":") - actions = self._actions_by_config[config_name] self.view.run_command("lsp_selection_set", {"regions": [(point, point)]}) + _, config_name = href.split(":") + actions = next(actions for name, actions in self._actions_by_config if name == config_name) if len(actions) > 1: window = self.view.window() if window: - items, selected_index = format_code_actions_for_quick_panel(actions) + items, selected_index = format_code_actions_for_quick_panel( + map(lambda action: (config_name, action), actions)) window.show_quick_panel( items, - lambda i: self.handle_code_action_select(config_name, i), + lambda i: self.handle_code_action_select(config_name, actions, i), selected_index=selected_index, placeholder="Code actions") else: - self.handle_code_action_select(config_name, 0) + self.handle_code_action_select(config_name, actions, 0) elif is_location_href(href): session_name, uri, row, col_utf16 = unpack_href_location(href) session = self.session_by_name(session_name) @@ -353,12 +355,13 @@ def _on_navigate(self, href: str, point: int) -> None: else: open_in_browser(href) - def handle_code_action_select(self, config_name: str, index: int) -> None: - if index > -1: + def handle_code_action_select(self, config_name: str, actions: List[CodeActionOrCommand], index: int) -> None: + if index == -1: + return - def run_async() -> None: - session = self.session_by_name(config_name) - if session: - session.run_code_action_async(self._actions_by_config[config_name][index], progress=True) + def run_async() -> None: + session = self.session_by_name(config_name) + if session: + session.run_code_action_async(actions[index], progress=True) - sublime.set_timeout_async(run_async) + sublime.set_timeout_async(run_async) From c7c633df01caadf6bcb1e67349eb0a42b7ca1584 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 5 Oct 2022 22:17:58 +0200 Subject: [PATCH 10/55] expose function for tests --- plugin/code_actions.py | 6 +++++- tests/test_code_actions.py | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 44170b866..1ac6b453c 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -154,9 +154,13 @@ def get_matching_on_save_kinds(session: Session, user_actions: Dict[str, bool]) (for example user's a.b matching session's a.b.c), then the more specific (a.b.c) must be returned as servers must receive only kinds that they advertise support for. """ - session_kinds = session.get_capability('codeActionProvider.codeActionKinds') # type: Optional[List[CodeActionKind]] # noqa: E501 + session_kinds = session.get_capability('codeActionProvider.codeActionKinds') # type: Optional[List[CodeActionKind]] if not session_kinds: return [] + return get_matching_kinds(user_actions, session_kinds) + + +def get_matching_kinds(user_actions: Dict[str, bool], session_kinds: List[CodeActionKind]) -> List[CodeActionKind]: matching_kinds = [] for session_kind in session_kinds: enabled = False diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index 28af0f5fa..e97171f4b 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -1,5 +1,5 @@ from copy import deepcopy -from LSP.plugin.code_actions import get_matching_kinds +from LSP.plugin.code_actions import get_matching_on_save_kinds from LSP.plugin.core.protocol import Point, Range from LSP.plugin.core.typing import Any, Dict, Generator, List, Tuple, Optional from LSP.plugin.core.url import filename_to_uri @@ -232,22 +232,22 @@ def _setup_document_with_missing_semicolon(self) -> Generator: class CodeActionMatchingTestCase(unittest.TestCase): def test_does_not_match(self) -> None: - actual = get_matching_kinds({'a.x': True}, ['a.b']) + actual = get_matching_on_save_kinds({'a.x': True}, ['a.b']) expected = [] # type: List[str] self.assertEquals(actual, expected) def test_matches_exact_action(self) -> None: - actual = get_matching_kinds({'a.b': True}, ['a.b']) + actual = get_matching_on_save_kinds({'a.b': True}, ['a.b']) expected = ['a.b'] self.assertEquals(actual, expected) def test_matches_more_specific_action(self) -> None: - actual = get_matching_kinds({'a.b': True}, ['a.b.c']) + actual = get_matching_on_save_kinds({'a.b': True}, ['a.b.c']) expected = ['a.b.c'] self.assertEquals(actual, expected) def test_does_not_match_disabled_action(self) -> None: - actual = get_matching_kinds({'a.b': True, 'a.b.c': False}, ['a.b.c']) + actual = get_matching_on_save_kinds({'a.b': True, 'a.b.c': False}, ['a.b.c']) expected = [] # type: List[str] self.assertEquals(actual, expected) From 3779d0239f917d4631b0c4e98efc55aaf740652f Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 5 Oct 2022 22:23:33 +0200 Subject: [PATCH 11/55] better split --- plugin/code_actions.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 1ac6b453c..8754a376d 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -92,7 +92,8 @@ def request_on_save_async( session_buffer_diagnostics, _ = listener.diagnostics_intersecting_region_async(region) def request_factory(session: Session) -> Optional[Request]: - matching_kinds = get_matching_on_save_kinds(session, on_save_actions) + session_kinds = get_session_kinds(session) + matching_kinds = get_matching_on_save_kinds(on_save_actions, session_kinds) if not matching_kinds: return None diagnostics = [] # type: List[Diagnostic] @@ -107,7 +108,8 @@ def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> Lis # Filter actions returned from the session so that only matching kinds are collected. # Since older servers don't support the "context.only" property, those will return all # actions that need to be then manually filtered. - matching_kinds = get_matching_on_save_kinds(session, on_save_actions) + session_kinds = get_session_kinds(session) + matching_kinds = get_matching_on_save_kinds(on_save_actions, session_kinds) return [a for a in actions if a.get('kind') in matching_kinds] return self._collect_code_actions_async(listener, request_factory, response_filter) @@ -143,7 +145,14 @@ def on_response( actions_manager = CodeActionsManager() -def get_matching_on_save_kinds(session: Session, user_actions: Dict[str, bool]) -> List[CodeActionKind]: +def get_session_kinds(session: Session) -> List[CodeActionKind]: + session_kinds = session.get_capability('codeActionProvider.codeActionKinds') # type: Optional[List[CodeActionKind]] + return session_kinds or [] + + +def get_matching_on_save_kinds( + user_actions: Dict[str, bool], session_kinds: List[CodeActionKind] +) -> List[CodeActionKind]: """ Filters user-enabled or disabled actions so that only ones matching the session kinds are returned. Returned kinds are those that are enabled and are not overridden by more @@ -154,13 +163,6 @@ def get_matching_on_save_kinds(session: Session, user_actions: Dict[str, bool]) (for example user's a.b matching session's a.b.c), then the more specific (a.b.c) must be returned as servers must receive only kinds that they advertise support for. """ - session_kinds = session.get_capability('codeActionProvider.codeActionKinds') # type: Optional[List[CodeActionKind]] - if not session_kinds: - return [] - return get_matching_kinds(user_actions, session_kinds) - - -def get_matching_kinds(user_actions: Dict[str, bool], session_kinds: List[CodeActionKind]) -> List[CodeActionKind]: matching_kinds = [] for session_kind in session_kinds: enabled = False From 5b2ea06d3868fdf71223c77b017d905953ab9c8c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 5 Oct 2022 22:34:30 +0200 Subject: [PATCH 12/55] fix bug and steal some changes from jwortmann --- docs/src/features.md | 2 +- plugin/code_actions.py | 3 ++- plugin/core/sessions.py | 1 + tests/test_code_actions.py | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/src/features.md b/docs/src/features.md index 998ad3409..c70630831 100644 --- a/docs/src/features.md +++ b/docs/src/features.md @@ -136,7 +136,7 @@ Code Actions are an umbrella term for "Quick Fixes" and "Refactorings". They are Formatting is different from Code Actions, because Formatting is supposed to _not_ mutate the abstract syntax tree of the file, only move around white space. Any Code Action will mutate the abstract syntax tree. -This package presents Code Actions as a bluish clickable annotation positioned to the right of the viewport. Alternatively, they can be presented as a light bulb in the Gutter Area. +This package presents "Quick Fix" Code Actions as a bluish clickable annotation positioned to the right of the viewport. Alternatively, they can be presented as a light bulb in the Gutter Area. Sublime Text has no concept of Code Actions. diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 8754a376d..ade48ed65 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -74,7 +74,8 @@ def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> Lis if only_kinds: return [a for a in actions if not is_command(a) and is_kinds_include_kind(only_kinds, a.get('kind'))] return [ - a for a in actions if is_command(a) or is_kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) + a for a in actions + if is_command(a) or not a.get('kind') or is_kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) ] task = self._collect_code_actions_async(listener, request_factory, response_filter) diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 4efa757c0..92355d0eb 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -336,6 +336,7 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor CodeActionKind.RefactorExtract, CodeActionKind.RefactorInline, CodeActionKind.RefactorRewrite, + CodeActionKind.SourceFixAll, CodeActionKind.SourceOrganizeImports, ] } diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index e97171f4b..2ebae6e89 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -45,7 +45,7 @@ def create_command(command_name: str, command_args: Optional[List[Any]] = None) def create_test_code_action(view: sublime.View, version: int, edits: List[Tuple[str, Range]], - kind: str = None) -> Dict[str, Any]: + kind: Optional[str] = None) -> Dict[str, Any]: action = { "title": "Fix errors", "edit": create_code_action_edit(view, version, edits) @@ -56,7 +56,7 @@ def create_test_code_action(view: sublime.View, version: int, edits: List[Tuple[ def create_test_code_action2(command_name: str, command_args: Optional[List[Any]] = None, - kind: str = None) -> Dict[str, Any]: + kind: Optional[str] = None) -> Dict[str, Any]: action = { "title": "Fix errors", "command": create_command(command_name, command_args) From 2423dc0f42c0bcc1309768e6952d4c83e2a79503 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 5 Oct 2022 23:02:32 +0200 Subject: [PATCH 13/55] better types --- plugin/code_actions.py | 4 ++-- plugin/core/protocol.py | 4 ++-- plugin/core/typing.py | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index ade48ed65..f76ba1213 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -167,7 +167,7 @@ def get_matching_on_save_kinds( matching_kinds = [] for session_kind in session_kinds: enabled = False - action_parts = cast(str, session_kind).split('.') + action_parts = session_kind.split('.') for i in range(len(action_parts)): current_part = '.'.join(action_parts[0:i + 1]) user_value = user_actions.get(current_part, None) @@ -185,7 +185,7 @@ def is_kinds_include_kind(kinds: List[CodeActionKind], kind: Optional[CodeAction """ if not kind: return False - kind_parts = cast(str, kind).split('.') + kind_parts = kind.split('.') return any(part for part in kind_parts if part in kinds) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index 49daa701e..8220070c6 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -1,4 +1,4 @@ -from .typing import Enum, IntEnum, IntFlag +from .typing import Enum, IntEnum, IntFlag, StrEnum from .typing import Any, Dict, Iterable, List, Literal, Mapping, NotRequired, Optional, TypedDict, Union import sublime @@ -327,7 +327,7 @@ class DocumentHighlightKind(IntEnum): """ Write-access of a symbol, like writing to a variable. """ -class CodeActionKind(Enum): +class CodeActionKind(StrEnum): """ A set of predefined code action kinds """ Empty = '' """ Empty kind. """ diff --git a/plugin/core/typing.py b/plugin/core/typing.py index 98c0cfae3..bcb251a85 100644 --- a/plugin/core/typing.py +++ b/plugin/core/typing.py @@ -2,7 +2,7 @@ if sys.version_info >= (3, 11, 0): - from enum import Enum, IntEnum, IntFlag + from enum import Enum, IntEnum, IntFlag, StrEnum from typing import Any from typing import Callable from typing import cast @@ -70,6 +70,9 @@ class IntEnum(Type): # type: ignore class IntFlag(Type): # type: ignore pass + class StrEnum(Type): # type: ignore + pass + class Any(Type): # type: ignore pass From 7ef333469fd893e4120c6da1b78383bada4ec26c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Wed, 5 Oct 2022 23:19:29 +0200 Subject: [PATCH 14/55] careful with stupid lambdas --- plugin/code_actions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index f76ba1213..511f75ca7 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -17,6 +17,7 @@ from .core.views import format_code_actions_for_quick_panel from .core.views import text_document_code_action_params from .save_command import LspSaveCommand, SaveTask +from functools import partial import sublime ConfigName = str @@ -137,8 +138,9 @@ def on_response( for session in listener.sessions_async('codeActionProvider'): request = request_factory(session) if request: + response_handler = partial(on_response, session) task = session.send_request_task(request) # type: Promise[Optional[List[CodeActionOrCommand]]] - tasks.append(task.then(lambda response: on_response(session, response))) + tasks.append(task.then(response_handler)) # Return only results for non-empty lists. return Promise.all(tasks).then(lambda sessions: list(filter(lambda session: len(session[1]), sessions))) From 4450573b39977a1545627df0d4adf625f3197d38 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 6 Oct 2022 23:38:05 +0200 Subject: [PATCH 15/55] address comments --- plugin/code_actions.py | 33 +++++++++++++++++++++++---------- tests/test_code_actions.py | 9 ++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 511f75ca7..d9484dff1 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -18,6 +18,7 @@ from .core.views import text_document_code_action_params from .save_command import LspSaveCommand, SaveTask from functools import partial +import re import sublime ConfigName = str @@ -73,10 +74,10 @@ def request_factory(session: Session) -> Optional[Request]: def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> List[CodeActionOrCommand]: # Filter out non "quickfix" code actions unless "only_kinds" is provided. if only_kinds: - return [a for a in actions if not is_command(a) and is_kinds_include_kind(only_kinds, a.get('kind'))] + return [a for a in actions if not is_command(a) and kinds_include_kind(only_kinds, a.get('kind'))] return [ a for a in actions - if is_command(a) or not a.get('kind') or is_kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) + if is_command(a) or not a.get('kind') or kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) ] task = self._collect_code_actions_async(listener, request_factory, response_filter) @@ -180,15 +181,22 @@ def get_matching_on_save_kinds( return matching_kinds -def is_kinds_include_kind(kinds: List[CodeActionKind], kind: Optional[CodeActionKind]) -> bool: +def kinds_include_kind(kinds: List[CodeActionKind], kind: Optional[CodeActionKind]) -> bool: """ - The "kinds" list includes "kind" if any of the "kinds" matches "kind" exactly or the "kind" prefix matches one - of the "kinds". + The "kinds" include "kind" if "kind" matches one of the "kinds" exactly or one of the "kinds" is a prefix + of the whole "kind" (where prefix must be followed by a dot). """ if not kind: return False - kind_parts = kind.split('.') - return any(part for part in kind_parts if part in kinds) + for kinds_item in kinds: + if kinds_item == kind: + return True + kind_dots = re.finditer(r'\.', kind) + for dot in kind_dots: + kind_prefix = kind[0:dot.start()] + if kind_prefix == kinds_item: + return True + return False class CodeActionOnSaveTask(SaveTask): @@ -278,10 +286,15 @@ def _handle_code_actions(self, response: List[CodeActionsByConfigName], run_firs actions = [] # type: List[Tuple[ConfigName, CodeActionOrCommand]] for config_name, session_actions in response: actions.extend([(config_name, action) for action in session_actions]) - if len(actions) == 1 and run_first: - self._handle_select(0, actions) + if actions: + if len(actions) == 1 and run_first: + self._handle_select(0, actions) + else: + self._show_code_actions(actions) else: - self._show_code_actions(actions) + window = self.view.window() + if window: + window.status_message("No code actions available") def _show_code_actions(self, actions: List[Tuple[ConfigName, CodeActionOrCommand]]) -> None: window = self.view.window() diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index 2ebae6e89..c2292bda1 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -1,5 +1,5 @@ from copy import deepcopy -from LSP.plugin.code_actions import get_matching_on_save_kinds +from LSP.plugin.code_actions import get_matching_on_save_kinds, kinds_include_kind from LSP.plugin.core.protocol import Point, Range from LSP.plugin.core.typing import Any, Dict, Generator, List, Tuple, Optional from LSP.plugin.core.url import filename_to_uri @@ -251,6 +251,13 @@ def test_does_not_match_disabled_action(self) -> None: expected = [] # type: List[str] self.assertEquals(actual, expected) + def test_kind_matching(self) -> None: + self.assertTrue(kinds_include_kind(['a'], 'a.b')) + self.assertFalse(kinds_include_kind(['a'], 'b.a')) + self.assertTrue(kinds_include_kind(['a.b'], 'a.b')) + self.assertFalse(kinds_include_kind(['a.b'], 'a')) + self.assertFalse(kinds_include_kind(['aa'], 'a')) + class CodeActionsListenerTestCase(TextDocumentTestCase): def setUp(self) -> Generator: From 8ee7136723d1d23f6c1151408a055ca6e61fb181 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 6 Oct 2022 23:48:19 +0200 Subject: [PATCH 16/55] extra tests --- tests/test_code_actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index c2292bda1..a0c49f9a6 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -254,9 +254,12 @@ def test_does_not_match_disabled_action(self) -> None: def test_kind_matching(self) -> None: self.assertTrue(kinds_include_kind(['a'], 'a.b')) self.assertFalse(kinds_include_kind(['a'], 'b.a')) + self.assertFalse(kinds_include_kind(['a.b'], 'b')) self.assertTrue(kinds_include_kind(['a.b'], 'a.b')) self.assertFalse(kinds_include_kind(['a.b'], 'a')) self.assertFalse(kinds_include_kind(['aa'], 'a')) + self.assertFalse(kinds_include_kind(['aa.b'], 'a')) + self.assertFalse(kinds_include_kind(['aa.b'], 'b')) class CodeActionsListenerTestCase(TextDocumentTestCase): From 8501670828450451188fccfcc6f9d8f0ac113319 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 7 Oct 2022 08:45:23 +0200 Subject: [PATCH 17/55] extra test --- tests/test_code_actions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_code_actions.py b/tests/test_code_actions.py index a0c49f9a6..cbd1f57c5 100644 --- a/tests/test_code_actions.py +++ b/tests/test_code_actions.py @@ -252,10 +252,13 @@ def test_does_not_match_disabled_action(self) -> None: self.assertEquals(actual, expected) def test_kind_matching(self) -> None: + # Positive self.assertTrue(kinds_include_kind(['a'], 'a.b')) + self.assertTrue(kinds_include_kind(['a.b'], 'a.b')) + self.assertTrue(kinds_include_kind(['a.b', 'b'], 'b.c')) + # Negative self.assertFalse(kinds_include_kind(['a'], 'b.a')) self.assertFalse(kinds_include_kind(['a.b'], 'b')) - self.assertTrue(kinds_include_kind(['a.b'], 'a.b')) self.assertFalse(kinds_include_kind(['a.b'], 'a')) self.assertFalse(kinds_include_kind(['aa'], 'a')) self.assertFalse(kinds_include_kind(['aa.b'], 'a')) From e38ae248ef4ef8ab496021ee77f2c317fa156964 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 7 Oct 2022 20:26:42 +0200 Subject: [PATCH 18/55] fix filtering and clicking on code actions annotation --- plugin/code_actions.py | 2 +- plugin/documents.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index d9484dff1..58d1d1236 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -27,7 +27,7 @@ def is_command(action: CodeActionOrCommand) -> TypeGuard[Command]: - return 'command' in action + return isinstance(action.get('command'), str) class CodeActionsManager: diff --git a/plugin/documents.py b/plugin/documents.py index 0c31ef7e9..020ad3935 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -620,7 +620,7 @@ def _on_code_actions(self, responses: List[CodeActionsByConfigName]) -> None: else: title = all_actions[0]['title'] title = "
".join(textwrap.wrap(title, width=30)) - code_actions_link = make_command_link('lsp_code_actions', title, {"commands_by_config": responses}) + code_actions_link = make_command_link('lsp_code_actions', title, {"code_actions_by_config": responses}) annotations = ["
{}
".format(code_actions_link)] annotation_color = self.view.style_for_scope("region.bluish markup.accent.codeaction.lsp")["foreground"] self.view.add_regions(SessionView.CODE_ACTIONS_KEY, regions, scope, icon, flags, annotations, annotation_color) From 092568b8b28e0b82541a6f9e3efef9307e0e3992 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 8 Oct 2022 19:19:19 +0200 Subject: [PATCH 19/55] simplify --- plugin/code_actions.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 58d1d1236..ef8eb0136 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -18,7 +18,6 @@ from .core.views import text_document_code_action_params from .save_command import LspSaveCommand, SaveTask from functools import partial -import re import sublime ConfigName = str @@ -191,11 +190,9 @@ def kinds_include_kind(kinds: List[CodeActionKind], kind: Optional[CodeActionKin for kinds_item in kinds: if kinds_item == kind: return True - kind_dots = re.finditer(r'\.', kind) - for dot in kind_dots: - kind_prefix = kind[0:dot.start()] - if kind_prefix == kinds_item: - return True + kinds_item_len = len(kinds_item) + if kind.startswith(kinds_item) and kind[kinds_item_len] == '.': + return True return False From a4766823a5b18a0c3fef7c72b72149272a655454 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 8 Oct 2022 21:20:31 +0200 Subject: [PATCH 20/55] address comments --- plugin/code_actions.py | 7 ++----- plugin/documents.py | 4 +++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index ef8eb0136..57c7863e4 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -74,10 +74,7 @@ def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> Lis # Filter out non "quickfix" code actions unless "only_kinds" is provided. if only_kinds: return [a for a in actions if not is_command(a) and kinds_include_kind(only_kinds, a.get('kind'))] - return [ - a for a in actions - if is_command(a) or not a.get('kind') or kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) - ] + return actions task = self._collect_code_actions_async(listener, request_factory, response_filter) if location_cache_key: @@ -191,7 +188,7 @@ def kinds_include_kind(kinds: List[CodeActionKind], kind: Optional[CodeActionKin if kinds_item == kind: return True kinds_item_len = len(kinds_item) - if kind.startswith(kinds_item) and kind[kinds_item_len] == '.': + if len(kind) > kinds_item_len and kind.startswith(kinds_item) and kind[kinds_item_len] == '.': return True return False diff --git a/plugin/documents.py b/plugin/documents.py index 020ad3935..ff5b755b5 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -6,6 +6,7 @@ from .core.panels import is_panel_open from .core.panels import PanelName from .core.promise import Promise +from .core.protocol import CodeActionKind from .core.protocol import CompletionItem from .core.protocol import CompletionItemKind from .core.protocol import CompletionList @@ -592,8 +593,9 @@ def _on_sighelp_navigate(self, href: str) -> None: def _do_code_actions_async(self) -> None: diagnostics_by_config, covering = self.diagnostics_intersecting_async(self._stored_region) + only_kinds = [CodeActionKind.QuickFix] actions_manager \ - .request_for_region_async(self.view, covering, diagnostics_by_config, manual=False) \ + .request_for_region_async(self.view, covering, diagnostics_by_config, only_kinds, manual=False) \ .then(self._on_code_actions) def _on_code_actions(self, responses: List[CodeActionsByConfigName]) -> None: From b79a8ce6fc47db267b0bd1acf3ce8dba6a7b89ac Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 8 Oct 2022 21:55:57 +0200 Subject: [PATCH 21/55] better fix --- plugin/code_actions.py | 8 +++++++- plugin/documents.py | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/plugin/code_actions.py b/plugin/code_actions.py index 57c7863e4..e1946fdcd 100644 --- a/plugin/code_actions.py +++ b/plugin/code_actions.py @@ -74,7 +74,13 @@ def response_filter(session: Session, actions: List[CodeActionOrCommand]) -> Lis # Filter out non "quickfix" code actions unless "only_kinds" is provided. if only_kinds: return [a for a in actions if not is_command(a) and kinds_include_kind(only_kinds, a.get('kind'))] - return actions + if manual: + return actions + # On implicit (selection change) request, only return commands and quick fix kinds. + return [ + a for a in actions + if is_command(a) or not a.get('kind') or kinds_include_kind([CodeActionKind.QuickFix], a.get('kind')) + ] task = self._collect_code_actions_async(listener, request_factory, response_filter) if location_cache_key: diff --git a/plugin/documents.py b/plugin/documents.py index ff5b755b5..48b0513bc 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -593,9 +593,8 @@ def _on_sighelp_navigate(self, href: str) -> None: def _do_code_actions_async(self) -> None: diagnostics_by_config, covering = self.diagnostics_intersecting_async(self._stored_region) - only_kinds = [CodeActionKind.QuickFix] actions_manager \ - .request_for_region_async(self.view, covering, diagnostics_by_config, only_kinds, manual=False) \ + .request_for_region_async(self.view, covering, diagnostics_by_config, manual=False) \ .then(self._on_code_actions) def _on_code_actions(self, responses: List[CodeActionsByConfigName]) -> None: From 235fc80bdbe826ea7d33885e5ba9914a3a040744 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 8 Oct 2022 21:57:42 +0200 Subject: [PATCH 22/55] import --- plugin/documents.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index 48b0513bc..020ad3935 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -6,7 +6,6 @@ from .core.panels import is_panel_open from .core.panels import PanelName from .core.promise import Promise -from .core.protocol import CodeActionKind from .core.protocol import CompletionItem from .core.protocol import CompletionItemKind from .core.protocol import CompletionList From db9b15fd1f0f7c7e80b385948f5f8407dcda4aed Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 8 Oct 2022 22:20:43 +0200 Subject: [PATCH 23/55] better quick fix presentation in hover popup --- plugin/hover.py | 18 ++++++++++-------- popups.css | 3 +++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/plugin/hover.py b/plugin/hover.py index 36cbfa3d7..97d56784d 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -84,14 +84,16 @@ def code_actions_content(actions_by_config: List[CodeActionsByConfigName]) -> st formatted = [] for config_name, actions in actions_by_config: action_count = len(actions) - if action_count > 0: - href = "{}:{}".format('code-actions', config_name) - if action_count > 1: - text = "choose code action ({} available)".format(action_count) - else: - text = actions[0].get('title', 'code action') - formatted.append('
[{}] Code action: {}
'.format( - config_name, make_link(href, text))) + if action_count == 0: + continue + if action_count > 1: + text = "choose ({} available)".format(action_count) + else: + text = actions[0].get('title', 'code action') + href = "{}:{}".format('code-actions', config_name) + link = make_link(href, text) + formatted.append( + '
Quick Fix: {} {}
'.format(link, config_name)) return "".join(formatted) diff --git a/popups.css b/popups.css index dd562ff79..f838a57b8 100644 --- a/popups.css +++ b/popups.css @@ -22,6 +22,9 @@ border-width: 0; border-radius: 0; } +.color-muted { + color: color(var(--foreground) alpha(0.50)); +} .diagnostics { margin-bottom: 0.5rem; font-family: var(--mdpopups-font-mono); From 3540306a663d4ba707331b076a9f0f40ff0ade08 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 8 Oct 2022 22:28:15 +0200 Subject: [PATCH 24/55] Add refactor to the context menu --- Context.sublime-menu | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Context.sublime-menu b/Context.sublime-menu index 08cb70ee5..5defb140f 100644 --- a/Context.sublime-menu +++ b/Context.sublime-menu @@ -31,6 +31,11 @@ "command": "lsp_code_actions", "caption": "Code Action…" }, + { + "command": "lsp_code_actions", + "args": {"only_kinds": ["refactor"]}, + "caption": "Refactor…" + }, { "command": "lsp_code_actions", "args": {"only_kinds": ["source"]}, From 6cfa3602676f075045b4bc3a1055be18ff5caa7b Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 11 Nov 2022 20:05:07 +0100 Subject: [PATCH 25/55] adjust --- plugin/session_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/session_view.py b/plugin/session_view.py index 45db3d22c..f5ee6a374 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -88,7 +88,7 @@ def on_before_remove(self) -> None: self.session_buffer.remove_session_view(self) listener = self.listener() if listener: - listener.on_diagnostics_updated_async() + listener.on_diagnostics_updated_async(False) @property def session(self) -> Session: From f5a8f3eff4c007dfd42e5256aac55f3c614dc09a Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 26 Feb 2023 18:41:38 +0100 Subject: [PATCH 26/55] WIP --- plugin/documents.py | 308 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 306 insertions(+), 2 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index 011e20d42..a51844513 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -28,7 +28,7 @@ from .core.types import DebouncerNonThreadSafe from .core.types import FEATURES_TIMEOUT from .core.types import SettingsRegistration -from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple +from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Literal, NotRequired, StrEnum, Tuple, TypedDict, Union # noqa: E501 from .core.typing import cast from .core.url import parse_uri from .core.url import view_to_uri @@ -50,6 +50,7 @@ from .session_buffer import SessionBuffer from .session_view import SessionView from functools import partial +from itertools import chain from weakref import WeakSet from weakref import WeakValueDictionary import itertools @@ -60,6 +61,301 @@ import webbrowser +class StackKind(StrEnum): + SPACE = 'space' + DIAGNOSTIC = 'diagnostic' + OVERLAP = 'overlap' + BLANK = 'blank' + +StackItemDiagnostic = TypedDict('StackItemDiagnostic', { + 'kind': Literal[StackKind.DIAGNOSTIC], + 'data': Diagnostic, +}) + +StackItemSpace = TypedDict('StackItemSpace', { + 'kind': Literal[StackKind.SPACE], + 'data': str, +}) + +StackItemOverlap = TypedDict('StackItemOverlap', { + 'kind': Literal[StackKind.OVERLAP], + 'data': None, +}) + +StackItem = Union[ + StackItemDiagnostic, + StackItemSpace, + StackItemOverlap, + # Tuple[Literal[StackKind.SPACE], str], + # Tuple[Literal[StackKind.OVERLAP], None], + # Tuple[Literal[StackKind.DIAGNOSTIC], Diagnostic], + # Tuple[Literal[StackKind.BLANK], Diagnostic], +] + +LineStack = TypedDict('LineStack', { + 'region': Optional[sublime.Region], + 'stack': List[StackItem] +}) + +StackMap = Dict[int, LineStack] + +Line = TypedDict('Line', { + 'class': str, + 'content': NotRequired[str], +}) + +DiagnosticBlock = TypedDict('DiagnosticBlock', { + 'content': List[List[Line]], + 'line': int, + 'region': sublime.Region, +}) + + +class DiagnosticLines: + CSS = ''' + .inline-block { + display: inline-block; + } + .d_error { + color: color(var(--redish) alpha(0.85)) + } + .d_error_bg { + background-color: color(var(--redish) alpha(0.1)) + } + .d_warning { + color: color(var(--yellowish) alpha(0.85)) + } + .d_warning_bg { + background-color: color(var(--yellowish) alpha(0.1)) + } + .d_info { + color: color(var(--bluish) alpha(0.85)) + } + .d_info_bg { + background-color: color(var(--bluish) alpha(0.1)) + } + .d_hint { + color: color(var(--greenish) alpha(0.85)) + } + .d_hint_bg { + background-color: color(var(--greenish) alpha(0.1)) + } + '''.strip() + HIGHLIGHTS = { + 1: 'error', + 2: 'warning', + 3: 'info', + 4: 'hint' + } + + COLORS = { + 'error': 'var(--redish)', + 'warning': 'var(--yellowish)', + 'info': 'var(--blueish)', + 'hint': 'var(--greenish)', + '': 'transparent', + } + + SYMBOLS = { + 'BOTTOM_LEFT': '└', + 'UPSIDE_DOWN_T': '┴', + 'MIDDLE_CROSS': '┼', + 'MIDDLE_RIGHT_CENTER': '├', + 'VERTICAL': '│', + 'HORIZONTAL': '─' + } + + def __init__(self, view: sublime.View, highlight_line_background: bool = False) -> None: + self._view = view + self._diagnostics = [] # type: List[Diagnostic] + self._highlight_line_background = highlight_line_background + self._phantoms = sublime.PhantomSet(view, 'lsp_lines') + + def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: + self._diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) + self._line_stacks = self._generate_line_stacks(self._diagnostics) + self._blocks = self._generate_diagnostic_blocks(self._line_stacks) + phantoms = [] # Type: List[sublime.Phantom] + for block in self._blocks: + content = self._generate_region_html(block) + phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) + self._phantoms.update(phantoms) + + def clear(self) -> None: + self._phantoms = sublime.PhantomSet(self._view, 'lsp-lines') + + def _generate_region_html(self, block: DiagnosticBlock) -> str: + lines = [ + ''.format(self.CSS) + ] + for line in block["content"]: + row_items = [] + for item in line: + item_class = 'd_{0}'.format(item['class']) + css_classes = ['inline-block', item_class] + if self._highlight_line_background: + css_classes.append('{0}_bg'.format(item_class)) + row_items.append('
{1}
'.format( + ' '.join(css_classes), item.get('content', '').replace(" ", " "))) + lines.append('
{0}
'.format(''.join(row_items))) + return '\n'.join(lines) + + def _preprocess_diagnostic(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]): + return [diagnostic[0] for diagnostic in diagnostics] + + def _sort_diagnostics(self, diagnostics: List[Diagnostic]): + return sorted(diagnostics, key=lambda x: (x['range']['start']['line'], x['range']['start']['character'])) + + def _generate_line_stacks(self, diagnostics: List[Diagnostic]) -> StackMap: + # Initialize an empty dictionary to store line stacks + line_stacks = {} # type: StackMap + # Set the initial values for the previous line number and previous column + prev_lnum = -1 + prev_col = 0 + # Iterate over the diagnostics + for diagnostic in diagnostics: + if not diagnostic['message'].strip(): + # Skip diagnostics with empty message. + continue + range_start = diagnostic['range']['start'] + current_line = range_start['line'] + current_col = range_start['character'] + # Create an empty list for the current line if it doesn't already exist in the dictionary + line_stacks.setdefault(current_line, {'region': None, 'stack': []}) + if line_stacks[current_line]['region'] is None: + region = range_to_region(diagnostic['range'], self._view) + region.b = region.a + line_stacks[current_line]['region'] = region + # Get the current stack for the current line + stack = line_stacks[current_line]['stack'] + # Check if the diagnostic is on a new line + if current_line != prev_lnum: + # If so, add an empty space to the stack + stack.append({'kind': StackKind.SPACE, 'data': ''}) + elif current_col != prev_col: + # If not on a new line but on a new column, add spacing to the stack + # Calculate the spacing by subtracting the previous column from the current column, minus 1 (to account + # for 0-based index) + spacing = (current_col - prev_col) - 1 + stack.append({'kind': StackKind.SPACE, 'data': ' ' * spacing}) + else: + # If the diagnostic is on the same exact spot as the previous one, add an overlap to the stack + stack.append({'kind': StackKind.OVERLAP, 'data': None}) + # If not blank, add the diagnostic to the stack + stack.append({'kind': StackKind.DIAGNOSTIC, 'data': diagnostic}) + # Update the previous line number and column for the next iteration + prev_lnum = current_line + prev_col = current_col + return line_stacks + + def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock]: + """ + Generates the diagnostic blocks from the given stacks + """ + blocks = [] + for key, line in stacks.items(): + block = {'line': key, 'content': [], 'region': line['region']} + for i, (diagnostic_type, data) in enumerate(reversed(line['stack'])): + if diagnostic_type != StackKind.DIAGNOSTIC: + continue + diagnostic = data + index = len(line['stack']) - 1 - i + left, overlap, multi = self._generate_left_side(line['stack'], index, diagnostic) + center = self._generate_center(overlap, multi, diagnostic) + for msg_line in diagnostic['message'].split('\n'): + block['content'].append(list(chain(left, center, [{ + 'content': msg_line, + 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)] + }]))) + if overlap: + center = [ + { + 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], + 'content': self.SYMBOLS['VERTICAL'] + }, + {'class': '', 'content': ' '}, + ] + else: + center = [{'class': '', 'content': ' '}] + blocks.append(block) + return blocks + + def _generate_left_side( + self, line: List[StackItem], index: int, diagnostic: Diagnostic + ) -> Tuple[List[Line], bool, int]: + """ + Generates the left side of the diagnostic block for a given line + """ + left = [] + overlap = False + multi = 0 + current_index = 0 + while current_index < index: + diagnostic_type = line[current_index][0] + data = line[current_index][1] + if diagnostic_type == StackKind.SPACE: + if multi == 0: + left.append({'class': '', 'content': data}) + else: + left.append({ + 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], + 'content': self.SYMBOLS['HORIZONTAL'] * len(data) + }) + elif diagnostic_type == StackKind.DIAGNOSTIC: + if current_index+1 != len(line) and line[current_index+1][0] != StackKind.OVERLAP: + left.append( + { + "class": self.HIGHLIGHTS[self._get_severity(data)], + "content": self.SYMBOLS['VERTICAL'], + } + ) + overlap = False + elif diagnostic_type == StackKind.BLANK: + if multi == 0: + left.append( + { + "class": self.HIGHLIGHTS[self._get_severity(data)], + "content": self.SYMBOLS['BOTTOM_LEFT'], + } + ) + else: + left.append( + { + "class": self.HIGHLIGHTS[self._get_severity(data)], + "content": self.SYMBOLS['UPSIDE_DOWN_T'], + } + ) + multi += 1 + elif diagnostic_type == StackKind.OVERLAP: + overlap = True + current_index += 1 + return left, overlap, multi + + def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> List[Line]: + """ + Generates the center symbol of the diagnostic block + """ + center_symbol = '' + if overlap and multi > 0: + center_symbol = self.SYMBOLS['MIDDLE_CROSS'] + elif overlap: + center_symbol = self.SYMBOLS['MIDDLE_RIGHT_CENTER'] + elif multi > 0: + center_symbol = self.SYMBOLS['UPSIDE_DOWN_T'] + else: + center_symbol = self.SYMBOLS['BOTTOM_LEFT'] + return [ + { + "class": self.HIGHLIGHTS[self._get_severity(diagnostic)], + "content": '{0}{1} '.format(center_symbol, self.SYMBOLS['HORIZONTAL'] * 4), + } + ] + + def _get_severity(self, diagnostic: Diagnostic) -> int: + # Default to error if no severity. + return diagnostic.get('severity', 1) + + SUBLIME_WORD_MASK = 515 @@ -157,6 +453,7 @@ def on_change() -> None: self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._completions_task = None # type: Optional[QueryCompletionsTask] self._stored_selection = [] # type: List[sublime.Region] + self._diagnostic_lines = DiagnosticLines(self.view, highlight_line_background=False) self._setup() def __del__(self) -> None: @@ -186,6 +483,7 @@ def _cleanup(self) -> None: self.view.erase_status(AbstractViewListener.TOTAL_ERRORS_AND_WARNINGS_STATUS_KEY) self._clear_highlight_regions() self._clear_session_views_async() + self._diagnostic_lines.clear() def _reset(self) -> None: # Have to do this on the main thread, since __init__ and __del__ are invoked on the main thread too @@ -289,6 +587,11 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: is_active_view = window and window.active_view() == self.view if is_active_view and self.view.change_count() == self._change_count_on_last_save: self._toggle_diagnostics_panel_if_needed_async() + all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] + for _, diagnostics in self._diagnostics_async(allow_stale=True): + all_diagnostics.extend(diagnostics) + self._diagnostic_lines.update(all_diagnostics) + def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: @@ -378,6 +681,8 @@ def on_activated_async(self) -> None: def on_selection_modified_async(self) -> None: first_region, any_different = self._update_stored_selection_async() + if any_different: + self._update_inline_diagnostics_async() if first_region is None: return if not self._is_in_higlighted_region(first_region.b): @@ -390,7 +695,6 @@ def on_selection_modified_async(self) -> None: self._when_selection_remains_stable_async(self._do_code_actions_async, first_region, after_ms=self.code_actions_debounce_time) self._update_diagnostic_in_status_bar_async() - self._update_inline_diagnostics_async() self._resolve_visible_code_lenses_async() def on_post_save_async(self) -> None: From 6e6339667ca9d733c82070eb809a361bd2f5c31d Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 3 Mar 2023 21:56:13 +0100 Subject: [PATCH 27/55] fix crash --- plugin/documents.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin/documents.py b/plugin/documents.py index a51844513..fde91786e 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -255,7 +255,9 @@ def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock] blocks = [] for key, line in stacks.items(): block = {'line': key, 'content': [], 'region': line['region']} - for i, (diagnostic_type, data) in enumerate(reversed(line['stack'])): + for i, stack in enumerate(reversed(line['stack'])): + diagnostic_type = stack['kind'] + data = stack['data'] if diagnostic_type != StackKind.DIAGNOSTIC: continue diagnostic = data @@ -291,8 +293,8 @@ def _generate_left_side( multi = 0 current_index = 0 while current_index < index: - diagnostic_type = line[current_index][0] - data = line[current_index][1] + diagnostic_type = line[current_index]['kind'] + data = line[current_index]['data'] if diagnostic_type == StackKind.SPACE: if multi == 0: left.append({'class': '', 'content': data}) @@ -302,7 +304,7 @@ def _generate_left_side( 'content': self.SYMBOLS['HORIZONTAL'] * len(data) }) elif diagnostic_type == StackKind.DIAGNOSTIC: - if current_index+1 != len(line) and line[current_index+1][0] != StackKind.OVERLAP: + if current_index + 1 != len(line) and line[current_index + 1]['kind'] != StackKind.OVERLAP: left.append( { "class": self.HIGHLIGHTS[self._get_severity(data)], From 3db120b242eea776c96e03adf3549bae7c199915 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:01:06 +0100 Subject: [PATCH 28/55] POC of inline diagnostics below errors --- plugin/diagnostics.py | 303 ++++++++++++++++++++++++++++++++++++++++++ plugin/documents.py | 300 +---------------------------------------- 2 files changed, 304 insertions(+), 299 deletions(-) create mode 100644 plugin/diagnostics.py diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py new file mode 100644 index 000000000..32a73067b --- /dev/null +++ b/plugin/diagnostics.py @@ -0,0 +1,303 @@ +from .core.protocol import Diagnostic +from .core.typing import Dict, List, Literal, NotRequired, Optional, StrEnum, Tuple, TypedDict, Union +from .core.views import range_to_region +from itertools import chain +import sublime + + +class StackKind(StrEnum): + SPACE = 'space' + DIAGNOSTIC = 'diagnostic' + OVERLAP = 'overlap' + BLANK = 'blank' + +StackItemDiagnostic = TypedDict('StackItemDiagnostic', { + 'kind': Literal[StackKind.DIAGNOSTIC], + 'data': Diagnostic, +}) + +StackItemSpace = TypedDict('StackItemSpace', { + 'kind': Literal[StackKind.SPACE], + 'data': str, +}) + +StackItemOverlap = TypedDict('StackItemOverlap', { + 'kind': Literal[StackKind.OVERLAP], + 'data': None, +}) + +StackItem = Union[ + StackItemDiagnostic, + StackItemSpace, + StackItemOverlap, + # Tuple[Literal[StackKind.SPACE], str], + # Tuple[Literal[StackKind.OVERLAP], None], + # Tuple[Literal[StackKind.DIAGNOSTIC], Diagnostic], + # Tuple[Literal[StackKind.BLANK], Diagnostic], +] + +LineStack = TypedDict('LineStack', { + 'region': Optional[sublime.Region], + 'stack': List[StackItem] +}) + +StackMap = Dict[int, LineStack] + +Line = TypedDict('Line', { + 'class': str, + 'content': NotRequired[str], +}) + +DiagnosticBlock = TypedDict('DiagnosticBlock', { + 'content': List[List[Line]], + 'line': int, + 'region': sublime.Region, +}) + + +class DiagnosticLines: + CSS = ''' + .inline-block { + display: inline-block; + } + .d_error { + color: color(var(--redish) alpha(0.85)) + } + .d_error_bg { + background-color: color(var(--redish) alpha(0.1)) + } + .d_warning { + color: color(var(--yellowish) alpha(0.85)) + } + .d_warning_bg { + background-color: color(var(--yellowish) alpha(0.1)) + } + .d_info { + color: color(var(--bluish) alpha(0.85)) + } + .d_info_bg { + background-color: color(var(--bluish) alpha(0.1)) + } + .d_hint { + color: color(var(--greenish) alpha(0.85)) + } + .d_hint_bg { + background-color: color(var(--greenish) alpha(0.1)) + } + '''.strip() + HIGHLIGHTS = { + 1: 'error', + 2: 'warning', + 3: 'info', + 4: 'hint' + } + + COLORS = { + 'error': 'var(--redish)', + 'warning': 'var(--yellowish)', + 'info': 'var(--blueish)', + 'hint': 'var(--greenish)', + '': 'transparent', + } + + SYMBOLS = { + 'BOTTOM_LEFT': '└', + 'UPSIDE_DOWN_T': '┴', + 'MIDDLE_CROSS': '┼', + 'MIDDLE_RIGHT_CENTER': '├', + 'VERTICAL': '│', + 'HORIZONTAL': '─' + } + + def __init__(self, view: sublime.View, highlight_line_background: bool = False) -> None: + self._view = view + self._diagnostics = [] # type: List[Diagnostic] + self._highlight_line_background = highlight_line_background + self._phantoms = sublime.PhantomSet(view, 'lsp_lines') + + def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: + self._diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) + self._line_stacks = self._generate_line_stacks(self._diagnostics) + self._blocks = self._generate_diagnostic_blocks(self._line_stacks) + phantoms = [] # Type: List[sublime.Phantom] + for block in self._blocks: + content = self._generate_region_html(block) + phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) + self._phantoms.update(phantoms) + + def clear(self) -> None: + self._phantoms = sublime.PhantomSet(self._view, 'lsp-lines') + + def _generate_region_html(self, block: DiagnosticBlock) -> str: + lines = [ + ''.format(self.CSS) + ] + for line in block["content"]: + row_items = [] + for item in line: + item_class = 'd_{0}'.format(item['class']) + css_classes = ['inline-block', item_class] + if self._highlight_line_background: + css_classes.append('{0}_bg'.format(item_class)) + row_items.append('
{1}
'.format( + ' '.join(css_classes), item.get('content', '').replace(" ", " "))) + lines.append('
{0}
'.format(''.join(row_items))) + return '\n'.join(lines) + + def _preprocess_diagnostic(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]): + return [diagnostic[0] for diagnostic in diagnostics] + + def _sort_diagnostics(self, diagnostics: List[Diagnostic]): + return sorted(diagnostics, key=lambda x: (x['range']['start']['line'], x['range']['start']['character'])) + + def _generate_line_stacks(self, diagnostics: List[Diagnostic]) -> StackMap: + # Initialize an empty dictionary to store line stacks + line_stacks = {} # type: StackMap + # Set the initial values for the previous line number and previous column + prev_lnum = -1 + prev_col = 0 + # Iterate over the diagnostics + for diagnostic in diagnostics: + if not diagnostic['message'].strip(): + # Skip diagnostics with empty message. + continue + range_start = diagnostic['range']['start'] + current_line = range_start['line'] + current_col = range_start['character'] + # Create an empty list for the current line if it doesn't already exist in the dictionary + line_stacks.setdefault(current_line, {'region': None, 'stack': []}) + if line_stacks[current_line]['region'] is None: + region = range_to_region(diagnostic['range'], self._view) + region.b = region.a + line_stacks[current_line]['region'] = region + # Get the current stack for the current line + stack = line_stacks[current_line]['stack'] + # Check if the diagnostic is on a new line + if current_line != prev_lnum: + # If so, add an empty space to the stack + stack.append({'kind': StackKind.SPACE, 'data': ''}) + elif current_col != prev_col: + # If not on a new line but on a new column, add spacing to the stack + # Calculate the spacing by subtracting the previous column from the current column, minus 1 (to account + # for 0-based index) + spacing = (current_col - prev_col) - 1 + stack.append({'kind': StackKind.SPACE, 'data': ' ' * spacing}) + else: + # If the diagnostic is on the same exact spot as the previous one, add an overlap to the stack + stack.append({'kind': StackKind.OVERLAP, 'data': None}) + # If not blank, add the diagnostic to the stack + stack.append({'kind': StackKind.DIAGNOSTIC, 'data': diagnostic}) + # Update the previous line number and column for the next iteration + prev_lnum = current_line + prev_col = current_col + return line_stacks + + def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock]: + """ + Generates the diagnostic blocks from the given stacks + """ + blocks = [] + for key, line in stacks.items(): + block = {'line': key, 'content': [], 'region': line['region']} + for i, stack in enumerate(reversed(line['stack'])): + diagnostic_type = stack['kind'] + data = stack['data'] + if diagnostic_type != StackKind.DIAGNOSTIC: + continue + diagnostic = data + index = len(line['stack']) - 1 - i + left, overlap, multi = self._generate_left_side(line['stack'], index, diagnostic) + center = self._generate_center(overlap, multi, diagnostic) + for msg_line in diagnostic['message'].split('\n'): + block['content'].append(list(chain(left, center, [{ + 'content': msg_line, + 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)] + }]))) + if overlap: + center = [ + { + 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], + 'content': self.SYMBOLS['VERTICAL'] + }, + {'class': '', 'content': ' '}, + ] + else: + center = [{'class': '', 'content': ' '}] + blocks.append(block) + return blocks + + def _generate_left_side( + self, line: List[StackItem], index: int, diagnostic: Diagnostic + ) -> Tuple[List[Line], bool, int]: + """ + Generates the left side of the diagnostic block for a given line + """ + left = [] + overlap = False + multi = 0 + current_index = 0 + while current_index < index: + diagnostic_type = line[current_index]['kind'] + data = line[current_index]['data'] + if diagnostic_type == StackKind.SPACE: + if multi == 0: + left.append({'class': '', 'content': data}) + else: + left.append({ + 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], + 'content': self.SYMBOLS['HORIZONTAL'] * len(data) + }) + elif diagnostic_type == StackKind.DIAGNOSTIC: + if current_index + 1 != len(line) and line[current_index + 1]['kind'] != StackKind.OVERLAP: + left.append( + { + "class": self.HIGHLIGHTS[self._get_severity(data)], + "content": self.SYMBOLS['VERTICAL'], + } + ) + overlap = False + elif diagnostic_type == StackKind.BLANK: + if multi == 0: + left.append( + { + "class": self.HIGHLIGHTS[self._get_severity(data)], + "content": self.SYMBOLS['BOTTOM_LEFT'], + } + ) + else: + left.append( + { + "class": self.HIGHLIGHTS[self._get_severity(data)], + "content": self.SYMBOLS['UPSIDE_DOWN_T'], + } + ) + multi += 1 + elif diagnostic_type == StackKind.OVERLAP: + overlap = True + current_index += 1 + return left, overlap, multi + + def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> List[Line]: + """ + Generates the center symbol of the diagnostic block + """ + center_symbol = '' + if overlap and multi > 0: + center_symbol = self.SYMBOLS['MIDDLE_CROSS'] + elif overlap: + center_symbol = self.SYMBOLS['MIDDLE_RIGHT_CENTER'] + elif multi > 0: + center_symbol = self.SYMBOLS['UPSIDE_DOWN_T'] + else: + center_symbol = self.SYMBOLS['BOTTOM_LEFT'] + return [ + { + "class": self.HIGHLIGHTS[self._get_severity(diagnostic)], + "content": '{0}{1} '.format(center_symbol, self.SYMBOLS['HORIZONTAL'] * 4), + } + ] + + def _get_severity(self, diagnostic: Diagnostic) -> int: + # Default to error if no severity. + return diagnostic.get('severity', 1) + diff --git a/plugin/documents.py b/plugin/documents.py index 4018126f8..e1e0215c9 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -28,7 +28,7 @@ from .core.types import DebouncerNonThreadSafe from .core.types import FEATURES_TIMEOUT from .core.types import SettingsRegistration -from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Literal, NotRequired, StrEnum, Tuple, TypedDict, Union # noqa: E501 +from .core.typing import Any, Callable, Optional, Dict, Generator, Iterable, List, Tuple from .core.typing import cast from .core.url import parse_uri from .core.url import view_to_uri @@ -50,7 +50,6 @@ from .session_buffer import SessionBuffer from .session_view import SessionView from functools import partial -from itertools import chain from weakref import WeakSet from weakref import WeakValueDictionary import itertools @@ -61,303 +60,6 @@ import webbrowser -class StackKind(StrEnum): - SPACE = 'space' - DIAGNOSTIC = 'diagnostic' - OVERLAP = 'overlap' - BLANK = 'blank' - -StackItemDiagnostic = TypedDict('StackItemDiagnostic', { - 'kind': Literal[StackKind.DIAGNOSTIC], - 'data': Diagnostic, -}) - -StackItemSpace = TypedDict('StackItemSpace', { - 'kind': Literal[StackKind.SPACE], - 'data': str, -}) - -StackItemOverlap = TypedDict('StackItemOverlap', { - 'kind': Literal[StackKind.OVERLAP], - 'data': None, -}) - -StackItem = Union[ - StackItemDiagnostic, - StackItemSpace, - StackItemOverlap, - # Tuple[Literal[StackKind.SPACE], str], - # Tuple[Literal[StackKind.OVERLAP], None], - # Tuple[Literal[StackKind.DIAGNOSTIC], Diagnostic], - # Tuple[Literal[StackKind.BLANK], Diagnostic], -] - -LineStack = TypedDict('LineStack', { - 'region': Optional[sublime.Region], - 'stack': List[StackItem] -}) - -StackMap = Dict[int, LineStack] - -Line = TypedDict('Line', { - 'class': str, - 'content': NotRequired[str], -}) - -DiagnosticBlock = TypedDict('DiagnosticBlock', { - 'content': List[List[Line]], - 'line': int, - 'region': sublime.Region, -}) - - -class DiagnosticLines: - CSS = ''' - .inline-block { - display: inline-block; - } - .d_error { - color: color(var(--redish) alpha(0.85)) - } - .d_error_bg { - background-color: color(var(--redish) alpha(0.1)) - } - .d_warning { - color: color(var(--yellowish) alpha(0.85)) - } - .d_warning_bg { - background-color: color(var(--yellowish) alpha(0.1)) - } - .d_info { - color: color(var(--bluish) alpha(0.85)) - } - .d_info_bg { - background-color: color(var(--bluish) alpha(0.1)) - } - .d_hint { - color: color(var(--greenish) alpha(0.85)) - } - .d_hint_bg { - background-color: color(var(--greenish) alpha(0.1)) - } - '''.strip() - HIGHLIGHTS = { - 1: 'error', - 2: 'warning', - 3: 'info', - 4: 'hint' - } - - COLORS = { - 'error': 'var(--redish)', - 'warning': 'var(--yellowish)', - 'info': 'var(--blueish)', - 'hint': 'var(--greenish)', - '': 'transparent', - } - - SYMBOLS = { - 'BOTTOM_LEFT': '└', - 'UPSIDE_DOWN_T': '┴', - 'MIDDLE_CROSS': '┼', - 'MIDDLE_RIGHT_CENTER': '├', - 'VERTICAL': '│', - 'HORIZONTAL': '─' - } - - def __init__(self, view: sublime.View, highlight_line_background: bool = False) -> None: - self._view = view - self._diagnostics = [] # type: List[Diagnostic] - self._highlight_line_background = highlight_line_background - self._phantoms = sublime.PhantomSet(view, 'lsp_lines') - - def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: - self._diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) - self._line_stacks = self._generate_line_stacks(self._diagnostics) - self._blocks = self._generate_diagnostic_blocks(self._line_stacks) - phantoms = [] # Type: List[sublime.Phantom] - for block in self._blocks: - content = self._generate_region_html(block) - phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) - self._phantoms.update(phantoms) - - def clear(self) -> None: - self._phantoms = sublime.PhantomSet(self._view, 'lsp-lines') - - def _generate_region_html(self, block: DiagnosticBlock) -> str: - lines = [ - ''.format(self.CSS) - ] - for line in block["content"]: - row_items = [] - for item in line: - item_class = 'd_{0}'.format(item['class']) - css_classes = ['inline-block', item_class] - if self._highlight_line_background: - css_classes.append('{0}_bg'.format(item_class)) - row_items.append('
{1}
'.format( - ' '.join(css_classes), item.get('content', '').replace(" ", " "))) - lines.append('
{0}
'.format(''.join(row_items))) - return '\n'.join(lines) - - def _preprocess_diagnostic(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]): - return [diagnostic[0] for diagnostic in diagnostics] - - def _sort_diagnostics(self, diagnostics: List[Diagnostic]): - return sorted(diagnostics, key=lambda x: (x['range']['start']['line'], x['range']['start']['character'])) - - def _generate_line_stacks(self, diagnostics: List[Diagnostic]) -> StackMap: - # Initialize an empty dictionary to store line stacks - line_stacks = {} # type: StackMap - # Set the initial values for the previous line number and previous column - prev_lnum = -1 - prev_col = 0 - # Iterate over the diagnostics - for diagnostic in diagnostics: - if not diagnostic['message'].strip(): - # Skip diagnostics with empty message. - continue - range_start = diagnostic['range']['start'] - current_line = range_start['line'] - current_col = range_start['character'] - # Create an empty list for the current line if it doesn't already exist in the dictionary - line_stacks.setdefault(current_line, {'region': None, 'stack': []}) - if line_stacks[current_line]['region'] is None: - region = range_to_region(diagnostic['range'], self._view) - region.b = region.a - line_stacks[current_line]['region'] = region - # Get the current stack for the current line - stack = line_stacks[current_line]['stack'] - # Check if the diagnostic is on a new line - if current_line != prev_lnum: - # If so, add an empty space to the stack - stack.append({'kind': StackKind.SPACE, 'data': ''}) - elif current_col != prev_col: - # If not on a new line but on a new column, add spacing to the stack - # Calculate the spacing by subtracting the previous column from the current column, minus 1 (to account - # for 0-based index) - spacing = (current_col - prev_col) - 1 - stack.append({'kind': StackKind.SPACE, 'data': ' ' * spacing}) - else: - # If the diagnostic is on the same exact spot as the previous one, add an overlap to the stack - stack.append({'kind': StackKind.OVERLAP, 'data': None}) - # If not blank, add the diagnostic to the stack - stack.append({'kind': StackKind.DIAGNOSTIC, 'data': diagnostic}) - # Update the previous line number and column for the next iteration - prev_lnum = current_line - prev_col = current_col - return line_stacks - - def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock]: - """ - Generates the diagnostic blocks from the given stacks - """ - blocks = [] - for key, line in stacks.items(): - block = {'line': key, 'content': [], 'region': line['region']} - for i, stack in enumerate(reversed(line['stack'])): - diagnostic_type = stack['kind'] - data = stack['data'] - if diagnostic_type != StackKind.DIAGNOSTIC: - continue - diagnostic = data - index = len(line['stack']) - 1 - i - left, overlap, multi = self._generate_left_side(line['stack'], index, diagnostic) - center = self._generate_center(overlap, multi, diagnostic) - for msg_line in diagnostic['message'].split('\n'): - block['content'].append(list(chain(left, center, [{ - 'content': msg_line, - 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)] - }]))) - if overlap: - center = [ - { - 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], - 'content': self.SYMBOLS['VERTICAL'] - }, - {'class': '', 'content': ' '}, - ] - else: - center = [{'class': '', 'content': ' '}] - blocks.append(block) - return blocks - - def _generate_left_side( - self, line: List[StackItem], index: int, diagnostic: Diagnostic - ) -> Tuple[List[Line], bool, int]: - """ - Generates the left side of the diagnostic block for a given line - """ - left = [] - overlap = False - multi = 0 - current_index = 0 - while current_index < index: - diagnostic_type = line[current_index]['kind'] - data = line[current_index]['data'] - if diagnostic_type == StackKind.SPACE: - if multi == 0: - left.append({'class': '', 'content': data}) - else: - left.append({ - 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], - 'content': self.SYMBOLS['HORIZONTAL'] * len(data) - }) - elif diagnostic_type == StackKind.DIAGNOSTIC: - if current_index + 1 != len(line) and line[current_index + 1]['kind'] != StackKind.OVERLAP: - left.append( - { - "class": self.HIGHLIGHTS[self._get_severity(data)], - "content": self.SYMBOLS['VERTICAL'], - } - ) - overlap = False - elif diagnostic_type == StackKind.BLANK: - if multi == 0: - left.append( - { - "class": self.HIGHLIGHTS[self._get_severity(data)], - "content": self.SYMBOLS['BOTTOM_LEFT'], - } - ) - else: - left.append( - { - "class": self.HIGHLIGHTS[self._get_severity(data)], - "content": self.SYMBOLS['UPSIDE_DOWN_T'], - } - ) - multi += 1 - elif diagnostic_type == StackKind.OVERLAP: - overlap = True - current_index += 1 - return left, overlap, multi - - def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> List[Line]: - """ - Generates the center symbol of the diagnostic block - """ - center_symbol = '' - if overlap and multi > 0: - center_symbol = self.SYMBOLS['MIDDLE_CROSS'] - elif overlap: - center_symbol = self.SYMBOLS['MIDDLE_RIGHT_CENTER'] - elif multi > 0: - center_symbol = self.SYMBOLS['UPSIDE_DOWN_T'] - else: - center_symbol = self.SYMBOLS['BOTTOM_LEFT'] - return [ - { - "class": self.HIGHLIGHTS[self._get_severity(diagnostic)], - "content": '{0}{1} '.format(center_symbol, self.SYMBOLS['HORIZONTAL'] * 4), - } - ] - - def _get_severity(self, diagnostic: Diagnostic) -> int: - # Default to error if no severity. - return diagnostic.get('severity', 1) - - SUBLIME_WORD_MASK = 515 From 7dd09085ee8ed38b934c89600758136f2480fba0 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:01:36 +0100 Subject: [PATCH 29/55] move --- LSP.sublime-settings | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index d19e57064..f5f88aed0 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -43,6 +43,12 @@ // --- Diagnostics -------------------------------------------------------------------- + // Show the diagnostics as inline annotations. + // When using the "at-cursor" value it's recommended to not use the "annotation" + // value for the "show_code_actions" option as then the code action annotations will + // show over the diagnostic annotations. + "show_diagnostics_inline": "none", + // Show errors and warnings count in the status bar "show_diagnostics_count_in_view_status": false, @@ -50,12 +56,6 @@ // under the cursor in status bar if available. "show_diagnostics_in_view_status": true, - // Show the diagnostics as inline annotations. - // When using the "at-cursor" value it's recommended to not use the "annotation" - // value for the "show_code_actions" option as then the code action annotations will - // show over the diagnostic annotations. - "show_diagnostics_inline": "none", - // Show highlights and gutter markers in the file views for diagnostics // with level equal to or less than: // none: 0 (never show) From 2a447952150e94a1e20f4d0704a0949f2d41c71e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:09:19 +0100 Subject: [PATCH 30/55] lint --- plugin/diagnostics.py | 2 +- plugin/documents.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 32a73067b..436fff9de 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -11,6 +11,7 @@ class StackKind(StrEnum): OVERLAP = 'overlap' BLANK = 'blank' + StackItemDiagnostic = TypedDict('StackItemDiagnostic', { 'kind': Literal[StackKind.DIAGNOSTIC], 'data': Diagnostic, @@ -300,4 +301,3 @@ def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> def _get_severity(self, diagnostic: Diagnostic) -> int: # Default to error if no severity. return diagnostic.get('severity', 1) - diff --git a/plugin/documents.py b/plugin/documents.py index e1e0215c9..3510ca6e6 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -46,6 +46,7 @@ from .core.views import text_document_position_params from .core.views import update_lsp_popup from .core.windows import WindowManager +from .diagnostics import DiagnosticLines from .hover import code_actions_content from .session_buffer import SessionBuffer from .session_view import SessionView @@ -297,7 +298,6 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: all_diagnostics.extend(diagnostics) self._diagnostic_lines.update(all_diagnostics) - def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: if self._stored_selection: From d2a9f84901ce975d8fea21396d0cfa525697138e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:13:30 +0100 Subject: [PATCH 31/55] no need to save as variables --- plugin/diagnostics.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 436fff9de..4a9a33ce6 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -112,16 +112,15 @@ class DiagnosticLines: def __init__(self, view: sublime.View, highlight_line_background: bool = False) -> None: self._view = view - self._diagnostics = [] # type: List[Diagnostic] self._highlight_line_background = highlight_line_background self._phantoms = sublime.PhantomSet(view, 'lsp_lines') def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: - self._diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) - self._line_stacks = self._generate_line_stacks(self._diagnostics) - self._blocks = self._generate_diagnostic_blocks(self._line_stacks) + sorted_diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) + line_stacks = self._generate_line_stacks(sorted_diagnostics) + blocks = self._generate_diagnostic_blocks(line_stacks) phantoms = [] # Type: List[sublime.Phantom] - for block in self._blocks: + for block in blocks: content = self._generate_region_html(block) phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) self._phantoms.update(phantoms) From 9d9d428e7547a685601394de2f174c8171a57e2e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:26:16 +0100 Subject: [PATCH 32/55] type-friendly code --- plugin/diagnostics.py | 88 ++++++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 4a9a33ce6..342ad2365 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -12,30 +12,36 @@ class StackKind(StrEnum): BLANK = 'blank' -StackItemDiagnostic = TypedDict('StackItemDiagnostic', { - 'kind': Literal[StackKind.DIAGNOSTIC], - 'data': Diagnostic, -}) +class StackItemBlank: -StackItemSpace = TypedDict('StackItemSpace', { - 'kind': Literal[StackKind.SPACE], - 'data': str, -}) + __slots__ = ('diagnostic', ) + + def __init__(self, diagnostic: Diagnostic) -> None: + self.diagnostic = diagnostic + + +class StackItemDiagnostic: + + __slots__ = ('diagnostic', ) + + def __init__(self, diagnostic: Diagnostic) -> None: + self.diagnostic = diagnostic + + +class StackItemOverlap: + + __slots__ = () + + +class StackItemSpace: + + __slots__ = ('text', ) + + def __init__(self, text: str) -> None: + self.text = text -StackItemOverlap = TypedDict('StackItemOverlap', { - 'kind': Literal[StackKind.OVERLAP], - 'data': None, -}) -StackItem = Union[ - StackItemDiagnostic, - StackItemSpace, - StackItemOverlap, - # Tuple[Literal[StackKind.SPACE], str], - # Tuple[Literal[StackKind.OVERLAP], None], - # Tuple[Literal[StackKind.DIAGNOSTIC], Diagnostic], - # Tuple[Literal[StackKind.BLANK], Diagnostic], -] +StackItem = Union[StackItemDiagnostic, StackItemSpace, StackItemOverlap] LineStack = TypedDict('LineStack', { 'region': Optional[sublime.Region], @@ -175,18 +181,18 @@ def _generate_line_stacks(self, diagnostics: List[Diagnostic]) -> StackMap: # Check if the diagnostic is on a new line if current_line != prev_lnum: # If so, add an empty space to the stack - stack.append({'kind': StackKind.SPACE, 'data': ''}) + stack.append(StackItemSpace('')) elif current_col != prev_col: # If not on a new line but on a new column, add spacing to the stack # Calculate the spacing by subtracting the previous column from the current column, minus 1 (to account # for 0-based index) spacing = (current_col - prev_col) - 1 - stack.append({'kind': StackKind.SPACE, 'data': ' ' * spacing}) + stack.append(StackItemSpace(' ' * spacing)) else: # If the diagnostic is on the same exact spot as the previous one, add an overlap to the stack - stack.append({'kind': StackKind.OVERLAP, 'data': None}) + stack.append(StackItemOverlap()) # If not blank, add the diagnostic to the stack - stack.append({'kind': StackKind.DIAGNOSTIC, 'data': diagnostic}) + stack.append(StackItemDiagnostic(diagnostic)) # Update the previous line number and column for the next iteration prev_lnum = current_line prev_col = current_col @@ -199,12 +205,10 @@ def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock] blocks = [] for key, line in stacks.items(): block = {'line': key, 'content': [], 'region': line['region']} - for i, stack in enumerate(reversed(line['stack'])): - diagnostic_type = stack['kind'] - data = stack['data'] - if diagnostic_type != StackKind.DIAGNOSTIC: + for i, item in enumerate(reversed(line['stack'])): + if not isinstance(item, StackItemDiagnostic): continue - diagnostic = data + diagnostic = item.diagnostic index = len(line['stack']) - 1 - i left, overlap, multi = self._generate_left_side(line['stack'], index, diagnostic) center = self._generate_center(overlap, multi, diagnostic) @@ -237,42 +241,42 @@ def _generate_left_side( multi = 0 current_index = 0 while current_index < index: - diagnostic_type = line[current_index]['kind'] - data = line[current_index]['data'] - if diagnostic_type == StackKind.SPACE: + item = line[current_index] + if isinstance(item, StackItemSpace): if multi == 0: - left.append({'class': '', 'content': data}) + left.append({'class': '', 'content': item.text}) else: left.append({ 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], - 'content': self.SYMBOLS['HORIZONTAL'] * len(data) + 'content': self.SYMBOLS['HORIZONTAL'] * len(item.text) }) - elif diagnostic_type == StackKind.DIAGNOSTIC: - if current_index + 1 != len(line) and line[current_index + 1]['kind'] != StackKind.OVERLAP: + elif isinstance(item, StackItemDiagnostic): + next_item =line[current_index + 1] + if current_index + 1 != len(line) and not isinstance(next_item, StackItemOverlap): left.append( { - "class": self.HIGHLIGHTS[self._get_severity(data)], + "class": self.HIGHLIGHTS[self._get_severity(item.diagnostic)], "content": self.SYMBOLS['VERTICAL'], } ) overlap = False - elif diagnostic_type == StackKind.BLANK: + elif isinstance(item, StackItemBlank): if multi == 0: left.append( { - "class": self.HIGHLIGHTS[self._get_severity(data)], + "class": self.HIGHLIGHTS[self._get_severity(item.diagnostic)], "content": self.SYMBOLS['BOTTOM_LEFT'], } ) else: left.append( { - "class": self.HIGHLIGHTS[self._get_severity(data)], + "class": self.HIGHLIGHTS[self._get_severity(item.diagnostic)], "content": self.SYMBOLS['UPSIDE_DOWN_T'], } ) multi += 1 - elif diagnostic_type == StackKind.OVERLAP: + elif isinstance(item, StackItemOverlap): overlap = True current_index += 1 return left, overlap, multi From 85408657af70e8ae2077ce527948005aa07b1497 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:27:33 +0100 Subject: [PATCH 33/55] lint --- plugin/diagnostics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 342ad2365..7aa1d4e14 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -1,5 +1,5 @@ from .core.protocol import Diagnostic -from .core.typing import Dict, List, Literal, NotRequired, Optional, StrEnum, Tuple, TypedDict, Union +from .core.typing import Dict, List, NotRequired, Optional, StrEnum, Tuple, TypedDict, Union from .core.views import range_to_region from itertools import chain import sublime @@ -30,7 +30,7 @@ def __init__(self, diagnostic: Diagnostic) -> None: class StackItemOverlap: - __slots__ = () + __slots__ = () class StackItemSpace: @@ -251,7 +251,7 @@ def _generate_left_side( 'content': self.SYMBOLS['HORIZONTAL'] * len(item.text) }) elif isinstance(item, StackItemDiagnostic): - next_item =line[current_index + 1] + next_item = line[current_index + 1] if current_index + 1 != len(line) and not isinstance(next_item, StackItemOverlap): left.append( { From 3ed247b8ff9355818d9195a573e0a3ba267248aa Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 12:29:58 +0100 Subject: [PATCH 34/55] unused class --- plugin/diagnostics.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 7aa1d4e14..d9d85bf1a 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -1,17 +1,10 @@ from .core.protocol import Diagnostic -from .core.typing import Dict, List, NotRequired, Optional, StrEnum, Tuple, TypedDict, Union +from .core.typing import Dict, List, NotRequired, Optional, Tuple, TypedDict, Union from .core.views import range_to_region from itertools import chain import sublime -class StackKind(StrEnum): - SPACE = 'space' - DIAGNOSTIC = 'diagnostic' - OVERLAP = 'overlap' - BLANK = 'blank' - - class StackItemBlank: __slots__ = ('diagnostic', ) From cac80c474409b785859f5596b129f2b1620f981c Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 6 Mar 2023 10:08:22 +0100 Subject: [PATCH 35/55] (hack) try to keep selection at same viewport position --- plugin/diagnostics.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index d9d85bf1a..eb63702d9 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -122,7 +122,14 @@ def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: for block in blocks: content = self._generate_region_html(block) phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) + x, y_before = self._view.text_to_layout(self._view.sel()[0].begin()) + _, y_viewport_before = self._view.viewport_position() self._phantoms.update(phantoms) + _, y_after = self._view.text_to_layout(self._view.sel()[0].begin()) + y_shift = y_after - y_before + if y_shift != 0: + new_y = y_viewport_before + y_shift + self._view.set_viewport_position((x, new_y), animate=False) def clear(self) -> None: self._phantoms = sublime.PhantomSet(self._view, 'lsp-lines') From 611916a75e4e8c2b561633bf8ee9bf8ad1b932fa Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 6 Mar 2023 10:47:07 +0100 Subject: [PATCH 36/55] better x --- plugin/diagnostics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index eb63702d9..fb288f52d 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -122,8 +122,8 @@ def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: for block in blocks: content = self._generate_region_html(block) phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) - x, y_before = self._view.text_to_layout(self._view.sel()[0].begin()) - _, y_viewport_before = self._view.viewport_position() + _, y_before = self._view.text_to_layout(self._view.sel()[0].begin()) + x, y_viewport_before = self._view.viewport_position() self._phantoms.update(phantoms) _, y_after = self._view.text_to_layout(self._view.sel()[0].begin()) y_shift = y_after - y_before From f3b6d82a20ec4e0b8d3a51a54997ae2458252a00 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 4 Mar 2023 22:31:06 +0100 Subject: [PATCH 37/55] lines --- plugin/diagnostics.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index fb288f52d..c36b97330 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -56,6 +56,7 @@ def __init__(self, text: str) -> None: class DiagnosticLines: + CSS = ''' .inline-block { display: inline-block; @@ -85,6 +86,7 @@ class DiagnosticLines: background-color: color(var(--greenish) alpha(0.1)) } '''.strip() + HIGHLIGHTS = { 1: 'error', 2: 'warning', From 055835e1f81c69994183ad59b53a05ccfcf04d2e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 10 Mar 2023 21:58:06 +0100 Subject: [PATCH 38/55] escape html in phantoms --- plugin/diagnostics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index c36b97330..8deabc6f6 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -2,6 +2,7 @@ from .core.typing import Dict, List, NotRequired, Optional, Tuple, TypedDict, Union from .core.views import range_to_region from itertools import chain +import html import sublime @@ -148,7 +149,7 @@ def _generate_region_html(self, block: DiagnosticBlock) -> str: if self._highlight_line_background: css_classes.append('{0}_bg'.format(item_class)) row_items.append('
{1}
'.format( - ' '.join(css_classes), item.get('content', '').replace(" ", " "))) + ' '.join(css_classes), html.escape(item.get('content', '')).replace(" ", " "))) lines.append('
{0}
'.format(''.join(row_items))) return '\n'.join(lines) From ea8d8fcb3db98549447f899a55cc189da2c8e174 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 10 Mar 2023 22:10:27 +0100 Subject: [PATCH 39/55] fix flicker? --- plugin/diagnostics.py | 7 +++++-- plugin/documents.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 8deabc6f6..17a72de40 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -117,14 +117,17 @@ def __init__(self, view: sublime.View, highlight_line_background: bool = False) self._highlight_line_background = highlight_line_background self._phantoms = sublime.PhantomSet(view, 'lsp_lines') - def update(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: + def update_async(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: sorted_diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) line_stacks = self._generate_line_stacks(sorted_diagnostics) blocks = self._generate_diagnostic_blocks(line_stacks) - phantoms = [] # Type: List[sublime.Phantom] + phantoms = [] # type: List[sublime.Phantom] for block in blocks: content = self._generate_region_html(block) phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) + sublime.set_timeout(lambda: self._update_sync(phantoms)) + + def _update_sync(self, phantoms: List[sublime.Phantom]) -> None: _, y_before = self._view.text_to_layout(self._view.sel()[0].begin()) x, y_viewport_before = self._view.viewport_position() self._phantoms.update(phantoms) diff --git a/plugin/documents.py b/plugin/documents.py index 3510ca6e6..567e4c5ad 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -296,7 +296,7 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] for _, diagnostics in self._diagnostics_async(allow_stale=True): all_diagnostics.extend(diagnostics) - self._diagnostic_lines.update(all_diagnostics) + self._diagnostic_lines.update_async(all_diagnostics) def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: From 8fd634ebce783ecfd841efe6a95d62b6be74f326 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 18 Apr 2023 00:01:36 +0200 Subject: [PATCH 40/55] lint --- plugin/diagnostics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 17a72de40..2342cc66d 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -255,7 +255,7 @@ def _generate_left_side( left.append({ 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], 'content': self.SYMBOLS['HORIZONTAL'] * len(item.text) - }) + }) elif isinstance(item, StackItemDiagnostic): next_item = line[current_index + 1] if current_index + 1 != len(line) and not isinstance(next_item, StackItemOverlap): From 6559797edd79ac9ce1671611b68d2f2153ff5542 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Tue, 18 Apr 2023 00:14:50 +0200 Subject: [PATCH 41/55] changes --- LSP.sublime-settings | 7 ++--- plugin/core/types.py | 2 ++ plugin/core/views.py | 25 +++++++++++------ plugin/diagnostics.py | 63 +++++++++++++++++++++++++++++++++++------- plugin/documents.py | 49 +++++++++++--------------------- plugin/session_view.py | 3 ++ sublime-package.json | 27 ++++++++++-------- 7 files changed, 108 insertions(+), 68 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index a6ef07685..f390256bd 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -43,11 +43,8 @@ // --- Diagnostics -------------------------------------------------------------------- - // Show the diagnostics as inline annotations. - // When using the "at-cursor" value it's recommended to not use the "annotation" - // value for the "show_code_actions" option as then the code action annotations will - // show over the diagnostic annotations. - "show_diagnostics_inline": "none", + // How to the diagnostics should be displayed. + "show_diagnostics": ["highlight", "annotation", "phantom"], // Show errors and warnings count in the status bar "show_diagnostics_count_in_view_status": false, diff --git a/plugin/core/types.py b/plugin/core/types.py index 56b35726d..d9f6649bf 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -211,6 +211,7 @@ class Settings: semantic_highlighting = cast(bool, None) show_code_actions = cast(str, None) show_code_lens = cast(str, None) + show_diagnostics = cast(list, None) show_inlay_hints = cast(bool, None) show_code_actions_in_hover = cast(bool, None) show_diagnostics_count_in_view_status = cast(bool, None) @@ -251,6 +252,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("semantic_highlighting", False) r("show_code_actions", "annotation") r("show_code_lens", "annotation") + r("show_diagnostics", ["highlight"]) r("show_inlay_hints", False) r("show_code_actions_in_hover", True) r("show_diagnostics_count_in_view_status", False) diff --git a/plugin/core/views.py b/plugin/core/views.py index 113fed9bb..288742055 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -42,6 +42,8 @@ from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, cast from .url import parse_uri from .workspace import is_subpath_of +from abc import ABCMeta +from abc import abstractmethod import html import itertools import linecache @@ -280,6 +282,14 @@ } +class RegionProvider(metaclass=ABCMeta): + + @classmethod + @abstractmethod + def initialize_region_keys(cls) -> None: + raise NotImplementedError() + + class InvalidUriSchemeException(Exception): def __init__(self, uri: str) -> None: self.uri = uri @@ -870,23 +880,20 @@ def diagnostic_severity(diagnostic: Diagnostic) -> DiagnosticSeverity: def format_diagnostics_for_annotation( - diagnostics: List[Diagnostic], view: sublime.View -) -> Tuple[List[sublime.Region], List[str]]: - regions = [] + 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: - lsp_range = diagnostic.get('range') - if not lsp_range: - continue message = text2html(diagnostic.get('message') or '') source = diagnostic.get('source') - css_class = DIAGNOSTIC_SEVERITY[diagnostic_severity(diagnostic) - 1][1] line = "[{}] {}".format(source, message) if source else message content = '
{3}
'.format( lsp_css().annotations, lsp_css().annotations_classname, css_class, line) - regions.append(range_to_region(lsp_range, view)) annotations.append(content) - return (regions, annotations) + return (annotations, color) def format_diagnostic_for_panel(diagnostic: Diagnostic) -> Tuple[str, Optional[int], Optional[str], Optional[str]]: diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 2342cc66d..24de6f6a9 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -1,6 +1,11 @@ from .core.protocol import Diagnostic +from .core.protocol import DiagnosticSeverity from .core.typing import Dict, List, NotRequired, Optional, Tuple, TypedDict, Union +from .core.views import DIAGNOSTIC_KINDS +from .core.views import diagnostic_severity +from .core.views import format_diagnostics_for_annotation from .core.views import range_to_region +from .core.views import RegionProvider from itertools import chain import html import sublime @@ -221,12 +226,12 @@ def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock] for msg_line in diagnostic['message'].split('\n'): block['content'].append(list(chain(left, center, [{ 'content': msg_line, - 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)] + 'class': self.HIGHLIGHTS[diagnostic_severity(diagnostic)] }]))) if overlap: center = [ { - 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], + 'class': self.HIGHLIGHTS[diagnostic_severity(diagnostic)], 'content': self.SYMBOLS['VERTICAL'] }, {'class': '', 'content': ' '}, @@ -253,7 +258,7 @@ def _generate_left_side( left.append({'class': '', 'content': item.text}) else: left.append({ - 'class': self.HIGHLIGHTS[self._get_severity(diagnostic)], + 'class': self.HIGHLIGHTS[diagnostic_severity(diagnostic)], 'content': self.SYMBOLS['HORIZONTAL'] * len(item.text) }) elif isinstance(item, StackItemDiagnostic): @@ -261,7 +266,7 @@ def _generate_left_side( if current_index + 1 != len(line) and not isinstance(next_item, StackItemOverlap): left.append( { - "class": self.HIGHLIGHTS[self._get_severity(item.diagnostic)], + "class": self.HIGHLIGHTS[diagnostic_severity(item.diagnostic)], "content": self.SYMBOLS['VERTICAL'], } ) @@ -270,14 +275,14 @@ def _generate_left_side( if multi == 0: left.append( { - "class": self.HIGHLIGHTS[self._get_severity(item.diagnostic)], + "class": self.HIGHLIGHTS[diagnostic_severity(item.diagnostic)], "content": self.SYMBOLS['BOTTOM_LEFT'], } ) else: left.append( { - "class": self.HIGHLIGHTS[self._get_severity(item.diagnostic)], + "class": self.HIGHLIGHTS[diagnostic_severity(item.diagnostic)], "content": self.SYMBOLS['UPSIDE_DOWN_T'], } ) @@ -302,11 +307,49 @@ def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> center_symbol = self.SYMBOLS['BOTTOM_LEFT'] return [ { - "class": self.HIGHLIGHTS[self._get_severity(diagnostic)], + "class": self.HIGHLIGHTS[diagnostic_severity(diagnostic)], "content": '{0}{1} '.format(center_symbol, self.SYMBOLS['HORIZONTAL'] * 4), } ] - def _get_severity(self, diagnostic: Diagnostic) -> int: - # Default to error if no severity. - return diagnostic.get('severity', 1) + +class DiagnosticsView(RegionProvider): + ANNOTATIONS_REGION_KEY = "lsp_d-annotations" + + @classmethod + def initialize_region_keys(cls, view: sublime.View) -> None: + r = [sublime.Region(0, 0)] + for severity in DIAGNOSTIC_KINDS.keys(): + view.add_regions(cls._annotation_key(severity), r) + + @classmethod + def _annotation_key(cls, severity: DiagnosticSeverity) -> str: + return '{}-{}'.format(cls.ANNOTATIONS_REGION_KEY, severity) + + def __init__(self, view: sublime.View) -> None: + self._view = view + + def clear_annotations_async(self) -> None: + for severity in DIAGNOSTIC_KINDS.keys(): + self._view.erase_regions(self._annotation_key(severity)) + + def update_diagnostic_annotations_async(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: + # To achieve the correct order of annotations (most severe shown first) and have the color of annotation + # match the diagnostic severity, we have to separately add regions for each severity, from most to least severe. + diagnostics_per_severity = {} # type: Dict[DiagnosticSeverity, List[Tuple[Diagnostic, sublime.Region]]] + for severity in DIAGNOSTIC_KINDS.keys(): + diagnostics_per_severity[severity] = [] + for diagnostic, region in diagnostics: + diagnostics_per_severity[diagnostic_severity(diagnostic)].append((diagnostic, region)) + flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE + for severity, diagnostics in diagnostics_per_severity.items(): + if not diagnostics: + continue + all_diagnostics = [] + regions = [] + for diagnostic, region in diagnostics: + all_diagnostics.append(diagnostic) + regions.append(region) + annotations, color = format_diagnostics_for_annotation(all_diagnostics, severity, self._view) + self._view.add_regions( + self._annotation_key(severity), regions, flags=flags, annotations=annotations, annotation_color=color) diff --git a/plugin/documents.py b/plugin/documents.py index bca4f3c3d..50af4ea43 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -32,13 +32,11 @@ from .core.typing import cast 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_HIGHLIGHT_KIND_SCOPES from .core.views import DOCUMENT_HIGHLIGHT_KINDS from .core.views import first_selection_region from .core.views import format_code_actions_for_quick_panel -from .core.views import format_diagnostics_for_annotation from .core.views import make_command_link from .core.views import MarkdownLangMap from .core.views import range_to_region @@ -47,6 +45,7 @@ from .core.views import update_lsp_popup from .core.windows import WindowManager from .diagnostics import DiagnosticLines +from .diagnostics import DiagnosticsView from .hover import code_actions_content from .session_buffer import SessionBuffer from .session_view import SessionView @@ -126,7 +125,6 @@ def __repr__(self) -> str: class DocumentSyncListener(sublime_plugin.ViewEventListener, AbstractViewListener): ACTIVE_DIAGNOSTIC = "lsp_active_diagnostic" - INLINE_DIAGNOSTIC_REGION_KEY = "lsp_d-annotations" code_actions_debounce_time = FEATURES_TIMEOUT color_boxes_debounce_time = FEATURES_TIMEOUT highlights_debounce_time = FEATURES_TIMEOUT @@ -159,6 +157,7 @@ def on_change() -> None: self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._completions_task = None # type: Optional[QueryCompletionsTask] self._stored_selection = [] # type: List[sublime.Region] + self._diagnostics_view = DiagnosticsView(self.view) self._diagnostic_lines = DiagnosticLines(self.view, highlight_line_background=False) self._setup() @@ -288,15 +287,23 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: if is_view_visible and userprefs().show_code_actions: self._do_code_actions_async() self._update_diagnostic_in_status_bar_async() - self._update_inline_diagnostics_async() window = self.view.window() is_active_view = window and window.active_view() == self.view if is_active_view and self.view.change_count() == self._change_count_on_last_save: self._toggle_diagnostics_panel_if_needed_async() + show_diagnostics = userprefs().show_diagnostics all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - for _, diagnostics in self._diagnostics_async(allow_stale=True): - all_diagnostics.extend(diagnostics) - self._diagnostic_lines.update_async(all_diagnostics) + print('show_diagnostics', show_diagnostics) + if 'phantom' in show_diagnostics or 'annotation' in show_diagnostics: + for _, diagnostics in self._diagnostics_async(allow_stale=True): + all_diagnostics.extend(diagnostics) + if 'phantom' in show_diagnostics: + self._diagnostic_lines.update_async(all_diagnostics) + else: + self._diagnostic_lines.clear() + self._diagnostics_view.clear_annotations_async() + if 'annotation' in show_diagnostics: + self._diagnostics_view.update_diagnostic_annotations_async(all_diagnostics) def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: @@ -311,28 +318,6 @@ def _update_diagnostic_in_status_bar_async(self) -> None: return self.view.erase_status(self.ACTIVE_DIAGNOSTIC) - def _update_inline_diagnostics_async(self) -> None: - selections_diagnostics = [] # type: List[Diagnostic] - for r in self.view.sel(): - session_buffer_diagnostics, _ = self.diagnostics_intersecting_region_async(r) - for _, diagnostics in session_buffer_diagnostics: - selections_diagnostics.extend(diagnostics) - self.view.erase_regions(self.INLINE_DIAGNOSTIC_REGION_KEY) - if userprefs().show_diagnostics_inline != 'at-cursor': - return - if selections_diagnostics: - sorted_diagnostics = sorted(selections_diagnostics, key=lambda d: d.get('severity', 1)) - first_diagnostic = sorted_diagnostics[0] - lsp_range = first_diagnostic.get('range') - if lsp_range: - scope = DIAGNOSTIC_SEVERITY[first_diagnostic.get('severity', 1) - 1][2] - icon = "" - flags = sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE - annotation_color = self.view.style_for_scope(scope).get('foreground') or 'red' - regions, annotations = format_diagnostics_for_annotation(sorted_diagnostics, self.view) - self.view.add_regions( - self.INLINE_DIAGNOSTIC_REGION_KEY, regions, scope, icon, flags, annotations, annotation_color) - def session_buffers_async(self, capability: Optional[str] = None) -> Generator[SessionBuffer, None, None]: for sv in self.session_views_async(): if capability is None or sv.has_capability_async(capability): @@ -388,9 +373,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() - if any_different: - self._update_inline_diagnostics_async() + first_region, _ = self._update_stored_selection_async() if first_region is None: return if not self._is_in_higlighted_region(first_region.b): @@ -888,7 +871,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() diff --git a/plugin/session_view.py b/plugin/session_view.py index 638404627..e950559a1 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -3,6 +3,7 @@ from .core.promise import Promise from .core.protocol import CodeLens from .core.protocol import CodeLensExtended +from .core.protocol import DiagnosticSeverity from .core.protocol import DiagnosticTag from .core.protocol import DocumentUri from .core.protocol import Notification @@ -13,6 +14,7 @@ from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY from .core.views import text_document_identifier +from .diagnostics import DiagnosticsView from .session_buffer import SessionBuffer from weakref import ref from weakref import WeakValueDictionary @@ -149,6 +151,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) + DiagnosticsView.initialize_region_keys(self.view) def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None: '''Remove all of our modifications to the view's "auto_complete_triggers"''' diff --git a/sublime-package.json b/sublime-package.json index b456765b9..f821c4853 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -447,17 +447,22 @@ "default": true, "markdownDescription": "Show the diagnostics description of the code under the cursor in status bar if available." }, - "show_diagnostics_inline": { - "default": "none", - "enum": [ - "at-cursor", - "none" - ], - "markdownEnumDescriptions": [ - "Show diagnostics that intersect with the current selection / cursor inline. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as then the code action annotations will show over the diagnostic annotations.", - "Don't show diagnostics inline." - ], - "markdownDescription": "Show the diagnostics as inline annotations." + "show_diagnostics": { + "default": ["highlight"], + "type": "array", + "items": { + "enum": [ + "highlight", + "annotation", + "phantom" + ] + }, + "uniqueItems": true, + // "markdownEnumDescriptions": [ + // "Show diagnostics that intersect with the current selection / cursor inline. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as then the code action annotations will show over the diagnostic annotations.", + // "Don't show diagnostics inline." + // ], + "markdownDescription": "How to the diagnostics should be displayed." }, "show_diagnostics_severity_level": { "type": "integer", From 4db1142783b4872ca56d56855c7f1446a1e22cb6 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 22 Apr 2023 22:13:38 +0200 Subject: [PATCH 42/55] remove log --- plugin/documents.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugin/documents.py b/plugin/documents.py index 50af4ea43..0a2d0f1c8 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -293,7 +293,6 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: self._toggle_diagnostics_panel_if_needed_async() show_diagnostics = userprefs().show_diagnostics all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - print('show_diagnostics', show_diagnostics) if 'phantom' in show_diagnostics or 'annotation' in show_diagnostics: for _, diagnostics in self._diagnostics_async(allow_stale=True): all_diagnostics.extend(diagnostics) From c2906ff8a18038a55c9ffa555ae27de0f5a0cfdb Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 22:33:36 +0200 Subject: [PATCH 43/55] setting description update --- LSP.sublime-settings | 9 +++++++-- plugin/core/sessions.py | 2 +- plugin/core/types.py | 6 +++--- plugin/goto_diagnostic.py | 2 +- sublime-package.json | 9 ++++----- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index 9feab3051..f6a69d1f4 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -43,8 +43,13 @@ // --- Diagnostics -------------------------------------------------------------------- - // How to the diagnostics should be displayed. - "show_diagnostics": ["highlight", "annotation", "phantom"], + // How to display diagnostics. + // Allowed options: + // - "highlight" - Show all diagnostic regions using highlights. Also see `diagnostics_highlight_style`. + // - "annotation" - Show diagnostics as annotations anchored at the right side of the corresponding line. + // When using this value it's recommended to not use the "annotation" value for the `show_code_actions` + // option as it's not possible to enforce which one is shown first then. + "show_diagnostics": ["highlight"], // Show errors and warnings count in the status bar "show_diagnostics_count_in_view_status": false, diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 80f686225..b8dcf65d5 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -819,7 +819,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 diff --git a/plugin/core/types.py b/plugin/core/types.py index 09542919f..f94b320b2 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -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 """ @@ -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: @@ -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: diff --git a/plugin/goto_diagnostic.py b/plugin/goto_diagnostic.py index 60c4849be..f9fec0ce2 100644 --- a/plugin/goto_diagnostic.py +++ b/plugin/goto_diagnostic.py @@ -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), diff --git a/sublime-package.json b/sublime-package.json index ae1ca72d3..9aff22234 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -462,14 +462,13 @@ "enum": [ "highlight", "annotation", - "phantom" ] }, "uniqueItems": true, - // "markdownEnumDescriptions": [ - // "Show diagnostics that intersect with the current selection / cursor inline. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as then the code action annotations will show over the diagnostic annotations.", - // "Don't show diagnostics inline." - // ], + "markdownEnumDescriptions": [ + "Show all diagnostic regions using highlights. Also see `diagnostics_highlight_style`.", + "Show diagnostics as annotations anchored at the right side of the corresponding line. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as it's not possible to enforce which one is shown first then.", + ], "markdownDescription": "How to the diagnostics should be displayed." }, "show_diagnostics_severity_level": { From 5155de2263e18fe716eefc3d478bb9722d648a39 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 22:40:13 +0200 Subject: [PATCH 44/55] unused --- plugin/core/types.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index f94b320b2..e3b093a42 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -219,7 +219,6 @@ class Settings: show_multiline_diagnostics_highlights = cast(bool, None) show_multiline_document_highlights = cast(bool, None) show_diagnostics_in_view_status = cast(bool, None) - show_diagnostics_inline = cast(Literal["at-cursor", "none"], None) show_diagnostics_panel_on_save = cast(int, None) show_diagnostics_severity_level = cast(int, None) show_references_in_quick_panel = cast(bool, None) @@ -259,7 +258,6 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("show_code_actions_in_hover", True) r("show_diagnostics_count_in_view_status", False) r("show_diagnostics_in_view_status", True) - r("show_diagnostics_inline", "none") r("show_multiline_diagnostics_highlights", True) r("show_multiline_document_highlights", True) r("show_diagnostics_panel_on_save", 0) From e04de6389494be07506f02e5dc6484337fc9dd4d Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 22:45:09 +0200 Subject: [PATCH 45/55] escape source --- plugin/core/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 38c2b62ee..4a4a7cad1 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -879,7 +879,7 @@ def format_diagnostics_for_annotation( for diagnostic in diagnostics: message = text2html(diagnostic.get('message') or '') source = diagnostic.get('source') - line = "[{}] {}".format(source, message) if source else message + line = "[{}] {}".format(text2html(source), message) if source else message content = '
{3}
'.format( lsp_css().annotations, lsp_css().annotations_classname, css_class, line) annotations.append(content) From cbad0c2c30ac0cc6c4c894065b3f61fbcda29812 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 22:45:33 +0200 Subject: [PATCH 46/55] remove RegionProvider --- plugin/core/views.py | 10 ---------- plugin/diagnostics.py | 9 +-------- plugin/session_view.py | 1 - 3 files changed, 1 insertion(+), 19 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 4a4a7cad1..3f9ef17b7 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -41,8 +41,6 @@ from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, cast from .url import parse_uri from .workspace import is_subpath_of -from abc import ABCMeta -from abc import abstractmethod import html import itertools import linecache @@ -281,14 +279,6 @@ } -class RegionProvider(metaclass=ABCMeta): - - @classmethod - @abstractmethod - def initialize_region_keys(cls) -> None: - raise NotImplementedError() - - class InvalidUriSchemeException(Exception): def __init__(self, uri: str) -> None: self.uri = uri diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 24de6f6a9..807fbf063 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -5,7 +5,6 @@ from .core.views import diagnostic_severity from .core.views import format_diagnostics_for_annotation from .core.views import range_to_region -from .core.views import RegionProvider from itertools import chain import html import sublime @@ -313,15 +312,9 @@ def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> ] -class DiagnosticsView(RegionProvider): +class DiagnosticsView(): ANNOTATIONS_REGION_KEY = "lsp_d-annotations" - @classmethod - def initialize_region_keys(cls, view: sublime.View) -> None: - r = [sublime.Region(0, 0)] - for severity in DIAGNOSTIC_KINDS.keys(): - view.add_regions(cls._annotation_key(severity), r) - @classmethod def _annotation_key(cls, severity: DiagnosticSeverity) -> str: return '{}-{}'.format(cls.ANNOTATIONS_REGION_KEY, severity) diff --git a/plugin/session_view.py b/plugin/session_view.py index 1331fa52e..9072dc833 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -160,7 +160,6 @@ 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) - DiagnosticsView.initialize_region_keys(self.view) def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None: '''Remove all of our modifications to the view's "auto_complete_triggers"''' From c73afdd837f73854bbe1063c032ec91361c1c51f Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 22:50:06 +0200 Subject: [PATCH 47/55] unused --- plugin/core/types.py | 4 ++-- plugin/session_view.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/plugin/core/types.py b/plugin/core/types.py index e3b093a42..56ba5e5ad 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -2,8 +2,8 @@ from .file_watcher import FileWatcherEventType from .logging import debug, set_debug_logging from .protocol import TextDocumentSyncKind -from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Literal, Union, Set, Tuple -from .typing import cast, TypedDict, TypeVar +from .typing import Any, Optional, List, Dict, Generator, Callable, Iterable, Union, Set, Tuple, TypedDict, TypeVar +from .typing import cast from .url import filename_to_uri from .url import parse_uri from wcmatch.glob import BRACE diff --git a/plugin/session_view.py b/plugin/session_view.py index 9072dc833..553dda2f1 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -3,7 +3,6 @@ from .core.promise import Promise from .core.protocol import CodeLens from .core.protocol import CodeLensExtended -from .core.protocol import DiagnosticSeverity from .core.protocol import DiagnosticTag from .core.protocol import DocumentUri from .core.protocol import Notification @@ -14,7 +13,6 @@ from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY from .core.views import text_document_identifier -from .diagnostics import DiagnosticsView from .session_buffer import SessionBuffer from weakref import ref from weakref import WeakValueDictionary From 747e7430f731771c67261d40d0139ab44cd9f9f7 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 22:49:24 +0200 Subject: [PATCH 48/55] remove phantom support --- plugin/diagnostics.py | 307 +----------------------------------------- plugin/documents.py | 12 +- 2 files changed, 3 insertions(+), 316 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 807fbf063..27aa3ded3 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -1,317 +1,12 @@ from .core.protocol import Diagnostic from .core.protocol import DiagnosticSeverity -from .core.typing import Dict, List, NotRequired, Optional, Tuple, TypedDict, Union +from .core.typing import Dict, List, Tuple from .core.views import DIAGNOSTIC_KINDS from .core.views import diagnostic_severity from .core.views import format_diagnostics_for_annotation -from .core.views import range_to_region -from itertools import chain -import html import sublime -class StackItemBlank: - - __slots__ = ('diagnostic', ) - - def __init__(self, diagnostic: Diagnostic) -> None: - self.diagnostic = diagnostic - - -class StackItemDiagnostic: - - __slots__ = ('diagnostic', ) - - def __init__(self, diagnostic: Diagnostic) -> None: - self.diagnostic = diagnostic - - -class StackItemOverlap: - - __slots__ = () - - -class StackItemSpace: - - __slots__ = ('text', ) - - def __init__(self, text: str) -> None: - self.text = text - - -StackItem = Union[StackItemDiagnostic, StackItemSpace, StackItemOverlap] - -LineStack = TypedDict('LineStack', { - 'region': Optional[sublime.Region], - 'stack': List[StackItem] -}) - -StackMap = Dict[int, LineStack] - -Line = TypedDict('Line', { - 'class': str, - 'content': NotRequired[str], -}) - -DiagnosticBlock = TypedDict('DiagnosticBlock', { - 'content': List[List[Line]], - 'line': int, - 'region': sublime.Region, -}) - - -class DiagnosticLines: - - CSS = ''' - .inline-block { - display: inline-block; - } - .d_error { - color: color(var(--redish) alpha(0.85)) - } - .d_error_bg { - background-color: color(var(--redish) alpha(0.1)) - } - .d_warning { - color: color(var(--yellowish) alpha(0.85)) - } - .d_warning_bg { - background-color: color(var(--yellowish) alpha(0.1)) - } - .d_info { - color: color(var(--bluish) alpha(0.85)) - } - .d_info_bg { - background-color: color(var(--bluish) alpha(0.1)) - } - .d_hint { - color: color(var(--greenish) alpha(0.85)) - } - .d_hint_bg { - background-color: color(var(--greenish) alpha(0.1)) - } - '''.strip() - - HIGHLIGHTS = { - 1: 'error', - 2: 'warning', - 3: 'info', - 4: 'hint' - } - - COLORS = { - 'error': 'var(--redish)', - 'warning': 'var(--yellowish)', - 'info': 'var(--blueish)', - 'hint': 'var(--greenish)', - '': 'transparent', - } - - SYMBOLS = { - 'BOTTOM_LEFT': '└', - 'UPSIDE_DOWN_T': '┴', - 'MIDDLE_CROSS': '┼', - 'MIDDLE_RIGHT_CENTER': '├', - 'VERTICAL': '│', - 'HORIZONTAL': '─' - } - - def __init__(self, view: sublime.View, highlight_line_background: bool = False) -> None: - self._view = view - self._highlight_line_background = highlight_line_background - self._phantoms = sublime.PhantomSet(view, 'lsp_lines') - - def update_async(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: - sorted_diagnostics = self._sort_diagnostics(self._preprocess_diagnostic(diagnostics)) - line_stacks = self._generate_line_stacks(sorted_diagnostics) - blocks = self._generate_diagnostic_blocks(line_stacks) - phantoms = [] # type: List[sublime.Phantom] - for block in blocks: - content = self._generate_region_html(block) - phantoms.append(sublime.Phantom(block['region'], content, sublime.LAYOUT_BELOW)) - sublime.set_timeout(lambda: self._update_sync(phantoms)) - - def _update_sync(self, phantoms: List[sublime.Phantom]) -> None: - _, y_before = self._view.text_to_layout(self._view.sel()[0].begin()) - x, y_viewport_before = self._view.viewport_position() - self._phantoms.update(phantoms) - _, y_after = self._view.text_to_layout(self._view.sel()[0].begin()) - y_shift = y_after - y_before - if y_shift != 0: - new_y = y_viewport_before + y_shift - self._view.set_viewport_position((x, new_y), animate=False) - - def clear(self) -> None: - self._phantoms = sublime.PhantomSet(self._view, 'lsp-lines') - - def _generate_region_html(self, block: DiagnosticBlock) -> str: - lines = [ - ''.format(self.CSS) - ] - for line in block["content"]: - row_items = [] - for item in line: - item_class = 'd_{0}'.format(item['class']) - css_classes = ['inline-block', item_class] - if self._highlight_line_background: - css_classes.append('{0}_bg'.format(item_class)) - row_items.append('
{1}
'.format( - ' '.join(css_classes), html.escape(item.get('content', '')).replace(" ", " "))) - lines.append('
{0}
'.format(''.join(row_items))) - return '\n'.join(lines) - - def _preprocess_diagnostic(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]): - return [diagnostic[0] for diagnostic in diagnostics] - - def _sort_diagnostics(self, diagnostics: List[Diagnostic]): - return sorted(diagnostics, key=lambda x: (x['range']['start']['line'], x['range']['start']['character'])) - - def _generate_line_stacks(self, diagnostics: List[Diagnostic]) -> StackMap: - # Initialize an empty dictionary to store line stacks - line_stacks = {} # type: StackMap - # Set the initial values for the previous line number and previous column - prev_lnum = -1 - prev_col = 0 - # Iterate over the diagnostics - for diagnostic in diagnostics: - if not diagnostic['message'].strip(): - # Skip diagnostics with empty message. - continue - range_start = diagnostic['range']['start'] - current_line = range_start['line'] - current_col = range_start['character'] - # Create an empty list for the current line if it doesn't already exist in the dictionary - line_stacks.setdefault(current_line, {'region': None, 'stack': []}) - if line_stacks[current_line]['region'] is None: - region = range_to_region(diagnostic['range'], self._view) - region.b = region.a - line_stacks[current_line]['region'] = region - # Get the current stack for the current line - stack = line_stacks[current_line]['stack'] - # Check if the diagnostic is on a new line - if current_line != prev_lnum: - # If so, add an empty space to the stack - stack.append(StackItemSpace('')) - elif current_col != prev_col: - # If not on a new line but on a new column, add spacing to the stack - # Calculate the spacing by subtracting the previous column from the current column, minus 1 (to account - # for 0-based index) - spacing = (current_col - prev_col) - 1 - stack.append(StackItemSpace(' ' * spacing)) - else: - # If the diagnostic is on the same exact spot as the previous one, add an overlap to the stack - stack.append(StackItemOverlap()) - # If not blank, add the diagnostic to the stack - stack.append(StackItemDiagnostic(diagnostic)) - # Update the previous line number and column for the next iteration - prev_lnum = current_line - prev_col = current_col - return line_stacks - - def _generate_diagnostic_blocks(self, stacks: StackMap) -> List[DiagnosticBlock]: - """ - Generates the diagnostic blocks from the given stacks - """ - blocks = [] - for key, line in stacks.items(): - block = {'line': key, 'content': [], 'region': line['region']} - for i, item in enumerate(reversed(line['stack'])): - if not isinstance(item, StackItemDiagnostic): - continue - diagnostic = item.diagnostic - index = len(line['stack']) - 1 - i - left, overlap, multi = self._generate_left_side(line['stack'], index, diagnostic) - center = self._generate_center(overlap, multi, diagnostic) - for msg_line in diagnostic['message'].split('\n'): - block['content'].append(list(chain(left, center, [{ - 'content': msg_line, - 'class': self.HIGHLIGHTS[diagnostic_severity(diagnostic)] - }]))) - if overlap: - center = [ - { - 'class': self.HIGHLIGHTS[diagnostic_severity(diagnostic)], - 'content': self.SYMBOLS['VERTICAL'] - }, - {'class': '', 'content': ' '}, - ] - else: - center = [{'class': '', 'content': ' '}] - blocks.append(block) - return blocks - - def _generate_left_side( - self, line: List[StackItem], index: int, diagnostic: Diagnostic - ) -> Tuple[List[Line], bool, int]: - """ - Generates the left side of the diagnostic block for a given line - """ - left = [] - overlap = False - multi = 0 - current_index = 0 - while current_index < index: - item = line[current_index] - if isinstance(item, StackItemSpace): - if multi == 0: - left.append({'class': '', 'content': item.text}) - else: - left.append({ - 'class': self.HIGHLIGHTS[diagnostic_severity(diagnostic)], - 'content': self.SYMBOLS['HORIZONTAL'] * len(item.text) - }) - elif isinstance(item, StackItemDiagnostic): - next_item = line[current_index + 1] - if current_index + 1 != len(line) and not isinstance(next_item, StackItemOverlap): - left.append( - { - "class": self.HIGHLIGHTS[diagnostic_severity(item.diagnostic)], - "content": self.SYMBOLS['VERTICAL'], - } - ) - overlap = False - elif isinstance(item, StackItemBlank): - if multi == 0: - left.append( - { - "class": self.HIGHLIGHTS[diagnostic_severity(item.diagnostic)], - "content": self.SYMBOLS['BOTTOM_LEFT'], - } - ) - else: - left.append( - { - "class": self.HIGHLIGHTS[diagnostic_severity(item.diagnostic)], - "content": self.SYMBOLS['UPSIDE_DOWN_T'], - } - ) - multi += 1 - elif isinstance(item, StackItemOverlap): - overlap = True - current_index += 1 - return left, overlap, multi - - def _generate_center(self, overlap: bool, multi: int, diagnostic: Diagnostic) -> List[Line]: - """ - Generates the center symbol of the diagnostic block - """ - center_symbol = '' - if overlap and multi > 0: - center_symbol = self.SYMBOLS['MIDDLE_CROSS'] - elif overlap: - center_symbol = self.SYMBOLS['MIDDLE_RIGHT_CENTER'] - elif multi > 0: - center_symbol = self.SYMBOLS['UPSIDE_DOWN_T'] - else: - center_symbol = self.SYMBOLS['BOTTOM_LEFT'] - return [ - { - "class": self.HIGHLIGHTS[diagnostic_severity(diagnostic)], - "content": '{0}{1} '.format(center_symbol, self.SYMBOLS['HORIZONTAL'] * 4), - } - ] - - class DiagnosticsView(): ANNOTATIONS_REGION_KEY = "lsp_d-annotations" diff --git a/plugin/documents.py b/plugin/documents.py index fe6280ed6..a873390fe 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -44,7 +44,6 @@ from .core.views import text_document_position_params from .core.views import update_lsp_popup from .core.windows import WindowManager -from .diagnostics import DiagnosticLines from .diagnostics import DiagnosticsView from .hover import code_actions_content from .session_buffer import SessionBuffer @@ -157,7 +156,6 @@ def on_change() -> None: self._completions_task = None # type: Optional[QueryCompletionsTask] self._stored_selection = [] # type: List[sublime.Region] self._diagnostics_view = DiagnosticsView(self.view) - self._diagnostic_lines = DiagnosticLines(self.view, highlight_line_background=False) self._setup() def __del__(self) -> None: @@ -187,7 +185,6 @@ def _cleanup(self) -> None: self.view.erase_status(AbstractViewListener.TOTAL_ERRORS_AND_WARNINGS_STATUS_KEY) self._clear_highlight_regions() self._clear_session_views_async() - self._diagnostic_lines.clear() def _reset(self) -> None: # Have to do this on the main thread, since __init__ and __del__ are invoked on the main thread too @@ -292,15 +289,10 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: self._toggle_diagnostics_panel_if_needed_async() show_diagnostics = userprefs().show_diagnostics all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - if 'phantom' in show_diagnostics or 'annotation' in show_diagnostics: - for _, diagnostics in self._diagnostics_async(allow_stale=True): - all_diagnostics.extend(diagnostics) - if 'phantom' in show_diagnostics: - self._diagnostic_lines.update_async(all_diagnostics) - else: - self._diagnostic_lines.clear() self._diagnostics_view.clear_annotations_async() if 'annotation' in show_diagnostics: + for _, diagnostics in self._diagnostics_async(allow_stale=True): + all_diagnostics.extend(diagnostics) self._diagnostics_view.update_diagnostic_annotations_async(all_diagnostics) def _update_diagnostic_in_status_bar_async(self) -> None: From 4dceb879f70492840836c8ef2deee64989dbe37e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 23:00:51 +0200 Subject: [PATCH 49/55] rename --- plugin/diagnostics.py | 2 +- plugin/documents.py | 7 +++---- sublime-package.json | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 27aa3ded3..460f6bd07 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -17,7 +17,7 @@ def _annotation_key(cls, severity: DiagnosticSeverity) -> str: def __init__(self, view: sublime.View) -> None: self._view = view - def clear_annotations_async(self) -> None: + def clear_annotations(self) -> None: for severity in DIAGNOSTIC_KINDS.keys(): self._view.erase_regions(self._annotation_key(severity)) diff --git a/plugin/documents.py b/plugin/documents.py index a873390fe..b631d0649 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -287,10 +287,9 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: is_active_view = window and window.active_view() == self.view if is_active_view and self.view.change_count() == self._change_count_on_last_save: self._toggle_diagnostics_panel_if_needed_async() - show_diagnostics = userprefs().show_diagnostics - all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - self._diagnostics_view.clear_annotations_async() - if 'annotation' in show_diagnostics: + self._diagnostics_view.clear_annotations() + if 'annotation' in userprefs().show_diagnostics: + all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] for _, diagnostics in self._diagnostics_async(allow_stale=True): all_diagnostics.extend(diagnostics) self._diagnostics_view.update_diagnostic_annotations_async(all_diagnostics) diff --git a/sublime-package.json b/sublime-package.json index 9aff22234..151e8909f 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -469,7 +469,7 @@ "Show all diagnostic regions using highlights. Also see `diagnostics_highlight_style`.", "Show diagnostics as annotations anchored at the right side of the corresponding line. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as it's not possible to enforce which one is shown first then.", ], - "markdownDescription": "How to the diagnostics should be displayed." + "markdownDescription": "How to display diagnostics." }, "show_diagnostics_severity_level": { "type": "integer", From 85ce5ae7dc37bb8d9dd78e68bb0bc741b13d79b5 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 23:07:26 +0200 Subject: [PATCH 50/55] typo --- plugin/core/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 3f9ef17b7..2ef9d81d1 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -883,7 +883,7 @@ def format_diagnostic_for_panel(diagnostic: Diagnostic) -> Tuple[str, Optional[i :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) From 96c9b441ac9f60a63c1a735fd001d83ec25d5fcb Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 23:09:33 +0200 Subject: [PATCH 51/55] Revert "remove RegionProvider" This reverts commit cbad0c2c30ac0cc6c4c894065b3f61fbcda29812. --- plugin/core/views.py | 10 ++++++++++ plugin/diagnostics.py | 9 ++++++++- plugin/session_view.py | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 2ef9d81d1..147663274 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -41,6 +41,8 @@ from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, cast from .url import parse_uri from .workspace import is_subpath_of +from abc import ABCMeta +from abc import abstractmethod import html import itertools import linecache @@ -279,6 +281,14 @@ } +class RegionProvider(metaclass=ABCMeta): + + @classmethod + @abstractmethod + def initialize_region_keys(cls) -> None: + raise NotImplementedError() + + class InvalidUriSchemeException(Exception): def __init__(self, uri: str) -> None: self.uri = uri diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 460f6bd07..569853b54 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -4,12 +4,19 @@ from .core.views import DIAGNOSTIC_KINDS from .core.views import diagnostic_severity from .core.views import format_diagnostics_for_annotation +from .core.views import RegionProvider import sublime -class DiagnosticsView(): +class DiagnosticsView(RegionProvider): ANNOTATIONS_REGION_KEY = "lsp_d-annotations" + @classmethod + def initialize_region_keys(cls, view: sublime.View) -> None: + r = [sublime.Region(0, 0)] + for severity in DIAGNOSTIC_KINDS.keys(): + view.add_regions(cls._annotation_key(severity), r) + @classmethod def _annotation_key(cls, severity: DiagnosticSeverity) -> str: return '{}-{}'.format(cls.ANNOTATIONS_REGION_KEY, severity) diff --git a/plugin/session_view.py b/plugin/session_view.py index 553dda2f1..419ec1c2a 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -13,6 +13,7 @@ from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY from .core.views import text_document_identifier +from .diagnostics import DiagnosticsView from .session_buffer import SessionBuffer from weakref import ref from weakref import WeakValueDictionary @@ -158,6 +159,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) + DiagnosticsView.initialize_region_keys(self.view) def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None: '''Remove all of our modifications to the view's "auto_complete_triggers"''' From c998fc99f26ffbf4ad37b12ed8ab7167ddf3cab7 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 10 Jun 2023 23:10:50 +0200 Subject: [PATCH 52/55] not needed for now --- plugin/core/views.py | 10 ---------- plugin/diagnostics.py | 3 +-- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/plugin/core/views.py b/plugin/core/views.py index 147663274..2ef9d81d1 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -41,8 +41,6 @@ from .typing import Callable, Optional, Dict, Any, Iterable, List, Union, Tuple, cast from .url import parse_uri from .workspace import is_subpath_of -from abc import ABCMeta -from abc import abstractmethod import html import itertools import linecache @@ -281,14 +279,6 @@ } -class RegionProvider(metaclass=ABCMeta): - - @classmethod - @abstractmethod - def initialize_region_keys(cls) -> None: - raise NotImplementedError() - - class InvalidUriSchemeException(Exception): def __init__(self, uri: str) -> None: self.uri = uri diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index 569853b54..e91013e27 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -4,11 +4,10 @@ from .core.views import DIAGNOSTIC_KINDS from .core.views import diagnostic_severity from .core.views import format_diagnostics_for_annotation -from .core.views import RegionProvider import sublime -class DiagnosticsView(RegionProvider): +class DiagnosticsView(): ANNOTATIONS_REGION_KEY = "lsp_d-annotations" @classmethod From 8e6bd315ce7bba7882638723eed25ba2222bee4e Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 12 Jun 2023 21:55:44 +0200 Subject: [PATCH 53/55] rename option --- LSP.sublime-settings | 13 +++++-------- plugin/core/types.py | 4 ++-- plugin/documents.py | 2 +- sublime-package.json | 19 ++++--------------- 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index f6a69d1f4..9e01e5ff1 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -43,14 +43,6 @@ // --- Diagnostics -------------------------------------------------------------------- - // How to display diagnostics. - // Allowed options: - // - "highlight" - Show all diagnostic regions using highlights. Also see `diagnostics_highlight_style`. - // - "annotation" - Show diagnostics as annotations anchored at the right side of the corresponding line. - // When using this value it's recommended to not use the "annotation" value for the `show_code_actions` - // option as it's not possible to enforce which one is shown first then. - "show_diagnostics": ["highlight"], - // Show errors and warnings count in the status bar "show_diagnostics_count_in_view_status": false, @@ -123,6 +115,11 @@ // See also: "diagnostics_highlight_style". "show_multiline_diagnostics_highlights": true, + // Show diagnostics as annotations anchored at the right side of the corresponding line. + // 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": false, + // --- Hover popup -------------------------------------------------------------------- // The maximum number of characters (approximately) before wrapping in the popup. diff --git a/plugin/core/types.py b/plugin/core/types.py index 56ba5e5ad..cef96aa10 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -212,7 +212,7 @@ class Settings: semantic_highlighting = cast(bool, None) show_code_actions = cast(str, None) show_code_lens = cast(str, None) - show_diagnostics = cast(list, None) + show_diagnostics_annotations = cast(bool, None) show_inlay_hints = cast(bool, None) show_code_actions_in_hover = cast(bool, None) show_diagnostics_count_in_view_status = cast(bool, None) @@ -253,7 +253,7 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("semantic_highlighting", False) r("show_code_actions", "annotation") r("show_code_lens", "annotation") - r("show_diagnostics", ["highlight"]) + r("show_diagnostics_annotations", False) r("show_inlay_hints", False) r("show_code_actions_in_hover", True) r("show_diagnostics_count_in_view_status", False) diff --git a/plugin/documents.py b/plugin/documents.py index b631d0649..cfb6b999e 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -288,7 +288,7 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: if is_active_view and self.view.change_count() == self._change_count_on_last_save: self._toggle_diagnostics_panel_if_needed_async() self._diagnostics_view.clear_annotations() - if 'annotation' in userprefs().show_diagnostics: + if userprefs().show_diagnostics_annotations: all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] for _, diagnostics in self._diagnostics_async(allow_stale=True): all_diagnostics.extend(diagnostics) diff --git a/sublime-package.json b/sublime-package.json index 151e8909f..ad9ea98c7 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -455,21 +455,10 @@ "default": true, "markdownDescription": "Show the diagnostics description of the code under the cursor in status bar if available." }, - "show_diagnostics": { - "default": ["highlight"], - "type": "array", - "items": { - "enum": [ - "highlight", - "annotation", - ] - }, - "uniqueItems": true, - "markdownEnumDescriptions": [ - "Show all diagnostic regions using highlights. Also see `diagnostics_highlight_style`.", - "Show diagnostics as annotations anchored at the right side of the corresponding line. When using this value it's recommended to not use the `\"annotation\"` value for the `show_code_actions` option as it's not possible to enforce which one is shown first then.", - ], - "markdownDescription": "How to display diagnostics." + "show_diagnostics_annotations": { + "default": false, + "type": "boolean", + "markdownDescription": "Show diagnostics as annotations anchored at the right side of the corresponding line. 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_severity_level": { "type": "integer", From e4cfdcbcc9195d727447f335f14f965dee45d2c9 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Mon, 12 Jun 2023 21:56:28 +0200 Subject: [PATCH 54/55] rename class --- plugin/diagnostics.py | 2 +- plugin/documents.py | 4 ++-- plugin/session_view.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index e91013e27..f1b7a9bc8 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -7,7 +7,7 @@ import sublime -class DiagnosticsView(): +class DiagnosticsAnnotationsView(): ANNOTATIONS_REGION_KEY = "lsp_d-annotations" @classmethod diff --git a/plugin/documents.py b/plugin/documents.py index cfb6b999e..c0a83e345 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -44,7 +44,7 @@ from .core.views import text_document_position_params from .core.views import update_lsp_popup from .core.windows import WindowManager -from .diagnostics import DiagnosticsView +from .diagnostics import DiagnosticsAnnotationsView from .hover import code_actions_content from .session_buffer import SessionBuffer from .session_view import SessionView @@ -155,7 +155,7 @@ def on_change() -> None: self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._completions_task = None # type: Optional[QueryCompletionsTask] self._stored_selection = [] # type: List[sublime.Region] - self._diagnostics_view = DiagnosticsView(self.view) + self._diagnostics_view = DiagnosticsAnnotationsView(self.view) self._setup() def __del__(self) -> None: diff --git a/plugin/session_view.py b/plugin/session_view.py index 419ec1c2a..0efeecca2 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -13,7 +13,7 @@ from .core.typing import Any, Iterable, List, Tuple, Optional, Dict, Generator from .core.views import DIAGNOSTIC_SEVERITY from .core.views import text_document_identifier -from .diagnostics import DiagnosticsView +from .diagnostics import DiagnosticsAnnotationsView from .session_buffer import SessionBuffer from weakref import ref from weakref import WeakValueDictionary @@ -159,7 +159,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) - DiagnosticsView.initialize_region_keys(self.view) + DiagnosticsAnnotationsView.initialize_region_keys(self.view) def _clear_auto_complete_triggers(self, settings: sublime.Settings) -> None: '''Remove all of our modifications to the view's "auto_complete_triggers"''' From f446b6662a049a03f5a1c7de534af20f027a6878 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sun, 18 Jun 2023 17:57:33 +0200 Subject: [PATCH 55/55] switch to show_diagnostics_annotations_severity_level --- LSP.sublime-settings | 15 ++++++---- plugin/core/types.py | 4 +-- plugin/diagnostics.py | 62 +++++++++++++++++++----------------------- plugin/documents.py | 8 ------ plugin/session_view.py | 4 ++- sublime-package.json | 23 ++++++++++++---- 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/LSP.sublime-settings b/LSP.sublime-settings index 9e01e5ff1..45b2c8280 100644 --- a/LSP.sublime-settings +++ b/LSP.sublime-settings @@ -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) @@ -115,11 +125,6 @@ // See also: "diagnostics_highlight_style". "show_multiline_diagnostics_highlights": true, - // Show diagnostics as annotations anchored at the right side of the corresponding line. - // 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": false, - // --- Hover popup -------------------------------------------------------------------- // The maximum number of characters (approximately) before wrapping in the popup. diff --git a/plugin/core/types.py b/plugin/core/types.py index cef96aa10..b6ce5ea0b 100644 --- a/plugin/core/types.py +++ b/plugin/core/types.py @@ -212,9 +212,9 @@ class Settings: semantic_highlighting = cast(bool, None) show_code_actions = cast(str, None) show_code_lens = cast(str, None) - show_diagnostics_annotations = cast(bool, 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) @@ -253,9 +253,9 @@ def r(name: str, default: Union[bool, int, str, list, dict]) -> None: r("semantic_highlighting", False) r("show_code_actions", "annotation") r("show_code_lens", "annotation") - r("show_diagnostics_annotations", False) 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) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index f1b7a9bc8..6c917ff40 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -1,6 +1,7 @@ from .core.protocol import Diagnostic from .core.protocol import DiagnosticSeverity -from .core.typing import Dict, List, Tuple +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 @@ -8,42 +9,35 @@ class DiagnosticsAnnotationsView(): - ANNOTATIONS_REGION_KEY = "lsp_d-annotations" - @classmethod - def initialize_region_keys(cls, view: sublime.View) -> None: - r = [sublime.Region(0, 0)] - for severity in DIAGNOSTIC_KINDS.keys(): - view.add_regions(cls._annotation_key(severity), r) - - @classmethod - def _annotation_key(cls, severity: DiagnosticSeverity) -> str: - return '{}-{}'.format(cls.ANNOTATIONS_REGION_KEY, severity) - - def __init__(self, view: sublime.View) -> None: + def __init__(self, view: sublime.View, config_name: str) -> None: self._view = view + self._config_name = config_name - def clear_annotations(self) -> None: + def initialize_region_keys(self) -> None: + r = [sublime.Region(0, 0)] for severity in DIAGNOSTIC_KINDS.keys(): - self._view.erase_regions(self._annotation_key(severity)) + self._view.add_regions(self._annotation_region_key(severity), r) - def update_diagnostic_annotations_async(self, diagnostics: List[Tuple[Diagnostic, sublime.Region]]) -> None: - # To achieve the correct order of annotations (most severe shown first) and have the color of annotation - # match the diagnostic severity, we have to separately add regions for each severity, from most to least severe. - diagnostics_per_severity = {} # type: Dict[DiagnosticSeverity, List[Tuple[Diagnostic, sublime.Region]]] - for severity in DIAGNOSTIC_KINDS.keys(): - diagnostics_per_severity[severity] = [] - for diagnostic, region in diagnostics: - diagnostics_per_severity[diagnostic_severity(diagnostic)].append((diagnostic, region)) + 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 - for severity, diagnostics in diagnostics_per_severity.items(): - if not diagnostics: - continue - all_diagnostics = [] - regions = [] - for diagnostic, region in diagnostics: - all_diagnostics.append(diagnostic) - regions.append(region) - annotations, color = format_diagnostics_for_annotation(all_diagnostics, severity, self._view) - self._view.add_regions( - self._annotation_key(severity), regions, flags=flags, annotations=annotations, annotation_color=color) + 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)) diff --git a/plugin/documents.py b/plugin/documents.py index c0a83e345..e13130bc2 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -44,7 +44,6 @@ from .core.views import text_document_position_params from .core.views import update_lsp_popup from .core.windows import WindowManager -from .diagnostics import DiagnosticsAnnotationsView from .hover import code_actions_content from .session_buffer import SessionBuffer from .session_view import SessionView @@ -155,7 +154,6 @@ def on_change() -> None: self._registration = SettingsRegistration(view.settings(), on_change=on_change) self._completions_task = None # type: Optional[QueryCompletionsTask] self._stored_selection = [] # type: List[sublime.Region] - self._diagnostics_view = DiagnosticsAnnotationsView(self.view) self._setup() def __del__(self) -> None: @@ -287,12 +285,6 @@ def on_diagnostics_updated_async(self, is_view_visible: bool) -> None: is_active_view = window and window.active_view() == self.view if is_active_view and self.view.change_count() == self._change_count_on_last_save: self._toggle_diagnostics_panel_if_needed_async() - self._diagnostics_view.clear_annotations() - if userprefs().show_diagnostics_annotations: - all_diagnostics = [] # type: List[Tuple[Diagnostic, sublime.Region]] - for _, diagnostics in self._diagnostics_async(allow_stale=True): - all_diagnostics.extend(diagnostics) - self._diagnostics_view.update_diagnostic_annotations_async(all_diagnostics) def _update_diagnostic_in_status_bar_async(self) -> None: if userprefs().show_diagnostics_in_view_status: diff --git a/plugin/session_view.py b/plugin/session_view.py index 7e99a77dc..6a205b3aa 100644 --- a/plugin/session_view.py +++ b/plugin/session_view.py @@ -52,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) @@ -160,7 +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) - DiagnosticsAnnotationsView.initialize_region_keys(self.view) + 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"''' @@ -298,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) diff --git a/sublime-package.json b/sublime-package.json index ad9ea98c7..00cd93edb 100644 --- a/sublime-package.json +++ b/sublime-package.json @@ -455,11 +455,6 @@ "default": true, "markdownDescription": "Show the diagnostics description of the code under the cursor in status bar if available." }, - "show_diagnostics_annotations": { - "default": false, - "type": "boolean", - "markdownDescription": "Show diagnostics as annotations anchored at the right side of the corresponding line. 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_severity_level": { "type": "integer", "default": 4, @@ -467,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,