Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inlay hint #2018

Merged
merged 68 commits into from
Aug 20, 2022
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
a9cf8b7
add inlay hints
predrag-codetribe Aug 11, 2022
e5b8147
use system font family for inlay hints
predrag-codetribe Aug 11, 2022
375ff3a
handle inlay hint text edits
predrag-codetribe Aug 12, 2022
48a65f2
handle inlay hint commands
predrag-codetribe Aug 12, 2022
0ba4906
mark unused event with _
predrag-codetribe Aug 12, 2022
4e43386
add resolvable properties
predrag-codetribe Aug 12, 2022
7b691d2
fix pyflake
predrag-codetribe Aug 12, 2022
90fb030
use command instead of label.command
predrag-codetribe Aug 12, 2022
21fb6e0
add show_inlay_hints setting, and ability to toggle that setting thro…
predrag-codetribe Aug 12, 2022
d60c157
Merge branch 'main' into inlay-hint
predrag-codetribe Aug 12, 2022
ea953eb
haha cannot name event "_event"
predrag-codetribe Aug 12, 2022
83b152e
fix use monospace instead of system
predrag-codetribe Aug 12, 2022
a8f30ed
implement workspace/inlayHint/refresh
predrag-codetribe Aug 12, 2022
fcb6ec5
run inlay hints when focusing the view
predrag-codetribe Aug 12, 2022
730e47d
remove phantom when they are clicked if they have textEdits
predrag-codetribe Aug 12, 2022
f6932b8
make mypy and pyflake happy
predrag-codetribe Aug 12, 2022
e1979d8
use label.command instead of command
predragnikolic Aug 12, 2022
e9dd64c
replace LspToggleInlayHintsCommand with LspToggleSettingCommand
predragnikolic Aug 12, 2022
6e5f09c
Revert "replace LspToggleInlayHintsCommand with LspToggleSettingCommand"
predragnikolic Aug 13, 2022
16d1b32
Do not re-initialize server when toggling inlay hints
predragnikolic Aug 13, 2022
d249241
remove handeling inlay hints commands in this PR
predragnikolic Aug 13, 2022
c5a6469
only make inlay hints that have text edits clickable
predragnikolic Aug 13, 2022
d833b4c
use the user font_face for the inlay-hint phantom font-family
predragnikolic Aug 13, 2022
a9e268b
correctly handle label commands
predragnikolic Aug 14, 2022
3d3d6b1
Update plugin/core/protocol.py
predragnikolic Aug 14, 2022
595471f
sort imports
predragnikolic Aug 14, 2022
c88bbd2
remove debug code
predragnikolic Aug 14, 2022
f2528b4
sort imports
predragnikolic Aug 14, 2022
113fbb9
remove check as a guard for potential preformace bennefit
predragnikolic Aug 14, 2022
3e81846
Merge branch 'inlay-hint' of github.com:sublimelsp/LSP into inlay-hint
predragnikolic Aug 14, 2022
ae17344
Update plugin/session_buffer.py
predragnikolic Aug 14, 2022
0566c0f
add import
predragnikolic Aug 14, 2022
0014a4d
remove params in m_workspace_inlayHint_refresh
predragnikolic Aug 14, 2022
fcf7212
rename SHOW_INLAY_HINTS to show_inlay_hints
predragnikolic Aug 14, 2022
f4e38fa
remove inlay hints on_before_remove
predragnikolic Aug 14, 2022
719bbbc
Revert "remove params in m_workspace_inlayHint_refresh"
predragnikolic Aug 14, 2022
ad6814e
drop LspToggleInlayHintsCommand to not block the PR
predragnikolic Aug 14, 2022
b67dd54
drop commands for lsp_toggle_inlay_hints
predragnikolic Aug 14, 2022
77c6bfb
document that location is not supported
predragnikolic Aug 15, 2022
75bda39
remove elif for an if
predragnikolic Aug 15, 2022
d7c7b0b
be more descriptive about the setting description
predragnikolic Aug 15, 2022
f07cc30
use inline comments instead of docs comments
predragnikolic Aug 15, 2022
d370af9
strip html tags for tooltips
predragnikolic Aug 15, 2022
ea1e4f2
use total=False for types and mark not required fields with comment
predragnikolic Aug 15, 2022
8ae7630
add not required for paddingLeft
predragnikolic Aug 15, 2022
405de83
rename handle_inlay_hint_command to handle_label_part_command
predragnikolic Aug 15, 2022
3779a13
remove html parser, instead show plain markdown from server
predragnikolic Aug 15, 2022
f57acee
move phantom set from session view to session buffer to avoid potenti…
predragnikolic Aug 15, 2022
ffb0280
use a more descriptive sentence for the show_inlay_hints setting
predragnikolic Aug 15, 2022
8269fbf
use type: ignore :(
predragnikolic Aug 15, 2022
b5370ec
remove unecessary line comment
predragnikolic Aug 15, 2022
99fdf69
be consistent add textDocument/inlayHint comment line
predragnikolic Aug 15, 2022
3fb3027
revert back the line that was accidentaly deleted when migrating the …
predragnikolic Aug 15, 2022
56f4f6e
remove all inlay hints on_before_remove
predragnikolic Aug 15, 2022
37857bb
rename present_inlay_hints_async to present_inlay_hints
predragnikolic Aug 15, 2022
9992e23
handle None response
predragnikolic Aug 15, 2022
f38e620
move self._inlay_hints_phantom_set before self._check_did_open(view)
predragnikolic Aug 15, 2022
b54f24c
move command's in the respected if is_clickable block
predragnikolic Aug 15, 2022
e80903e
remove present_inlay_hints_async from interface
predragnikolic Aug 15, 2022
cd5b946
remove unrelevant comment
predragnikolic Aug 15, 2022
e63422e
move inlay_hint_to_phantom before get_inlay_hint_html
predragnikolic Aug 15, 2022
3823e3b
Revert "remove all inlay hints on_before_remove"
predragnikolic Aug 15, 2022
2523ed8
remove inlay hints if there are no session_views
predragnikolic Aug 15, 2022
9f22d31
now the remove_all_inlay_hints can go away
predragnikolic Aug 15, 2022
343c6eb
Merge branch 'main' into inlay-hint
predragnikolic Aug 19, 2022
53beedc
remove margin from phantoms even if inlay hints have paddingLeft or p…
predragnikolic Aug 20, 2022
653f9be
change default value to false
predragnikolic Aug 20, 2022
804f1c0
be more explicit about the setting description
predragnikolic Aug 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Default.sublime-commands
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,9 @@
{
"caption": "LSP: Find References",
"command": "lsp_symbol_references"
},
{
"caption": "LSP: Toggle Inlay Hints",
"command": "lsp_toggle_inlay_hints"
}
]
14 changes: 14 additions & 0 deletions Default.sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,20 @@
// }
// ]
// },
// Toggle inlay hints.
// {
// "command": "lsp_toggle_inlay_hints",
// "keys": [
// "UNBOUND"
// ],
// "context": [
// {
// "key": "lsp.session_with_capability",
// "operator": "equal",
// "operand": "inlayHintProvider"
// }
// ]
// },
// Internal key-binding
{
"keys": [
Expand Down
3 changes: 3 additions & 0 deletions LSP.sublime-settings
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@
// "phantom" - show a phantom on the top when code actions are available
"show_code_lens": "annotation",

// Show inlay hints
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved
"show_inlay_hints": true,
rwols marked this conversation as resolved.
Show resolved Hide resolved

// Show code actions in hover popup if available
"show_code_actions_in_hover": true,

Expand Down
2 changes: 2 additions & 0 deletions boot.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
from .plugin.goto import LspSymbolTypeDefinitionCommand
from .plugin.goto_diagnostic import LspGotoDiagnosticCommand
from .plugin.hover import LspHoverCommand
from .plugin.inlay_hint import LspInlayHintClickCommand
from .plugin.inlay_hint import LspToggleInlayHintsCommand
from .plugin.panels import LspShowDiagnosticsPanelCommand
from .plugin.panels import LspToggleServerPanelCommand
from .plugin.references import LspSymbolReferencesCommand
Expand Down
39 changes: 39 additions & 0 deletions plugin/core/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,37 @@ class SemanticTokenModifiers:
'watchers': List[FileSystemWatcher],
}, total=True)

