From c4e1f070276701b62d6384379ae835eb9797169c Mon Sep 17 00:00:00 2001 From: auvred Date: Wed, 2 Sep 2026 15:46:07 +0300 Subject: [PATCH 1/2] Add auto-import retry to getCompletionsAtPosition in API --- tsc/internal/api/session.go | 47 +++++++++++++------ tsc/internal/api/session_completion_test.go | 52 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 3cc6a23a0babd..015c1cce842ae 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -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. @@ -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 } @@ -4010,21 +4010,38 @@ 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) + 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 } @@ -4077,7 +4094,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 } diff --git a/tsc/internal/api/session_completion_test.go b/tsc/internal/api/session_completion_test.go index dd9dd7b6535f0..bb39024b3179e 100644 --- a/tsc/internal/api/session_completion_test.go +++ b/tsc/internal/api/session_completion_test.go @@ -5,6 +5,8 @@ import ( "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" ) @@ -139,3 +141,53 @@ 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() + ctx := context.Background() + + snapshotResp, err := session.handleUpdateSnapshot(ctx, &UpdateSnapshotParams{ + OpenFiles: []DocumentIdentifier{{FileName: fileName}}, + }) + assert.NilError(t, err) + proj, err := session.handleGetDefaultProjectForFile(ctx, &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(ctx, &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") +} From 6c2fde900b720a3c4ede6751d3a421ed1fd4fc49 Mon Sep 17 00:00:00 2001 From: auvred Date: Thu, 3 Sep 2026 09:04:35 +0300 Subject: [PATCH 2/2] Handle context cancelation and use t.Context() instead of context.Background() in test --- tsc/internal/api/session.go | 3 +++ tsc/internal/api/session_completion_test.go | 26 ++++++++------------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/tsc/internal/api/session.go b/tsc/internal/api/session.go index 015c1cce842ae..27e79de817ef6 100644 --- a/tsc/internal/api/session.go +++ b/tsc/internal/api/session.go @@ -4031,6 +4031,9 @@ func (s *Session) handleGetCompletionsAtPosition(ctx context.Context, params *Ge 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 { diff --git a/tsc/internal/api/session_completion_test.go b/tsc/internal/api/session_completion_test.go index bb39024b3179e..c58be89173662 100644 --- a/tsc/internal/api/session_completion_test.go +++ b/tsc/internal/api/session_completion_test.go @@ -1,7 +1,6 @@ package api import ( - "context" "testing" "github.com/microsoft/TypeScript/tsc/internal/bundled" @@ -42,14 +41,12 @@ 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}, }) @@ -57,7 +54,7 @@ func TestCompletionSymbolTypeIsResolvable(t *testing.T) { 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}, @@ -75,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, @@ -116,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}, }) @@ -132,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}, @@ -163,20 +158,19 @@ func TestCompletionRetriesWithAutoImports(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") - completions, err := session.handleGetCompletionsAtPosition(ctx, &GetCompletionsAtPositionParams{ + completions, err := session.handleGetCompletionsAtPosition(t.Context(), &GetCompletionsAtPositionParams{ Snapshot: snapshotResp.Snapshot, Project: proj.Id, File: DocumentIdentifier{FileName: fileName},