Skip to content
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions internal/ls/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ func (c *Converters) PositionToLineAndCharacter(script Script, position core.Tex
// UTF-8 offset to UTF-8/16 0-indexed line and character

lineMap := c.getLineMap(script.FileName())

// If lineMap is nil (file not in cache), create a temporary one from the script text
if lineMap == nil {
lineMap = ComputeLineStarts(script.Text())
}

line, isLineStart := slices.BinarySearch(lineMap.LineStarts, position)
if !isLineStart {
Expand Down
190 changes: 190 additions & 0 deletions internal/ls/sourcemaphost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package ls

import (
"strings"

"github.com/microsoft/typescript-go/internal/compiler"
"github.com/microsoft/typescript-go/internal/core"
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
"github.com/microsoft/typescript-go/internal/sourcemap"
"github.com/microsoft/typescript-go/internal/tspath"
)

type sourcemapHost struct {
program *compiler.Program
}


func (h *sourcemapHost) GetSourceFileLike(fileName string) sourcemap.SourceFileLike {
if content, ok := h.readFileWithFallback(fileName); ok {
return &sourcemapSourceFile{
text: content,
lineStarts: core.ComputeLineStarts(content),
}
}
return nil
}

func (h *sourcemapHost) GetCanonicalFileName(path string) string {
return tspath.GetCanonicalFileName(path, h.program.UseCaseSensitiveFileNames())
}

func (h *sourcemapHost) Log(text string) {
}

func (h *sourcemapHost) UseCaseSensitiveFileNames() bool {
return h.program.UseCaseSensitiveFileNames()
}

func (h *sourcemapHost) GetCurrentDirectory() string {
return h.program.Host().GetCurrentDirectory()
}

func (h *sourcemapHost) ReadFile(path string) (string, bool) {
return h.readFileWithFallback(path)
}

func (h *sourcemapHost) FileExists(path string) bool {
return h.program.Host().FS().FileExists(path)
}

// Tries to read a file from the program's FS first, and falls back to underlying FS for files not tracked by the program.
func (h *sourcemapHost) readFileWithFallback(fileName string) (string, bool) {
if content, ok := h.program.Host().FS().ReadFile(fileName); ok {
return content, true
}

if fallbackFS, ok := h.program.Host().FS().(sourcemap.FallbackFileReader); ok {
return fallbackFS.ReadFileWithFallback(fileName)
}

return "", false
}


type sourcemapSourceFile struct {
text string
lineStarts []core.TextPos
}

func (f *sourcemapSourceFile) Text() string {
return f.text
}

func (f *sourcemapSourceFile) LineStarts() []core.TextPos {
return f.lineStarts
}

type sourcemapFileReader struct {
host *sourcemapHost
}

func (r *sourcemapFileReader) ReadFile(path string) (string, bool) {
return r.host.readFileWithFallback(path)
}

// Creates a SourceMapper for the given program.
func CreateSourceMapperForProgram(program *compiler.Program) sourcemap.SourceMapper {
host := &sourcemapHost{program: program}
fileReader := &sourcemapFileReader{host: host}
return sourcemap.CreateSourceMapper(host, fileReader)
}


// Maps a single definition location using source maps.
func MapSingleDefinitionLocation(program *compiler.Program, location lsproto.Location, languageService *LanguageService) *lsproto.Location {
fileName := location.Uri.FileName()

if !strings.HasSuffix(fileName, ".d.ts") {
return nil
}

if strings.HasPrefix(fileName, "^/bundled/") {
return nil
}

host := &sourcemapHost{program: program}
sourceMapper := CreateSourceMapperForProgram(program)

return tryMapLocation(sourceMapper, host, location, languageService)
}

func tryMapLocation(sourceMapper sourcemap.SourceMapper, host *sourcemapHost, location lsproto.Location, languageService *LanguageService) *lsproto.Location {
fileName := location.Uri.FileName()

declContent, ok := host.readFileWithFallback(fileName)
if !ok {
return nil
}

declLineStarts := core.ComputeLineStarts(declContent)
declStartPos := int(declLineStarts[location.Range.Start.Line]) + int(location.Range.Start.Character)

startInput := sourcemap.DocumentPosition{
FileName: fileName,
Pos: declStartPos,
}

startResult := sourceMapper.TryGetSourcePosition(startInput)
if startResult == nil {
return nil
}

declEndPos := int(declLineStarts[location.Range.End.Line]) + int(location.Range.End.Character)
originalRangeLength := declEndPos - declStartPos
sourceEndPos := startResult.Pos + originalRangeLength

program := host.program
sourceFile := program.GetSourceFile(startResult.FileName)
var sourceStartLSP lsproto.Position
var sourceEndLSP lsproto.Position

if sourceFile != nil {
// Source file is in the program, use LS converters
if sourceEndPos > len(sourceFile.Text()) {
sourceEndPos = len(sourceFile.Text())
}

sourceStartLSP = languageService.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startResult.Pos))
sourceEndLSP = languageService.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(sourceEndPos))

} else {
sourceContent, ok := host.readFileWithFallback(startResult.FileName)
if !ok {
return nil
}

if sourceEndPos > len(sourceContent) {
sourceEndPos = len(sourceContent)
}

textScript := &textScript{
fileName: startResult.FileName,
text: sourceContent,
}

sourceStartLSP = languageService.converters.PositionToLineAndCharacter(textScript, core.TextPos(startResult.Pos))
sourceEndLSP = languageService.converters.PositionToLineAndCharacter(textScript, core.TextPos(sourceEndPos))
}
return &lsproto.Location{
Uri: FileNameToDocumentURI(startResult.FileName),
Range: lsproto.Range{
Start: sourceStartLSP,
End: sourceEndLSP,
},
}
}

