Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions internal/ls/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down
172 changes: 171 additions & 1 deletion internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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),
})
}
Expand Down Expand Up @@ -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
}
90 changes: 90 additions & 0 deletions internal/ls/definition_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Author

Choose a reason for hiding this comment

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

I specifically added this to have an automated test for functionality that wasn't tested by the baselines.

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")
}
}
2 changes: 1 addition & 1 deletion internal/project/compilerhost.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Author

Choose a reason for hiding this comment

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

This fixes issues with being able to get the source map files. It conforms to the fallback mechanism that TypeScript had which was going to the disk - https://github.com/microsoft/TypeScript/blob/release-5.9/src/services/sourcemaps.ts#L147-L151

}

// Realpath implements vfs.FS.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ---


Expand Down
Original file line number Diff line number Diff line change
@@ -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 ===
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ===
Expand Down