diff --git a/annotations.css b/annotations.css index 2ca83b0ee..61396d804 100644 --- a/annotations.css +++ b/annotations.css @@ -1,8 +1,18 @@ -.lsp_annotation { +body { margin: 0; border-width: 0; font-family: system; } +p { + display: inline; +} +code { + font-family: monospace; + padding: 0.05rem 0.25rem; +} +.osx code { + font-size: 0.9rem; +} .error { color: color(var(--redish) alpha(0.85)); } diff --git a/plugin/color.py b/plugin/color.py index 80f8e98f2..43b5a8aea 100644 --- a/plugin/color.py +++ b/plugin/color.py @@ -29,7 +29,7 @@ def run(self, edit: sublime.Edit, color_information: ColorInformation) -> None: def want_event(self) -> bool: return False - def _handle_response_async(self, response: list[ColorPresentation]) -> None: + def _handle_response_async(self, response: list[ColorPresentation] | None) -> None: if not response: return window = self.view.window() diff --git a/plugin/core/css.py b/plugin/core/css.py index 052057984..669241592 100644 --- a/plugin/core/css.py +++ b/plugin/core/css.py @@ -13,7 +13,6 @@ def __init__(self) -> None: self.sheets_classname = "lsp_sheet" self.inlay_hints = sublime.load_resource("Packages/LSP/inlay_hints.css") self.annotations = sublime.load_resource("Packages/LSP/annotations.css") - self.annotations_classname = "lsp_annotation" g_css: CSS | None = None diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index c6648508e..6e8056385 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -915,7 +915,7 @@ class CodeLensResolveResponse(TypedDict): class ColorPresentationResponse(TypedDict): method: Literal['textDocument/colorPresentation'] - result: List['ColorPresentation'] + result: Union[List['ColorPresentation'], None] class CompletionResponse(TypedDict): @@ -950,7 +950,7 @@ class DiagnosticRefreshResponse(TypedDict): class DocumentColorResponse(TypedDict): method: Literal['textDocument/documentColor'] - result: List['ColorInformation'] + result: Union[List['ColorInformation'], None] class DocumentDiagnosticResponse(TypedDict): diff --git a/plugin/core/sessions.py b/plugin/core/sessions.py index 74816466a..18dac4e4a 100644 --- a/plugin/core/sessions.py +++ b/plugin/core/sessions.py @@ -471,6 +471,7 @@ def get_initialize_params( "valueSet": SUPPORTED_DIAGNOSTIC_TAGS }, "codeDescriptionSupport": True, + "markupMessageSupport": True, "dataSupport": True }, "selectionRange": { diff --git a/plugin/core/views.py b/plugin/core/views.py index ac4c3021a..3b8dbec00 100644 --- a/plugin/core/views.py +++ b/plugin/core/views.py @@ -42,6 +42,7 @@ from .constants import CODE_ACTION_KINDS from .constants import MARKO_MD_PARSER_VERSION from .constants import ST_CACHE_PATH +from .constants import ST_PLATFORM from .constants import ST_STORAGE_PATH from .constants import SUBLIME_KIND_SCOPES from .constants import SublimeKind @@ -740,14 +741,14 @@ def diagnostic_icon(severity: DiagnosticSeverity) -> str: return "" if severity == DiagnosticSeverity.Hint else userprefs().diagnostics_gutter_marker -def format_diagnostics_for_annotation(diagnostics: list[Diagnostic], css_class: str) -> list[str]: +def format_diagnostics_for_annotation(view: sublime.View, diagnostics: list[Diagnostic], css_class: str) -> list[str]: annotations = [] for diagnostic in diagnostics: - message = text2html(diagnostic.get('message') or '') + message = _format_diagnostic_message(view, diagnostic['message']) source = diagnostic.get('source') line = f'{message} {text2html(source)}' if source else message - content = '
{}
'.format( - lsp_css().annotations_classname, lsp_css().annotations, css_class, line) + content = '
{}
'.format( + ST_PLATFORM, lsp_css().annotations, css_class, line) annotations.append(content) return annotations @@ -763,7 +764,8 @@ def format_diagnostic_for_panel(diagnostic: Diagnostic) -> tuple[str, int | None using the information given. """ formatted, code, href = diagnostic_source_and_code(diagnostic) - lines = diagnostic["message"].splitlines() or [""] + message = diagnostic['message'] + lines = (message['value'] if isinstance(message, dict) else message).splitlines() or [""] result = " {:>4}:{:<4}{:<8}{}".format( diagnostic["range"]["start"]["line"] + 1, diagnostic["range"]["start"]["character"] + 1, @@ -834,6 +836,10 @@ def is_location_href(href: str) -> bool: return href.startswith("location:") +def _format_diagnostic_message(view: sublime.View, message: str | MarkupContent) -> str: + return minihtml(view, message, FORMAT_MARKUP_CONTENT) if isinstance(message, dict) else text2html(message) + + def _format_diagnostic_related_info( config: ClientConfig, info: DiagnosticRelatedInformation, @@ -880,7 +886,7 @@ def lightbulb_html(color: str, star: bool) -> str: def format_diagnostics_for_html( - version: int, + view: sublime.View, diagnostics_by_config: Sequence[tuple[SessionBufferProtocol, Sequence[Diagnostic]]], code_actions_by_config: dict[str, list[Command | CodeAction]], lightbulb_color: str, @@ -895,21 +901,23 @@ def format_diagnostics_for_html( action for action in actions_for_config if diagnostic in action.get('diagnostics', []) ] diagnostic_html = format_diagnostic_for_html( - sb.session.config, version, diagnostic, code_actions, lightbulb_color, base_dir) + view, sb.session.config, diagnostic, code_actions, lightbulb_color, base_dir) diagnostics_html.append((diagnostic_severity(diagnostic), diagnostic_html)) return f'
{"".join(d[1] for d in sorted(diagnostics_html, key=itemgetter(0)))}
' if \ diagnostics_html else '' def format_diagnostic_for_html( + view: sublime.View, config: ClientConfig, - version: int, diagnostic: Diagnostic, code_actions: list[Command | CodeAction], lightbulb_color: str, base_dir: str | None = None ) -> str: - content = _html_element('span', diagnostic["message"]) + message = diagnostic['message'] + raw_message = message['value'] if isinstance(message, dict) else message + content = _format_diagnostic_message(view, message) code = diagnostic.get("code") source = diagnostic.get("source") if source or code is not None: @@ -923,17 +931,19 @@ def format_diagnostic_for_html( else: meta_info += f'({text2html(str(code))})' content += " " + _html_element("span", meta_info, class_name="color-muted", escape=False) - copy_text = f"{diagnostic['message']} {f'({source})' if source else ''}".strip().replace(' ', ' ') + copy_text = f"{raw_message} {f'({source})' if source else ''}".strip().replace(' ', ' ') content += f"""""" if related_infos := diagnostic.get("relatedInformation"): info = "
".join(_format_diagnostic_related_info(config, info, base_dir) for info in related_infos) content += '
' + _html_element("div", info, escape=False) - for code_action in sorted(code_actions, key=lambda a: a.get('isPreferred', False), reverse=True): - icon = lightbulb_html(lightbulb_color, code_action.get('isPreferred', False)) - code_action_uri = encode_code_action_uri(config.name, version, code_action) - content += '
' + icon + make_link(code_action_uri, code_action['title'], tooltip='Run Code Action') + if code_actions: + version = view.change_count() + for code_action in sorted(code_actions, key=lambda a: a.get('isPreferred', False), reverse=True): + icon = lightbulb_html(lightbulb_color, code_action.get('isPreferred', False)) + code_action_uri = encode_code_action_uri(config.name, version, code_action) + content += '
' + icon + make_link(code_action_uri, code_action['title'], tooltip='Run Code Action') severity_class = DIAGNOSTIC_STYLES[diagnostic_severity(diagnostic)].css_class return html_wrapper(content, class_name=severity_class) diff --git a/plugin/diagnostics.py b/plugin/diagnostics.py index e2884882a..fa2a98aed 100644 --- a/plugin/diagnostics.py +++ b/plugin/diagnostics.py @@ -142,7 +142,7 @@ def draw(self, diagnostics: list[tuple[Diagnostic, sublime.Region]]) -> None: matching_diagnostics[0].append(diagnostic) matching_diagnostics[1].append(region) css_class = DIAGNOSTIC_STYLES[severity].css_class - annotations = format_diagnostics_for_annotation(matching_diagnostics[0], css_class) + annotations = format_diagnostics_for_annotation(self._view, matching_diagnostics[0], css_class) color = self._severity_colors[severity] self._view.add_regions( self._annotation_region_key(severity), matching_diagnostics[1], flags=flags, diff --git a/plugin/documents.py b/plugin/documents.py index d0871dc4e..88229a100 100644 --- a/plugin/documents.py +++ b/plugin/documents.py @@ -360,7 +360,9 @@ def _update_diagnostic_in_status_bar_async(self) -> None: if session_buffer_diagnostics: for _, diagnostics in session_buffer_diagnostics: if diag := next(iter(diagnostics), None): - self.view.set_status(self.ACTIVE_DIAGNOSTIC, diag["message"]) + message = diag['message'] + msg = message['value'] if isinstance(message, dict) else message + self.view.set_status(self.ACTIVE_DIAGNOSTIC, msg) return self.view.erase_status(self.ACTIVE_DIAGNOSTIC) @@ -590,7 +592,7 @@ def _on_code_actions_for_hover_gutter_async( base_dir = self._manager.get_project_path(filename) \ if self._manager and (filename := self.view.file_name()) else None content = format_diagnostics_for_html( - self.view.change_count(), diagnostics, dict(code_actions), self.lightbulb_color, base_dir) + self.view, diagnostics, dict(code_actions), self.lightbulb_color, base_dir) show_lsp_popup( self.view, content, diff --git a/plugin/goto.py b/plugin/goto.py index 3b377d4fd..84b4e5061 100644 --- a/plugin/goto.py +++ b/plugin/goto.py @@ -301,9 +301,10 @@ def list_items(self) -> tuple[list[sublime.ListInputItem], int]: caret_pos = region.b if self._preview and (region := first_selection_region(self._preview)) is not None else 0 for index, diagnostic_data in enumerate(self.diagnostics): diagnostic = diagnostic_data['diagnostic'] - message = diagnostic['message'] or '…' + message = diagnostic['message'] + raw_message = (message['value'] if isinstance(message, dict) else message) or '…' severity = diagnostic_severity(diagnostic) - text = f"{'_EWIH'[severity]}: {message.splitlines()[0]}" + text = f"{'_EWIH'[severity]}: {raw_message.splitlines()[0]}" value = cast(dict, diagnostic_data) code = str(diagnostic.get('code', '')) kind = DIAGNOSTIC_KINDS[severity] diff --git a/plugin/hover.py b/plugin/hover.py index 050719419..c5a6b32fe 100644 --- a/plugin/hover.py +++ b/plugin/hover.py @@ -258,7 +258,7 @@ def _show_hover(self, listener: AbstractViewListener, point: int, only_diagnosti prefs = userprefs() if only_diagnostics or prefs.show_diagnostics_in_hover: contents += format_diagnostics_for_html( - self.view.change_count(), + self.view, self._diagnostics_by_config, self._actions_by_config, listener.lightbulb_color, diff --git a/plugin/session_buffer.py b/plugin/session_buffer.py index 6301a858a..24836b1f5 100644 --- a/plugin/session_buffer.py +++ b/plugin/session_buffer.py @@ -567,9 +567,8 @@ def _do_color_boxes_async(self, view: sublime.View, version: int) -> None: self._if_view_unchanged(self._on_color_boxes_async, version) ) - def _on_color_boxes_async(self, view: sublime.View, response: list[ColorInformation]) -> None: - # None-check guards against spec violation from vue server - https://github.com/volarjs/volar.js/issues/301. - phantoms = [] if response is None else [lsp_color_to_phantom(view, color_info) for color_info in response] # pyright: ignore[reportUnnecessaryComparison] + def _on_color_boxes_async(self, view: sublime.View, response: list[ColorInformation] | None) -> None: + phantoms = [lsp_color_to_phantom(view, color_info) for color_info in response] if response else [] sublime.set_timeout(lambda: self._color_phantoms.update(phantoms)) def clear_color_boxes_async(self) -> None: diff --git a/popups.css b/popups.css index 7b37a79d9..b07068ecd 100644 --- a/popups.css +++ b/popups.css @@ -73,6 +73,18 @@ .lsp_popup .m-0 { margin: 0; } +.diagnostics p { + display: inline; +} +.diagnostics code, +.diagnostics code.highlight { + background-color: color(white alpha(0.1)); + border-color: transparent; +} +html.light .diagnostics code, +html.light .diagnostics code.highlight { + background-color: color(black alpha(0.1)); +} .color-muted { color: color(var(--foreground) alpha(0.6)); } diff --git a/protocol/__init__.py b/protocol/__init__.py index b49fab257..11f5c4742 100644 --- a/protocol/__init__.py +++ b/protocol/__init__.py @@ -1,6 +1,6 @@ # ruff: noqa: E501, UP006, UP007 # Code generated. DO NOT EDIT. -# LSP v3.17.0 +# LSP v3.18.0 from __future__ import annotations @@ -3706,16 +3706,16 @@ class DocumentOnTypeFormattingRegistrationOptions(TypedDict): class RenameParams(TypedDict): """The parameters of a {@link RenameRequest}.""" - textDocument: 'TextDocumentIdentifier' - """The document to rename.""" - position: 'Position' - """The position at which this request was sent.""" newName: str """ The new name of the symbol. If the given name is not valid the request must return a {@link ResponseError} with an appropriate message set. """ + textDocument: 'TextDocumentIdentifier' + """The text document.""" + position: 'Position' + """The position inside the text document.""" workDoneToken: NotRequired['ProgressToken'] """An optional token that a server can use to report work done progress.""" @@ -5024,8 +5024,12 @@ class Diagnostic(TypedDict): diagnostic, e.g. 'typescript' or 'super lint'. It usually appears in the user interface. """ - message: str - """The diagnostic's message. It usually appears in the user interface""" + message: Union[str, 'MarkupContent'] + """ + The diagnostic's message. It usually appears in the user interface. + + @since 3.18.0 - support for `MarkupContent`. This is guarded by the client capability `textDocument.diagnostic.markupMessageSupport`. + """ tags: NotRequired[List['DiagnosticTag']] """ Additional metadata about the diagnostic. @@ -7554,6 +7558,13 @@ class DiagnosticClientCapabilities(TypedDict): @since 3.16.0 """ + markupMessageSupport: NotRequired[bool] + """ + Whether the client supports `MarkupContent` in diagnostic messages. + + @since 3.18.0 + @proposed + """ dataSupport: NotRequired[bool] """ Whether code action supports the `data` property which is diff --git a/tests/test_views.py b/tests/test_views.py index 0998145a9..8545f7f75 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -398,8 +398,8 @@ def test_format_diagnostic_for_html(self) -> None: client_config = make_stdio_test_config() # They should result in the same minihtml. self.assertEqual( - format_diagnostic_for_html(client_config, 0, diagnostic1, [], '#ffffff', "/foo/bar"), - format_diagnostic_for_html(client_config, 0, diagnostic2, [], '#ffffff', "/foo/bar") + format_diagnostic_for_html(self.view, client_config, diagnostic1, [], '#ffffff', "/foo/bar"), + format_diagnostic_for_html(self.view, client_config, diagnostic2, [], '#ffffff', "/foo/bar") ) def test_escaped_newline_in_markdown(self) -> None: