Skip to content

Commit b6634d8

Browse files
authored
Add auto-import retry to getCompletionsAtPosition in API (#64133)
1 parent e4912cf commit b6634d8

2 files changed

Lines changed: 93 additions & 27 deletions

File tree

tsc/internal/api/session.go

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -583,13 +583,13 @@ func (s *Session) setupChecker(ctx context.Context, snapshot SnapshotID, project
583583
// are produced on the persistent API checker and stay resolvable. Only safe when the
584584
// LS operation acquires a checker exactly once; nested acquisitions (e.g. find-all-
585585
// references) would deadlock on the single-slot persistent checker.
586-
func (s *Session) setupLanguageService(sd *snapshotData, program *compiler.Program, projectHandle ProjectID, activeFile string) (*ls.LanguageService, error) {
586+
func (s *Session) setupLanguageService(snapshot *project.Snapshot, program *compiler.Program, projectHandle ProjectID, activeFile string) (*ls.LanguageService, error) {
587587
projectName := parseProjectHandle(projectHandle)
588-
proj := sd.snapshot.ProjectCollection.GetProjectByPath(projectName)
588+
proj := snapshot.ProjectCollection.GetProjectByPath(projectName)
589589
if proj == nil {
590590
return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectName)
591591
}
592-
return ls.NewLanguageService(proj.ID(), program, sd.snapshot, activeFile), nil
592+
return ls.NewLanguageService(proj.ID(), program, snapshot, activeFile), nil
593593
}
594594

595595
// HandleRequest implements Handler.
@@ -3977,7 +3977,7 @@ func (s *Session) handleGetSignatureUsages(ctx context.Context, params *GetSigna
39773977
return nil, nil
39783978
}
39793979

3980-
langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
3980+
langSvc, err := s.setupLanguageService(sd.snapshot, program, params.Project, "")
39813981
if err != nil {
39823982
return nil, err
39833983
}
@@ -4010,21 +4010,41 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge
40104010
if err != nil {
40114011
return nil, err
40124012
}
4013+
run := func(snapshot *project.Snapshot, program *compiler.Program) (*ls.CompletionList, error) {
4014+
sourceFile := program.GetSourceFile(params.File.ToFileName())
4015+
if sourceFile == nil {
4016+
return nil, nil
4017+
}
4018+
langSvc, e := s.setupLanguageService(snapshot, program, params.Project, "")
4019+
if e != nil {
4020+
return nil, e
4021+
}
4022+
internalPos := sourceFile.GetPositionMap().UTF16ToUTF8(int(params.Position))
4023+
return langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol)
4024+
}
4025+
40134026
program, err := sd.getProgram(params.Project)
40144027
if err != nil {
40154028
return nil, err
40164029
}
4017-
sourceFile := program.GetSourceFile(params.File.ToFileName())
4018-
if sourceFile == nil {
4019-
return nil, nil
4020-
}
4021-
langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
4022-
if err != nil {
4023-
return nil, err
4030+
result, err := run(sd.snapshot, program)
4031+
if errors.Is(err, ls.ErrNeedsAutoImports) {
4032+
preparedSnapshot := s.projectSession.GetSnapshotWithAutoImports(ctx, sd.snapshot, params.File.ToURI(s.projectSession.GetCurrentDirectory()))
4033+
defer preparedSnapshot.Deref(s.projectSession)
4034+
if err = ctx.Err(); err != nil {
4035+
return nil, err
4036+
}
4037+
projectPath := parseProjectHandle(params.Project)
4038+
proj := preparedSnapshot.ProjectCollection.GetProjectByPath(projectPath)
4039+
if proj == nil {
4040+
return nil, fmt.Errorf("%w: project %s not found", ErrClientError, projectPath)
4041+
}
4042+
program = proj.GetProgram()
4043+
if program == nil {
4044+
return nil, fmt.Errorf("%w: project has no program", ErrClientError)
4045+
}
4046+
result, err = run(preparedSnapshot, program)
40244047
}
4025-
positionMap := sourceFile.GetPositionMap()
4026-
internalPos := positionMap.UTF16ToUTF8(int(params.Position))
4027-
result, err := langSvc.GetCompletionsAtPosition(ctx, sourceFile, internalPos, params.TriggerCharacter, params.IncludeSymbol)
40284048
if err != nil || result == nil {
40294049
return nil, err
40304050
}
@@ -4077,7 +4097,7 @@ func (s *Session) handleGetReferencedSymbolsForNode(ctx context.Context, params
40774097
return nil, nil
40784098
}
40794099

4080-
langSvc, err := s.setupLanguageService(sd, program, params.Project, "")
4100+
langSvc, err := s.setupLanguageService(sd.snapshot, program, params.Project, "")
40814101
if err != nil {
40824102
return nil, err
40834103
}

tsc/internal/api/session_completion_test.go

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package api
22

33
import (
4-
"context"
54
"testing"
65

76
"github.com/microsoft/TypeScript/tsc/internal/bundled"
7+
"github.com/microsoft/TypeScript/tsc/internal/core"
8+
"github.com/microsoft/TypeScript/tsc/internal/ls/lsutil"
89
"github.com/microsoft/TypeScript/tsc/internal/testutil/projecttestutil"
910
"gotest.tools/v3/assert"
1011
)
@@ -40,22 +41,20 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) {
4041
session := NewSession(projectSession, nil)
4142
defer session.Close()
4243

43-
ctx := context.Background()
44-
45-
snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{
44+
snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
4645
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
4746
})
4847
assert.NilError(t, err)
4948

50-
proj, err := session.handleGetDefaultProjectForFile(ctx, &GetDefaultProjectForFileParams{
49+
proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{
5150
Snapshot: snapshotResp.Snapshot,
5251
File: DocumentIdentifier{FileName: fileName},
5352
})
5453
assert.NilError(t, err)
5554
assert.Assert(t, proj != nil, "file should resolve to a default project")
5655

5756
// content is pure ASCII, so the UTF-16 caret offset equals the byte length.
58-
completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{
57+
completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
5958
Snapshot: snapshotResp.Snapshot,
6059
Project: proj.Id,
6160
File: DocumentIdentifier{FileName: fileName},
@@ -73,7 +72,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) {
7372
continue
7473
}
7574
sawSymbol = true
76-
typeResp, err := session.handleGetTypeOfSymbol(ctx, &GetTypeOfSymbolParams{
75+
typeResp, err := session.handleGetTypeOfSymbol(t.Context(), &GetTypeOfSymbolParams{
7776
Snapshot: snapshotResp.Snapshot,
7877
Project: proj.Id,
7978
Symbol: entry.Symbol.Id,
@@ -114,14 +113,12 @@ func TestCompletionOnInferredProject(t *testing.T) {
114113
session := NewSession(projectSession, nil)
115114
defer session.Close()
116115

117-
ctx := context.Background()
118-
119-
snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{
116+
snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
120117
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
121118
})
122119
assert.NilError(t, err)
123120

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

131128
// This request previously panicked in setupLanguageService.
132129
// content is pure ASCII, so the UTF-16 caret offset equals the byte length.
133-
completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{
130+
completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
134131
Snapshot: snapshotResp.Snapshot,
135132
Project: proj.Id,
136133
File: DocumentIdentifier{FileName: fileName},
@@ -139,3 +136,52 @@ func TestCompletionOnInferredProject(t *testing.T) {
139136
assert.NilError(t, err)
140137
assert.Assert(t, completions != nil, "expected a completion list for array members")
141138
}
139+
140+
func TestCompletionRetriesWithAutoImports(t *testing.T) {
141+
t.Parallel()
142+
if !bundled.Embedded {
143+
t.Skip("bundled files are not embedded")
144+
}
145+
146+
const fileName = "/home/projects/p/src/index.ts"
147+
const content = "someV"
148+
projectSession, _ := projecttestutil.Setup(map[string]any{
149+
"/home/projects/p/tsconfig.json": `{ "compilerOptions": { "module": "esnext", "target": "esnext" } }`,
150+
"/home/projects/p/src/export.ts": "export const someValue = 1;",
151+
fileName: content,
152+
})
153+
defer projectSession.Close()
154+
projectSession.Configure(lsutil.UserPreferences{
155+
IncludeCompletionsForModuleExports: core.TSTrue,
156+
IncludeCompletionsForImportStatements: core.TSTrue,
157+
})
158+
159+
session := NewSession(projectSession, nil)
160+
defer session.Close()
161+
162+
snapshotResp, err := session.handleUpdateSnapshot(t.Context(), &UpdateSnapshotParams{
163+
OpenFiles: []DocumentIdentifier{{FileName: fileName}},
164+
})
165+
assert.NilError(t, err)
166+
proj, err := session.handleGetDefaultProjectForFile(t.Context(), &GetDefaultProjectForFileParams{
167+
Snapshot: snapshotResp.Snapshot,
168+
File: DocumentIdentifier{FileName: fileName},
169+
})
170+
assert.NilError(t, err)
171+
assert.Assert(t, proj != nil, "file should resolve to a default project")
172+
173+
completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{
174+
Snapshot: snapshotResp.Snapshot,
175+
Project: proj.Id,
176+
File: DocumentIdentifier{FileName: fileName},
177+
Position: uint32(len(content)),
178+
})
179+
assert.NilError(t, err)
180+
assert.Assert(t, completions != nil, "expected a completion list")
181+
for _, entry := range completions.Entries {
182+
if entry.Name == "someValue" {
183+
return
184+
}
185+
}
186+
t.Fatal("expected auto-import completion for someValue")
187+
}

0 commit comments

Comments
 (0)