Skip to content

Commit

Permalink
Implement a custom "find callers" command (#90)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rchl authored Oct 6, 2021
1 parent 0337650 commit bc814ef
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions LSP-typescript.sublime-commands
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@
"command_args": ["${file}"]
}
},
{
"caption": "LSP-typescript: Find Callers",
"command": "lsp_typescript_calls",
"args": {
"direction": "incoming"
},
},
]
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
48 changes: 48 additions & 0 deletions commands.py
Original file line number Diff line number Diff line change
@@ -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)
5 changes: 5 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[mypy]
# ignore_missing_imports = True
# check_untyped_defs = True
disallow_untyped_defs = True
strict_optional = True
6 changes: 3 additions & 3 deletions plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
29 changes: 29 additions & 0 deletions protocol.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit bc814ef

Please sign in to comment.