diff --git a/internal/api/api.go b/internal/api/api.go index 15fc7c096f..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()) + 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()) + 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()) + languageService := ls.NewLanguageService(project.GetProgram(), snapshot) t := languageService.GetTypeOfSymbol(ctx, symbol) if t == nil { return nil, nil diff --git a/internal/core/core.go b/internal/core/core.go index 6a78811d69..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)) } @@ -648,3 +650,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/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/definition.go b/internal/ls/definition.go index 5e765fcc0b..abefaf932e 100644 --- a/internal/ls/definition.go +++ b/internal/ls/definition.go @@ -104,20 +104,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/host.go b/internal/ls/host.go index a2d4888e43..dfeaa02a62 100644 --- a/internal/ls/host.go +++ b/internal/ls/host.go @@ -1,9 +1,10 @@ package ls -import ( - "github.com/microsoft/typescript-go/internal/compiler" -) +import "github.com/microsoft/typescript-go/internal/sourcemap" type Host interface { - GetProgram() *compiler.Program + UseCaseSensitiveFileNames() bool + ReadFile(path string) (contents string, ok bool) + Converters() *Converters + GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo } diff --git a/internal/ls/languageservice.go b/internal/ls/languageservice.go index 7e47e50e34..c7c4596dce 100644 --- a/internal/ls/languageservice.go +++ b/internal/ls/languageservice.go @@ -4,22 +4,30 @@ 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 + program *compiler.Program + converters *Converters + documentPositionMappers map[string]*sourcemap.DocumentPositionMapper } -func NewLanguageService(host Host, converters *Converters) *LanguageService { +func NewLanguageService( + program *compiler.Program, + host Host, +) *LanguageService { return &LanguageService{ - host: host, - converters: converters, + 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) { @@ -36,3 +44,24 @@ 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.host.ReadFile(fileName) +} + +func (l *LanguageService) UseCaseSensitiveFileNames() bool { + return l.host.UseCaseSensitiveFileNames() +} + +func (l *LanguageService) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { + return l.host.GetECMALineInfo(fileName) +} diff --git a/internal/ls/linemap.go b/internal/ls/linemap.go index 1c2c05518e..345f7cd997 100644 --- a/internal/ls/linemap.go +++ b/internal/ls/linemap.go @@ -9,8 +9,10 @@ import ( "github.com/microsoft/typescript-go/internal/core" ) +type LSPLineStarts []core.TextPos + type LSPLineMap struct { - LineStarts []core.TextPos + LineStarts LSPLineStarts AsciiOnly bool // TODO(jakebailey): collect ascii-only info per line } diff --git a/internal/ls/source_map.go b/internal/ls/source_map.go new file mode 100644 index 0000000000..62eafda161 --- /dev/null +++ b/internal/ls/source_map.go @@ -0,0 +1,81 @@ +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(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, + } + } + 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, + } +} + +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) getScript(fileName string) *script { + text, ok := l.host.ReadFile(fileName) + if !ok { + return nil + } + return &script{fileName: fileName, text: text} +} + +func (l *LanguageService) tryGetSourcePosition( + fileName string, + position core.TextPos, +) *sourcemap.DocumentPosition { + newPos := l.tryGetSourcePositionWorker(fileName, position) + if newPos != nil { + if _, ok := l.ReadFile(newPos.FileName); !ok { // File doesn't exist + 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(position)}) + if documentPos == nil { + return 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 d0078ba56d..86e27a9fb7 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/overlayfs.go b/internal/project/overlayfs.go index 37cb1ae498..47ec2d95f9 100644 --- a/internal/project/overlayfs.go +++ b/internal/project/overlayfs.go @@ -7,6 +7,7 @@ import ( "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/ls" "github.com/microsoft/typescript-go/internal/lsp/lsproto" + "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/zeebo/xxh3" @@ -24,6 +25,7 @@ type FileHandle interface { MatchesDiskText() bool IsOverlay() bool LSPLineMap() *ls.LSPLineMap + ECMALineInfo() *sourcemap.ECMALineInfo Kind() core.ScriptKind } @@ -32,8 +34,10 @@ type fileBase struct { content string hash xxh3.Uint128 - lineMapOnce sync.Once - lineMap *ls.LSPLineMap + lineMapOnce sync.Once + lineMap *ls.LSPLineMap + lineInfoOnce sync.Once + lineInfo *sourcemap.ECMALineInfo } func (f *fileBase) FileName() string { @@ -55,6 +59,14 @@ func (f *fileBase) LSPLineMap() *ls.LSPLineMap { return f.lineMap } +func (f *fileBase) ECMALineInfo() *sourcemap.ECMALineInfo { + f.lineInfoOnce.Do(func() { + lineStarts := core.ComputeECMALineStarts(f.content) + f.lineInfo = sourcemap.CreateECMALineInfo(f.content, lineStarts) + }) + return f.lineInfo +} + type diskFile struct { fileBase needsReload bool 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 4e78c28884..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()), 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/project/snapshot.go b/internal/project/snapshot.go index e53f113df8..8d41e426c5 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -13,6 +13,7 @@ import ( "github.com/microsoft/typescript-go/internal/project/ata" "github.com/microsoft/typescript-go/internal/project/dirty" "github.com/microsoft/typescript-go/internal/project/logging" + "github.com/microsoft/typescript-go/internal/sourcemap" "github.com/microsoft/typescript-go/internal/tspath" ) @@ -81,6 +82,13 @@ func (s *Snapshot) LSPLineMap(fileName string) *ls.LSPLineMap { return nil } +func (s *Snapshot) GetECMALineInfo(fileName string) *sourcemap.ECMALineInfo { + if file := s.fs.GetFile(fileName); file != nil { + return file.ECMALineInfo() + } + return nil +} + func (s *Snapshot) Converters() *ls.Converters { return s.converters } @@ -89,6 +97,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..1b45acf0a2 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,8 +27,11 @@ type snapshotFS struct { fs vfs.FS overlays map[tspath.Path]*overlay diskFiles map[tspath.Path]*diskFile + readFiles collections.SyncMap[tspath.Path, memoizedDiskFile] } +type memoizedDiskFile func() *diskFile + func (s *snapshotFS) FS() vfs.FS { return s.fs } @@ -37,6 +43,15 @@ func (s *snapshotFS) GetFile(fileName string) FileHandle { if file, ok := s.diskFiles[s.toPath(fileName)]; ok { return file } + newEntry := memoizedDiskFile(sync.OnceValue(func() *diskFile { + if contents, ok := s.fs.ReadFile(fileName); ok { + return newDiskFile(fileName, contents) + } + return nil + })) + if entry, ok := s.readFiles.LoadOrStore(s.toPath(fileName), newEntry); ok { + return entry() + } return nil } diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index 90ef2f1aef..ec6efc7b01 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 GetECMAPositionOfLineAndCharacter(sourceFile *ast.SourceFile, line int, cha } 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/lineinfo.go b/internal/sourcemap/lineinfo.go index f3dee2cab1..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 GetECMALineInfo(text string, lineStarts []core.TextPos) *ECMALineInfo { +func CreateECMALineInfo(text string, lineStarts core.ECMALineStarts) *ECMALineInfo { return &ECMALineInfo{ text: text, lineStarts: lineStarts, diff --git a/internal/sourcemap/source_mapper.go b/internal/sourcemap/source_mapper.go new file mode 100644 index 0000000000..d89d542a56 --- /dev/null +++ b/internal/sourcemap/source_mapper.go @@ -0,0 +1,313 @@ +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 + GetECMALineInfo(fileName string) *ECMALineInfo + ReadFile(fileName string) (string, bool) +} + +// 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 + +// Maps source positions to generated positions and vice versa. +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 != "" { + 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.GetECMALineInfo(generatedAbsoluteFilePath) + if lineInfo != nil { + generatedPosition = scanner.ComputePositionOfLineAndCharacterEx( + lineInfo.lineStarts, + mapping.GeneratedLine, + mapping.GeneratedCharacter, + &lineInfo.text, + true, /*allowEdits*/ + ) + } + + sourcePosition := -1 + if mapping.IsSourceMapping() { + lineInfo := host.GetECMALineInfo(sourceFileAbsolutePaths[mapping.SourceIndex]) + if lineInfo != nil { + pos := scanner.ComputePositionOfLineAndCharacterEx( + lineInfo.lineStarts, + mapping.SourceLine, + mapping.SourceCharacter, + &lineInfo.text, + true, /*allowEdits*/ + ) + 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 := tryParseBase64Url(mapFileName); matched { + if base64Object != "" { + if decoded, err := base64.StdEncoding.DecodeString(base64Object); err == nil { + return convertDocumentToSourceMapper(host, string(decoded), generatedFileName) + } + } + // Not a data URL we can parse, skip it + mapFileName = "" + } + } + + var possibleMapLocations []string + if mapFileName != "" { + possibleMapLocations = append(possibleMapLocations, mapFileName) + } + possibleMapLocations = append(possibleMapLocations, generatedFileName+".map") + for _, location := range possibleMapLocations { + mapFileName := tspath.GetNormalizedAbsolutePath(location, tspath.GetDirectoryPath(generatedFileName)) + if mapFileContents, ok := host.ReadFile(mapFileName); ok { + return convertDocumentToSourceMapper(host, mapFileContents, mapFileName) + } + } + return nil +} + +func 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.GetECMALineInfo(fileName) + return TryGetSourceMappingURL(lineInfo) +} + +// 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 { + 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 d5d6521673..c7467f06dd 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.GetECMALineInfo(jsFile.Content, writer.jsLineMap))) + sourceMapRecorder.WriteLineF("mapUrl: %s", sourcemap.TryGetSourceMappingURL(sourcemap.CreateECMALineInfo(jsFile.Content, writer.jsLineMap))) sourceMapRecorder.WriteLineF("sourceRoot: %s", sourceMap.SourceRoot) sourceMapRecorder.WriteLineF("sources: %s", strings.Join(sourceMap.Sources, ",")) if len(sourceMap.SourcesContent) > 0 { 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