From c920059b9e5497873e8202b5889063b4443c2eb4 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 26 Sep 2025 23:09:14 +0000 Subject: [PATCH 01/23] source maps + go to def --- internal/api/api.go | 6 +- internal/core/core.go | 19 ++ internal/ls/definition.go | 37 ++- internal/ls/languageservice.go | 51 ++- internal/ls/source_map.go | 84 +++++ internal/project/session.go | 2 +- internal/project/snapshot.go | 12 + internal/project/snapshotfs.go | 20 ++ internal/sourcemap/lineinfo.go | 2 +- internal/sourcemap/source_mapper.go | 307 ++++++++++++++++++ .../harnessutil/sourcemap_recorder.go | 2 +- 11 files changed, 526 insertions(+), 16 deletions(-) create mode 100644 internal/ls/source_map.go create mode 100644 internal/sourcemap/source_mapper.go diff --git a/internal/api/api.go b/internal/api/api.go index 15fc7c096f..c521516df9 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -160,7 +160,7 @@ func (api *API) GetSymbolAtPosition(ctx context.Context, projectId Handle[projec return nil, errors.New("project not found") } - languageService := ls.NewLanguageService(project, snapshot.Converters()) + languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()) symbol, err := languageService.GetSymbolAtPosition(ctx, fileName, position) if err != nil || symbol == nil { return nil, err @@ -202,7 +202,7 @@ func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[projec if node == nil { return nil, fmt.Errorf("node of kind %s not found at position %d in file %q", kind.String(), pos, sourceFile.FileName()) } - languageService := ls.NewLanguageService(project, snapshot.Converters()) + languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()) symbol := languageService.GetSymbolAtLocation(ctx, node) if symbol == nil { return nil, nil @@ -232,7 +232,7 @@ func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Pr if !ok { return nil, fmt.Errorf("symbol %q not found", symbolHandle) } - languageService := ls.NewLanguageService(project, snapshot.Converters()) + languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()) t := languageService.GetTypeOfSymbol(ctx, symbol) if t == nil { return nil, nil diff --git a/internal/core/core.go b/internal/core/core.go index 45198a0370..2b53728039 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -648,3 +648,22 @@ func Deduplicate[T comparable](slice []T) []T { } return slice } + +func DeduplicateSorted[T any](slice []T, isEqual func(a, b T) bool) []T { + if len(slice) == 0 { + return slice + } + last := slice[0] + deduplicated := slice[:1] + for i := 1; i < len(slice); i++ { + next := slice[i] + if isEqual(last, next) { + continue + } + + deduplicated = append(deduplicated, next) + last = next + } + + return deduplicated +} diff --git a/internal/ls/definition.go b/internal/ls/definition.go index 5e765fcc0b..0ea3c2076f 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -8,6 +8,7 @@ import ( "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" ) @@ -22,28 +23,54 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp c, done := program.GetTypeCheckerForFile(ctx, file) defer done() + return l.getMappedDefinition(l.provideDefinitions(c, node)), nil +} + +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}) + debug.Assert(mappedTarget.Uri == mappedSelection.Uri, "target and selection should be in same file") + (*definitions.DefinitionLinks)[i].TargetUri = mappedTarget.Uri + (*definitions.DefinitionLinks)[i].TargetRange = mappedTarget.Range + (*definitions.DefinitionLinks)[i].TargetSelectionRange = mappedSelection.Range + } + } + return definitions +} + +func (l *LanguageService) provideDefinitions(c *checker.Checker, node *ast.Node) lsproto.DefinitionResponse { if node.Kind == ast.KindOverrideKeyword { if sym := getSymbolForOverriddenMember(c, node); sym != nil { - return l.createLocationsFromDeclarations(sym.Declarations), nil + return l.createLocationsFromDeclarations(sym.Declarations) } } if ast.IsJumpStatementTarget(node) { if label := getTargetLabel(node.Parent, node.Text()); label != nil { - return l.createLocationsFromDeclarations([]*ast.Node{label}), nil + return l.createLocationsFromDeclarations([]*ast.Node{label}) } } if node.Kind == ast.KindCaseKeyword || node.Kind == ast.KindDefaultKeyword && ast.IsDefaultClause(node.Parent) { if stmt := ast.FindAncestor(node.Parent, ast.IsSwitchStatement); stmt != nil { file := ast.GetSourceFileOfNode(stmt) - return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())), nil + return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())) } } if node.Kind == ast.KindReturnKeyword || node.Kind == ast.KindYieldKeyword || node.Kind == ast.KindAwaitKeyword { if fn := ast.FindAncestor(node, ast.IsFunctionLikeDeclaration); fn != nil { - return l.createLocationsFromDeclarations([]*ast.Node{fn}), nil + return l.createLocationsFromDeclarations([]*ast.Node{fn}) } } @@ -54,7 +81,7 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp nonFunctionDeclarations := core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return !ast.IsFunctionLike(node) }) declarations = append(nonFunctionDeclarations, calledDeclaration) } - return l.createLocationsFromDeclarations(declarations), nil + return l.createLocationsFromDeclarations(declarations) } func (l *LanguageService) ProvideTypeDefinition(ctx context.Context, documentURI lsproto.DocumentUri, position lsproto.Position) (lsproto.DefinitionResponse, error) { diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 7e47e50e34..484c78d512 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -4,17 +4,29 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/sourcemap" ) type LanguageService struct { - host Host - converters *Converters + host Host + converters *Converters + documentPositionMappers map[string]sourcemap.DocumentPositionMapper // !!! TODO: needs sync? + useCaseSensitiveFileNames bool + readFile func(path string) (contents string, ok bool) } -func NewLanguageService(host Host, converters *Converters) *LanguageService { +func NewLanguageService( + host Host, + converters *Converters, + readFile func(path string) (contents string, ok bool), + useCaseSensitiveFileNames bool, +) *LanguageService { return &LanguageService{ - host: host, - converters: converters, + host: host, + converters: converters, + readFile: readFile, + useCaseSensitiveFileNames: useCaseSensitiveFileNames, + documentPositionMappers: map[string]sourcemap.DocumentPositionMapper{}, } } @@ -36,3 +48,32 @@ func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*c } return program, file } + +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.readFile(fileName) +} + +func (l *LanguageService) UseCaseSensitiveFileNames() bool { + return l.useCaseSensitiveFileNames +} + +func (l *LanguageService) GetLineInfo(fileName string) *sourcemap.LineInfo { + text, ok := l.ReadFile(fileName) + if !ok { + return nil + } + lineMap := l.converters.getLineMap(fileName) + if lineMap == nil { + return nil + } + return sourcemap.CreateLineInfo(text, lineMap.LineStarts) +} diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go new file mode 100644 index 0000000000..57d297e921 --- /dev/null +++ b/internal/ls/source_map.go @@ -0,0 +1,84 @@ +package ls + +import ( + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/sourcemap" + "github.com/microsoft/typescript-go/internal/tspath" +) + +func (l *LanguageService) getMappedLocation(location *lsproto.Location) *lsproto.Location { + uriStart, start := l.tryGetSourceLSPPosition(location.Uri.FileName(), &location.Range.Start) + if uriStart == nil { + return location + } + uriEnd, end := l.tryGetSourceLSPPosition(location.Uri.FileName(), &location.Range.End) + debug.Assert(uriEnd == uriStart, "start and end should be in same file") + debug.Assert(end != nil, "end position should be valid") + return &lsproto.Location{ + Uri: *uriStart, + Range: lsproto.Range{Start: *start, End: *end}, + } +} + +func (l *LanguageService) getMappedPosition() { + // !!! HERE +} + +type script struct { + fileName string + text string +} + +func (s *script) FileName() string { + return s.fileName +} + +func (s *script) Text() string { + return s.text +} + +func (l *LanguageService) tryGetSourceLSPPosition( + genFileName string, + position *lsproto.Position, +) (*lsproto.DocumentUri, *lsproto.Position) { + genText, ok := l.ReadFile(genFileName) + if !ok { + return nil, nil // That shouldn't happen + } + genPos := l.converters.LineAndCharacterToPosition(&script{fileName: genFileName, text: genText}, *position) + documentPos := l.tryGetSourcePosition(genFileName, genPos) + if documentPos == nil { + return nil, nil + } + documentURI := FileNameToDocumentURI(documentPos.FileName) + sourceText, ok := l.ReadFile(documentPos.FileName) + if !ok { + return nil, nil + } + sourcePos := l.converters.PositionToLineAndCharacter( + &script{fileName: documentPos.FileName, text: sourceText}, + core.TextPos(documentPos.Pos), + ) + return &documentURI, &sourcePos +} + +func (l *LanguageService) tryGetSourcePosition( + fileName string, + genPosition core.TextPos, +) *sourcemap.DocumentPosition { + if !tspath.IsDeclarationFileName(fileName) { + return nil + } + + positionMapper := l.GetDocumentPositionMapper(fileName) + documentPos := positionMapper.GetSourcePosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(genPosition)}) + if documentPos == nil { + return nil + } + if newPos := l.tryGetSourcePosition(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil { + return newPos + } + return documentPos +} diff --git a/internal/project/session.go b/internal/project/session.go index 4e78c28884..0a85e93c9d 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -367,7 +367,7 @@ func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUr if project == nil { return nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project, snapshot.Converters()), nil + return ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()), nil } func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*overlay, change SnapshotChange) *Snapshot { diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index b5d7d1722e..9485b9ba00 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -89,6 +89,18 @@ 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 { + return "", false + } + return handle.Content(), true +} + type APISnapshotRequest struct { OpenProjects *collections.Set[string] CloseProjects *collections.Set[tspath.Path] diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index 29b51a1dfe..071a048aa1 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -1,6 +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/tspath" @@ -24,6 +27,12 @@ type snapshotFS struct { fs vfs.FS overlays map[tspath.Path]*overlay diskFiles map[tspath.Path]*diskFile + readFiles collections.SyncMap[tspath.Path, memoizedFileEntry] +} + +// !!! newtype? +type memoizedFileEntry struct { + read func() *diskFile } func (s *snapshotFS) FS() vfs.FS { @@ -37,6 +46,17 @@ func (s *snapshotFS) GetFile(fileName string) FileHandle { if file, ok := s.diskFiles[s.toPath(fileName)]; ok { return file } + newEntry := memoizedFileEntry{ + read: sync.OnceValue(func() *diskFile { + if contents, ok := s.fs.ReadFile(fileName); ok { + return newDiskFile(fileName, contents) + } + return nil + }), + } + if entry, ok := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry); ok { + return entry.read() + } return nil } diff --git a/internal/sourcemap/lineinfo.go b/internal/sourcemap/lineinfo.go index 6baf74788e..81df871dfb 100644 --- a/internal/sourcemap/lineinfo.go +++ b/internal/sourcemap/lineinfo.go @@ -7,7 +7,7 @@ type LineInfo struct { lineStarts []core.TextPos } -func GetLineInfo(text string, lineStarts []core.TextPos) *LineInfo { +func CreateLineInfo(text string, lineStarts []core.TextPos) *LineInfo { return &LineInfo{ text: text, lineStarts: lineStarts, diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go new file mode 100644 index 0000000000..ff407cdb0c --- /dev/null +++ b/internal/sourcemap/source_mapper.go @@ -0,0 +1,307 @@ +package sourcemap + +import ( + "encoding/base64" + "slices" + "strings" + + "github.com/go-json-experiment/json" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" + "github.com/microsoft/typescript-go/internal/scanner" + "github.com/microsoft/typescript-go/internal/stringutil" + "github.com/microsoft/typescript-go/internal/tspath" +) + +type Host interface { + UseCaseSensitiveFileNames() bool + GetLineInfo(fileName string) *LineInfo + ReadFile(fileName string) (string, bool) +} + +// Maps source positions to generated positions and vice versa. +type DocumentPositionMapper interface { + GetSourcePosition(*DocumentPosition) *DocumentPosition + GetGeneratedPosition(*DocumentPosition) *DocumentPosition +} + +// Similar to `Mapping`, but position-based. +type MappedPosition struct { + generatedPosition int + sourcePosition int + sourceIndex SourceIndex + nameIndex NameIndex +} + +const ( + missingPosition = -1 +) + +func (m *MappedPosition) isSourceMappedPosition() bool { + return m.sourceIndex != MissingSource && m.sourcePosition != missingPosition +} + +type SourceMappedPosition = MappedPosition + +type documentPositionMapper struct { + useCaseSensitiveFileNames bool + + sourceFileAbsolutePaths []string + sourceToSourceIndexMap map[string]SourceIndex + generatedAbsoluteFilePath string + + generatedMappings []*MappedPosition + sourceMappings map[SourceIndex][]*SourceMappedPosition +} + +func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath string) DocumentPositionMapper { + mapDirectory := tspath.GetDirectoryPath(mapPath) + var sourceRoot string + if sourceMap.SourceRoot != "" { + tspath.GetNormalizedAbsolutePath(sourceMap.SourceRoot, mapDirectory) + } else { + sourceRoot = mapDirectory + } + generatedAbsoluteFilePath := tspath.GetNormalizedAbsolutePath(sourceMap.File, mapDirectory) + sourceFileAbsolutePaths := core.Map(sourceMap.Sources, func(source string) string { + return tspath.GetNormalizedAbsolutePath(source, sourceRoot) + }) + useCaseSensitiveFileNames := host.UseCaseSensitiveFileNames() + sourceToSourceIndexMap := make(map[string]SourceIndex, len(sourceFileAbsolutePaths)) + for i, source := range sourceFileAbsolutePaths { + sourceToSourceIndexMap[tspath.GetCanonicalFileName(source, useCaseSensitiveFileNames)] = SourceIndex(i) + } + + var decodedMappings []*MappedPosition + var generatedMappings []*MappedPosition + sourceMappings := make(map[SourceIndex][]*SourceMappedPosition) + + // getDecodedMappings() + decoder := DecodeMappings(sourceMap.Mappings) + for mapping := range decoder.Values() { + // processMapping() + generatedPosition := -1 + lineInfo := host.GetLineInfo(generatedAbsoluteFilePath) + if lineInfo != nil { + generatedPosition = scanner.ComputePositionOfLineAndCharacter(lineInfo.lineStarts, mapping.GeneratedLine, mapping.GeneratedCharacter) + } + + sourcePosition := -1 + if mapping.IsSourceMapping() { + lineInfo := host.GetLineInfo(sourceFileAbsolutePaths[mapping.SourceIndex]) + if lineInfo != nil { + pos := scanner.ComputePositionOfLineAndCharacter(lineInfo.lineStarts, mapping.SourceLine, mapping.SourceCharacter) + sourcePosition = pos + } + } + + decodedMappings = append(decodedMappings, &MappedPosition{ + generatedPosition: generatedPosition, + sourceIndex: mapping.SourceIndex, + sourcePosition: sourcePosition, + nameIndex: mapping.NameIndex, + }) + } + if decoder.Error() != nil { + decodedMappings = nil + } + + // getSourceMappings() + for _, mapping := range decodedMappings { + if !mapping.isSourceMappedPosition() { + continue + } + sourceIndex := mapping.sourceIndex + list := sourceMappings[sourceIndex] + list = append(list, &SourceMappedPosition{ + generatedPosition: mapping.generatedPosition, + sourceIndex: sourceIndex, + sourcePosition: mapping.sourcePosition, + nameIndex: mapping.nameIndex, + }) + sourceMappings[sourceIndex] = list + } + for i, list := range sourceMappings { + slices.SortFunc(list, func(a, b *SourceMappedPosition) int { + debug.Assert(a.sourceIndex == b.sourceIndex, "All source mappings should have the same source index") + return a.sourcePosition - b.sourcePosition + }) + sourceMappings[i] = core.DeduplicateSorted(list, func(a, b *SourceMappedPosition) bool { + return a.generatedPosition == b.generatedPosition && + a.sourceIndex == b.sourceIndex && + a.sourcePosition == b.sourcePosition + }) + } + + // getGeneratedMappings() + generatedMappings = decodedMappings + slices.SortFunc(generatedMappings, func(a, b *MappedPosition) int { + return a.generatedPosition - b.generatedPosition + }) + generatedMappings = core.DeduplicateSorted(generatedMappings, func(a, b *MappedPosition) bool { + return a.generatedPosition == b.generatedPosition && + a.sourceIndex == b.sourceIndex && + a.sourcePosition == b.sourcePosition + }) + + return &documentPositionMapper{ + useCaseSensitiveFileNames: useCaseSensitiveFileNames, + sourceFileAbsolutePaths: sourceFileAbsolutePaths, + sourceToSourceIndexMap: sourceToSourceIndexMap, + generatedAbsoluteFilePath: generatedAbsoluteFilePath, + generatedMappings: generatedMappings, + sourceMappings: sourceMappings, + } +} + +type DocumentPosition struct { + FileName string + Pos int +} + +func (d *documentPositionMapper) GetSourcePosition(loc *DocumentPosition) *DocumentPosition { + if d == nil { + return nil + } + if len(d.generatedMappings) == 0 { + return nil + } + + targetIndex, _ := slices.BinarySearchFunc(d.generatedMappings, loc.Pos, func(m *MappedPosition, pos int) int { + return m.generatedPosition - pos + }) + + if targetIndex < 0 || targetIndex >= len(d.generatedMappings) { + return nil + } + + mapping := d.generatedMappings[targetIndex] + if !mapping.isSourceMappedPosition() { + return nil + } + + // Closest position + return &DocumentPosition{ + FileName: d.sourceFileAbsolutePaths[mapping.sourceIndex], + Pos: mapping.sourcePosition, + } +} + +func (d *documentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *DocumentPosition { + if d == nil { + return nil + } + sourceIndex, ok := d.sourceToSourceIndexMap[tspath.GetCanonicalFileName(loc.FileName, d.useCaseSensitiveFileNames)] + if !ok { + return nil + } + if sourceIndex < 0 || int(sourceIndex) >= len(d.sourceMappings) { + return nil + } + sourceMappings := d.sourceMappings[sourceIndex] + targetIndex, _ := slices.BinarySearchFunc(sourceMappings, loc.Pos, func(m *SourceMappedPosition, pos int) int { + return m.sourcePosition - pos + }) + + if targetIndex < 0 || targetIndex >= len(sourceMappings) { + return nil + } + + mapping := sourceMappings[targetIndex] + if mapping.sourceIndex != sourceIndex { + return nil + } + + // Closest position + return &DocumentPosition{ + FileName: d.generatedAbsoluteFilePath, + Pos: mapping.generatedPosition, + } +} + +func GetDocumentPositionMapper(host Host, generatedFileName string) DocumentPositionMapper { + mapFileName := tryGetSourceMappingURL(host, generatedFileName) + if mapFileName != "" { + if base64Object, matched := tryParseBase46Url(mapFileName); matched { + if base64Object != "" { + if decoded, err := base64.StdEncoding.DecodeString(base64Object); err == nil { + return convertDocumentToSourceMapper(host, string(decoded), generatedFileName) + } + } + // Not a data URL we can parse, skip it + mapFileName = "" + } + } + + var possibleMapLocations []string + if mapFileName != "" { + possibleMapLocations = append(possibleMapLocations, mapFileName) + } + 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) + } + } + return nil +} + +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 + return nil + } + + // Don't support source maps that contain inlined sources + if core.Some(sourceMap.SourcesContent, func(s *string) bool { return s != nil }) { + return nil + } + + return createDocumentPositionMapper(host, sourceMap, mapFileName) +} + +func tryParseRawSourceMap(contents string) *RawSourceMap { + sourceMap := &RawSourceMap{} + err := json.Unmarshal([]byte(contents), sourceMap) + if err != nil { + return nil + } + if sourceMap.Version != 3 { + return nil + } + return sourceMap +} + +func tryGetSourceMappingURL(host Host, fileName string) string { + lineInfo := host.GetLineInfo(fileName) + return TryGetSourceMappingURL(lineInfo) +} + +// Originally: /^data:(?:application\/json;charset=[uU][tT][fF]-8;base64,([A-Za-z0-9+/=]+)$)?/ +// Should have been /^data:(?:application\/json;(?:charset=[uU][tT][fF]-8;)?base64,([A-Za-z0-9+/=]+)$)?/ +func tryParseBase46Url(url string) (parseableUrl string, isBase64Url bool) { + var found bool + if url, found = strings.CutPrefix(url, `data:`); !found { + return "", false + } + if url, found = strings.CutPrefix(url, `application/json;`); !found { + return "", true + } + if url, found = strings.CutPrefix(url, `charset=`); found { + if !strings.EqualFold(url[:len(`utf-8;`)], `utf-8;`) { + return "", true + } + url = url[len(`utf-8;`):] + } + if url, found = strings.CutPrefix(url, `base64,`); !found { + return "", true + } + for _, r := range url { + if !(stringutil.IsASCIILetter(r) || stringutil.IsDigit(r) || r == '+' || r == '/' || r == '=') { + return "", true + } + } + return url, true +} diff --git a/internal/testutil/harnessutil/sourcemap_recorder.go b/internal/testutil/harnessutil/sourcemap_recorder.go index 48cdf024c6..fa8dba1e08 100644 --- a/internal/testutil/harnessutil/sourcemap_recorder.go +++ b/internal/testutil/harnessutil/sourcemap_recorder.go @@ -104,7 +104,7 @@ func newSourceMapSpanWriter(sourceMapRecorder *writerAggregator, sourceMap *sour sourceMapRecorder.WriteLine("===================================================================") sourceMapRecorder.WriteLineF("JsFile: %s", sourceMap.File) - sourceMapRecorder.WriteLineF("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.GetLineInfo(jsFile.Content, writer.jsLineMap))) + sourceMapRecorder.WriteLineF("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.CreateLineInfo(jsFile.Content, writer.jsLineMap))) sourceMapRecorder.WriteLineF("sourceRoot: %s", sourceMap.SourceRoot) sourceMapRecorder.WriteLineF("sources: %s", strings.Join(sourceMap.Sources, ",")) if len(sourceMap.SourcesContent) > 0 { From 8423ecf7366ee1bfc54bd93c1e0a8376ebd062f3 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 29 Sep 2025 21:44:12 +0000 Subject: [PATCH 02/23] refactor, fix and update tests --- internal/api/api.go | 6 +- internal/ls/definition.go | 72 ++++++++---------- internal/ls/languageservice.go | 9 ++- internal/ls/source_map.go | 73 +++++++++---------- internal/ls/utilities.go | 9 +++ internal/project/session.go | 2 +- internal/project/snapshot.go | 4 + internal/scanner/scanner.go | 46 +++++++----- internal/sourcemap/source_mapper.go | 39 ++++++---- ...eclarationMapGoToDefinition.baseline.jsonc | 12 +-- ...efinitionRelativeSourceRoot.baseline.jsonc | 12 +-- ...nSameNameDifferentDirectory.baseline.jsonc | 24 +++--- ...arationMapsOutOfDateMapping.baseline.jsonc | 7 +- 13 files changed, 169 insertions(+), 146 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index c521516df9..65a915c4d9 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -160,7 +160,7 @@ func (api *API) GetSymbolAtPosition(ctx context.Context, projectId Handle[projec return nil, errors.New("project not found") } - languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()) + languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()) symbol, err := languageService.GetSymbolAtPosition(ctx, fileName, position) if err != nil || symbol == nil { return nil, err @@ -202,7 +202,7 @@ func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[projec if node == nil { return nil, fmt.Errorf("node of kind %s not found at position %d in file %q", kind.String(), pos, sourceFile.FileName()) } - languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()) + languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()) symbol := languageService.GetSymbolAtLocation(ctx, node) if symbol == nil { return nil, nil @@ -232,7 +232,7 @@ func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Pr if !ok { return nil, fmt.Errorf("symbol %q not found", symbolHandle) } - languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()) + languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()) t := languageService.GetTypeOfSymbol(ctx, symbol) if t == nil { return nil, nil diff --git a/internal/ls/definition.go b/internal/ls/definition.go index 0ea3c2076f..e9187fdd33 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -8,7 +8,6 @@ import ( "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/checker" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" ) @@ -23,54 +22,28 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp c, done := program.GetTypeCheckerForFile(ctx, file) defer done() - return l.getMappedDefinition(l.provideDefinitions(c, node)), nil -} - -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}) - debug.Assert(mappedTarget.Uri == mappedSelection.Uri, "target and selection should be in same file") - (*definitions.DefinitionLinks)[i].TargetUri = mappedTarget.Uri - (*definitions.DefinitionLinks)[i].TargetRange = mappedTarget.Range - (*definitions.DefinitionLinks)[i].TargetSelectionRange = mappedSelection.Range - } - } - return definitions -} - -func (l *LanguageService) provideDefinitions(c *checker.Checker, node *ast.Node) lsproto.DefinitionResponse { if node.Kind == ast.KindOverrideKeyword { if sym := getSymbolForOverriddenMember(c, node); sym != nil { - return l.createLocationsFromDeclarations(sym.Declarations) + return l.createLocationsFromDeclarations(sym.Declarations), nil } } if ast.IsJumpStatementTarget(node) { if label := getTargetLabel(node.Parent, node.Text()); label != nil { - return l.createLocationsFromDeclarations([]*ast.Node{label}) + return l.createLocationsFromDeclarations([]*ast.Node{label}), nil } } if node.Kind == ast.KindCaseKeyword || node.Kind == ast.KindDefaultKeyword && ast.IsDefaultClause(node.Parent) { if stmt := ast.FindAncestor(node.Parent, ast.IsSwitchStatement); stmt != nil { file := ast.GetSourceFileOfNode(stmt) - return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())) + return l.createLocationFromFileAndRange(file, scanner.GetRangeOfTokenAtPosition(file, stmt.Pos())), nil } } if node.Kind == ast.KindReturnKeyword || node.Kind == ast.KindYieldKeyword || node.Kind == ast.KindAwaitKeyword { if fn := ast.FindAncestor(node, ast.IsFunctionLikeDeclaration); fn != nil { - return l.createLocationsFromDeclarations([]*ast.Node{fn}) + return l.createLocationsFromDeclarations([]*ast.Node{fn}), nil } } @@ -81,9 +54,31 @@ func (l *LanguageService) provideDefinitions(c *checker.Checker, node *ast.Node) nonFunctionDeclarations := core.Filter(slices.Clip(declarations), func(node *ast.Node) bool { return !ast.IsFunctionLike(node) }) declarations = append(nonFunctionDeclarations, calledDeclaration) } - return l.createLocationsFromDeclarations(declarations) + return l.createLocationsFromDeclarations(declarations), nil } +// 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}) +// debug.Assert(mappedTarget.Uri == mappedSelection.Uri, "target and selection should be in same file") +// (*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))) @@ -131,20 +126,17 @@ func (l *LanguageService) createLocationsFromDeclarations(declarations []*ast.No for _, decl := range declarations { file := ast.GetSourceFileOfNode(decl) name := core.OrElse(ast.GetNameOfDeclaration(decl), decl) - locations = core.AppendIfUnique(locations, lsproto.Location{ - Uri: FileNameToDocumentURI(file.FileName()), - Range: *l.createLspRangeFromNode(name, file), - }) + nodeRange := createRangeFromNode(name, file) + mappedLocation := l.getMappedLocation(file.FileName(), nodeRange) + locations = core.AppendIfUnique(locations, mappedLocation) } 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: &lsproto.Location{ - Uri: FileNameToDocumentURI(file.FileName()), - Range: *l.createLspRangeFromBounds(textRange.Pos(), textRange.End(), file), - }, + Location: &mappedLocation, } } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 484c78d512..2210959ac5 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -10,23 +10,26 @@ import ( type LanguageService struct { host Host converters *Converters - documentPositionMappers map[string]sourcemap.DocumentPositionMapper // !!! TODO: needs sync? + documentPositionMappers map[string]*sourcemap.DocumentPositionMapper useCaseSensitiveFileNames bool readFile func(path string) (contents string, ok bool) + fileExists func(path string) bool } func NewLanguageService( host Host, converters *Converters, readFile func(path string) (contents string, ok bool), + fileExists func(path string) bool, useCaseSensitiveFileNames bool, ) *LanguageService { return &LanguageService{ host: host, converters: converters, readFile: readFile, + fileExists: fileExists, useCaseSensitiveFileNames: useCaseSensitiveFileNames, - documentPositionMappers: map[string]sourcemap.DocumentPositionMapper{}, + documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, } } @@ -49,7 +52,7 @@ func (l *LanguageService) getProgramAndFile(documentURI lsproto.DocumentUri) (*c return program, file } -func (l *LanguageService) GetDocumentPositionMapper(fileName string) sourcemap.DocumentPositionMapper { +func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { d, ok := l.documentPositionMappers[fileName] if !ok { d = sourcemap.GetDocumentPositionMapper(l, fileName) diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index 57d297e921..c0003ef0af 100644 --- a/internal/ls/source_map.go +++ b/internal/ls/source_map.go @@ -8,24 +8,25 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" ) -func (l *LanguageService) getMappedLocation(location *lsproto.Location) *lsproto.Location { - uriStart, start := l.tryGetSourceLSPPosition(location.Uri.FileName(), &location.Range.Start) - if uriStart == nil { - return location +func (l *LanguageService) getMappedLocation(fileName string, fileRange core.TextRange) lsproto.Location { + startPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.Pos())) + if startPos == nil { + lspRange := l.createLspRangeFromRange(fileRange, l.getScript(fileName)) + return lsproto.Location{ + Uri: FileNameToDocumentURI(fileName), + Range: *lspRange, + } } - uriEnd, end := l.tryGetSourceLSPPosition(location.Uri.FileName(), &location.Range.End) - debug.Assert(uriEnd == uriStart, "start and end should be in same file") - debug.Assert(end != nil, "end position should be valid") - return &lsproto.Location{ - Uri: *uriStart, - Range: lsproto.Range{Start: *start, End: *end}, + endPos := l.tryGetSourcePosition(fileName, core.TextPos(fileRange.End())) + 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{ + Uri: FileNameToDocumentURI(startPos.FileName), + Range: *lspRange, } } -func (l *LanguageService) getMappedPosition() { - // !!! HERE -} - type script struct { fileName string text string @@ -39,45 +40,41 @@ func (s *script) Text() string { return s.text } -func (l *LanguageService) tryGetSourceLSPPosition( - genFileName string, - position *lsproto.Position, -) (*lsproto.DocumentUri, *lsproto.Position) { - genText, ok := l.ReadFile(genFileName) - if !ok { - return nil, nil // That shouldn't happen - } - genPos := l.converters.LineAndCharacterToPosition(&script{fileName: genFileName, text: genText}, *position) - documentPos := l.tryGetSourcePosition(genFileName, genPos) - if documentPos == nil { - return nil, nil - } - documentURI := FileNameToDocumentURI(documentPos.FileName) - sourceText, ok := l.ReadFile(documentPos.FileName) +func (l *LanguageService) getScript(fileName string) *script { + text, ok := l.readFile(fileName) if !ok { - return nil, nil + return nil } - sourcePos := l.converters.PositionToLineAndCharacter( - &script{fileName: documentPos.FileName, text: sourceText}, - core.TextPos(documentPos.Pos), - ) - return &documentURI, &sourcePos + return &script{fileName: fileName, text: text} } func (l *LanguageService) tryGetSourcePosition( fileName string, - genPosition core.TextPos, + position core.TextPos, +) *sourcemap.DocumentPosition { + newPos := l.tryGetSourcePositionWorker(fileName, position) + if newPos != nil { + if !l.fileExists(newPos.FileName) { + return nil + } + } + return newPos +} + +func (l *LanguageService) tryGetSourcePositionWorker( + fileName string, + position core.TextPos, ) *sourcemap.DocumentPosition { if !tspath.IsDeclarationFileName(fileName) { return nil } positionMapper := l.GetDocumentPositionMapper(fileName) - documentPos := positionMapper.GetSourcePosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(genPosition)}) + documentPos := positionMapper.GetSourcePosition(&sourcemap.DocumentPosition{FileName: fileName, Pos: int(position)}) if documentPos == nil { return nil } - if newPos := l.tryGetSourcePosition(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil { + if newPos := l.tryGetSourcePositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil { return newPos } return documentPos diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 2cfdff6842..0992273d76 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -433,11 +433,20 @@ func (l *LanguageService) createLspRangeFromNode(node *ast.Node, file *ast.Sourc return l.createLspRangeFromBounds(scanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/), node.End(), file) } +func createRangeFromNode(node *ast.Node, file *ast.SourceFile) core.TextRange { + return core.NewTextRange(scanner.GetTokenPosOfNode(node, file, false /*includeJSDoc*/), node.End()) +} + func (l *LanguageService) createLspRangeFromBounds(start, end int, file *ast.SourceFile) *lsproto.Range { lspRange := l.converters.ToLSPRange(file, core.NewTextRange(start, end)) return &lspRange } +func (l *LanguageService) createLspRangeFromRange(textRange core.TextRange, script Script) *lsproto.Range { + lspRange := l.converters.ToLSPRange(script, textRange) + return &lspRange +} + func (l *LanguageService) createLspPosition(position int, file *ast.SourceFile) lsproto.Position { return l.converters.PositionToLineAndCharacter(file, core.TextPos(position)) } diff --git a/internal/project/session.go b/internal/project/session.go index 0a85e93c9d..f12d3aae5e 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -367,7 +367,7 @@ func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUr if project == nil { return nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.UseCaseSensitiveFileNames()), nil + return ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()), nil } func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*overlay, change SnapshotChange) *Snapshot { diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 9485b9ba00..77c3fd15d7 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -101,6 +101,10 @@ func (s *Snapshot) ReadFile(fileName string) (string, bool) { return handle.Content(), true } +func (s *Snapshot) FileExists(fileName string) bool { + return s.fs.fs.FileExists(fileName) +} + type APISnapshotRequest struct { OpenProjects *collections.Set[string] CloseProjects *collections.Set[tspath.Path] diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index ede9405f83..8b73ffee1c 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/jsnum" "github.com/microsoft/typescript-go/internal/stringutil" @@ -2451,31 +2452,42 @@ func GetPositionOfLineAndCharacter(sourceFile *ast.SourceFile, line int, charact } func ComputePositionOfLineAndCharacter(lineStarts []core.TextPos, line int, character int) int { - /// !!! debugText, allowEdits + return ComputePositionOfLineAndCharacterEx(lineStarts, line, character, nil, false) +} + +func ComputePositionOfLineAndCharacterEx(lineStarts []core.TextPos, line int, character int, text *string, allowEdits bool) int { if line < 0 || line >= len(lineStarts) { - // if (allowEdits) { - // // Clamp line to nearest allowable value - // line = line < 0 ? 0 : line >= lineStarts.length ? lineStarts.length - 1 : line; - // } - panic(fmt.Sprintf("Bad line number. Line: %d, lineStarts.length: %d.", line, len(lineStarts))) + if allowEdits { + // Clamp line to nearest allowable value + if line < 0 { + line = 0 + } else if line >= len(lineStarts) { + line = len(lineStarts) - 1 + } + } else { + panic(fmt.Sprintf("Bad line number. Line: %d, lineStarts.length: %d.", line, len(lineStarts))) + } } res := int(lineStarts[line]) + character - // !!! - // if (allowEdits) { - // // Clamp to nearest allowable values to allow the underlying to be edited without crashing (accuracy is lost, instead) - // // TODO: Somehow track edits between file as it was during the creation of sourcemap we have and the current file and - // // apply them to the computed position to improve accuracy - // return res > lineStarts[line + 1] ? lineStarts[line + 1] : typeof debugText === "string" && res > debugText.length ? debugText.length : res; - // } + if allowEdits { + // Clamp to nearest allowable values to allow the underlying to be edited without crashing (accuracy is lost, instead) + // TODO: Somehow track edits between file as it was during the creation of sourcemap we have and the current file and + // apply them to the computed position to improve accuracy + if line+1 < len(lineStarts) && res > int(lineStarts[line+1]) { + return int(lineStarts[line+1]) + } + if text != nil && res > len(*text) { + return len(*text) + } + return res + } if line < len(lineStarts)-1 && res >= int(lineStarts[line+1]) { panic("Computed position is beyond that of the following line.") + } else if text != nil { + debug.Assert(res <= len(*text)) // Allow single character overflow for trailing newline } - // !!! - // else if (debugText !== undefined) { - // Debug.assert(res <= debugText.length); // Allow single character overflow for trailing newline - // } return res } diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go index ff407cdb0c..8dbdba4037 100644 --- a/internal/sourcemap/source_mapper.go +++ b/internal/sourcemap/source_mapper.go @@ -19,12 +19,6 @@ type Host interface { ReadFile(fileName string) (string, bool) } -// Maps source positions to generated positions and vice versa. -type DocumentPositionMapper interface { - GetSourcePosition(*DocumentPosition) *DocumentPosition - GetGeneratedPosition(*DocumentPosition) *DocumentPosition -} - // Similar to `Mapping`, but position-based. type MappedPosition struct { generatedPosition int @@ -43,7 +37,8 @@ func (m *MappedPosition) isSourceMappedPosition() bool { type SourceMappedPosition = MappedPosition -type documentPositionMapper struct { +// Maps source positions to generated positions and vice versa. +type DocumentPositionMapper struct { useCaseSensitiveFileNames bool sourceFileAbsolutePaths []string @@ -54,11 +49,11 @@ type documentPositionMapper struct { sourceMappings map[SourceIndex][]*SourceMappedPosition } -func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath string) DocumentPositionMapper { +func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath string) *DocumentPositionMapper { mapDirectory := tspath.GetDirectoryPath(mapPath) var sourceRoot string if sourceMap.SourceRoot != "" { - tspath.GetNormalizedAbsolutePath(sourceMap.SourceRoot, mapDirectory) + sourceRoot = tspath.GetNormalizedAbsolutePath(sourceMap.SourceRoot, mapDirectory) } else { sourceRoot = mapDirectory } @@ -83,14 +78,26 @@ func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath st generatedPosition := -1 lineInfo := host.GetLineInfo(generatedAbsoluteFilePath) if lineInfo != nil { - generatedPosition = scanner.ComputePositionOfLineAndCharacter(lineInfo.lineStarts, mapping.GeneratedLine, mapping.GeneratedCharacter) + generatedPosition = scanner.ComputePositionOfLineAndCharacterEx( + lineInfo.lineStarts, + mapping.GeneratedLine, + mapping.GeneratedCharacter, + &lineInfo.text, + true, /*allowEdits*/ + ) } sourcePosition := -1 if mapping.IsSourceMapping() { lineInfo := host.GetLineInfo(sourceFileAbsolutePaths[mapping.SourceIndex]) if lineInfo != nil { - pos := scanner.ComputePositionOfLineAndCharacter(lineInfo.lineStarts, mapping.SourceLine, mapping.SourceCharacter) + pos := scanner.ComputePositionOfLineAndCharacterEx( + lineInfo.lineStarts, + mapping.SourceLine, + mapping.SourceCharacter, + &lineInfo.text, + true, /*allowEdits*/ + ) sourcePosition = pos } } @@ -144,7 +151,7 @@ func createDocumentPositionMapper(host Host, sourceMap *RawSourceMap, mapPath st a.sourcePosition == b.sourcePosition }) - return &documentPositionMapper{ + return &DocumentPositionMapper{ useCaseSensitiveFileNames: useCaseSensitiveFileNames, sourceFileAbsolutePaths: sourceFileAbsolutePaths, sourceToSourceIndexMap: sourceToSourceIndexMap, @@ -159,7 +166,7 @@ type DocumentPosition struct { Pos int } -func (d *documentPositionMapper) GetSourcePosition(loc *DocumentPosition) *DocumentPosition { +func (d *DocumentPositionMapper) GetSourcePosition(loc *DocumentPosition) *DocumentPosition { if d == nil { return nil } @@ -187,7 +194,7 @@ func (d *documentPositionMapper) GetSourcePosition(loc *DocumentPosition) *Docum } } -func (d *documentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *DocumentPosition { +func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *DocumentPosition { if d == nil { return nil } @@ -219,7 +226,7 @@ func (d *documentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do } } -func GetDocumentPositionMapper(host Host, generatedFileName string) DocumentPositionMapper { +func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPositionMapper { mapFileName := tryGetSourceMappingURL(host, generatedFileName) if mapFileName != "" { if base64Object, matched := tryParseBase46Url(mapFileName); matched { @@ -247,7 +254,7 @@ func GetDocumentPositionMapper(host Host, generatedFileName string) DocumentPosi return nil } -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/declarationMapGoToDefinition.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinition.baseline.jsonc index a12955d6fa..f7a410817c 100644 --- a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinition.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinition.baseline.jsonc @@ -1,11 +1,11 @@ // === goToDefinition === -// === /indexdef.d.ts === -// export declare class Foo { +// === /index.ts === +// export class Foo { // member: string; -// [|methodName|](propName: SomeType): void; -// otherMethod(): { -// x: number; -// y?: undefined; +// [|methodName|](propName: SomeType): void {} +// otherMethod() { +// if (Math.random() > 0.5) { +// return {x: 42}; // // --- (line: 7) skipped --- // === /mymodule.ts === diff --git a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionRelativeSourceRoot.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionRelativeSourceRoot.baseline.jsonc index 5a4c9e8859..7d76808348 100644 --- a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionRelativeSourceRoot.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionRelativeSourceRoot.baseline.jsonc @@ -1,11 +1,11 @@ // === goToDefinition === -// === /out/indexdef.d.ts === -// export declare class Foo { +// === /index.ts === +// export class Foo { // member: string; -// [|methodName|](propName: SomeType): void; -// otherMethod(): { -// x: number; -// y?: undefined; +// [|methodName|](propName: SomeType): void {} +// otherMethod() { +// if (Math.random() > 0.5) { +// return {x: 42}; // // --- (line: 7) skipped --- // === /mymodule.ts === diff --git a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc index bd7f38ec57..e8f7348bf6 100644 --- a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsGoToDefinitionSameNameDifferentDirectory.baseline.jsonc @@ -1,11 +1,10 @@ // === goToDefinition === -// === /BaseClass/Source.d.ts === -// declare class [|Control|] { -// constructor(); -// /** this is a super var */ -// myVar: boolean | 'yeah'; -// } -// //# sourceMappingURL=Source.d.ts.map +// === /BaseClass/Source.ts === +// class [|Control|]{ +// constructor(){ +// return; +// } +// // --- (line: 5) skipped --- // === /buttonClass/Source.ts === // // I cannot F12 navigate to Control @@ -19,13 +18,14 @@ // === goToDefinition === -// === /BaseClass/Source.d.ts === -// declare class Control { -// constructor(); +// === /BaseClass/Source.ts === +// class Control{ +// constructor(){ +// return; +// } // /** this is a super var */ -// [|myVar|]: boolean | 'yeah'; +// public [|myVar|]: boolean | 'yeah' = true; // } -// //# sourceMappingURL=Source.d.ts.map // === /buttonClass/Source.ts === // --- (line: 3) skipped --- diff --git a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsOutOfDateMapping.baseline.jsonc b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsOutOfDateMapping.baseline.jsonc index 66e44c10de..a7d9041f83 100644 --- a/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsOutOfDateMapping.baseline.jsonc +++ b/testdata/baselines/reference/fourslash/goToDefinition/declarationMapsOutOfDateMapping.baseline.jsonc @@ -1,9 +1,8 @@ // === goToDefinition === -// === /home/src/workspaces/project/node_modules/a/dist/index.d.ts === -// export declare class [|Foo|] { -// bar: any; +// === /home/src/workspaces/project/node_modules/a/src/index.ts === +// export class [|Foo|] { // } -// //# sourceMappingURL=index.d.ts.map +// // === /home/src/workspaces/project/index.ts === // import { Foo/*GOTO DEF*/ } from "a"; \ No newline at end of file From ca938172e81be870a4ca974e4c14f8838bb1847c Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 29 Sep 2025 22:26:34 +0000 Subject: [PATCH 03/23] refactor --- internal/ls/definition.go | 22 ---------------------- internal/project/snapshotfs.go | 23 +++++++++-------------- 2 files changed, 9 insertions(+), 36 deletions(-) diff --git a/internal/ls/definition.go b/internal/ls/definition.go index e9187fdd33..abefaf932e 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -57,28 +57,6 @@ func (l *LanguageService) ProvideDefinition(ctx context.Context, documentURI lsp return l.createLocationsFromDeclarations(declarations), nil } -// 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}) -// debug.Assert(mappedTarget.Uri == mappedSelection.Uri, "target and selection should be in same file") -// (*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))) diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index 071a048aa1..1b45acf0a2 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -27,13 +27,10 @@ type snapshotFS struct { fs vfs.FS overlays map[tspath.Path]*overlay diskFiles map[tspath.Path]*diskFile - readFiles collections.SyncMap[tspath.Path, memoizedFileEntry] + readFiles collections.SyncMap[tspath.Path, memoizedDiskFile] } -// !!! newtype? -type memoizedFileEntry struct { - read func() *diskFile -} +type memoizedDiskFile func() *diskFile func (s *snapshotFS) FS() vfs.FS { return s.fs @@ -46,16 +43,14 @@ func (s *snapshotFS) GetFile(fileName string) FileHandle { if file, ok := s.diskFiles[s.toPath(fileName)]; ok { return file } - newEntry := memoizedFileEntry{ - read: sync.OnceValue(func() *diskFile { - if contents, ok := s.fs.ReadFile(fileName); ok { - return newDiskFile(fileName, contents) - } - return nil - }), - } + newEntry := memoizedDiskFile(sync.OnceValue(func() *diskFile { + if contents, ok := s.fs.ReadFile(fileName); ok { + return newDiskFile(fileName, contents) + } + return nil + })) if entry, ok := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry); ok { - return entry.read() + return entry() } return nil } From bb7a5b30dbcd98f1b3372b01b22bbdd18f79dbc1 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 29 Sep 2025 22:29:58 +0000 Subject: [PATCH 04/23] fix typo --- internal/sourcemap/source_mapper.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go index 8dbdba4037..ba6c2f6fcf 100644 --- a/internal/sourcemap/source_mapper.go +++ b/internal/sourcemap/source_mapper.go @@ -229,7 +229,7 @@ func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPositionMapper { mapFileName := tryGetSourceMappingURL(host, generatedFileName) if mapFileName != "" { - if base64Object, matched := tryParseBase46Url(mapFileName); matched { + if base64Object, matched := tryParseBase64Url(mapFileName); matched { if base64Object != "" { if decoded, err := base64.StdEncoding.DecodeString(base64Object); err == nil { return convertDocumentToSourceMapper(host, string(decoded), generatedFileName) @@ -288,7 +288,7 @@ func tryGetSourceMappingURL(host Host, fileName string) string { // Originally: /^data:(?:application\/json;charset=[uU][tT][fF]-8;base64,([A-Za-z0-9+/=]+)$)?/ // Should have been /^data:(?:application\/json;(?:charset=[uU][tT][fF]-8;)?base64,([A-Za-z0-9+/=]+)$)?/ -func tryParseBase46Url(url string) (parseableUrl string, isBase64Url bool) { +func tryParseBase64Url(url string) (parseableUrl string, isBase64Url bool) { var found bool if url, found = strings.CutPrefix(url, `data:`); !found { return "", false From cef9f67aee4c45817c18aa8e36cb66d6317c71bc Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 30 Sep 2025 00:42:23 +0000 Subject: [PATCH 05/23] refactor LS host --- internal/api/api.go | 6 +++--- internal/ls/autoimports.go | 2 +- internal/ls/host.go | 9 ++++----- internal/ls/languageservice.go | 31 +++++++++++------------------ internal/ls/source_map.go | 4 ++-- internal/project/project.go | 4 ---- internal/project/session.go | 2 +- internal/sourcemap/source_mapper.go | 3 +-- 8 files changed, 24 insertions(+), 37 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 65a915c4d9..d2bad10599 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -160,7 +160,7 @@ func (api *API) GetSymbolAtPosition(ctx context.Context, projectId Handle[projec return nil, errors.New("project not found") } - languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()) + languageService := ls.NewLanguageService(project.GetProgram(), snapshot) symbol, err := languageService.GetSymbolAtPosition(ctx, fileName, position) if err != nil || symbol == nil { return nil, err @@ -202,7 +202,7 @@ func (api *API) GetSymbolAtLocation(ctx context.Context, projectId Handle[projec if node == nil { return nil, fmt.Errorf("node of kind %s not found at position %d in file %q", kind.String(), pos, sourceFile.FileName()) } - languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()) + languageService := ls.NewLanguageService(project.GetProgram(), snapshot) symbol := languageService.GetSymbolAtLocation(ctx, node) if symbol == nil { return nil, nil @@ -232,7 +232,7 @@ func (api *API) GetTypeOfSymbol(ctx context.Context, projectId Handle[project.Pr if !ok { return nil, fmt.Errorf("symbol %q not found", symbolHandle) } - languageService := ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()) + languageService := ls.NewLanguageService(project.GetProgram(), snapshot) t := languageService.GetTypeOfSymbol(ctx, symbol) if t == nil { return nil, nil diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index 3b38e0d637..7c1d586a28 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -702,7 +702,7 @@ func (l *LanguageService) createPackageJsonImportFilter(fromFile *ast.SourceFile return nil } specifier := modulespecifiers.GetNodeModulesPackageName( - l.host.GetProgram().Options(), + l.program.Options(), fromFile, importedFileName, moduleSpecifierResolutionHost, diff --git a/internal/ls/host.go b/internal/ls/host.go index a2d4888e43..f89d62f5fb 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -1,9 +1,8 @@ package ls -import ( - "github.com/microsoft/typescript-go/internal/compiler" -) - type Host interface { - GetProgram() *compiler.Program + UseCaseSensitiveFileNames() bool + ReadFile(path string) (contents string, ok bool) + FileExists(path string) bool + Converters() *Converters } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 2210959ac5..544a98dcd9 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -8,33 +8,26 @@ import ( ) type LanguageService struct { - host Host - converters *Converters - documentPositionMappers map[string]*sourcemap.DocumentPositionMapper - useCaseSensitiveFileNames bool - readFile func(path string) (contents string, ok bool) - fileExists func(path string) bool + host Host + program *compiler.Program + converters *Converters + documentPositionMappers map[string]*sourcemap.DocumentPositionMapper } func NewLanguageService( + program *compiler.Program, host Host, - converters *Converters, - readFile func(path string) (contents string, ok bool), - fileExists func(path string) bool, - useCaseSensitiveFileNames bool, ) *LanguageService { return &LanguageService{ - host: host, - converters: converters, - readFile: readFile, - fileExists: fileExists, - useCaseSensitiveFileNames: useCaseSensitiveFileNames, - documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, + host: host, + program: program, + converters: host.Converters(), + documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{}, } } func (l *LanguageService) GetProgram() *compiler.Program { - return l.host.GetProgram() + return l.program } func (l *LanguageService) tryGetProgramAndFile(fileName string) (*compiler.Program, *ast.SourceFile) { @@ -62,11 +55,11 @@ func (l *LanguageService) GetDocumentPositionMapper(fileName string) *sourcemap. } func (l *LanguageService) ReadFile(fileName string) (string, bool) { - return l.readFile(fileName) + return l.host.ReadFile(fileName) } func (l *LanguageService) UseCaseSensitiveFileNames() bool { - return l.useCaseSensitiveFileNames + return l.host.UseCaseSensitiveFileNames() } func (l *LanguageService) GetLineInfo(fileName string) *sourcemap.LineInfo { diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index c0003ef0af..022fa05353 100644 --- a/internal/ls/source_map.go +++ b/internal/ls/source_map.go @@ -41,7 +41,7 @@ func (s *script) Text() string { } func (l *LanguageService) getScript(fileName string) *script { - text, ok := l.readFile(fileName) + text, ok := l.host.ReadFile(fileName) if !ok { return nil } @@ -54,7 +54,7 @@ func (l *LanguageService) tryGetSourcePosition( ) *sourcemap.DocumentPosition { newPos := l.tryGetSourcePositionWorker(fileName, position) if newPos != nil { - if !l.fileExists(newPos.FileName) { + if !l.host.FileExists(newPos.FileName) { return nil } } diff --git a/internal/project/project.go b/internal/project/project.go index 6354e0ab30..317621294b 100644 --- a/internal/project/project.go +++ b/internal/project/project.go @@ -9,7 +9,6 @@ import ( "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" - "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/logging" @@ -49,8 +48,6 @@ const ( PendingReloadFull ) -var _ ls.Host = (*Project)(nil) - // Project represents a TypeScript project. // If changing struct fields, also update the Clone method. type Project struct { @@ -195,7 +192,6 @@ func (p *Project) ConfigFilePath() tspath.Path { return p.configFilePath } -// GetProgram implements ls.Host. func (p *Project) GetProgram() *compiler.Program { return p.Program } diff --git a/internal/project/session.go b/internal/project/session.go index f12d3aae5e..0b6e537d54 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -367,7 +367,7 @@ func (s *Session) GetLanguageService(ctx context.Context, uri lsproto.DocumentUr if project == nil { return nil, fmt.Errorf("no project found for URI %s", uri) } - return ls.NewLanguageService(project, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()), nil + return ls.NewLanguageService(project.GetProgram(), snapshot), nil } func (s *Session) UpdateSnapshot(ctx context.Context, overlays map[tspath.Path]*overlay, change SnapshotChange) *Snapshot { diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go index ba6c2f6fcf..b957eb195e 100644 --- a/internal/sourcemap/source_mapper.go +++ b/internal/sourcemap/source_mapper.go @@ -286,8 +286,7 @@ func tryGetSourceMappingURL(host Host, fileName string) string { return TryGetSourceMappingURL(lineInfo) } -// Originally: /^data:(?:application\/json;charset=[uU][tT][fF]-8;base64,([A-Za-z0-9+/=]+)$)?/ -// Should have been /^data:(?:application\/json;(?:charset=[uU][tT][fF]-8;)?base64,([A-Za-z0-9+/=]+)$)?/ +// Equivalent to /^data:(?:application\/json;(?:charset=[uU][tT][fF]-8;)?base64,([A-Za-z0-9+/=]+)$)?/ func tryParseBase64Url(url string) (parseableUrl string, isBase64Url bool) { var found bool if url, found = strings.CutPrefix(url, `data:`); !found { From c80e7a3a2eb77ce763e0cd7217ff3ed96ae963b6 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 1 Oct 2025 10:18:07 -0700 Subject: [PATCH 06/23] rename line maps with ECMA/LSP --- internal/ast/ast.go | 26 +++++----- internal/astnav/tokens_test.go | 2 +- internal/checker/grammarchecks.go | 4 +- internal/compiler/program.go | 6 +-- internal/core/core.go | 6 +-- internal/diagnosticwriter/diagnosticwriter.go | 16 +++--- internal/format/api.go | 6 +-- internal/format/indent.go | 26 +++++----- internal/format/rulecontext.go | 4 +- internal/format/span.go | 50 +++++++++---------- internal/format/util.go | 8 +-- internal/fourslash/baselineutil.go | 6 +-- internal/fourslash/fourslash.go | 10 ++-- internal/fourslash/test_parser.go | 4 +- internal/ls/changetrackerimpl.go | 6 +-- internal/ls/completions.go | 8 +-- internal/ls/converters.go | 4 +- internal/ls/linemap.go | 8 +-- internal/ls/utilities.go | 4 +- internal/lsutil/asi.go | 4 +- internal/printer/printer.go | 10 ++-- internal/printer/textwriter.go | 2 +- internal/printer/utilities.go | 2 +- internal/project/compilerhost.go | 8 --- internal/project/overlayfs.go | 12 ++--- internal/project/snapshot.go | 6 +-- internal/scanner/scanner.go | 22 ++++---- internal/sourcemap/lineinfo.go | 10 ++-- internal/sourcemap/source.go | 2 +- internal/sourcemap/util.go | 2 +- .../harnessutil/sourcemap_recorder.go | 8 +-- .../testutil/tsbaseline/error_baseline.go | 2 +- .../tsbaseline/type_symbol_baseline.go | 4 +- internal/transformers/jsxtransforms/jsx.go | 2 +- internal/transformers/utilities.go | 4 +- 35 files changed, 148 insertions(+), 156 deletions(-) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 6aa647cdf1..848c717a96 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -10724,10 +10724,10 @@ type SourceFile struct { ClassifiableNames collections.Set[string] PatternAmbientModules []*PatternAmbientModule - // Fields set by LineMap + // Fields set by ECMALineMap - lineMapMu sync.RWMutex - lineMap []core.TextPos + ecmaLineMapMu sync.RWMutex + ecmaLineMap []core.TextPos // Fields set by language service @@ -10862,17 +10862,17 @@ func (f *NodeFactory) UpdateSourceFile(node *SourceFile, statements *StatementLi return node.AsNode() } -func (node *SourceFile) LineMap() []core.TextPos { - node.lineMapMu.RLock() - lineMap := node.lineMap - node.lineMapMu.RUnlock() +func (node *SourceFile) ECMALineMap() []core.TextPos { + node.ecmaLineMapMu.RLock() + lineMap := node.ecmaLineMap + node.ecmaLineMapMu.RUnlock() if lineMap == nil { - node.lineMapMu.Lock() - defer node.lineMapMu.Unlock() - lineMap = node.lineMap + node.ecmaLineMapMu.Lock() + defer node.ecmaLineMapMu.Unlock() + lineMap = node.ecmaLineMap if lineMap == nil { - lineMap = core.ComputeLineStarts(node.Text()) - node.lineMap = lineMap + lineMap = core.ComputeECMALineStarts(node.Text()) + node.ecmaLineMap = lineMap } } return lineMap @@ -11054,7 +11054,7 @@ func getDeclarationName(declaration *Node) string { type SourceFileLike interface { Text() string - LineMap() []core.TextPos + ECMALineMap() []core.TextPos } type CommentRange struct { diff --git a/internal/astnav/tokens_test.go b/internal/astnav/tokens_test.go index b2ea39491a..b5eb7ba39f 100644 --- a/internal/astnav/tokens_test.go +++ b/internal/astnav/tokens_test.go @@ -240,7 +240,7 @@ func tsGetTouchingPropertyName(t testing.TB, fileText string, positions []int) [ } func writeRangeDiff(output *strings.Builder, file *ast.SourceFile, diff tokenDiff, rng core.TextRange, position int) { - lines := file.LineMap() + lines := file.ECMALineMap() tsTokenPos := position goTokenPos := position diff --git a/internal/checker/grammarchecks.go b/internal/checker/grammarchecks.go index dc98b20881..ee64d4072b 100644 --- a/internal/checker/grammarchecks.go +++ b/internal/checker/grammarchecks.go @@ -814,8 +814,8 @@ func (c *Checker) checkGrammarArrowFunction(node *ast.Node, file *ast.SourceFile } equalsGreaterThanToken := arrowFunc.EqualsGreaterThanToken - startLine, _ := scanner.GetLineAndCharacterOfPosition(file, equalsGreaterThanToken.Pos()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(file, equalsGreaterThanToken.End()) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, equalsGreaterThanToken.Pos()) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, equalsGreaterThanToken.End()) return startLine != endLine && c.grammarErrorOnNode(equalsGreaterThanToken, diagnostics.Line_terminator_not_permitted_before_arrow) } diff --git a/internal/compiler/program.go b/internal/compiler/program.go index 0ae09543f6..b70a00197f 100644 --- a/internal/compiler/program.go +++ b/internal/compiler/program.go @@ -1074,10 +1074,10 @@ func (p *Program) getDiagnosticsWithPrecedingDirectives(sourceFile *ast.SourceFi // Build map of directives by line number directivesByLine := make(map[int]ast.CommentDirective) for _, directive := range sourceFile.CommentDirectives { - line, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, directive.Loc.Pos()) + line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, directive.Loc.Pos()) directivesByLine[line] = directive } - lineStarts := scanner.GetLineStarts(sourceFile) + lineStarts := scanner.GetECMALineStarts(sourceFile) filtered := make([]*ast.Diagnostic, 0, len(diags)) for _, diagnostic := range diags { ignoreDiagnostic := false @@ -1225,7 +1225,7 @@ func (p *Program) getDiagnosticsHelper(ctx context.Context, sourceFile *ast.Sour func (p *Program) LineCount() int { var count int for _, file := range p.files { - count += len(file.LineMap()) + count += len(file.ECMALineMap()) } return count } diff --git a/internal/core/core.go b/internal/core/core.go index 45198a0370..6a78811d69 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -362,12 +362,12 @@ func Coalesce[T *U, U any](a T, b T) T { } } -func ComputeLineStarts(text string) []TextPos { +func ComputeECMALineStarts(text string) []TextPos { result := make([]TextPos, 0, strings.Count(text, "\n")+1) - return slices.AppendSeq(result, ComputeLineStartsSeq(text)) + return slices.AppendSeq(result, ComputeECMALineStartsSeq(text)) } -func ComputeLineStartsSeq(text string) iter.Seq[TextPos] { +func ComputeECMALineStartsSeq(text string) iter.Seq[TextPos] { return func(yield func(TextPos) bool) { textLen := TextPos(len(text)) var pos TextPos diff --git a/internal/diagnosticwriter/diagnosticwriter.go b/internal/diagnosticwriter/diagnosticwriter.go index b4d879d224..9603935457 100644 --- a/internal/diagnosticwriter/diagnosticwriter.go +++ b/internal/diagnosticwriter/diagnosticwriter.go @@ -84,13 +84,13 @@ func FormatDiagnosticWithColorAndContext(output io.Writer, diagnostic *ast.Diagn } func writeCodeSnippet(writer io.Writer, sourceFile *ast.SourceFile, start int, length int, squiggleColor string, indent string, formatOpts *FormattingOptions) { - firstLine, firstLineChar := scanner.GetLineAndCharacterOfPosition(sourceFile, start) - lastLine, lastLineChar := scanner.GetLineAndCharacterOfPosition(sourceFile, start+length) + firstLine, firstLineChar := scanner.GetECMALineAndCharacterOfPosition(sourceFile, start) + lastLine, lastLineChar := scanner.GetECMALineAndCharacterOfPosition(sourceFile, start+length) if length == 0 { lastLineChar++ // When length is zero, squiggle the character right after the start position. } - lastLineOfFile, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, len(sourceFile.Text())) + lastLineOfFile, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, len(sourceFile.Text())) hasMoreThanFiveLines := lastLine-firstLine >= 4 gutterWidth := len(strconv.Itoa(lastLine + 1)) @@ -113,10 +113,10 @@ func writeCodeSnippet(writer io.Writer, sourceFile *ast.SourceFile, start int, l i = lastLine - 1 } - lineStart := scanner.GetPositionOfLineAndCharacter(sourceFile, i, 0) + lineStart := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, i, 0) var lineEnd int if i < lastLineOfFile { - lineEnd = scanner.GetPositionOfLineAndCharacter(sourceFile, i+1, 0) + lineEnd = scanner.GetECMAPositionOfLineAndCharacter(sourceFile, i+1, 0) } else { lineEnd = sourceFile.Loc.End() } @@ -216,7 +216,7 @@ func writeWithStyleAndReset(output io.Writer, text string, formatStyle string) { } func WriteLocation(output io.Writer, file *ast.SourceFile, pos int, formatOpts *FormattingOptions, writeWithStyleAndReset FormattedWriter) { - firstLine, firstChar := scanner.GetLineAndCharacterOfPosition(file, pos) + firstLine, firstChar := scanner.GetECMALineAndCharacterOfPosition(file, pos) var relativeFileName string if formatOpts != nil { relativeFileName = tspath.ConvertToRelativePath(file.FileName(), formatOpts.ComparePathsOptions) @@ -357,7 +357,7 @@ func prettyPathForFileError(file *ast.SourceFile, fileErrors []*ast.Diagnostic, if file == nil || len(fileErrors) == 0 { return "" } - line, _ := scanner.GetLineAndCharacterOfPosition(file, fileErrors[0].Loc().Pos()) + line, _ := scanner.GetECMALineAndCharacterOfPosition(file, fileErrors[0].Loc().Pos()) fileName := file.FileName() if tspath.PathIsAbsolute(fileName) && tspath.PathIsAbsolute(formatOpts.CurrentDirectory) { fileName = tspath.ConvertToRelativePath(file.FileName(), formatOpts.ComparePathsOptions) @@ -378,7 +378,7 @@ func WriteFormatDiagnostics(output io.Writer, diagnostics []*ast.Diagnostic, for func WriteFormatDiagnostic(output io.Writer, diagnostic *ast.Diagnostic, formatOpts *FormattingOptions) { if diagnostic.File() != nil { - line, character := scanner.GetLineAndCharacterOfPosition(diagnostic.File(), diagnostic.Loc().Pos()) + line, character := scanner.GetECMALineAndCharacterOfPosition(diagnostic.File(), diagnostic.Loc().Pos()) fileName := diagnostic.File().FileName() relativeFileName := tspath.ConvertToRelativePath(fileName, formatOpts.ComparePathsOptions) fmt.Fprintf(output, "%s(%d,%d): ", relativeFileName, line+1, character+1) diff --git a/internal/format/api.go b/internal/format/api.go index 77be9feece..f9262cd76b 100644 --- a/internal/format/api.go +++ b/internal/format/api.go @@ -146,18 +146,18 @@ func FormatOnSemicolon(ctx context.Context, sourceFile *ast.SourceFile, position } func FormatOnEnter(ctx context.Context, sourceFile *ast.SourceFile, position int) []core.TextChange { - line, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, position) + line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, position) if line == 0 { return nil } // get start position for the previous line - startPos := int(scanner.GetLineStarts(sourceFile)[line-1]) + startPos := int(scanner.GetECMALineStarts(sourceFile)[line-1]) // After the enter key, the cursor is now at a new line. The new line may or may not contain non-whitespace characters. // If the new line has only whitespaces, we won't want to format this line, because that would remove the indentation as // trailing whitespaces. So the end of the formatting span should be the later one between: // 1. the end of the previous line // 2. the last non-whitespace character in the current line - endOfFormatSpan := scanner.GetEndLinePosition(sourceFile, line) + endOfFormatSpan := scanner.GetECMAEndLinePosition(sourceFile, line) for endOfFormatSpan > startPos { ch, s := utf8.DecodeRuneInString(sourceFile.Text()[endOfFormatSpan:]) if s == 0 || stringutil.IsWhiteSpaceSingleLine(ch) { // on multibyte character keep backing up diff --git a/internal/format/indent.go b/internal/format/indent.go index 589a160032..ee8649fe7a 100644 --- a/internal/format/indent.go +++ b/internal/format/indent.go @@ -13,7 +13,7 @@ import ( ) func GetIndentationForNode(n *ast.Node, ignoreActualIndentationRange *core.TextRange, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { - startline, startpos := scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) + startline, startpos := scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) return getIndentationForNodeWorker(n, startline, startpos, ignoreActualIndentationRange /*indentationDelta*/, 0, sourceFile /*isNextChild*/, false, options) } @@ -100,7 +100,7 @@ func getIndentationForNodeWorker( parent = current.Parent if useTrueStart { - currentStartLine, currentStartCharacter = scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(current, sourceFile, false)) + currentStartLine, currentStartCharacter = scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(current, sourceFile, false)) } else { currentStartLine = containingListOrParentStartLine currentStartCharacter = containingListOrParentStartCharacter @@ -131,7 +131,7 @@ func isArgumentAndStartLineOverlapsExpressionBeingCalled(parent *ast.Node, child return false } expressionOfCallExpressionEnd := parent.Expression().End() - expressionOfCallExpressionEndLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd) + expressionOfCallExpressionEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, expressionOfCallExpressionEnd) return expressionOfCallExpressionEndLine == childStartLine } @@ -166,7 +166,7 @@ func getActualIndentationForListStartLine(list *ast.NodeList, sourceFile *ast.So if list == nil { return -1 } - line, char := scanner.GetLineAndCharacterOfPosition(sourceFile, list.Loc.Pos()) + line, char := scanner.GetECMALineAndCharacterOfPosition(sourceFile, list.Loc.Pos()) return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options) } @@ -185,7 +185,7 @@ func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile * continue } // skip list items that ends on the same line with the current list element - prevEndLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, list.Nodes[i].End()) + prevEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, list.Nodes[i].End()) if prevEndLine != line { return findColumnForFirstNonWhitespaceCharacterInLine(line, char, sourceFile, options) } @@ -196,7 +196,7 @@ func deriveActualIndentationFromList(list *ast.NodeList, index int, sourceFile * } func findColumnForFirstNonWhitespaceCharacterInLine(line int, char int, sourceFile *ast.SourceFile, options *FormatCodeSettings) int { - lineStart := scanner.GetPositionOfLineAndCharacter(sourceFile, line, 0) + lineStart := scanner.GetECMAPositionOfLineAndCharacter(sourceFile, line, 0) return FindFirstNonWhitespaceColumn(lineStart, lineStart+char, sourceFile, options) } @@ -247,7 +247,7 @@ func childStartsOnTheSameLineWithElseInIfStatement(parent *ast.Node, child *ast. } func getStartLineAndCharacterForNode(n *ast.Node, sourceFile *ast.SourceFile) (line int, character int) { - return scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) + return scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.GetTokenPosOfNode(n, sourceFile, false)) } func GetContainingList(node *ast.Node, sourceFile *ast.SourceFile) *ast.NodeList { @@ -356,7 +356,7 @@ func getContainingListOrParentStart(parent *ast.Node, child *ast.Node, sourceFil } else { startPos = scanner.GetTokenPosOfNode(parent, sourceFile, false) } - return scanner.GetLineAndCharacterOfPosition(sourceFile, startPos) + return scanner.GetECMALineAndCharacterOfPosition(sourceFile, startPos) } func isControlFlowEndingStatement(kind ast.Kind, parentKind ast.Kind) bool { @@ -439,8 +439,8 @@ func NodeWillIndentChild(settings *FormatCodeSettings, parent *ast.Node, child * return rangeIsOnOneLine(child.Loc, sourceFile) } if parent.Kind == ast.KindBinaryExpression && sourceFile != nil && childKind == ast.KindJsxElement { - parentStartLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), parent.Pos())) - childStartLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), child.Pos())) + parentStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), parent.Pos())) + childStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, scanner.SkipTrivia(sourceFile.Text(), child.Pos())) return parentStartLine != childStartLine } if parent.Kind != ast.KindBinaryExpression { @@ -516,7 +516,7 @@ func NodeWillIndentChild(settings *FormatCodeSettings, parent *ast.Node, child * // branch beginning on the line that the whenTrue branch ends. func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast.Node, childStartLine int, sourceFile *ast.SourceFile) bool { if parent.Kind == ast.KindConditionalExpression && (child == parent.AsConditionalExpression().WhenTrue || child == parent.AsConditionalExpression().WhenFalse) { - conditionEndLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, parent.AsConditionalExpression().Condition.End()) + conditionEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, parent.AsConditionalExpression().Condition.End()) if child == parent.AsConditionalExpression().WhenTrue { return childStartLine == conditionEndLine } else { @@ -528,7 +528,7 @@ func childIsUnindentedBranchOfConditionalExpression(parent *ast.Node, child *ast // 0 L2: indented two stops, one because whenTrue was indented // ); and one because of the parentheses spanning multiple lines trueStartLine, _ := getStartLineAndCharacterForNode(parent.AsConditionalExpression().WhenTrue, sourceFile) - trueEndLine, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End()) + trueEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, parent.AsConditionalExpression().WhenTrue.End()) return conditionEndLine == trueStartLine && trueEndLine == childStartLine } } @@ -550,7 +550,7 @@ func argumentStartsOnSameLineAsPreviousArgument(parent *ast.Node, child *ast.Nod } previousNode := parent.Arguments()[currentIndex-1] - lineOfPreviousNode, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, previousNode.End()) + lineOfPreviousNode, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, previousNode.End()) if childStartLine == lineOfPreviousNode { return true } diff --git a/internal/format/rulecontext.go b/internal/format/rulecontext.go index 0e57e24d38..7b91c7bfff 100644 --- a/internal/format/rulecontext.go +++ b/internal/format/rulecontext.go @@ -584,8 +584,8 @@ func isSemicolonDeletionContext(context *formattingContext) bool { nextTokenStart = scanner.GetTokenPosOfNode(nextRealToken, context.SourceFile, false) } - startLine, _ := scanner.GetLineAndCharacterOfPosition(context.SourceFile, context.currentTokenSpan.Loc.Pos()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(context.SourceFile, nextTokenStart) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(context.SourceFile, context.currentTokenSpan.Loc.Pos()) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(context.SourceFile, nextTokenStart) if startLine == endLine { return nextTokenKind == ast.KindCloseBraceToken || nextTokenKind == ast.KindEndOfFile } diff --git a/internal/format/span.go b/internal/format/span.go index 7276a97ebf..4657949f7e 100644 --- a/internal/format/span.go +++ b/internal/format/span.go @@ -85,7 +85,7 @@ func getOwnOrInheritedDelta(n *ast.Node, options *FormatCodeSettings, sourceFile previousLine := -1 var child *ast.Node for n != nil { - line, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, withTokenStart(n, sourceFile).Pos()) + line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, withTokenStart(n, sourceFile).Pos()) if previousLine != -1 && line != previousLine { break } @@ -242,10 +242,10 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange { w.formattingScanner.advance() if w.formattingScanner.isOnToken() { - startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, withTokenStart(w.enclosingNode, w.sourceFile).Pos()) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, withTokenStart(w.enclosingNode, w.sourceFile).Pos()) undecoratedStartLine := startLine if ast.HasDecorators(w.enclosingNode) { - undecoratedStartLine, _ = scanner.GetLineAndCharacterOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(w.enclosingNode, w.sourceFile)) + undecoratedStartLine, _ = scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(w.enclosingNode, w.sourceFile)) } w.processNode(w.enclosingNode, w.enclosingNode, startLine, undecoratedStartLine, w.initialIndentation, w.delta) @@ -262,7 +262,7 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange { } w.indentTriviaItems(remainingTrivia, indentation, true, func(item TextRangeWithKind) { - startLine, startChar := scanner.GetLineAndCharacterOfPosition(w.sourceFile, item.Loc.Pos()) + startLine, startChar := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, item.Loc.Pos()) w.processRange(item, startLine, startChar, w.enclosingNode, w.enclosingNode, nil) w.insertIndentation(item.Loc.Pos(), indentation, false) }) @@ -305,7 +305,7 @@ func (w *formatSpanWorker) execute(s *formattingScanner) []core.TextChange { if parent == nil { parent = w.previousParent } - line, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, tokenInfo.Loc.Pos()) + line, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, tokenInfo.Loc.Pos()) w.processPair( tokenInfo, line, @@ -343,11 +343,11 @@ func (w *formatSpanWorker) processChildNode( } childStartPos := scanner.GetTokenPosOfNode(child, w.sourceFile, false) - childStartLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, childStartPos) + childStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, childStartPos) undecoratedChildStartLine := childStartLine if ast.HasDecorators(child) { - undecoratedChildStartLine, _ = scanner.GetLineAndCharacterOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(child, w.sourceFile)) + undecoratedChildStartLine, _ = scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, getNonDecoratorTokenPosOfNode(child, w.sourceFile)) } // if child is a list item - try to get its indentation, only if parent is within the original range. @@ -457,7 +457,7 @@ func (w *formatSpanWorker) processChildNodes( break } else if tokenInfo.token.Kind == listStartToken { // consume list start token - startLine, _ = scanner.GetLineAndCharacterOfPosition(w.sourceFile, tokenInfo.token.Loc.Pos()) + startLine, _ = scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, tokenInfo.token.Loc.Pos()) w.consumeTokenAndAdvanceScanner(tokenInfo, parent, parentDynamicIndentation, parent, false) @@ -578,7 +578,7 @@ func (w *formatSpanWorker) tryComputeIndentationForListItem(startPos int, endPos return inheritedIndentation } } else { - startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, startPos) startLinePosition := GetLineStartPositionForPosition(startPos, w.sourceFile) column := FindFirstNonWhitespaceColumn(startLinePosition, startPos, w.sourceFile, w.formattingContext.Options) if startLine != parentStartLine || startPos == column { @@ -747,7 +747,7 @@ func (w *formatSpanWorker) processRange(r TextRangeWithKind, rangeStartLine int, if !rangeHasError { if w.previousRange == NewTextRangeWithKind(0, 0, 0) { // trim whitespaces starting from the beginning of the span up to the current line - originalStartLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, w.originalRange.Pos()) + originalStartLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, w.originalRange.Pos()) w.trimTrailingWhitespacesForLines(originalStartLine, rangeStartLine, NewTextRangeWithKind(0, 0, 0)) } else { lineAction = w.processPair(r, rangeStartLine, parent, w.previousRange, w.previousRangeStartLine, w.previousParent, contextNode, dynamicIndentation) @@ -765,7 +765,7 @@ func (w *formatSpanWorker) processRange(r TextRangeWithKind, rangeStartLine int, func (w *formatSpanWorker) processTrivia(trivia []TextRangeWithKind, parent *ast.Node, contextNode *ast.Node, dynamicIndentation *dynamicIndenter) { for _, triviaItem := range trivia { if isComment(triviaItem.Kind) && triviaItem.Loc.ContainedBy(w.originalRange) { - triviaItemStartLine, triviaItemStartCharacter := scanner.GetLineAndCharacterOfPosition(w.sourceFile, triviaItem.Loc.Pos()) + triviaItemStartLine, triviaItemStartCharacter := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, triviaItem.Loc.Pos()) w.processRange(triviaItem, triviaItemStartLine, triviaItemStartCharacter, parent, contextNode, dynamicIndentation) } } @@ -797,17 +797,17 @@ func (w *formatSpanWorker) trimTrailingWhitespacesForRemainingRange(trivias []Te } func (w *formatSpanWorker) trimTrailingWitespacesForPositions(startPos int, endPos int, previousRange TextRangeWithKind) { - startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, startPos) - endLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, endPos) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, startPos) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, endPos) w.trimTrailingWhitespacesForLines(startLine, endLine+1, previousRange) } func (w *formatSpanWorker) trimTrailingWhitespacesForLines(line1 int, line2 int, r TextRangeWithKind) { - lineStarts := scanner.GetLineStarts(w.sourceFile) + lineStarts := scanner.GetECMALineStarts(w.sourceFile) for line := line1; line < line2; line++ { lineStartPosition := int(lineStarts[line]) - lineEndPosition := scanner.GetEndLinePosition(w.sourceFile, line) + lineEndPosition := scanner.GetECMAEndLinePosition(w.sourceFile, line) // do not trim whitespaces in comments or template expression if r != NewTextRangeWithKind(0, 0, 0) && (isComment(r.Kind) || isStringOrRegularExpressionOrTemplateLiteral(r.Kind)) && r.Loc.Pos() <= lineEndPosition && r.Loc.End() > lineEndPosition { @@ -862,8 +862,8 @@ func (w *formatSpanWorker) insertIndentation(pos int, indentation int, lineAdded // insert indentation string at the very beginning of the token w.recordReplace(pos, 0, indentationString) } else { - tokenStartLine, tokenStartCharacter := scanner.GetLineAndCharacterOfPosition(w.sourceFile, pos) - startLinePosition := int(scanner.GetLineStarts(w.sourceFile)[tokenStartLine]) + tokenStartLine, tokenStartCharacter := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, pos) + startLinePosition := int(scanner.GetECMALineStarts(w.sourceFile)[tokenStartLine]) if indentation != w.characterToColumn(startLinePosition, tokenStartCharacter) || w.indentationIsDifferent(indentationString, startLinePosition) { w.recordReplace(startLinePosition, tokenStartCharacter, indentationString) } @@ -909,8 +909,8 @@ func (w *formatSpanWorker) indentTriviaItems(trivia []TextRangeWithKind, comment func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, indentation int, firstLineIsIndented bool, indentFinalLine bool) { // split comment in lines - startLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, commentRange.Pos()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, commentRange.End()) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, commentRange.Pos()) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, commentRange.End()) if startLine == endLine { if !firstLineIsIndented { @@ -923,9 +923,9 @@ func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, i parts := make([]core.TextRange, 0, strings.Count(w.sourceFile.Text()[commentRange.Pos():commentRange.End()], "\n")) startPos := commentRange.Pos() for line := startLine; line < endLine; line++ { - endOfLine := scanner.GetEndLinePosition(w.sourceFile, line) + endOfLine := scanner.GetECMAEndLinePosition(w.sourceFile, line) parts = append(parts, core.NewTextRange(startPos, endOfLine)) - startPos = int(scanner.GetLineStarts(w.sourceFile)[line+1]) + startPos = int(scanner.GetECMALineStarts(w.sourceFile)[line+1]) } if indentFinalLine { @@ -936,7 +936,7 @@ func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, i return } - startLinePos := int(scanner.GetLineStarts(w.sourceFile)[startLine]) + startLinePos := int(scanner.GetECMALineStarts(w.sourceFile)[startLine]) nonWhitespaceInFirstPartCharacter, nonWhitespaceInFirstPartColumn := findFirstNonWhitespaceCharacterAndColumn(startLinePos, parts[0].Pos(), w.sourceFile, w.formattingContext.Options) @@ -950,7 +950,7 @@ func (w *formatSpanWorker) indentMultilineComment(commentRange core.TextRange, i // shift all parts on the delta size delta := indentation - nonWhitespaceInFirstPartColumn for i := startIndex; i < len(parts); i++ { - startLinePos := int(scanner.GetLineStarts(w.sourceFile)[startLine]) + startLinePos := int(scanner.GetECMALineStarts(w.sourceFile)[startLine]) nonWhitespaceCharacter := nonWhitespaceInFirstPartCharacter nonWhitespaceColumn := nonWhitespaceInFirstPartColumn if i != 0 { @@ -1021,7 +1021,7 @@ func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo tokenI lineAction := LineActionNone isTokenInRange := currentTokenInfo.token.Loc.ContainedBy(w.originalRange) - tokenStartLine, tokenStartChar := scanner.GetLineAndCharacterOfPosition(w.sourceFile, currentTokenInfo.token.Loc.Pos()) + tokenStartLine, tokenStartChar := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, currentTokenInfo.token.Loc.Pos()) if isTokenInRange { rangeHasError := w.rangeContainsError(currentTokenInfo.token.Loc) @@ -1033,7 +1033,7 @@ func (w *formatSpanWorker) consumeTokenAndAdvanceScanner(currentTokenInfo tokenI if lineAction == LineActionNone { // indent token only if end line of previous range does not match start line of the token if savePreviousRange != NewTextRangeWithKind(0, 0, 0) { - prevEndLine, _ := scanner.GetLineAndCharacterOfPosition(w.sourceFile, savePreviousRange.Loc.End()) + prevEndLine, _ := scanner.GetECMALineAndCharacterOfPosition(w.sourceFile, savePreviousRange.Loc.End()) indentToken = lastTriviaWasNewLine && tokenStartLine != prevEndLine } } else { diff --git a/internal/format/util.go b/internal/format/util.go index 4cce2ed250..0bf8bc7048 100644 --- a/internal/format/util.go +++ b/internal/format/util.go @@ -10,8 +10,8 @@ import ( ) func rangeIsOnOneLine(node core.TextRange, file *ast.SourceFile) bool { - startLine, _ := scanner.GetLineAndCharacterOfPosition(file, node.Pos()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(file, node.End()) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, node.Pos()) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, node.End()) return startLine == endLine } @@ -76,8 +76,8 @@ func getCloseTokenForOpenToken(kind ast.Kind) ast.Kind { } func GetLineStartPositionForPosition(position int, sourceFile *ast.SourceFile) int { - lineStarts := scanner.GetLineStarts(sourceFile) - line, _ := scanner.GetLineAndCharacterOfPosition(sourceFile, position) + lineStarts := scanner.GetECMALineStarts(sourceFile) + line, _ := scanner.GetECMALineAndCharacterOfPosition(sourceFile, position) return int(lineStarts[line]) } diff --git a/internal/fourslash/baselineutil.go b/internal/fourslash/baselineutil.go index fcfbaca77c..83e683b1d4 100644 --- a/internal/fourslash/baselineutil.go +++ b/internal/fourslash/baselineutil.go @@ -357,7 +357,7 @@ type textWithContext struct { isLibFile bool fileName string content string // content of the original file - lineStarts *ls.LineMap + lineStarts *ls.LSPLineMap converters *ls.Converters // posLineInfo @@ -386,10 +386,10 @@ func newTextWithContext(fileName string, content string) *textWithContext { pos: lsproto.Position{Line: 0, Character: 0}, fileName: fileName, content: content, - lineStarts: ls.ComputeLineStarts(content), + lineStarts: ls.ComputeLSPLineStarts(content), } - t.converters = ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LineMap { + t.converters = ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LSPLineMap { return t.lineStarts }) t.readableContents.WriteString("// === " + fileName + " ===") diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 2fff230f31..6b9e932aad 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -51,7 +51,7 @@ type FourslashTest struct { type scriptInfo struct { fileName string content string - lineMap *ls.LineMap + lineMap *ls.LSPLineMap version int32 } @@ -59,14 +59,14 @@ func newScriptInfo(fileName string, content string) *scriptInfo { return &scriptInfo{ fileName: fileName, content: content, - lineMap: ls.ComputeLineStarts(content), + lineMap: ls.ComputeLSPLineStarts(content), version: 1, } } func (s *scriptInfo) editContent(start int, end int, newText string) { s.content = s.content[:start] + newText + s.content[end:] - s.lineMap = ls.ComputeLineStarts(s.content) + s.lineMap = ls.ComputeLSPLineStarts(s.content) s.version++ } @@ -170,7 +170,7 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten } }() - converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName string) *ls.LineMap { + converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(fileName string) *ls.LSPLineMap { scriptInfo, ok := scriptInfos[fileName] if !ok { return nil @@ -1484,7 +1484,7 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames ))) currentFile := newScriptInfo(f.activeFilename, fileContent) - converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LineMap { + converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LSPLineMap { return currentFile.lineMap }) var list []*lsproto.CompletionItem diff --git a/internal/fourslash/test_parser.go b/internal/fourslash/test_parser.go index 346a5eb811..da8de6fc79 100644 --- a/internal/fourslash/test_parser.go +++ b/internal/fourslash/test_parser.go @@ -368,8 +368,8 @@ func parseFileContent(fileName string, content string, fileOptions map[string]st outputString := output.String() // Set LS positions for markers - lineMap := ls.ComputeLineStarts(outputString) - converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LineMap { + lineMap := ls.ComputeLSPLineStarts(outputString) + converters := ls.NewConverters(lsproto.PositionEncodingKindUTF8, func(_ string) *ls.LSPLineMap { return lineMap }) diff --git a/internal/ls/changetrackerimpl.go b/internal/ls/changetrackerimpl.go index 39e073fad3..e8f3d13e69 100644 --- a/internal/ls/changetrackerimpl.go +++ b/internal/ls/changetrackerimpl.go @@ -204,7 +204,7 @@ func (ct *changeTracker) getAdjustedStartPosition(sourceFile *ast.SourceFile, no if fullStart == start { return start } - lineStarts := sourceFile.LineMap() + lineStarts := sourceFile.ECMALineMap() fullStartLineIndex := scanner.ComputeLineOfPosition(lineStarts, fullStart) fullStartLinePos := int(lineStarts[fullStartLineIndex]) if startOfLinePos == fullStartLinePos { @@ -249,7 +249,7 @@ func (ct *changeTracker) getEndPositionOfMultilineTrailingComment(sourceFile *as if trailingOpt == trailingTriviaOptionInclude { // If the trailing comment is a multiline comment that extends to the next lines, // return the end of the comment and track it for the next nodes to adjust. - lineStarts := sourceFile.LineMap() + lineStarts := sourceFile.ECMALineMap() nodeEndLine := scanner.ComputeLineOfPosition(lineStarts, node.End()) for comment := range scanner.GetTrailingCommentRanges(ct.NodeFactory, sourceFile.Text(), node.End()) { // Single line can break the loop as trivia will only be this line. @@ -363,7 +363,7 @@ func (ct *changeTracker) getInsertionPositionAtSourceFileTop(sourceFile *ast.Sou firstNodeLine := -1 lenStatements := len(sourceFile.Statements.Nodes) - lineMap := sourceFile.LineMap() + lineMap := sourceFile.ECMALineMap() for _, r := range ranges { if r.Kind == ast.KindMultiLineCommentTrivia { if printer.IsPinnedComment(text, r) { diff --git a/internal/ls/completions.go b/internal/ls/completions.go index 9a1dc9836d..6966553a1e 100644 --- a/internal/ls/completions.go +++ b/internal/ls/completions.go @@ -2541,13 +2541,13 @@ func jsxAttributeCompletionStyleIs(preferenceStyle *JsxAttributeCompletionStyle, } func getLineOfPosition(file *ast.SourceFile, pos int) int { - line, _ := scanner.GetLineAndCharacterOfPosition(file, pos) + line, _ := scanner.GetECMALineAndCharacterOfPosition(file, pos) return line } func getLineEndOfPosition(file *ast.SourceFile, pos int) int { line := getLineOfPosition(file, pos) - lineStarts := scanner.GetLineStarts(file) + lineStarts := scanner.GetECMALineStarts(file) var lastCharPos int if line+1 >= len(lineStarts) { lastCharPos = file.End() @@ -3532,8 +3532,8 @@ func getContextualKeywords(file *ast.SourceFile, contextToken *ast.Node, positio // Source: https://tc39.es/proposal-import-assertions/ if contextToken != nil { parent := contextToken.Parent - tokenLine, _ := scanner.GetLineAndCharacterOfPosition(file, contextToken.End()) - currentLine, _ := scanner.GetLineAndCharacterOfPosition(file, position) + tokenLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, contextToken.End()) + currentLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, position) if (ast.IsImportDeclaration(parent) || ast.IsExportDeclaration(parent) && parent.AsExportDeclaration().ModuleSpecifier != nil) && contextToken == parent.ModuleSpecifier() && diff --git a/internal/ls/converters.go b/internal/ls/converters.go index af16188501..b7730f3e5d 100644 --- a/internal/ls/converters.go +++ b/internal/ls/converters.go @@ -14,7 +14,7 @@ import ( ) type Converters struct { - getLineMap func(fileName string) *LineMap + getLineMap func(fileName string) *LSPLineMap positionEncoding lsproto.PositionEncodingKind } @@ -23,7 +23,7 @@ type Script interface { Text() string } -func NewConverters(positionEncoding lsproto.PositionEncodingKind, getLineMap func(fileName string) *LineMap) *Converters { +func NewConverters(positionEncoding lsproto.PositionEncodingKind, getLineMap func(fileName string) *LSPLineMap) *Converters { return &Converters{ getLineMap: getLineMap, positionEncoding: positionEncoding, diff --git a/internal/ls/linemap.go b/internal/ls/linemap.go index 612eca39d0..1c2c05518e 100644 --- a/internal/ls/linemap.go +++ b/internal/ls/linemap.go @@ -9,12 +9,12 @@ import ( "github.com/microsoft/typescript-go/internal/core" ) -type LineMap struct { +type LSPLineMap struct { LineStarts []core.TextPos AsciiOnly bool // TODO(jakebailey): collect ascii-only info per line } -func ComputeLineStarts(text string) *LineMap { +func ComputeLSPLineStarts(text string) *LSPLineMap { // This is like core.ComputeLineStarts, but only considers "\n", "\r", and "\r\n" as line breaks, // and reports when the text is ASCII-only. lineStarts := make([]core.TextPos, 0, strings.Count(text, "\n")+1) @@ -45,13 +45,13 @@ func ComputeLineStarts(text string) *LineMap { } lineStarts = append(lineStarts, lineStart) - return &LineMap{ + return &LSPLineMap{ LineStarts: lineStarts, AsciiOnly: asciiOnly, } } -func (lm *LineMap) ComputeIndexOfLineStart(targetPos core.TextPos) int { +func (lm *LSPLineMap) ComputeIndexOfLineStart(targetPos core.TextPos) int { // port of computeLineOfPosition(lineStarts: readonly number[], position: number, lowerBound?: number): number { lineNumber, ok := slices.BinarySearchFunc(lm.LineStarts, targetPos, func(p, t core.TextPos) int { return cmp.Compare(int(p), int(t)) diff --git a/internal/ls/utilities.go b/internal/ls/utilities.go index 0610524c7b..d0078ba56d 100644 --- a/internal/ls/utilities.go +++ b/internal/ls/utilities.go @@ -490,10 +490,10 @@ func probablyUsesSemicolons(file *ast.SourceFile) bool { if lastToken != nil && lastToken.Kind == ast.KindSemicolonToken { withSemicolon++ } else if lastToken != nil && lastToken.Kind != ast.KindCommaToken { - lastTokenLine, _ := scanner.GetLineAndCharacterOfPosition( + lastTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( file, astnav.GetStartOfNode(lastToken, file, false /*includeJSDoc*/)) - nextTokenLine, _ := scanner.GetLineAndCharacterOfPosition( + nextTokenLine, _ := scanner.GetECMALineAndCharacterOfPosition( file, scanner.GetRangeOfTokenAtPosition(file, lastToken.End()).Pos()) // Avoid counting missing semicolon in single-line objects: diff --git a/internal/lsutil/asi.go b/internal/lsutil/asi.go index 08456f2739..0cea3e6d4d 100644 --- a/internal/lsutil/asi.go +++ b/internal/lsutil/asi.go @@ -98,7 +98,7 @@ func NodeIsASICandidate(node *ast.Node, file *ast.SourceFile) bool { return true } - startLine, _ := scanner.GetLineAndCharacterOfPosition(file, node.End()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/)) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, node.End()) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(file, astnav.GetStartOfNode(nextToken, file, false /*includeJSDoc*/)) return startLine != endLine } diff --git a/internal/printer/printer.go b/internal/printer/printer.go index c98583b6cc..f9581f9e10 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -629,7 +629,7 @@ func (p *Printer) writeCommentRange(comment ast.CommentRange) { } text := p.currentSourceFile.Text() - lineMap := p.currentSourceFile.LineMap() + lineMap := p.currentSourceFile.ECMALineMap() p.writeCommentRangeWorker(text, lineMap, comment.Kind, comment.TextRange) } @@ -5227,7 +5227,7 @@ func (p *Printer) writeSynthesizedComment(comment SynthesizedComment) { text := formatSynthesizedComment(comment) var lineMap []core.TextPos if comment.Kind == ast.KindMultiLineCommentTrivia { - lineMap = core.ComputeLineStarts(text) + lineMap = core.ComputeECMALineStarts(text) } p.writeCommentRangeWorker(text, lineMap, comment.Kind, core.NewTextRange(0, len(text))) } @@ -5294,7 +5294,7 @@ func (p *Printer) shouldEmitNewLineBeforeLeadingCommentOfPosition(pos int, comme // If the leading comments start on different line than the start of node, write new line return p.currentSourceFile != nil && pos != commentPos && - scanner.ComputeLineOfPosition(p.currentSourceFile.LineMap(), pos) != scanner.ComputeLineOfPosition(p.currentSourceFile.LineMap(), commentPos) + scanner.ComputeLineOfPosition(p.currentSourceFile.ECMALineMap(), pos) != scanner.ComputeLineOfPosition(p.currentSourceFile.ECMALineMap(), commentPos) } func (p *Printer) emitTrailingComments(pos int, commentSeparator commentSeparator) { @@ -5329,7 +5329,7 @@ func (p *Printer) emitDetachedComments(textRange core.TextRange) (result detache } text := p.currentSourceFile.Text() - lineMap := p.currentSourceFile.LineMap() + lineMap := p.currentSourceFile.ECMALineMap() var leadingComments []ast.CommentRange if p.commentsDisabled { @@ -5482,7 +5482,7 @@ func (p *Printer) emitPos(pos int) { return } - sourceLine, sourceCharacter := scanner.GetLineAndCharacterOfPosition(p.sourceMapSource, pos) + sourceLine, sourceCharacter := scanner.GetECMALineAndCharacterOfPosition(p.sourceMapSource, pos) if err := p.sourceMapGenerator.AddSourceMapping( p.writer.GetLine(), p.writer.GetColumn(), diff --git a/internal/printer/textwriter.go b/internal/printer/textwriter.go index f36742e0a5..6b1bd0252f 100644 --- a/internal/printer/textwriter.go +++ b/internal/printer/textwriter.go @@ -89,7 +89,7 @@ func (w *textWriter) updateLineCountAndPosFor(s string) { var count int var lastLineStart core.TextPos - for lineStart := range core.ComputeLineStartsSeq(s) { + for lineStart := range core.ComputeECMALineStartsSeq(s) { count++ lastLineStart = lineStart } diff --git a/internal/printer/utilities.go b/internal/printer/utilities.go index 0b28e865ab..41f20efb10 100644 --- a/internal/printer/utilities.go +++ b/internal/printer/utilities.go @@ -369,7 +369,7 @@ func GetLinesBetweenPositions(sourceFile *ast.SourceFile, pos1 int, pos2 int) in if pos1 == pos2 { return 0 } - lineStarts := scanner.GetLineStarts(sourceFile) + lineStarts := scanner.GetECMALineStarts(sourceFile) lower := core.IfElse(pos1 < pos2, pos1, pos2) isNegative := lower == pos2 upper := core.IfElse(isNegative, pos1, pos2) diff --git a/internal/project/compilerhost.go b/internal/project/compilerhost.go index 36cf2dfc4c..d58b3522b8 100644 --- a/internal/project/compilerhost.go +++ b/internal/project/compilerhost.go @@ -6,7 +6,6 @@ import ( "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" - "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/project/logging" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" @@ -131,13 +130,6 @@ func (c *compilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.Sourc return nil } -func (c *compilerHost) GetLineMap(fileName string) *ls.LineMap { - if fh := c.compilerFS.source.GetFile(fileName); fh != nil { - return fh.LineMap() - } - return nil -} - // Trace implements compiler.CompilerHost. func (c *compilerHost) Trace(msg string) { panic("unimplemented") diff --git a/internal/project/overlayfs.go b/internal/project/overlayfs.go index a59e1a74d6..37cb1ae498 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -23,7 +23,7 @@ type FileHandle interface { Version() int32 MatchesDiskText() bool IsOverlay() bool - LineMap() *ls.LineMap + LSPLineMap() *ls.LSPLineMap Kind() core.ScriptKind } @@ -33,7 +33,7 @@ type fileBase struct { hash xxh3.Uint128 lineMapOnce sync.Once - lineMap *ls.LineMap + lineMap *ls.LSPLineMap } func (f *fileBase) FileName() string { @@ -48,9 +48,9 @@ func (f *fileBase) Content() string { return f.content } -func (f *fileBase) LineMap() *ls.LineMap { +func (f *fileBase) LSPLineMap() *ls.LSPLineMap { f.lineMapOnce.Do(func() { - f.lineMap = ls.ComputeLineStarts(f.content) + f.lineMap = ls.ComputeLSPLineStarts(f.content) }) return f.lineMap } @@ -318,8 +318,8 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma panic("overlay not found for changed file: " + uri) } for _, change := range events.changes { - converters := ls.NewConverters(fs.positionEncoding, func(fileName string) *ls.LineMap { - return o.LineMap() + converters := ls.NewConverters(fs.positionEncoding, func(fileName string) *ls.LSPLineMap { + return o.LSPLineMap() }) for _, textChange := range change.Changes { if partialChange := textChange.Partial; partialChange != nil { diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index b5d7d1722e..e53f113df8 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -59,7 +59,7 @@ func NewSnapshot( ProjectCollection: &ProjectCollection{toPath: toPath}, compilerOptionsForInferredProjects: compilerOptionsForInferredProjects, } - s.converters = ls.NewConverters(s.sessionOptions.PositionEncoding, s.LineMap) + s.converters = ls.NewConverters(s.sessionOptions.PositionEncoding, s.LSPLineMap) s.refCount.Store(1) return s } @@ -74,9 +74,9 @@ func (s *Snapshot) GetFile(fileName string) FileHandle { return s.fs.GetFile(fileName) } -func (s *Snapshot) LineMap(fileName string) *ls.LineMap { +func (s *Snapshot) LSPLineMap(fileName string) *ls.LSPLineMap { if file := s.fs.GetFile(fileName); file != nil { - return file.LineMap() + return file.LSPLineMap() } return nil } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index ede9405f83..90ef2f1aef 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -2331,12 +2331,12 @@ func getErrorRangeForArrowFunction(sourceFile *ast.SourceFile, node *ast.Node) c pos := SkipTrivia(sourceFile.Text(), node.Pos()) body := node.AsArrowFunction().Body if body != nil && body.Kind == ast.KindBlock { - startLine, _ := GetLineAndCharacterOfPosition(sourceFile, body.Pos()) - endLine, _ := GetLineAndCharacterOfPosition(sourceFile, body.End()) + startLine, _ := GetECMALineAndCharacterOfPosition(sourceFile, body.Pos()) + endLine, _ := GetECMALineAndCharacterOfPosition(sourceFile, body.End()) if startLine < endLine { // The arrow function spans multiple lines, // make the error span be the first line, inclusive. - return core.NewTextRange(pos, GetEndLinePosition(sourceFile, startLine)) + return core.NewTextRange(pos, GetECMAEndLinePosition(sourceFile, startLine)) } } return core.NewTextRange(pos, node.End()) @@ -2424,19 +2424,19 @@ func ComputeLineOfPosition(lineStarts []core.TextPos, pos int) int { return low - 1 } -func GetLineStarts(sourceFile ast.SourceFileLike) []core.TextPos { - return sourceFile.LineMap() +func GetECMALineStarts(sourceFile ast.SourceFileLike) []core.TextPos { + return sourceFile.ECMALineMap() } -func GetLineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) (line int, character int) { - lineMap := GetLineStarts(sourceFile) +func GetECMALineAndCharacterOfPosition(sourceFile ast.SourceFileLike, pos int) (line int, character int) { + lineMap := GetECMALineStarts(sourceFile) line = ComputeLineOfPosition(lineMap, pos) character = utf8.RuneCountInString(sourceFile.Text()[lineMap[line]:pos]) return line, character } -func GetEndLinePosition(sourceFile *ast.SourceFile, line int) int { - pos := int(GetLineStarts(sourceFile)[line]) +func GetECMAEndLinePosition(sourceFile *ast.SourceFile, line int) int { + pos := int(GetECMALineStarts(sourceFile)[line]) for { ch, size := utf8.DecodeRuneInString(sourceFile.Text()[pos:]) if size == 0 || stringutil.IsLineBreak(ch) { @@ -2446,8 +2446,8 @@ func GetEndLinePosition(sourceFile *ast.SourceFile, line int) int { } } -func GetPositionOfLineAndCharacter(sourceFile *ast.SourceFile, line int, character int) int { - return ComputePositionOfLineAndCharacter(GetLineStarts(sourceFile), line, character) +func GetECMAPositionOfLineAndCharacter(sourceFile *ast.SourceFile, line int, character int) int { + return ComputePositionOfLineAndCharacter(GetECMALineStarts(sourceFile), line, character) } func ComputePositionOfLineAndCharacter(lineStarts []core.TextPos, line int, character int) int { diff --git a/internal/sourcemap/lineinfo.go b/internal/sourcemap/lineinfo.go index 6baf74788e..f3dee2cab1 100644 --- a/internal/sourcemap/lineinfo.go +++ b/internal/sourcemap/lineinfo.go @@ -2,23 +2,23 @@ package sourcemap import "github.com/microsoft/typescript-go/internal/core" -type LineInfo struct { +type ECMALineInfo struct { text string lineStarts []core.TextPos } -func GetLineInfo(text string, lineStarts []core.TextPos) *LineInfo { - return &LineInfo{ +func GetECMALineInfo(text string, lineStarts []core.TextPos) *ECMALineInfo { + return &ECMALineInfo{ text: text, lineStarts: lineStarts, } } -func (li *LineInfo) LineCount() int { +func (li *ECMALineInfo) LineCount() int { return len(li.lineStarts) } -func (li *LineInfo) LineText(line int) string { +func (li *ECMALineInfo) LineText(line int) string { pos := li.lineStarts[line] var end core.TextPos if line+1 < len(li.lineStarts) { diff --git a/internal/sourcemap/source.go b/internal/sourcemap/source.go index bbc6c7b983..e781e7b874 100644 --- a/internal/sourcemap/source.go +++ b/internal/sourcemap/source.go @@ -5,5 +5,5 @@ import "github.com/microsoft/typescript-go/internal/core" type Source interface { Text() string FileName() string - LineMap() []core.TextPos + ECMALineMap() []core.TextPos } diff --git a/internal/sourcemap/util.go b/internal/sourcemap/util.go index 4ee874ca7a..c36003eec4 100644 --- a/internal/sourcemap/util.go +++ b/internal/sourcemap/util.go @@ -8,7 +8,7 @@ import ( ) // Tries to find the sourceMappingURL comment at the end of a file. -func TryGetSourceMappingURL(lineInfo *LineInfo) string { +func TryGetSourceMappingURL(lineInfo *ECMALineInfo) string { for index := lineInfo.LineCount() - 1; index >= 0; index-- { line := lineInfo.LineText(index) line = strings.TrimLeftFunc(line, unicode.IsSpace) diff --git a/internal/testutil/harnessutil/sourcemap_recorder.go b/internal/testutil/harnessutil/sourcemap_recorder.go index 48cdf024c6..d5d6521673 100644 --- a/internal/testutil/harnessutil/sourcemap_recorder.go +++ b/internal/testutil/harnessutil/sourcemap_recorder.go @@ -94,7 +94,7 @@ func newSourceMapSpanWriter(sourceMapRecorder *writerAggregator, sourceMap *sour sourceMapSources: sourceMap.Sources, sourceMapNames: sourceMap.Names, jsFile: jsFile, - jsLineMap: core.ComputeLineStarts(jsFile.Content), + jsLineMap: core.ComputeECMALineStarts(jsFile.Content), spansOnSingleLine: make([]sourceMapSpanWithDecodeErrors, 0), prevWrittenSourcePos: 0, nextJsLineToWrite: 0, @@ -104,7 +104,7 @@ func newSourceMapSpanWriter(sourceMapRecorder *writerAggregator, sourceMap *sour sourceMapRecorder.WriteLine("===================================================================") sourceMapRecorder.WriteLineF("JsFile: %s", sourceMap.File) - sourceMapRecorder.WriteLineF("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.GetLineInfo(jsFile.Content, writer.jsLineMap))) + sourceMapRecorder.WriteLineF("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.GetECMALineInfo(jsFile.Content, writer.jsLineMap))) sourceMapRecorder.WriteLineF("sourceRoot: %s", sourceMap.SourceRoot) sourceMapRecorder.WriteLineF("sources: %s", strings.Join(sourceMap.Sources, ",")) if len(sourceMap.SourcesContent) > 0 { @@ -187,7 +187,7 @@ func (w *sourceMapSpanWriter) recordNewSourceFileSpan(sourceMapSpan *sourcemap.M w.sourceMapRecorder.WriteLineF("sourceFile:%s", w.sourceMapSources[w.spansOnSingleLine[0].sourceMapSpan.SourceIndex]) w.sourceMapRecorder.WriteLine("-------------------------------------------------------------------") - w.tsLineMap = core.ComputeLineStarts(newSourceFileCode) + w.tsLineMap = core.ComputeECMALineStarts(newSourceFileCode) w.tsCode = newSourceFileCode w.prevWrittenSourcePos = 0 } @@ -302,7 +302,7 @@ func (sw *recordedSpanWriter) writeSourceMapSourceText(currentSpan *sourceMapSpa sw.w.sourceMapRecorder.WriteLine(decodeError) } - tsCodeLineMap := core.ComputeLineStarts(sourceText) + tsCodeLineMap := core.ComputeECMALineStarts(sourceText) for i := range tsCodeLineMap { if i == 0 { sw.writeSourceMapIndent(sw.prevEmittedCol, sw.markerIds[index]) diff --git a/internal/testutil/tsbaseline/error_baseline.go b/internal/testutil/tsbaseline/error_baseline.go index 7a27f6067a..1490c267e5 100644 --- a/internal/testutil/tsbaseline/error_baseline.go +++ b/internal/testutil/tsbaseline/error_baseline.go @@ -171,7 +171,7 @@ func iterateErrorBaseline(t *testing.T, inputFiles []*harnessutil.TestFile, inpu markedErrorCount := 0 // For each line, emit the line followed by any error squiggles matching this line - lineStarts := core.ComputeLineStarts(inputFile.Content) + lineStarts := core.ComputeECMALineStarts(inputFile.Content) lines := lineDelimiter.Split(inputFile.Content, -1) for lineIndex, line := range lines { diff --git a/internal/testutil/tsbaseline/type_symbol_baseline.go b/internal/testutil/tsbaseline/type_symbol_baseline.go index 414aa04d91..d8f40d133c 100644 --- a/internal/testutil/tsbaseline/type_symbol_baseline.go +++ b/internal/testutil/tsbaseline/type_symbol_baseline.go @@ -343,7 +343,7 @@ func forEachASTNode(node *ast.Node) []*ast.Node { func (walker *typeWriterWalker) writeTypeOrSymbol(node *ast.Node, isSymbolWalk bool) *typeWriterResult { actualPos := scanner.SkipTrivia(walker.currentSourceFile.Text(), node.Pos()) - line, _ := scanner.GetLineAndCharacterOfPosition(walker.currentSourceFile, actualPos) + line, _ := scanner.GetECMALineAndCharacterOfPosition(walker.currentSourceFile, actualPos) sourceText := scanner.GetSourceTextOfNodeFromSourceFile(walker.currentSourceFile, node, false /*includeTrivia*/) fileChecker, done := walker.getTypeCheckerForCurrentFile() defer done() @@ -434,7 +434,7 @@ func (walker *typeWriterWalker) writeTypeOrSymbol(node *ast.Node, isSymbolWalk b } declSourceFile := ast.GetSourceFileOfNode(declaration) - declLine, declChar := scanner.GetLineAndCharacterOfPosition(declSourceFile, declaration.Pos()) + declLine, declChar := scanner.GetECMALineAndCharacterOfPosition(declSourceFile, declaration.Pos()) fileName := tspath.GetBaseFileName(declSourceFile.FileName()) symbolString.WriteString("Decl(") symbolString.WriteString(fileName) diff --git a/internal/transformers/jsxtransforms/jsx.go b/internal/transformers/jsxtransforms/jsx.go index ff9ad4d36b..1b8be9984f 100644 --- a/internal/transformers/jsxtransforms/jsx.go +++ b/internal/transformers/jsxtransforms/jsx.go @@ -577,7 +577,7 @@ func (tx *JSXTransformer) visitJsxOpeningLikeElementOrFragmentJSX( args = append(args, tx.Factory().NewFalseExpression()) } // __source development flag - line, col := scanner.GetLineAndCharacterOfPosition(originalFile.AsSourceFile(), location.Pos()) + line, col := scanner.GetECMALineAndCharacterOfPosition(originalFile.AsSourceFile(), location.Pos()) args = append(args, tx.Factory().NewObjectLiteralExpression(tx.Factory().NewNodeList([]*ast.Node{ tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("fileName"), nil, nil, tx.getCurrentFileNameExpression()), tx.Factory().NewPropertyAssignment(nil, tx.Factory().NewIdentifier("lineNumber"), nil, nil, tx.Factory().NewNumericLiteral(strconv.FormatInt(int64(line+1), 10))), diff --git a/internal/transformers/utilities.go b/internal/transformers/utilities.go index 6c84728ce6..96bb72c525 100644 --- a/internal/transformers/utilities.go +++ b/internal/transformers/utilities.go @@ -259,8 +259,8 @@ func IsOriginalNodeSingleLine(emitContext *printer.EmitContext, node *ast.Node) if source == nil { return false } - startLine, _ := scanner.GetLineAndCharacterOfPosition(source, original.Loc.Pos()) - endLine, _ := scanner.GetLineAndCharacterOfPosition(source, original.Loc.End()) + startLine, _ := scanner.GetECMALineAndCharacterOfPosition(source, original.Loc.Pos()) + endLine, _ := scanner.GetECMALineAndCharacterOfPosition(source, original.Loc.End()) return startLine == endLine } From 8debdea45b65207cfbf50a1eef945152594c4c9e Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 1 Oct 2025 18:29:06 +0000 Subject: [PATCH 07/23] add type alias for line starts --- internal/core/core.go | 4 +++- internal/ls/linemap.go | 2 ++ internal/sourcemap/lineinfo.go | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/internal/core/core.go b/internal/core/core.go index bd7ee0b552..3bd84b8075 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -362,7 +362,9 @@ func Coalesce[T *U, U any](a T, b T) T { } } -func ComputeECMALineStarts(text string) []TextPos { +type ECMALineStarts []TextPos + +func ComputeECMALineStarts(text string) ECMALineStarts { result := make([]TextPos, 0, strings.Count(text, "\n")+1) return slices.AppendSeq(result, ComputeECMALineStartsSeq(text)) } diff --git a/internal/ls/linemap.go b/internal/ls/linemap.go index 1c2c05518e..a3634e1d44 100644 --- a/internal/ls/linemap.go +++ b/internal/ls/linemap.go @@ -9,6 +9,8 @@ import ( "github.com/microsoft/typescript-go/internal/core" ) +type LSPLineStarts []core.TextPos + type LSPLineMap struct { LineStarts []core.TextPos AsciiOnly bool // TODO(jakebailey): collect ascii-only info per line diff --git a/internal/sourcemap/lineinfo.go b/internal/sourcemap/lineinfo.go index 2b1a23d0db..c2f4aad60f 100644 --- a/internal/sourcemap/lineinfo.go +++ b/internal/sourcemap/lineinfo.go @@ -4,10 +4,10 @@ import "github.com/microsoft/typescript-go/internal/core" type ECMALineInfo struct { text string - lineStarts []core.TextPos + lineStarts core.ECMALineStarts } -func CreateECMALineInfo(text string, lineStarts []core.TextPos) *ECMALineInfo { +func CreateECMALineInfo(text string, lineStarts core.ECMALineStarts) *ECMALineInfo { return &ECMALineInfo{ text: text, lineStarts: lineStarts, From 75267d18d920cf95c7c2c6fd5245e24b86c5af2c Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 1 Oct 2025 18:49:52 +0000 Subject: [PATCH 08/23] remove fileExists from LS host --- internal/ls/host.go | 1 - internal/ls/source_map.go | 2 +- internal/project/snapshot.go | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/ls/host.go b/internal/ls/host.go index d9a1b0af47..dfeaa02a62 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -5,7 +5,6 @@ import "github.com/microsoft/typescript-go/internal/sourcemap" type Host interface { UseCaseSensitiveFileNames() bool ReadFile(path string) (contents string, ok bool) - FileExists(path string) bool Converters() *Converters GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo } diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index 022fa05353..62eafda161 100644 --- a/internal/ls/source_map.go +++ b/internal/ls/source_map.go @@ -54,7 +54,7 @@ func (l *LanguageService) tryGetSourcePosition( ) *sourcemap.DocumentPosition { newPos := l.tryGetSourcePositionWorker(fileName, position) if newPos != nil { - if !l.host.FileExists(newPos.FileName) { + if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist return nil } } diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 53b46d48e4..8d41e426c5 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -109,10 +109,6 @@ func (s *Snapshot) ReadFile(fileName string) (string, bool) { return handle.Content(), true } -func (s *Snapshot) FileExists(fileName string) bool { - return s.fs.fs.FileExists(fileName) -} - type APISnapshotRequest struct { OpenProjects *collections.Set[string] CloseProjects *collections.Set[tspath.Path] From c26faa93b8ff525f8ec7fdf27e0ff054128a4969 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 6 Oct 2025 21:53:41 +0000 Subject: [PATCH 09/23] source maps for disk files --- internal/ls/definition.go | 56 +++++- internal/ls/host.go | 6 +- internal/ls/languageservice.go | 40 ++--- internal/ls/source_map.go | 19 +-- internal/lsp/server.go | 36 +++- internal/project/ata/ata_test.go | 26 +-- internal/project/configfilechanges_test.go | 14 +- internal/project/dirty/syncmap.go | 44 +++++ internal/project/overlayfs.go | 14 +- internal/project/project_test.go | 22 +-- .../project/projectcollectionbuilder_test.go | 2 +- .../project/projectreferencesprogram_test.go | 2 +- internal/project/refcounting_test.go | 4 +- internal/project/session.go | 38 ++++- internal/project/session_test.go | 68 ++++---- internal/project/snapshot.go | 71 ++++++-- internal/project/snapshot_test.go | 4 +- internal/project/snapshotfs.go | 161 ++++++++++++++++-- internal/project/untitled_test.go | 4 +- internal/sourcemap/source_mapper.go | 33 +++- 20 files changed, 510 insertions(+), 154 deletions(-) diff --git a/internal/ls/definition.go b/internal/ls/definition.go index abefaf932e..072a2efb24 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -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()) + } + } + + 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))) @@ -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), + }, } } diff --git a/internal/ls/host.go b/internal/ls/host.go index dfeaa02a62..24c3ba5149 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -1,10 +1,8 @@ package ls -import "github.com/microsoft/typescript-go/internal/sourcemap" - type Host interface { - UseCaseSensitiveFileNames() bool + // UseCaseSensitiveFileNames() bool ReadFile(path string) (contents string, ok bool) Converters() *Converters - GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo + // GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index c7c4596dce..10720b0126 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,12 @@ 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) + // d, ok := l.documentPositionMappers[fileName] + // if !ok { + // d = sourcemap.GetDocumentPositionMapper(l, fileName) + // l.documentPositionMappers[fileName] = d + // } + // return d + // !!! + return nil } diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index 62eafda161..cc6b8c737f 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(), core.TextPos(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(), core.TextPos(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/server.go b/internal/lsp/server.go index 62d3baa28c..2a67f2ec08 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -449,7 +449,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) @@ -463,6 +462,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 }) @@ -519,7 +519,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 } @@ -536,6 +536,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() @@ -780,7 +810,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/dirty/syncmap.go b/internal/project/dirty/syncmap.go index 3a69d2deef..e0886d83c0 100644 --- a/internal/project/dirty/syncmap.go +++ b/internal/project/dirty/syncmap.go @@ -333,3 +333,47 @@ func (m *SyncMap[K, V]) Finalize() (map[K]V, bool) { }) return result, changed } + +type Change[V any] struct { + Old V + New V + Deleted bool +} + +func (m *SyncMap[K, V]) Finalize2() (map[K]V, bool, map[K]*Change[V]) { + var changed bool + result := m.base + ensureCloned := func() { + if !changed { + if m.base == nil { + result = make(map[K]V) + } else { + result = maps.Clone(m.base) + } + changed = true + } + } + + 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 { + 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, changes +} 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_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 0b6e537d54..c2423c6f42 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" @@ -144,7 +145,10 @@ func NewSession(init *SessionInit) *Session { init.Options, parseCache, extendedConfigCache, - &ConfigFileRegistry{}, + &ConfigFileRegistry{ + configs: map[tspath.Path]*configFileEntry{}, + configFileNames: map[tspath.Path]*configFileNames{}, + }, nil, toPath, ), @@ -333,7 +337,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 @@ -365,9 +369,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 { @@ -641,3 +645,29 @@ 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, addedFiles := snapshot.CloneWithSourceMaps(files, s) + // go s.updateSnapshotWithAddedFiles(addedFiles) + s.updateSnapshotWithAddedFiles(addedFiles) + return ls.NewLanguageService(project.GetProgram(), snapshotWithFiles), nil +} + +func (s *Session) updateSnapshotWithAddedFiles(addedFiles map[tspath.Path]*dirty.Change[*diskFile]) { + s.snapshotMu.Lock() + oldSnapshot := s.snapshot + newSnapshot := oldSnapshot.CloneWithChanges(addedFiles, 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. + + // !!! TODO: update file watchers with patch +} diff --git a/internal/project/session_test.go b/internal/project/session_test.go index 9da57316bd..494db3f6b2 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -51,7 +51,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) @@ -104,7 +104,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) @@ -119,7 +119,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() @@ -141,7 +141,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() @@ -156,7 +156,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") @@ -179,7 +179,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() @@ -196,7 +196,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) @@ -219,7 +219,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() @@ -242,7 +242,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) @@ -282,7 +282,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) @@ -304,7 +304,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) @@ -314,7 +314,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) @@ -338,7 +338,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) @@ -348,7 +348,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) @@ -443,11 +443,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() @@ -476,11 +476,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() @@ -502,7 +502,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() @@ -516,7 +516,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()) @@ -529,7 +529,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() @@ -543,7 +543,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) }) @@ -566,7 +566,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) @@ -586,7 +586,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) @@ -607,7 +607,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) @@ -622,7 +622,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) @@ -644,7 +644,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) @@ -659,7 +659,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) @@ -679,7 +679,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() @@ -698,7 +698,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) @@ -719,7 +719,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() @@ -738,7 +738,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) @@ -759,7 +759,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() @@ -778,7 +778,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..dc4b8d4c19 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" @@ -82,13 +84,6 @@ func (s *Snapshot) LSPLineMap(fileName string) *ls.LSPLineMap { return nil } -func (s *Snapshot) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { - if file := s.fs.GetFile(fileName); file != nil { - return file.ECMALineInfo() - } - return nil -} - func (s *Snapshot) Converters() *ls.Converters { return s.converters } @@ -97,10 +92,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 +100,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] @@ -298,6 +293,60 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma 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. +func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Snapshot, map[tspath.Path]*dirty.Change[*diskFile]) { + 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.Finalize2() + newId := session.snapshotID.Add(1) + newSnapshot := NewSnapshot( + newId, + snapshotFS, + s.sessionOptions, + session.parseCache, + session.extendedConfigCache, + s.ConfigFileRegistry, + s.compilerOptionsForInferredProjects, + s.toPath, + ) + var logger *logging.LogTree + if session.options.LoggingEnabled { + logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with source maps for %v", s.id, genFiles)) + } + newSnapshot.parentId = s.id + newSnapshot.ProjectCollection = s.ProjectCollection + newSnapshot.builderLogs = logger + return newSnapshot, changes +} + +func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskFile], session *Session) *Snapshot { + fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) + fs.applyDiskFileChanges(changes) + snapshotFS, _ := fs.Finalize() + newId := session.snapshotID.Add(1) + newSnapshot := NewSnapshot( + newId, + snapshotFS, + s.sessionOptions, + session.parseCache, + session.extendedConfigCache, + s.ConfigFileRegistry, + s.compilerOptionsForInferredProjects, + s.toPath, + ) + var logger *logging.LogTree + if session.options.LoggingEnabled { + logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with changes %v", s.id, slices.Collect(maps.Keys(changes)))) + } + newSnapshot.parentId = s.id + newSnapshot.ProjectCollection = s.ProjectCollection + newSnapshot.builderLogs = logger + return newSnapshot +} + func (s *Snapshot) Ref() { s.refCount.Add(1) } diff --git a/internal/project/snapshot_test.go b/internal/project/snapshot_test.go index a075039b33..8c02cc6897 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 1b45acf0a2..de8c82fad5 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() *diskFile - func (s *snapshotFS) FS() vfs.FS { return s.fs } @@ -43,14 +38,24 @@ func (s *snapshotFS) GetFile(fileName string) FileHandle { if file, ok := s.diskFiles[s.toPath(fileName)]; ok { return file } - newEntry := memoizedDiskFile(sync.OnceValue(func() *diskFile { - if contents, ok := s.fs.ReadFile(fileName); ok { - return newDiskFile(fileName, contents) + return nil +} + +func (s *snapshotFS) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { + var sourceMapInfo *sourceMapInfo + if file, ok := s.overlays[s.toPath(fileName)]; ok { + sourceMapInfo = file.sourceMapInfo + } + if file, ok := s.diskFiles[s.toPath(fileName)]; ok { + sourceMapInfo = file.sourceMapInfo + } + if sourceMapInfo != nil { + if sourceMapInfo.documentMapper != nil { + return sourceMapInfo.documentMapper.m + } + if sourceMapInfo.sourceMapPath != "" { + return s.GetDocumentPositionMapper(sourceMapInfo.sourceMapPath) } - return nil - })) - if entry, ok := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry); ok { - return entry() } return nil } @@ -93,6 +98,16 @@ func (s *snapshotFSBuilder) Finalize() (*snapshotFS, bool) { }, changed } +func (s *snapshotFSBuilder) Finalize2() (*snapshotFS, bool, map[tspath.Path]*dirty.Change[*diskFile]) { + diskFiles, changed, changes := s.diskFiles.Finalize2() + return &snapshotFS{ + fs: s.fs, + overlays: s.overlays, + diskFiles: diskFiles, + toPath: s.toPath, + }, changed, changes +} + func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { path := s.toPath(fileName) return s.GetFileByPath(fileName, path) @@ -111,6 +126,9 @@ func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) Fil file.content = content file.hash = xxh3.Hash128([]byte(content)) file.needsReload = false + // This should not ever be needed while updating a snapshot with source maps + file.sourceMapInfo = nil + file.lineInfo = nil }) } else { entry.Delete() @@ -142,3 +160,120 @@ func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { } } } + +// 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.GetFile(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.GetFile(fileName); file != nil { + return file.Content(), true + } + return "", false +} + +// !!! TODO: consolidate this and `GetFileByPath` +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) { + file.content = content + file.hash = xxh3.Hash128([]byte(content)) + file.needsReload = false + // This should not ever be needed while updating a snapshot with source maps + file.sourceMapInfo = nil + file.lineInfo = nil + }) + } else { + entry.Delete() + } + } + }) + } + if entry == nil || entry.Value() == nil { + return nil + } + return entry.Value() +} + +func (s *snapshotFSBuilder) computeDocumentPositionMapper(genFileName string) { + // !!! TODO: What if file is in overlay? + genFilePath := s.toPath(genFileName) + entry, _ := s.diskFiles.Load(genFilePath) + if entry == nil { + return + } + file := entry.Value() + if file == nil { + return + } + // Source map information already computed + if file.sourceMapInfo != nil { + return + } + // Compute source map information + 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(changes map[tspath.Path]*dirty.Change[*diskFile]) { + for path, change := range changes { + if change.Deleted { + if change.Old != nil { + panic("Deleting files not supported") + } + continue + } + entry, _ := s.diskFiles.Load(path) + if entry != nil { + entry.Change(func(file *diskFile) { + 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/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go index d89d542a56..9f6e85f5fd 100644 --- a/internal/sourcemap/source_mapper.go +++ b/internal/sourcemap/source_mapper.go @@ -232,7 +232,7 @@ func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPos if base64Object, matched := tryParseBase64Url(mapFileName); matched { if base64Object != "" { if decoded, err := base64.StdEncoding.DecodeString(base64Object); err == nil { - return convertDocumentToSourceMapper(host, string(decoded), generatedFileName) + return ConvertDocumentToSourceMapper(host, string(decoded), generatedFileName) } } // Not a data URL we can parse, skip it @@ -248,13 +248,40 @@ func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPos for _, location := range possibleMapLocations { mapFileName := tspath.GetNormalizedAbsolutePath(location, tspath.GetDirectoryPath(generatedFileName)) if mapFileContents, ok := host.ReadFile(mapFileName); ok { - return convertDocumentToSourceMapper(host, mapFileContents, mapFileName) + return ConvertDocumentToSourceMapper(host, mapFileContents, mapFileName) } } return nil } -func convertDocumentToSourceMapper(host Host, contents string, mapFileName 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 string(decoded), true + } + } + // Not a data URL we can parse, skip it + mapFileName = "" + } + } + var possibleMapLocations []string + if mapFileName != "" { + possibleMapLocations = append(possibleMapLocations, mapFileName) + } + possibleMapLocations = append(possibleMapLocations, generatedFileName+".map") + for _, location := range possibleMapLocations { + mapFileName := tspath.GetNormalizedAbsolutePath(location, tspath.GetDirectoryPath(generatedFileName)) + if _, ok := host.ReadFile(mapFileName); ok { + return mapFileName, false + } + } + return "", false +} + +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 From 459ebfe4d22dd0379477fbcd9ad562c34546ea35 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Mon, 6 Oct 2025 23:37:36 +0000 Subject: [PATCH 10/23] WIP: overlays --- internal/project/configfileregistrybuilder.go | 4 +- internal/project/overlayfs.go | 49 +++++++++++- internal/project/projectcollectionbuilder.go | 32 +++++--- internal/project/session.go | 13 ++-- internal/project/snapshot.go | 11 ++- internal/project/snapshotfs.go | 74 +++++++++++++++---- 6 files changed, 138 insertions(+), 45 deletions(-) diff --git a/internal/project/configfileregistrybuilder.go b/internal/project/configfileregistrybuilder.go index 725bc343a8..9b16b8f938 100644 --- a/internal/project/configfileregistrybuilder.go +++ b/internal/project/configfileregistrybuilder.go @@ -443,7 +443,7 @@ func (c *configFileRegistryBuilder) getConfigFileNameForFile(fileName string, pa configName := c.computeConfigFileName(fileName, false, logger) - if _, ok := c.fs.overlays[path]; ok { + if _, ok := c.fs.overlays.Load(path); ok { c.configFileNames.Add(path, &configFileNames{ nearestConfigFileName: configName, }) @@ -471,7 +471,7 @@ func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName string, p // Look for config in parent folders of config file result := c.computeConfigFileName(configFileName, true, logger) - if _, ok := c.fs.overlays[path]; ok { + if _, ok := c.fs.overlays.Load(path); ok { entry.Change(func(value *configFileNames) { if value.ancestors == nil { value.ancestors = make(map[string]string) diff --git a/internal/project/overlayfs.go b/internal/project/overlayfs.go index 043e7acdfa..dac6c422ac 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "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" @@ -38,6 +39,8 @@ type fileBase struct { lineMap *ls.LSPLineMap lineInfoOnce sync.Once lineInfo *sourcemap.ECMALineInfo + + sourceMapInfo *sourceMapInfo } func (f *fileBase) FileName() string { @@ -69,8 +72,7 @@ func (f *fileBase) ECMALineInfo() *sourcemap.ECMALineInfo { type diskFile struct { fileBase - needsReload bool - sourceMapInfo *sourceMapInfo + needsReload bool } type sourceMapInfo struct { @@ -176,6 +178,19 @@ func (o *overlay) Kind() core.ScriptKind { return o.kind } +func (o *overlay) Clone() *overlay { + return &overlay{ + fileBase: fileBase{ + fileName: o.fileName, + content: o.content, + hash: o.hash, + }, + version: o.version, + kind: o.kind, + matchesDiskText: o.matchesDiskText, + } +} + type overlayFS struct { toPath func(string) tspath.Path fs vfs.FS @@ -383,3 +398,33 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma fs.overlays = newOverlays return result, newOverlays } + +func (fs *overlayFS) processBuilderChanges(changes map[tspath.Path]*dirty.Change[*overlay]) map[tspath.Path]*overlay { + fs.mu.Lock() + defer fs.mu.Unlock() + newOverlays := maps.Clone(fs.overlays) + for path, change := range changes { + if change.Deleted { + if change.Old != nil { + panic("Deleting files not supported") + } + // Failed file read + continue + } + // New file + if change.Old == nil { + if _, ok := newOverlays[path]; !ok { + newOverlays[path] = change.New + } + continue + } + // Updated file + if overlay, ok := newOverlays[path]; ok { + if overlay.Hash() == change.Old.Hash() { + overlay.sourceMapInfo = change.New.sourceMapInfo + } + } + } + fs.overlays = newOverlays + return newOverlays +} diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index 7717016908..ad184d8aa1 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -151,11 +151,13 @@ func (b *projectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque } } - for _, overlay := range b.fs.overlays { - if entry := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); entry != nil { + b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { + fileName := overlay.Value().FileName() + if entry := b.findDefaultConfiguredProject(fileName, b.toPath(fileName)); entry != nil { delete(projectsToClose, entry.Value().configFilePath) } - } + return true + }) for projectPath := range projectsToClose { if entry, ok := b.configuredProjects.Load(projectPath); ok { @@ -239,13 +241,15 @@ func (b *projectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, log }) var inferredProjectFiles []string - for _, overlay := range b.fs.overlays { - if p := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); p != nil { + b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { + fileName := overlay.Value().FileName() + if p := b.findDefaultConfiguredProject(fileName, b.toPath(fileName)); p != nil { toRemoveProjects.Delete(p.Value().configFilePath) } else { - inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) + inferredProjectFiles = append(inferredProjectFiles, fileName) } - } + return true + }) for projectPath := range toRemoveProjects.Keys() { if openFileResult.retain.Has(projectPath) { @@ -298,11 +302,14 @@ func (b *projectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, logge // If the structure of other projects changed, we might need to move files // in/out of the inferred project. var inferredProjectFiles []string - for path, overlay := range b.fs.overlays { - if b.findDefaultConfiguredProject(overlay.FileName(), path) == nil { - inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) + b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { + path := overlay.Key() + fileName := overlay.Value().FileName() + if b.findDefaultConfiguredProject(fileName, path) == nil { + inferredProjectFiles = append(inferredProjectFiles, fileName) } - } + return true + }) if len(inferredProjectFiles) > 0 { b.updateInferredProjectRoots(inferredProjectFiles, logger) } @@ -395,7 +402,8 @@ func (b *projectCollectionBuilder) markProjectsAffectedByConfigChanges( // Recompute default projects for open files that now have different config file presence. var hasChanges bool for path := range configChangeResult.affectedFiles { - fileName := b.fs.overlays[path].FileName() + overlay, _ := b.fs.overlays.Load(path) + fileName := overlay.Value().FileName() _ = b.ensureConfiguredProjectAndAncestorsForOpenFile(fileName, path, logger) hasChanges = true } diff --git a/internal/project/session.go b/internal/project/session.go index c2423c6f42..c83ecdaa67 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -17,7 +17,6 @@ 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" @@ -653,21 +652,21 @@ func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsp // 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, addedFiles := snapshot.CloneWithSourceMaps(files, s) - // go s.updateSnapshotWithAddedFiles(addedFiles) - s.updateSnapshotWithAddedFiles(addedFiles) + snapshotWithFiles, changes := snapshot.CloneWithSourceMaps(files, s) + go s.updateSnapshotWithAddedFiles(changes) return ls.NewLanguageService(project.GetProgram(), snapshotWithFiles), nil } -func (s *Session) updateSnapshotWithAddedFiles(addedFiles map[tspath.Path]*dirty.Change[*diskFile]) { +func (s *Session) updateSnapshotWithAddedFiles(changes *builderFileChanges) { + overlays := s.fs.processBuilderChanges(changes.overlayChanges) s.snapshotMu.Lock() oldSnapshot := s.snapshot - newSnapshot := oldSnapshot.CloneWithChanges(addedFiles, s) + newSnapshot := oldSnapshot.CloneWithChanges(changes.diskChanges, overlays, 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. - // !!! TODO: update file watchers with patch + // !!! TODO: update file watchers (`updateWatches`) } diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index dc4b8d4c19..1976e13e37 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -3,8 +3,6 @@ package project import ( "context" "fmt" - "maps" - "slices" "sync/atomic" "time" @@ -295,7 +293,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma // 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. -func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Snapshot, map[tspath.Path]*dirty.Change[*diskFile]) { +func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Snapshot, *builderFileChanges) { fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) for _, genFile := range genFiles { fs.computeDocumentPositionMapper(genFile) @@ -322,8 +320,9 @@ func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Sn return newSnapshot, changes } -func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskFile], session *Session) *Snapshot { - fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) +func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskFile], overlays map[tspath.Path]*overlay, session *Session) *Snapshot { + // !!! Log time + fs := newSnapshotFSBuilder(s.fs.fs, overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) fs.applyDiskFileChanges(changes) snapshotFS, _ := fs.Finalize() newId := session.snapshotID.Add(1) @@ -339,7 +338,7 @@ func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskF ) var logger *logging.LogTree if session.options.LoggingEnabled { - logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with changes %v", s.id, slices.Collect(maps.Keys(changes)))) + logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with changes", s.id)) } newSnapshot.parentId = s.id newSnapshot.ProjectCollection = s.ProjectCollection diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index de8c82fad5..0a4b480622 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -62,7 +62,7 @@ func (s *snapshotFS) GetDocumentPositionMapper(fileName string) *sourcemap.Docum type snapshotFSBuilder struct { fs vfs.FS - overlays map[tspath.Path]*overlay + overlays *dirty.SyncMap[tspath.Path, *overlay] diskFiles *dirty.SyncMap[tspath.Path, *diskFile] toPath func(string) tspath.Path } @@ -78,7 +78,7 @@ func newSnapshotFSBuilder( cachedFS.Enable() return &snapshotFSBuilder{ fs: cachedFS, - overlays: overlays, + overlays: dirty.NewSyncMap(overlays, nil), diskFiles: dirty.NewSyncMap(diskFiles, nil), toPath: toPath, } @@ -89,23 +89,30 @@ func (s *snapshotFSBuilder) FS() vfs.FS { } func (s *snapshotFSBuilder) Finalize() (*snapshotFS, bool) { - diskFiles, changed := s.diskFiles.Finalize() + diskFiles, diskChanged := s.diskFiles.Finalize() + overlays, overlayChanged := s.overlays.Finalize() return &snapshotFS{ fs: s.fs, - overlays: s.overlays, + overlays: overlays, diskFiles: diskFiles, toPath: s.toPath, - }, changed + }, diskChanged || overlayChanged +} + +type builderFileChanges struct { + diskChanges map[tspath.Path]*dirty.Change[*diskFile] + overlayChanges map[tspath.Path]*dirty.Change[*overlay] } -func (s *snapshotFSBuilder) Finalize2() (*snapshotFS, bool, map[tspath.Path]*dirty.Change[*diskFile]) { - diskFiles, changed, changes := s.diskFiles.Finalize2() +func (s *snapshotFSBuilder) Finalize2() (*snapshotFS, bool, *builderFileChanges) { + diskFiles, diskChanged, diskChanges := s.diskFiles.Finalize2() + overlays, overlayChanged, overlayChanges := s.overlays.Finalize2() return &snapshotFS{ fs: s.fs, - overlays: s.overlays, + overlays: overlays, diskFiles: diskFiles, toPath: s.toPath, - }, changed, changes + }, diskChanged || overlayChanged, &builderFileChanges{diskChanges, overlayChanges} } func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { @@ -114,8 +121,8 @@ func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { } func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) FileHandle { - if file, ok := s.overlays[path]; ok { - return file + if entry, ok := s.overlays.Load(path); ok { + return entry.Value() } entry, _ := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true}) if entry != nil { @@ -210,10 +217,36 @@ func (s *snapshotFSBuilder) getDiskFileByPath(fileName string, path tspath.Path) } func (s *snapshotFSBuilder) computeDocumentPositionMapper(genFileName string) { - // !!! TODO: What if file is in overlay? genFilePath := s.toPath(genFileName) - entry, _ := s.diskFiles.Load(genFilePath) - if entry == nil { + if entry, ok := s.overlays.Load(genFilePath); ok { + file := entry.Value() + if file == nil { + return + } + // Source map information already computed + if file.sourceMapInfo != nil { + return + } + // Compute source map information + 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 *overlay) { + file.sourceMapInfo = &sourceMapInfo{documentMapper: &documentMapper{m: docMapper}} + }) + } else { + // Store path to map file + entry.Change(func(file *overlay) { + file.sourceMapInfo = &sourceMapInfo{sourceMapPath: url} + }) + } + if url != "" { + s.computeDocumentPositionMapperForMap(url) + } + } + entry, ok := s.diskFiles.Load(genFilePath) + if !ok { return } file := entry.Value() @@ -259,14 +292,23 @@ func (s *snapshotFSBuilder) computeDocumentPositionMapperForMap(mapFileName stri }) } -func (s *snapshotFSBuilder) applyDiskFileChanges(changes map[tspath.Path]*dirty.Change[*diskFile]) { - for path, change := range changes { +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) { From e2fb0189f46b1507e63d9d836714ccf91e8b5276 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 7 Oct 2025 17:45:33 +0000 Subject: [PATCH 11/23] fix bundled and update LS --- internal/bundled/embed.go | 5 +++++ internal/bundled/noembed.go | 4 ++++ internal/ls/converters.go | 4 ++++ internal/ls/host.go | 5 +++-- internal/ls/languageservice.go | 9 +-------- internal/ls/source_map.go | 4 ++-- internal/lsp/lsproto/lsp.go | 4 ++++ internal/project/projectcollectionbuilder.go | 14 +++++++------- 8 files changed, 30 insertions(+), 19 deletions(-) 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/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/host.go b/internal/ls/host.go index 24c3ba5149..0ecfd6d1ae 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -1,8 +1,9 @@ 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 10720b0126..9a82cf4e47 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -44,12 +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 - // !!! - return nil + return l.host.GetDocumentPositionMapper(fileName) } diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go index cc6b8c737f..4cc2ecbfa5 100644 --- a/internal/ls/source_map.go +++ b/internal/ls/source_map.go @@ -12,11 +12,11 @@ func (l *LanguageService) getMappedLocation(location *lsproto.Location) *lsproto 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(), core.TextPos(rangeStart)) + startPos := l.tryGetSourcePosition(location.Uri.FileName(), rangeStart) if startPos == nil { return location } - endPos := l.tryGetSourcePosition(location.Uri.FileName(), core.TextPos(rangeEnd)) + 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)) 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/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index ad184d8aa1..3f84e2518f 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -242,11 +242,11 @@ func (b *projectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, log var inferredProjectFiles []string b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { - fileName := overlay.Value().FileName() - if p := b.findDefaultConfiguredProject(fileName, b.toPath(fileName)); p != nil { + overlayName := overlay.Value().FileName() + if p := b.findDefaultConfiguredProject(overlayName, b.toPath(overlayName)); p != nil { toRemoveProjects.Delete(p.Value().configFilePath) } else { - inferredProjectFiles = append(inferredProjectFiles, fileName) + inferredProjectFiles = append(inferredProjectFiles, overlayName) } return true }) @@ -303,10 +303,10 @@ func (b *projectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, logge // in/out of the inferred project. var inferredProjectFiles []string b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { - path := overlay.Key() - fileName := overlay.Value().FileName() - if b.findDefaultConfiguredProject(fileName, path) == nil { - inferredProjectFiles = append(inferredProjectFiles, fileName) + overlayPath := overlay.Key() + overlayFileName := overlay.Value().FileName() + if b.findDefaultConfiguredProject(overlayFileName, overlayPath) == nil { + inferredProjectFiles = append(inferredProjectFiles, overlayFileName) } return true }) From 4eab0b78c3665072e7e36b680cb46e9edb8a8538 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 7 Oct 2025 18:06:15 +0000 Subject: [PATCH 12/23] Revert "WIP: overlays" This reverts commit 459ebfe4d22dd0379477fbcd9ad562c34546ea35. --- internal/project/configfileregistrybuilder.go | 4 +- internal/project/overlayfs.go | 49 +----------- internal/project/projectcollectionbuilder.go | 32 +++----- internal/project/session.go | 13 ++-- internal/project/snapshot.go | 11 +-- internal/project/snapshotfs.go | 74 ++++--------------- 6 files changed, 45 insertions(+), 138 deletions(-) diff --git a/internal/project/configfileregistrybuilder.go b/internal/project/configfileregistrybuilder.go index 9b16b8f938..725bc343a8 100644 --- a/internal/project/configfileregistrybuilder.go +++ b/internal/project/configfileregistrybuilder.go @@ -443,7 +443,7 @@ func (c *configFileRegistryBuilder) getConfigFileNameForFile(fileName string, pa configName := c.computeConfigFileName(fileName, false, logger) - if _, ok := c.fs.overlays.Load(path); ok { + if _, ok := c.fs.overlays[path]; ok { c.configFileNames.Add(path, &configFileNames{ nearestConfigFileName: configName, }) @@ -471,7 +471,7 @@ func (c *configFileRegistryBuilder) getAncestorConfigFileName(fileName string, p // Look for config in parent folders of config file result := c.computeConfigFileName(configFileName, true, logger) - if _, ok := c.fs.overlays.Load(path); ok { + if _, ok := c.fs.overlays[path]; ok { entry.Change(func(value *configFileNames) { if value.ancestors == nil { value.ancestors = make(map[string]string) diff --git a/internal/project/overlayfs.go b/internal/project/overlayfs.go index dac6c422ac..043e7acdfa 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -7,7 +7,6 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "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" @@ -39,8 +38,6 @@ type fileBase struct { lineMap *ls.LSPLineMap lineInfoOnce sync.Once lineInfo *sourcemap.ECMALineInfo - - sourceMapInfo *sourceMapInfo } func (f *fileBase) FileName() string { @@ -72,7 +69,8 @@ func (f *fileBase) ECMALineInfo() *sourcemap.ECMALineInfo { type diskFile struct { fileBase - needsReload bool + needsReload bool + sourceMapInfo *sourceMapInfo } type sourceMapInfo struct { @@ -178,19 +176,6 @@ func (o *overlay) Kind() core.ScriptKind { return o.kind } -func (o *overlay) Clone() *overlay { - return &overlay{ - fileBase: fileBase{ - fileName: o.fileName, - content: o.content, - hash: o.hash, - }, - version: o.version, - kind: o.kind, - matchesDiskText: o.matchesDiskText, - } -} - type overlayFS struct { toPath func(string) tspath.Path fs vfs.FS @@ -398,33 +383,3 @@ func (fs *overlayFS) processChanges(changes []FileChange) (FileChangeSummary, ma fs.overlays = newOverlays return result, newOverlays } - -func (fs *overlayFS) processBuilderChanges(changes map[tspath.Path]*dirty.Change[*overlay]) map[tspath.Path]*overlay { - fs.mu.Lock() - defer fs.mu.Unlock() - newOverlays := maps.Clone(fs.overlays) - for path, change := range changes { - if change.Deleted { - if change.Old != nil { - panic("Deleting files not supported") - } - // Failed file read - continue - } - // New file - if change.Old == nil { - if _, ok := newOverlays[path]; !ok { - newOverlays[path] = change.New - } - continue - } - // Updated file - if overlay, ok := newOverlays[path]; ok { - if overlay.Hash() == change.Old.Hash() { - overlay.sourceMapInfo = change.New.sourceMapInfo - } - } - } - fs.overlays = newOverlays - return newOverlays -} diff --git a/internal/project/projectcollectionbuilder.go b/internal/project/projectcollectionbuilder.go index 3f84e2518f..7717016908 100644 --- a/internal/project/projectcollectionbuilder.go +++ b/internal/project/projectcollectionbuilder.go @@ -151,13 +151,11 @@ func (b *projectCollectionBuilder) HandleAPIRequest(apiRequest *APISnapshotReque } } - b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { - fileName := overlay.Value().FileName() - if entry := b.findDefaultConfiguredProject(fileName, b.toPath(fileName)); entry != nil { + for _, overlay := range b.fs.overlays { + if entry := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); entry != nil { delete(projectsToClose, entry.Value().configFilePath) } - return true - }) + } for projectPath := range projectsToClose { if entry, ok := b.configuredProjects.Load(projectPath); ok { @@ -241,15 +239,13 @@ func (b *projectCollectionBuilder) DidChangeFiles(summary FileChangeSummary, log }) var inferredProjectFiles []string - b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { - overlayName := overlay.Value().FileName() - if p := b.findDefaultConfiguredProject(overlayName, b.toPath(overlayName)); p != nil { + for _, overlay := range b.fs.overlays { + if p := b.findDefaultConfiguredProject(overlay.FileName(), b.toPath(overlay.FileName())); p != nil { toRemoveProjects.Delete(p.Value().configFilePath) } else { - inferredProjectFiles = append(inferredProjectFiles, overlayName) + inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) } - return true - }) + } for projectPath := range toRemoveProjects.Keys() { if openFileResult.retain.Has(projectPath) { @@ -302,14 +298,11 @@ func (b *projectCollectionBuilder) DidRequestFile(uri lsproto.DocumentUri, logge // If the structure of other projects changed, we might need to move files // in/out of the inferred project. var inferredProjectFiles []string - b.fs.overlays.Range(func(overlay *dirty.SyncMapEntry[tspath.Path, *overlay]) bool { - overlayPath := overlay.Key() - overlayFileName := overlay.Value().FileName() - if b.findDefaultConfiguredProject(overlayFileName, overlayPath) == nil { - inferredProjectFiles = append(inferredProjectFiles, overlayFileName) + for path, overlay := range b.fs.overlays { + if b.findDefaultConfiguredProject(overlay.FileName(), path) == nil { + inferredProjectFiles = append(inferredProjectFiles, overlay.FileName()) } - return true - }) + } if len(inferredProjectFiles) > 0 { b.updateInferredProjectRoots(inferredProjectFiles, logger) } @@ -402,8 +395,7 @@ func (b *projectCollectionBuilder) markProjectsAffectedByConfigChanges( // Recompute default projects for open files that now have different config file presence. var hasChanges bool for path := range configChangeResult.affectedFiles { - overlay, _ := b.fs.overlays.Load(path) - fileName := overlay.Value().FileName() + fileName := b.fs.overlays[path].FileName() _ = b.ensureConfiguredProjectAndAncestorsForOpenFile(fileName, path, logger) hasChanges = true } diff --git a/internal/project/session.go b/internal/project/session.go index c83ecdaa67..c2423c6f42 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" @@ -652,21 +653,21 @@ func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsp // 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) - go s.updateSnapshotWithAddedFiles(changes) + snapshotWithFiles, addedFiles := snapshot.CloneWithSourceMaps(files, s) + // go s.updateSnapshotWithAddedFiles(addedFiles) + s.updateSnapshotWithAddedFiles(addedFiles) return ls.NewLanguageService(project.GetProgram(), snapshotWithFiles), nil } -func (s *Session) updateSnapshotWithAddedFiles(changes *builderFileChanges) { - overlays := s.fs.processBuilderChanges(changes.overlayChanges) +func (s *Session) updateSnapshotWithAddedFiles(addedFiles map[tspath.Path]*dirty.Change[*diskFile]) { s.snapshotMu.Lock() oldSnapshot := s.snapshot - newSnapshot := oldSnapshot.CloneWithChanges(changes.diskChanges, overlays, s) + newSnapshot := oldSnapshot.CloneWithChanges(addedFiles, 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. - // !!! TODO: update file watchers (`updateWatches`) + // !!! TODO: update file watchers with patch } diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 1976e13e37..dc4b8d4c19 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" @@ -293,7 +295,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma // 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. -func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Snapshot, *builderFileChanges) { +func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Snapshot, map[tspath.Path]*dirty.Change[*diskFile]) { fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) for _, genFile := range genFiles { fs.computeDocumentPositionMapper(genFile) @@ -320,9 +322,8 @@ func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Sn return newSnapshot, changes } -func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskFile], overlays map[tspath.Path]*overlay, session *Session) *Snapshot { - // !!! Log time - fs := newSnapshotFSBuilder(s.fs.fs, overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) +func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskFile], session *Session) *Snapshot { + fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) fs.applyDiskFileChanges(changes) snapshotFS, _ := fs.Finalize() newId := session.snapshotID.Add(1) @@ -338,7 +339,7 @@ func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskF ) var logger *logging.LogTree if session.options.LoggingEnabled { - logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with changes", s.id)) + logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with changes %v", s.id, slices.Collect(maps.Keys(changes)))) } newSnapshot.parentId = s.id newSnapshot.ProjectCollection = s.ProjectCollection diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index 0a4b480622..de8c82fad5 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -62,7 +62,7 @@ func (s *snapshotFS) GetDocumentPositionMapper(fileName string) *sourcemap.Docum type snapshotFSBuilder struct { fs vfs.FS - overlays *dirty.SyncMap[tspath.Path, *overlay] + overlays map[tspath.Path]*overlay diskFiles *dirty.SyncMap[tspath.Path, *diskFile] toPath func(string) tspath.Path } @@ -78,7 +78,7 @@ func newSnapshotFSBuilder( cachedFS.Enable() return &snapshotFSBuilder{ fs: cachedFS, - overlays: dirty.NewSyncMap(overlays, nil), + overlays: overlays, diskFiles: dirty.NewSyncMap(diskFiles, nil), toPath: toPath, } @@ -89,30 +89,23 @@ func (s *snapshotFSBuilder) FS() vfs.FS { } func (s *snapshotFSBuilder) Finalize() (*snapshotFS, bool) { - diskFiles, diskChanged := s.diskFiles.Finalize() - overlays, overlayChanged := s.overlays.Finalize() + diskFiles, changed := s.diskFiles.Finalize() return &snapshotFS{ fs: s.fs, - overlays: overlays, + overlays: s.overlays, diskFiles: diskFiles, toPath: s.toPath, - }, diskChanged || overlayChanged -} - -type builderFileChanges struct { - diskChanges map[tspath.Path]*dirty.Change[*diskFile] - overlayChanges map[tspath.Path]*dirty.Change[*overlay] + }, changed } -func (s *snapshotFSBuilder) Finalize2() (*snapshotFS, bool, *builderFileChanges) { - diskFiles, diskChanged, diskChanges := s.diskFiles.Finalize2() - overlays, overlayChanged, overlayChanges := s.overlays.Finalize2() +func (s *snapshotFSBuilder) Finalize2() (*snapshotFS, bool, map[tspath.Path]*dirty.Change[*diskFile]) { + diskFiles, changed, changes := s.diskFiles.Finalize2() return &snapshotFS{ fs: s.fs, - overlays: overlays, + overlays: s.overlays, diskFiles: diskFiles, toPath: s.toPath, - }, diskChanged || overlayChanged, &builderFileChanges{diskChanges, overlayChanges} + }, changed, changes } func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { @@ -121,8 +114,8 @@ func (s *snapshotFSBuilder) GetFile(fileName string) FileHandle { } func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) FileHandle { - if entry, ok := s.overlays.Load(path); ok { - return entry.Value() + if file, ok := s.overlays[path]; ok { + return file } entry, _ := s.diskFiles.LoadOrStore(path, &diskFile{fileBase: fileBase{fileName: fileName}, needsReload: true}) if entry != nil { @@ -217,36 +210,10 @@ func (s *snapshotFSBuilder) getDiskFileByPath(fileName string, path tspath.Path) } func (s *snapshotFSBuilder) computeDocumentPositionMapper(genFileName string) { + // !!! TODO: What if file is in overlay? genFilePath := s.toPath(genFileName) - if entry, ok := s.overlays.Load(genFilePath); ok { - file := entry.Value() - if file == nil { - return - } - // Source map information already computed - if file.sourceMapInfo != nil { - return - } - // Compute source map information - 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 *overlay) { - file.sourceMapInfo = &sourceMapInfo{documentMapper: &documentMapper{m: docMapper}} - }) - } else { - // Store path to map file - entry.Change(func(file *overlay) { - file.sourceMapInfo = &sourceMapInfo{sourceMapPath: url} - }) - } - if url != "" { - s.computeDocumentPositionMapperForMap(url) - } - } - entry, ok := s.diskFiles.Load(genFilePath) - if !ok { + entry, _ := s.diskFiles.Load(genFilePath) + if entry == nil { return } file := entry.Value() @@ -292,23 +259,14 @@ func (s *snapshotFSBuilder) computeDocumentPositionMapperForMap(mapFileName stri }) } -func (s *snapshotFSBuilder) applyDiskFileChanges( - diskChanges map[tspath.Path]*dirty.Change[*diskFile], -) { - for path, change := range diskChanges { +func (s *snapshotFSBuilder) applyDiskFileChanges(changes map[tspath.Path]*dirty.Change[*diskFile]) { + for path, change := range changes { 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) { From 166b687abe4935d9713c919cefd59c5ef455302b Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 7 Oct 2025 19:04:23 +0000 Subject: [PATCH 13/23] refactors/fixes --- internal/project/session.go | 2 +- internal/project/snapshot.go | 3 +- internal/project/snapshotfs.go | 78 ++++++++++++++-------------------- 3 files changed, 36 insertions(+), 47 deletions(-) diff --git a/internal/project/session.go b/internal/project/session.go index c2423c6f42..fd842b573f 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -662,7 +662,7 @@ func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsp func (s *Session) updateSnapshotWithAddedFiles(addedFiles map[tspath.Path]*dirty.Change[*diskFile]) { s.snapshotMu.Lock() oldSnapshot := s.snapshot - newSnapshot := oldSnapshot.CloneWithChanges(addedFiles, s) + newSnapshot := oldSnapshot.CloneWithDiskChanges(addedFiles, s) s.snapshot = newSnapshot s.snapshotMu.Unlock() diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index dc4b8d4c19..7cfa9758f6 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -295,6 +295,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma // 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]) { fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) for _, genFile := range genFiles { @@ -322,7 +323,7 @@ func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Sn return newSnapshot, changes } -func (s *Snapshot) CloneWithChanges(changes map[tspath.Path]*dirty.Change[*diskFile], session *Session) *Snapshot { +func (s *Snapshot) CloneWithDiskChanges(changes map[tspath.Path]*dirty.Change[*diskFile], session *Session) *Snapshot { fs := newSnapshotFSBuilder(s.fs.fs, s.fs.overlays, s.fs.diskFiles, s.sessionOptions.PositionEncoding, s.toPath) fs.applyDiskFileChanges(changes) snapshotFS, _ := fs.Finalize() diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index de8c82fad5..aa1555128c 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -42,19 +42,14 @@ func (s *snapshotFS) GetFile(fileName string) FileHandle { } func (s *snapshotFS) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { - var sourceMapInfo *sourceMapInfo - if file, ok := s.overlays[s.toPath(fileName)]; ok { - sourceMapInfo = file.sourceMapInfo - } if file, ok := s.diskFiles[s.toPath(fileName)]; ok { - sourceMapInfo = file.sourceMapInfo - } - if sourceMapInfo != nil { - if sourceMapInfo.documentMapper != nil { - return sourceMapInfo.documentMapper.m - } - if sourceMapInfo.sourceMapPath != "" { - return s.GetDocumentPositionMapper(sourceMapInfo.sourceMapPath) + 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 @@ -117,29 +112,7 @@ func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) Fil if file, ok := s.overlays[path]; ok { return file } - 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) { - file.content = content - file.hash = xxh3.Hash128([]byte(content)) - file.needsReload = false - // This should not ever be needed while updating a snapshot with source maps - file.sourceMapInfo = nil - file.lineInfo = nil - }) - } else { - entry.Delete() - } - } - }) - } - if entry == nil || entry.Value() == nil { - return nil - } - return entry.Value() + return s.getDiskFileByPath(fileName, path) } func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { @@ -168,7 +141,7 @@ func (s *snapshotFSBuilder) UseCaseSensitiveFileNames() bool { // GetECMALineInfo implements sourcemap.Host. func (s *snapshotFSBuilder) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { - if file := s.GetFile(fileName); file != nil { + if file := s.getDiskFile(fileName); file != nil { return file.ECMALineInfo() } return nil @@ -176,13 +149,16 @@ func (s *snapshotFSBuilder) GetECMALineInfo(fileName string) *sourcemap.ECMALine // ReadFile implements sourcemap.Host. func (s *snapshotFSBuilder) ReadFile(fileName string) (contents string, ok bool) { - if file := s.GetFile(fileName); file != nil { + if file := s.getDiskFile(fileName); file != nil { return file.Content(), true } return "", false } -// !!! TODO: consolidate this and `GetFileByPath` +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 { @@ -190,12 +166,12 @@ func (s *snapshotFSBuilder) getDiskFileByPath(fileName string, path tspath.Path) 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 - // This should not ever be needed while updating a snapshot with source maps - file.sourceMapInfo = nil - file.lineInfo = nil }) } else { entry.Delete() @@ -210,8 +186,12 @@ func (s *snapshotFSBuilder) getDiskFileByPath(fileName string, path tspath.Path) } func (s *snapshotFSBuilder) computeDocumentPositionMapper(genFileName string) { - // !!! TODO: What if file is in overlay? 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 @@ -224,7 +204,6 @@ func (s *snapshotFSBuilder) computeDocumentPositionMapper(genFileName string) { if file.sourceMapInfo != nil { return } - // Compute source map information url, isInline := sourcemap.GetSourceMapURL(s, genFileName) if isInline { // Store document mapper directly in disk file for an inline source map @@ -259,14 +238,23 @@ func (s *snapshotFSBuilder) computeDocumentPositionMapperForMap(mapFileName stri }) } -func (s *snapshotFSBuilder) applyDiskFileChanges(changes map[tspath.Path]*dirty.Change[*diskFile]) { - for path, change := range changes { +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) { From 78e4e239a46a251f99f66977afc46aa5033f73b1 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 7 Oct 2025 19:41:47 +0000 Subject: [PATCH 14/23] fix merge --- internal/ls/definition.go | 13 +++++---- internal/project/session_test.go | 4 +-- internal/project/snapshot.go | 10 ------- internal/project/snapshotfs.go | 5 +++- internal/sourcemap/source_mapper.go | 28 ------------------- .../harnessutil/sourcemap_recorder.go | 8 +++--- 6 files changed, 18 insertions(+), 50 deletions(-) diff --git a/internal/ls/definition.go b/internal/ls/definition.go index e8ed75dfa4..072a2efb24 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -147,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), + }, } } diff --git a/internal/project/session_test.go b/internal/project/session_test.go index ab9fc4709c..ae6519ea8b 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -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) }) diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 279ab76f6c..384be48100 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -99,13 +99,6 @@ func (s *Snapshot) ID() uint64 { return s.id } -<<<<<<< HEAD -======= -func (s *Snapshot) UseCaseSensitiveFileNames() bool { - return s.fs.fs.UseCaseSensitiveFileNames() -} - ->>>>>>> main func (s *Snapshot) ReadFile(fileName string) (string, bool) { handle := s.GetFile(fileName) if handle == nil { @@ -114,13 +107,10 @@ func (s *Snapshot) ReadFile(fileName string) (string, bool) { return handle.Content(), true } -<<<<<<< HEAD func (s *Snapshot) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { return s.fs.GetDocumentPositionMapper(fileName) } -======= ->>>>>>> main type APISnapshotRequest struct { OpenProjects *collections.Set[string] CloseProjects *collections.Set[tspath.Path] diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index c5e61e559f..153ab070d3 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -125,7 +125,10 @@ func (s *snapshotFSBuilder) GetFileByPath(fileName string, path tspath.Path) Fil if file, ok := s.overlays[path]; ok { return file } - return s.getDiskFileByPath(fileName, path) + if file := s.getDiskFileByPath(fileName, path); file != nil { + return file + } + return nil } func (s *snapshotFSBuilder) markDirtyFiles(change FileChangeSummary) { diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go index 9f6e85f5fd..4afe794291 100644 --- a/internal/sourcemap/source_mapper.go +++ b/internal/sourcemap/source_mapper.go @@ -226,34 +226,6 @@ func (d *DocumentPositionMapper) GetGeneratedPosition(loc *DocumentPosition) *Do } } -func GetDocumentPositionMapper(host Host, generatedFileName string) *DocumentPositionMapper { - 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) - } - } - // Not a data URL we can parse, skip it - mapFileName = "" - } - } - - var possibleMapLocations []string - if mapFileName != "" { - possibleMapLocations = append(possibleMapLocations, mapFileName) - } - 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) - } - } - return nil -} - func GetSourceMapURL(host Host, generatedFileName string) (url string, isInline bool) { mapFileName := tryGetSourceMappingURL(host, generatedFileName) if mapFileName != "" { diff --git a/internal/testutil/harnessutil/sourcemap_recorder.go b/internal/testutil/harnessutil/sourcemap_recorder.go index b601ec027c..18e869f8d1 100644 --- a/internal/testutil/harnessutil/sourcemap_recorder.go +++ b/internal/testutil/harnessutil/sourcemap_recorder.go @@ -103,10 +103,10 @@ func newSourceMapSpanWriter(sourceMapRecorder *writerAggregator, sourceMap *sour } sourceMapRecorder.WriteLine("===================================================================") - sourceMapRecorder.WriteLineF("JsFile: %s", sourceMap.File) - sourceMapRecorder.WriteLineF("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.CreateECMALineInfo(jsFile.Content, writer.jsLineMap))) - sourceMapRecorder.WriteLineF("sourceRoot: %s", sourceMap.SourceRoot) - sourceMapRecorder.WriteLineF("sources: %s", strings.Join(sourceMap.Sources, ",")) + sourceMapRecorder.WriteLinef("JsFile: %s", sourceMap.File) + sourceMapRecorder.WriteLinef("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.CreateECMALineInfo(jsFile.Content, writer.jsLineMap))) + sourceMapRecorder.WriteLinef("sourceRoot: %s", sourceMap.SourceRoot) + sourceMapRecorder.WriteLinef("sources: %s", strings.Join(sourceMap.Sources, ",")) if len(sourceMap.SourcesContent) > 0 { content, err := json.Marshal(sourceMap.SourcesContent) if err != nil { From bb1f175e0830cf336277f7e7eb5c3adca7091100 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 7 Oct 2025 19:45:52 +0000 Subject: [PATCH 15/23] fix submodule --- _submodules/TypeScript | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_submodules/TypeScript b/_submodules/TypeScript index add6971195..bc7d42611e 160000 --- a/_submodules/TypeScript +++ b/_submodules/TypeScript @@ -1 +1 @@ -Subproject commit add697119549734b24d46b30b9f6d2e757c6d53a +Subproject commit bc7d42611e35678c7cbddb104aa4b117a95ccdfa From 1a4114a85ccf4d64aa14031554198c0c5e2a8a3e Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 8 Oct 2025 21:23:47 +0000 Subject: [PATCH 16/23] file watching --- internal/project/session.go | 17 ++- internal/project/snapshot.go | 170 +++++++++++++++++++++++++++- internal/project/watch.go | 2 +- internal/sourcemap/source_mapper.go | 4 + 4 files changed, 182 insertions(+), 11 deletions(-) diff --git a/internal/project/session.go b/internal/project/session.go index 86ddd4959d..cbefa72994 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -700,21 +700,26 @@ func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsp // 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, addedFiles := snapshot.CloneWithSourceMaps(files, s) - // go s.updateSnapshotWithAddedFiles(addedFiles) - s.updateSnapshotWithAddedFiles(addedFiles) + snapshotWithFiles, changes := snapshot.CloneWithSourceMaps(files, s) + s.backgroundQueue.Enqueue(ctx, func(ctx context.Context) { + s.updateSnapshotWithDiskChanges(changes) + }) return ls.NewLanguageService(project.GetProgram(), snapshotWithFiles), nil } -func (s *Session) updateSnapshotWithAddedFiles(addedFiles map[tspath.Path]*dirty.Change[*diskFile]) { +func (s *Session) updateSnapshotWithDiskChanges(changes map[tspath.Path]*dirty.Change[*diskFile]) { s.snapshotMu.Lock() oldSnapshot := s.snapshot - newSnapshot := oldSnapshot.CloneWithDiskChanges(addedFiles, s) + 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. - // !!! TODO: update file watchers with patch + 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/snapshot.go b/internal/project/snapshot.go index 384be48100..89234f483c 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -35,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 @@ -64,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 } @@ -231,12 +240,37 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma if changedFiles { 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() removedFiles++ return true @@ -296,6 +330,21 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } } + newSnapshot.extraDiskFiles = maps.Clone(s.extraDiskFiles) + core.DiffMapsFunc( + s.fs.diskFiles, + newSnapshot.fs.diskFiles, + func(a, b *diskFile) bool { + return a.Hash() == b.Hash() + }, + func(path tspath.Path, addedFile *diskFile) {}, + func(path tspath.Path, removedFile *diskFile) { + delete(newSnapshot.extraDiskFiles, path) + }, + func(path tspath.Path, oldFile, newFile *diskFile) {}, + ) + newSnapshot.extraDiskFilesWatch = s.extraDiskFilesWatch.Clone(newSnapshot.extraDiskFiles) + logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start)) return newSnapshot } @@ -327,10 +376,18 @@ func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Sn newSnapshot.parentId = s.id newSnapshot.ProjectCollection = s.ProjectCollection newSnapshot.builderLogs = logger + // We don't need to update the extra files watcher here because the resulting snapshot will be + // discarded after fulfilling the request. 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 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() @@ -345,16 +402,121 @@ func (s *Snapshot) CloneWithDiskChanges(changes map[tspath.Path]*dirty.Change[*d s.compilerOptionsForInferredProjects, s.toPath, ) - var logger *logging.LogTree - if session.options.LoggingEnabled { - logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with changes %v", s.id, slices.Collect(maps.Keys(changes)))) - } + newSnapshot.parentId = s.id newSnapshot.ProjectCollection = s.ProjectCollection newSnapshot.builderLogs = logger + newSnapshot.extraDiskFiles = maps.Clone(s.extraDiskFiles) + core.DiffMapsFunc( + s.fs.diskFiles, + newSnapshot.fs.diskFiles, + func(a, b *diskFile) bool { + return a.Hash() == b.Hash() + }, + func(path tspath.Path, addedFile *diskFile) { + newSnapshot.extraDiskFiles[path] = addedFile.FileName() + }, + func(path tspath.Path, removedFile *diskFile) { + delete(newSnapshot.extraDiskFiles, path) + }, + func(path tspath.Path, oldFile, newFile *diskFile) { + // Shouldn't happen + }, + ) + newSnapshot.extraDiskFilesWatch = s.extraDiskFilesWatch.Clone(newSnapshot.extraDiskFiles) + logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start)) 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) computeGlobPatterns(files map[tspath.Path]string) patternsAndIgnored { + workspaceDirectory := s.sessionOptions.CurrentDirectory + comparePathsOptions := tspath.ComparePathsOptions{ + CurrentDirectory: workspaceDirectory, + UseCaseSensitiveFileNames: s.fs.fs.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/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 4afe794291..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 From dd76d5530d52d9a3f6759966cf7bdb47549e9bec Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 8 Oct 2025 22:38:15 +0000 Subject: [PATCH 17/23] ignore file watching messages on fourslash --- internal/fourslash/fourslash.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index a488f7d2c5..d30f2c53f1 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) { From 5d3772020f1460939ba874874fe4757d1c495caf Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 8 Oct 2025 22:52:15 +0000 Subject: [PATCH 18/23] remove ability for snapshot to read files --- internal/project/snapshotfs.go | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index 153ab070d3..2b5fb96c3f 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -1,9 +1,6 @@ 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" @@ -28,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 } @@ -44,14 +38,7 @@ 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 - })) - entry, _ := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry) - return entry() + return nil } func (s *snapshotFS) GetDocumentPositionMapper(fileName string) *sourcemap.DocumentPositionMapper { From a66fe91337990496f206d5ad8d3f421148172341 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Wed, 8 Oct 2025 23:14:48 +0000 Subject: [PATCH 19/23] add test with declaration map changes --- internal/fourslash/fourslash.go | 5 ++ ...eclarationMapGoToDefinitionChanges_test.go | 66 +++++++++++++++++++ ...ionMapGoToDefinitionChanges.baseline.jsonc | 34 ++++++++++ 3 files changed, 105 insertions(+) create mode 100644 internal/fourslash/tests/declarationMapGoToDefinitionChanges_test.go create mode 100644 testdata/baselines/reference/fourslash/goToDefinition/declarationMapGoToDefinitionChanges.baseline.jsonc diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index d30f2c53f1..9ed236d8d2 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -1204,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/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 From a68e5e173432dec4ea728cd5c642829c79ca8373 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 10 Oct 2025 19:03:40 +0000 Subject: [PATCH 20/23] cleanup, logging --- internal/project/configfileregistrybuilder.go | 2 +- internal/project/dirty/syncmap.go | 45 +++---------------- internal/project/projectcollectionbuilder.go | 2 +- internal/project/session.go | 8 ++++ internal/project/snapshot.go | 20 +++++---- internal/project/snapshotfs.go | 14 +----- 6 files changed, 31 insertions(+), 60 deletions(-) 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 e0886d83c0..d6dfacbb65 100644 --- a/internal/project/dirty/syncmap.go +++ b/internal/project/dirty/syncmap.go @@ -303,44 +303,7 @@ func (m *SyncMap[K, V]) Range(fn func(*SyncMapEntry[K, V]) bool) { } } -func (m *SyncMap[K, V]) Finalize() (map[K]V, bool) { - var changed bool - result := m.base - ensureCloned := func() { - if !changed { - if m.base == nil { - result = make(map[K]V) - } else { - result = maps.Clone(m.base) - } - changed = true - } - } - - m.dirty.Range(func(key K, entry *SyncMapEntry[K, V]) bool { - if entry.delete { - ensureCloned() - delete(result, key) - } else if entry.dirty { - ensureCloned() - if m.finalizeValue != nil { - result[key] = m.finalizeValue(entry.value, entry.original) - } else { - result[key] = entry.value - } - } - return true - }) - return result, changed -} - -type Change[V any] struct { - Old V - New V - Deleted bool -} - -func (m *SyncMap[K, V]) Finalize2() (map[K]V, bool, map[K]*Change[V]) { +func (m *SyncMap[K, V]) Finalize() (map[K]V, bool, map[K]*Change[V]) { var changed bool result := m.base ensureCloned := func() { @@ -377,3 +340,9 @@ func (m *SyncMap[K, V]) Finalize2() (map[K]V, bool, map[K]*Change[V]) { }) return result, changed, changes } + +type Change[V any] struct { + Old V + New V + Deleted bool +} 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/session.go b/internal/project/session.go index cbefa72994..99ae744f41 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -701,6 +701,10 @@ func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsp 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) }) @@ -716,6 +720,10 @@ func (s *Session) updateSnapshotWithDiskChanges(changes map[tspath.Path]*dirty.C // 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) diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 89234f483c..ce0dad00f4 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -281,7 +281,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } } - snapshotFS, _ := fs.Finalize() + snapshotFS, _, _ := fs.Finalize() newSnapshot := NewSnapshot( newSnapshotID, snapshotFS, @@ -353,11 +353,18 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma // 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.Finalize2() + snapshotFS, _, changes := fs.Finalize() + logger.Logf(`New files due to source maps: %v`, slices.Collect(maps.Keys(changes))) newId := session.snapshotID.Add(1) newSnapshot := NewSnapshot( newId, @@ -369,28 +376,25 @@ func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Sn s.compilerOptionsForInferredProjects, s.toPath, ) - var logger *logging.LogTree - if session.options.LoggingEnabled { - logger = logging.NewLogTree(fmt.Sprintf("Cloning snapshot %d with source maps for %v", s.id, genFiles)) - } newSnapshot.parentId = s.id newSnapshot.ProjectCollection = s.ProjectCollection newSnapshot.builderLogs = logger // We don't need to update the extra files watcher here because the resulting snapshot will be // discarded after fulfilling the request. + 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 changes %v", s.id, slices.Collect(maps.Keys(changes)))) + 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() + snapshotFS, _, _ := fs.Finalize() newId := session.snapshotID.Add(1) newSnapshot := NewSnapshot( newId, diff --git a/internal/project/snapshotfs.go b/internal/project/snapshotfs.go index 2b5fb96c3f..38e96adf3c 100644 --- a/internal/project/snapshotfs.go +++ b/internal/project/snapshotfs.go @@ -83,18 +83,8 @@ func (s *snapshotFSBuilder) FS() vfs.FS { return s.fs } -func (s *snapshotFSBuilder) Finalize() (*snapshotFS, bool) { - diskFiles, changed := s.diskFiles.Finalize() - return &snapshotFS{ - fs: s.fs, - overlays: s.overlays, - diskFiles: diskFiles, - toPath: s.toPath, - }, changed -} - -func (s *snapshotFSBuilder) Finalize2() (*snapshotFS, bool, map[tspath.Path]*dirty.Change[*diskFile]) { - diskFiles, changed, changes := s.diskFiles.Finalize2() +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, From 32f5cb5e05bfa1b1dd43b15a880288577aa228c6 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 10 Oct 2025 19:56:45 +0000 Subject: [PATCH 21/23] remove unused function --- internal/project/snapshot.go | 44 ------------------------------------ 1 file changed, 44 deletions(-) diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index ce0dad00f4..a3f1632d2b 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -477,50 +477,6 @@ func getComputeGlobPatterns(workspaceDirectory string, useCaseSensitiveFileNames } } -func (s *Snapshot) computeGlobPatterns(files map[tspath.Path]string) patternsAndIgnored { - workspaceDirectory := s.sessionOptions.CurrentDirectory - comparePathsOptions := tspath.ComparePathsOptions{ - CurrentDirectory: workspaceDirectory, - UseCaseSensitiveFileNames: s.fs.fs.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) } From c07dad5bed033697ba7880ebd3423779d80dca4a Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 14 Oct 2025 17:33:59 +0000 Subject: [PATCH 22/23] code review --- internal/ls/definition.go | 16 +++++++++--- internal/project/session.go | 3 +++ internal/project/snapshot.go | 49 ++++++++++++++---------------------- 3 files changed, 35 insertions(+), 33 deletions(-) diff --git a/internal/ls/definition.go b/internal/ls/definition.go index 072a2efb24..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) { @@ -61,18 +62,27 @@ func (l *LanguageService) GetFilesToMapFromDefinition(response lsproto.Definitio var files []string if response.Location != nil { - files = append(files, response.Location.Uri.FileName()) + fileName := response.Location.Uri.FileName() + if tspath.IsDeclarationFileName(fileName) { + files = append(files, fileName) + } } if response.Locations != nil { for _, location := range *response.Locations { - files = core.AppendIfUnique(files, location.Uri.FileName()) + fileName := location.Uri.FileName() + if tspath.IsDeclarationFileName(fileName) { + files = core.AppendIfUnique(files, fileName) + } } } if response.DefinitionLinks != nil { for _, link := range *response.DefinitionLinks { - files = core.AppendIfUnique(files, link.TargetUri.FileName()) + fileName := link.TargetUri.FileName() + if tspath.IsDeclarationFileName(fileName) { + files = core.AppendIfUnique(files, fileName) + } } } diff --git a/internal/project/session.go b/internal/project/session.go index 99ae744f41..67cc87528f 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -422,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 { diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index a3f1632d2b..553bfa2abf 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -226,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 { @@ -238,6 +240,7 @@ 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. @@ -272,12 +275,14 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma 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) } } @@ -330,20 +335,8 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma } } - newSnapshot.extraDiskFiles = maps.Clone(s.extraDiskFiles) - core.DiffMapsFunc( - s.fs.diskFiles, - newSnapshot.fs.diskFiles, - func(a, b *diskFile) bool { - return a.Hash() == b.Hash() - }, - func(path tspath.Path, addedFile *diskFile) {}, - func(path tspath.Path, removedFile *diskFile) { - delete(newSnapshot.extraDiskFiles, path) - }, - func(path tspath.Path, oldFile, newFile *diskFile) {}, - ) - newSnapshot.extraDiskFilesWatch = s.extraDiskFilesWatch.Clone(newSnapshot.extraDiskFiles) + 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 @@ -411,22 +404,18 @@ func (s *Snapshot) CloneWithDiskChanges(changes map[tspath.Path]*dirty.Change[*d newSnapshot.ProjectCollection = s.ProjectCollection newSnapshot.builderLogs = logger newSnapshot.extraDiskFiles = maps.Clone(s.extraDiskFiles) - core.DiffMapsFunc( - s.fs.diskFiles, - newSnapshot.fs.diskFiles, - func(a, b *diskFile) bool { - return a.Hash() == b.Hash() - }, - func(path tspath.Path, addedFile *diskFile) { - newSnapshot.extraDiskFiles[path] = addedFile.FileName() - }, - func(path tspath.Path, removedFile *diskFile) { - delete(newSnapshot.extraDiskFiles, path) - }, - func(path tspath.Path, oldFile, newFile *diskFile) { - // Shouldn't happen - }, - ) + 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) logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start)) return newSnapshot From 064ddf5c33702290357759d3f4a1822e3efc259f Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Tue, 14 Oct 2025 20:02:29 +0000 Subject: [PATCH 23/23] don't clone snapshot if we don't have to --- internal/project/session.go | 11 ++++++++--- internal/project/snapshot.go | 29 ++++++++++++----------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/project/session.go b/internal/project/session.go index 67cc87528f..0929be2abd 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -709,15 +709,20 @@ func (s *Session) GetLanguageServiceWithMappedFiles(ctx context.Context, uri lsp s.logger.Write("") } s.backgroundQueue.Enqueue(ctx, func(ctx context.Context) { - s.updateSnapshotWithDiskChanges(changes) + s.updateSnapshotWithDiskChanges(changes, snapshot, snapshotWithFiles) }) return ls.NewLanguageService(project.GetProgram(), snapshotWithFiles), nil } -func (s *Session) updateSnapshotWithDiskChanges(changes map[tspath.Path]*dirty.Change[*diskFile]) { +func (s *Session) updateSnapshotWithDiskChanges(changes map[tspath.Path]*dirty.Change[*diskFile], snapshot, snapshotWithFiles *Snapshot) { s.snapshotMu.Lock() oldSnapshot := s.snapshot - newSnapshot := oldSnapshot.CloneWithDiskChanges(changes, s) + var newSnapshot *Snapshot + if oldSnapshot == snapshot { + newSnapshot = snapshotWithFiles + } else { + newSnapshot = oldSnapshot.CloneWithDiskChanges(changes, s) + } s.snapshot = newSnapshot s.snapshotMu.Unlock() diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 553bfa2abf..de89605a9f 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -358,22 +358,7 @@ func (s *Snapshot) CloneWithSourceMaps(genFiles []string, session *Session) (*Sn } snapshotFS, _, changes := fs.Finalize() logger.Logf(`New files due to source maps: %v`, slices.Collect(maps.Keys(changes))) - 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 - // We don't need to update the extra files watcher here because the resulting snapshot will be - // discarded after fulfilling the request. + 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 } @@ -388,6 +373,17 @@ func (s *Snapshot) CloneWithDiskChanges(changes map[tspath.Path]*dirty.Change[*d 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, @@ -417,7 +413,6 @@ func (s *Snapshot) CloneWithDiskChanges(changes map[tspath.Path]*dirty.Change[*d } } newSnapshot.extraDiskFilesWatch = s.extraDiskFilesWatch.Clone(newSnapshot.extraDiskFiles) - logger.Logf("Finished cloning snapshot %d into snapshot %d in %v", s.id, newSnapshot.id, time.Since(start)) return newSnapshot }