diff --git a/internal/fourslash/_scripts/convertFourslash.mts b/internal/fourslash/_scripts/convertFourslash.mts index d958369d81..e9887fff35 100644 --- a/internal/fourslash/_scripts/convertFourslash.mts +++ b/internal/fourslash/_scripts/convertFourslash.mts @@ -1619,6 +1619,7 @@ function stringToTristate(s: string): string { function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefined { const inlayHintPreferences: string[] = []; + const moduleSpecifierPreferences: string[] = []; const preferences: string[] = []; for (const prop of arg.properties) { if (ts.isPropertyAssignment(prop)) { @@ -1643,19 +1644,19 @@ function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefin } regexes.push(getGoStringLiteral(strElem.text)); } - preferences.push(`AutoImportSpecifierExcludeRegexes: []string{${regexes.join(", ")}}`); + moduleSpecifierPreferences.push(`AutoImportSpecifierExcludeRegexes: []string{${regexes.join(", ")}}`); break; case "importModuleSpecifierPreference": if (!ts.isStringLiteralLike(prop.initializer)) { return undefined; } - preferences.push(`ImportModuleSpecifierPreference: ${prop.initializer.getText()}`); + moduleSpecifierPreferences.push(`ImportModuleSpecifierPreference: ${prop.initializer.getText()}`); break; case "importModuleSpecifierEnding": if (!ts.isStringLiteralLike(prop.initializer)) { return undefined; } - preferences.push(`ImportModuleSpecifierEnding: ${prop.initializer.getText()}`); + moduleSpecifierPreferences.push(`ImportModuleSpecifierEnding: ${prop.initializer.getText()}`); break; case "includePackageJsonAutoImports": if (!ts.isStringLiteralLike(prop.initializer)) { @@ -1736,6 +1737,9 @@ function parseUserPreferences(arg: ts.ObjectLiteralExpression): string | undefin if (inlayHintPreferences.length > 0) { preferences.push(`InlayHints: lsutil.InlayHintsPreferences{${inlayHintPreferences.join(",")}}`); } + if (moduleSpecifierPreferences.length > 0) { + preferences.push(`ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{${moduleSpecifierPreferences.join(",")}}`); + } if (preferences.length === 0) { return "nil /*preferences*/"; } diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 488c0beff8..541eeff11c 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -232,7 +232,7 @@ func NewFourslash(t *testing.T, capabilities *lsproto.ClientCapabilities, conten in: inputWriter, out: outputReader, testData: &testData, - userPreferences: lsutil.NewDefaultUserPreferences(), // !!! parse default preferences for fourslash case? + userPreferences: lsutil.DefaultUserPreferences, vfs: fs, scriptInfos: scriptInfos, converters: converters, @@ -657,7 +657,7 @@ func (f *FourslashTest) Configure(t *testing.T, config *lsutil.UserPreferences) } func (f *FourslashTest) ConfigureWithReset(t *testing.T, config *lsutil.UserPreferences) (reset func()) { - originalConfig := f.userPreferences.Copy() + originalConfig := f.userPreferences f.Configure(t, config) return func() { f.Configure(t, originalConfig) @@ -1271,7 +1271,7 @@ func (f *FourslashTest) VerifyApplyCodeActionFromCompletion(t *testing.T, marker userPreferences = options.UserPreferences } else { // Default preferences: enables auto-imports - userPreferences = lsutil.NewDefaultUserPreferences() + userPreferences = lsutil.DefaultUserPreferences } reset := f.ConfigureWithReset(t, userPreferences) @@ -3126,10 +3126,8 @@ func (f *FourslashTest) BaselineAutoImportsCompletions(t *testing.T, markerNames reset := f.ConfigureWithReset(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierEnding: f.userPreferences.ImportModuleSpecifierEnding, - ImportModuleSpecifierPreference: f.userPreferences.ImportModuleSpecifierPreference, + ModuleSpecifier: f.userPreferences.ModuleSpecifier, AutoImportFileExcludePatterns: f.userPreferences.AutoImportFileExcludePatterns, - AutoImportSpecifierExcludeRegexes: f.userPreferences.AutoImportSpecifierExcludeRegexes, PreferTypeOnlyAutoImports: f.userPreferences.PreferTypeOnlyAutoImports, }) defer reset() @@ -3417,7 +3415,7 @@ func (f *FourslashTest) VerifyBaselineInlayHints( preferences := testPreferences if preferences == nil { - preferences = lsutil.NewDefaultUserPreferences() + preferences = lsutil.DefaultUserPreferences } reset := f.ConfigureWithReset(t, preferences) defer reset() @@ -3737,11 +3735,11 @@ type VerifyWorkspaceSymbolCase struct { // `verify.navigateTo` in Strada. func (f *FourslashTest) VerifyWorkspaceSymbol(t *testing.T, cases []*VerifyWorkspaceSymbolCase) { - originalPreferences := f.userPreferences.Copy() + originalPreferences := f.userPreferences for _, testCase := range cases { preferences := testCase.Preferences if preferences == nil { - preferences = lsutil.NewDefaultUserPreferences() + preferences = lsutil.DefaultUserPreferences } f.Configure(t, preferences) result := sendRequest(t, f, lsproto.WorkspaceSymbolInfo, &lsproto.WorkspaceSymbolParams{Query: testCase.Pattern}) diff --git a/internal/fourslash/tests/autoImportSpecifierExcludeRegexes_test.go b/internal/fourslash/tests/autoImportSpecifierExcludeRegexes_test.go index 18dd087b07..b4f2fe7b6c 100644 --- a/internal/fourslash/tests/autoImportSpecifierExcludeRegexes_test.go +++ b/internal/fourslash/tests/autoImportSpecifierExcludeRegexes_test.go @@ -24,7 +24,9 @@ ignoredSym/*2*/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.Configure(t, &lsutil.UserPreferences{ - AutoImportSpecifierExcludeRegexes: []string{".*ignoreme.*"}, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + AutoImportSpecifierExcludeRegexes: []string{".*ignoreme.*"}, + }, IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, }) diff --git a/internal/fourslash/tests/gen/autoImportCrossProject_baseUrl_toDist_test.go b/internal/fourslash/tests/gen/autoImportCrossProject_baseUrl_toDist_test.go index 04009ad3af..ac991cf309 100644 --- a/internal/fourslash/tests/gen/autoImportCrossProject_baseUrl_toDist_test.go +++ b/internal/fourslash/tests/gen/autoImportCrossProject_baseUrl_toDist_test.go @@ -46,5 +46,5 @@ export function saveMe() { defer done() f.MarkTestAsStradaServer() f.GoToFile(t, "/home/src/workspaces/project/web/src/Helper.ts") - f.VerifyImportFixModuleSpecifiers(t, "", []string{"../../common/src/MyModule"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"../../common/src/MyModule"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) } diff --git a/internal/fourslash/tests/gen/autoImportCrossProject_paths_toDist2_test.go b/internal/fourslash/tests/gen/autoImportCrossProject_paths_toDist2_test.go index 62c7ed904b..6b30f8c6f4 100644 --- a/internal/fourslash/tests/gen/autoImportCrossProject_paths_toDist2_test.go +++ b/internal/fourslash/tests/gen/autoImportCrossProject_paths_toDist2_test.go @@ -48,5 +48,5 @@ export function saveMe() { defer done() f.MarkTestAsStradaServer() f.GoToFile(t, "/home/src/workspaces/project/web/src/Helper.ts") - f.VerifyImportFixModuleSpecifiers(t, "", []string{"@common/MyModule"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"@common/MyModule"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) } diff --git a/internal/fourslash/tests/gen/autoImportPackageJsonImportsCaseSensitivity_test.go b/internal/fourslash/tests/gen/autoImportPackageJsonImportsCaseSensitivity_test.go index 7576dc5bb8..b330973d0b 100644 --- a/internal/fourslash/tests/gen/autoImportPackageJsonImportsCaseSensitivity_test.go +++ b/internal/fourslash/tests/gen/autoImportPackageJsonImportsCaseSensitivity_test.go @@ -27,5 +27,5 @@ export function add(a: number, b: number) {} add/*imports*/;` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "imports", []string{"#src/add.ts"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) + f.VerifyImportFixModuleSpecifiers(t, "imports", []string{"#src/add.ts"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) } diff --git a/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference1_test.go b/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference1_test.go index eb0458d9b0..c13a8eb665 100644 --- a/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference1_test.go +++ b/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference1_test.go @@ -25,5 +25,5 @@ export function something(name: string): any; something/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"./src/a/b/c/something"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "relative"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"./src/a/b/c/something"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "relative"}}) } diff --git a/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference2_test.go b/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference2_test.go index 516b360fbd..023622439c 100644 --- a/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference2_test.go +++ b/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference2_test.go @@ -25,5 +25,5 @@ export function something(name: string): any; something/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"./src/a/b/c/something"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "project-relative"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"./src/a/b/c/something"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "project-relative"}}) } diff --git a/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference3_test.go b/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference3_test.go index 4e09c97c83..5e5261c30f 100644 --- a/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference3_test.go +++ b/internal/fourslash/tests/gen/autoImportPackageJsonImportsPreference3_test.go @@ -25,5 +25,5 @@ export function something(name: string): any; something/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"#a/b/c/something"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"#a/b/c/something"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) } diff --git a/internal/fourslash/tests/gen/autoImportPaths_test.go b/internal/fourslash/tests/gen/autoImportPaths_test.go index 675f1a878d..a1e949eaa0 100644 --- a/internal/fourslash/tests/gen/autoImportPaths_test.go +++ b/internal/fourslash/tests/gen/autoImportPaths_test.go @@ -33,5 +33,5 @@ bar/**/ export const bar = 0;` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"package2/file1"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "shortest"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"package2/file1"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "shortest"}}) } diff --git a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go index b5f6ad8dbb..4c1e15e223 100644 --- a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go +++ b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes1_test.go @@ -28,15 +28,15 @@ x/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, nil /*preferences*/) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"utils"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/UTILS/"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/UTILS/i"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/ambient/utils/"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/ambient\\/utils/"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/.*?$"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"^ambient/"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient/utils"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"ambient$"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"oops("}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"utils"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/UTILS/"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/UTILS/i"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/ambient/utils/"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/ambient\\/utils/"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"/.*?$"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"^ambient/"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"ambient$"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"ambient", "ambient/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"oops("}}}) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ IsIncomplete: false, ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ diff --git a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes2_test.go b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes2_test.go index a09da0af0f..df2c2a78d1 100644 --- a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes2_test.go +++ b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes2_test.go @@ -28,8 +28,8 @@ add/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyImportFixModuleSpecifiers(t, "", []string{"./utils"}, nil /*preferences*/) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"@app/utils"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"^\\./"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"@app/utils"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"./utils"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative", AutoImportSpecifierExcludeRegexes: []string{"^@app/"}}) - f.VerifyImportFixModuleSpecifiers(t, "", []string{}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"utils"}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"@app/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"^\\./"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"@app/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"./utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative", AutoImportSpecifierExcludeRegexes: []string{"^@app/"}}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"utils"}}}) } diff --git a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes3_test.go b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes3_test.go index 8606273dd8..b65ab91909 100644 --- a/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes3_test.go +++ b/internal/fourslash/tests/gen/autoImportSpecifierExcludeRegexes3_test.go @@ -31,5 +31,5 @@ add/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() f.VerifyImportFixModuleSpecifiers(t, "", []string{"pkg", "pkg/utils"}, nil /*preferences*/) - f.VerifyImportFixModuleSpecifiers(t, "", []string{"pkg/utils"}, &lsutil.UserPreferences{AutoImportSpecifierExcludeRegexes: []string{"^pkg$"}}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"pkg/utils"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{AutoImportSpecifierExcludeRegexes: []string{"^pkg$"}}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFixJsEnding_test.go b/internal/fourslash/tests/gen/importNameCodeFixJsEnding_test.go index 6856cbca09..c90bb7d268 100644 --- a/internal/fourslash/tests/gen/importNameCodeFixJsEnding_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFixJsEnding_test.go @@ -23,5 +23,5 @@ export declare function customElement(name: string): any; customElement/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"lit/decorators.js"}, &lsutil.UserPreferences{ImportModuleSpecifierEnding: "js"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"lit/decorators.js"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierEnding: "js"}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl1_test.go b/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl1_test.go index 545974d129..22b5c98de7 100644 --- a/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl1_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl1_test.go @@ -34,5 +34,5 @@ f1();`, `import { f1 } from "b/x"; f1();`, - }, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) + }, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl2_test.go b/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl2_test.go index 8c0a7a5129..7c4a61d7d9 100644 --- a/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl2_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFixNewImportBaseUrl2_test.go @@ -34,5 +34,5 @@ f1();`, `import { f1 } from "../b/x"; f1();`, - }, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "relative"}) + }, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "relative"}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFix_barrelExport2_test.go b/internal/fourslash/tests/gen/importNameCodeFix_barrelExport2_test.go index ea5a0dc1db..3b4a7858e4 100644 --- a/internal/fourslash/tests/gen/importNameCodeFix_barrelExport2_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFix_barrelExport2_test.go @@ -35,6 +35,6 @@ export { A } from "../foo/a"; export * from "./a";` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "sibling", []string{"proj/foo/a", "proj/src/utils", "proj", "proj/foo"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) - f.VerifyImportFixModuleSpecifiers(t, "parent", []string{"proj/foo", "proj/foo/a", "proj/src/utils", "proj"}, &lsutil.UserPreferences{ImportModuleSpecifierPreference: "non-relative"}) + f.VerifyImportFixModuleSpecifiers(t, "sibling", []string{"proj/foo/a", "proj/src/utils", "proj", "proj/foo"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) + f.VerifyImportFixModuleSpecifiers(t, "parent", []string{"proj/foo", "proj/foo/a", "proj/src/utils", "proj"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierPreference: "non-relative"}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFix_barrelExport3_test.go b/internal/fourslash/tests/gen/importNameCodeFix_barrelExport3_test.go index c5bab3ae61..cbbced0fbe 100644 --- a/internal/fourslash/tests/gen/importNameCodeFix_barrelExport3_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFix_barrelExport3_test.go @@ -31,6 +31,6 @@ A/*parent*/ export * from "./a";` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "sibling", []string{"./a", "./index", "../index"}, &lsutil.UserPreferences{ImportModuleSpecifierEnding: "index"}) - f.VerifyImportFixModuleSpecifiers(t, "parent", []string{"../foo/a", "../foo/index", "../index"}, &lsutil.UserPreferences{ImportModuleSpecifierEnding: "index"}) + f.VerifyImportFixModuleSpecifiers(t, "sibling", []string{"./a", "./index", "../index"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierEnding: "index"}}) + f.VerifyImportFixModuleSpecifiers(t, "parent", []string{"../foo/a", "../foo/index", "../index"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierEnding: "index"}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFix_pathsWithExtension_test.go b/internal/fourslash/tests/gen/importNameCodeFix_pathsWithExtension_test.go index a010baf7ca..7cd7b39700 100644 --- a/internal/fourslash/tests/gen/importNameCodeFix_pathsWithExtension_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFix_pathsWithExtension_test.go @@ -32,5 +32,5 @@ export function helloWorld() {} helloWorld/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"#internals/example"}, &lsutil.UserPreferences{ImportModuleSpecifierEnding: "js"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"#internals/example"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierEnding: "js"}}) } diff --git a/internal/fourslash/tests/gen/importNameCodeFix_typesVersions_test.go b/internal/fourslash/tests/gen/importNameCodeFix_typesVersions_test.go index 7220848902..3cccf79b8a 100644 --- a/internal/fourslash/tests/gen/importNameCodeFix_typesVersions_test.go +++ b/internal/fourslash/tests/gen/importNameCodeFix_typesVersions_test.go @@ -36,5 +36,5 @@ import {} from "unified"; x/**/` f, done := fourslash.NewFourslash(t, nil /*capabilities*/, content) defer done() - f.VerifyImportFixModuleSpecifiers(t, "", []string{"unified", "unified/types/ts3.444/index.js"}, &lsutil.UserPreferences{ImportModuleSpecifierEnding: "js"}) + f.VerifyImportFixModuleSpecifiers(t, "", []string{"unified", "unified/types/ts3.444/index.js"}, &lsutil.UserPreferences{ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ImportModuleSpecifierEnding: "js"}}) } diff --git a/internal/fourslash/tests/importModuleSpecifierEnding_test.go b/internal/fourslash/tests/importModuleSpecifierEnding_test.go index eef70fa359..7e0b3cc712 100644 --- a/internal/fourslash/tests/importModuleSpecifierEnding_test.go +++ b/internal/fourslash/tests/importModuleSpecifierEnding_test.go @@ -24,7 +24,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ @@ -51,7 +53,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ @@ -78,7 +82,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ @@ -105,7 +111,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceJs, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierEnding: modulespecifiers.ImportModuleSpecifierEndingPreferenceJs, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ diff --git a/internal/fourslash/tests/importModuleSpecifierPreference_test.go b/internal/fourslash/tests/importModuleSpecifierPreference_test.go index 08a9627457..80d5f50849 100644 --- a/internal/fourslash/tests/importModuleSpecifierPreference_test.go +++ b/internal/fourslash/tests/importModuleSpecifierPreference_test.go @@ -24,7 +24,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceShortest, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceShortest, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ @@ -51,7 +53,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ @@ -78,7 +82,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ @@ -114,7 +120,9 @@ helper/**/` f.Configure(t, &lsutil.UserPreferences{ IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, - ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, + ModuleSpecifier: lsutil.ModuleSpecifierUserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, + }, }) f.VerifyCompletions(t, "", &fourslash.CompletionsExpectedList{ ItemDefaults: &fourslash.CompletionsExpectedItemDefaults{ diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index c88f426880..80deae1c0c 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -639,7 +639,7 @@ func isIndexFileName(fileName string) bool { // returns `-1` if `a` is better than `b` func compareModuleSpecifierRelativity(a *ImportFix, b *ImportFix, preferences *lsutil.UserPreferences) int { - switch preferences.ImportModuleSpecifierPreference { + switch preferences.ModuleSpecifier.ImportModuleSpecifierPreference { case modulespecifiers.ImportModuleSpecifierPreferenceNonRelative, modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative: return core.CompareBooleans(a.moduleSpecifierKind == modulespecifiers.ResultKindRelative, b.moduleSpecifierKind == modulespecifiers.ResultKindRelative) } diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index d57197a027..4d89398c94 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -1,102 +1,109 @@ package lsutil import ( + "reflect" "slices" "strings" + "sync" + "github.com/go-json-experiment/json" + "github.com/go-json-experiment/json/jsontext" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/modulespecifiers" - "github.com/microsoft/typescript-go/internal/tsoptions" ) -func NewDefaultUserPreferences() *UserPreferences { - return &UserPreferences{ - IncludeCompletionsForModuleExports: core.TSTrue, - IncludeCompletionsForImportStatements: core.TSTrue, +var DefaultUserPreferences = &UserPreferences{ + IncludeCompletionsForModuleExports: core.TSTrue, + IncludeCompletionsForImportStatements: core.TSTrue, - AllowRenameOfImportPath: core.TSTrue, - ProvideRefactorNotApplicableReason: true, - IncludeCompletionsWithSnippetText: core.TSTrue, - DisplayPartsForJSDoc: true, - DisableLineTextInReferences: true, - ReportStyleChecksAsWarnings: true, + AllowRenameOfImportPath: core.TSTrue, + ProvideRefactorNotApplicableReason: true, + IncludeCompletionsWithSnippetText: core.TSTrue, + DisplayPartsForJSDoc: true, + DisableLineTextInReferences: true, + ReportStyleChecksAsWarnings: true, - ExcludeLibrarySymbolsInNavTo: true, - } + ExcludeLibrarySymbolsInNavTo: true, } +// UserPreferences represents TypeScript language service preferences. +// +// Fields are populated using two tags: +// - `raw:"name"` or `raw:"name,invert"` - TypeScript/raw name for unstable section lookup +// - `config:"path.to.setting"` or `config:"path.to.setting,invert"` - VS Code nested config path +// +// At least one tag must be present on each preference field. +// The `,invert` modifier inverts boolean values (e.g., VS Code's "suppress" -> our "include"). type UserPreferences struct { - QuotePreference QuotePreference - LazyConfiguredProjectsFromExternalProject bool // !!! + QuotePreference QuotePreference `raw:"quotePreference" config:"preferences.quoteStyle"` + LazyConfiguredProjectsFromExternalProject bool `raw:"lazyConfiguredProjectsFromExternalProject"` // !!! // A positive integer indicating the maximum length of a hover text before it is truncated. // // Default: `500` - MaximumHoverLength int // !!! + MaximumHoverLength int `raw:"maximumHoverLength"` // !!! // ------- Completions ------- // If enabled, TypeScript will search through all external modules' exports and add them to the completions list. // This affects lone identifier completions but not completions on the right hand side of `obj.`. - IncludeCompletionsForModuleExports core.Tristate + IncludeCompletionsForModuleExports core.Tristate `raw:"includeCompletionsForModuleExports" config:"suggest.autoImports"` // Enables auto-import-style completions on partially-typed import statements. E.g., allows // `import write|` to be completed to `import { writeFile } from "fs"`. - IncludeCompletionsForImportStatements core.Tristate + IncludeCompletionsForImportStatements core.Tristate `raw:"includeCompletionsForImportStatements" config:"suggest.includeCompletionsForImportStatements"` // Unless this option is `false`, member completion lists triggered with `.` will include entries // on potentially-null and potentially-undefined values, with insertion text to replace // preceding `.` tokens with `?.`. - IncludeAutomaticOptionalChainCompletions core.Tristate + IncludeAutomaticOptionalChainCompletions core.Tristate `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"` // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. - IncludeCompletionsWithSnippetText core.Tristate // !!! + IncludeCompletionsWithSnippetText core.Tristate `raw:"includeCompletionsWithSnippetText"` // !!! // If enabled, completions for class members (e.g. methods and properties) will include // a whole declaration for the member. // E.g., `class A { f| }` could be completed to `class A { foo(): number {} }`, instead of // `class A { foo }`. - IncludeCompletionsWithClassMemberSnippets core.Tristate // !!! + IncludeCompletionsWithClassMemberSnippets core.Tristate `raw:"includeCompletionsWithClassMemberSnippets" config:"suggest.classMemberSnippets.enabled"` // !!! // If enabled, object literal methods will have a method declaration completion entry in addition // to the regular completion entry containing just the method name. // E.g., `const objectLiteral: T = { f| }` could be completed to `const objectLiteral: T = { foo(): void {} }`, // in addition to `const objectLiteral: T = { foo }`. - IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate // !!! - JsxAttributeCompletionStyle JsxAttributeCompletionStyle + IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!! + JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"` // ------- AutoImports -------- - ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference - // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" - ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference - IncludePackageJsonAutoImports IncludePackageJsonAutoImports - AutoImportSpecifierExcludeRegexes []string - AutoImportFileExcludePatterns []string - PreferTypeOnlyAutoImports core.Tristate + ModuleSpecifier ModuleSpecifierUserPreferences + + IncludePackageJsonAutoImports IncludePackageJsonAutoImports `raw:"includePackageJsonAutoImports" config:"preferences.includePackageJsonAutoImports"` + AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"` + PreferTypeOnlyAutoImports core.Tristate `raw:"preferTypeOnlyAutoImports" config:"preferences.preferTypeOnlyAutoImports"` // ------- OrganizeImports ------- // Indicates whether imports should be organized in a case-insensitive manner. // // Default: TSUnknown ("auto" in strada), will perform detection - OrganizeImportsIgnoreCase core.Tristate // !!! + OrganizeImportsIgnoreCase core.Tristate `raw:"organizeImportsIgnoreCase" config:"preferences.organizeImports.caseSensitivity"` // !!! // Indicates whether imports should be organized via an "ordinal" (binary) comparison using the numeric value of their // code points, or via "unicode" collation (via the Unicode Collation Algorithm (https://unicode.org/reports/tr10/#Scope)) // // using rules associated with the locale specified in organizeImportsCollationLocale. // // Default: Ordinal - OrganizeImportsCollation OrganizeImportsCollation // !!! + OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!! // Indicates the locale to use for "unicode" collation. If not specified, the locale `"en"` is used as an invariant // for the sake of consistent sorting. Use `"auto"` to use the detected UI locale. // // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `"en"` - OrganizeImportsLocale string // !!! + OrganizeImportsLocale string `raw:"organizeImportsLocale" config:"preferences.organizeImports.locale"` // !!! // Indicates whether numeric collation should be used for digit sequences in strings. When `true`, will collate // strings such that `a1z < a2z < a100z`. When `false`, will collate strings such that `a1z < a100z < a2z`. // // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `false` - OrganizeImportsNumericCollation bool // !!! + OrganizeImportsNumericCollation bool `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!! // Indicates whether accents and other diacritic marks are considered unequal for the purpose of collation. When // `true`, characters with accents and other diacritics will be collated in the order defined by the locale specified // in organizeImportsCollationLocale. @@ -104,35 +111,34 @@ type UserPreferences struct { // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `true` - OrganizeImportsAccentCollation bool // !!! + OrganizeImportsAccentCollation bool `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!! // Indicates whether upper case or lower case should sort first. When `false`, the default order for the locale // specified in organizeImportsCollationLocale is used. // // This preference is ignored if: - // - organizeImportsCollation is not `unicode` - // - organizeImportsIgnoreCase is `true` - // - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive. + // - organizeImportsCollation is not `unicode` + // - organizeImportsIgnoreCase is `true` + // - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive. // // Default: `false` - OrganizeImportsCaseFirst OrganizeImportsCaseFirst // !!! + OrganizeImportsCaseFirst OrganizeImportsCaseFirst `raw:"organizeImportsCaseFirst" config:"preferences.organizeImports.caseFirst"` // !!! // Indicates where named type-only imports should sort. "inline" sorts named imports without regard to if the import is type-only. // // Default: `auto`, which defaults to `last` - OrganizeImportsTypeOrder OrganizeImportsTypeOrder // !!! + OrganizeImportsTypeOrder OrganizeImportsTypeOrder `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!! // ------- MoveToFile ------- - AllowTextChangesInNewFiles bool // !!! + AllowTextChangesInNewFiles bool `raw:"allowTextChangesInNewFiles"` // !!! // ------- Rename ------- - // renamed from `providePrefixAndSuffixTextForRename` - UseAliasesForRename core.Tristate - AllowRenameOfImportPath core.Tristate + UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"` + AllowRenameOfImportPath core.Tristate `raw:"allowRenameOfImportPath"` // ------- CodeFixes/Refactors ------- - ProvideRefactorNotApplicableReason bool // !!! + ProvideRefactorNotApplicableReason bool `raw:"provideRefactorNotApplicableReason"` // !!! // ------- InlayHints ------- @@ -144,84 +150,62 @@ type UserPreferences struct { // ------- Symbols ------- - ExcludeLibrarySymbolsInNavTo bool + ExcludeLibrarySymbolsInNavTo bool `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"` // ------- Misc ------- - DisableSuggestions bool // !!! - DisableLineTextInReferences bool // !!! - DisplayPartsForJSDoc bool // !!! - ReportStyleChecksAsWarnings bool // !!! If this changes, we need to ask the client to recompute diagnostics + DisableSuggestions bool `raw:"disableSuggestions"` // !!! + DisableLineTextInReferences bool `raw:"disableLineTextInReferences"` // !!! + DisplayPartsForJSDoc bool `raw:"displayPartsForJSDoc"` // !!! + ReportStyleChecksAsWarnings bool `raw:"reportStyleChecksAsWarnings"` // !!! If this changes, we need to ask the client to recompute diagnostics } type InlayHintsPreferences struct { - IncludeInlayParameterNameHints IncludeInlayParameterNameHints - IncludeInlayParameterNameHintsWhenArgumentMatchesName bool - IncludeInlayFunctionParameterTypeHints bool - IncludeInlayVariableTypeHints bool - IncludeInlayVariableTypeHintsWhenTypeMatchesName bool - IncludeInlayPropertyDeclarationTypeHints bool - IncludeInlayFunctionLikeReturnTypeHints bool - IncludeInlayEnumMemberValueHints bool + IncludeInlayParameterNameHints IncludeInlayParameterNameHints `raw:"includeInlayParameterNameHints" config:"inlayHints.parameterNames.enabled"` + IncludeInlayParameterNameHintsWhenArgumentMatchesName bool `raw:"includeInlayParameterNameHintsWhenArgumentMatchesName" config:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"` + IncludeInlayFunctionParameterTypeHints bool `raw:"includeInlayFunctionParameterTypeHints" config:"inlayHints.parameterTypes.enabled"` + IncludeInlayVariableTypeHints bool `raw:"includeInlayVariableTypeHints" config:"inlayHints.variableTypes.enabled"` + IncludeInlayVariableTypeHintsWhenTypeMatchesName bool `raw:"includeInlayVariableTypeHintsWhenTypeMatchesName" config:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"` + IncludeInlayPropertyDeclarationTypeHints bool `raw:"includeInlayPropertyDeclarationTypeHints" config:"inlayHints.propertyDeclarationTypes.enabled"` + IncludeInlayFunctionLikeReturnTypeHints bool `raw:"includeInlayFunctionLikeReturnTypeHints" config:"inlayHints.functionLikeReturnTypes.enabled"` + IncludeInlayEnumMemberValueHints bool `raw:"includeInlayEnumMemberValueHints" config:"inlayHints.enumMemberValues.enabled"` } type CodeLensUserPreferences struct { - ReferencesCodeLensEnabled bool - ImplementationsCodeLensEnabled bool - ReferencesCodeLensShowOnAllFunctions bool - ImplementationsCodeLensShowOnInterfaceMethods bool - ImplementationsCodeLensShowOnAllClassMethods bool + ReferencesCodeLensEnabled bool `raw:"referencesCodeLensEnabled" config:"referencesCodeLens.enabled"` + ImplementationsCodeLensEnabled bool `raw:"implementationsCodeLensEnabled" config:"implementationsCodeLens.enabled"` + ReferencesCodeLensShowOnAllFunctions bool `raw:"referencesCodeLensShowOnAllFunctions" config:"referencesCodeLens.showOnAllFunctions"` + ImplementationsCodeLensShowOnInterfaceMethods bool `raw:"implementationsCodeLensShowOnInterfaceMethods" config:"implementationsCodeLens.showOnInterfaceMethods"` + ImplementationsCodeLensShowOnAllClassMethods bool `raw:"implementationsCodeLensShowOnAllClassMethods" config:"implementationsCodeLens.showOnAllClassMethods"` +} + +type ModuleSpecifierUserPreferences struct { + ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `raw:"importModuleSpecifierPreference" config:"preferences.importModuleSpecifier"` // !!! + // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" + ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!! + AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!! } +// --- Enum Types --- + +type QuotePreference string + +const ( + QuotePreferenceUnknown QuotePreference = "" + QuotePreferenceAuto QuotePreference = "auto" + QuotePreferenceDouble QuotePreference = "double" + QuotePreferenceSingle QuotePreference = "single" +) + type JsxAttributeCompletionStyle string const ( - JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = "" // !!! + JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = "" JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto" JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces" JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none" ) -func parseJsxAttributeCompletionStyle(val any) JsxAttributeCompletionStyle { - if s, ok := val.(string); ok { - switch strings.ToLower(s) { - case "braces": - return JsxAttributeCompletionStyleBraces - case "none": - return JsxAttributeCompletionStyleNone - } - } - return JsxAttributeCompletionStyleAuto -} - -func parseImportModuleSpecifierPreference(val any) modulespecifiers.ImportModuleSpecifierPreference { - if s, ok := val.(string); ok { - switch strings.ToLower(s) { - case "project-relative": - return modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative - case "relative": - return modulespecifiers.ImportModuleSpecifierPreferenceRelative - case "non-relative": - return modulespecifiers.ImportModuleSpecifierPreferenceNonRelative - } - } - return modulespecifiers.ImportModuleSpecifierPreferenceShortest -} - -func parseImportModuleSpecifierEndingPreference(val any) modulespecifiers.ImportModuleSpecifierEndingPreference { - if s, ok := val.(string); ok { - switch strings.ToLower(s) { - case "minimal": - return modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal - case "index": - return modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex - case "js": - return modulespecifiers.ImportModuleSpecifierEndingPreferenceJs - } - } - return modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto -} - type IncludeInlayParameterNameHints string const ( @@ -230,41 +214,15 @@ const ( IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals" ) -func parseInlayParameterNameHints(val any) IncludeInlayParameterNameHints { - if prefStr, ok := val.(string); ok { - switch prefStr { - case "all": - return IncludeInlayParameterNameHintsAll - case "literals": - return IncludeInlayParameterNameHintsLiterals - } - } - return IncludeInlayParameterNameHintsNone -} - type IncludePackageJsonAutoImports string const ( - IncludePackageJsonAutoImportsUnknown IncludePackageJsonAutoImports = "" // !!! + IncludePackageJsonAutoImportsUnknown IncludePackageJsonAutoImports = "" IncludePackageJsonAutoImportsAuto IncludePackageJsonAutoImports = "auto" IncludePackageJsonAutoImportsOn IncludePackageJsonAutoImports = "on" IncludePackageJsonAutoImportsOff IncludePackageJsonAutoImports = "off" ) -func parseIncludePackageJsonAutoImports(val any) IncludePackageJsonAutoImports { - if s, ok := val.(string); ok { - switch strings.ToLower(s) { - case "on": - return IncludePackageJsonAutoImportsOn - case "off": - return IncludePackageJsonAutoImportsOff - default: - return IncludePackageJsonAutoImportsAuto - } - } - return IncludePackageJsonAutoImportsUnknown -} - type OrganizeImportsCollation bool const ( @@ -272,13 +230,6 @@ const ( OrganizeImportsCollationUnicode OrganizeImportsCollation = true ) -func parseOrganizeImportsCollation(val any) OrganizeImportsCollation { - if b, ok := val.(string); ok && strings.ToLower(b) == "unicode" { - return OrganizeImportsCollationUnicode - } - return OrganizeImportsCollationOrdinal -} - type OrganizeImportsCaseFirst int const ( @@ -287,18 +238,6 @@ const ( OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2 ) -func parseOrganizeImportsCaseFirst(caseFirst any) OrganizeImportsCaseFirst { - if caseFirstStr, ok := caseFirst.(string); ok { - switch caseFirstStr { - case "lower": - return OrganizeImportsCaseFirstLower - case "upper": - return OrganizeImportsCaseFirstUpper - } - } - return OrganizeImportsCaseFirstFalse -} - type OrganizeImportsTypeOrder int const ( @@ -308,414 +247,503 @@ const ( OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 3 ) -func parseOrganizeImportsTypeOrder(typeOrder any) OrganizeImportsTypeOrder { - if typeOrderStr, ok := typeOrder.(string); ok { - switch typeOrderStr { - case "last": - return OrganizeImportsTypeOrderLast - case "inline": - return OrganizeImportsTypeOrderInline - case "first": - return OrganizeImportsTypeOrderFirst +// --- Reflection-based parsing infrastructure --- + +// typeParsers maps reflect.Type to a function that parses a value into that type. +var typeParsers = map[reflect.Type]func(any) any{ + reflect.TypeFor[core.Tristate](): func(val any) any { + if b, ok := val.(bool); ok { + if b { + return core.TSTrue + } + return core.TSFalse } - } - return OrganizeImportsTypeOrderAuto + return core.TSUnknown + }, + reflect.TypeFor[QuotePreference](): func(val any) any { + if s, ok := val.(string); ok { + switch strings.ToLower(s) { + case "auto": + return QuotePreferenceAuto + case "double": + return QuotePreferenceDouble + case "single": + return QuotePreferenceSingle + } + } + return QuotePreferenceUnknown + }, + reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any { + if s, ok := val.(string); ok { + switch strings.ToLower(s) { + case "braces": + return JsxAttributeCompletionStyleBraces + case "none": + return JsxAttributeCompletionStyleNone + } + } + return JsxAttributeCompletionStyleAuto + }, + reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any { + if s, ok := val.(string); ok { + switch s { + case "all": + return IncludeInlayParameterNameHintsAll + case "literals": + return IncludeInlayParameterNameHintsLiterals + } + } + return IncludeInlayParameterNameHintsNone + }, + reflect.TypeFor[IncludePackageJsonAutoImports](): func(val any) any { + if s, ok := val.(string); ok { + switch strings.ToLower(s) { + case "on": + return IncludePackageJsonAutoImportsOn + case "off": + return IncludePackageJsonAutoImportsOff + default: + return IncludePackageJsonAutoImportsAuto + } + } + return IncludePackageJsonAutoImportsUnknown + }, + reflect.TypeFor[OrganizeImportsCollation](): func(val any) any { + if s, ok := val.(string); ok && strings.ToLower(s) == "unicode" { + return OrganizeImportsCollationUnicode + } + return OrganizeImportsCollationOrdinal + }, + reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any { + if s, ok := val.(string); ok { + switch s { + case "lower": + return OrganizeImportsCaseFirstLower + case "upper": + return OrganizeImportsCaseFirstUpper + } + } + return OrganizeImportsCaseFirstFalse + }, + reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any { + if s, ok := val.(string); ok { + switch s { + case "last": + return OrganizeImportsTypeOrderLast + case "inline": + return OrganizeImportsTypeOrderInline + case "first": + return OrganizeImportsTypeOrderFirst + } + } + return OrganizeImportsTypeOrderAuto + }, + reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any { + if s, ok := val.(string); ok { + switch strings.ToLower(s) { + case "project-relative": + return modulespecifiers.ImportModuleSpecifierPreferenceProjectRelative + case "relative": + return modulespecifiers.ImportModuleSpecifierPreferenceRelative + case "non-relative": + return modulespecifiers.ImportModuleSpecifierPreferenceNonRelative + } + } + return modulespecifiers.ImportModuleSpecifierPreferenceShortest + }, + reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any { + if s, ok := val.(string); ok { + switch strings.ToLower(s) { + case "minimal": + return modulespecifiers.ImportModuleSpecifierEndingPreferenceMinimal + case "index": + return modulespecifiers.ImportModuleSpecifierEndingPreferenceIndex + case "js": + return modulespecifiers.ImportModuleSpecifierEndingPreferenceJs + } + } + return modulespecifiers.ImportModuleSpecifierEndingPreferenceAuto + }, } -type QuotePreference string - -const ( - QuotePreferenceUnknown QuotePreference = "" - QuotePreferenceAuto QuotePreference = "auto" - QuotePreferenceDouble QuotePreference = "double" - QuotePreferenceSingle QuotePreference = "single" -) - -func parseQuotePreference(val any) QuotePreference { - if s, ok := val.(string); ok { - switch strings.ToLower(s) { - case "auto": - return QuotePreferenceAuto - case "double": - return QuotePreferenceDouble - case "single": - return QuotePreferenceSingle +// typeSerializers maps reflect.Type to a function that serializes a value of that type. +// For types which do not serialize as-is (tristate, enums, etc). +var typeSerializers = map[reflect.Type]func(any) any{ + reflect.TypeFor[core.Tristate](): func(val any) any { + switch val.(core.Tristate) { + case core.TSTrue: + return true + case core.TSFalse: + return false + default: + return nil } - } - return QuotePreferenceUnknown + }, + reflect.TypeFor[OrganizeImportsCollation](): func(val any) any { + if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode { + return "unicode" + } + return "ordinal" + }, + reflect.TypeFor[OrganizeImportsCaseFirst](): func(val any) any { + switch val.(OrganizeImportsCaseFirst) { + case OrganizeImportsCaseFirstLower: + return "lower" + case OrganizeImportsCaseFirstUpper: + return "upper" + default: + return "default" + } + }, + reflect.TypeFor[OrganizeImportsTypeOrder](): func(val any) any { + switch val.(OrganizeImportsTypeOrder) { + case OrganizeImportsTypeOrderLast: + return "last" + case OrganizeImportsTypeOrderInline: + return "inline" + case OrganizeImportsTypeOrderFirst: + return "first" + default: + return "auto" + } + }, } -func (p *UserPreferences) Copy() *UserPreferences { - if p == nil { - return nil - } - prefCopy := *p - prefCopy.AutoImportSpecifierExcludeRegexes = slices.Clone(p.AutoImportSpecifierExcludeRegexes) - prefCopy.AutoImportFileExcludePatterns = slices.Clone(p.AutoImportFileExcludePatterns) - return &prefCopy +type fieldInfo struct { + rawName string // raw name for unstable section lookup (e.g., "quotePreference") + configPath string // dotted path for config (e.g., "preferences.quoteStyle") + fieldPath []int // index path to field in struct + rawInvert bool // whether to invert boolean values for raw name + configInvert bool // whether to invert boolean values for config path } -func (p *UserPreferences) CopyOrDefault() *UserPreferences { - if p == nil { - return NewDefaultUserPreferences() +var fieldInfoCache = sync.OnceValue(func() []fieldInfo { + return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil) +}) + +// unstableNameIndex maps raw names to fieldInfo index for unstable section lookup. +var unstableNameIndex = sync.OnceValue(func() map[string]int { + infos := fieldInfoCache() + index := make(map[string]int, len(infos)) + for i, info := range infos { + if info.rawName != "" { + index[info.rawName] = i + } } - return p.Copy() -} + return index +}) -func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { - return modulespecifiers.UserPreferences{ - ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, - ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding, - AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, - } -} +func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo { + var infos []fieldInfo + for i := range t.NumField() { + field := t.Field(i) + currentPath := append(slices.Clone(indexPath), i) -// ------ Parsing Config Response ------- + rawTag := field.Tag.Get("raw") + configTag := field.Tag.Get("config") -// returns non-nil if should break loop -func (p *UserPreferences) Parse(item any) *UserPreferences { - if item == nil { - // continue - } else if config, ok := item.(map[string]any); ok { - p.parseWorker(config) - } else if item, ok := item.(*UserPreferences); ok { - // case for fourslash - return item.CopyOrDefault() - } - return nil -} + if rawTag == "" && configTag == "" { + // Embedded struct without tags - recurse into it + if field.Type.Kind() == reflect.Struct { + infos = append(infos, collectFieldInfos(field.Type, currentPath)...) + continue + } + panic("raw or vscode tag required for field " + field.Name) + } -func (p *UserPreferences) parseWorker(config map[string]any) { - // Process unstable preferences first so that they do not overwrite stable properties - if unstable, ok := config["unstable"]; ok { - // unstable properties must be named the same as userPreferences - p.parseAll(unstable) - } - for name, values := range config { - switch name { - case "unstable": - continue - case "inlayHints": - p.parseInlayHints(values) - case "referencesCodeLens": - p.parseReferencesCodeLens(values) - case "implementationsCodeLens": - p.parseImplementationsCodeLens(values) - case "suggest": - p.parseSuggest(values) - case "preferences": - p.parsePreferences(values) - case "workspaceSymbols": - p.parseWorkspaceSymbols(values) - case "format": - // !!! - case "tsserver": - // !!! - case "tsc": - // !!! - case "experimental": - // !!! - default: - p.set(name, values) + info := fieldInfo{ + fieldPath: currentPath, } - } -} -func (p *UserPreferences) parseAll(prefs any) { - prefsMap, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range prefsMap { - p.set(name, value) - } -} + // Parse raw tag: "name" or "name,invert" + if rawTag != "" { + parts := strings.Split(rawTag, ",") + info.rawName = parts[0] + for _, part := range parts[1:] { + if part == "invert" { + info.rawInvert = true + } + } + } -func (p *UserPreferences) parseInlayHints(prefs any) { - inlayHintsPreferences, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range inlayHintsPreferences { - if v, ok := value.(map[string]any); ok { - // vscode's inlay hints settings are nested objects with "enabled" and other properties - switch name { - case "parameterNames": - if enabled, ok := v["enabled"]; ok { - p.set("includeInlayParameterNameHints", enabled) + // Parse config tag: "path.to.setting" or "path.to.setting,invert" + if configTag != "" { + parts := strings.Split(configTag, ",") + info.configPath = parts[0] + for _, part := range parts[1:] { + if part == "invert" { + info.configInvert = true } - p.InlayHints.IncludeInlayParameterNameHintsWhenArgumentMatchesName = parseSuppress(v, "suppressWhenArgumentMatchesName") - case "parameterTypes": - p.InlayHints.IncludeInlayFunctionParameterTypeHints = parseEnabledBool(v) - case "variableTypes": - p.InlayHints.IncludeInlayVariableTypeHints = parseEnabledBool(v) - p.InlayHints.IncludeInlayVariableTypeHintsWhenTypeMatchesName = parseSuppress(v, "suppressWhenTypeMatchesName") - case "propertyDeclarationTypes": - p.InlayHints.IncludeInlayPropertyDeclarationTypeHints = parseEnabledBool(v) - case "functionLikeReturnTypes": - p.InlayHints.IncludeInlayFunctionLikeReturnTypeHints = parseEnabledBool(v) - case "enumMemberValues": - p.InlayHints.IncludeInlayEnumMemberValueHints = parseEnabledBool(v) } - } else { - // non-vscode case - p.set(name, v) } + + infos = append(infos, info) } + return infos } -func (p *UserPreferences) parseReferencesCodeLens(prefs any) { - referencesCodeLens, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range referencesCodeLens { - switch name { - case "enabled": - p.set("referencesCodeLensEnabled", value) - case "showOnAllFunctions": - p.set("referencesCodeLensShowOnAllFunctions", value) +func getNestedValue(config map[string]any, path string) (any, bool) { + parts := strings.Split(path, ".") + current := any(config) + for _, part := range parts { + m, ok := current.(map[string]any) + if !ok { + return nil, false + } + current, ok = m[part] + if !ok { + return nil, false } } + return current, true } -func (p *UserPreferences) parseImplementationsCodeLens(prefs any) { - implementationsCodeLens, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range implementationsCodeLens { - switch name { - case "enabled": - p.set("implementationsCodeLensEnabled", value) - case "showOnInterfaceMethods": - p.set("implementationsCodeLensShowOnInterfaceMethods", value) - case "showOnAllClassMethods": - p.set("implementationsCodeLensShowOnAllClassMethods", value) +func setNestedValue(config map[string]any, path string, value any) { + parts := strings.Split(path, ".") + current := config + for _, part := range parts[:len(parts)-1] { + next, ok := current[part].(map[string]any) + if !ok { + next = make(map[string]any) + current[part] = next } + current = next } + current[parts[len(parts)-1]] = value } -func (p *UserPreferences) parseSuggest(prefs any) { - completionsPreferences, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range completionsPreferences { - switch name { - case "autoImports": - p.set("includeCompletionsForModuleExports", value) - case "objectLiteralMethodSnippets": - if v, ok := value.(map[string]any); ok { - p.set("includeCompletionsWithObjectLiteralMethodSnippets", parseEnabledBool(v)) +func (p *UserPreferences) parseWorker(config map[string]any) { + v := reflect.ValueOf(p).Elem() + infos := fieldInfoCache() + + // Process "unstable" section first - allows any field to be set by raw name. + // This mirrors VS Code's behavior: { ...config.get('unstable'), ...stableOptions } + // where stable options are spread after and take precedence. + if unstable, ok := config["unstable"].(map[string]any); ok { + index := unstableNameIndex() + for name, value := range unstable { + if idx, found := index[name]; found { + info := infos[idx] + field := getFieldByPath(v, info.fieldPath) + if info.rawInvert { + if b, ok := value.(bool); ok { + value = !b + } + } + setFieldFromValue(field, value) } - case "classMemberSnippets": - if v, ok := value.(map[string]any); ok { - p.set("includeCompletionsWithClassMemberSnippets", parseEnabledBool(v)) + } + } + + // Process path-based config (VS Code style nested paths). + // These run after unstable, so stable config values take precedence. + for _, info := range infos { + if info.configPath == "" { + continue + } + val, ok := getNestedValue(config, info.configPath) + if !ok { + continue + } + + field := getFieldByPath(v, info.fieldPath) + if info.configInvert { + if b, ok := val.(bool); ok { + val = !b } - case "includeAutomaticOptionalChainCompletions": - p.set("includeAutomaticOptionalChainCompletions", value) - case "includeCompletionsForImportStatements": - p.set("includeCompletionsForImportStatements", value) } + setFieldFromValue(field, val) } } -func (p *UserPreferences) parsePreferences(prefs any) { - prefsMap, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range prefsMap { - if name == "organizeImports" { - p.parseOrganizeImportsPreferences(value) - } else { - p.set(name, value) - } +func getFieldByPath(v reflect.Value, path []int) reflect.Value { + for _, idx := range path { + v = v.Field(idx) } + return v } -func (p *UserPreferences) parseOrganizeImportsPreferences(prefs any) { - // !!! this used to be in the typescript-language-features extension - prefsMap, ok := prefs.(map[string]any) - if !ok { +func setFieldFromValue(field reflect.Value, val any) { + if val == nil { return } - if typeOrder, ok := prefsMap["typeOrder"]; ok { - p.set("organizeimportstypeorder", parseOrganizeImportsTypeOrder(typeOrder)) + + // Check custom parsers first (for types like Tristate, OrganizeImportsCollation, etc.) + if parser, ok := typeParsers[field.Type()]; ok { + field.Set(reflect.ValueOf(parser(val))) + return } - if caseSensitivity, ok := prefsMap["caseSensitivity"]; ok { - if caseSensitivityStr, ok := caseSensitivity.(string); ok { - // default is already "auto" - switch caseSensitivityStr { - case "caseInsensitive": - p.OrganizeImportsIgnoreCase = core.TSTrue - case "caseSensitive": - p.OrganizeImportsIgnoreCase = core.TSFalse + + switch field.Kind() { + case reflect.Bool: + if b, ok := val.(bool); ok { + field.SetBool(b) + } + case reflect.Int: + switch v := val.(type) { + case int: + field.SetInt(int64(v)) + case float64: + field.SetInt(int64(v)) + } + case reflect.String: + if s, ok := val.(string); ok { + field.SetString(s) + } + case reflect.Slice: + if arr, ok := val.([]any); ok { + result := reflect.MakeSlice(field.Type(), 0, len(arr)) + for _, item := range arr { + if s, ok := item.(string); ok { + result = reflect.Append(result, reflect.ValueOf(s)) + } } + field.Set(result) } } - if collation, ok := prefsMap["unicodeCollation"]; ok { - // The rest of the settings are only applicable when using unicode collation - if collationStr, ok := collation.(string); ok && collationStr == "unicode" { - p.set("organizeimportscollation", OrganizeImportsCollationUnicode) - if locale, ok := prefsMap["locale"]; ok { - p.set("organizeimportslocale", locale) - } - if numeric, ok := prefsMap["numericCollation"]; ok { - p.set("organizeimportsnumericcollation", numeric) - } - if accent, ok := prefsMap["accentCollation"]; ok { - p.set("organizeimportsaccentcollation", accent) +} + +func (p *UserPreferences) MarshalJSONTo(enc *jsontext.Encoder) error { + config := make(map[string]any) + v := reflect.ValueOf(p).Elem() + + for _, info := range fieldInfoCache() { + field := getFieldByPath(v, info.fieldPath) + + val := serializeField(field) + if val == nil { + continue + } + + // Prefer config path if available, otherwise use unstable section + if info.configPath != "" { + if info.configInvert { + if b, ok := val.(bool); ok { + val = !b + } } - if caseFirst, ok := prefsMap["caseFirst"]; ok && !p.OrganizeImportsIgnoreCase.IsTrue() { - p.set("organizeimportscasefirst", caseFirst) + setNestedValue(config, info.configPath, val) + } else if info.rawName != "" { + if info.rawInvert { + if b, ok := val.(bool); ok { + val = !b + } } + setNestedValue(config, "unstable."+info.rawName, val) } } + + return json.MarshalEncode(enc, config, json.Deterministic(true)) } -func (p *UserPreferences) parseWorkspaceSymbols(prefs any) { - symbolPreferences, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range symbolPreferences { - switch name { - // !!! scope - case "excludeLibrarySymbols": - p.ExcludeLibrarySymbolsInNavTo = parseBoolWithDefault(value, true) - default: - p.set(name, value) +func serializeField(field reflect.Value) any { + // Check custom serializers first (for types like Tristate, OrganizeImportsCollation, etc.) + if serializer, ok := typeSerializers[field.Type()]; ok { + return serializer(field.Interface()) + } + + switch field.Kind() { + case reflect.Bool: + return field.Bool() + case reflect.Int: + return int(field.Int()) + case reflect.String: + return field.String() + case reflect.Slice: + if field.IsNil() { + return nil } + result := make([]string, field.Len()) + for i := range field.Len() { + result[i] = field.Index(i).String() + } + return result + default: + return field.Interface() } } -func parseEnabledBool(v map[string]any) bool { - // vscode nested option - if enabled, ok := v["enabled"]; ok { - if e, ok := enabled.(bool); ok { - return e - } +func (p *UserPreferences) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + var config map[string]any + if err := json.UnmarshalDecode(dec, &config); err != nil { + return err } - return false + // Start with defaults, then overlay parsed values + *p = *DefaultUserPreferences.Copy() + p.parseWorker(config) + return nil +} + +// --- Helper methods --- + +func deepCopy[T any](src T) T { + var dst T + deepCopyValue(reflect.ValueOf(&dst).Elem(), reflect.ValueOf(src)) + return dst } -func parseSuppress(v map[string]any, name string) bool { - // vscode nested option - if val, ok := v[name]; ok { - if suppress, ok := val.(bool); ok { - return !suppress +func deepCopyValue(dst, src reflect.Value) { + switch src.Kind() { + case reflect.Pointer: + if src.IsNil() { + dst.SetZero() + return + } + dst.Set(reflect.New(src.Type().Elem())) + deepCopyValue(dst.Elem(), src.Elem()) + case reflect.Struct: + for i := range src.NumField() { + deepCopyValue(dst.Field(i), src.Field(i)) + } + case reflect.Slice: + if src.IsNil() { + dst.SetZero() + return + } + dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Len())) + for i := range src.Len() { + deepCopyValue(dst.Index(i), src.Index(i)) + } + case reflect.Map: + if src.IsNil() { + dst.SetZero() + return + } + dst.Set(reflect.MakeMapWithSize(src.Type(), src.Len())) + for _, key := range src.MapKeys() { + val := src.MapIndex(key) + copiedVal := reflect.New(val.Type()).Elem() + deepCopyValue(copiedVal, val) + dst.SetMapIndex(key, copiedVal) } + default: + dst.Set(src) } - return false } -func parseBoolWithDefault(val any, defaultV bool) bool { - if v, ok := val.(bool); ok { - return v - } - return defaultV +func (p *UserPreferences) Copy() *UserPreferences { + return deepCopy(p) } -func parseIntWithDefault(val any, defaultV int) int { - if v, ok := val.(int); ok { - return v - } - return defaultV +func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { + return modulespecifiers.UserPreferences(p.ModuleSpecifier) } -func (p *UserPreferences) set(name string, value any) { - switch strings.ToLower(name) { - case "quotePreference": - p.QuotePreference = parseQuotePreference(value) - case "lazyconfiguredprojectsfromexternalproject": - p.LazyConfiguredProjectsFromExternalProject = parseBoolWithDefault(value, false) - case "maximumhoverlength": - p.MaximumHoverLength = parseIntWithDefault(value, 500) - case "includecompletionsformoduleexports": - p.IncludeCompletionsForModuleExports = tsoptions.ParseTristate(value) - case "includecompletionsforimportstatements": - p.IncludeCompletionsForImportStatements = tsoptions.ParseTristate(value) - case "includeautomaticoptionalchaincompletions": - p.IncludeAutomaticOptionalChainCompletions = tsoptions.ParseTristate(value) - case "includecompletionswithsnippettext": - p.IncludeCompletionsWithSnippetText = tsoptions.ParseTristate(value) - case "includecompletionswithclassmembersnippets": - p.IncludeCompletionsWithClassMemberSnippets = tsoptions.ParseTristate(value) - case "includecompletionswithobjectliteralmethodsnippets": - p.IncludeCompletionsWithObjectLiteralMethodSnippets = tsoptions.ParseTristate(value) - case "jsxattributecompletionstyle": - p.JsxAttributeCompletionStyle = parseJsxAttributeCompletionStyle(value) - case "importmodulespecifierpreference": - p.ImportModuleSpecifierPreference = parseImportModuleSpecifierPreference(value) - case "importmodulespecifierending": - p.ImportModuleSpecifierEnding = parseImportModuleSpecifierEndingPreference(value) - case "includepackagejsonautoimports": - p.IncludePackageJsonAutoImports = parseIncludePackageJsonAutoImports(value) - case "autoimportspecifierexcluderegexes": - p.AutoImportSpecifierExcludeRegexes = tsoptions.ParseStringArray(value) - case "autoimportfileexcludepatterns": - p.AutoImportFileExcludePatterns = tsoptions.ParseStringArray(value) - case "prefertypeonlyautoimports": - p.PreferTypeOnlyAutoImports = tsoptions.ParseTristate(value) - case "organizeimportsignorecase": - p.OrganizeImportsIgnoreCase = tsoptions.ParseTristate(value) - case "organizeimportscollation": - p.OrganizeImportsCollation = parseOrganizeImportsCollation(value) - case "organizeimportslocale": - p.OrganizeImportsLocale = tsoptions.ParseString(value) - case "organizeimportsnumericcollation": - p.OrganizeImportsNumericCollation = parseBoolWithDefault(value, false) - case "organizeimportsaccentcollation": - p.OrganizeImportsAccentCollation = parseBoolWithDefault(value, true) - case "organizeimportscasefirst": - p.OrganizeImportsCaseFirst = parseOrganizeImportsCaseFirst(value) - case "organizeimportstypeorder": - p.OrganizeImportsTypeOrder = parseOrganizeImportsTypeOrder(value) - case "allowtextchangesinnewfiles": - p.AllowTextChangesInNewFiles = parseBoolWithDefault(value, true) // !!! - case "usealiasesforrename", "provideprefixandsuffixtextforrename": - p.UseAliasesForRename = tsoptions.ParseTristate(value) - case "allowrenameofimportpath": - p.AllowRenameOfImportPath = tsoptions.ParseTristate(value) - case "providerefactornotapplicablereason": - p.ProvideRefactorNotApplicableReason = parseBoolWithDefault(value, true) - case "includeinlayparameternamehints": - p.InlayHints.IncludeInlayParameterNameHints = parseInlayParameterNameHints(value) - case "includeinlayparameternamehintswhenargumentmatchesname": - p.InlayHints.IncludeInlayParameterNameHintsWhenArgumentMatchesName = parseBoolWithDefault(value, false) - case "includeinlayfunctionparametertypehints": - p.InlayHints.IncludeInlayFunctionParameterTypeHints = parseBoolWithDefault(value, false) - case "includeinlayvariabletypehints": - p.InlayHints.IncludeInlayVariableTypeHints = parseBoolWithDefault(value, false) - case "includeinlayvariabletypehintswhentypematchesname": - p.InlayHints.IncludeInlayVariableTypeHintsWhenTypeMatchesName = parseBoolWithDefault(value, false) - case "includeinlaypropertydeclarationtypehints": - p.InlayHints.IncludeInlayPropertyDeclarationTypeHints = parseBoolWithDefault(value, false) - case "includeinlayfunctionlikereturntypehints": - p.InlayHints.IncludeInlayFunctionLikeReturnTypeHints = parseBoolWithDefault(value, false) - case "includeinlayenummembervaluehints": - p.InlayHints.IncludeInlayEnumMemberValueHints = parseBoolWithDefault(value, false) - case "excludelibrarysymbolsinnavto": - p.ExcludeLibrarySymbolsInNavTo = parseBoolWithDefault(value, true) - case "disablesuggestions": - p.DisableSuggestions = parseBoolWithDefault(value, false) - case "disablelinetextinreferences": - p.DisableLineTextInReferences = parseBoolWithDefault(value, true) - case "displaypartsforjsdoc": - p.DisplayPartsForJSDoc = parseBoolWithDefault(value, true) - case "reportstylechecksaswarnings": - p.ReportStyleChecksAsWarnings = parseBoolWithDefault(value, true) - case "referencescodelensenabled": - p.CodeLens.ReferencesCodeLensEnabled = parseBoolWithDefault(value, false) - case "implementationscodelensenabled": - p.CodeLens.ImplementationsCodeLensEnabled = parseBoolWithDefault(value, false) - case "referencescodelensshowonallfunctions": - p.CodeLens.ReferencesCodeLensShowOnAllFunctions = parseBoolWithDefault(value, false) - case "implementationscodelensshowoninterfacemethods": - p.CodeLens.ImplementationsCodeLensShowOnInterfaceMethods = parseBoolWithDefault(value, false) - case "implementationscodelensshowonallclassmethods": - p.CodeLens.ImplementationsCodeLensShowOnAllClassMethods = parseBoolWithDefault(value, false) +// ParseUserPreferences parses user preferences from a config map or returns existing preferences. +// For config maps: returns a fresh *UserPreferences with defaults applied, then overlaid with parsed values. +// For *UserPreferences: returns the same pointer (caller should not mutate). +// Returns nil if item is nil or unrecognized type. +func ParseUserPreferences(item any) *UserPreferences { + if item == nil { + return nil + } + if config, ok := item.(map[string]any); ok { + p := DefaultUserPreferences.Copy() + p.parseWorker(config) + return p } + if prefs, ok := item.(*UserPreferences); ok { + return prefs + } + return nil } diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go new file mode 100644 index 0000000000..817beb0ee7 --- /dev/null +++ b/internal/ls/lsutil/userpreferences_test.go @@ -0,0 +1,330 @@ +package lsutil + +import ( + "reflect" + "testing" + + "github.com/go-json-experiment/json" + "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "gotest.tools/v3/assert" +) + +func fillNonZeroValues(v reflect.Value) { + t := v.Type() + for i := range t.NumField() { + field := v.Field(i) + if !field.CanSet() { + continue + } + switch field.Kind() { + case reflect.Bool: + field.SetBool(true) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + field.SetInt(1) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + field.SetUint(1) + case reflect.String: + val := getValidStringValue(field.Type()) + field.SetString(val) + case reflect.Slice: + if field.Type().Elem().Kind() == reflect.String { + field.Set(reflect.ValueOf([]string{"test"})) + } + case reflect.Struct: + fillNonZeroValues(field) + } + } +} + +func getValidStringValue(t reflect.Type) string { + typeName := t.String() + switch typeName { + case "lsutil.QuotePreference": + return string(QuotePreferenceSingle) + case "lsutil.JsxAttributeCompletionStyle": + return string(JsxAttributeCompletionStyleBraces) + case "lsutil.IncludePackageJsonAutoImports": + return string(IncludePackageJsonAutoImportsOn) + case "lsutil.IncludeInlayParameterNameHints": + return string(IncludeInlayParameterNameHintsAll) + case "modulespecifiers.ImportModuleSpecifierPreference": + return string(modulespecifiers.ImportModuleSpecifierPreferenceRelative) + case "modulespecifiers.ImportModuleSpecifierEndingPreference": + return string(modulespecifiers.ImportModuleSpecifierEndingPreferenceJs) + default: + return "test" + } +} + +func TestUserPreferencesRoundtrip(t *testing.T) { + t.Parallel() + + original := &UserPreferences{} + fillNonZeroValues(reflect.ValueOf(original).Elem()) + + jsonBytes, err := json.Marshal(original) + assert.NilError(t, err) + + t.Run("UnmarshalJSONFrom", func(t *testing.T) { + t.Parallel() + parsed := &UserPreferences{} + err2 := json.Unmarshal(jsonBytes, parsed) + assert.NilError(t, err2) + assert.DeepEqual(t, original, parsed) + }) + + t.Run("parseWorker", func(t *testing.T) { + t.Parallel() + var config map[string]any + err2 := json.Unmarshal(jsonBytes, &config) + assert.NilError(t, err2) + parsed := &UserPreferences{} + parsed.parseWorker(config) + assert.DeepEqual(t, original, parsed) + }) +} + +func TestUserPreferencesSerialize(t *testing.T) { + t.Parallel() + + t.Run("config path field serializes to nested path", func(t *testing.T) { + t.Parallel() + prefs := &UserPreferences{ + QuotePreference: QuotePreferenceSingle, + } + jsonBytes, err := json.Marshal(prefs) + assert.NilError(t, err) + + var actual map[string]any + err = json.Unmarshal(jsonBytes, &actual) + assert.NilError(t, err) + + preferences := actual["preferences"].(map[string]any) + assert.Equal(t, "single", preferences["quoteStyle"]) + }) + + t.Run("raw-only field serializes to unstable section", func(t *testing.T) { + t.Parallel() + prefs := &UserPreferences{ + DisableSuggestions: true, + } + jsonBytes, err := json.Marshal(prefs) + assert.NilError(t, err) + + var actual map[string]any + err = json.Unmarshal(jsonBytes, &actual) + assert.NilError(t, err) + + unstable := actual["unstable"].(map[string]any) + assert.Equal(t, true, unstable["disableSuggestions"]) + }) + + t.Run("inlay hint inversion on serialize", func(t *testing.T) { + t.Parallel() + prefs := &UserPreferences{ + InlayHints: InlayHintsPreferences{ + IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll, + IncludeInlayParameterNameHintsWhenArgumentMatchesName: true, + }, + } + jsonBytes, err := json.Marshal(prefs) + assert.NilError(t, err) + + var actual map[string]any + err = json.Unmarshal(jsonBytes, &actual) + assert.NilError(t, err) + + inlayHints := actual["inlayHints"].(map[string]any) + parameterNames := inlayHints["parameterNames"].(map[string]any) + assert.Equal(t, "all", parameterNames["enabled"]) + assert.Equal(t, false, parameterNames["suppressWhenArgumentMatchesName"]) // inverted + }) + + t.Run("mixed config and unstable fields", func(t *testing.T) { + t.Parallel() + prefs := &UserPreferences{ + QuotePreference: QuotePreferenceSingle, + DisableSuggestions: true, + DisplayPartsForJSDoc: true, + } + jsonBytes, err := json.Marshal(prefs) + assert.NilError(t, err) + + var actual map[string]any + err = json.Unmarshal(jsonBytes, &actual) + assert.NilError(t, err) + + preferences := actual["preferences"].(map[string]any) + assert.Equal(t, "single", preferences["quoteStyle"]) + + unstable := actual["unstable"].(map[string]any) + assert.Equal(t, true, unstable["disableSuggestions"]) + assert.Equal(t, true, unstable["displayPartsForJSDoc"]) + }) +} + +func TestUserPreferencesParseUnstable(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + expected *UserPreferences + }{ + { + name: "unstable fields with correct casing", + json: `{ + "unstable": { + "disableSuggestions": true, + "maximumHoverLength": 100, + "allowRenameOfImportPath": true + } + }`, + expected: &UserPreferences{ + DisableSuggestions: true, + MaximumHoverLength: 100, + AllowRenameOfImportPath: core.TSTrue, + }, + }, + { + name: "nested preferences path", + json: `{ + "preferences": { + "quoteStyle": "single", + "useAliasesForRenames": true + } + }`, + expected: &UserPreferences{ + QuotePreference: QuotePreferenceSingle, + UseAliasesForRename: core.TSTrue, + }, + }, + { + name: "suggest section", + json: `{ + "suggest": { + "autoImports": false, + "includeCompletionsForImportStatements": true + } + }`, + expected: &UserPreferences{ + IncludeCompletionsForModuleExports: core.TSFalse, + IncludeCompletionsForImportStatements: core.TSTrue, + }, + }, + { + name: "inlayHints with invert", + json: `{ + "inlayHints": { + "parameterNames": { + "enabled": "all", + "suppressWhenArgumentMatchesName": true + } + } + }`, + expected: &UserPreferences{ + InlayHints: InlayHintsPreferences{ + IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll, + IncludeInlayParameterNameHintsWhenArgumentMatchesName: false, // inverted + }, + }, + }, + { + name: "mixed config", + json: `{ + "unstable": { + "displayPartsForJSDoc": true + }, + "preferences": { + "importModuleSpecifier": "relative" + }, + "workspaceSymbols": { + "excludeLibrarySymbols": true + } + }`, + expected: &UserPreferences{ + DisplayPartsForJSDoc: true, + ModuleSpecifier: ModuleSpecifierUserPreferences{ + ImportModuleSpecifierPreference: modulespecifiers.ImportModuleSpecifierPreferenceRelative, + }, + ExcludeLibrarySymbolsInNavTo: true, + }, + }, + { + name: "stable config overrides unstable", + json: `{ + "unstable": { + "quotePreference": "double" + }, + "preferences": { + "quoteStyle": "single" + } + }`, + expected: &UserPreferences{ + QuotePreference: QuotePreferenceSingle, // stable wins + }, + }, + { + name: "unstable sets value when no stable config", + json: `{ + "unstable": { + "includeCompletionsWithSnippetText": false + } + }`, + expected: &UserPreferences{ + IncludeCompletionsWithSnippetText: core.TSFalse, + }, + }, + { + name: "any field can be passed via unstable by its raw name", + json: `{ + "unstable": { + "quotePreference": "double", + "includeCompletionsForModuleExports": true, + "excludeLibrarySymbolsInNavTo": true + } + }`, + expected: &UserPreferences{ + QuotePreference: QuotePreferenceDouble, + IncludeCompletionsForModuleExports: core.TSTrue, + ExcludeLibrarySymbolsInNavTo: true, + }, + }, + { + name: "TypeScript raw names work in unstable section", + json: `{ + "unstable": { + "includeCompletionsForModuleExports": true, + "quotePreference": "single", + "providePrefixAndSuffixTextForRename": true, + "includeInlayParameterNameHints": "all", + "organizeImportsLocale": "en" + } + }`, + expected: &UserPreferences{ + IncludeCompletionsForModuleExports: core.TSTrue, + QuotePreference: QuotePreferenceSingle, + UseAliasesForRename: core.TSTrue, + OrganizeImportsLocale: "en", + InlayHints: InlayHintsPreferences{ + IncludeInlayParameterNameHints: IncludeInlayParameterNameHintsAll, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var config map[string]any + err := json.Unmarshal([]byte(tt.json), &config) + assert.NilError(t, err) + + parsed := &UserPreferences{} + parsed.parseWorker(config) + + assert.DeepEqual(t, tt.expected, parsed) + }) + } +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index ad30b34eae..ce5ad5daba 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -272,8 +272,8 @@ func (s *Server) RefreshCodeLens(ctx context.Context) error { func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferences, error) { caps := lsproto.GetClientCapabilities(ctx) if !caps.Workspace.Configuration { - // if no configuration request capapbility, return default preferences - return s.session.NewUserPreferences(), nil + // if no configuration request capability, return default preferences + return lsutil.DefaultUserPreferences, nil } configs, err := sendClientRequest(ctx, s, lsproto.WorkspaceConfigurationInfo, &lsproto.ConfigurationParams{ Items: []*lsproto.ConfigurationItem{ @@ -286,13 +286,12 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferen return nil, fmt.Errorf("configure request failed: %w", err) } s.logger.Infof("configuration: %+v, %T", configs, configs) - userPreferences := s.session.NewUserPreferences() for _, item := range configs { - if parsed := userPreferences.Parse(item); parsed != nil { + if parsed := lsutil.ParseUserPreferences(item); parsed != nil { return parsed, nil } } - return userPreferences, nil + return lsutil.DefaultUserPreferences, nil } func (s *Server) Run(ctx context.Context) error { @@ -930,11 +929,9 @@ func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, para } // !!! Both the 'javascript' and 'js/ts' scopes need to be checked for settings as well. tsSettings := settings["typescript"] - userPreferences := s.session.UserPreferences() - if parsed := userPreferences.Parse(tsSettings); parsed != nil { - userPreferences = parsed + if parsed := lsutil.ParseUserPreferences(tsSettings); parsed != nil { + s.session.Configure(parsed) } - s.session.Configure(userPreferences) return nil } diff --git a/internal/project/session.go b/internal/project/session.go index 0ef5d204b9..df29446599 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -86,8 +86,6 @@ type Session struct { // released from the parseCache. programCounter *programCounter - // read-only after initialization - initialPreferences *lsutil.UserPreferences userPreferences *lsutil.UserPreferences // !!! update to Config compilerOptionsForInferredProjects *core.CompilerOptions typingsInstaller *ata.TypingsInstaller @@ -190,16 +188,11 @@ func (s *Session) GetCurrentDirectory() string { return s.options.CurrentDirectory } -// Gets current UserPreferences, always a copy +// Gets current UserPreferences func (s *Session) UserPreferences() *lsutil.UserPreferences { s.configRWMu.Lock() defer s.configRWMu.Unlock() - return s.userPreferences.Copy() -} - -// Gets original UserPreferences of the session -func (s *Session) NewUserPreferences() *lsutil.UserPreferences { - return s.initialPreferences.CopyOrDefault() + return s.userPreferences } // Trace implements module.ResolutionHost @@ -222,8 +215,7 @@ func (s *Session) Configure(userPreferences *lsutil.UserPreferences) { } func (s *Session) InitializeWithConfig(userPreferences *lsutil.UserPreferences) { - s.initialPreferences = userPreferences.CopyOrDefault() - s.Configure(s.initialPreferences) + s.Configure(userPreferences) } func (s *Session) DidOpenFile(ctx context.Context, uri lsproto.DocumentUri, version int32, content string, languageKind lsproto.LanguageKind) { @@ -706,7 +698,7 @@ func (s *Session) flushChanges(ctx context.Context) (FileChangeSummary, map[tspa var newConfig *Config if s.pendingConfigChanges { newConfig = &Config{ - tsUserPreferences: s.userPreferences.Copy(), + tsUserPreferences: s.userPreferences, } } s.pendingConfigChanges = false diff --git a/internal/project/session_test.go b/internal/project/session_test.go index b1ffed559e..b8429dcae0 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -866,7 +866,7 @@ func TestSession(t *testing.T) { session.Configure(&lsutil.UserPreferences{}) // Change user preferences for code lens and inlay hints. - newPrefs := session.UserPreferences() + newPrefs := session.UserPreferences().Copy() newPrefs.CodeLens.ReferencesCodeLensEnabled = !newPrefs.CodeLens.ReferencesCodeLensEnabled newPrefs.InlayHints.IncludeInlayFunctionLikeReturnTypeHints = !newPrefs.InlayHints.IncludeInlayFunctionLikeReturnTypeHints session.Configure(newPrefs) diff --git a/internal/project/snapshot.go b/internal/project/snapshot.go index 50889c7cbd..454f7e6c2f 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -350,7 +350,7 @@ func (s *Snapshot) Clone(ctx context.Context, change SnapshotChange, overlays ma config := s.config if change.newConfig != nil { if change.newConfig.tsUserPreferences != nil { - config.tsUserPreferences = change.newConfig.tsUserPreferences.CopyOrDefault() + config.tsUserPreferences = change.newConfig.tsUserPreferences } if change.newConfig.formatOptions != nil { config.formatOptions = change.newConfig.formatOptions diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 6425dbacf4..370748819d 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -162,55 +162,65 @@ Config File Names:: "params": { "settings": { "typescript": { - "QuotePreference": "", - "LazyConfiguredProjectsFromExternalProject": false, - "MaximumHoverLength": 0, - "IncludeCompletionsForModuleExports": null, - "IncludeCompletionsForImportStatements": null, - "IncludeAutomaticOptionalChainCompletions": null, - "IncludeCompletionsWithSnippetText": null, - "IncludeCompletionsWithClassMemberSnippets": null, - "IncludeCompletionsWithObjectLiteralMethodSnippets": null, - "JsxAttributeCompletionStyle": "", - "ImportModuleSpecifierPreference": "", - "ImportModuleSpecifierEnding": "", - "IncludePackageJsonAutoImports": "", - "AutoImportSpecifierExcludeRegexes": [], - "AutoImportFileExcludePatterns": [], - "PreferTypeOnlyAutoImports": null, - "OrganizeImportsIgnoreCase": null, - "OrganizeImportsCollation": false, - "OrganizeImportsLocale": "", - "OrganizeImportsNumericCollation": false, - "OrganizeImportsAccentCollation": false, - "OrganizeImportsCaseFirst": 0, - "OrganizeImportsTypeOrder": 0, - "AllowTextChangesInNewFiles": false, - "UseAliasesForRename": null, - "AllowRenameOfImportPath": null, - "ProvideRefactorNotApplicableReason": false, - "InlayHints": { - "IncludeInlayParameterNameHints": "", - "IncludeInlayParameterNameHintsWhenArgumentMatchesName": false, - "IncludeInlayFunctionParameterTypeHints": false, - "IncludeInlayVariableTypeHints": false, - "IncludeInlayVariableTypeHintsWhenTypeMatchesName": false, - "IncludeInlayPropertyDeclarationTypeHints": false, - "IncludeInlayFunctionLikeReturnTypeHints": false, - "IncludeInlayEnumMemberValueHints": false + "implementationsCodeLens": { + "enabled": true, + "showOnAllClassMethods": true, + "showOnInterfaceMethods": true }, - "CodeLens": { - "ReferencesCodeLensEnabled": true, - "ImplementationsCodeLensEnabled": true, - "ReferencesCodeLensShowOnAllFunctions": true, - "ImplementationsCodeLensShowOnInterfaceMethods": true, - "ImplementationsCodeLensShowOnAllClassMethods": true + "inlayHints": { + "enumMemberValues": { + "enabled": false + }, + "functionLikeReturnTypes": { + "enabled": false + }, + "parameterNames": { + "enabled": "", + "suppressWhenArgumentMatchesName": true + }, + "parameterTypes": { + "enabled": false + }, + "propertyDeclarationTypes": { + "enabled": false + }, + "variableTypes": { + "enabled": false, + "suppressWhenTypeMatchesName": true + } }, - "ExcludeLibrarySymbolsInNavTo": false, - "DisableSuggestions": false, - "DisableLineTextInReferences": false, - "DisplayPartsForJSDoc": false, - "ReportStyleChecksAsWarnings": false + "preferences": { + "importModuleSpecifier": "", + "importModuleSpecifierEnding": "", + "includePackageJsonAutoImports": "", + "jsxAttributeCompletionStyle": "", + "organizeImports": { + "accentCollation": false, + "caseFirst": "default", + "locale": "", + "numericCollation": false, + "typeOrder": "auto", + "unicodeCollation": "ordinal" + }, + "quoteStyle": "" + }, + "referencesCodeLens": { + "enabled": true, + "showOnAllFunctions": true + }, + "unstable": { + "allowTextChangesInNewFiles": false, + "disableLineTextInReferences": false, + "disableSuggestions": false, + "displayPartsForJSDoc": false, + "lazyConfiguredProjectsFromExternalProject": false, + "maximumHoverLength": 0, + "provideRefactorNotApplicableReason": false, + "reportStyleChecksAsWarnings": false + }, + "workspaceSymbols": { + "excludeLibrarySymbols": false + } } } } @@ -610,55 +620,71 @@ Config:: "params": { "settings": { "typescript": { - "QuotePreference": "", - "LazyConfiguredProjectsFromExternalProject": false, - "MaximumHoverLength": 0, - "IncludeCompletionsForModuleExports": true, - "IncludeCompletionsForImportStatements": true, - "IncludeAutomaticOptionalChainCompletions": null, - "IncludeCompletionsWithSnippetText": true, - "IncludeCompletionsWithClassMemberSnippets": null, - "IncludeCompletionsWithObjectLiteralMethodSnippets": null, - "JsxAttributeCompletionStyle": "", - "ImportModuleSpecifierPreference": "", - "ImportModuleSpecifierEnding": "", - "IncludePackageJsonAutoImports": "", - "AutoImportSpecifierExcludeRegexes": [], - "AutoImportFileExcludePatterns": [], - "PreferTypeOnlyAutoImports": null, - "OrganizeImportsIgnoreCase": null, - "OrganizeImportsCollation": false, - "OrganizeImportsLocale": "", - "OrganizeImportsNumericCollation": false, - "OrganizeImportsAccentCollation": false, - "OrganizeImportsCaseFirst": 0, - "OrganizeImportsTypeOrder": 0, - "AllowTextChangesInNewFiles": false, - "UseAliasesForRename": null, - "AllowRenameOfImportPath": true, - "ProvideRefactorNotApplicableReason": true, - "InlayHints": { - "IncludeInlayParameterNameHints": "", - "IncludeInlayParameterNameHintsWhenArgumentMatchesName": false, - "IncludeInlayFunctionParameterTypeHints": false, - "IncludeInlayVariableTypeHints": false, - "IncludeInlayVariableTypeHintsWhenTypeMatchesName": false, - "IncludeInlayPropertyDeclarationTypeHints": false, - "IncludeInlayFunctionLikeReturnTypeHints": false, - "IncludeInlayEnumMemberValueHints": false + "implementationsCodeLens": { + "enabled": false, + "showOnAllClassMethods": false, + "showOnInterfaceMethods": false + }, + "inlayHints": { + "enumMemberValues": { + "enabled": false + }, + "functionLikeReturnTypes": { + "enabled": false + }, + "parameterNames": { + "enabled": "", + "suppressWhenArgumentMatchesName": true + }, + "parameterTypes": { + "enabled": false + }, + "propertyDeclarationTypes": { + "enabled": false + }, + "variableTypes": { + "enabled": false, + "suppressWhenTypeMatchesName": true + } + }, + "preferences": { + "importModuleSpecifier": "", + "importModuleSpecifierEnding": "", + "includePackageJsonAutoImports": "", + "jsxAttributeCompletionStyle": "", + "organizeImports": { + "accentCollation": false, + "caseFirst": "default", + "locale": "", + "numericCollation": false, + "typeOrder": "auto", + "unicodeCollation": "ordinal" + }, + "quoteStyle": "" + }, + "referencesCodeLens": { + "enabled": false, + "showOnAllFunctions": false + }, + "suggest": { + "autoImports": true, + "includeCompletionsForImportStatements": true }, - "CodeLens": { - "ReferencesCodeLensEnabled": false, - "ImplementationsCodeLensEnabled": false, - "ReferencesCodeLensShowOnAllFunctions": false, - "ImplementationsCodeLensShowOnInterfaceMethods": false, - "ImplementationsCodeLensShowOnAllClassMethods": false + "unstable": { + "allowRenameOfImportPath": true, + "allowTextChangesInNewFiles": false, + "disableLineTextInReferences": true, + "disableSuggestions": false, + "displayPartsForJSDoc": true, + "includeCompletionsWithSnippetText": true, + "lazyConfiguredProjectsFromExternalProject": false, + "maximumHoverLength": 0, + "provideRefactorNotApplicableReason": true, + "reportStyleChecksAsWarnings": true }, - "ExcludeLibrarySymbolsInNavTo": true, - "DisableSuggestions": false, - "DisableLineTextInReferences": true, - "DisplayPartsForJSDoc": true, - "ReportStyleChecksAsWarnings": true + "workspaceSymbols": { + "excludeLibrarySymbols": true + } } } }