Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c920059
source maps + go to def
gabritto Sep 26, 2025
8423ecf
refactor, fix and update tests
gabritto Sep 29, 2025
ca93817
refactor
gabritto Sep 29, 2025
672434a
Merge branch 'main' into gabritto/def1
gabritto Sep 29, 2025
bb7a5b3
fix typo
gabritto Sep 29, 2025
cef9f67
refactor LS host
gabritto Sep 30, 2025
c80e7a3
rename line maps with ECMA/LSP
gabritto Oct 1, 2025
4a6c6ea
Merge remote-tracking branch 'origin/gabritto/renamelinemaps' into ga…
gabritto Oct 1, 2025
8debdea
add type alias for line starts
gabritto Oct 1, 2025
bd07e42
Merge branch 'main' into gabritto/def1
gabritto Oct 1, 2025
75267d1
remove fileExists from LS host
gabritto Oct 1, 2025
c26faa9
source maps for disk files
gabritto Oct 6, 2025
459ebfe
WIP: overlays
gabritto Oct 6, 2025
e2fb018
fix bundled and update LS
gabritto Oct 7, 2025
4eab0b7
Revert "WIP: overlays"
gabritto Oct 7, 2025
166b687
refactors/fixes
gabritto Oct 7, 2025
b285539
Merge branch 'main' into gabritto/cachedsourcemaps
gabritto Oct 7, 2025
78e4e23
fix merge
gabritto Oct 7, 2025
bb1f175
fix submodule
gabritto Oct 7, 2025
1a4114a
file watching
gabritto Oct 8, 2025
dd76d55
ignore file watching messages on fourslash
gabritto Oct 8, 2025
5d37720
remove ability for snapshot to read files
gabritto Oct 8, 2025
a66fe91
add test with declaration map changes
gabritto Oct 8, 2025
a68e5e1
cleanup, logging
gabritto Oct 10, 2025
32f5cb5
remove unused function
gabritto Oct 10, 2025
c6cf35f
Merge branch 'main' into gabritto/cachedsourcemaps
gabritto Oct 10, 2025
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
5 changes: 5 additions & 0 deletions internal/bundled/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ func libPath() string {
return scheme + "libs"
}

func IsBundled(path string) bool {
_, ok := splitPath(path)
return ok
}

// wrappedFS is implemented directly rather than going through [io/fs.FS].
// Our vfs.FS works with file contents in terms of strings, and that's
// what go:embed does under the hood, but going through fs.FS will cause
Expand Down
4 changes: 4 additions & 0 deletions internal/bundled/noembed.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,7 @@ var libPath = sync.OnceValue(func() string {

return tspath.NormalizeSlashes(dir)
})

func IsBundled(path string) bool {
return false
}
22 changes: 17 additions & 5 deletions internal/fourslash/fourslash.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,19 @@ func sendRequest[Params, Resp any](t *testing.T, f *FourslashTest, info lsproto.
params,
)
f.writeMsg(t, req.Message())
resp := f.readMsg(t)
if resp == nil {
return nil, *new(Resp), false
for {
resp := f.readMsg(t)
if resp == nil {
return nil, *new(Resp), false
}
// Ignore file watching requests: fourslash client already sends a notification for every file changed.
if resp.Kind == lsproto.MessageKindRequest && (resp.AsRequest().Method == lsproto.MethodClientRegisterCapability ||
resp.AsRequest().Method == lsproto.MethodClientUnregisterCapability) {
continue
}
result, ok := resp.AsResponse().Result.(Resp)
return resp, result, ok
}
result, ok := resp.AsResponse().Result.(Resp)
return resp, result, ok
}

func sendNotification[Params any](t *testing.T, f *FourslashTest, info lsproto.NotificationInfo[Params], params Params) {
Expand Down Expand Up @@ -1197,6 +1204,11 @@ func (f *FourslashTest) getSelection() core.TextRange {
)
}

func (f *FourslashTest) ReplaceAll(t *testing.T, text string) {
script := f.getScriptInfo(f.activeFilename)
f.editScriptAndUpdateMarkers(t, f.activeFilename, 0, len(script.content), text)
}

