diff --git a/internal/bundled/embed.go b/internal/bundled/embed.go index 36c00137b2..a1124a3e57 100644 --- a/internal/bundled/embed.go +++ b/internal/bundled/embed.go @@ -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 diff --git a/internal/bundled/noembed.go b/internal/bundled/noembed.go index 8a3e29235d..ffa5ab0288 100644 --- a/internal/bundled/noembed.go +++ b/internal/bundled/noembed.go @@ -44,3 +44,7 @@ var libPath = sync.OnceValue(func() string { return tspath.NormalizeSlashes(dir) }) + +func IsBundled(path string) bool { + return false +} diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index a488f7d2c5..9ed236d8d2 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -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) { @@ -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? diff --git a/internal/fourslash/tests/declarationMapGoToDefinitionChanges_test.go b/internal/fourslash/tests/declarationMapGoToDefinitionChanges_test.go new file mode 100644 index 0000000000..e946d0f5c7 --- /dev/null +++ b/internal/fourslash/tests/declarationMapGoToDefinitionChanges_test.go @@ -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") +} diff --git a/internal/ls/converters.go b/internal/ls/converters.go index b7730f3e5d..7f80bdc5b6 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -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" @@ -101,6 +102,9 @@ var extraEscapeReplacer = strings.NewReplacer( ) func FileNameToDocumentURI(fileName string) lsproto.DocumentUri { + if bundled.IsBundled(fileName) { + return lsproto.DocumentUri(fileName) + } if strings.HasPrefix(fileName, "^/") { scheme, rest, ok := strings.Cut(fileName[2:], "/") if !ok { diff --git a/internal/ls/definition.go b/internal/ls/definition.go index abefaf932e..7710135805 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -10,6 +10,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/tspath" ) func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) { @@ -57,6 +58,58 @@ 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 { + fileName := response.Location.Uri.FileName() + if tspath.IsDeclarationFileName(fileName) { + files = append(files, fileName) + } + } + + if response.Locations != nil { + for _, location := range *response.Locations { + fileName := location.Uri.FileName() + if tspath.IsDeclarationFileName(fileName) { + files = core.AppendIfUnique(files, fileName) + } + } + } + + if response.DefinitionLinks != nil { + for _, link := range *response.DefinitionLinks { + fileName := link.TargetUri.FileName() + if tspath.IsDeclarationFileName(fileName) { + files = core.AppendIfUnique(files, 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))) @@ -104,17 +157,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), + }, } } diff --git a/internal/ls/host.go b/internal/ls/host.go index dfeaa02a62..0ecfd6d1ae 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -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 } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index c7c4596dce..9a82cf4e47 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -8,10 +8,9 @@ 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( @@ -19,10 +18,9 @@ func NewLanguageService( host Host, ) *LanguageService { return &LanguageService{ - host: host, - program: program, - converters: host.Converters(), - documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, + host: host, + program: program, + converters: host.Converters(), } } @@ -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) } diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index 62eafda161..4cc2ecbfa5 100644 --- a/internal/ls/source_map.go +++ b/internal/ls/source_map.go @@ -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, } @@ -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 } } diff --git a/internal/lsp/lsproto/lsp.go b/internal/lsp/lsproto/lsp.go index dd172077c2..20d03aac46 100644 --- a/internal/lsp/lsproto/lsp.go +++ b/internal/lsp/lsproto/lsp.go @@ -7,6 +7,7 @@ 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" ) @@ -14,6 +15,9 @@ import ( 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 != "" { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 54f78f2e16..48ee658dad 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -450,7 +450,6 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDiagnosticInfo, (*Server).handleDocumentDiagnostic) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentHoverInfo, (*Server).handleHover) - registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDefinitionInfo, (*Server).handleDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentTypeDefinitionInfo, (*Server).handleTypeDefinition) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentCompletionInfo, (*Server).handleCompletion) registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentReferencesInfo, (*Server).handleReferences) @@ -464,6 +463,7 @@ var handlers = sync.OnceValue(func() handlerMap { registerLanguageServiceDocumentRequestHandler(handlers, lsproto.TextDocumentDocumentHighlightInfo, (*Server).handleDocumentHighlight) registerRequestHandler(handlers, lsproto.WorkspaceSymbolInfo, (*Server).handleWorkspaceSymbol) registerRequestHandler(handlers, lsproto.CompletionItemResolveInfo, (*Server).handleCompletionItemResolve) + registerDefinitionHandler(handlers) return handlers }) @@ -520,7 +520,7 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR if req.Params != nil { params = req.Params.(Req) } - ls, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) + ls, _, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) if err != nil { return err } @@ -537,6 +537,36 @@ func registerLanguageServiceDocumentRequestHandler[Req lsproto.HasTextDocumentUR } } +func registerDefinitionHandler(handlers handlerMap) { + handlers[lsproto.MethodTextDocumentDefinition] = func(s *Server, ctx context.Context, req *lsproto.RequestMessage) error { + var params *lsproto.DefinitionParams + // Ignore empty params. + if req.Params != nil { + params = req.Params.(*lsproto.DefinitionParams) + } + ls, snapshot, err := s.session.GetLanguageService(ctx, params.TextDocumentURI()) + if err != nil { + return err + } + defer s.recover(req) + resp, err := s.handleDefinition(ctx, ls, params) + if err != nil { + return err + } + if ctx.Err() != nil { + return ctx.Err() + } + mappedFiles := ls.GetFilesToMapFromDefinition(resp) + ls, err = s.session.GetLanguageServiceWithMappedFiles(ctx, params.TextDocumentURI(), snapshot, mappedFiles) + if err != nil { + return err + } + resp = ls.GetMappedDefinition(resp) + s.sendResult(req.ID, resp) + return nil + } +} + func (s *Server) recover(req *lsproto.RequestMessage) { if r := recover(); r != nil { stack := debug.Stack() @@ -799,7 +829,7 @@ func (s *Server) handleCompletionItemResolve(ctx context.Context, params *lsprot if err != nil { return nil, err } - languageService, err := s.session.GetLanguageService(ctx, ls.FileNameToDocumentURI(data.FileName)) + languageService, _, err := s.session.GetLanguageService(ctx, ls.FileNameToDocumentURI(data.FileName)) if err != nil { return nil, err } diff --git a/internal/project/ata/ata_test.go b/internal/project/ata/ata_test.go index 7a6f2915ed..9ce6adcbad 100644 --- a/internal/project/ata/ata_test.go +++ b/internal/project/ata/ata_test.go @@ -39,7 +39,7 @@ func TestATA(t *testing.T) { // Open the file session.DidOpenFile(context.Background(), uri, 1, content, lsproto.LanguageKindJavaScript) session.WaitForBackgroundTasks() - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) // Verify the local config.js file is included in the program program := ls.GetProgram() @@ -117,7 +117,7 @@ func TestATA(t *testing.T) { assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") // Verify the types file was installed - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) assert.NilError(t, err) program := ls.GetProgram() jqueryTypesFile := program.GetSourceFile(projecttestutil.TestTypingsLocation + "/node_modules/@types/jquery/index.d.ts") @@ -306,7 +306,7 @@ func TestATA(t *testing.T) { assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") // Verify the types file was installed - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) assert.NilError(t, err) jqueryTypesFile := ls.GetProgram().GetSourceFile(projecttestutil.TestTypingsLocation + "/node_modules/@types/jquery/index.d.ts") assert.Assert(t, jqueryTypesFile != nil, "jquery types should be installed") @@ -343,7 +343,7 @@ func TestATA(t *testing.T) { assert.Equal(t, calls[1].Args[2], "@types/jquery@latest") // Verify the types file was installed - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) assert.NilError(t, err) jqueryTypesFile := ls.GetProgram().GetSourceFile(projecttestutil.TestTypingsLocation + "/node_modules/@types/jquery/index.d.ts") assert.Assert(t, jqueryTypesFile != nil, "jquery types should be installed") @@ -383,7 +383,7 @@ func TestATA(t *testing.T) { Uri: lsproto.DocumentUri("file:///user/username/projects/project/package.json"), }}) // diagnostics refresh triggered - simulate by getting the language service - _, _ = session.GetLanguageService(context.Background(), uri) + _, _, _ = session.GetLanguageService(context.Background(), uri) session.WaitForBackgroundTasks() calls = utils.NpmExecutor().NpmInstallCalls() @@ -391,7 +391,7 @@ func TestATA(t *testing.T) { assert.Assert(t, slices.Contains(calls[1].Args, "@types/commander@latest")) // Verify types file present - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() assert.Assert(t, program.GetSourceFile(projecttestutil.TestTypingsLocation+"/node_modules/@types/commander/index.d.ts") != nil) @@ -419,7 +419,7 @@ func TestATA(t *testing.T) { assert.Equal(t, 2, len(calls)) assert.Assert(t, slices.Contains(calls[1].Args, "@types/commander@latest")) - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() // Types file present @@ -449,7 +449,7 @@ func TestATA(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, files["/user/username/projects/project/app.js"].(string), lsproto.LanguageKindJavaScript) session.WaitForBackgroundTasks() - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() // Expect updated content from installed typings @@ -475,7 +475,7 @@ func TestATA(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, files["/user/username/projects/project/app.js"].(string), lsproto.LanguageKindJavaScript) session.WaitForBackgroundTasks() - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() // Expect existing content unchanged @@ -509,7 +509,7 @@ func TestATA(t *testing.T) { assert.DeepEqual(t, npmCalls[0].Args, []string{"install", "--ignore-scripts", "types-registry@latest"}) // And the program should include the local @types/node declaration file - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() assert.Assert(t, program.GetSourceFile("/user/username/projects/project/node_modules/@types/node/index.d.ts") != nil) @@ -536,7 +536,7 @@ func TestATA(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, files["/user/username/projects/project/app.js"].(string), lsproto.LanguageKindJavaScript) session.WaitForBackgroundTasks() - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() // Expect updated content from installed typings @@ -562,7 +562,7 @@ func TestATA(t *testing.T) { session.DidOpenFile(context.Background(), uri, 1, files["/user/username/projects/project/app.js"].(string), lsproto.LanguageKindJavaScript) session.WaitForBackgroundTasks() - ls, err := session.GetLanguageService(context.Background(), uri) + ls, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) program := ls.GetProgram() // Expect existing content unchanged @@ -608,7 +608,7 @@ func TestATA(t *testing.T) { assert.Assert(t, slices.Contains(installArgs, "@types/node@latest")) // Verify the types files were installed - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///user/username/projects/project/app.js")) assert.NilError(t, err) program := ls.GetProgram() nodeTypesFile := program.GetSourceFile(projecttestutil.TestTypingsLocation + "/node_modules/@types/node/index.d.ts") diff --git a/internal/project/configfilechanges_test.go b/internal/project/configfilechanges_test.go index 036d243963..88f63777a7 100644 --- a/internal/project/configfilechanges_test.go +++ b/internal/project/configfilechanges_test.go @@ -42,7 +42,7 @@ func TestConfigFileChanges(t *testing.T) { }, }) - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) assert.Equal(t, ls.GetProgram().Options().Target, core.ScriptTargetESNext) }) @@ -61,7 +61,7 @@ func TestConfigFileChanges(t *testing.T) { }, }) - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) assert.Equal(t, ls.GetProgram().Options().Strict, core.TSFalse) }) @@ -82,7 +82,7 @@ func TestConfigFileChanges(t *testing.T) { }, }) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshotAfter, release := session.Snapshot() defer release() @@ -103,7 +103,7 @@ func TestConfigFileChanges(t *testing.T) { }, }) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -125,7 +125,7 @@ func TestConfigFileChanges(t *testing.T) { }, }) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/subfolder/foo.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/subfolder/foo.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -141,7 +141,7 @@ func TestConfigFileChanges(t *testing.T) { }, }) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/subfolder/foo.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/subfolder/foo.ts")) assert.NilError(t, err) snapshot, release = session.Snapshot() defer release() @@ -179,7 +179,7 @@ func TestConfigFileChanges(t *testing.T) { }) // Accessing the language service should trigger project update - ls, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + ls, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) assert.Equal(t, ls.GetProgram().Options().Strict, core.TSTrue) }) diff --git a/internal/project/configfileregistrybuilder.go b/internal/project/configfileregistrybuilder.go index a4b5a7ff6d..240fd22cbe 100644 --- a/internal/project/configfileregistrybuilder.go +++ b/internal/project/configfileregistrybuilder.go @@ -63,7 +63,7 @@ func (c *configFileRegistryBuilder) Finalize() *ConfigFileRegistry { } } - if configs, changedConfigs := c.configs.Finalize(); changedConfigs { + if configs, changedConfigs, _ := c.configs.Finalize(); changedConfigs { ensureCloned() newRegistry.configs = configs } diff --git a/internal/project/dirty/syncmap.go b/internal/project/dirty/syncmap.go index 3a69d2deef..d6dfacbb65 100644 --- a/internal/project/dirty/syncmap.go +++ b/internal/project/dirty/syncmap.go @@ -303,7 +303,7 @@ func (m *SyncMap[K, V]) Range(fn func(*SyncMapEntry[K, V]) bool) { } } -func (m *SyncMap[K, V]) Finalize() (map[K]V, bool) { +func (m *SyncMap[K, V]) Finalize() (map[K]V, bool, map[K]*Change[V]) { var changed bool result := m.base ensureCloned := func() { @@ -317,19 +317,32 @@ func (m *SyncMap[K, V]) Finalize() (map[K]V, bool) { } } + changes := make(map[K]*Change[V]) m.dirty.Range(func(key K, entry *SyncMapEntry[K, V]) bool { + var change *Change[V] if entry.delete { ensureCloned() delete(result, key) + change = &Change[V]{Old: entry.original, Deleted: true} } else if entry.dirty { ensureCloned() if m.finalizeValue != nil { - result[key] = m.finalizeValue(entry.value, entry.original) + value := m.finalizeValue(entry.value, entry.original) + result[key] = value + change = &Change[V]{Old: entry.original, New: value} } else { result[key] = entry.value + change = &Change[V]{Old: entry.original, New: entry.value} } } + changes[key] = change return true }) - return result, changed + return result, changed, changes +} + +type Change[V any] struct { + Old V + New V + Deleted bool } diff --git a/internal/project/overlayfs.go b/internal/project/overlayfs.go index 47ec2d95f9..043e7acdfa 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -69,7 +69,19 @@ func (f *fileBase) ECMALineInfo() *sourcemap.ECMALineInfo { type diskFile struct { fileBase - needsReload bool + needsReload bool + sourceMapInfo *sourceMapInfo +} + +type sourceMapInfo struct { + // Path to external source map + sourceMapPath string + // Inline source map + documentMapper *documentMapper +} + +type documentMapper struct { + m *sourcemap.DocumentPositionMapper } func newDiskFile(fileName string, content string) *diskFile { diff --git a/internal/project/project_test.go b/internal/project/project_test.go index 90ad9cda64..191d14b59b 100644 --- a/internal/project/project_test.go +++ b/internal/project/project_test.go @@ -29,7 +29,7 @@ func TestProjectProgramUpdateKind(t *testing.T) { } session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -46,12 +46,12 @@ func TestProjectProgramUpdateKind(t *testing.T) { } session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) session.DidChangeFile(context.Background(), "file:///src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{{ Partial: &lsproto.TextDocumentContentChangePartial{Text: "\n", Range: lsproto.Range{Start: lsproto.Position{Line: 0, Character: 20}, End: lsproto.Position{Line: 0, Character: 20}}}, }}) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -68,12 +68,12 @@ func TestProjectProgramUpdateKind(t *testing.T) { } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) err = utils.FS().WriteFile("/src/tsconfig.json", `{"compilerOptions": {"strict": false}}`, false) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{Uri: lsproto.DocumentUri("file:///src/tsconfig.json"), Type: lsproto.FileChangeTypeChanged}}) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -90,14 +90,14 @@ func TestProjectProgramUpdateKind(t *testing.T) { } session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) content := "export const y = 2;" err = utils.FS().WriteFile("/src/newfile.ts", content, false) assert.NilError(t, err) session.DidChangeWatchedFiles(context.Background(), []*lsproto.FileEvent{{Uri: lsproto.DocumentUri("file:///src/newfile.ts"), Type: lsproto.FileChangeTypeCreated}}) session.DidOpenFile(context.Background(), "file:///src/newfile.ts", 1, content, lsproto.LanguageKindTypeScript) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/newfile.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/newfile.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -115,13 +115,13 @@ func TestProjectProgramUpdateKind(t *testing.T) { } session, _ := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///src/index.ts", 1, files["/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err := session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) // Change index.ts to add an unresolvable import session.DidChangeFile(context.Background(), "file:///src/index.ts", 2, []lsproto.TextDocumentContentChangePartialOrWholeDocument{{ Partial: &lsproto.TextDocumentContentChangePartial{Text: "\nimport \"./does-not-exist\";\n", Range: lsproto.Range{Start: lsproto.Position{Line: 0, Character: 0}, End: lsproto.Position{Line: 0, Character: 0}}}, }}) - _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) + _, _, err = session.GetLanguageService(context.Background(), lsproto.DocumentUri("file:///src/index.ts")) assert.NilError(t, err) snapshot, release := session.Snapshot() defer release() @@ -161,7 +161,7 @@ func TestProject(t *testing.T) { // Sanity check: ensure ATA performed at least one install npmCalls := utils.NpmExecutor().NpmInstallCalls() assert.Assert(t, len(npmCalls) > 0, "expected at least one npm install call from ATA") - _, err := session.GetLanguageService(context.Background(), uri1) + _, _, err := session.GetLanguageService(context.Background(), uri1) assert.NilError(t, err) // 3) Open another inferred project file @@ -171,7 +171,7 @@ func TestProject(t *testing.T) { // 4) Get a language service for the second file // If commandLineWithTypingsFiles was not reset, the new program command line // won't include the newly opened file and this will fail. - _, err = session.GetLanguageService(context.Background(), uri2) + _, _, err = session.GetLanguageService(context.Background(), uri2) assert.NilError(t, err) }) } diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index c898292bea..b4ad7cf415 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -83,7 +83,7 @@ func (b *projectCollectionBuilder) Finalize(logger *logging.LogTree) (*ProjectCo } } - if configuredProjects, configuredProjectsChanged := b.configuredProjects.Finalize(); configuredProjectsChanged { + if configuredProjects, configuredProjectsChanged, _ := b.configuredProjects.Finalize(); configuredProjectsChanged { ensureCloned() newProjectCollection.configuredProjects = configuredProjects } diff --git a/internal/project/projectcollectionbuilder_test.go b/internal/project/projectcollectionbuilder_test.go index bc40f9fd1c..8684204992 100644 --- a/internal/project/projectcollectionbuilder_test.go +++ b/internal/project/projectcollectionbuilder_test.go @@ -38,7 +38,7 @@ func TestProjectCollectionBuilder(t *testing.T) { assert.Assert(t, snapshot.ProjectCollection.ConfiguredProject(tspath.Path("/user/username/projects/myproject/tsconfig-src.json")) != nil) // Ensure request can use existing snapshot - _, err := session.GetLanguageService(context.Background(), uri) + _, _, err := session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) requestSnapshot, requestRelease := session.Snapshot() defer requestRelease() diff --git a/internal/project/projectreferencesprogram_test.go b/internal/project/projectreferencesprogram_test.go index df4062bfb7..118a3b2e51 100644 --- a/internal/project/projectreferencesprogram_test.go +++ b/internal/project/projectreferencesprogram_test.go @@ -289,7 +289,7 @@ func TestProjectReferencesProgram(t *testing.T) { }, }) - _, err = session.GetLanguageService(context.Background(), uri) + _, _, err = session.GetLanguageService(context.Background(), uri) assert.NilError(t, err) snapshot, release = session.Snapshot() defer release() diff --git a/internal/project/refcounting_test.go b/internal/project/refcounting_test.go index eef05a7898..08c1923495 100644 --- a/internal/project/refcounting_test.go +++ b/internal/project/refcounting_test.go @@ -67,7 +67,7 @@ func TestRefCountingCaches(t *testing.T) { }, }, }) - ls, err := session.GetLanguageService(context.Background(), "file:///user/username/projects/myproject/src/main.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///user/username/projects/myproject/src/main.ts") assert.NilError(t, err) assert.Assert(t, ls.GetProgram().GetSourceFile("/user/username/projects/myproject/src/main.ts") != main) assert.Equal(t, ls.GetProgram().GetSourceFile("/user/username/projects/myproject/src/utils.ts"), utils) @@ -95,7 +95,7 @@ func TestRefCountingCaches(t *testing.T) { assert.Equal(t, utilsEntry.refCount, 1) session.DidCloseFile(context.Background(), "file:///user/username/projects/myproject/src/main.ts") - _, err := session.GetLanguageService(context.Background(), "file:///user/username/projects/myproject/src/utils.ts") + _, _, err := session.GetLanguageService(context.Background(), "file:///user/username/projects/myproject/src/utils.ts") assert.NilError(t, err) assert.Equal(t, utilsEntry.refCount, 1) assert.Equal(t, mainEntry.refCount, 0) diff --git a/internal/project/session.go b/internal/project/session.go index a51831ed21..0929be2abd 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -17,6 +17,7 @@ import ( "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/background" + "github.com/microsoft/typescript-go/internal/project/dirty" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" @@ -149,7 +150,10 @@ func NewSession(init *SessionInit) *Session { init.Options, parseCache, extendedConfigCache, - &ConfigFileRegistry{}, + &ConfigFileRegistry{ + configs: map[tspath.Path]*configFileEntry{}, + configFileNames: map[tspath.Path]*configFileNames{}, + }, nil, toPath, ), @@ -339,7 +343,7 @@ func (s *Session) Snapshot() (*Snapshot, func()) { } } -func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, error) { +func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUri) (*ls.LanguageService, *Snapshot, error) { var snapshot *Snapshot fileChanges, overlays, ataChanges := s.flushChanges(ctx) updateSnapshot := !fileChanges.IsEmpty() || len(ataChanges) > 0 @@ -371,9 +375,9 @@ func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUr project = snapshot.GetDefaultProject(uri) } if project == nil { - return nil, fmt.Errorf("no project found for URI %s", uri) + return nil, nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project.GetProgram(), snapshot), nil + return ls.NewLanguageService(project.GetProgram(), snapshot), snapshot, nil } func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*overlay, change SnapshotChange) *Snapshot { @@ -418,6 +422,9 @@ func (s *Session) WaitForBackgroundTasks() { func updateWatch[T any](ctx context.Context, session *Session, logger logging.Logger, oldWatcher, newWatcher *WatchedFiles[T]) []error { var errors []error + if oldWatcher == newWatcher { + return errors + } session.watchesMu.Lock() defer session.watchesMu.Unlock() if newWatcher != nil { @@ -688,3 +695,47 @@ func (s *Session) triggerATAForUpdatedProjects(newSnapshot *Snapshot) { } } } + +// Creates a new language service that is based on the input snapshot but has additional files loaded. +func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsproto.DocumentUri, snapshot *Snapshot, files []string) (*ls.LanguageService, error) { + project := snapshot.GetDefaultProject(uri) + if project == nil { + // This should not happen, since we ensured the project was loaded the first time a language service was requested. + return nil, fmt.Errorf("no project found for URI %s", uri) + } + snapshotWithFiles, changes := snapshot.CloneWithSourceMaps(files, s) + if s.options.LoggingEnabled { + s.logger.Write(snapshotWithFiles.builderLogs.String()) + s.logger.Write("") + } + s.backgroundQueue.Enqueue(ctx, func(ctx context.Context) { + s.updateSnapshotWithDiskChanges(changes, snapshot, snapshotWithFiles) + }) + return ls.NewLanguageService(project.GetProgram(), snapshotWithFiles), nil +} + +func (s *Session) updateSnapshotWithDiskChanges(changes map[tspath.Path]*dirty.Change[*diskFile], snapshot, snapshotWithFiles *Snapshot) { + s.snapshotMu.Lock() + oldSnapshot := s.snapshot + var newSnapshot *Snapshot + if oldSnapshot == snapshot { + newSnapshot = snapshotWithFiles + } else { + newSnapshot = oldSnapshot.CloneWithDiskChanges(changes, s) + } + s.snapshot = newSnapshot + s.snapshotMu.Unlock() + + // We don't need to dispose the old snapshot here because the new snapshot will have the same programs + // and config files as the old snapshot, so the reference counts will be the same. + if s.options.LoggingEnabled { + s.logger.Write(newSnapshot.builderLogs.String()) + s.logger.Write("") + } + + ctx := context.Background() + err := updateWatch(ctx, s, s.logger, oldSnapshot.extraDiskFilesWatch, newSnapshot.extraDiskFilesWatch) + if err != nil && s.options.LoggingEnabled { + s.logger.Log(fmt.Errorf("error updating extra disk file watches: %v", err)) + } +} diff --git a/internal/project/session_test.go b/internal/project/session_test.go index 164f7eea68..ae6519ea8b 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -55,7 +55,7 @@ func TestSession(t *testing.T) { assert.Assert(t, configuredProject != nil) // Get language service to access the program - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) @@ -108,7 +108,7 @@ func TestSession(t *testing.T) { defer release() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 1) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.js") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.js") assert.NilError(t, err) program := ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/index.js") != nil) @@ -123,7 +123,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() @@ -145,7 +145,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() @@ -160,7 +160,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, defaultFiles["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() indexFileBefore := programBefore.GetSourceFile("/home/projects/TS/p1/src/index.ts") @@ -183,7 +183,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() @@ -200,7 +200,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) // Verify y.ts is not initially in the program - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() assert.Check(t, programBefore.GetSourceFile("/home/projects/TS/p1/y.ts") == nil) @@ -223,7 +223,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() @@ -246,7 +246,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() assert.Equal(t, len(programBefore.GetSourceFiles()), 2) @@ -286,7 +286,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programAfter := lsAfter.GetProgram() assert.Equal(t, len(programAfter.GetSourceFiles()), 3) @@ -308,7 +308,7 @@ func TestSession(t *testing.T) { assert.NilError(t, utils.FS().Remove("/home/projects/TS/p1/src/x.ts")) session.DidCloseFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts") - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) @@ -318,7 +318,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, "", lsproto.LanguageKindTypeScript) - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) @@ -342,7 +342,7 @@ func TestSession(t *testing.T) { session.DidCloseFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts") - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Check(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") == nil) @@ -352,7 +352,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, "", lsproto.LanguageKindTypeScript) - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Assert(t, program.GetSourceFile("/home/projects/TS/p1/src/x.ts") != nil) @@ -447,11 +447,11 @@ func TestSession(t *testing.T) { defer release() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - ls1, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls1, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program1 := ls1.GetProgram() - ls2, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts") + ls2, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts") assert.NilError(t, err) program2 := ls2.GetProgram() @@ -480,11 +480,11 @@ func TestSession(t *testing.T) { defer release() assert.Equal(t, len(snapshot.ProjectCollection.Projects()), 2) - ls1, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls1, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program1 := ls1.GetProgram() - ls2, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts") + ls2, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p2/src/index.ts") assert.NilError(t, err) program2 := ls2.GetProgram() @@ -506,7 +506,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() @@ -520,7 +520,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) // Program should remain the same since the file is open and changes are handled through DidChangeTextDocument assert.Equal(t, programBefore, lsAfter.GetProgram()) @@ -533,7 +533,7 @@ func TestSession(t *testing.T) { session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() @@ -547,7 +547,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) assert.Check(t, lsAfter.GetProgram() != programBefore) }) @@ -579,7 +579,7 @@ func TestSession(t *testing.T) { LoggingEnabled: true, }) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - lsBefore, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsBefore, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) programBefore := lsBefore.GetProgram() session.WaitForBackgroundTasks() @@ -606,7 +606,7 @@ func TestSession(t *testing.T) { }, }) - lsAfter, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + lsAfter, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) assert.Check(t, lsAfter.GetProgram() != programBefore) }) @@ -631,7 +631,7 @@ func TestSession(t *testing.T) { session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) @@ -651,7 +651,7 @@ func TestSession(t *testing.T) { }, }) - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) @@ -672,7 +672,7 @@ func TestSession(t *testing.T) { session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) @@ -687,7 +687,7 @@ func TestSession(t *testing.T) { }, }) - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 1) @@ -709,7 +709,7 @@ func TestSession(t *testing.T) { session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/x.ts", 1, files["/home/projects/TS/p1/src/x.ts"].(string), lsproto.LanguageKindTypeScript) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program := ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 0) @@ -724,7 +724,7 @@ func TestSession(t *testing.T) { }, }) - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/x.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/x.ts"))), 1) @@ -744,7 +744,7 @@ func TestSession(t *testing.T) { session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() @@ -763,7 +763,7 @@ func TestSession(t *testing.T) { }) // Error should be resolved - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) @@ -784,7 +784,7 @@ func TestSession(t *testing.T) { session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() @@ -803,7 +803,7 @@ func TestSession(t *testing.T) { }) // Error should be resolved and the new file should be included in the program - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) @@ -824,7 +824,7 @@ func TestSession(t *testing.T) { session, utils := projecttestutil.Setup(files) session.DidOpenFile(context.Background(), "file:///home/projects/TS/p1/src/index.ts", 1, files["/home/projects/TS/p1/src/index.ts"].(string), lsproto.LanguageKindTypeScript) - ls, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program := ls.GetProgram() @@ -843,7 +843,7 @@ func TestSession(t *testing.T) { }) // Error should be resolved and the new file should be included in the program - ls, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") + ls, _, err = session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/src/index.ts") assert.NilError(t, err) program = ls.GetProgram() assert.Equal(t, len(program.GetSemanticDiagnostics(projecttestutil.WithRequestID(t.Context()), program.GetSourceFile("/home/projects/TS/p1/src/index.ts"))), 0) diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 8d41e426c5..de89605a9f 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -3,6 +3,8 @@ package project import ( "context" "fmt" + "maps" + "slices" "sync/atomic" "time" @@ -33,6 +35,9 @@ type Snapshot struct { ProjectCollection *ProjectCollection ConfigFileRegistry *ConfigFileRegistry compilerOptionsForInferredProjects *core.CompilerOptions + // Disk files present in the snapshot due to source map references. + extraDiskFiles map[tspath.Path]string + extraDiskFilesWatch *WatchedFiles[map[tspath.Path]string] builderLogs *logging.LogTree apiError error @@ -62,6 +67,12 @@ func NewSnapshot( } s.converters = ls.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap) s.refCount.Store(1) + s.extraDiskFiles = make(map[tspath.Path]string) + s.extraDiskFilesWatch = NewWatchedFiles( + "extra snapshot files", + lsproto.WatchKindChange|lsproto.WatchKindDelete, + getComputeGlobPatterns(s.sessionOptions.CurrentDirectory, fs.fs.UseCaseSensitiveFileNames()), + ) return s } @@ -97,10 +108,6 @@ func (s *Snapshot) ID() uint64 { return s.id } -func (s *Snapshot) UseCaseSensitiveFileNames() bool { - return s.fs.fs.UseCaseSensitiveFileNames() -} - func (s *Snapshot) ReadFile(fileName string) (string, bool) { handle := s.GetFile(fileName) if handle == nil { @@ -109,6 +116,10 @@ func (s *Snapshot) ReadFile(fileName string) (string, bool) { return handle.Content(), true } +func (s *Snapshot) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { + return s.fs.GetDocumentPositionMapper(fileName) +} + type APISnapshotRequest struct { OpenProjects *collections.Set[string] CloseProjects *collections.Set[tspath.Path] @@ -215,6 +226,8 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma projectCollection, configFileRegistry := projectCollectionBuilder.Finalize(logger) + extraDiskFiles := s.extraDiskFiles + extraDiskFilesWatch := s.extraDiskFilesWatch // Clean cached disk files not touched by any open project. It's not important that we do this on // file open specifically, but we don't need to do it on every snapshot clone. if len(change.fileChanges.Opened) != 0 { @@ -227,25 +240,53 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } // The set of seen files can change only if a program was constructed (not cloned) during this snapshot. if changedFiles { + extraDiskFiles = maps.Clone(s.extraDiskFiles) cleanFilesStart := time.Now() removedFiles := 0 + // Files referenced by projects. + rootFiles := collections.Set[tspath.Path]{} + // Map of file to the file that references it, e.g. `foo.d.ts.map` -> `foo.d.ts` + referencedToFile := map[tspath.Path]tspath.Path{} fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { + if sourceMapInfo := entry.Value().sourceMapInfo; sourceMapInfo != nil { + if sourceMapInfo.sourceMapPath != "" { + referencedToFile[s.toPath(sourceMapInfo.sourceMapPath)] = entry.Key() + } else if mapper := sourceMapInfo.documentMapper.m; mapper != nil { + for _, sourceFile := range mapper.GetSourceFiles() { + referencedToFile[s.toPath(sourceFile)] = entry.Key() + } + } + } for _, project := range projectCollection.Projects() { if project.host.seenFiles.Has(entry.Key()) { + rootFiles.Add(entry.Key()) + return true + } + } + return true + }) + // Delete files not transitively referenced by any project. + fs.diskFiles.Range(func(entry *dirty.SyncMapEntry[tspath.Path, *diskFile]) bool { + referenced := entry.Key() + for referenced != "" { + if rootFiles.Has(referenced) { return true } + referenced = referencedToFile[referenced] } entry.Delete() + delete(extraDiskFiles, entry.Key()) removedFiles++ return true }) if session.options.LoggingEnabled { logger.Logf("Removed %d cached files in %v", removedFiles, time.Since(cleanFilesStart)) } + extraDiskFilesWatch = extraDiskFilesWatch.Clone(extraDiskFiles) } } - snapshotFS, _ := fs.Finalize() + snapshotFS, _, _ := fs.Finalize() newSnapshot := NewSnapshot( newSnapshotID, snapshotFS, @@ -294,10 +335,132 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } } + newSnapshot.extraDiskFiles = extraDiskFiles + newSnapshot.extraDiskFilesWatch = extraDiskFilesWatch + + logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start)) + return newSnapshot +} + +// Creates a clone of the snapshot that ensures source maps are computed for the given files. +// Returns the new snapshot and a patch of changes made to diskFiles. +// The changes only include additional files that were read, or source map information added to existing files. +func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Snapshot, map[tspath.Path]*dirty.Change[*diskFile]) { + var logger *logging.LogTree + if session.options.LoggingEnabled { + logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with source maps for %v", s.id, genFiles)) + } + + start := time.Now() + fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) + for _, genFile := range genFiles { + fs.computeDocumentPositionMapper(genFile) + } + snapshotFS, _, changes := fs.Finalize() + logger.Logf(`New files due to source maps: %v`, slices.Collect(maps.Keys(changes))) + newSnapshot := s.cloneFromSnapshotFSChanges(snapshotFS, changes, session, logger) + logger.Logf(`Finished cloning snapshot %d into snapshot %d with source maps in %v`, s.id, newSnapshot.id, time.Since(start)) + return newSnapshot, changes +} + +func (s *Snapshot) CloneWithDiskChanges(changes map[tspath.Path]*dirty.Change[*diskFile], session *Session) *Snapshot { + var logger *logging.LogTree + if session.options.LoggingEnabled { + logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with disk changes changes %v", s.id, slices.Collect(maps.Keys(changes)))) + } + + start := time.Now() + fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) + fs.applyDiskFileChanges(changes) + snapshotFS, _, _ := fs.Finalize() + newSnapshot := s.cloneFromSnapshotFSChanges(snapshotFS, changes, session, logger) logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start)) return newSnapshot } +func (s *Snapshot) cloneFromSnapshotFSChanges( + snapshotFS *snapshotFS, + changes map[tspath.Path]*dirty.Change[*diskFile], + session *Session, + logger *logging.LogTree, +) *Snapshot { + newId := session.snapshotID.Add(1) + newSnapshot := NewSnapshot( + newId, + snapshotFS, + s.sessionOptions, + session.parseCache, + session.extendedConfigCache, + s.ConfigFileRegistry, + s.compilerOptionsForInferredProjects, + s.toPath, + ) + + newSnapshot.parentId = s.id + newSnapshot.ProjectCollection = s.ProjectCollection + newSnapshot.builderLogs = logger + newSnapshot.extraDiskFiles = maps.Clone(s.extraDiskFiles) + for path, change := range changes { + if change.Deleted { + if change.Old != nil { + panic("Deleting files not supported") + } + continue + } + if change.Old == nil { + newSnapshot.extraDiskFiles[path] = change.New.FileName() + continue + } + } + newSnapshot.extraDiskFilesWatch = s.extraDiskFilesWatch.Clone(newSnapshot.extraDiskFiles) + return newSnapshot +} + +func getComputeGlobPatterns(workspaceDirectory string, useCaseSensitiveFileNames bool) func(files map[tspath.Path]string) patternsAndIgnored { + return func(files map[tspath.Path]string) patternsAndIgnored { + comparePathsOptions := tspath.ComparePathsOptions{ + CurrentDirectory: workspaceDirectory, + UseCaseSensitiveFileNames: useCaseSensitiveFileNames, + } + externalDirectories := make(map[tspath.Path]string) + var seenDirs collections.Set[string] + var includeWorkspace bool + for path, fileName := range files { + if !seenDirs.AddIfAbsent(tspath.GetDirectoryPath(string(path))) { + continue + } + if tspath.ContainsPath(workspaceDirectory, string(path), comparePathsOptions) { + includeWorkspace = true + } + externalDirectories[path.GetDirectoryPath()] = fileName + } + + var globs []string + var ignored map[string]struct{} + if includeWorkspace { + globs = append(globs, getRecursiveGlobPattern(workspaceDirectory)) + } + if len(externalDirectories) > 0 { + externalDirectoryParents, ignoredExternalDirs := tspath.GetCommonParents( + slices.Collect(maps.Values(externalDirectories)), + minWatchLocationDepth, + getPathComponentsForWatching, + comparePathsOptions, + ) + slices.Sort(externalDirectoryParents) + ignored = ignoredExternalDirs + for _, dir := range externalDirectoryParents { + globs = append(globs, getRecursiveGlobPattern(dir)) + } + } + + return patternsAndIgnored{ + patterns: globs, + ignored: ignored, + } + } +} + func (s *Snapshot) Ref() { s.refCount.Add(1) } diff --git a/internal/project/snapshot_test.go b/internal/project/snapshot_test.go index ded5e16b5e..8c944d9ccd 100644 --- a/internal/project/snapshot_test.go +++ b/internal/project/snapshot_test.go @@ -56,12 +56,10 @@ func TestSnapshot(t *testing.T) { }, }, }) - _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.ts") + _, _, err := session.GetLanguageService(context.Background(), "file:///home/projects/TS/p1/index.ts") assert.NilError(t, err) snapshotAfter, release := session.Snapshot() defer release() - - // Configured project was updated by a clone assert.Equal(t, snapshotAfter.ProjectCollection.ConfiguredProject(tspath.Path("/home/projects/ts/p1/tsconfig.json")).ProgramUpdateKind, ProgramUpdateKindCloned) // Inferred project wasn't updated last snapshot change, so its program update kind is still NewFiles assert.Equal(t, snapshotBefore.ProjectCollection.InferredProject(), snapshotAfter.ProjectCollection.InferredProject()) diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index 441299e318..38e96adf3c 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -1,11 +1,9 @@ package project import ( - "sync" - - "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/dirty" + "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" @@ -27,11 +25,8 @@ type snapshotFS struct { fs vfs.FS overlays map[tspath.Path]*overlay diskFiles map[tspath.Path]*diskFile - readFiles collections.SyncMap[tspath.Path, memoizedDiskFile] } -type memoizedDiskFile func() FileHandle - func (s *snapshotFS) FS() vfs.FS { return s.fs } @@ -43,14 +38,21 @@ func (s *snapshotFS) GetFile(fileName string) FileHandle { if file, ok := s.diskFiles[s.toPath(fileName)]; ok { return file } - newEntry := memoizedDiskFile(sync.OnceValue(func() FileHandle { - if contents, ok := s.fs.ReadFile(fileName); ok { - return newDiskFile(fileName, contents) + return nil +} + +func (s *snapshotFS) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { + if file, ok := s.diskFiles[s.toPath(fileName)]; ok { + if file.sourceMapInfo != nil { + if file.sourceMapInfo.documentMapper != nil { + return file.sourceMapInfo.documentMapper.m + } + if file.sourceMapInfo.sourceMapPath != "" { + return s.GetDocumentPositionMapper(file.sourceMapInfo.sourceMapPath) + } } - return nil - })) - entry, _ := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry) - return entry() + } + return nil } type snapshotFSBuilder struct { @@ -81,14 +83,14 @@ func (s *snapshotFSBuilder) FS() vfs.FS { return s.fs } -func (s *snapshotFSBuilder) Finalize() (*snapshotFS, bool) { - diskFiles, changed := s.diskFiles.Finalize() +func (s *snapshotFSBuilder) Finalize() (*snapshotFS, bool, map[tspath.Path]*dirty.Change[*diskFile]) { + diskFiles, changed, changes := s.diskFiles.Finalize() return &snapshotFS{ fs: s.fs, overlays: s.overlays, diskFiles: diskFiles, toPath: s.toPath, - }, changed + }, changed, changes } func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { @@ -100,12 +102,66 @@ func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) Fil if file, ok := s.overlays[path]; ok { return file } + if file := s.getDiskFileByPath(fileName, path); file != nil { + return file + } + return nil +} + +func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { + for uri := range change.Changed.Keys() { + path := s.toPath(uri.FileName()) + if entry, ok := s.diskFiles.Load(path); ok { + entry.Change(func(file *diskFile) { + file.needsReload = true + }) + } + } + for uri := range change.Deleted.Keys() { + path := s.toPath(uri.FileName()) + if entry, ok := s.diskFiles.Load(path); ok { + entry.Change(func(file *diskFile) { + file.needsReload = true + }) + } + } +} + +// UseCaseSensitiveFileNames implements sourcemap.Host. +func (s *snapshotFSBuilder) UseCaseSensitiveFileNames() bool { + return s.fs.UseCaseSensitiveFileNames() +} + +// GetECMALineInfo implements sourcemap.Host. +func (s *snapshotFSBuilder) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { + if file := s.getDiskFile(fileName); file != nil { + return file.ECMALineInfo() + } + return nil +} + +// ReadFile implements sourcemap.Host. +func (s *snapshotFSBuilder) ReadFile(fileName string) (contents string, ok bool) { + if file := s.getDiskFile(fileName); file != nil { + return file.Content(), true + } + return "", false +} + +func (s *snapshotFSBuilder) getDiskFile(fileName string) *diskFile { + return s.getDiskFileByPath(fileName, s.toPath(fileName)) +} + +func (s *snapshotFSBuilder) getDiskFileByPath(fileName string, path tspath.Path) *diskFile { entry, _ := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true}) if entry != nil { entry.Locked(func(entry dirty.Value[*diskFile]) { if entry.Value() != nil && !entry.Value().MatchesDiskText() { if content, ok := s.fs.ReadFile(fileName); ok { entry.Change(func(file *diskFile) { + if file.sourceMapInfo != nil || file.lineInfo != nil { + panic("Should not have source map info or line info when reloading file") + } file.content = content file.hash = xxh3.Hash128([]byte(content)) file.needsReload = false @@ -122,20 +178,82 @@ func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) Fil return entry.Value() } -func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { - for uri := range change.Changed.Keys() { - path := s.toPath(uri.FileName()) - if entry, ok := s.diskFiles.Load(path); ok { - entry.Change(func(file *diskFile) { - file.needsReload = true - }) - } +func (s *snapshotFSBuilder) computeDocumentPositionMapper(genFileName string) { + genFilePath := s.toPath(genFileName) + // For simplicity of implementation, we always use the disk file for computing and storing source map information. + // If the file is in the overlays (i.e. open in the editor) and matches the disk text, using the disk file is ok. + // If the file is in the overlays and does not match the disk text, we are in a bad state: + // the .d.ts file has been edited in the editor, so the source map, if present, is out of date. + s.getDiskFileByPath(genFileName, genFilePath) + entry, _ := s.diskFiles.Load(genFilePath) + if entry == nil { + return } - for uri := range change.Deleted.Keys() { - path := s.toPath(uri.FileName()) - if entry, ok := s.diskFiles.Load(path); ok { + file := entry.Value() + if file == nil { + return + } + // Source map information already computed + if file.sourceMapInfo != nil { + return + } + url, isInline := sourcemap.GetSourceMapURL(s, genFileName) + if isInline { + // Store document mapper directly in disk file for an inline source map + docMapper := sourcemap.ConvertDocumentToSourceMapper(s, url, genFileName) + entry.Change(func(file *diskFile) { + file.sourceMapInfo = &sourceMapInfo{documentMapper: &documentMapper{m: docMapper}} + }) + } else { + // Store path to map file + entry.Change(func(file *diskFile) { + file.sourceMapInfo = &sourceMapInfo{sourceMapPath: url} + }) + } + if url != "" { + s.computeDocumentPositionMapperForMap(url) + } +} + +func (s *snapshotFSBuilder) computeDocumentPositionMapperForMap(mapFileName string) { + mapFilePath := s.toPath(mapFileName) + mapFile := s.getDiskFileByPath(mapFileName, mapFilePath) + if mapFile == nil { + return + } + if mapFile.sourceMapInfo != nil { + return + } + docMapper := sourcemap.ConvertDocumentToSourceMapper(s, mapFile.Content(), mapFileName) + entry, _ := s.diskFiles.Load(mapFilePath) + entry.Change(func(file *diskFile) { + file.sourceMapInfo = &sourceMapInfo{documentMapper: &documentMapper{m: docMapper}} + }) +} + +func (s *snapshotFSBuilder) applyDiskFileChanges( + diskChanges map[tspath.Path]*dirty.Change[*diskFile], +) { + for path, change := range diskChanges { + if change.Deleted { + if change.Old != nil { + panic("Deleting files not supported") + } + // Failed file read + continue + } + // New file + if change.Old == nil { + s.diskFiles.LoadOrStore(path, change.New) + continue + } + // Updated file + entry, _ := s.diskFiles.Load(path) + if entry != nil { entry.Change(func(file *diskFile) { - file.needsReload = true + if file.Hash() == change.Old.Hash() { + file.sourceMapInfo = change.New.sourceMapInfo + } }) } } diff --git a/internal/project/untitled_test.go b/internal/project/untitled_test.go index 54d3cfa763..1f1354839c 100644 --- a/internal/project/untitled_test.go +++ b/internal/project/untitled_test.go @@ -53,7 +53,7 @@ x++;` session.DidOpenFile(ctx, "file:///Untitled-2.ts", 1, testContent, lsproto.LanguageKindTypeScript) // Get language service - languageService, err := session.GetLanguageService(ctx, "file:///Untitled-2.ts") + languageService, _, err := session.GetLanguageService(ctx, "file:///Untitled-2.ts") assert.NilError(t, err) // Test the filename that the source file reports @@ -125,7 +125,7 @@ x++;` assert.Assert(t, snapshot.ProjectCollection.InferredProject() != nil) // Get language service for the untitled file - languageService, err := session.GetLanguageService(ctx, "untitled:Untitled-2") + languageService, _, err := session.GetLanguageService(ctx, "untitled:Untitled-2") assert.NilError(t, err) program := languageService.GetProgram() diff --git a/internal/project/watch.go b/internal/project/watch.go index 2354040e3e..24855121a6 100644 --- a/internal/project/watch.go +++ b/internal/project/watch.go @@ -357,5 +357,5 @@ func getNonRootFileGlobs(workspaceDir string, libDirectory string, sourceFiles [ } func getRecursiveGlobPattern(directory string) string { - return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory), "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json}") + return fmt.Sprintf("%s/%s", tspath.RemoveTrailingDirectorySeparator(directory), "**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,json,d.ts,d.ts.map}") } diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go index d89d542a56..e8ade13149 100644 --- a/internal/sourcemap/source_mapper.go +++ b/internal/sourcemap/source_mapper.go @@ -49,6 +49,10 @@ type DocumentPositionMapper struct { sourceMappings map[SourceIndex][]*SourceMappedPosition } +func (d *DocumentPositionMapper) GetSourceFiles() []string { + return d.sourceFileAbsolutePaths +} + func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath string) *DocumentPositionMapper { mapDirectory := tspath.GetDirectoryPath(mapPath) var sourceRoot string @@ -226,20 +230,19 @@ func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do } } -func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPositionMapper { +func GetSourceMapURL(host Host, generatedFileName string) (url string, isInline bool) { mapFileName := tryGetSourceMappingURL(host, generatedFileName) if mapFileName != "" { if base64Object, matched := tryParseBase64Url(mapFileName); matched { if base64Object != "" { if decoded, err := base64.StdEncoding.DecodeString(base64Object); err == nil { - return convertDocumentToSourceMapper(host, string(decoded), generatedFileName) + return string(decoded), true } } // Not a data URL we can parse, skip it mapFileName = "" } } - var possibleMapLocations []string if mapFileName != "" { possibleMapLocations = append(possibleMapLocations, mapFileName) @@ -247,14 +250,14 @@ func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPos possibleMapLocations = append(possibleMapLocations, generatedFileName+".map") for _, location := range possibleMapLocations { mapFileName := tspath.GetNormalizedAbsolutePath(location, tspath.GetDirectoryPath(generatedFileName)) - if mapFileContents, ok := host.ReadFile(mapFileName); ok { - return convertDocumentToSourceMapper(host, mapFileContents, mapFileName) + if _, ok := host.ReadFile(mapFileName); ok { + return mapFileName, false } } - return nil + return "", false } -func convertDocumentToSourceMapper(host Host, contents string, mapFileName string) *DocumentPositionMapper { +func ConvertDocumentToSourceMapper(host Host, contents string, mapFileName string) *DocumentPositionMapper { sourceMap := tryParseRawSourceMap(contents) if sourceMap == nil || len(sourceMap.Sources) == 0 || sourceMap.File == "" || sourceMap.Mappings == "" { // invalid map diff --git a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinitionChanges.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinitionChanges.baseline.jsonc new file mode 100644 index 0000000000..56cadf29ae --- /dev/null +++ b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinitionChanges.baseline.jsonc @@ -0,0 +1,34 @@ +// === goToDefinition === +// === /index.ts === +// export class Foo { +// member: string; +// [|methodName|](propName: SomeType): void {} +// otherMethod() { +// if (Math.random() > 0.5) { +// return {x: 42}; +// // --- (line: 7) skipped --- + +// === /mymodule.ts === +// import * as mod from "./index"; +// const instance = new mod.Foo(); +// instance./*GOTO DEF*/methodName({member: 12}); +// instance.otherMethod(); + + + +// === goToDefinition === +// === /index.ts === +// export class Foo { +// member: string; +// methodName(propName: SomeType): void {} +// [|otherMethod|]() { +// if (Math.random() > 0.5) { +// return {x: 42}; +// } +// // --- (line: 8) skipped --- + +// === /mymodule.ts === +// import * as mod from "./index"; +// const instance = new mod.Foo(); +// instance.methodName({member: 12}); +// instance./*GOTO DEF*/otherMethod(); \ No newline at end of file