InlayHintParams = TypedDict('InlayHintParams', {
'textDocument': TextDocumentIdentifier,
'range': RangeLsp,
}, total=True)

InlayHintLabelPart = TypedDict('InlayHintLabelPart', {
'value': str,
'tooltip': Union[str, MarkupContent],
'location': Location,
'command': Command
rchl marked this conversation as resolved.
Show resolved Hide resolved
}, total=False)


class InlayHintKind:
Type = 1
Parameter = 1
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved


InlayHint = TypedDict('InlayHint', {
'position': Position,
'label': Union[str, List[InlayHintLabelPart]],
'kind': Optional[int],
'textEdits': Optional[List[TextEdit]],
'tooltip': Optional[Union[str, MarkupContent]],
'paddingLeft': Optional[bool],
'paddingRight': Optional[bool],
'data': Optional[Any]
}, total=True)

InlayHintResponse = Union[List[InlayHint], None]

WatchKind = int
WatchKindCreate = 1
WatchKindChange = 2
Expand Down Expand Up @@ -452,6 +483,14 @@ def resolveCompletionItem(cls, params: CompletionItem, view: sublime.View) -> 'R
def resolveDocumentLink(cls, params: DocumentLink, view: sublime.View) -> 'Request':
return Request("documentLink/resolve", params, view)