// textScript is a simple wrapper that implements the Script interface for raw text content
type textScript struct {
fileName string
text string
}

func (t *textScript) FileName() string {
return t.fileName
}

func (t *textScript) Text() string {
return t.text
}
57 changes: 56 additions & 1 deletion internal/lsp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os/signal"
"runtime/debug"
"slices"
"strings"
"sync"
"sync/atomic"
"syscall"
Expand Down Expand Up @@ -710,7 +711,61 @@ func (s *Server) handleSignatureHelp(ctx context.Context, languageService *ls.La
}

func (s *Server) handleDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.DefinitionParams) (lsproto.DefinitionResponse, error) {
return ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position)
rawResponse, err := ls.ProvideDefinition(ctx, params.TextDocument.Uri, params.Position)
if err != nil {
return rawResponse, err
}

if locations := rawResponse.Locations; locations != nil {
mappedLocations := s.mapDefinitionLocationsForProject(*locations, params.TextDocument.Uri, ls)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{
Locations: &mappedLocations,
}, nil
}

return rawResponse, nil
}

func (s *Server) mapDefinitionLocationsForProject(locations []lsproto.Location, requestingFileUri lsproto.DocumentUri, languageService *ls.LanguageService) []lsproto.Location {
mappedLocations := make([]lsproto.Location, 0, len(locations))

snapshot, release := s.session.Snapshot()
defer release()

requestingProject := snapshot.GetDefaultProject(requestingFileUri)
if requestingProject == nil {
return locations
}

program := requestingProject.GetProgram()
if program == nil {
return locations
}

for _, location := range locations {
// Skip bundled library URIs that have invalid URL encoding
// These URIs contain "%3A" which causes panics in FileName()
uriString := string(location.Uri)
if strings.Contains(uriString, "bundled%3A") {
mappedLocations = append(mappedLocations, location)
continue
}

fileName := location.Uri.FileName()
if !strings.HasSuffix(fileName, ".d.ts") {
mappedLocations = append(mappedLocations, location)
continue
}

// Only call the source mapping for .d.ts files
if mappedLocation := ls.MapSingleDefinitionLocation(program, location, languageService); mappedLocation != nil {
mappedLocations = append(mappedLocations, *mappedLocation)
} else {
mappedLocations = append(mappedLocations, location)
}
}

return mappedLocations
}

func (s *Server) handleTypeDefinition(ctx context.Context, ls *ls.LanguageService, params *lsproto.TypeDefinitionParams) (lsproto.TypeDefinitionResponse, error) {
Expand Down
9 changes: 9 additions & 0 deletions internal/project/compilerhost.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ func (fs *compilerFS) ReadFile(path string) (contents string, ok bool) {
return "", false
}

// ReadFileWithFallback implements vfs.FS.
// This reads the file from the underlying FS if it's not tracked by the program.
func (fs *compilerFS) ReadFileWithFallback(path string) (contents string, ok bool) {
if fh := fs.source.GetFile(path); fh != nil {
return fh.Content(), true
}
return fs.source.FS().ReadFile(path)
}

// Realpath implements vfs.FS.
func (fs *compilerFS) Realpath(path string) string {
return fs.source.FS().Realpath(path)
Expand Down
Loading