From c920059b9e5497873e8202b5889063b4443c2eb4 Mon Sep 17 00:00:00 2001 From: Gabriela Araujo Britto Date: Fri, 26 Sep 2025 23:09:14 +0000 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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]