@classmethod
def inlayHint(cls, params: InlayHintParams, view: sublime.View) -> 'Request':
return Request('textDocument/inlayHint', params, view)

@classmethod
def resolveInlayHint(cls, params: InlayHint, view: sublime.View) -> 'Request':
return Request('inlayHint/resolve', params, view)

@classmethod
def shutdown(cls) -> 'Request':
return Request("shutdown")
Expand Down
25 changes: 25 additions & 0 deletions plugin/core/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,13 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
"codeLens": {
"dynamicRegistration": True
},
"inlayHint": {
"dynamicRegistration": True,
"resolveSupport": {
# not sure if I can just put "command" because the command is nested in the label
"properties": ["textEdits", "command"]
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved
}
},
"semanticTokens": {
"dynamicRegistration": True,
"requests": {
Expand Down Expand Up @@ -391,6 +398,9 @@ def get_initialize_params(variables: Dict[str, str], workspace_folders: List[Wor
"codeLens": {
"refreshSupport": True
},
"inlayHint": {
"refreshSupport": True
},
"semanticTokens": {
"refreshSupport": True
}
Expand Down Expand Up @@ -487,6 +497,12 @@ def get_resolved_code_lenses_for_region(self, region: sublime.Region) -> Generat
def start_code_lenses_async(self) -> None:
...

def present_inlay_hints_async(self, phantoms: List[sublime.Phantom]) -> None:
...

def remove_inlay_hint_phantom(self, phantom_uuid: str) -> None:
...


class SessionBufferProtocol(Protocol):

Expand Down Expand Up @@ -542,6 +558,9 @@ def set_semantic_tokens_pending_refresh(self, needs_refresh: bool = True) -> Non
def get_semantic_tokens(self) -> List[Any]:
...

def do_inlay_hints_async(self, view: sublime.View) -> None:
rchl marked this conversation as resolved.
Show resolved Hide resolved
...


class AbstractViewListener(metaclass=ABCMeta):

Expand Down Expand Up @@ -1622,6 +1641,12 @@ def m_workspace_semanticTokens_refresh(self, params: Any, request_id: Any) -> No
else:
sv.session_buffer.set_semantic_tokens_pending_refresh()

def m_workspace_inlayHint_refresh(self, params: None, request_id: Any) -> None:
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved
"""handles the workspace/inlayHint/refresh request"""
for sv in self.session_views_async():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of requesting new inlay hints for all of the views, maybe it would be better to use the logic that I used at the semantic tokens refresh. Meaning that only requests for the views that are currently visible ( = selected) are made, and for the other views a variable is set that a request should be made as soon as the view gets activated. See also the description of the refresh requests in the lsp specs, that say clients can delay request for non-visible tabs. The reason I did it this way was because one server (I think it was rust-analyzer) would send a refresh request after each text change. And if someone has for example 10 tabs in the window, but only one of it active, then we send 9 useless requests all the time and do the inlay hints processing and phantom updates even for views that are not visible currently.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think that I would work on optimizing this.

Inlay hints doesn't seem like an expensive request (like semantic token is).
If this turns to be a problem only than I would optimize and add more code.

Here is one implementation detail of how inlay hints currently function.
If the server implements the workspace/inlayHint/refresh request,
we will request inlay hints for each session view.

So the client will refresh the inlay hints each time the server desires,
as an end result we should not end up with stale inlay hints in one view ever.

Even if the server supports or does not support the workspace/inlayHint/refresh request,
there is this peace of code that will request inlay hints as soon as the view gets active:

https://github.com/sublimelsp/LSP/pull/2018/files#diff-982c110867d9809c978a5597e4da66ce7538b327675a66b8f27214413b7c0ec1R345

This logic is redundant for servers that do support workspace/inlayHint/refresh request,
but I would say that it is necessary for servers for that do not support workspace/inlayHint/refresh. (just in case to update stale inlay hints as soon as the user focuses one view)

But as I said I would not add any code for optimizing this until it turns out to be a perf issue.

sv.session_buffer.do_inlay_hints_async(sv.view)
self.send_response(Response(request_id, None))

def m_textDocument_publishDiagnostics(self, params: Any) -> None:
"""handles the textDocument/publishDiagnostics notification"""
uri = params["uri"]
Expand Down
2 changes: 2 additions & 0 deletions plugin/core/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class Settings:
semantic_highlighting = None # type: bool
show_code_actions = None # type: str
show_code_lens = None # type: str
show_inlay_hints = None # type: bool
show_code_actions_in_hover = None # type: bool
show_diagnostics_count_in_view_status = None # type: bool
show_multiline_diagnostics_highlights = None # type: bool
Expand Down Expand Up @@ -243,6 +244,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_inlay_hints", True)
rwols marked this conversation as resolved.
Show resolved Hide resolved
r("show_code_actions_in_hover", True)
r("show_diagnostics_count_in_view_status", False)
r("show_diagnostics_in_view_status", True)
Expand Down
1 change: 1 addition & 0 deletions plugin/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ def on_activated_async(self) -> None:
if sb.semantic_tokens.needs_refresh:
sb.semantic_tokens.needs_refresh = False
sb.do_semantic_tokens_async(self.view)
sb.do_inlay_hints_async(self.view)

def on_selection_modified_async(self) -> None:
different, current_region = self._update_stored_region_async()
Expand Down
10 changes: 5 additions & 5 deletions plugin/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def format_document(text_command: LspTextCommand) -> Promise[FormatResponse]:
return Promise.resolve(None)


def apply_response_to_view(response: Optional[List[TextEdit]], view: sublime.View) -> None:
def apply_text_edits_to_view(response: Optional[List[TextEdit]], view: sublime.View) -> None:
edits = list(parse_text_edit(change) for change in response) if response else []
view.run_command('lsp_apply_document_edit', {'changes': edits})

Expand Down Expand Up @@ -66,7 +66,7 @@ def _will_save_wait_until_async(self, session: Session) -> None:

def _on_response(self, response: Any) -> None:
if response and not self._cancelled:
apply_response_to_view(response, self._task_runner.view)
apply_text_edits_to_view(response, self._task_runner.view)
sublime.set_timeout_async(self._handle_next_session_async)


Expand All @@ -85,7 +85,7 @@ def run_async(self) -> None:

def _on_response(self, response: FormatResponse) -> None:
if response and not isinstance(response, Error) and not self._cancelled:
apply_response_to_view(response, self._task_runner.view)
apply_text_edits_to_view(response, self._task_runner.view)
sublime.set_timeout_async(self._on_complete)


Expand All @@ -105,7 +105,7 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None:

def on_result(self, result: FormatResponse) -> None:
if result and not isinstance(result, Error):
apply_response_to_view(result, self.view)
apply_text_edits_to_view(result, self.view)


class LspFormatDocumentRangeCommand(LspTextCommand):
Expand All @@ -125,4 +125,4 @@ def run(self, edit: sublime.Edit, event: Optional[dict] = None) -> None:
selection = first_selection_region(self.view)
if session and selection is not None:
req = text_document_range_formatting(self.view, selection)
session.send_request(req, lambda response: apply_response_to_view(response, self.view))
session.send_request(req, lambda response: apply_text_edits_to_view(response, self.view))
128 changes: 128 additions & 0 deletions plugin/inlay_hint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from .core.protocol import InlayHintLabelPart, MarkupContent, Point, InlayHint, Request
from .core.registry import LspTextCommand
from .core.typing import List, Optional, Union
from .core.views import FORMAT_MARKUP_CONTENT, point_to_offset, minihtml
from .formatting import apply_text_edits_to_view
import html
import sublime
import uuid


class LspToggleInlayHintsCommand(LspTextCommand):
capability = 'inlayHintProvider'

def run(self, _edit: sublime.Edit, _event: Optional[dict] = None) -> None:
settings = sublime.load_settings("LSP.sublime-settings")
show_inlay_hints = settings.get("show_inlay_hints", False)
settings.set("show_inlay_hints", not show_inlay_hints)
sublime.save_settings("LSP.sublime-settings")


class LspInlayHintClickCommand(LspTextCommand):
capability = 'inlayHintProvider'

def run(self, _edit: sublime.Edit, session_name: str, inlay_hint: InlayHint, phantom_uuid: str,
event: Optional[dict] = None) -> None:
session = self.session_by_name(session_name, 'inlayHintProvider')
if session and session.has_capability('inlayHintProvider.resolveProvider'):
request = Request.resolveInlayHint(inlay_hint, self.view)
session.send_request_async(request, lambda response: self.handle(session_name, response, phantom_uuid))
return
self.handle(session_name, inlay_hint, phantom_uuid)

def handle(self, session_name: str, inlay_hint: InlayHint, phantom_uuid: str) -> None:
self.handle_inlay_hint_text_edits(session_name, inlay_hint, phantom_uuid)
self.handle_inlay_hint_command(session_name, inlay_hint)

def handle_inlay_hint_text_edits(self, session_name: str, inlay_hint: InlayHint, phantom_uuid: str) -> None:
session = self.session_by_name(session_name, 'inlayHintProvider')
if not session:
return
text_edits = inlay_hint.get('textEdits')
if not text_edits:
return
for sv in session.session_views_async():
sv.remove_inlay_hint_phantom(phantom_uuid)
apply_text_edits_to_view(text_edits, self.view)

def handle_inlay_hint_command(self, session_name: str, inlay_hint: InlayHint) -> None:
label_parts = inlay_hint.get('label')
if not isinstance(label_parts, list):
return
for label_part in label_parts:
command = label_part.get('command')
if not command:
continue
args = {
"session_name": session_name,
"command_name": command["command"],
"command_args": command["arguments"]
}
self.view.run_command("lsp_execute", args)


INLAY_HINT_HTML = """
<body id="lsp-inlay-hint">
<style>
.inlay-hint {{
background-color: color(var(--foreground) alpha(0.08));
border-radius: 4px;
margin-left: {margin_left};
margin-right: {margin_right};
padding: 0.05em 4px;
font-size: 0.9em;
font-family: monospace;
}}

.inlay-hint a {{
color: color(var(--foreground) alpha(0.6));
text-decoration: none;
}}
</style>
<div class="inlay-hint" title="{tooltip}">
<a href="{command}">{label}</a>
</div>
</body>
"""


def format_inlay_hint_tooltip(view: sublime.View, tooltip: Optional[Union[str, MarkupContent]]) -> str:
if isinstance(tooltip, str):
return html.escape(tooltip)
elif isinstance(tooltip, dict): # MarkupContent
return minihtml(view, tooltip, allowed_formats=FORMAT_MARKUP_CONTENT)
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved
else:
return ""
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved


def format_inlay_hint_label(view: sublime.View, label: Union[str, List[InlayHintLabelPart]]) -> str:
if isinstance(label, str):
return html.escape(label)
else:
return "".join("<div title=\"{tooltip}\">{value}</div>".format(
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved
tooltip=format_inlay_hint_tooltip(view, label_part.get("tooltip")),
value=label_part.get("value")
) for label_part in label)
predragnikolic marked this conversation as resolved.
Show resolved Hide resolved


def inlay_hint_to_phantom(view: sublime.View, inlay_hint: InlayHint, session_name: str) -> sublime.Phantom:
region = sublime.Region(point_to_offset(Point.from_lsp(inlay_hint["position"]), view))
tooltip = format_inlay_hint_tooltip(view, inlay_hint.get("tooltip"))
label = format_inlay_hint_label(view, inlay_hint["label"])
margin_left = "0.6rem" if inlay_hint.get("paddingLeft", False) else "0"
margin_right = "0.6rem" if inlay_hint.get("paddingRight", False) else "0"
rchl marked this conversation as resolved.
Show resolved Hide resolved
phantom_uuid = str(uuid.uuid4())
command = sublime.command_url('lsp_inlay_hint_click', {
'session_name': session_name,
'inlay_hint': inlay_hint,
'phantom_uuid': phantom_uuid
})
content = INLAY_HINT_HTML.format(
margin_left=margin_left,
margin_right=margin_right,
tooltip=tooltip,
label=label,
command=command)
p = sublime.Phantom(region, content, sublime.LAYOUT_INLINE)
setattr(p, 'lsp_uuid', phantom_uuid)
return p
Loading