Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
50 changes: 35 additions & 15 deletions tsc/internal/api/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -583,13 +583,13 @@ func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, project
// are produced on the persistent API checker and stay resolvable. Only safe when the
// LS operation acquires a checker exactly once; nested acquisitions (e.g. find-all-
// references) would deadlock on the single-slot persistent checker.
func (s *Session) setupLanguageService(sd *snapshotData, program *compiler.Program, projectHandle ProjectID, activeFile string) (*ls.LanguageService, error) {
func (s *Session) setupLanguageService(snapshot *project.Snapshot, program *compiler.Program, projectHandle ProjectID, activeFile string) (*ls.LanguageService, error) {
projectName := parseProjectHandle(projectHandle)
proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName)
proj := snapshot.ProjectCollection.GetProjectByPath(projectName)
if proj == nil {
return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName)
}
return ls.NewLanguageService(proj.ID(), program, sd.snapshot, activeFile), nil
return ls.NewLanguageService(proj.ID(), program, snapshot, activeFile), nil
}

// HandleRequest implements Handler.
Expand Down Expand Up @@ -3977,7 +3977,7 @@ func (s *Session) handleGetSignatureUsages(ctx context.Context, params *GetSigna
return nil, nil
}

langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
langSvc, err := s.setupLanguageService(sd.snapshot, program, params.Project, "")
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -4010,21 +4010,41 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge
if err != nil {
return nil, err
}
run := func(snapshot *project.Snapshot, program *compiler.Program) (*ls.CompletionList, error) {
sourceFile := program.GetSourceFile(params.File.ToFileName())
if sourceFile == nil {
return nil, nil
}
langSvc, e := s.setupLanguageService(snapshot, program, params.Project, "")
if e != nil {
return nil, e
}
internalPos := sourceFile.GetPositionMap().UTF16ToUTF8(int(params.Position))
return langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol)
}

program, err := sd.getProgram(params.Project)
if err != nil {
return nil, err
}
sourceFile := program.GetSourceFile(params.File.ToFileName())
if sourceFile == nil {
return nil, nil
}
langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
if err != nil {
return nil, err
result, err := run(sd.snapshot, program)
if errors.Is(err, ls.ErrNeedsAutoImports) {
preparedSnapshot := s.projectSession.GetSnapshotWithAutoImports(ctx, sd.snapshot, params.File.ToURI(s.projectSession.GetCurrentDirectory()))
defer preparedSnapshot.Deref(s.projectSession)
if err = ctx.Err(); err != nil {
return nil, err
}
projectPath := parseProjectHandle(params.Project)
proj := preparedSnapshot.ProjectCollection.GetProjectByPath(projectPath)
if proj == nil {
return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectPath)
}
program = proj.GetProgram()
if program == nil {
return nil, fmt.Errorf("%w: project has no program", ErrClientError)
}
result, err = run(preparedSnapshot, program)
}
positionMap := sourceFile.GetPositionMap()
internalPos := positionMap.UTF16ToUTF8(int(params.Position))
result, err := langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol)
if err != nil || result == nil {
return nil, err
}
Expand Down Expand Up @@ -4077,7 +4097,7 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params
return nil, nil
}

langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
langSvc, err := s.setupLanguageService(sd.snapshot, program, params.Project, "")
if err != nil {
return nil, err
}
Expand Down
70 changes: 58 additions & 12 deletions tsc/internal/api/session_completion_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package api

import (
"context"
"testing"

"github.com/microsoft/TypeScript/tsc/internal/bundled"
"github.com/microsoft/TypeScript/tsc/internal/core"
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
"github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil"
"gotest.tools/v3/assert"
)
Expand Down Expand Up @@ -40,22 +41,20 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) {
session := NewSession(projectSession, nil)
defer session.Close()

ctx := context.Background()

snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{
snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)

proj, err := session.handleGetDefaultProjectForFile(ctx, &GetDefaultProjectForFileParams{
proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{
Snapshot: snapshotResp.Snapshot,
File: DocumentIdentifier{FileName: fileName},
})
assert.NilError(t, err)
assert.Assert(t, proj != nil, "file should resolve to a default project")

// content is pure ASCII, so the UTF-16 caret offset equals the byte length.
completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{
completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
File: DocumentIdentifier{FileName: fileName},
Expand All @@ -73,7 +72,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) {
continue
}
sawSymbol = true
typeResp, err := session.handleGetTypeOfSymbol(ctx, &GetTypeOfSymbolParams{
typeResp, err := session.handleGetTypeOfSymbol(t.Context(), &GetTypeOfSymbolParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
Symbol: entry.Symbol.Id,
Expand Down Expand Up @@ -114,14 +113,12 @@ func TestCompletionOnInferredProject(t *testing.T) {
session := NewSession(projectSession, nil)
defer session.Close()

ctx := context.Background()

snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{
snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)

proj, err := session.handleGetDefaultProjectForFile(ctx, &GetDefaultProjectForFileParams{
proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{
Snapshot: snapshotResp.Snapshot,
File: DocumentIdentifier{FileName: fileName},
})
Expand All @@ -130,7 +127,7 @@ func TestCompletionOnInferredProject(t *testing.T) {

// This request previously panicked in setupLanguageService.
// content is pure ASCII, so the UTF-16 caret offset equals the byte length.
completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{
completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
File: DocumentIdentifier{FileName: fileName},
Expand All @@ -139,3 +136,52 @@ func TestCompletionOnInferredProject(t *testing.T) {
assert.NilError(t, err)
assert.Assert(t, completions != nil, "expected a completion list for array members")
}

func TestCompletionRetriesWithAutoImports(t *testing.T) {
t.Parallel()
if !bundled.Embedded {
t.Skip("bundled files are not embedded")
}

const fileName = "/home/projects/p/src/index.ts"
const content = "someV"
projectSession, _ := projecttestutil.Setup(map[string]any{
"/home/projects/p/tsconfig.json": `{ "compilerOptions": { "module": "esnext", "target": "esnext" } }`,
"/home/projects/p/src/export.ts": "export const someValue = 1;",
fileName: content,
})
defer projectSession.Close()
projectSession.Configure(lsutil.UserPreferences{
IncludeCompletionsForModuleExports: core.TSTrue,
IncludeCompletionsForImportStatements: core.TSTrue,
})

session := NewSession(projectSession, nil)
defer session.Close()

snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
})
assert.NilError(t, err)
proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{
Snapshot: snapshotResp.Snapshot,
File: DocumentIdentifier{FileName: fileName},
})
assert.NilError(t, err)
assert.Assert(t, proj != nil, "file should resolve to a default project")

completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
Snapshot: snapshotResp.Snapshot,
Project: proj.Id,
File: DocumentIdentifier{FileName: fileName},
Position: uint32(len(content)),
})
assert.NilError(t, err)
assert.Assert(t, completions != nil, "expected a completion list")
for _, entry := range completions.Entries {
if entry.Name == "someValue" {
return
}
}
t.Fatal("expected auto-import completion for someValue")
}