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)