func (f *FourslashTest) Replace(t *testing.T, start int, length int, text string) {
f.editScriptAndUpdateMarkers(t, f.activeFilename, start, start+length, text)
// f.checkPostEditInvariants() // !!! do we need this?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package fourslash_test

import (
"testing"

"github.com/microsoft/typescript-go/internal/fourslash"
"github.com/microsoft/typescript-go/internal/testutil"
)

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

defer testutil.RecoverAndFail(t, "Panic on fourslash test")
const content = `// @Filename: index.ts
export class Foo {
member: string;
methodName(propName: SomeType): void {}
otherMethod() {
if (Math.random() > 0.5) {
return {x: 42};
}
return {y: "yes"};
}
}

export interface SomeType {
member: number;
}
// @Filename: index.d.ts.map
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,qBAAa,GAAG;IACZ,MAAM,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;CAC5C;AAED,MAAM,WAAW,QAAQ;IACrB,MAAM,EAAE,MAAM,CAAC;CAClB"}
// @Filename: index.d.ts
export declare class Foo {
member: string;
methodName(propName: SomeType): void;
}
export interface SomeType {
member: number;
}
//# sourceMappingURL=index.d.ts.map
// @Filename: mymodule.ts
import * as mod from "./index";
const instance = new mod.Foo();
instance.[|/*1*/methodName|]({member: 12});
instance.[|/*2*/otherMethod|]();`
f := fourslash.NewFourslash(t, nil /*capabilities*/, content)
f.VerifyBaselineGoToDefinition(t, "1")
f.GoToFile(t, "index.d.ts")
f.ReplaceAll(t, `export declare class Foo {
member: string;
methodName(propName: SomeType): void;
otherMethod(): {
x: number;
y?: undefined;
} | {
y: string;
x?: undefined;
};
}
export interface SomeType {
member: number;
}
//# sourceMappingURL=index.d.ts.map`)
f.GoToFile(t, "index.d.ts.map")
f.ReplaceAll(t, `{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA,qBAAa,GAAG;IACZ,MAAM,EAAE,MAAM,CAAC;IACV,UAAU,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IACzC,WAAW;;;;;;;CAMd;AAED,MAAM,WAAW,QAAQ;IACrB,MAAM,EAAE,MAAM,CAAC;CAClB"}`)
f.VerifyBaselineGoToDefinition(t, "2")
}
4 changes: 4 additions & 0 deletions internal/ls/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"unicode/utf16"
"unicode/utf8"

"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/tspath"
Expand Down Expand Up @@ -101,6 +102,9 @@ var extraEscapeReplacer = strings.NewReplacer(
)

func FileNameToDocumentURI(fileName string) lsproto.DocumentUri {
if bundled.IsBundled(fileName) {
Copy link
Member Author

Choose a reason for hiding this comment

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

This is needed to avoid crashes in the tests, since a file name for a bundled file doesn't round trip with the current implementations of FileNameToDocumentURI and documentUri.FileName().

return lsproto.DocumentUri(fileName)
}
if strings.HasPrefix(fileName, "^/") {
scheme, rest, ok := strings.Cut(fileName[2:], "/")
if !ok {
Expand Down
56 changes: 51 additions & 5 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,49 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp
return l.createLocationsFromDeclarations(declarations), nil
}

func (l *LanguageService) GetFilesToMapFromDefinition(response lsproto.DefinitionResponse) []string {
var files []string

if response.Location != nil {
files = append(files, response.Location.Uri.FileName())
}

if response.Locations != nil {
for _, location := range *response.Locations {
files = core.AppendIfUnique(files, location.Uri.FileName())
Copy link
Member

Choose a reason for hiding this comment

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

Perf nit: AppendIfUnique is an order of magnitude slower than building a map[string]struct{} and converting to a slice by the time N=100.

Copy link
Member

Choose a reason for hiding this comment

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

More importantly, I was expecting to see a tspath.IsDeclarationFileName condition somewhere around here. Is there ever a case where we would need to try to map a location except when it’s a declaration file?

}
}

if response.DefinitionLinks != nil {
for _, link := range *response.DefinitionLinks {
files = core.AppendIfUnique(files, link.TargetUri.FileName())
}
}

return files
}

func (l *LanguageService) GetMappedDefinition(definitions lsproto.DefinitionResponse) lsproto.DefinitionResponse {
if definitions.Location != nil {
definitions.Location = l.getMappedLocation(definitions.Location)
}
if definitions.Locations != nil {
for i, loc := range *definitions.Locations {
(*definitions.Locations)[i] = *l.getMappedLocation(&loc)
}
}
if definitions.DefinitionLinks != nil {
for i, link := range *definitions.DefinitionLinks {
mappedTarget := l.getMappedLocation(&lsproto.Location{Uri: link.TargetUri, Range: link.TargetRange})
mappedSelection := l.getMappedLocation(&lsproto.Location{Uri: link.TargetUri, Range: link.TargetSelectionRange})
(*definitions.DefinitionLinks)[i].TargetUri = mappedTarget.Uri
(*definitions.DefinitionLinks)[i].TargetRange = mappedTarget.Range
(*definitions.DefinitionLinks)[i].TargetSelectionRange = mappedSelection.Range
}
}
return definitions
}

func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) {
program, file := l.getProgramAndFile(documentURI)
node := astnav.GetTouchingPropertyName(file, int(l.converters.LineAndCharacterToPosition(file, position)))
Expand Down Expand Up @@ -104,17 +147,20 @@ func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.No
for _, decl := range declarations {
file := ast.GetSourceFileOfNode(decl)
name := core.OrElse(ast.GetNameOfDeclaration(decl), decl)
nodeRange := createRangeFromNode(name, file)
mappedLocation := l.getMappedLocation(file.FileName(), nodeRange)
locations = core.AppendIfUnique(locations, mappedLocation)
locations = core.AppendIfUnique(locations, lsproto.Location{
Uri: FileNameToDocumentURI(file.FileName()),
Range: *l.createLspRangeFromNode(name, file),
})
}
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{Locations: &locations}
}

func (l *LanguageService) createLocationFromFileAndRange(file *ast.SourceFile, textRange core.TextRange) lsproto.DefinitionResponse {
mappedLocation := l.getMappedLocation(file.FileName(), textRange)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{
Location: &mappedLocation,
Location: &lsproto.Location{
Uri: FileNameToDocumentURI(file.FileName()),
Range: *l.createLspRangeFromBounds(textRange.Pos(), textRange.End(), file),
},
}
}

Expand Down
3 changes: 1 addition & 2 deletions internal/ls/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ package ls
import "github.com/microsoft/typescript-go/internal/sourcemap"

type Host interface {
UseCaseSensitiveFileNames() bool
ReadFile(path string) (contents string, ok bool)
Converters() *Converters
GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo
GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper
}
33 changes: 7 additions & 26 deletions internal/ls/languageservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@ import (
)

type LanguageService struct {
host Host
program *compiler.Program
converters *Converters
documentPositionMappers map[string]*sourcemap.DocumentPositionMapper
host Host
program *compiler.Program
converters *Converters
}

func NewLanguageService(
program *compiler.Program,
host Host,
) *LanguageService {
return &LanguageService{
host: host,
program: program,
converters: host.Converters(),
documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{},
host: host,
program: program,
converters: host.Converters(),
}
}

Expand All @@ -46,22 +44,5 @@ func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*c
}

