Skip to content
8 changes: 7 additions & 1 deletion internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -842,7 +842,13 @@ func (f *FourslashTest) VerifyBaselineGoToDefinition(
} else if result.Location != nil {
resultAsLocations = []lsproto.Location{*result.Location}
} else if result.DefinitionLinks != nil {
t.Fatalf("Unexpected definition response type at marker '%s': %T", *f.lastKnownMarkerName, result.DefinitionLinks)
// For DefinitionLinks, extract the target locations
resultAsLocations = core.Map(*result.DefinitionLinks, func(link *lsproto.LocationLink) lsproto.Location {
return lsproto.Location{
Uri: link.TargetUri,
Range: link.TargetSelectionRange,
}
})
}

f.addResultToBaseline(t, "goToDefinition", f.getBaselineForLocationsWithFileContents(resultAsLocations, baselineFourslashLocationsOptions{
Expand Down
55 changes: 50 additions & 5 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/microsoft/typescript-go/internal/scanner"
)

func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position, clientCapabilities *lsproto.DefinitionClientCapabilities) (lsproto.DefinitionResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
if node.Kind == ast.KindSourceFile {
Expand All @@ -24,13 +24,13 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp

if node.Kind == ast.KindOverrideKeyword {
if sym := getSymbolForOverriddenMember(c, node); sym != nil {
return l.createLocationsFromDeclarations(sym.Declarations), nil
return l.createDefinitionResponse(sym.Declarations, node, file, clientCapabilities), nil
}
}

if ast.IsJumpStatementTarget(node) {
if label := getTargetLabel(node.Parent, node.Text()); label != nil {
return l.createLocationsFromDeclarations([]*ast.Node{label}), nil
return l.createDefinitionResponse([]*ast.Node{label}, node, file, clientCapabilities), nil
}
}

Expand All @@ -43,7 +43,7 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp

if node.Kind == ast.KindReturnKeyword || node.Kind == ast.KindYieldKeyword || node.Kind == ast.KindAwaitKeyword {
if fn := ast.FindAncestor(node, ast.IsFunctionLikeDeclaration); fn != nil {
return l.createLocationsFromDeclarations([]*ast.Node{fn}), nil
return l.createDefinitionResponse([]*ast.Node{fn}, node, file, clientCapabilities), nil
}
}

Expand All @@ -54,7 +54,7 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp
nonFunctionDeclarations := core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return !ast.IsFunctionLike(node) })
declarations = append(nonFunctionDeclarations, calledDeclaration)
}
return l.createLocationsFromDeclarations(declarations), nil
return l.createDefinitionResponse(declarations, node, file, clientCapabilities), nil
}

func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
Expand Down Expand Up @@ -99,6 +99,51 @@ func getDeclarationNameForKeyword(node *ast.Node) *ast.Node {
return node
}

func (l *LanguageService) createDefinitionResponse(declarations []*ast.Node, originNode *ast.Node, originFile *ast.SourceFile, clientCapabilities *lsproto.DefinitionClientCapabilities) lsproto.DefinitionResponse {
// Check if client supports LocationLink
if clientCapabilities != nil && clientCapabilities.LinkSupport != nil && *clientCapabilities.LinkSupport {
return l.createLocationLinksFromDeclarations(declarations, originNode, originFile)
}
// Fall back to traditional Location response
return l.createLocationsFromDeclarations(declarations)
}

func (l *LanguageService) createLocationLinksFromDeclarations(declarations []*ast.Node, originNode *ast.Node, originFile *ast.SourceFile) lsproto.DefinitionResponse {
someHaveBody := core.Some(declarations, func(node *ast.Node) bool { return node.Body() != nil })
links := make([]*lsproto.LocationLink, 0, len(declarations))

// Calculate origin selection range (the "bound span")
originSelectionRange := l.createLspRangeFromNode(originNode, originFile)

for _, decl := range declarations {
if !someHaveBody || decl.Body() != nil {
file := ast.GetSourceFileOfNode(decl)
name := core.OrElse(ast.GetNameOfDeclaration(decl), decl)

// For targetRange, use the full declaration range
var targetRange *lsproto.Range
if decl.Body() != nil {
// For declarations with body, include the full declaration
targetRange = l.createLspRangeFromBounds(scanner.GetTokenPosOfNode(decl, file, false), decl.End(), file)
} else {
// For declarations without body, use the declaration itself
targetRange = l.createLspRangeFromNode(decl, file)
}

// For targetSelectionRange, use just the name/identifier part
targetSelectionRange := l.createLspRangeFromNode(name, file)

links = append(links, &lsproto.LocationLink{
OriginSelectionRange: originSelectionRange,
TargetUri: FileNameToDocumentURI(file.FileName()),
TargetRange: *targetRange,
TargetSelectionRange: *targetSelectionRange,
})
}
}
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{DefinitionLinks: &links}
}

func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.Node) lsproto.DefinitionResponse {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is using this function anymore? Can it be used in the new code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createLocationsFromDeclarations function is still being used as the fallback when LinkSupport is false in the createDefinitionResponse method. It provides backwards compatibility for clients that don't support LocationLink.

locations := make([]lsproto.Location, 0, len(declarations))
for _, decl := range declarations {
Expand Down
38 changes: 38 additions & 0 deletions internal/ls/definition_link_support_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ls

import (
"testing"

"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"gotest.tools/v3/assert"
)

func TestLocationLinkSupport(t *testing.T) {
t.Parallel()

// Simple integration test to ensure LocationLink support works
// without causing import cycles

// Test that client capabilities are correctly used
linkSupport := true
capabilities := &lsproto.DefinitionClientCapabilities{
LinkSupport: &linkSupport,
}

// Test that the capability checking logic works
assert.Assert(t, capabilities != nil)
assert.Assert(t, capabilities.LinkSupport != nil)
assert.Assert(t, *capabilities.LinkSupport)

// Test with capabilities disabled
linkSupportFalse := false
capabilitiesDisabled := &lsproto.DefinitionClientCapabilities{
LinkSupport: &linkSupportFalse,
}
assert.Assert(t, capabilitiesDisabled.LinkSupport != nil)
assert.Assert(t, !*capabilitiesDisabled.LinkSupport)

// Test with nil capabilities
var nilCapabilities *lsproto.DefinitionClientCapabilities
assert.Assert(t, nilCapabilities == nil)
}
29 changes: 28 additions & 1 deletion internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.La
}

func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) {
return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position)
return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position, definitionCapabilities(s.initializeParams))
}

func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TypeDefinitionParams) (lsproto.TypeDefinitionResponse, error) {
Expand Down Expand Up @@ -886,3 +886,30 @@ func getCompletionClientCapabilities(params *lsproto.InitializeParams) *lsproto.
}
return params.Capabilities.TextDocument.Completion
}

