From 0bddd76f703b14df8d279a98b12b33831291ad66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Sun, 17 Oct 2021 14:46:35 +0200 Subject: [PATCH] Use the new LSP API for handling inline hints (#96) * Use the new LSP API for handling inline hints * updating naming --- inlayHints.py | 97 ----------------------------------------- plugin.py | 116 +++++++++++++++++++++++++++++++++++++++++--------- protocol.py | 24 ++++++++++- 3 files changed, 120 insertions(+), 117 deletions(-) delete mode 100644 inlayHints.py diff --git a/inlayHints.py b/inlayHints.py deleted file mode 100644 index e4195ab..0000000 --- a/inlayHints.py +++ /dev/null @@ -1,97 +0,0 @@ -from .protocol import InlayHint, InlayHintRequestParams, InlayHintResponse -from html import escape as html_escape -from LSP.plugin import Request -from LSP.plugin import Session -from LSP.plugin.core.protocol import Point -from LSP.plugin.core.registry import windows -from LSP.plugin.core.types import debounced -from LSP.plugin.core.types import FEATURES_TIMEOUT -from LSP.plugin.core.typing import List, Optional -from LSP.plugin.core.views import point_to_offset, uri_from_view -from LSP.plugin.core.views import text_document_identifier -import sublime -import sublime_plugin - - -def inlay_hint_to_phantom(view: sublime.View, hint: InlayHint) -> sublime.Phantom: - html = """ - - -
- {label} -
- - """ - point = Point.from_lsp(hint['position']) - region = sublime.Region(point_to_offset(point, view)) - label = html_escape(hint["text"]) - html = html.format(label=label) - return sublime.Phantom(region, html, sublime.LAYOUT_INLINE) - - -def session_by_name(view: sublime.View, session_name: str) -> Optional[Session]: - listener = windows.listener_for_view(view) - if listener: - for sv in listener.session_views_async(): - if sv.session.config.name == session_name: - return sv.session - return None - - -class InlayHintsListener(sublime_plugin.ViewEventListener): - def on_modified_async(self) -> None: - change_count = self.view.change_count() - # increase the timeout to avoid rare issue with hints being requested before the textdocument/didChange - TIMEOUT = FEATURES_TIMEOUT + 100 - debounced( - self.request_inlay_hints_async, - TIMEOUT, - lambda: self.view.change_count() == change_count, - async_thread=True, - ) - - def on_load_async(self) -> None: - self.request_inlay_hints_async() - - def on_activated_async(self) -> None: - self.request_inlay_hints_async() - - def request_inlay_hints_async(self) -> None: - session = session_by_name(self.view, 'LSP-typescript') - if session is None: - return - params = { - "textDocument": text_document_identifier(self.view) - } # type: InlayHintRequestParams - session.send_request_async( - Request("typescript/inlayHints", params), - self.on_inlay_hints_async - ) - - def on_inlay_hints_async(self, response: InlayHintResponse) -> None: - session = session_by_name(self.view, 'LSP-typescript') - if session is None: - return - buffer = session.get_session_buffer_for_uri_async(uri_from_view(self.view)) - if not buffer: - return - key = "_lsp_typescript_inlay_hints" - phantom_set = getattr(buffer, key, None) - if phantom_set is None: - phantom_set = sublime.PhantomSet(self.view, key) - setattr(buffer, key, phantom_set) - phantoms = [inlay_hint_to_phantom(self.view, hint) for hint in response['inlayHints']] - sublime.set_timeout(lambda: self.present_inlay_hints(phantoms, phantom_set)) - - def present_inlay_hints(self, phantoms: List[sublime.Phantom], phantom_set: sublime.PhantomSet) -> None: - if not self.view.is_valid(): - return - phantom_set.update(phantoms) diff --git a/plugin.py b/plugin.py index 4e923ee..81a0ec8 100644 --- a/plugin.py +++ b/plugin.py @@ -1,9 +1,14 @@ +from .protocol import InlayHint, InlayHintRequestParams, InlayHintResponse, CompletionCodeActionCommand +from html import escape as html_escape from LSP.plugin import ClientConfig +from LSP.plugin import SessionBufferProtocol from LSP.plugin import uri_to_filename from LSP.plugin import WorkspaceFolder from LSP.plugin.core.protocol import Point from LSP.plugin.core.typing import Any, Callable, Dict, List, Mapping, Optional from LSP.plugin.core.views import point_to_offset +from LSP.plugin.core.views import text_document_identifier +from lsp_utils import ApiWrapperInterface from lsp_utils import NpmClientHandler from lsp_utils import request_handler import os @@ -26,6 +31,50 @@ def plugin_unloaded() -> None: LspTypescriptPlugin.cleanup() +def inlay_hint_to_phantom(view: sublime.View, hint: InlayHint) -> sublime.Phantom: + html = """ + + +
+ {label} +
+ + """ + point = Point.from_lsp(hint['position']) + region = sublime.Region(point_to_offset(point, view)) + label = html_escape(hint["text"]) + html = html.format(label=label) + return sublime.Phantom(region, html, sublime.LAYOUT_INLINE) + + +def to_lsp_edits(items: List[CompletionCodeActionCommand]) -> Dict[str, List[TextEditTuple]]: + workspace_edits = {} # type: Dict[str, List[TextEditTuple]] + for item in items: + for change in item['changes']: + file_changes = [] # List[TextEditTuple] + for text_change in change['textChanges']: + start = text_change['start'] + end = text_change['end'] + file_changes.append( + ( + (start['line'] - 1, start['offset'] - 1), + (end['line'] - 1, end['offset'] - 1), + text_change['newText'].replace("\r", ""), + None, + ) + ) + workspace_edits[change['fileName']] = file_changes + return workspace_edits + + class LspTypescriptPlugin(NpmClientHandler): package_name = __package__ server_directory = 'typescript-language-server' @@ -42,6 +91,13 @@ def is_allowed_to_start( if not workspace_folders: return 'This server only works when the window workspace includes some folders!' + def __init__(self, *args: Any, **kwargs: Any) -> None: + self._api = None # type: Optional[ApiWrapperInterface] + super().__init__(*args, **kwargs) + + def on_ready(self, api: ApiWrapperInterface) -> None: + self._api = api + @request_handler('_typescript.rename') def on_typescript_rename(self, position_params: Any, respond: Callable[[None], None]) -> None: filename = uri_to_filename(position_params['textDocument']['uri']) @@ -59,30 +115,52 @@ def on_typescript_rename(self, position_params: Any, respond: Callable[[None], N # Server doesn't require any specific response. respond(None) + # --- AbstractPlugin handlers -------------------------------------------------------------------------------------- + + def on_session_buffer_changed_async(self, session_buffer: SessionBufferProtocol) -> None: + self._request_inlay_hints_async(session_buffer) + def on_pre_server_command(self, command: Mapping[str, Any], done_callback: Callable[[], None]) -> bool: if command['command'] == '_typescript.applyCompletionCodeAction': _, items = command['arguments'] session = self.weaksession() if session: - apply_workspace_edit(session.window, self._to_lsp_edits(items)).then(lambda _: done_callback()) + apply_workspace_edit(session.window, to_lsp_edits(items)).then(lambda _: done_callback()) return True return False - def _to_lsp_edits(self, items: Any) -> Dict[str, List[TextEditTuple]]: - workspace_edits = {} # type: Dict[str, List[TextEditTuple]] - for item in items: - for change in item['changes']: - file_changes = [] # List[TextEditTuple] - for text_change in change['textChanges']: - start = text_change['start'] - end = text_change['end'] - file_changes.append( - ( - (start['line'] - 1, start['offset'] - 1), - (end['line'] - 1, end['offset'] - 1), - text_change['newText'].replace("\r", ""), - None, - ) - ) - workspace_edits[change['fileName']] = file_changes - return workspace_edits + # --- Inlay Hints handlers ----------------------------------------------------------------------------------------- + + def _request_inlay_hints_async(self, session_buffer: SessionBufferProtocol) -> None: + if not self._api: + return + uri = session_buffer.get_uri() + if not uri: + return + params = {"textDocument": text_document_identifier(uri)} # type: InlayHintRequestParams + self._api.send_request( + "typescript/inlayHints", params, + lambda result, is_error: self._on_inlay_hints_async(result, is_error, session_buffer)) + + def _on_inlay_hints_async( + self, response: InlayHintResponse, is_error: bool, session_buffer: SessionBufferProtocol + ) -> None: + if is_error: + return + view = next(iter(session_buffer.session_views)).view + if not view: + return + key = "_lsp_typescript_inlay_hints" + phantom_set = getattr(session_buffer, key, None) + if phantom_set is None: + phantom_set = sublime.PhantomSet(view, key) + setattr(session_buffer, key, phantom_set) + phantoms = [inlay_hint_to_phantom(view, hint) for hint in response['inlayHints']] + sublime.set_timeout(lambda: self.present_inlay_hints(view, phantoms, phantom_set)) + + def present_inlay_hints( + self, view: sublime.View, phantoms: List[sublime.Phantom], phantom_set: sublime.PhantomSet + ) -> None: + if not view.is_valid(): + return + phantom_set.update(phantoms) diff --git a/protocol.py b/protocol.py index 98303d1..2577e15 100644 --- a/protocol.py +++ b/protocol.py @@ -1,5 +1,5 @@ from LSP.plugin.core.protocol import Location, Position, RangeLsp, TextDocumentIdentifier -from LSP.plugin.core.typing import List, Literal, Optional, TypedDict, Union +from LSP.plugin.core.typing import Any, List, Literal, Optional, TypedDict, Union CallsDirection = Union[Literal['incoming'], Literal['outgoing']] @@ -46,3 +46,25 @@ InlayHintResponse = TypedDict('CallsResponse', { 'inlayHints': List[InlayHint] }, total=True) + +TypescriptLocation = TypedDict('TypescriptLocation', { + 'line': int, + 'offset': int, +}, total=True) + +CodeEdit = TypedDict('CodeEdit', { + 'start': TypescriptLocation, + 'end': TypescriptLocation, + 'newText': str, +}, total=True) + +FileCodeEdit = TypedDict('FileCodeEdits', { + 'fileName': str, + 'textChanges': List[CodeEdit], +}, total=True) + +CompletionCodeActionCommand = TypedDict('CompletionCodeActionCommand', { + 'commands': List[Any], + 'description': str, + 'changes': List[FileCodeEdit], +}, total=True)