From bc814efc1afeb1b92718769b6b69c37f62f4f1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ch=C5=82odnicki?= Date: Wed, 6 Oct 2021 10:11:01 +0200 Subject: [PATCH] Implement a custom "find callers" command (#90) typescript-language-server implements a custom "textDocument/calls" request which is a lot like normal "find references" but it skips all references that are not calling the symbol. So it filters a lot of noise like references to function being imported or just passed around. The "textDocument/calls" request supports finding both callers (implemented here) and a sort of an opposite mode of operation where it finds all functions called inside the selected symbol (function). I find that kinda useless so didn't implement it but it can be easily added by creating another `lsp_typescript_calls` command and passing "direction": "outgoing" argument to it. --- .gitattributes | 1 + LSP-typescript.sublime-commands | 7 +++++ README.md | 4 +++ commands.py | 48 +++++++++++++++++++++++++++++++++ mypy.ini | 5 ++++ plugin.py | 6 ++--- protocol.py | 29 ++++++++++++++++++++ 7 files changed, 97 insertions(+), 3 deletions(-) create mode 100644 commands.py create mode 100644 mypy.ini create mode 100644 protocol.py diff --git a/.gitattributes b/.gitattributes index d67f536..23d3232 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ .dependabot export-ignore .github/ export-ignore codecov.yml export-ignore +mypy.ini export-ignore tests/ export-ignore tox.ini export-ignore unittesting.json export-ignore diff --git a/LSP-typescript.sublime-commands b/LSP-typescript.sublime-commands index 773df51..bc458d0 100644 --- a/LSP-typescript.sublime-commands +++ b/LSP-typescript.sublime-commands @@ -16,4 +16,11 @@ "command_args": ["${file}"] } }, + { + "caption": "LSP-typescript: Find Callers", + "command": "lsp_typescript_calls", + "args": { + "direction": "incoming" + }, + }, ] diff --git a/README.md b/README.md index 9e00cc7..cbe874a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ To sort or remove unused imports you can trigger the `LSP-typescript: Organize I }, ``` +## Find Callers command + +The `LSP-typescript: Find Callers` command can be used to find what is calling the given symbol. It has some overlap with the built-in `LSP: Find References` command but returns only the places where the symbol was called. + ## Usage in projects that also use Flow TypeScript can [check vanilla JavaScript](https://www.typescriptlang.org/docs/handbook/type-checking-javascript-files.html), but may break on JavaScript with Flow types in it. To keep LSP-typescript enabled for TS and vanilla JS, while ignoring Flow-typed files, you must install [JSCustom](https://packagecontrol.io/packages/JSCustom) and configure it like so: diff --git a/commands.py b/commands.py new file mode 100644 index 0000000..4a84eff --- /dev/null +++ b/commands.py @@ -0,0 +1,48 @@ +from .protocol import Call, CallsDirection, CallsRequestParams, CallsResponse +from LSP.plugin import Request +from LSP.plugin import Session +from LSP.plugin.core.protocol import LocationLink +from LSP.plugin.core.registry import LspTextCommand +from LSP.plugin.core.typing import Optional +from LSP.plugin.core.views import text_document_position_params +from LSP.plugin.locationpicker import LocationPicker +import functools +import sublime + + +SESSION_NAME = "LSP-typescript" + + +class LspTypescriptCallsCommand(LspTextCommand): + + session_name = SESSION_NAME + + def is_enabled(self) -> bool: + selection = self.view.sel() + return len(selection) > 0 and super().is_enabled() + + def run(self, edit: sublime.Edit, direction: CallsDirection) -> None: + session = self.session_by_name(self.session_name) + if session is None: + return + position_params = text_document_position_params(self.view, self.view.sel()[0].b) + params = { + 'textDocument': position_params['textDocument'], + 'position': position_params['position'], + 'direction': direction + } # type: CallsRequestParams + session.send_request(Request("textDocument/calls", params), functools.partial(self.on_result_async, session)) + + def on_result_async(self, session: Session, result: Optional[CallsResponse]) -> None: + if not result: + return + + def to_location_link(call: Call) -> LocationLink: + return { + 'targetUri': call['location']['uri'], + 'targetSelectionRange': call['location']['range'], + } + + locations = list(map(to_location_link, result['calls'])) + self.view.run_command("add_jump_record", {"selection": [(r.a, r.b) for r in self.view.sel()]}) + LocationPicker(self.view, session, locations, side_by_side=False) diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..9d36ec7 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +# ignore_missing_imports = True +# check_untyped_defs = True +disallow_untyped_defs = True +strict_optional = True diff --git a/plugin.py b/plugin.py index 15a903e..21ccc3c 100644 --- a/plugin.py +++ b/plugin.py @@ -43,12 +43,12 @@ def is_allowed_to_start( return 'This server only works when the window workspace includes some folders!' @request_handler('_typescript.rename') - def on_typescript_rename(self, textDocumentPositionParams: Any, respond: Callable[[None], None]) -> None: - filename = uri_to_filename(textDocumentPositionParams['textDocument']['uri']) + def on_typescript_rename(self, position_params: Any, respond: Callable[[None], None]) -> None: + filename = uri_to_filename(position_params['textDocument']['uri']) view = sublime.active_window().open_file(filename) if view: - lsp_point = Point.from_lsp(textDocumentPositionParams['position']) + lsp_point = Point.from_lsp(position_params['position']) point = point_to_offset(lsp_point, view) sel = view.sel() diff --git a/protocol.py b/protocol.py new file mode 100644 index 0000000..6f8ab00 --- /dev/null +++ b/protocol.py @@ -0,0 +1,29 @@ +from LSP.plugin.core.protocol import Location, Position, RangeLsp, TextDocumentIdentifier +from LSP.plugin.core.typing import List, Literal, Optional, TypedDict, Union + + +CallsDirection = Union[Literal['incoming'], Literal['outgoing']] + +CallsRequestParams = TypedDict('CallsRequestParams', { + 'textDocument': TextDocumentIdentifier, + 'position': Position, + 'direction': CallsDirection +}, total=True) + +DefinitionSymbol = TypedDict('DefinitionSymbol', { + 'name': str, + 'detail': Optional[str], + 'kind': int, + 'location': Location, + 'selectionRange': RangeLsp, +}, total=True) + +Call = TypedDict('Call', { + 'location': Location, + 'symbol': DefinitionSymbol, +}, total=True) + +CallsResponse = TypedDict('CallsResponse', { + 'symbol': Optional[DefinitionSymbol], + 'calls': List[Call], +}, total=True)