Skip to content
Merged
12 changes: 11 additions & 1 deletion annotations.css
Original file line number Diff line number Diff line change
@@ -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));
}
Expand Down
2 changes: 1 addition & 1 deletion plugin/color.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 0 additions & 1 deletion plugin/core/css.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ def get_initialize_params(
"valueSet": SUPPORTED_DIAGNOSTIC_TAGS
},
"codeDescriptionSupport": True,
"markupMessageSupport": True,
"dataSupport": True
},
"selectionRange": {
Expand Down
38 changes: 24 additions & 14 deletions plugin/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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} <span class="color-muted">{text2html(source)}</span>' if source else message
content = '<body id="annotation" class="{}"><style>{}</style><div class="{}">{}</div></body>'.format(
lsp_css().annotations_classname, lsp_css().annotations, css_class, line)
content = '<body id="lsp-annotation" class="{}"><style>{}</style><div class="{}">{}</div></body>'.format(
ST_PLATFORM, lsp_css().annotations, css_class, line)
annotations.append(content)
return annotations

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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'<div class="diagnostics">{"".join(d[1] for d in sorted(diagnostics_html, key=itemgetter(0)))}</div>' 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:
Expand All @@ -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"""<a class='copy-icon' title='Copy to clipboard' href='{sublime.command_url(
'lsp_copy_text', {'text': copy_text}
)}'>⧉</a>"""
if related_infos := diagnostic.get("relatedInformation"):
info = "<br>".join(_format_diagnostic_related_info(config, info, base_dir) for info in related_infos)
content += '<hr>' + _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 += '<hr>' + 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 += '<hr>' + 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)

Expand Down
2 changes: 1 addition & 1 deletion plugin/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 4 additions & 2 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions plugin/goto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion plugin/hover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions plugin/session_buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 12 additions & 0 deletions popups.css
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
25 changes: 18 additions & 7 deletions protocol/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down