Skip to content
Closed
Show file tree
Hide file tree
Changes from 16 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
212 changes: 212 additions & 0 deletions internal/ls/sourcemaphost.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
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) *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)
}

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

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

declLineStarts := core.ComputeLineStarts(declContent)

if int(location.Range.Start.Line) >= len(declLineStarts) || int(location.Range.End.Line) >= len(declLineStarts) {
return nil
}

declStartPos := int(declLineStarts[location.Range.Start.Line]) + int(location.Range.Start.Character)
declEndPos := int(declLineStarts[location.Range.End.Line]) + int(location.Range.End.Character)

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

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

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

sourceLineStarts := core.ComputeLineStarts(sourceContent)

sourceStartLine, sourceStartChar := core.PositionToLineAndCharacter(startResult.Pos, sourceLineStarts)

originalRangeLength := declEndPos - declStartPos

if declStartPos >= 0 && declEndPos <= len(declContent) {
originalText := strings.TrimSpace(declContent[declStartPos:declEndPos])

if isSimpleIdentifier(originalText) {
sourceEndPos := startResult.Pos + len(originalText)

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

sourceEndLine, sourceEndChar := core.PositionToLineAndCharacter(sourceEndPos, sourceLineStarts)

return &lsproto.Location{
Uri: FileNameToDocumentURI(startResult.FileName),
Range: lsproto.Range{
Start: lsproto.Position{Line: uint32(sourceStartLine), Character: uint32(sourceStartChar)},
End: lsproto.Position{Line: uint32(sourceEndLine), Character: uint32(sourceEndChar)},
},
}
}
}

sourceEndPos := startResult.Pos + originalRangeLength

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

sourceEndLine, sourceEndChar := core.PositionToLineAndCharacter(sourceEndPos, sourceLineStarts)

return &lsproto.Location{
Uri: FileNameToDocumentURI(startResult.FileName),
Range: lsproto.Range{
Start: lsproto.Position{Line: uint32(sourceStartLine), Character: uint32(sourceStartChar)},
End: lsproto.Position{Line: uint32(sourceEndLine), Character: uint32(sourceEndChar)},
},
}
}


func isIdentifierChar(ch byte) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch == '_' || ch == '$'
}

func isSimpleIdentifier(text string) bool {
if len(text) == 0 {
return false
}

first := text[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_' || first == '$') {
return false
}

for i := 1; i < len(text); i++ {
if !isIdentifierChar(text[i]) {
return false
}
}

return true
}
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)
return lsproto.LocationOrLocationsOrDefinitionLinksOrNull{
Locations: &mappedLocations,
}, nil
}

return rawResponse, nil
}

func (s *Server) mapDefinitionLocationsForProject(locations []lsproto.Location, requestingFileUri lsproto.DocumentUri) []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); 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