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: