diff --git a/internal/ls/converters.go b/internal/ls/converters.go index af16188501..ee105a5893 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -135,6 +135,9 @@ func (c *Converters) LineAndCharacterToPosition(script Script, lineAndCharacter // UTF-8/16 0-indexed line and character to UTF-8 offset lineMap := c.getLineMap(script.FileName()) + if lineMap == nil { + lineMap = ComputeLineStarts(script.Text()) + } line := core.TextPos(lineAndCharacter.Line) char := core.TextPos(lineAndCharacter.Character) @@ -167,6 +170,9 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex // UTF-8 offset to UTF-8/16 0-indexed line and character lineMap := c.getLineMap(script.FileName()) + if lineMap == nil { + lineMap = ComputeLineStarts(script.Text()) + } line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position) if !isLineStart { diff --git a/internal/ls/definition.go b/internal/ls/definition.go index 5e765fcc0b..3852a15921 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -2,14 +2,20 @@ package ls import ( "context" + "math" "slices" + "strings" + "github.com/go-json-experiment/json" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/parser" "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/tspath" ) func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) { @@ -104,8 +110,16 @@ func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.No for _, decl := range declarations { file := ast.GetSourceFileOfNode(decl) name := core.OrElse(ast.GetNameOfDeclaration(decl), decl) + + fileName := file.FileName() + if tspath.IsDeclarationFileName(fileName) { + if mappedLocation := l.tryMapToOriginalSource(file, name); mappedLocation != nil { + locations = core.AppendIfUnique(locations, *mappedLocation) + continue + } + } locations = core.AppendIfUnique(locations, lsproto.Location{ - Uri: FileNameToDocumentURI(file.FileName()), + Uri: FileNameToDocumentURI(fileName), Range: *l.createLspRangeFromNode(name, file), }) } @@ -235,3 +249,159 @@ func getDeclarationsFromType(t *checker.Type) []*ast.Node { } return result } + +func (l *LanguageService) tryMapToOriginalSource(declFile *ast.SourceFile, node *ast.Node) *lsproto.Location { + fs := l.GetProgram().Host().FS() + + declFileName := declFile.FileName() + declContent, ok := fs.ReadFile(declFileName) + if !ok { + return nil + } + + lineMap := l.converters.getLineMap(declFileName) + if lineMap == nil { + lineMap = ComputeLineStarts(declContent) + } + lineInfo := sourcemap.GetLineInfo(declContent, lineMap.LineStarts) + + sourceMappingURL := sourcemap.TryGetSourceMappingURL(lineInfo) + if sourceMappingURL == "" || strings.HasPrefix(sourceMappingURL, "data:") { + return nil + } + + sourceMapPath := tspath.NormalizePath(tspath.CombinePaths(tspath.GetDirectoryPath(declFileName), sourceMappingURL)) + sourceMapContent, ok := fs.ReadFile(sourceMapPath) + if !ok { + return nil + } + + var sourceMapData struct { + SourceRoot string `json:"sourceRoot"` + Sources []string `json:"sources"` + Mappings string `json:"mappings"` + } + if err := json.Unmarshal([]byte(sourceMapContent), &sourceMapData); err != nil { + return nil + } + + decoder := sourcemap.DecodeMappings(sourceMapData.Mappings) + if decoder.Error() != nil { + return nil + } + + declPosition := l.converters.PositionToLineAndCharacter(declFile, core.TextPos(node.Pos())) + + var bestMapping *sourcemap.Mapping + for mapping := range decoder.Values() { + if mapping.GeneratedLine == int(declPosition.Line) && + mapping.GeneratedCharacter <= int(declPosition.Character) && + mapping.IsSourceMapping() { + if bestMapping == nil || mapping.GeneratedCharacter > bestMapping.GeneratedCharacter { + bestMapping = mapping + } + } + } + + if bestMapping == nil || int(bestMapping.SourceIndex) >= len(sourceMapData.Sources) { + return nil + } + + sourceFileName := sourceMapData.Sources[bestMapping.SourceIndex] + if !tspath.PathIsAbsolute(sourceFileName) { + if sourceMapData.SourceRoot != "" { + sourceFileName = tspath.CombinePaths(sourceMapData.SourceRoot, sourceFileName) + } + sourceFileName = tspath.NormalizePath(tspath.CombinePaths(tspath.GetDirectoryPath(declFileName), sourceFileName)) + } + + if !fs.FileExists(sourceFileName) { + return nil + } + + sourceContent, ok := fs.ReadFile(sourceFileName) + if !ok { + return nil + } + + sourceFileScript := &sourceFileScript{ + fileName: sourceFileName, + text: sourceContent, + lineMap: ComputeLineStarts(sourceContent).LineStarts, + } + + sourceLspPosition := lsproto.Position{ + Line: uint32(bestMapping.SourceLine), + Character: uint32(bestMapping.SourceCharacter), + } + + var sourceStartLsp, sourceEndLsp lsproto.Position + + var symbolName string + if node.Kind == ast.KindIdentifier || node.Kind == ast.KindPrivateIdentifier { + symbolName = node.Text() + } + if symbolName != "" { + sourceBytePos := l.converters.LineAndCharacterToPosition(sourceFileScript, sourceLspPosition) + targetPos := int(sourceBytePos) + + sourceFile := parser.ParseSourceFile(ast.SourceFileParseOptions{ + FileName: sourceFileName, + Path: tspath.ToPath(sourceFileName, "", true), + }, sourceContent, core.GetScriptKindFromFileName(sourceFileName)) + + positions := getPossibleSymbolReferencePositions(sourceFile, symbolName, nil) + + // Find the closest position to our target + bestMatch := -1 + bestDistance := math.MaxInt + for _, pos := range positions { + distance := targetPos - pos + if distance < 0 { + distance = -distance + } + if distance < bestDistance { + bestDistance = distance + bestMatch = pos + } + } + + if bestMatch != -1 { + sourceStartPos := core.TextPos(bestMatch) + sourceEndPos := core.TextPos(bestMatch + len(symbolName)) + sourceStartLsp = l.converters.PositionToLineAndCharacter(sourceFileScript, sourceStartPos) + sourceEndLsp = l.converters.PositionToLineAndCharacter(sourceFileScript, sourceEndPos) + } + } + + if sourceStartLsp == (lsproto.Position{}) { + sourceStartLsp = sourceLspPosition + sourceEndLsp = sourceLspPosition + } + + return &lsproto.Location{ + Uri: FileNameToDocumentURI(sourceFileName), + Range: lsproto.Range{ + Start: sourceStartLsp, + End: sourceEndLsp, + }, + } +} + +type sourceFileScript struct { + fileName string + text string + lineMap []core.TextPos +} + +func (s *sourceFileScript) FileName() string { + return s.fileName +} + +func (s *sourceFileScript) Text() string { + return s.text +} + +func (s *sourceFileScript) LineMap() []core.TextPos { + return s.lineMap +} diff --git a/internal/ls/definition_test.go b/internal/ls/definition_test.go new file mode 100644 index 0000000000..a15c113d8e --- /dev/null +++ b/internal/ls/definition_test.go @@ -0,0 +1,90 @@ +package ls_test + +import ( + "context" + "testing" + + "github.com/microsoft/typescript-go/internal/bundled" + "github.com/microsoft/typescript-go/internal/ls" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/testutil/projecttestutil" + "gotest.tools/v3/assert" +) + +func TestInternalAliasGoToDefinition(t *testing.T) { + t.Parallel() + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + // Source file content + sourceContent := `export function helperFunction() { + return "helper result"; +} + +export const helperConstant = 42;` + + // Declaration file content + declContent := `export declare function helperFunction(): string; +export declare const helperConstant = 42; +//# sourceMappingURL=utils.d.ts.map` + + // Source map content + sourceMapContent := `{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/internal/helpers/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,eAAO,MAAM,cAAc,KAAK,CAAC"}` + + // Main file content + mainContent := `import { helperFunction } from "@internal/helpers/utils"; + +const result = helperFunction();` + + // tsconfig.json with path mapping + tsconfigContent := `{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@internal/*": ["dist/internal/*", "src/internal/*"] + }, + "declaration": true, + "declarationMap": true, + "outDir": "dist" + } +}` + + // Set up test files + files := map[string]any{ + "/tsconfig.json": tsconfigContent, + "/src/internal/helpers/utils.ts": sourceContent, + "/dist/internal/helpers/utils.d.ts": declContent, + "/dist/internal/helpers/utils.d.ts.map": sourceMapContent, + "/main.ts": mainContent, + } + + session, _ := projecttestutil.Setup(files) + + ctx := projecttestutil.WithRequestID(context.Background()) + session.DidOpenFile(ctx, "file:///main.ts", 1, mainContent, lsproto.LanguageKindTypeScript) + + languageService, err := session.GetLanguageService(ctx, "file:///main.ts") + assert.NilError(t, err) + + uri := lsproto.DocumentUri("file:///main.ts") + lspPosition := lsproto.Position{Line: 0, Character: 9} + + definition, err := languageService.ProvideDefinition(ctx, uri, lspPosition) + assert.NilError(t, err) + + if definition.Locations != nil { + assert.Assert(t, len(*definition.Locations) == 1, "Expected 1 definition location, got %d", len(*definition.Locations)) + + location := (*definition.Locations)[0] + expectedURI := ls.FileNameToDocumentURI("/src/internal/helpers/utils.ts") + actualURI := location.Uri + + assert.Equal(t, string(expectedURI), string(actualURI), "Should resolve to source .ts file, not .d.ts file") + } else if definition.Location != nil { + expectedURI := ls.FileNameToDocumentURI("/src/internal/helpers/utils.ts") + assert.Equal(t, string(expectedURI), string(definition.Location.Uri), "Should resolve to source .ts file, not .d.ts file") + } else { + t.Fatal("No definition found - expected to find definition") + } +} diff --git a/internal/project/compilerhost.go b/internal/project/compilerhost.go index 36cf2dfc4c..99cf571ff7 100644 --- a/internal/project/compilerhost.go +++ b/internal/project/compilerhost.go @@ -172,7 +172,7 @@ func (fs *compilerFS) ReadFile(path string) (contents string, ok bool) { if fh := fs.source.GetFile(path); fh != nil { return fh.Content(), true } - return "", false + return fs.source.FS().ReadFile(path) } // Realpath implements vfs.FS. diff --git a/testdata/baselines/reference/fourslash/goToDef/DeclarationMapGoToDefinition.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDef/DeclarationMapGoToDefinition.baseline.jsonc index 2f6b197efc..f25e7cfd7a 100644 --- a/testdata/baselines/reference/fourslash/goToDef/DeclarationMapGoToDefinition.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDef/DeclarationMapGoToDefinition.baseline.jsonc @@ -1,12 +1,12 @@ // === goToDefinition === -// === /indexdef.d.ts === +// === /index.ts === -// export declare class Foo { +// export class Foo { // member: string; -// [|methodName|](propName: SomeType): void; -// otherMethod(): { -// x: number; -// y?: undefined; +// [|methodName|](propName: SomeType): void {} +// otherMethod() { +// if (Math.random() > 0.5) { +// return {x: 42}; // // --- (line: 7) skipped --- diff --git a/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc index 26e09ac286..1464bdb6fa 100644 --- a/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc @@ -1,12 +1,11 @@ // === goToDefinition === -// === /BaseClass/Source.d.ts === +// === /BaseClass/Source.ts === -// declare class [|Control|] { -// constructor(); -// /** this is a super var */ -// myVar: boolean | 'yeah'; -// } -// //# sourceMappingURL=Source.d.ts.map +// class [|Control|]{ +// constructor(){ +// return; +// } +// // --- (line: 5) skipped --- // === /buttonClass/Source.ts === diff --git a/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsOutOfDateMapping.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsOutOfDateMapping.baseline.jsonc index 2d1cff1953..1972908cc8 100644 --- a/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsOutOfDateMapping.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDef/DeclarationMapsOutOfDateMapping.baseline.jsonc @@ -1,10 +1,9 @@ // === goToDefinition === -// === /home/src/workspaces/project/node_modules/a/dist/index.d.ts === +// === /home/src/workspaces/project/node_modules/a/src/index.ts === -// export declare class [|Foo|] { -// bar: any; +// export class [|Foo|] { // } -// //# sourceMappingURL=index.d.ts.map +// // === /home/src/workspaces/project/index.ts ===