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, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames())
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, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames())
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, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames())
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
}
35 changes: 27 additions & 8 deletions internal/ls/definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,28 @@ 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)))
Expand Down Expand Up @@ -104,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,
}
}

Expand Down
54 changes: 49 additions & 5 deletions internal/ls/languageservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,32 @@ 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
useCaseSensitiveFileNames bool
readFile func(path string) (contents string, ok bool)
fileExists func(path string) bool
}

func NewLanguageService(host Host, converters *Converters) *LanguageService {
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,
host: host,
converters: converters,
readFile: readFile,
fileExists: fileExists,
useCaseSensitiveFileNames: useCaseSensitiveFileNames,
documentPositionMappers: map[string]*sourcemap.DocumentPositionMapper{},
}
}

Expand All @@ -36,3 +51,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)
}
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.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.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
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, snapshot.Converters(), snapshot.ReadFile, snapshot.FileExists, snapshot.UseCaseSensitiveFileNames()), 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
20 changes: 20 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,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 {
Expand All @@ -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
}

Expand Down
Loading
Loading