From a9acd63920040326cf134dc31a7c7a4101b1dd7d Mon Sep 17 00:00:00 2001 From: lsh4711 <120231876+lsh4711@users.noreply.github.com> Date: Thu, 15 Oct 2026 07:17:22 +0900 Subject: [PATCH 1/2] Clear auto-import specifier cache when user preferences change --- tsc/internal/ls/autoimport/registry.go | 4 +- tsc/internal/lsp/server_completion_test.go | 64 ++++++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) diff --git a/tsc/internal/ls/autoimport/registry.go b/tsc/internal/ls/autoimport/registry.go index 4b8a71042a41a..a335571832d50 100644 --- a/tsc/internal/ls/autoimport/registry.go +++ b/tsc/internal/ls/autoimport/registry.go @@ -397,9 +397,7 @@ func (r *Registry) Clone(ctx context.Context, change RegistryChange, host Regist builder := newRegistryBuilder(r, host, currentDirectory) if change.UserPreferences != nil { builder.userPreferences = *change.UserPreferences - if !core.UnorderedEqual(builder.userPreferences.AutoImportSpecifierExcludeRegexes, r.userPreferences.AutoImportSpecifierExcludeRegexes) { - builder.specifierCache.Clear() - } + builder.specifierCache.Clear() } builder.updateBucketAndDirectoryExistence(change, logger) builder.markBucketsDirty(change, logger) diff --git a/tsc/internal/lsp/server_completion_test.go b/tsc/internal/lsp/server_completion_test.go index 1cbaec2f4d69e..e83607fcbb021 100644 --- a/tsc/internal/lsp/server_completion_test.go +++ b/tsc/internal/lsp/server_completion_test.go @@ -11,6 +11,7 @@ import ( "github.com/microsoft/TypeScript/tsc/internal/ls/lsutil" "github.com/microsoft/TypeScript/tsc/internal/lsp" "github.com/microsoft/TypeScript/tsc/internal/lsp/lsproto" + "github.com/microsoft/TypeScript/tsc/internal/modulespecifiers" "github.com/microsoft/TypeScript/tsc/internal/testutil/lsptestutil" "github.com/microsoft/TypeScript/tsc/internal/tspath" "github.com/microsoft/TypeScript/tsc/internal/vfs/vfstest" @@ -229,6 +230,69 @@ func TestAutoImportCompletionForUnopenedFile(t *testing.T) { assert.Equal(t, item.Data.AutoImport.ModuleSpecifier, "./a") } +func TestAutoImportModuleSpecifierAfterUserPreferenceChange(t *testing.T) { + t.Parallel() + + if !bundled.Embedded { + t.Skip("bundled files are not embedded") + } + + prefs := &lsutil.UserPreferences{ + IncludeCompletionsForModuleExports: core.TSTrue, + IncludeCompletionsForImportStatements: core.TSTrue, + } + client := initCompletionClient(t, map[string]string{ + "/home/projects/tsconfig.json": `{"compilerOptions": {"module": "esnext", "target": "esnext", "paths": {"~/*": ["./src/*"]}}}`, + "/home/projects/src/util/path.ts": "export const somePath = 10;", + "/home/projects/src/util/index.ts": "s", + }, prefs) + + uri := lsconv.FileNameToDocumentURI("/home/projects/src/util/index.ts") + client.SendNotification(t, lsproto.TextDocumentDidOpenInfo, &lsproto.DidOpenTextDocumentParams{ + TextDocument: &lsproto.TextDocumentItem{Uri: uri, LanguageId: "typescript", Text: "s"}, + }) + + moduleSpecifier := func() string { + msg, resp, ok := client.SendRequest(t, lsproto.TextDocumentCompletionInfo, &lsproto.CompletionParams{ + TextDocument: lsproto.TextDocumentIdentifier{Uri: uri}, + Position: lsproto.Position{Line: 0, Character: 1}, + Context: &lsproto.CompletionContext{}, + }) + assert.Assert(t, ok, "expected a response") + assert.Assert(t, msg.AsResponse().Error == nil) + item := findCompletionItem(completionItems(resp), "somePath") + assert.Assert(t, item != nil) + assert.Assert(t, item.Data != nil && item.Data.AutoImport != nil) + return item.Data.AutoImport.ModuleSpecifier + } + + assert.Equal(t, moduleSpecifier(), "./path") + + excluded := *prefs + excluded.AutoImportSpecifierExcludeRegexes = []string{"^\\./path$"} + client.SendNotification(t, lsproto.WorkspaceDidChangeConfigurationInfo, &lsproto.DidChangeConfigurationParams{ + Settings: map[string]any{"typescript": &excluded}, + }) + + assert.Equal(t, moduleSpecifier(), "~/util/path") + + jsEnding := excluded + jsEnding.ImportModuleSpecifierEnding = modulespecifiers.ImportModuleSpecifierEndingPreferenceJs + client.SendNotification(t, lsproto.WorkspaceDidChangeConfigurationInfo, &lsproto.DidChangeConfigurationParams{ + Settings: map[string]any{"typescript": &jsEnding}, + }) + + assert.Equal(t, moduleSpecifier(), "./path.js") + + nonRelative := jsEnding + nonRelative.ImportModuleSpecifierPreference = modulespecifiers.ImportModuleSpecifierPreferenceNonRelative + client.SendNotification(t, lsproto.WorkspaceDidChangeConfigurationInfo, &lsproto.DidChangeConfigurationParams{ + Settings: map[string]any{"typescript": &nonRelative}, + }) + + assert.Equal(t, moduleSpecifier(), "~/util/path.js") +} + // TestCompletionSnapshotFreezing verifies that the auto-import retry uses the // snapshot captured in the sync phase, not a newer one that includes a // concurrent DidChange. Without snapshot freezing the retry would flush the From f7d0c9ca9c2bc4db913d805131cb8c7bcfbe25b1 Mon Sep 17 00:00:00 2001 From: lsh4711 <120231876+lsh4711@users.noreply.github.com> Date: Fri, 16 Oct 2026 06:15:08 +0900 Subject: [PATCH 2/2] Only clear the specifier cache when module specifier preferences change --- tsc/internal/ls/autoimport/registry.go | 4 +- .../modulespecifiers/specifiers_test.go | 66 +++++++++++++++++++ tsc/internal/modulespecifiers/types.go | 6 ++ 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/tsc/internal/ls/autoimport/registry.go b/tsc/internal/ls/autoimport/registry.go index a335571832d50..08e3ae9f92114 100644 --- a/tsc/internal/ls/autoimport/registry.go +++ b/tsc/internal/ls/autoimport/registry.go @@ -397,7 +397,9 @@ func (r *Registry) Clone(ctx context.Context, change RegistryChange, host Regist builder := newRegistryBuilder(r, host, currentDirectory) if change.UserPreferences != nil { builder.userPreferences = *change.UserPreferences - builder.specifierCache.Clear() + if !builder.userPreferences.ModuleSpecifierPreferences().Equal(r.userPreferences.ModuleSpecifierPreferences()) { + builder.specifierCache.Clear() + } } builder.updateBucketAndDirectoryExistence(change, logger) builder.markBucketsDirty(change, logger) diff --git a/tsc/internal/modulespecifiers/specifiers_test.go b/tsc/internal/modulespecifiers/specifiers_test.go index 920ca2c3c71dc..45b86a4023a66 100644 --- a/tsc/internal/modulespecifiers/specifiers_test.go +++ b/tsc/internal/modulespecifiers/specifiers_test.go @@ -537,3 +537,69 @@ func TestTryGetModuleNameFromExportsOrImports(t *testing.T) { } }) } + +func TestUserPreferencesEqual(t *testing.T) { + t.Parallel() + base := UserPreferences{ + ImportModuleSpecifierPreference: ImportModuleSpecifierPreferenceShortest, + ImportModuleSpecifierEnding: ImportModuleSpecifierEndingPreferenceMinimal, + AutoImportSpecifierExcludeRegexes: []string{"^a", "^b"}, + } + tests := []struct { + name string + other UserPreferences + expected bool + }{ + { + name: "same preferences", + other: base, + expected: true, + }, + { + name: "exclude regexes in a different order", + other: UserPreferences{ + ImportModuleSpecifierPreference: ImportModuleSpecifierPreferenceShortest, + ImportModuleSpecifierEnding: ImportModuleSpecifierEndingPreferenceMinimal, + AutoImportSpecifierExcludeRegexes: []string{"^b", "^a"}, + }, + expected: true, + }, + { + name: "different import module specifier preference", + other: UserPreferences{ + ImportModuleSpecifierPreference: ImportModuleSpecifierPreferenceNonRelative, + ImportModuleSpecifierEnding: ImportModuleSpecifierEndingPreferenceMinimal, + AutoImportSpecifierExcludeRegexes: []string{"^a", "^b"}, + }, + expected: false, + }, + { + name: "different import module specifier ending", + other: UserPreferences{ + ImportModuleSpecifierPreference: ImportModuleSpecifierPreferenceShortest, + ImportModuleSpecifierEnding: ImportModuleSpecifierEndingPreferenceJs, + AutoImportSpecifierExcludeRegexes: []string{"^a", "^b"}, + }, + expected: false, + }, + { + name: "different exclude regexes", + other: UserPreferences{ + ImportModuleSpecifierPreference: ImportModuleSpecifierPreferenceShortest, + ImportModuleSpecifierEnding: ImportModuleSpecifierEndingPreferenceMinimal, + AutoImportSpecifierExcludeRegexes: []string{"^a"}, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + result := base.Equal(tt.other) + if result != tt.expected { + t.Errorf("Equal() = %v, expected %v", result, tt.expected) + } + }) + } +} diff --git a/tsc/internal/modulespecifiers/types.go b/tsc/internal/modulespecifiers/types.go index 20377ea5a7bf1..77ab22073e841 100644 --- a/tsc/internal/modulespecifiers/types.go +++ b/tsc/internal/modulespecifiers/types.go @@ -95,6 +95,12 @@ type UserPreferences struct { AutoImportSpecifierExcludeRegexes []string } +func (p UserPreferences) Equal(other UserPreferences) bool { + return p.ImportModuleSpecifierPreference == other.ImportModuleSpecifierPreference && + p.ImportModuleSpecifierEnding == other.ImportModuleSpecifierEnding && + core.UnorderedEqual(p.AutoImportSpecifierExcludeRegexes, other.AutoImportSpecifierExcludeRegexes) +} + type ModuleSpecifierOptions struct { OverrideImportMode core.ResolutionMode }