diff --git a/plugin/completion.py b/plugin/completion.py index da7a3ff69..c1d15f94d 100644 --- a/plugin/completion.py +++ b/plugin/completion.py @@ -34,7 +34,7 @@ def run_async() -> None: def _format_documentation(self, content: Union[str, Dict[str, str]]) -> str: return minihtml(self.view, content, allowed_formats=FORMAT_STRING | FORMAT_MARKUP_CONTENT) - def _handle_resolve_response_async(self, item: Optional[dict]) -> None: + def _handle_resolve_response_async(self, item: CompletionItem) -> None: detail = "" documentation = "" if item: @@ -69,7 +69,7 @@ def _on_navigate(self, url: str) -> None: class LspCompleteCommand(sublime_plugin.TextCommand): - def epilogue(self, item: Dict[str, Any], session_name: Optional[str] = None) -> None: + def epilogue(self, item: CompletionItem, session_name: Optional[str] = None) -> None: additional_edits = item.get('additionalTextEdits') if additional_edits: edits = [parse_text_edit(additional_edit) for additional_edit in additional_edits] @@ -98,9 +98,11 @@ def run(self, edit: sublime.Edit, item: Any, session_name: Optional[str] = None) class LspCompleteTextEditCommand(LspCompleteCommand): - def run(self, edit: sublime.Edit, item: Any, session_name: Optional[str] = None) -> None: - text_edit = item["textEdit"] - new_text = text_edit['newText'] + def run(self, edit: sublime.Edit, item: CompletionItem, session_name: Optional[str] = None) -> None: + text_edit = item.get("textEdit") + if not text_edit: + return + new_text = text_edit["newText"] edit_region = range_to_region(Range.from_lsp(text_edit['range']), self.view) if item.get("insertTextFormat", InsertTextFormat.PlainText) == InsertTextFormat.Snippet: for region in self.translated_regions(edit_region): diff --git a/plugin/core/edit.py b/plugin/core/edit.py index 271b3afe1..717a7d3a5 100644 --- a/plugin/core/edit.py +++ b/plugin/core/edit.py @@ -1,6 +1,7 @@ from .logging import debug from .open import open_file from .promise import Promise +from .protocol import TextEdit as LspTextEdit, Position from .typing import List, Dict, Any, Iterable, Optional, Tuple from .url import uri_to_filename from functools import partial @@ -9,11 +10,11 @@ # tuple of start, end, newText, version -TextEdit = Tuple[Tuple[int, int], Tuple[int, int], str, Optional[int]] +TextEditTuple = Tuple[Tuple[int, int], Tuple[int, int], str, Optional[int]] -def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextEdit]]: - changes = {} # type: Dict[str, List[TextEdit]] +def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextEditTuple]]: + changes = {} # type: Dict[str, List[TextEditTuple]] document_changes = workspace_edit.get('documentChanges') if isinstance(document_changes, list): for document_change in document_changes: @@ -32,11 +33,11 @@ def parse_workspace_edit(workspace_edit: Dict[str, Any]) -> Dict[str, List[TextE return changes -def parse_range(range: Dict[str, int]) -> Tuple[int, int]: +def parse_range(range: Position) -> Tuple[int, int]: return range['line'], range['character'] -def parse_text_edit(text_edit: Dict[str, Any], version: int = None) -> TextEdit: +def parse_text_edit(text_edit: LspTextEdit, version: int = None) -> TextEditTuple: return ( parse_range(text_edit['range']['start']), parse_range(text_edit['range']['end']), @@ -46,7 +47,7 @@ def parse_text_edit(text_edit: Dict[str, Any], version: int = None) -> TextEdit: ) -def sort_by_application_order(changes: Iterable[TextEdit]) -> List[TextEdit]: +def sort_by_application_order(changes: Iterable[TextEditTuple]) -> List[TextEditTuple]: # The spec reads: # > However, it is possible that multiple edits have the same start position: multiple # > inserts, or any number of inserts followed by a single remove or replace edit. If @@ -58,12 +59,12 @@ def sort_by_application_order(changes: Iterable[TextEdit]) -> List[TextEdit]: return list(sorted(changes, key=operator.itemgetter(0))) -def apply_workspace_edit(window: sublime.Window, changes: Dict[str, List[TextEdit]]) -> Promise: +def apply_workspace_edit(window: sublime.Window, changes: Dict[str, List[TextEditTuple]]) -> Promise: """Apply workspace edits. This function must be called from the main thread!""" return Promise.all([open_file(window, fn).then(partial(_apply_edits, edits)) for fn, edits in changes.items()]) -def _apply_edits(edits: List[TextEdit], view: Optional[sublime.View]) -> None: +def _apply_edits(edits: List[TextEditTuple], view: Optional[sublime.View]) -> None: if view and view.is_valid(): # Text commands run blocking. After this call has returned the changes are applied. view.run_command("lsp_apply_document_edit", {"changes": edits}) diff --git a/plugin/core/protocol.py b/plugin/core/protocol.py index ebca917dc..f39ed2da9 100644 --- a/plugin/core/protocol.py +++ b/plugin/core/protocol.py @@ -46,6 +46,11 @@ class SignatureHelpTriggerKind: ContentChange = 3 +class InsertTextMode: + AsIs = 1 + AdjustIndentation = 2 + + DocumentUri = str Position = TypedDict('Position', { @@ -171,7 +176,30 @@ class SignatureHelpTriggerKind: 'relatedInformation': List[DiagnosticRelatedInformation] }, total=False) -CompletionItem = Dict[str, Any] +TextEdit = TypedDict('TextEdit', { + 'newText': str, + 'range': RangeLsp +}, total=True) + +CompletionItem = TypedDict('CompletionItem', { + 'additionalTextEdits': List[TextEdit], + 'command': Command, + 'commitCharacters': List[str], + 'data': Any, + 'deprecated': bool, + 'detail': str, + 'documentation': Union[str, Dict[str, str]], + 'filterText': str, + 'insertText': str, + 'insertTextFormat': InsertTextFormat, + 'insertTextMode': InsertTextMode, + 'kind': int, + 'label': str, + 'preselect': bool, + 'sortText': str, + 'tags': List[CompletionItemTag], + 'textEdit': TextEdit +}, total=False) CompletionList = TypedDict('CompletionList', { 'isIncomplete': bool, @@ -235,7 +263,7 @@ def documentHighlight(cls, params: Mapping[str, Any], view: sublime.View) -> 'Re return Request("textDocument/documentHighlight", params, view) @classmethod - def resolveCompletionItem(cls, params: Mapping[str, Any], view: sublime.View) -> 'Request': + def resolveCompletionItem(cls, params: CompletionItem, view: sublime.View) -> 'Request': return Request("completionItem/resolve", params, view) @classmethod diff --git a/plugin/edit.py b/plugin/edit.py index 16db85571..47a3d4cc3 100644 --- a/plugin/edit.py +++ b/plugin/edit.py @@ -1,6 +1,6 @@ import sublime import sublime_plugin -from .core.edit import sort_by_application_order, TextEdit +from .core.edit import sort_by_application_order, TextEditTuple from .core.logging import debug from .core.typing import List, Optional, Any, Generator from contextlib import contextmanager @@ -21,7 +21,7 @@ def temporary_setting(settings: sublime.Settings, key: str, val: Any) -> Generat class LspApplyDocumentEditCommand(sublime_plugin.TextCommand): - def run(self, edit: Any, changes: Optional[List[TextEdit]] = None) -> None: + def run(self, edit: Any, changes: Optional[List[TextEditTuple]] = None) -> None: # Apply the changes in reverse, so that we don't invalidate the range # of any change that we haven't applied yet. if not changes: diff --git a/plugin/formatting.py b/plugin/formatting.py index f65c47fd8..6efafaf9b 100644 --- a/plugin/formatting.py +++ b/plugin/formatting.py @@ -1,4 +1,5 @@ from .core.edit import parse_text_edit +from .core.protocol import TextEdit from .core.registry import LspTextCommand from .core.registry import sessions_for_view from .core.sessions import Session @@ -13,7 +14,7 @@ import sublime -def apply_response_to_view(response: Optional[List[dict]], view: sublime.View) -> None: +def apply_response_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}) diff --git a/plugin/rename.py b/plugin/rename.py index e940490a8..d84d89e12 100644 --- a/plugin/rename.py +++ b/plugin/rename.py @@ -1,6 +1,6 @@ from .core.edit import apply_workspace_edit from .core.edit import parse_workspace_edit -from .core.edit import TextEdit +from .core.edit import TextEditTuple from .core.panels import ensure_panel from .core.panels import PanelName from .core.protocol import Range @@ -163,7 +163,12 @@ def _get_relative_path(self, file_path: str) -> str: else: return file_path - def _render_rename_panel(self, changes: Dict[str, List[TextEdit]], total_changes: int, file_count: int) -> None: + def _render_rename_panel( + self, + changes: Dict[str, List[TextEditTuple]], + total_changes: int, + file_count: int + ) -> None: window = self.view.window() if not window: return