func definitionCapabilities(params *lsproto.InitializeParams) *lsproto.DefinitionClientCapabilities {
if params == nil || params.Capabilities == nil || params.Capabilities.TextDocument == nil {
// Return default capabilities with LinkSupport enabled
return &lsproto.DefinitionClientCapabilities{
LinkSupport: ptrTo(true),
}
}

capabilities := params.Capabilities.TextDocument.Definition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

call this definitionCapabilities because it looks like it's checked above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to definitionCapabilities. Commit a8ba72c.

if capabilities == nil {
// Return default capabilities with LinkSupport enabled
return &lsproto.DefinitionClientCapabilities{
LinkSupport: ptrTo(true),
}
}

// If capabilities exist but LinkSupport is not specified, default to true
if capabilities.LinkSupport == nil {
// Copy existing capabilities and override LinkSupport
result := *capabilities
result.LinkSupport = ptrTo(true)
return &result
}

return capabilities
}
2 changes: 1 addition & 1 deletion internal/project/untitled_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ x++;`
assert.Assert(t, len(refs) == 3, "Expected 3 references, got %d", len(refs))

// Also test definition using ProvideDefinition
definition, err := languageService.ProvideDefinition(ctx, uri, lspPosition)
definition, err := languageService.ProvideDefinition(ctx, uri, lspPosition, nil)
assert.NilError(t, err)
if definition.Locations != nil {
t.Logf("Definition found: %d locations", len(*definition.Locations))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@
// [|constructor(protected readonly cArg: string) {}|]
// }
//
// export class [|Derived|] extends Base {
// readonly email = this.cArg.getByLabel('Email')
// readonly password = this.cArg.getByLabel('Password')
// }
// export class Derived extends Base {
// // --- (line: 6) skipped ---

// === /main.ts ===
// import { Derived } from './definitions'
Expand All @@ -16,14 +14,6 @@


// === goToDefinition ===
// === /defInSameFile.ts ===
// import { Base } from './definitions'
// class [|SameFile|] extends Base {
// readonly name: string = 'SameFile'
// }
// const SameFile = new /*GOTO DEF*/SameFile(cArg)
// const wrapper = new Base(cArg)

// === /definitions.ts ===
// export class Base {
// [|constructor(protected readonly cArg: string) {}|]
Expand All @@ -32,12 +22,20 @@
// export class Derived extends Base {
// // --- (line: 6) skipped ---

// === /defInSameFile.ts ===
// import { Base } from './definitions'
// class SameFile extends Base {
// readonly name: string = 'SameFile'
// }
// const SameFile = new /*GOTO DEF*/SameFile(cArg)
// const wrapper = new Base(cArg)



// === goToDefinition ===
// === /hasConstructor.ts ===
// import { Base } from './definitions'
// class [|HasConstructor|] extends Base {
// class HasConstructor extends Base {
// [|constructor() {}|]
// readonly name: string = '';
// }
Expand All @@ -47,7 +45,7 @@

// === goToDefinition ===
// === /definitions.ts ===
// export class [|Base|] {
// export class Base {
// [|constructor(protected readonly cArg: string) {}|]
// }
//
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// === goToDefinition ===
// === /goToDefinitionConstructorOfClassExpression01.ts ===
// var x = class [|C|] {
// var x = class C {
// [|constructor() {
// var other = new /*GOTO DEF*/C;
// }|]
Expand All @@ -13,11 +13,10 @@

// === goToDefinition ===
// === /goToDefinitionConstructorOfClassExpression01.ts ===
// --- (line: 3) skipped ---
// }
// --- (line: 4) skipped ---
// }
//
// var y = class [|C|] extends x {
// var y = class C extends x {
// [|constructor() {
// super();
// var other = new /*GOTO DEF*/C;
Expand All @@ -38,12 +37,11 @@
// }
//
// var y = class C extends x {
// constructor() {
// super();
// var other = new C;
// }
// // --- (line: 8) skipped ---

// --- (line: 11) skipped ---
// }
// var z = class [|C|] extends x {
// var z = class C extends x {
// m() {
// return new /*GOTO DEF*/C;
// }
Expand All @@ -68,7 +66,7 @@

// === goToDefinition ===
// === /goToDefinitionConstructorOfClassExpression01.ts ===
// var [|x|] = class C {
// var x = class C {
// [|constructor() {
// var other = new C;
// }|]
Expand All @@ -89,11 +87,10 @@

// === goToDefinition ===
// === /goToDefinitionConstructorOfClassExpression01.ts ===
// --- (line: 3) skipped ---
// }
// --- (line: 4) skipped ---
// }
//
// var [|y|] = class C extends x {
// var y = class C extends x {
// [|constructor() {
// super();
// var other = new C;
Expand Down Expand Up @@ -121,17 +118,9 @@
// }
//
// var y = class C extends x {
// constructor() {
// super();
// var other = new C;
// }
// }
// var [|z|] = class C extends x {
// m() {
// return new C;
// }
// }
//
// // --- (line: 8) skipped ---

// --- (line: 18) skipped ---
// var x1 = new C();
// var x2 = new x();
// var y1 = new y();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
// export var x;
// }
//
// class [|Foo|] {
// class Foo {
// [|constructor() {
// }|]
// }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
// === goToDefinition ===
// === /goToDefinitionConstructorOverloads.ts ===
// class ConstructorOverload {
// /*GOTO DEF*/[|constructor();|]
// [|constructor(foo: string);|]
// /*GOTO DEF*/constructor();
// constructor(foo: string);
// [|constructor(foo: any) { }|]
// }
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// === /index.js ===
// const Core = {}
//
// Core.[|Test|] = class {
// Core.Test = class {
// [|constructor() { }|]
// }
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@

// === goToDefinition ===
// === /goToDefinitionFunctionOverloads.ts ===
// function /*GOTO DEF*/[|functionOverload|](value: number);
// function [|functionOverload|](value: string);
// function /*GOTO DEF*/functionOverload(value: number);
// function functionOverload(value: string);
// function [|functionOverload|]() {}
//
// functionOverload(123);
Expand Down
Loading