Skip to content

Commit cacd61b

Browse files
authored
LSP: Jump to the function name (#792)
1 parent 37a196a commit cacd61b

File tree

2 files changed

+131
-11
lines changed

2 files changed

+131
-11
lines changed

internal/ls/definition.go

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/microsoft/typescript-go/internal/ast"
77
"github.com/microsoft/typescript-go/internal/astnav"
8+
"github.com/microsoft/typescript-go/internal/checker"
89
"github.com/microsoft/typescript-go/internal/core"
910
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
1011
"github.com/microsoft/typescript-go/internal/scanner"
@@ -20,24 +21,69 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp
2021
checker, done := program.GetTypeCheckerForFile(ctx, file)
2122
defer done()
2223

24+
calledDeclaration := tryGetSignatureDeclaration(checker, node)
25+
if calledDeclaration != nil {
26+
name := ast.GetNameOfDeclaration(calledDeclaration)
27+
if name != nil {
28+
return l.createLocationsFromDeclarations([]*ast.Node{name})
29+
}
30+
}
31+
2332
if symbol := checker.GetSymbolAtLocation(node); symbol != nil {
2433
if symbol.Flags&ast.SymbolFlagsAlias != 0 {
2534
if resolved, ok := checker.ResolveAlias(symbol); ok {
2635
symbol = resolved
2736
}
2837
}
2938

30-
locations := make([]lsproto.Location, 0, len(symbol.Declarations))
31-
for _, decl := range symbol.Declarations {
32-
file := ast.GetSourceFileOfNode(decl)
33-
loc := decl.Loc
34-
pos := scanner.GetTokenPosOfNode(decl, file, false /*includeJSDoc*/)
35-
locations = append(locations, lsproto.Location{
36-
Uri: FileNameToDocumentURI(file.FileName()),
37-
Range: l.converters.ToLSPRange(file, core.NewTextRange(pos, loc.End())),
38-
})
39-
}
40-
return &lsproto.Definition{Locations: &locations}, nil
39+
return l.createLocationsFromDeclarations(symbol.Declarations)
4140
}
4241
return nil, nil
4342
}
43+
44+
func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) (*lsproto.Definition, error) {
45+
locations := make([]lsproto.Location, 0, len(declarations))
46+
for _, decl := range declarations {
47+
file := ast.GetSourceFileOfNode(decl)
48+
loc := decl.Loc
49+
pos := scanner.GetTokenPosOfNode(decl, file, false /*includeJSDoc*/)
50+
locations = append(locations, lsproto.Location{
51+
Uri: FileNameToDocumentURI(file.FileName()),
52+
Range: l.converters.ToLSPRange(file, core.NewTextRange(pos, loc.End())),
53+
})
54+
}
55+
return &lsproto.Definition{Locations: &locations}, nil
56+
}
57+
58+
/** Returns a CallLikeExpression where `node` is the target being invoked. */
59+
func getAncestorCallLikeExpression(node *ast.Node) *ast.Node {
60+
target := ast.FindAncestor(node, func(n *ast.Node) bool {
61+
return !isRightSideOfPropertyAccess(n)
62+
})
63+
64+
callLike := target.Parent
65+
if callLike != nil && ast.IsCallLikeExpression(callLike) && ast.GetInvokedExpression(callLike) == target {
66+
return callLike
67+
}
68+
69+
return nil
70+
}
71+
72+
func tryGetSignatureDeclaration(typeChecker *checker.Checker, node *ast.Node) *ast.Node {
73+
var signature *checker.Signature
74+
callLike := getAncestorCallLikeExpression(node)
75+
if callLike != nil {
76+
signature = typeChecker.GetResolvedSignature(callLike)
77+
}
78+
79+
// Don't go to a function type, go to the value having that type.
80+
var declaration *ast.Node
81+
if signature != nil && signature.Declaration() != nil {
82+
declaration = signature.Declaration()
83+
if ast.IsFunctionLike(declaration) && !ast.IsFunctionTypeNode(declaration) {
84+
return declaration
85+
}
86+
}
87+
88+
return nil
89+
}

internal/ls/definition_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package ls_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/microsoft/typescript-go/internal/bundled"
7+
"github.com/microsoft/typescript-go/internal/fourslash"
8+
"github.com/microsoft/typescript-go/internal/ls"
9+
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
10+
"github.com/microsoft/typescript-go/internal/testutil/projecttestutil"
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
func TestDefinition(t *testing.T) {
15+
t.Parallel()
16+
if !bundled.Embedded {
17+
// Without embedding, we'd need to read all of the lib files out from disk into the MapFS.
18+
// Just skip this for now.
19+
t.Skip("bundled files are not embedded")
20+
}
21+
22+
testCases := []struct {
23+
title string
24+
input string
25+
expected map[string]lsproto.Definition
26+
}{
27+
{
28+
title: "localFunction",
29+
input: `
30+
// @filename: index.ts
31+
function localFunction() { }
32+
/*localFunction*/localFunction();`,
33+
expected: map[string]lsproto.Definition{
34+
"localFunction": {
35+
Locations: &[]lsproto.Location{{
36+
Uri: ls.FileNameToDocumentURI("/index.ts"),
37+
Range: lsproto.Range{Start: lsproto.Position{Character: 9}, End: lsproto.Position{Character: 22}},
38+
}},
39+
},
40+
},
41+
},
42+
}
43+
44+
for _, testCase := range testCases {
45+
t.Run(testCase.title, func(t *testing.T) {
46+
t.Parallel()
47+
runDefinitionTest(t, testCase.input, testCase.expected)
48+
})
49+
}
50+
}
51+
52+
func runDefinitionTest(t *testing.T, input string, expected map[string]lsproto.Definition) {
53+
testData := fourslash.ParseTestData(t, input, "/mainFile.ts")
54+
file := testData.Files[0].FileName()
55+
markerPositions := testData.MarkerPositions
56+
ctx := projecttestutil.WithRequestID(t.Context())
57+
languageService, done := createLanguageService(ctx, file, map[string]any{
58+
file: testData.Files[0].Content,
59+
})
60+
defer done()
61+
62+
for markerName, expectedResult := range expected {
63+
marker, ok := markerPositions[markerName]
64+
if !ok {
65+
t.Fatalf("No marker found for '%s'", markerName)
66+
}
67+
locations, err := languageService.ProvideDefinition(
68+
ctx,
69+
ls.FileNameToDocumentURI(file),
70+
marker.LSPosition)
71+
assert.NilError(t, err)
72+
assert.DeepEqual(t, *locations, expectedResult)
73+
}
74+
}

0 commit comments

Comments
 (0)