func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper {
d, ok := l.documentPositionMappers[fileName]
if !ok {
d = sourcemap.GetDocumentPositionMapper(l, fileName)
l.documentPositionMappers[fileName] = d
}
return d
}

func (l *LanguageService) ReadFile(fileName string) (string, bool) {
return l.host.ReadFile(fileName)
}

func (l *LanguageService) UseCaseSensitiveFileNames() bool {
return l.host.UseCaseSensitiveFileNames()
}

func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo {
return l.host.GetECMALineInfo(fileName)
return l.host.GetDocumentPositionMapper(fileName)
}
19 changes: 9 additions & 10 deletions internal/ls/source_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@ import (
"github.com/microsoft/typescript-go/internal/tspath"
)

func (l *LanguageService) getMappedLocation(fileName string, fileRange core.TextRange) lsproto.Location {
startPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.Pos()))
func (l *LanguageService) getMappedLocation(location *lsproto.Location) *lsproto.Location {
script := l.getScript(location.Uri.FileName())
rangeStart := l.converters.LineAndCharacterToPosition(script, location.Range.Start)
rangeEnd := l.converters.LineAndCharacterToPosition(script, location.Range.End)
startPos := l.tryGetSourcePosition(location.Uri.FileName(), rangeStart)
if startPos == nil {
lspRange := l.createLspRangeFromRange(fileRange, l.getScript(fileName))
return lsproto.Location{
Uri: FileNameToDocumentURI(fileName),
Range: *lspRange,
}
return location
}
endPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.End()))
endPos := l.tryGetSourcePosition(location.Uri.FileName(), rangeEnd)
debug.Assert(endPos.FileName == startPos.FileName, "start and end should be in same file")
newRange := core.NewTextRange(startPos.Pos, endPos.Pos)
lspRange := l.createLspRangeFromRange(newRange, l.getScript(startPos.FileName))
return lsproto.Location{
return &lsproto.Location{
Uri: FileNameToDocumentURI(startPos.FileName),
Range: *lspRange,
}
Expand Down Expand Up @@ -54,7 +53,7 @@ func (l *LanguageService) tryGetSourcePosition(
) *sourcemap.DocumentPosition {
newPos := l.tryGetSourcePositionWorker(fileName, position)
if newPos != nil {
if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist
if _, ok := l.host.ReadFile(newPos.FileName); !ok { // File doesn't exist
return nil
}
}
Expand Down
4 changes: 4 additions & 0 deletions internal/lsp/lsproto/lsp.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ import (

"github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext"
"github.com/microsoft/typescript-go/internal/bundled"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/tspath"
)

type DocumentUri string // !!!

func (uri DocumentUri) FileName() string {
if bundled.IsBundled(string(uri)) {
return string(uri)
}
if strings.HasPrefix(string(uri), "file://") {
parsed := core.Must(url.Parse(string(uri)))
if parsed.Host != "" {
Expand Down
Loading