Skip to content
Merged
6 changes: 3 additions & 3 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/ls/autoimports.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 5 additions & 8 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
9 changes: 4 additions & 5 deletions internal/ls/host.go
Original file line number Diff line number Diff line change
@@ -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
}
49 changes: 43 additions & 6 deletions internal/ls/languageservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -36,3 +44,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.host.ReadFile(fileName)
}

func (l *LanguageService) UseCaseSensitiveFileNames() bool {
return l.host.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)
}
81 changes: 81 additions & 0 deletions internal/ls/source_map.go
Original file line number Diff line number Diff line change
@@ -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 !l.host.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(position)})
if documentPos == nil {
return nil
}
if newPos := l.tryGetSourcePositionWorker(documentPos.FileName, core.TextPos(documentPos.Pos)); newPos != nil {
return newPos
}
return documentPos
}
9 changes: 9 additions & 0 deletions internal/ls/utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
4 changes: 0 additions & 4 deletions internal/project/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion internal/project/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
16 changes: 16 additions & 0 deletions internal/project/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@ 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
}

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]
Expand Down
15 changes: 15 additions & 0 deletions internal/project/snapshotfs.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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
}
Expand All @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

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

This is probably fine, but I had imagined a change to cachedvfs.FS instead. Was there a reason not to put the cache there? I’m not sure why there wasn’t a cache there to begin with—presumably the result of every ReadFile gets stored somewhere in memory, and my memory is that the underlying memory for the string will be shared when it’s assigned from the ReadFile result to the cache to the diskFile to the SourceFile and so on.

Copy link
Member Author

Choose a reason for hiding this comment

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

There were other usages of cachedvfs.FS where I wasn't sure caching file reads would make sense or how it would affect those usages, and Sheetal seemed to think we shouldn't cache files at that level.

Copy link
Member

Choose a reason for hiding this comment

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

yeah caching at cached.vfs level means you need to clear it on "writeFile" otherwise tsc -b will run into issues if we dont handle that well - which would add to cost right? and normally in those scenarios the text gets cached in "sourceFile" so another cache is generally not needed

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for clarifying!

Copy link
Member

Choose a reason for hiding this comment

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

I guess that makes sense; there are already some weird inconsistencies with cachedvfs (FileExists can be out of sync with Stat can be out of sync with ReadFile) but I guess we can avoid growing those any more.

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
}

Expand Down
Loading