From c4286f58c114a0db3061922a3e8d8739adf9e6c5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 4 Dec 2025 20:02:58 -0800 Subject: [PATCH 01/19] Make user prefs serializable --- internal/ls/lsutil/userpreferences.go | 234 +++++++++++++++++++-- internal/ls/lsutil/userpreferences_test.go | 87 ++++++++ 2 files changed, 300 insertions(+), 21 deletions(-) create mode 100644 internal/ls/lsutil/userpreferences_test.go diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 055717bf8c..47d1c06e9b 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -4,6 +4,8 @@ import ( "slices" "strings" + "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" @@ -173,6 +175,194 @@ type CodeLensUserPreferences struct { ImplementationsCodeLensShowOnAllClassMethods bool } +type jsonWriter struct { + enc *jsontext.Encoder + err error +} + +func (w *jsonWriter) token(t jsontext.Token) { + if w.err == nil { + w.err = w.enc.WriteToken(t) + } +} + +func (w *jsonWriter) key(k string) { w.token(jsontext.String(k)) } +func (w *jsonWriter) str(s string) { w.token(jsontext.String(s)) } +func (w *jsonWriter) boolean(b bool) { w.token(jsontext.Bool(b)) } +func (w *jsonWriter) beginObject() { w.token(jsontext.BeginObject) } +func (w *jsonWriter) endObject() { w.token(jsontext.EndObject) } +func (w *jsonWriter) beginArray() { w.token(jsontext.BeginArray) } +func (w *jsonWriter) endArray() { w.token(jsontext.EndArray) } +func (w *jsonWriter) prop(k string, v string) { w.key(k); w.str(v) } +func (w *jsonWriter) boolProp(k string, v bool) { w.key(k); w.boolean(v) } + +func (w *jsonWriter) intProp(k string, v int) { + w.key(k) + if w.err == nil { + w.err = w.enc.WriteToken(jsontext.Int(int64(v))) + } +} + +func (w *jsonWriter) tristateProp(k string, v core.Tristate) { + w.key(k) + switch v { + case core.TSTrue: + w.boolean(true) + case core.TSFalse: + w.boolean(false) + default: + w.token(jsontext.Null) + } +} + +func (w *jsonWriter) stringArray(arr []string) { + w.beginArray() + for _, s := range arr { + w.str(s) + } + w.endArray() +} + +func (w *jsonWriter) object(key string, fn func()) { + w.key(key) + w.beginObject() + fn() + w.endObject() +} + +func (u *UserPreferences) MarshalJSONTo(enc *jsontext.Encoder) error { + w := &jsonWriter{enc: enc} + w.beginObject() + + w.object("unstable", func() { + w.boolProp("lazyConfiguredProjectsFromExternalProject", u.LazyConfiguredProjectsFromExternalProject) + w.intProp("maximumHoverLength", u.MaximumHoverLength) + w.tristateProp("includeCompletionsWithSnippetText", u.IncludeCompletionsWithSnippetText) + w.boolProp("allowTextChangesInNewFiles", u.AllowTextChangesInNewFiles) + w.boolProp("allowRenameOfImportPath", u.AllowRenameOfImportPath) + w.boolProp("provideRefactorNotApplicableReason", u.ProvideRefactorNotApplicableReason) + w.boolProp("disableSuggestions", u.DisableSuggestions) + w.boolProp("disableLineTextInReferences", u.DisableLineTextInReferences) + w.boolProp("displayPartsForJSDoc", u.DisplayPartsForJSDoc) + }) + + w.object("referencesCodeLens", func() { + w.boolProp("enabled", u.CodeLens.ReferencesCodeLensEnabled) + w.boolProp("showOnAllFunctions", u.CodeLens.ReferencesCodeLensShowOnAllFunctions) + }) + + w.object("implementationsCodeLens", func() { + w.boolProp("enabled", u.CodeLens.ImplementationsCodeLensEnabled) + w.boolProp("showOnInterfaceMethods", u.CodeLens.ImplementationsCodeLensShowOnInterfaceMethods) + w.boolProp("showOnAllClassMethods", u.CodeLens.ImplementationsCodeLensShowOnAllClassMethods) + }) + + w.object("inlayHints", func() { + w.object("parameterNames", func() { + enabledVal := string(u.InlayHints.IncludeInlayParameterNameHints) + if enabledVal == "" { + enabledVal = "none" + } + w.prop("enabled", enabledVal) + w.boolProp("suppressWhenArgumentMatchesName", !u.InlayHints.IncludeInlayParameterNameHintsWhenArgumentMatchesName) + }) + w.object("parameterTypes", func() { + w.boolProp("enabled", u.InlayHints.IncludeInlayFunctionParameterTypeHints) + }) + w.object("variableTypes", func() { + w.boolProp("enabled", u.InlayHints.IncludeInlayVariableTypeHints) + w.boolProp("suppressWhenTypeMatchesName", !u.InlayHints.IncludeInlayVariableTypeHintsWhenTypeMatchesName) + }) + w.object("propertyDeclarationTypes", func() { + w.boolProp("enabled", u.InlayHints.IncludeInlayPropertyDeclarationTypeHints) + }) + w.object("functionLikeReturnTypes", func() { + w.boolProp("enabled", u.InlayHints.IncludeInlayFunctionLikeReturnTypeHints) + }) + w.object("enumMemberValues", func() { + w.boolProp("enabled", u.InlayHints.IncludeInlayEnumMemberValueHints) + }) + }) + + w.object("preferences", func() { + w.prop("quoteStyle", string(u.QuotePreference)) + w.prop("importModuleSpecifier", string(u.ImportModuleSpecifierPreference)) + w.prop("importModuleSpecifierEnding", string(u.ImportModuleSpecifierEnding)) + w.prop("includePackageJsonAutoImports", string(u.IncludePackageJsonAutoImports)) + w.key("autoImportFileExcludePatterns") + w.stringArray(u.AutoImportFileExcludePatterns) + w.key("autoImportSpecifierExcludeRegexes") + w.stringArray(u.AutoImportSpecifierExcludeRegexes) + w.boolProp("preferTypeOnlyAutoImports", u.PreferTypeOnlyAutoImports) + w.prop("jsxAttributeCompletionStyle", string(u.JsxAttributeCompletionStyle)) + w.tristateProp("useAliasesForRenames", u.UseAliasesForRename) + + w.object("organizeImports", func() { + if u.OrganizeImportsIgnoreCase == core.TSTrue { + w.prop("caseSensitivity", "caseInsensitive") + } else if u.OrganizeImportsIgnoreCase == core.TSFalse { + w.prop("caseSensitivity", "caseSensitive") + } + typeOrderStr := "auto" + switch u.OrganizeImportsTypeOrder { + case OrganizeImportsTypeOrderLast: + typeOrderStr = "last" + case OrganizeImportsTypeOrderInline: + typeOrderStr = "inline" + case OrganizeImportsTypeOrderFirst: + typeOrderStr = "first" + } + w.prop("typeOrder", typeOrderStr) + if u.OrganizeImportsCollation == OrganizeImportsCollationUnicode { + w.prop("unicodeCollation", "unicode") + } else { + w.prop("unicodeCollation", "ordinal") + } + w.prop("locale", u.OrganizeImportsLocale) + w.boolProp("numericCollation", u.OrganizeImportsNumericCollation) + w.boolProp("accentCollation", u.OrganizeImportsAccentCollation) + caseFirstStr := "default" + switch u.OrganizeImportsCaseFirst { + case OrganizeImportsCaseFirstLower: + caseFirstStr = "lower" + case OrganizeImportsCaseFirstUpper: + caseFirstStr = "upper" + } + w.prop("caseFirst", caseFirstStr) + }) + }) + + w.object("suggest", func() { + w.tristateProp("autoImports", u.IncludeCompletionsForModuleExports) + w.tristateProp("includeCompletionsForImportStatements", u.IncludeCompletionsForImportStatements) + w.tristateProp("includeAutomaticOptionalChainCompletions", u.IncludeAutomaticOptionalChainCompletions) + w.object("classMemberSnippets", func() { + w.tristateProp("enabled", u.IncludeCompletionsWithClassMemberSnippets) + }) + w.object("objectLiteralMethodSnippets", func() { + w.tristateProp("enabled", u.IncludeCompletionsWithObjectLiteralMethodSnippets) + }) + }) + + w.object("workspaceSymbols", func() { + w.boolProp("excludeLibrarySymbols", u.ExcludeLibrarySymbolsInNavTo) + }) + + w.boolProp("reportStyleChecksAsWarnings", u.ReportStyleChecksAsWarnings) + + w.endObject() + return w.err +} + +func (u *UserPreferences) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + var config map[string]any + if err := json.UnmarshalDecode(dec, &config); err != nil { + return err + } + u.parseWorker(config) + return nil +} + type JsxAttributeCompletionStyle string const ( @@ -507,11 +697,11 @@ func (p *UserPreferences) parseSuggest(prefs any) { p.set("includeCompletionsForModuleExports", value) case "objectLiteralMethodSnippets": if v, ok := value.(map[string]any); ok { - p.set("includeCompletionsWithObjectLiteralMethodSnippets", parseEnabledBool(v)) + p.set("includeCompletionsWithObjectLiteralMethodSnippets", v["enabled"]) } case "classMemberSnippets": if v, ok := value.(map[string]any); ok { - p.set("includeCompletionsWithClassMemberSnippets", parseEnabledBool(v)) + p.set("includeCompletionsWithClassMemberSnippets", v["enabled"]) } case "includeAutomaticOptionalChainCompletions": p.set("includeAutomaticOptionalChainCompletions", value) @@ -542,7 +732,7 @@ func (p *UserPreferences) parseOrganizeImportsPreferences(prefs any) { return } if typeOrder, ok := prefsMap["typeOrder"]; ok { - p.set("organizeimportstypeorder", parseOrganizeImportsTypeOrder(typeOrder)) + p.OrganizeImportsTypeOrder = parseOrganizeImportsTypeOrder(typeOrder) } if caseSensitivity, ok := prefsMap["caseSensitivity"]; ok { if caseSensitivityStr, ok := caseSensitivity.(string); ok { @@ -556,23 +746,22 @@ func (p *UserPreferences) parseOrganizeImportsPreferences(prefs any) { } } 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) - } - if caseFirst, ok := prefsMap["caseFirst"]; ok && !p.OrganizeImportsIgnoreCase.IsTrue() { - p.set("organizeimportscasefirst", caseFirst) - } + p.OrganizeImportsCollation = OrganizeImportsCollationUnicode } } + if locale, ok := prefsMap["locale"]; ok { + p.OrganizeImportsLocale = tsoptions.ParseString(locale) + } + if numeric, ok := prefsMap["numericCollation"]; ok { + p.OrganizeImportsNumericCollation = parseBoolWithDefault(numeric, false) + } + if accent, ok := prefsMap["accentCollation"]; ok { + p.OrganizeImportsAccentCollation = parseBoolWithDefault(accent, true) + } + if caseFirst, ok := prefsMap["caseFirst"]; ok { + p.OrganizeImportsCaseFirst = parseOrganizeImportsCaseFirst(caseFirst) + } } func (p *UserPreferences) parseWorkspaceSymbols(prefs any) { @@ -619,15 +808,18 @@ func parseBoolWithDefault(val any, defaultV bool) bool { } func parseIntWithDefault(val any, defaultV int) int { - if v, ok := val.(int); ok { + switch v := val.(type) { + case int: return v + case float64: + return int(v) } return defaultV } func (p *UserPreferences) set(name string, value any) { switch strings.ToLower(name) { - case "quotePreference": + case "quotepreference", "quotestyle": p.QuotePreference = parseQuotePreference(value) case "lazyconfiguredprojectsfromexternalproject": p.LazyConfiguredProjectsFromExternalProject = parseBoolWithDefault(value, false) @@ -647,7 +839,7 @@ func (p *UserPreferences) set(name string, value any) { p.IncludeCompletionsWithObjectLiteralMethodSnippets = tsoptions.ParseTristate(value) case "jsxattributecompletionstyle": p.JsxAttributeCompletionStyle = parseJsxAttributeCompletionStyle(value) - case "importmodulespecifierpreference": + case "importmodulespecifier", "importmodulespecifierpreference": p.ImportModuleSpecifierPreference = parseImportModuleSpecifierPreference(value) case "importmodulespecifierending": p.ImportModuleSpecifierEnding = parseImportModuleSpecifierEndingPreference(value) @@ -675,7 +867,7 @@ func (p *UserPreferences) set(name string, value any) { p.OrganizeImportsTypeOrder = parseOrganizeImportsTypeOrder(value) case "allowtextchangesinnewfiles": p.AllowTextChangesInNewFiles = parseBoolWithDefault(value, true) // !!! - case "usealiasesforrename", "provideprefixandsuffixtextforrename": + case "usealiasesforrename", "usealiasesforrenames", "provideprefixandsuffixtextforrename": p.UseAliasesForRename = tsoptions.ParseTristate(value) case "allowrenameofimportpath": p.AllowRenameOfImportPath = parseBoolWithDefault(value, true) diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go new file mode 100644 index 0000000000..5921928414 --- /dev/null +++ b/internal/ls/lsutil/userpreferences_test.go @@ -0,0 +1,87 @@ +package lsutil + +import ( + "reflect" + "testing" + + "github.com/go-json-experiment/json" + "github.com/microsoft/typescript-go/internal/modulespecifiers" + "gotest.tools/v3/assert" +) + +func fillNonZeroValues(v reflect.Value, path string) { + t := v.Type() + for i := range t.NumField() { + field := v.Field(i) + fieldType := t.Field(i) + if !field.CanSet() { + continue + } + fieldPath := path + "." + fieldType.Name + 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(), fieldPath) + field.SetString(val) + case reflect.Slice: + if field.Type().Elem().Kind() == reflect.String { + field.Set(reflect.ValueOf([]string{"test"})) + } + case reflect.Struct: + fillNonZeroValues(field, fieldPath) + } + } +} + +func getValidStringValue(t reflect.Type, path string) 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(), "UserPreferences") + + jsonBytes, err := json.Marshal(original) + assert.NilError(t, err) + + t.Run("UnmarshalJSONFrom", func(t *testing.T) { + t.Parallel() + parsed := &UserPreferences{} + err := json.Unmarshal(jsonBytes, parsed) + assert.NilError(t, err) + assert.DeepEqual(t, original, parsed) + }) + + t.Run("parseWorker", func(t *testing.T) { + t.Parallel() + var config map[string]any + err := json.Unmarshal(jsonBytes, &config) + assert.NilError(t, err) + parsed := &UserPreferences{} + parsed.parseWorker(config) + assert.DeepEqual(t, original, parsed) + }) +} From bca6d8f11d362f49566ede12fe49739ce6e17378 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 5 Dec 2025 08:14:09 -0800 Subject: [PATCH 02/19] Very redo --- internal/ls/lsutil/userpreferences.go | 1226 ++++++++------------ internal/ls/lsutil/userpreferences_test.go | 8 +- 2 files changed, 480 insertions(+), 754 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 47d1c06e9b..c37607fff1 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -1,14 +1,15 @@ 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 { @@ -27,391 +28,101 @@ func NewDefaultUserPreferences() *UserPreferences { } } +// UserPreferences represents TypeScript language service preferences. +// +// Fields can be populated from two sources: +// 1. Direct property names (from VS Code's "unstable" config or direct tsserver protocol) +// These are matched case-insensitively by Go field name. +// 2. VS Code's nested config structure (e.g., "suggest.autoImports", "inlayHints.parameterNames.enabled") +// These use the `pref` tag to specify the dotted path. +// +// The `pref` tag format: "path.to.setting" or "path.to.setting,invert" for boolean inversion. type UserPreferences struct { - QuotePreference QuotePreference - LazyConfiguredProjectsFromExternalProject bool // !!! - - // A positive integer indicating the maximum length of a hover text before it is truncated. - // - // Default: `500` - MaximumHoverLength int // !!! - - // ------- 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 - // 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 - // 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 - // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. - IncludeCompletionsWithSnippetText core.Tristate // !!! - // 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 // !!! - // 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 - - // ------- 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 bool // !!! - - // ------- OrganizeImports ------- - - // Indicates whether imports should be organized in a case-insensitive manner. - // - // Default: TSUnknown ("auto" in strada), will perform detection - OrganizeImportsIgnoreCase core.Tristate // !!! - // 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 // !!! - // 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 // !!! - // 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 // !!! - // 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. - // - // This preference is ignored if organizeImportsCollation is not `unicode`. - // - // Default: `true` - OrganizeImportsAccentCollation bool // !!! - // 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. - // - // Default: `false` - OrganizeImportsCaseFirst OrganizeImportsCaseFirst // !!! - // 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 // !!! - - // ------- MoveToFile ------- - - AllowTextChangesInNewFiles bool // !!! - - // ------- Rename ------- - - // renamed from `providePrefixAndSuffixTextForRename` - UseAliasesForRename core.Tristate - AllowRenameOfImportPath bool // !!! - - // ------- CodeFixes/Refactors ------- - - ProvideRefactorNotApplicableReason bool // !!! - - // ------- InlayHints ------- + QuotePreference QuotePreference `pref:"preferences.quoteStyle"` + LazyConfiguredProjectsFromExternalProject bool + MaximumHoverLength int + + IncludeCompletionsForModuleExports core.Tristate `pref:"suggest.autoImports"` + IncludeCompletionsForImportStatements core.Tristate `pref:"suggest.includeCompletionsForImportStatements"` + IncludeAutomaticOptionalChainCompletions core.Tristate `pref:"suggest.includeAutomaticOptionalChainCompletions"` + IncludeCompletionsWithSnippetText core.Tristate + IncludeCompletionsWithClassMemberSnippets core.Tristate `pref:"suggest.classMemberSnippets.enabled"` + IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `pref:"suggest.objectLiteralMethodSnippets.enabled"` + JsxAttributeCompletionStyle JsxAttributeCompletionStyle `pref:"preferences.jsxAttributeCompletionStyle"` + + ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `pref:"preferences.importModuleSpecifier"` + ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `pref:"preferences.importModuleSpecifierEnding"` + IncludePackageJsonAutoImports IncludePackageJsonAutoImports `pref:"preferences.includePackageJsonAutoImports"` + AutoImportSpecifierExcludeRegexes []string `pref:"preferences.autoImportSpecifierExcludeRegexes"` + AutoImportFileExcludePatterns []string `pref:"preferences.autoImportFileExcludePatterns"` + PreferTypeOnlyAutoImports bool `pref:"preferences.preferTypeOnlyAutoImports"` + + OrganizeImportsIgnoreCase core.Tristate `pref:"preferences.organizeImports.caseSensitivity"` + OrganizeImportsCollation OrganizeImportsCollation `pref:"preferences.organizeImports.unicodeCollation"` + OrganizeImportsLocale string `pref:"preferences.organizeImports.locale"` + OrganizeImportsNumericCollation bool `pref:"preferences.organizeImports.numericCollation"` + OrganizeImportsAccentCollation bool `pref:"preferences.organizeImports.accentCollation"` + OrganizeImportsCaseFirst OrganizeImportsCaseFirst `pref:"preferences.organizeImports.caseFirst"` + OrganizeImportsTypeOrder OrganizeImportsTypeOrder `pref:"preferences.organizeImports.typeOrder"` + + AllowTextChangesInNewFiles bool + + UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames,alias:providePrefixAndSuffixTextForRename"` + AllowRenameOfImportPath bool + + ProvideRefactorNotApplicableReason bool InlayHints InlayHintsPreferences - // ------- CodeLens ------- - CodeLens CodeLensUserPreferences - // ------- Symbols ------- - - ExcludeLibrarySymbolsInNavTo bool + ExcludeLibrarySymbolsInNavTo bool `pref:"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 + DisableLineTextInReferences bool + DisplayPartsForJSDoc bool + ReportStyleChecksAsWarnings bool } type InlayHintsPreferences struct { - IncludeInlayParameterNameHints IncludeInlayParameterNameHints - IncludeInlayParameterNameHintsWhenArgumentMatchesName bool - IncludeInlayFunctionParameterTypeHints bool - IncludeInlayVariableTypeHints bool - IncludeInlayVariableTypeHintsWhenTypeMatchesName bool - IncludeInlayPropertyDeclarationTypeHints bool - IncludeInlayFunctionLikeReturnTypeHints bool - IncludeInlayEnumMemberValueHints bool + IncludeInlayParameterNameHints IncludeInlayParameterNameHints `pref:"inlayHints.parameterNames.enabled"` + IncludeInlayParameterNameHintsWhenArgumentMatchesName bool `pref:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"` + IncludeInlayFunctionParameterTypeHints bool `pref:"inlayHints.parameterTypes.enabled"` + IncludeInlayVariableTypeHints bool `pref:"inlayHints.variableTypes.enabled"` + IncludeInlayVariableTypeHintsWhenTypeMatchesName bool `pref:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"` + IncludeInlayPropertyDeclarationTypeHints bool `pref:"inlayHints.propertyDeclarationTypes.enabled"` + IncludeInlayFunctionLikeReturnTypeHints bool `pref:"inlayHints.functionLikeReturnTypes.enabled"` + IncludeInlayEnumMemberValueHints bool `pref:"inlayHints.enumMemberValues.enabled"` } type CodeLensUserPreferences struct { - ReferencesCodeLensEnabled bool - ImplementationsCodeLensEnabled bool - ReferencesCodeLensShowOnAllFunctions bool - ImplementationsCodeLensShowOnInterfaceMethods bool - ImplementationsCodeLensShowOnAllClassMethods bool -} - -type jsonWriter struct { - enc *jsontext.Encoder - err error + ReferencesCodeLensEnabled bool `pref:"referencesCodeLens.enabled"` + ImplementationsCodeLensEnabled bool `pref:"implementationsCodeLens.enabled"` + ReferencesCodeLensShowOnAllFunctions bool `pref:"referencesCodeLens.showOnAllFunctions"` + ImplementationsCodeLensShowOnInterfaceMethods bool `pref:"implementationsCodeLens.showOnInterfaceMethods"` + ImplementationsCodeLensShowOnAllClassMethods bool `pref:"implementationsCodeLens.showOnAllClassMethods"` } -func (w *jsonWriter) token(t jsontext.Token) { - if w.err == nil { - w.err = w.enc.WriteToken(t) - } -} +// --- Enum Types --- -func (w *jsonWriter) key(k string) { w.token(jsontext.String(k)) } -func (w *jsonWriter) str(s string) { w.token(jsontext.String(s)) } -func (w *jsonWriter) boolean(b bool) { w.token(jsontext.Bool(b)) } -func (w *jsonWriter) beginObject() { w.token(jsontext.BeginObject) } -func (w *jsonWriter) endObject() { w.token(jsontext.EndObject) } -func (w *jsonWriter) beginArray() { w.token(jsontext.BeginArray) } -func (w *jsonWriter) endArray() { w.token(jsontext.EndArray) } -func (w *jsonWriter) prop(k string, v string) { w.key(k); w.str(v) } -func (w *jsonWriter) boolProp(k string, v bool) { w.key(k); w.boolean(v) } - -func (w *jsonWriter) intProp(k string, v int) { - w.key(k) - if w.err == nil { - w.err = w.enc.WriteToken(jsontext.Int(int64(v))) - } -} - -func (w *jsonWriter) tristateProp(k string, v core.Tristate) { - w.key(k) - switch v { - case core.TSTrue: - w.boolean(true) - case core.TSFalse: - w.boolean(false) - default: - w.token(jsontext.Null) - } -} - -func (w *jsonWriter) stringArray(arr []string) { - w.beginArray() - for _, s := range arr { - w.str(s) - } - w.endArray() -} - -func (w *jsonWriter) object(key string, fn func()) { - w.key(key) - w.beginObject() - fn() - w.endObject() -} - -func (u *UserPreferences) MarshalJSONTo(enc *jsontext.Encoder) error { - w := &jsonWriter{enc: enc} - w.beginObject() - - w.object("unstable", func() { - w.boolProp("lazyConfiguredProjectsFromExternalProject", u.LazyConfiguredProjectsFromExternalProject) - w.intProp("maximumHoverLength", u.MaximumHoverLength) - w.tristateProp("includeCompletionsWithSnippetText", u.IncludeCompletionsWithSnippetText) - w.boolProp("allowTextChangesInNewFiles", u.AllowTextChangesInNewFiles) - w.boolProp("allowRenameOfImportPath", u.AllowRenameOfImportPath) - w.boolProp("provideRefactorNotApplicableReason", u.ProvideRefactorNotApplicableReason) - w.boolProp("disableSuggestions", u.DisableSuggestions) - w.boolProp("disableLineTextInReferences", u.DisableLineTextInReferences) - w.boolProp("displayPartsForJSDoc", u.DisplayPartsForJSDoc) - }) - - w.object("referencesCodeLens", func() { - w.boolProp("enabled", u.CodeLens.ReferencesCodeLensEnabled) - w.boolProp("showOnAllFunctions", u.CodeLens.ReferencesCodeLensShowOnAllFunctions) - }) - - w.object("implementationsCodeLens", func() { - w.boolProp("enabled", u.CodeLens.ImplementationsCodeLensEnabled) - w.boolProp("showOnInterfaceMethods", u.CodeLens.ImplementationsCodeLensShowOnInterfaceMethods) - w.boolProp("showOnAllClassMethods", u.CodeLens.ImplementationsCodeLensShowOnAllClassMethods) - }) - - w.object("inlayHints", func() { - w.object("parameterNames", func() { - enabledVal := string(u.InlayHints.IncludeInlayParameterNameHints) - if enabledVal == "" { - enabledVal = "none" - } - w.prop("enabled", enabledVal) - w.boolProp("suppressWhenArgumentMatchesName", !u.InlayHints.IncludeInlayParameterNameHintsWhenArgumentMatchesName) - }) - w.object("parameterTypes", func() { - w.boolProp("enabled", u.InlayHints.IncludeInlayFunctionParameterTypeHints) - }) - w.object("variableTypes", func() { - w.boolProp("enabled", u.InlayHints.IncludeInlayVariableTypeHints) - w.boolProp("suppressWhenTypeMatchesName", !u.InlayHints.IncludeInlayVariableTypeHintsWhenTypeMatchesName) - }) - w.object("propertyDeclarationTypes", func() { - w.boolProp("enabled", u.InlayHints.IncludeInlayPropertyDeclarationTypeHints) - }) - w.object("functionLikeReturnTypes", func() { - w.boolProp("enabled", u.InlayHints.IncludeInlayFunctionLikeReturnTypeHints) - }) - w.object("enumMemberValues", func() { - w.boolProp("enabled", u.InlayHints.IncludeInlayEnumMemberValueHints) - }) - }) - - w.object("preferences", func() { - w.prop("quoteStyle", string(u.QuotePreference)) - w.prop("importModuleSpecifier", string(u.ImportModuleSpecifierPreference)) - w.prop("importModuleSpecifierEnding", string(u.ImportModuleSpecifierEnding)) - w.prop("includePackageJsonAutoImports", string(u.IncludePackageJsonAutoImports)) - w.key("autoImportFileExcludePatterns") - w.stringArray(u.AutoImportFileExcludePatterns) - w.key("autoImportSpecifierExcludeRegexes") - w.stringArray(u.AutoImportSpecifierExcludeRegexes) - w.boolProp("preferTypeOnlyAutoImports", u.PreferTypeOnlyAutoImports) - w.prop("jsxAttributeCompletionStyle", string(u.JsxAttributeCompletionStyle)) - w.tristateProp("useAliasesForRenames", u.UseAliasesForRename) - - w.object("organizeImports", func() { - if u.OrganizeImportsIgnoreCase == core.TSTrue { - w.prop("caseSensitivity", "caseInsensitive") - } else if u.OrganizeImportsIgnoreCase == core.TSFalse { - w.prop("caseSensitivity", "caseSensitive") - } - typeOrderStr := "auto" - switch u.OrganizeImportsTypeOrder { - case OrganizeImportsTypeOrderLast: - typeOrderStr = "last" - case OrganizeImportsTypeOrderInline: - typeOrderStr = "inline" - case OrganizeImportsTypeOrderFirst: - typeOrderStr = "first" - } - w.prop("typeOrder", typeOrderStr) - if u.OrganizeImportsCollation == OrganizeImportsCollationUnicode { - w.prop("unicodeCollation", "unicode") - } else { - w.prop("unicodeCollation", "ordinal") - } - w.prop("locale", u.OrganizeImportsLocale) - w.boolProp("numericCollation", u.OrganizeImportsNumericCollation) - w.boolProp("accentCollation", u.OrganizeImportsAccentCollation) - caseFirstStr := "default" - switch u.OrganizeImportsCaseFirst { - case OrganizeImportsCaseFirstLower: - caseFirstStr = "lower" - case OrganizeImportsCaseFirstUpper: - caseFirstStr = "upper" - } - w.prop("caseFirst", caseFirstStr) - }) - }) - - w.object("suggest", func() { - w.tristateProp("autoImports", u.IncludeCompletionsForModuleExports) - w.tristateProp("includeCompletionsForImportStatements", u.IncludeCompletionsForImportStatements) - w.tristateProp("includeAutomaticOptionalChainCompletions", u.IncludeAutomaticOptionalChainCompletions) - w.object("classMemberSnippets", func() { - w.tristateProp("enabled", u.IncludeCompletionsWithClassMemberSnippets) - }) - w.object("objectLiteralMethodSnippets", func() { - w.tristateProp("enabled", u.IncludeCompletionsWithObjectLiteralMethodSnippets) - }) - }) - - w.object("workspaceSymbols", func() { - w.boolProp("excludeLibrarySymbols", u.ExcludeLibrarySymbolsInNavTo) - }) - - w.boolProp("reportStyleChecksAsWarnings", u.ReportStyleChecksAsWarnings) - - w.endObject() - return w.err -} +type QuotePreference string -func (u *UserPreferences) UnmarshalJSONFrom(dec *jsontext.Decoder) error { - var config map[string]any - if err := json.UnmarshalDecode(dec, &config); err != nil { - return err - } - u.parseWorker(config) - return nil -} +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 ( @@ -420,41 +131,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 ( @@ -462,13 +147,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 ( @@ -477,18 +155,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 ( @@ -498,416 +164,476 @@ 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 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 + }, +} + +// typeSerializers maps reflect.Type to a function that serializes a value of that type. +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 + } + }, + reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any { + s := val.(IncludeInlayParameterNameHints) + if s == "" { + return "none" + } + return string(s) + }, + 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" + } + }, +} + +type fieldInfo struct { + lowerName string // lowercase Go field name for direct matching + aliases []string // additional lowercase names (e.g., "provideprefixandsuffixtextforrename") + path string // dotted path for VS Code config (e.g., "preferences.quoteStyle") + fieldPath []int // index path to field in struct + invert bool // whether to invert boolean values +} + +var fieldInfoCache = sync.OnceValue(func() []fieldInfo { + var infos []fieldInfo + collectFieldInfos(reflect.TypeFor[UserPreferences](), nil, &infos) + return infos +}) + +// lowerNameIndex maps lowercase field names to fieldInfo index for O(1) lookup +var lowerNameIndex = sync.OnceValue(func() map[string]int { + infos := fieldInfoCache() + index := make(map[string]int, len(infos)*2) + for i, info := range infos { + index[info.lowerName] = i + for _, alias := range info.aliases { + index[alias] = i } } - return OrganizeImportsTypeOrderAuto -} + return index +}) -type QuotePreference string +func collectFieldInfos(t reflect.Type, indexPath []int, infos *[]fieldInfo) { + for i := range t.NumField() { + field := t.Field(i) + currentPath := append(slices.Clone(indexPath), i) -const ( - QuotePreferenceUnknown QuotePreference = "" - QuotePreferenceAuto QuotePreference = "auto" - QuotePreferenceDouble QuotePreference = "double" - QuotePreferenceSingle QuotePreference = "single" -) + if field.Type.Kind() == reflect.Struct && field.Tag.Get("pref") == "" { + collectFieldInfos(field.Type, currentPath, infos) + continue + } -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 + info := fieldInfo{ + lowerName: strings.ToLower(field.Name), + fieldPath: currentPath, } - } - return QuotePreferenceUnknown -} -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 -} + tag := field.Tag.Get("pref") + if tag != "" { + // Parse tag: "path" or "path,invert" or "path,alias:name" + parts := strings.Split(tag, ",") + info.path = parts[0] + for _, part := range parts[1:] { + if part == "invert" { + info.invert = true + } else if alias, ok := strings.CutPrefix(part, "alias:"); ok { + info.aliases = append(info.aliases, strings.ToLower(alias)) + } + } + } -func (p *UserPreferences) CopyOrDefault() *UserPreferences { - if p == nil { - return NewDefaultUserPreferences() + *infos = append(*infos, info) } - return p.Copy() } -func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { - return modulespecifiers.UserPreferences{ - ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, - ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding, - AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, +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 } -// ------ Parsing Config Response ------- - -// 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() +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 } - return nil + current[parts[len(parts)-1]] = value } 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) + v := reflect.ValueOf(p).Elem() + infos := fieldInfoCache() + index := lowerNameIndex() + + // Process "unstable" first - these are spread directly by field name + if unstable, ok := config["unstable"].(map[string]any); ok { + for name, value := range unstable { + if idx, found := index[strings.ToLower(name)]; found { + info := infos[idx] + field := getFieldByPath(v, info.fieldPath) + if info.invert { + if b, ok := value.(bool); ok { + value = !b + } + } + setFieldFromValue(field, value) + } + } } - for name, values := range config { - switch name { - case "unstable": + + // Process path-based config (VS Code style) + for _, info := range infos { + if info.path == "" { + continue + } + val, ok := getNestedValue(config, info.path) + if !ok { 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) } - } -} -func (p *UserPreferences) parseAll(prefs any) { - prefsMap, ok := prefs.(map[string]any) - if !ok { - return - } - for name, value := range prefsMap { - p.set(name, value) + field := getFieldByPath(v, info.fieldPath) + if info.invert { + if b, ok := val.(bool); ok { + val = !b + } + } + setFieldFromValue(field, val) } -} -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) + // Process direct field names at root level (non-VS Code clients, fourslash tests) + for name, value := range config { + if name == "unstable" { + continue + } + // Skip known VS Code config sections that are handled by path-based parsing + switch name { + case "preferences", "suggest", "inlayHints", "referencesCodeLens", + "implementationsCodeLens", "workspaceSymbols", "format", "tsserver", "tsc", "experimental": + continue + } + if idx, found := index[strings.ToLower(name)]; found { + info := infos[idx] + field := getFieldByPath(v, info.fieldPath) + if info.invert { + if b, ok := value.(bool); ok { + value = !b } - 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) + setFieldFromValue(field, value) } } } -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 getFieldByPath(v reflect.Value, path []int) reflect.Value { + for _, idx := range path { + v = v.Field(idx) } + return v } -func (p *UserPreferences) parseImplementationsCodeLens(prefs any) { - implementationsCodeLens, ok := prefs.(map[string]any) - if !ok { +func setFieldFromValue(field reflect.Value, val any) { + if val == nil { 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 (p *UserPreferences) parseSuggest(prefs any) { - completionsPreferences, ok := prefs.(map[string]any) - if !ok { + // Check custom parsers first (for types like Tristate, OrganizeImportsCollation, etc.) + if parser, ok := typeParsers[field.Type()]; ok { + field.Set(reflect.ValueOf(parser(val))) 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", v["enabled"]) - } - case "classMemberSnippets": - if v, ok := value.(map[string]any); ok { - p.set("includeCompletionsWithClassMemberSnippets", v["enabled"]) + + 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)) + } } - case "includeAutomaticOptionalChainCompletions": - p.set("includeAutomaticOptionalChainCompletions", value) - case "includeCompletionsForImportStatements": - p.set("includeCompletionsForImportStatements", value) + field.Set(result) } } } -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 (p *UserPreferences) MarshalJSONTo(enc *jsontext.Encoder) error { + config := make(map[string]any) + v := reflect.ValueOf(p).Elem() -func (p *UserPreferences) parseOrganizeImportsPreferences(prefs any) { - // !!! this used to be in the typescript-language-features extension - prefsMap, ok := prefs.(map[string]any) - if !ok { - return - } - if typeOrder, ok := prefsMap["typeOrder"]; ok { - p.OrganizeImportsTypeOrder = parseOrganizeImportsTypeOrder(typeOrder) - } - 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 + for _, info := range fieldInfoCache() { + field := getFieldByPath(v, info.fieldPath) + + val := serializeField(field) + if val == nil { + continue + } + if info.invert { + if b, ok := val.(bool); ok { + val = !b } } - } - if collation, ok := prefsMap["unicodeCollation"]; ok { - if collationStr, ok := collation.(string); ok && collationStr == "unicode" { - p.OrganizeImportsCollation = OrganizeImportsCollationUnicode + + // Use the path if available, otherwise use the lowercase field name at root level + if info.path != "" { + setNestedValue(config, info.path, val) + } else { + config[info.lowerName] = val } } - if locale, ok := prefsMap["locale"]; ok { - p.OrganizeImportsLocale = tsoptions.ParseString(locale) - } - if numeric, ok := prefsMap["numericCollation"]; ok { - p.OrganizeImportsNumericCollation = parseBoolWithDefault(numeric, false) - } - if accent, ok := prefsMap["accentCollation"]; ok { - p.OrganizeImportsAccentCollation = parseBoolWithDefault(accent, true) - } - if caseFirst, ok := prefsMap["caseFirst"]; ok { - p.OrganizeImportsCaseFirst = parseOrganizeImportsCaseFirst(caseFirst) - } + + return json.MarshalEncode(enc, config) } -func (p *UserPreferences) parseWorkspaceSymbols(prefs any) { - symbolPreferences, ok := prefs.(map[string]any) - if !ok { - return +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()) } - for name, value := range symbolPreferences { - switch name { - // !!! scope - case "excludeLibrarySymbols": - p.ExcludeLibrarySymbolsInNavTo = parseBoolWithDefault(value, true) - default: - p.set(name, value) + + 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 + p.parseWorker(config) + return nil } -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 - } +// --- Helper methods --- + +func (p *UserPreferences) Copy() *UserPreferences { + if p == nil { + return nil } - return false + prefCopy := *p + prefCopy.AutoImportSpecifierExcludeRegexes = slices.Clone(p.AutoImportSpecifierExcludeRegexes) + prefCopy.AutoImportFileExcludePatterns = slices.Clone(p.AutoImportFileExcludePatterns) + return &prefCopy } -func parseBoolWithDefault(val any, defaultV bool) bool { - if v, ok := val.(bool); ok { - return v +func (p *UserPreferences) CopyOrDefault() *UserPreferences { + if p == nil { + return NewDefaultUserPreferences() } - return defaultV + return p.Copy() } -func parseIntWithDefault(val any, defaultV int) int { - switch v := val.(type) { - case int: - return v - case float64: - return int(v) +func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { + return modulespecifiers.UserPreferences{ + ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, + ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding, + AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, } - return defaultV } -func (p *UserPreferences) set(name string, value any) { - switch strings.ToLower(name) { - case "quotepreference", "quotestyle": - 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 "importmodulespecifier", "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 = parseBoolWithDefault(value, false) - 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", "usealiasesforrenames", "provideprefixandsuffixtextforrename": - p.UseAliasesForRename = tsoptions.ParseTristate(value) - case "allowrenameofimportpath": - p.AllowRenameOfImportPath = parseBoolWithDefault(value, true) - 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) +func (p *UserPreferences) Parse(item any) *UserPreferences { + if item == nil { + return nil + } + if config, ok := item.(map[string]any); ok { + p.parseWorker(config) + return nil } + if item, ok := item.(*UserPreferences); ok { + return item.CopyOrDefault() + } + return nil } diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index 5921928414..06fb6ac3d1 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -70,16 +70,16 @@ func TestUserPreferencesRoundtrip(t *testing.T) { t.Run("UnmarshalJSONFrom", func(t *testing.T) { t.Parallel() parsed := &UserPreferences{} - err := json.Unmarshal(jsonBytes, parsed) - assert.NilError(t, err) + 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 - err := json.Unmarshal(jsonBytes, &config) - assert.NilError(t, err) + err2 := json.Unmarshal(jsonBytes, &config) + assert.NilError(t, err2) parsed := &UserPreferences{} parsed.parseWorker(config) assert.DeepEqual(t, original, parsed) From 1deb813b70a7582874c225ea85aec88cff02b525 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:19:18 -0800 Subject: [PATCH 03/19] Deep copy --- internal/ls/lsutil/userpreferences.go | 53 +++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index c37607fff1..37d684cde3 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -599,14 +599,53 @@ func (p *UserPreferences) UnmarshalJSONFrom(dec *jsontext.Decoder) error { // --- Helper methods --- -func (p *UserPreferences) Copy() *UserPreferences { - if p == nil { - return nil +func deepCopy[T any](src T) T { + var dst T + deepCopyValue(reflect.ValueOf(&dst).Elem(), reflect.ValueOf(src)) + return dst +} + +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 := 0; i < src.NumField(); i++ { + 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 := 0; i < src.Len(); i++ { + 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) } - prefCopy := *p - prefCopy.AutoImportSpecifierExcludeRegexes = slices.Clone(p.AutoImportSpecifierExcludeRegexes) - prefCopy.AutoImportFileExcludePatterns = slices.Clone(p.AutoImportFileExcludePatterns) - return &prefCopy +} + +func (p *UserPreferences) Copy() *UserPreferences { + return deepCopy(p) } func (p *UserPreferences) CopyOrDefault() *UserPreferences { From 1763af6fdad43835358e624f698050f153db2315 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 5 Dec 2025 21:51:29 -0800 Subject: [PATCH 04/19] Another struct --- internal/ls/autoimports.go | 2 +- internal/ls/lsutil/userpreferences.go | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/ls/autoimports.go b/internal/ls/autoimports.go index d1bd867fb8..dd1a1a01a9 100644 --- a/internal/ls/autoimports.go +++ b/internal/ls/autoimports.go @@ -637,7 +637,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 37d684cde3..fe5ed6ac54 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -50,12 +50,11 @@ type UserPreferences struct { IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `pref:"suggest.objectLiteralMethodSnippets.enabled"` JsxAttributeCompletionStyle JsxAttributeCompletionStyle `pref:"preferences.jsxAttributeCompletionStyle"` - ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `pref:"preferences.importModuleSpecifier"` - ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `pref:"preferences.importModuleSpecifierEnding"` - IncludePackageJsonAutoImports IncludePackageJsonAutoImports `pref:"preferences.includePackageJsonAutoImports"` - AutoImportSpecifierExcludeRegexes []string `pref:"preferences.autoImportSpecifierExcludeRegexes"` - AutoImportFileExcludePatterns []string `pref:"preferences.autoImportFileExcludePatterns"` - PreferTypeOnlyAutoImports bool `pref:"preferences.preferTypeOnlyAutoImports"` + ModuleSpecifier ModuleSpecifierUserPreferences + + IncludePackageJsonAutoImports IncludePackageJsonAutoImports `pref:"preferences.includePackageJsonAutoImports"` + AutoImportFileExcludePatterns []string `pref:"preferences.autoImportFileExcludePatterns"` + PreferTypeOnlyAutoImports bool `pref:"preferences.preferTypeOnlyAutoImports"` OrganizeImportsIgnoreCase core.Tristate `pref:"preferences.organizeImports.caseSensitivity"` OrganizeImportsCollation OrganizeImportsCollation `pref:"preferences.organizeImports.unicodeCollation"` @@ -103,6 +102,12 @@ type CodeLensUserPreferences struct { ImplementationsCodeLensShowOnAllClassMethods bool `pref:"implementationsCodeLens.showOnAllClassMethods"` } +type ModuleSpecifierUserPreferences struct { + ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `pref:"preferences.importModuleSpecifier"` + ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `pref:"preferences.importModuleSpecifierEnding"` + AutoImportSpecifierExcludeRegexes []string `pref:"preferences.autoImportSpecifierExcludeRegexes"` +} + // --- Enum Types --- type QuotePreference string @@ -656,11 +661,7 @@ func (p *UserPreferences) CopyOrDefault() *UserPreferences { } func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { - return modulespecifiers.UserPreferences{ - ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, - ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding, - AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, - } + return modulespecifiers.UserPreferences(p.ModuleSpecifier) } func (p *UserPreferences) Parse(item any) *UserPreferences { From 86b1d94749b1b3aa382f1d14d5c311e4de0dfd58 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:05:59 -0800 Subject: [PATCH 05/19] Simpler --- internal/ls/lsutil/userpreferences.go | 14 ++++++++++---- internal/lsp/server.go | 11 ++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index fe5ed6ac54..48865966ee 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -598,6 +598,8 @@ func (p *UserPreferences) UnmarshalJSONFrom(dec *jsontext.Decoder) error { if err := json.UnmarshalDecode(dec, &config); err != nil { return err } + // Start with defaults, then overlay parsed values + *p = *NewDefaultUserPreferences() p.parseWorker(config) return nil } @@ -664,16 +666,20 @@ func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPref return modulespecifiers.UserPreferences(p.ModuleSpecifier) } -func (p *UserPreferences) Parse(item any) *UserPreferences { +// ParseUserPreferences parses user preferences from a config map or copies from existing preferences. +// Always returns a fresh *UserPreferences with defaults applied, then overlaid with parsed values. +// Returns nil if item is nil. +func ParseUserPreferences(item any) *UserPreferences { if item == nil { return nil } if config, ok := item.(map[string]any); ok { + p := NewDefaultUserPreferences() p.parseWorker(config) - return nil + return p } - if item, ok := item.(*UserPreferences); ok { - return item.CopyOrDefault() + if prefs, ok := item.(*UserPreferences); ok { + return prefs.CopyOrDefault() } return nil } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index bf211a992d..2ad96595d2 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -287,13 +287,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 s.session.NewUserPreferences(), nil } func (s *Server) Run(ctx context.Context) error { @@ -1101,11 +1100,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 } From bf21cb2b51496ca9d8fdbbaec96e4e861ce51d4a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 5 Dec 2025 22:10:45 -0800 Subject: [PATCH 06/19] Simpler --- internal/lsp/server.go | 6 +++--- internal/project/session.go | 10 +--------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 2ad96595d2..d2afaafb31 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -273,8 +273,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.NewDefaultUserPreferences(), nil } configs, err := sendClientRequest(ctx, s, lsproto.WorkspaceConfigurationInfo, &lsproto.ConfigurationParams{ Items: []*lsproto.ConfigurationItem{ @@ -292,7 +292,7 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferen return parsed, nil } } - return s.session.NewUserPreferences(), nil + return lsutil.NewDefaultUserPreferences(), nil } func (s *Server) Run(ctx context.Context) error { diff --git a/internal/project/session.go b/internal/project/session.go index 813c675985..91bda4d0cc 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 @@ -197,11 +195,6 @@ func (s *Session) UserPreferences() *lsutil.UserPreferences { return s.userPreferences.Copy() } -// Gets original UserPreferences of the session -func (s *Session) NewUserPreferences() *lsutil.UserPreferences { - return s.initialPreferences.CopyOrDefault() -} - // Trace implements module.ResolutionHost func (s *Session) Trace(msg string) { panic("ATA module resolution should not use tracing") @@ -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.CopyOrDefault()) } func (s *Session) DidOpenFile(ctx context.Context, uri lsproto.DocumentUri, version int32, content string, languageKind lsproto.LanguageKind) { From 88a928fdefd6c940800ef72fbc85d9fe1493eca5 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:10:08 -0800 Subject: [PATCH 07/19] Remove copying --- internal/ls/lsutil/userpreferences.go | 20 +++++++------------- internal/project/session.go | 8 ++++---- internal/project/session_test.go | 3 ++- internal/project/snapshot.go | 2 +- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 48865966ee..f4441682c9 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -622,7 +622,7 @@ func deepCopyValue(dst, src reflect.Value) { dst.Set(reflect.New(src.Type().Elem())) deepCopyValue(dst.Elem(), src.Elem()) case reflect.Struct: - for i := 0; i < src.NumField(); i++ { + for i := range src.NumField() { deepCopyValue(dst.Field(i), src.Field(i)) } case reflect.Slice: @@ -631,7 +631,7 @@ func deepCopyValue(dst, src reflect.Value) { return } dst.Set(reflect.MakeSlice(src.Type(), src.Len(), src.Len())) - for i := 0; i < src.Len(); i++ { + for i := range src.Len() { deepCopyValue(dst.Index(i), src.Index(i)) } case reflect.Map: @@ -655,20 +655,14 @@ func (p *UserPreferences) Copy() *UserPreferences { return deepCopy(p) } -func (p *UserPreferences) CopyOrDefault() *UserPreferences { - if p == nil { - return NewDefaultUserPreferences() - } - return p.Copy() -} - func (p *UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { return modulespecifiers.UserPreferences(p.ModuleSpecifier) } -// ParseUserPreferences parses user preferences from a config map or copies from existing preferences. -// Always returns a fresh *UserPreferences with defaults applied, then overlaid with parsed values. -// Returns nil if item is nil. +// 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 @@ -679,7 +673,7 @@ func ParseUserPreferences(item any) *UserPreferences { return p } if prefs, ok := item.(*UserPreferences); ok { - return prefs.CopyOrDefault() + return prefs } return nil } diff --git a/internal/project/session.go b/internal/project/session.go index 91bda4d0cc..1e1ca6e0aa 100644 --- a/internal/project/session.go +++ b/internal/project/session.go @@ -188,11 +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() + return s.userPreferences } // Trace implements module.ResolutionHost @@ -215,7 +215,7 @@ func (s *Session) Configure(userPreferences *lsutil.UserPreferences) { } func (s *Session) InitializeWithConfig(userPreferences *lsutil.UserPreferences) { - s.Configure(userPreferences.CopyOrDefault()) + s.Configure(userPreferences) } func (s *Session) DidOpenFile(ctx context.Context, uri lsproto.DocumentUri, version int32, content string, languageKind lsproto.LanguageKind) { @@ -698,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..1b48283af4 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -866,7 +866,8 @@ func TestSession(t *testing.T) { session.Configure(&lsutil.UserPreferences{}) // Change user preferences for code lens and inlay hints. - newPrefs := session.UserPreferences() + // Create a copy since prefs are immutable - Configure uses pointer comparison to detect changes. + 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 20f6ea2f45..8f4a25ce81 100644 --- a/internal/project/snapshot.go +++ b/internal/project/snapshot.go @@ -334,7 +334,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 From 67db3e967b9bc88bf2894fe46ed10fdbbe51d693 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:12:31 -0800 Subject: [PATCH 08/19] more unused copies --- internal/fourslash/fourslash.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index b9e223b88d..40dbca6079 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -646,7 +646,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) @@ -3602,7 +3602,7 @@ 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 { From 18be10206188c350fed28d901c909488785d05a8 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:40:29 -0800 Subject: [PATCH 09/19] Restore comments --- internal/ls/lsutil/userpreferences.go | 144 ++++++++++++++++++++------ internal/project/session_test.go | 1 - 2 files changed, 113 insertions(+), 32 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index f4441682c9..fe2e126f1a 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -39,48 +39,129 @@ func NewDefaultUserPreferences() *UserPreferences { // The `pref` tag format: "path.to.setting" or "path.to.setting,invert" for boolean inversion. type UserPreferences struct { QuotePreference QuotePreference `pref:"preferences.quoteStyle"` - LazyConfiguredProjectsFromExternalProject bool - MaximumHoverLength int - - IncludeCompletionsForModuleExports core.Tristate `pref:"suggest.autoImports"` - IncludeCompletionsForImportStatements core.Tristate `pref:"suggest.includeCompletionsForImportStatements"` - IncludeAutomaticOptionalChainCompletions core.Tristate `pref:"suggest.includeAutomaticOptionalChainCompletions"` - IncludeCompletionsWithSnippetText core.Tristate - IncludeCompletionsWithClassMemberSnippets core.Tristate `pref:"suggest.classMemberSnippets.enabled"` - IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `pref:"suggest.objectLiteralMethodSnippets.enabled"` + LazyConfiguredProjectsFromExternalProject bool // !!! + + // A positive integer indicating the maximum length of a hover text before it is truncated. + // + // Default: `500` + MaximumHoverLength int // !!! + + // ------- 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 `pref:"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 `pref:"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 `pref:"suggest.includeAutomaticOptionalChainCompletions"` + // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. + IncludeCompletionsWithSnippetText core.Tristate // !!! + // 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 `pref:"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 `pref:"suggest.objectLiteralMethodSnippets.enabled"` // !!! JsxAttributeCompletionStyle JsxAttributeCompletionStyle `pref:"preferences.jsxAttributeCompletionStyle"` - ModuleSpecifier ModuleSpecifierUserPreferences + // ------- AutoImports -------- - IncludePackageJsonAutoImports IncludePackageJsonAutoImports `pref:"preferences.includePackageJsonAutoImports"` - AutoImportFileExcludePatterns []string `pref:"preferences.autoImportFileExcludePatterns"` - PreferTypeOnlyAutoImports bool `pref:"preferences.preferTypeOnlyAutoImports"` + ModuleSpecifier ModuleSpecifierUserPreferences - OrganizeImportsIgnoreCase core.Tristate `pref:"preferences.organizeImports.caseSensitivity"` - OrganizeImportsCollation OrganizeImportsCollation `pref:"preferences.organizeImports.unicodeCollation"` - OrganizeImportsLocale string `pref:"preferences.organizeImports.locale"` - OrganizeImportsNumericCollation bool `pref:"preferences.organizeImports.numericCollation"` - OrganizeImportsAccentCollation bool `pref:"preferences.organizeImports.accentCollation"` - OrganizeImportsCaseFirst OrganizeImportsCaseFirst `pref:"preferences.organizeImports.caseFirst"` - OrganizeImportsTypeOrder OrganizeImportsTypeOrder `pref:"preferences.organizeImports.typeOrder"` + IncludePackageJsonAutoImports IncludePackageJsonAutoImports `pref:"preferences.includePackageJsonAutoImports"` // !!! + AutoImportFileExcludePatterns []string `pref:"preferences.autoImportFileExcludePatterns"` // !!! + PreferTypeOnlyAutoImports bool `pref:"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 `pref:"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 `pref:"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 `pref:"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 `pref:"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. + // + // This preference is ignored if organizeImportsCollation is not `unicode`. + // + // Default: `true` + OrganizeImportsAccentCollation bool `pref:"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. + // + // Default: `false` + OrganizeImportsCaseFirst OrganizeImportsCaseFirst `pref:"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 `pref:"preferences.organizeImports.typeOrder"` // !!! + + // ------- MoveToFile ------- + + AllowTextChangesInNewFiles bool // !!! + + // ------- Rename ------- + + // renamed from `providePrefixAndSuffixTextForRename` + UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames,alias:providePrefixAndSuffixTextForRename"` + AllowRenameOfImportPath bool // !!! - AllowTextChangesInNewFiles bool + // ------- CodeFixes/Refactors ------- - UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames,alias:providePrefixAndSuffixTextForRename"` - AllowRenameOfImportPath bool + ProvideRefactorNotApplicableReason bool // !!! - ProvideRefactorNotApplicableReason bool + // ------- InlayHints ------- InlayHints InlayHintsPreferences + // ------- CodeLens ------- + CodeLens CodeLensUserPreferences + // ------- Symbols ------- + ExcludeLibrarySymbolsInNavTo bool `pref:"workspaceSymbols.excludeLibrarySymbols"` - DisableSuggestions bool - DisableLineTextInReferences bool - DisplayPartsForJSDoc bool - ReportStyleChecksAsWarnings bool + // ------- Misc ------- + + DisableSuggestions bool // !!! + DisableLineTextInReferences bool // !!! + DisplayPartsForJSDoc bool // !!! + ReportStyleChecksAsWarnings bool // !!! If this changes, we need to ask the client to recompute diagnostics } type InlayHintsPreferences struct { @@ -103,9 +184,10 @@ type CodeLensUserPreferences struct { } type ModuleSpecifierUserPreferences struct { - ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `pref:"preferences.importModuleSpecifier"` - ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `pref:"preferences.importModuleSpecifierEnding"` - AutoImportSpecifierExcludeRegexes []string `pref:"preferences.autoImportSpecifierExcludeRegexes"` + ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `pref:"preferences.importModuleSpecifier"` // !!! + // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" + ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `pref:"preferences.importModuleSpecifierEnding"` // !!! + AutoImportSpecifierExcludeRegexes []string `pref:"preferences.autoImportSpecifierExcludeRegexes"` // !!! } // --- Enum Types --- diff --git a/internal/project/session_test.go b/internal/project/session_test.go index 1b48283af4..b8429dcae0 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -866,7 +866,6 @@ func TestSession(t *testing.T) { session.Configure(&lsutil.UserPreferences{}) // Change user preferences for code lens and inlay hints. - // Create a copy since prefs are immutable - Configure uses pointer comparison to detect changes. newPrefs := session.UserPreferences().Copy() newPrefs.CodeLens.ReferencesCodeLensEnabled = !newPrefs.CodeLens.ReferencesCodeLensEnabled newPrefs.InlayHints.IncludeInlayFunctionLikeReturnTypeHints = !newPrefs.InlayHints.IncludeInlayFunctionLikeReturnTypeHints From 7ff185eb5cbbcb6f8b527aa68a9fe0a37430db7e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:55:20 -0800 Subject: [PATCH 10/19] Default just pointer --- internal/fourslash/fourslash.go | 8 +++---- internal/ls/lsutil/userpreferences.go | 30 +++++++++++++-------------- internal/lsp/server.go | 4 ++-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 40dbca6079..5813e38426 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, // !!! parse default preferences for fourslash case? vfs: fs, scriptInfos: scriptInfos, converters: converters, @@ -1260,7 +1260,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) @@ -3282,7 +3282,7 @@ func (f *FourslashTest) VerifyBaselineInlayHints( preferences := testPreferences if preferences == nil { - preferences = lsutil.NewDefaultUserPreferences() + preferences = lsutil.DefaultUserPreferences } reset := f.ConfigureWithReset(t, preferences) defer reset() @@ -3606,7 +3606,7 @@ func (f *FourslashTest) VerifyWorkspaceSymbol(t *testing.T, cases []*VerifyWorks 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/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index fe2e126f1a..d9d13369e0 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -12,20 +12,18 @@ import ( "github.com/microsoft/typescript-go/internal/modulespecifiers" ) -func NewDefaultUserPreferences() *UserPreferences { - return &UserPreferences{ - IncludeCompletionsForModuleExports: core.TSTrue, - IncludeCompletionsForImportStatements: core.TSTrue, - - AllowRenameOfImportPath: true, - ProvideRefactorNotApplicableReason: true, - IncludeCompletionsWithSnippetText: core.TSTrue, - DisplayPartsForJSDoc: true, - DisableLineTextInReferences: true, - ReportStyleChecksAsWarnings: true, - - ExcludeLibrarySymbolsInNavTo: true, - } +var DefaultUserPreferences = &UserPreferences{ + IncludeCompletionsForModuleExports: core.TSTrue, + IncludeCompletionsForImportStatements: core.TSTrue, + + AllowRenameOfImportPath: true, + ProvideRefactorNotApplicableReason: true, + IncludeCompletionsWithSnippetText: core.TSTrue, + DisplayPartsForJSDoc: true, + DisableLineTextInReferences: true, + ReportStyleChecksAsWarnings: true, + + ExcludeLibrarySymbolsInNavTo: true, } // UserPreferences represents TypeScript language service preferences. @@ -681,7 +679,7 @@ func (p *UserPreferences) UnmarshalJSONFrom(dec *jsontext.Decoder) error { return err } // Start with defaults, then overlay parsed values - *p = *NewDefaultUserPreferences() + *p = *DefaultUserPreferences.Copy() p.parseWorker(config) return nil } @@ -750,7 +748,7 @@ func ParseUserPreferences(item any) *UserPreferences { return nil } if config, ok := item.(map[string]any); ok { - p := NewDefaultUserPreferences() + p := DefaultUserPreferences.Copy() p.parseWorker(config) return p } diff --git a/internal/lsp/server.go b/internal/lsp/server.go index d2afaafb31..92718492de 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -274,7 +274,7 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferen caps := lsproto.GetClientCapabilities(ctx) if !caps.Workspace.Configuration { // if no configuration request capability, return default preferences - return lsutil.NewDefaultUserPreferences(), nil + return lsutil.DefaultUserPreferences, nil } configs, err := sendClientRequest(ctx, s, lsproto.WorkspaceConfigurationInfo, &lsproto.ConfigurationParams{ Items: []*lsproto.ConfigurationItem{ @@ -292,7 +292,7 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserPreferen return parsed, nil } } - return lsutil.NewDefaultUserPreferences(), nil + return lsutil.DefaultUserPreferences, nil } func (s *Server) Run(ctx context.Context) error { From 0c95a82f43f3960f069f6f461281f431d093e6fe Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:55:44 -0800 Subject: [PATCH 11/19] Remove --- internal/fourslash/fourslash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 5813e38426..d5fa4f726a 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.DefaultUserPreferences, // !!! parse default preferences for fourslash case? + userPreferences: lsutil.DefaultUserPreferences, vfs: fs, scriptInfos: scriptInfos, converters: converters, From 8b08250dc7167d9964072232427805ab4cd23db6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:22:51 -0800 Subject: [PATCH 12/19] Cleanup --- internal/ls/lsutil/userpreferences.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index d9d13369e0..656b41b4f4 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -369,6 +369,7 @@ var typeParsers = map[reflect.Type]func(any) any{ } // 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) { @@ -380,13 +381,6 @@ var typeSerializers = map[reflect.Type]func(any) any{ return nil } }, - reflect.TypeFor[IncludeInlayParameterNameHints](): func(val any) any { - s := val.(IncludeInlayParameterNameHints) - if s == "" { - return "none" - } - return string(s) - }, reflect.TypeFor[OrganizeImportsCollation](): func(val any) any { if val.(OrganizeImportsCollation) == OrganizeImportsCollationUnicode { return "unicode" From 46cf5f1cc487bf9e56085af3d4d72145c4cf6a83 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:54:59 -0800 Subject: [PATCH 13/19] remove old dead test code --- internal/ls/lsutil/userpreferences_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index 06fb6ac3d1..93a8c5d8cb 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -9,15 +9,13 @@ import ( "gotest.tools/v3/assert" ) -func fillNonZeroValues(v reflect.Value, path string) { +func fillNonZeroValues(v reflect.Value) { t := v.Type() for i := range t.NumField() { field := v.Field(i) - fieldType := t.Field(i) if !field.CanSet() { continue } - fieldPath := path + "." + fieldType.Name switch field.Kind() { case reflect.Bool: field.SetBool(true) @@ -26,19 +24,19 @@ func fillNonZeroValues(v reflect.Value, path string) { case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: field.SetUint(1) case reflect.String: - val := getValidStringValue(field.Type(), fieldPath) + 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, fieldPath) + fillNonZeroValues(field) } } } -func getValidStringValue(t reflect.Type, path string) string { +func getValidStringValue(t reflect.Type) string { typeName := t.String() switch typeName { case "lsutil.QuotePreference": @@ -62,7 +60,7 @@ func TestUserPreferencesRoundtrip(t *testing.T) { t.Parallel() original := &UserPreferences{} - fillNonZeroValues(reflect.ValueOf(original).Elem(), "UserPreferences") + fillNonZeroValues(reflect.ValueOf(original).Elem()) jsonBytes, err := json.Marshal(original) assert.NilError(t, err) From 711f30227d1f0447e16256098eacc647a28f9e22 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:56:48 -0800 Subject: [PATCH 14/19] Just return --- internal/ls/lsutil/userpreferences.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 656b41b4f4..6d034be23f 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -420,9 +420,7 @@ type fieldInfo struct { } var fieldInfoCache = sync.OnceValue(func() []fieldInfo { - var infos []fieldInfo - collectFieldInfos(reflect.TypeFor[UserPreferences](), nil, &infos) - return infos + return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil) }) // lowerNameIndex maps lowercase field names to fieldInfo index for O(1) lookup @@ -438,13 +436,14 @@ var lowerNameIndex = sync.OnceValue(func() map[string]int { return index }) -func collectFieldInfos(t reflect.Type, indexPath []int, infos *[]fieldInfo) { +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) if field.Type.Kind() == reflect.Struct && field.Tag.Get("pref") == "" { - collectFieldInfos(field.Type, currentPath, infos) + infos = append(infos, collectFieldInfos(field.Type, currentPath)...) continue } @@ -467,8 +466,9 @@ func collectFieldInfos(t reflect.Type, indexPath []int, infos *[]fieldInfo) { } } - *infos = append(*infos, info) + infos = append(infos, info) } + return infos } func getNestedValue(config map[string]any, path string) (any, bool) { From 6c703eb26e1d6e3f8250555092ef0ad8e9400cc9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:04:43 -0800 Subject: [PATCH 15/19] Delete alias support --- internal/ls/lsutil/userpreferences.go | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 6d034be23f..a146159e16 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -134,8 +134,7 @@ type UserPreferences struct { // ------- Rename ------- - // renamed from `providePrefixAndSuffixTextForRename` - UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames,alias:providePrefixAndSuffixTextForRename"` + UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames"` AllowRenameOfImportPath bool // !!! // ------- CodeFixes/Refactors ------- @@ -412,11 +411,10 @@ var typeSerializers = map[reflect.Type]func(any) any{ } type fieldInfo struct { - lowerName string // lowercase Go field name for direct matching - aliases []string // additional lowercase names (e.g., "provideprefixandsuffixtextforrename") - path string // dotted path for VS Code config (e.g., "preferences.quoteStyle") - fieldPath []int // index path to field in struct - invert bool // whether to invert boolean values + lowerName string // lowercase Go field name for direct matching + path string // dotted path for VS Code config (e.g., "preferences.quoteStyle") + fieldPath []int // index path to field in struct + invert bool // whether to invert boolean values } var fieldInfoCache = sync.OnceValue(func() []fieldInfo { @@ -426,12 +424,9 @@ var fieldInfoCache = sync.OnceValue(func() []fieldInfo { // lowerNameIndex maps lowercase field names to fieldInfo index for O(1) lookup var lowerNameIndex = sync.OnceValue(func() map[string]int { infos := fieldInfoCache() - index := make(map[string]int, len(infos)*2) + index := make(map[string]int, len(infos)) for i, info := range infos { index[info.lowerName] = i - for _, alias := range info.aliases { - index[alias] = i - } } return index }) @@ -454,14 +449,12 @@ func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo { tag := field.Tag.Get("pref") if tag != "" { - // Parse tag: "path" or "path,invert" or "path,alias:name" + // Parse tag: "path" or "path,invert" parts := strings.Split(tag, ",") info.path = parts[0] for _, part := range parts[1:] { if part == "invert" { info.invert = true - } else if alias, ok := strings.CutPrefix(part, "alias:"); ok { - info.aliases = append(info.aliases, strings.ToLower(alias)) } } } From 4728fbabc177e8d25373d9cc8eb4a7d1fadd3588 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:35:11 -0800 Subject: [PATCH 16/19] Explict unstable --- internal/ls/lsutil/userpreferences.go | 109 ++++++---------- internal/ls/lsutil/userpreferences_test.go | 145 +++++++++++++++++++++ 2 files changed, 184 insertions(+), 70 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index a146159e16..925a2ec6b2 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -28,21 +28,17 @@ var DefaultUserPreferences = &UserPreferences{ // UserPreferences represents TypeScript language service preferences. // -// Fields can be populated from two sources: -// 1. Direct property names (from VS Code's "unstable" config or direct tsserver protocol) -// These are matched case-insensitively by Go field name. -// 2. VS Code's nested config structure (e.g., "suggest.autoImports", "inlayHints.parameterNames.enabled") -// These use the `pref` tag to specify the dotted path. -// +// Fields are populated from VS Code's nested config structure using the `pref` tag. // The `pref` tag format: "path.to.setting" or "path.to.setting,invert" for boolean inversion. +// Fields under "unstable.*" are experimental and may change. type UserPreferences struct { QuotePreference QuotePreference `pref:"preferences.quoteStyle"` - LazyConfiguredProjectsFromExternalProject bool // !!! + LazyConfiguredProjectsFromExternalProject bool `pref:"unstable.lazyConfiguredProjectsFromExternalProject"` // !!! // A positive integer indicating the maximum length of a hover text before it is truncated. // // Default: `500` - MaximumHoverLength int // !!! + MaximumHoverLength int `pref:"unstable.maximumHoverLength"` // !!! // ------- Completions ------- @@ -57,7 +53,7 @@ type UserPreferences struct { // preceding `.` tokens with `?.`. IncludeAutomaticOptionalChainCompletions core.Tristate `pref:"suggest.includeAutomaticOptionalChainCompletions"` // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. - IncludeCompletionsWithSnippetText core.Tristate // !!! + IncludeCompletionsWithSnippetText core.Tristate `pref:"unstable.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 @@ -130,16 +126,16 @@ type UserPreferences struct { // ------- MoveToFile ------- - AllowTextChangesInNewFiles bool // !!! + AllowTextChangesInNewFiles bool `pref:"unstable.allowTextChangesInNewFiles"` // !!! // ------- Rename ------- UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames"` - AllowRenameOfImportPath bool // !!! + AllowRenameOfImportPath bool `pref:"unstable.allowRenameOfImportPath"` // !!! // ------- CodeFixes/Refactors ------- - ProvideRefactorNotApplicableReason bool // !!! + ProvideRefactorNotApplicableReason bool `pref:"unstable.provideRefactorNotApplicableReason"` // !!! // ------- InlayHints ------- @@ -155,10 +151,10 @@ type UserPreferences struct { // ------- Misc ------- - DisableSuggestions bool // !!! - DisableLineTextInReferences bool // !!! - DisplayPartsForJSDoc bool // !!! - ReportStyleChecksAsWarnings bool // !!! If this changes, we need to ask the client to recompute diagnostics + DisableSuggestions bool `pref:"unstable.disableSuggestions"` // !!! + DisableLineTextInReferences bool `pref:"unstable.disableLineTextInReferences"` // !!! + DisplayPartsForJSDoc bool `pref:"unstable.displayPartsForJSDoc"` // !!! + ReportStyleChecksAsWarnings bool `pref:"unstable.reportStyleChecksAsWarnings"` // !!! If this changes, we need to ask the client to recompute diagnostics } type InlayHintsPreferences struct { @@ -411,8 +407,8 @@ var typeSerializers = map[reflect.Type]func(any) any{ } type fieldInfo struct { - lowerName string // lowercase Go field name for direct matching - path string // dotted path for VS Code config (e.g., "preferences.quoteStyle") + fieldName string // Go field name for unstable section lookup + path string // dotted path for config (e.g., "preferences.quoteStyle" or "unstable.fieldName") fieldPath []int // index path to field in struct invert bool // whether to invert boolean values } @@ -421,12 +417,12 @@ var fieldInfoCache = sync.OnceValue(func() []fieldInfo { return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil) }) -// lowerNameIndex maps lowercase field names to fieldInfo index for O(1) lookup -var lowerNameIndex = sync.OnceValue(func() map[string]int { +// fieldNameIndex maps lowercase field names to fieldInfo index +var fieldNameIndex = sync.OnceValue(func() map[string]int { infos := fieldInfoCache() index := make(map[string]int, len(infos)) for i, info := range infos { - index[info.lowerName] = i + index[strings.ToLower(info.fieldName)] = i } return index }) @@ -437,25 +433,26 @@ func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo { field := t.Field(i) currentPath := append(slices.Clone(indexPath), i) - if field.Type.Kind() == reflect.Struct && field.Tag.Get("pref") == "" { - infos = append(infos, collectFieldInfos(field.Type, currentPath)...) - continue + tag := field.Tag.Get("pref") + if tag == "" { + // Embedded struct without pref tag - recurse into it + if field.Type.Kind() == reflect.Struct { + infos = append(infos, collectFieldInfos(field.Type, currentPath)...) + continue + } + panic("pref tag required for field " + field.Name) } + // Parse tag: "path" or "path,invert" + parts := strings.Split(tag, ",") info := fieldInfo{ - lowerName: strings.ToLower(field.Name), + fieldName: field.Name, + path: parts[0], fieldPath: currentPath, } - - tag := field.Tag.Get("pref") - if tag != "" { - // Parse tag: "path" or "path,invert" - parts := strings.Split(tag, ",") - info.path = parts[0] - for _, part := range parts[1:] { - if part == "invert" { - info.invert = true - } + for _, part := range parts[1:] { + if part == "invert" { + info.invert = true } } @@ -497,10 +494,12 @@ func setNestedValue(config map[string]any, path string, value any) { func (p *UserPreferences) parseWorker(config map[string]any) { v := reflect.ValueOf(p).Elem() infos := fieldInfoCache() - index := lowerNameIndex() - // Process "unstable" first - these are spread directly by field name + // Process "unstable" section first - allows any field to be set by Go field 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 := fieldNameIndex() for name, value := range unstable { if idx, found := index[strings.ToLower(name)]; found { info := infos[idx] @@ -515,11 +514,9 @@ func (p *UserPreferences) parseWorker(config map[string]any) { } } - // Process path-based config (VS Code style) + // 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.path == "" { - continue - } val, ok := getNestedValue(config, info.path) if !ok { continue @@ -533,29 +530,6 @@ func (p *UserPreferences) parseWorker(config map[string]any) { } setFieldFromValue(field, val) } - - // Process direct field names at root level (non-VS Code clients, fourslash tests) - for name, value := range config { - if name == "unstable" { - continue - } - // Skip known VS Code config sections that are handled by path-based parsing - switch name { - case "preferences", "suggest", "inlayHints", "referencesCodeLens", - "implementationsCodeLens", "workspaceSymbols", "format", "tsserver", "tsc", "experimental": - continue - } - if idx, found := index[strings.ToLower(name)]; found { - info := infos[idx] - field := getFieldByPath(v, info.fieldPath) - if info.invert { - if b, ok := value.(bool); ok { - value = !b - } - } - setFieldFromValue(field, value) - } - } } func getFieldByPath(v reflect.Value, path []int) reflect.Value { @@ -622,12 +596,7 @@ func (p *UserPreferences) MarshalJSONTo(enc *jsontext.Encoder) error { } } - // Use the path if available, otherwise use the lowercase field name at root level - if info.path != "" { - setNestedValue(config, info.path, val) - } else { - config[info.lowerName] = val - } + setNestedValue(config, info.path, val) } return json.MarshalEncode(enc, config) diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index 93a8c5d8cb..f8465096c1 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -83,3 +84,147 @@ func TestUserPreferencesRoundtrip(t *testing.T) { assert.DeepEqual(t, original, parsed) }) } + +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: true, + }, + }, + { + 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 Go field name", + json: `{ + "unstable": { + "quotePreference": "double", + "includeCompletionsForModuleExports": true, + "excludeLibrarySymbolsInNavTo": true + } + }`, + expected: &UserPreferences{ + QuotePreference: QuotePreferenceDouble, + IncludeCompletionsForModuleExports: core.TSTrue, + ExcludeLibrarySymbolsInNavTo: true, + }, + }, + } + + 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) + }) + } +} From 8ae6651ab547d9364f3d4f4bb42dea748e269987 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:37:37 -0800 Subject: [PATCH 17/19] test, what --- .../state/codeLensAcrossProjects.baseline | 217 ++++++++++-------- 1 file changed, 123 insertions(+), 94 deletions(-) diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 1d19ad7caa..89d9465c8b 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -162,55 +162,67 @@ 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": false, - "OrganizeImportsIgnoreCase": null, - "OrganizeImportsCollation": false, - "OrganizeImportsLocale": "", - "OrganizeImportsNumericCollation": false, - "OrganizeImportsAccentCollation": false, - "OrganizeImportsCaseFirst": 0, - "OrganizeImportsTypeOrder": 0, - "AllowTextChangesInNewFiles": false, - "UseAliasesForRename": null, - "AllowRenameOfImportPath": false, - "ProvideRefactorNotApplicableReason": false, - "InlayHints": { - "IncludeInlayParameterNameHints": "", - "IncludeInlayParameterNameHintsWhenArgumentMatchesName": false, - "IncludeInlayFunctionParameterTypeHints": false, - "IncludeInlayVariableTypeHints": false, - "IncludeInlayVariableTypeHintsWhenTypeMatchesName": false, - "IncludeInlayPropertyDeclarationTypeHints": false, - "IncludeInlayFunctionLikeReturnTypeHints": false, - "IncludeInlayEnumMemberValueHints": false + "referencesCodeLens": { + "enabled": true, + "showOnAllFunctions": true }, - "CodeLens": { - "ReferencesCodeLensEnabled": true, - "ImplementationsCodeLensEnabled": true, - "ReferencesCodeLensShowOnAllFunctions": true, - "ImplementationsCodeLensShowOnInterfaceMethods": true, - "ImplementationsCodeLensShowOnAllClassMethods": true + "implementationsCodeLens": { + "enabled": true, + "showOnInterfaceMethods": true, + "showOnAllClassMethods": true }, - "ExcludeLibrarySymbolsInNavTo": false, - "DisableSuggestions": false, - "DisableLineTextInReferences": false, - "DisplayPartsForJSDoc": false, - "ReportStyleChecksAsWarnings": false + "workspaceSymbols": { + "excludeLibrarySymbols": false + }, + "preferences": { + "importModuleSpecifier": "", + "importModuleSpecifierEnding": "", + "includePackageJsonAutoImports": "", + "preferTypeOnlyAutoImports": false, + "organizeImports": { + "accentCollation": false, + "caseFirst": "default", + "typeOrder": "auto", + "unicodeCollation": "ordinal", + "locale": "", + "numericCollation": false + }, + "quoteStyle": "", + "jsxAttributeCompletionStyle": "" + }, + "unstable": { + "disableLineTextInReferences": false, + "reportStyleChecksAsWarnings": false, + "maximumHoverLength": 0, + "allowRenameOfImportPath": false, + "provideRefactorNotApplicableReason": false, + "displayPartsForJSDoc": false, + "lazyConfiguredProjectsFromExternalProject": false, + "allowTextChangesInNewFiles": false, + "disableSuggestions": false + }, + "inlayHints": { + "functionLikeReturnTypes": { + "enabled": false + }, + "enumMemberValues": { + "enabled": false + }, + "parameterNames": { + "enabled": "", + "suppressWhenArgumentMatchesName": true + }, + "parameterTypes": { + "enabled": false + }, + "variableTypes": { + "enabled": false, + "suppressWhenTypeMatchesName": true + }, + "propertyDeclarationTypes": { + "enabled": false + } + } } } } @@ -610,55 +622,72 @@ 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": false, - "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 + "unstable": { + "maximumHoverLength": 0, + "includeCompletionsWithSnippetText": true, + "allowRenameOfImportPath": true, + "reportStyleChecksAsWarnings": true, + "allowTextChangesInNewFiles": false, + "provideRefactorNotApplicableReason": true, + "disableSuggestions": false, + "disableLineTextInReferences": true, + "displayPartsForJSDoc": true, + "lazyConfiguredProjectsFromExternalProject": false + }, + "suggest": { + "autoImports": true, + "includeCompletionsForImportStatements": true + }, + "inlayHints": { + "enumMemberValues": { + "enabled": false + }, + "parameterNames": { + "enabled": "", + "suppressWhenArgumentMatchesName": true + }, + "parameterTypes": { + "enabled": false + }, + "variableTypes": { + "enabled": false, + "suppressWhenTypeMatchesName": true + }, + "propertyDeclarationTypes": { + "enabled": false + }, + "functionLikeReturnTypes": { + "enabled": false + } + }, + "referencesCodeLens": { + "enabled": false, + "showOnAllFunctions": false + }, + "implementationsCodeLens": { + "showOnAllClassMethods": false, + "enabled": false, + "showOnInterfaceMethods": false }, - "CodeLens": { - "ReferencesCodeLensEnabled": false, - "ImplementationsCodeLensEnabled": false, - "ReferencesCodeLensShowOnAllFunctions": false, - "ImplementationsCodeLensShowOnInterfaceMethods": false, - "ImplementationsCodeLensShowOnAllClassMethods": false + "workspaceSymbols": { + "excludeLibrarySymbols": true }, - "ExcludeLibrarySymbolsInNavTo": true, - "DisableSuggestions": false, - "DisableLineTextInReferences": true, - "DisplayPartsForJSDoc": true, - "ReportStyleChecksAsWarnings": true + "preferences": { + "importModuleSpecifierEnding": "", + "includePackageJsonAutoImports": "", + "preferTypeOnlyAutoImports": false, + "organizeImports": { + "numericCollation": false, + "accentCollation": false, + "caseFirst": "default", + "typeOrder": "auto", + "unicodeCollation": "ordinal", + "locale": "" + }, + "quoteStyle": "", + "jsxAttributeCompletionStyle": "", + "importModuleSpecifier": "" + } } } } From 74c815b5f80fff4e21105054349cf9c4a5470ca9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 20:51:20 -0800 Subject: [PATCH 18/19] Move stuff around but I don't know how to improve quoteStyle, quotePreference, etc --- internal/ls/lsutil/userpreferences.go | 24 ++++++++++++++-------- internal/ls/lsutil/userpreferences_test.go | 10 ++++----- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 925a2ec6b2..5ad1710310 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -407,8 +407,7 @@ var typeSerializers = map[reflect.Type]func(any) any{ } type fieldInfo struct { - fieldName string // Go field name for unstable section lookup - path string // dotted path for config (e.g., "preferences.quoteStyle" or "unstable.fieldName") + path string // dotted path for config (e.g., "preferences.quoteStyle" or "unstable.disableSuggestions") fieldPath []int // index path to field in struct invert bool // whether to invert boolean values } @@ -417,12 +416,20 @@ var fieldInfoCache = sync.OnceValue(func() []fieldInfo { return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil) }) -// fieldNameIndex maps lowercase field names to fieldInfo index -var fieldNameIndex = sync.OnceValue(func() map[string]int { +// unstableNameIndex maps the camelCase name (from "unstable.name" paths) to fieldInfo index. +// This allows any field to be set via the unstable section using its camelCase name. +var unstableNameIndex = sync.OnceValue(func() map[string]int { infos := fieldInfoCache() index := make(map[string]int, len(infos)) for i, info := range infos { - index[strings.ToLower(info.fieldName)] = i + // Extract the last part of any path as the unstable key + // e.g., "preferences.quoteStyle" -> "quoteStyle" + // e.g., "unstable.disableSuggestions" -> "disableSuggestions" + if idx := strings.LastIndex(info.path, "."); idx >= 0 { + index[info.path[idx+1:]] = i + } else { + index[info.path] = i + } } return index }) @@ -446,7 +453,6 @@ func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo { // Parse tag: "path" or "path,invert" parts := strings.Split(tag, ",") info := fieldInfo{ - fieldName: field.Name, path: parts[0], fieldPath: currentPath, } @@ -495,13 +501,13 @@ 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 Go field name. + // Process "unstable" section first - allows any field to be set by camelCase 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 := fieldNameIndex() + index := unstableNameIndex() for name, value := range unstable { - if idx, found := index[strings.ToLower(name)]; found { + if idx, found := index[name]; found { info := infos[idx] field := getFieldByPath(v, info.fieldPath) if info.invert { diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index f8465096c1..7071c4b809 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -176,7 +176,7 @@ func TestUserPreferencesParseUnstable(t *testing.T) { name: "stable config overrides unstable", json: `{ "unstable": { - "quotePreference": "double" + "quoteStyle": "double" }, "preferences": { "quoteStyle": "single" @@ -198,12 +198,12 @@ func TestUserPreferencesParseUnstable(t *testing.T) { }, }, { - name: "any field can be passed via unstable by Go field name", + name: "any field can be passed via unstable by its camelCase name", json: `{ "unstable": { - "quotePreference": "double", - "includeCompletionsForModuleExports": true, - "excludeLibrarySymbolsInNavTo": true + "quoteStyle": "double", + "autoImports": true, + "excludeLibrarySymbols": true } }`, expected: &UserPreferences{ From 2b4b1f618b05f8d903e376a1adac7399621c908a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 8 Dec 2025 21:44:04 -0800 Subject: [PATCH 19/19] Try and deal with old vs new --- internal/ls/lsutil/userpreferences.go | 188 ++++++++++-------- internal/ls/lsutil/userpreferences_test.go | 110 +++++++++- .../state/codeLensAcrossProjects.baseline | 162 +++++++-------- 3 files changed, 294 insertions(+), 166 deletions(-) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index 5ad1710310..c929bc376e 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -28,79 +28,82 @@ var DefaultUserPreferences = &UserPreferences{ // UserPreferences represents TypeScript language service preferences. // -// Fields are populated from VS Code's nested config structure using the `pref` tag. -// The `pref` tag format: "path.to.setting" or "path.to.setting,invert" for boolean inversion. -// Fields under "unstable.*" are experimental and may change. +// 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 `pref:"preferences.quoteStyle"` - LazyConfiguredProjectsFromExternalProject bool `pref:"unstable.lazyConfiguredProjectsFromExternalProject"` // !!! + 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 `pref:"unstable.maximumHoverLength"` // !!! + 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 `pref:"suggest.autoImports"` + 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 `pref:"suggest.includeCompletionsForImportStatements"` + 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 `pref:"suggest.includeAutomaticOptionalChainCompletions"` + IncludeAutomaticOptionalChainCompletions core.Tristate `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"` // Allows completions to be formatted with snippet text, indicated by `CompletionItem["isSnippet"]`. - IncludeCompletionsWithSnippetText core.Tristate `pref:"unstable.includeCompletionsWithSnippetText"` // !!! + 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 `pref:"suggest.classMemberSnippets.enabled"` // !!! + 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 `pref:"suggest.objectLiteralMethodSnippets.enabled"` // !!! - JsxAttributeCompletionStyle JsxAttributeCompletionStyle `pref:"preferences.jsxAttributeCompletionStyle"` + IncludeCompletionsWithObjectLiteralMethodSnippets core.Tristate `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!! + JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"` // ------- AutoImports -------- ModuleSpecifier ModuleSpecifierUserPreferences - IncludePackageJsonAutoImports IncludePackageJsonAutoImports `pref:"preferences.includePackageJsonAutoImports"` // !!! - AutoImportFileExcludePatterns []string `pref:"preferences.autoImportFileExcludePatterns"` // !!! - PreferTypeOnlyAutoImports bool `pref:"preferences.preferTypeOnlyAutoImports"` // !!! + IncludePackageJsonAutoImports IncludePackageJsonAutoImports `raw:"includePackageJsonAutoImports" config:"preferences.includePackageJsonAutoImports"` // !!! + AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"` // !!! + PreferTypeOnlyAutoImports bool `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 `pref:"preferences.organizeImports.caseSensitivity"` // !!! + 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 `pref:"preferences.organizeImports.unicodeCollation"` // !!! + 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 `pref:"preferences.organizeImports.locale"` // !!! + 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 `pref:"preferences.organizeImports.numericCollation"` // !!! + 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. @@ -108,7 +111,7 @@ type UserPreferences struct { // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `true` - OrganizeImportsAccentCollation bool `pref:"preferences.organizeImports.accentCollation"` // !!! + 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. // @@ -118,24 +121,24 @@ type UserPreferences struct { // - organizeImportsIgnoreCase is `auto` and the auto-detected case sensitivity is case-insensitive. // // Default: `false` - OrganizeImportsCaseFirst OrganizeImportsCaseFirst `pref:"preferences.organizeImports.caseFirst"` // !!! + 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 `pref:"preferences.organizeImports.typeOrder"` // !!! + OrganizeImportsTypeOrder OrganizeImportsTypeOrder `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!! // ------- MoveToFile ------- - AllowTextChangesInNewFiles bool `pref:"unstable.allowTextChangesInNewFiles"` // !!! + AllowTextChangesInNewFiles bool `raw:"allowTextChangesInNewFiles"` // !!! // ------- Rename ------- - UseAliasesForRename core.Tristate `pref:"preferences.useAliasesForRenames"` - AllowRenameOfImportPath bool `pref:"unstable.allowRenameOfImportPath"` // !!! + UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"` + AllowRenameOfImportPath bool `raw:"allowRenameOfImportPath"` // !!! // ------- CodeFixes/Refactors ------- - ProvideRefactorNotApplicableReason bool `pref:"unstable.provideRefactorNotApplicableReason"` // !!! + ProvideRefactorNotApplicableReason bool `raw:"provideRefactorNotApplicableReason"` // !!! // ------- InlayHints ------- @@ -147,40 +150,40 @@ type UserPreferences struct { // ------- Symbols ------- - ExcludeLibrarySymbolsInNavTo bool `pref:"workspaceSymbols.excludeLibrarySymbols"` + ExcludeLibrarySymbolsInNavTo bool `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"` // ------- Misc ------- - DisableSuggestions bool `pref:"unstable.disableSuggestions"` // !!! - DisableLineTextInReferences bool `pref:"unstable.disableLineTextInReferences"` // !!! - DisplayPartsForJSDoc bool `pref:"unstable.displayPartsForJSDoc"` // !!! - ReportStyleChecksAsWarnings bool `pref:"unstable.reportStyleChecksAsWarnings"` // !!! 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 `pref:"inlayHints.parameterNames.enabled"` - IncludeInlayParameterNameHintsWhenArgumentMatchesName bool `pref:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"` - IncludeInlayFunctionParameterTypeHints bool `pref:"inlayHints.parameterTypes.enabled"` - IncludeInlayVariableTypeHints bool `pref:"inlayHints.variableTypes.enabled"` - IncludeInlayVariableTypeHintsWhenTypeMatchesName bool `pref:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"` - IncludeInlayPropertyDeclarationTypeHints bool `pref:"inlayHints.propertyDeclarationTypes.enabled"` - IncludeInlayFunctionLikeReturnTypeHints bool `pref:"inlayHints.functionLikeReturnTypes.enabled"` - IncludeInlayEnumMemberValueHints bool `pref:"inlayHints.enumMemberValues.enabled"` + 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 `pref:"referencesCodeLens.enabled"` - ImplementationsCodeLensEnabled bool `pref:"implementationsCodeLens.enabled"` - ReferencesCodeLensShowOnAllFunctions bool `pref:"referencesCodeLens.showOnAllFunctions"` - ImplementationsCodeLensShowOnInterfaceMethods bool `pref:"implementationsCodeLens.showOnInterfaceMethods"` - ImplementationsCodeLensShowOnAllClassMethods bool `pref:"implementationsCodeLens.showOnAllClassMethods"` + 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 `pref:"preferences.importModuleSpecifier"` // !!! + 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 `pref:"preferences.importModuleSpecifierEnding"` // !!! - AutoImportSpecifierExcludeRegexes []string `pref:"preferences.autoImportSpecifierExcludeRegexes"` // !!! + ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!! + AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!! } // --- Enum Types --- @@ -407,28 +410,24 @@ var typeSerializers = map[reflect.Type]func(any) any{ } type fieldInfo struct { - path string // dotted path for config (e.g., "preferences.quoteStyle" or "unstable.disableSuggestions") - fieldPath []int // index path to field in struct - invert bool // whether to invert boolean values + 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 } var fieldInfoCache = sync.OnceValue(func() []fieldInfo { return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil) }) -// unstableNameIndex maps the camelCase name (from "unstable.name" paths) to fieldInfo index. -// This allows any field to be set via the unstable section using its camelCase name. +// 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 { - // Extract the last part of any path as the unstable key - // e.g., "preferences.quoteStyle" -> "quoteStyle" - // e.g., "unstable.disableSuggestions" -> "disableSuggestions" - if idx := strings.LastIndex(info.path, "."); idx >= 0 { - index[info.path[idx+1:]] = i - } else { - index[info.path] = i + if info.rawName != "" { + index[info.rawName] = i } } return index @@ -440,25 +439,41 @@ func collectFieldInfos(t reflect.Type, indexPath []int) []fieldInfo { field := t.Field(i) currentPath := append(slices.Clone(indexPath), i) - tag := field.Tag.Get("pref") - if tag == "" { - // Embedded struct without pref tag - recurse into it + rawTag := field.Tag.Get("raw") + configTag := field.Tag.Get("config") + + 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("pref tag required for field " + field.Name) + panic("raw or vscode tag required for field " + field.Name) } - // Parse tag: "path" or "path,invert" - parts := strings.Split(tag, ",") info := fieldInfo{ - path: parts[0], fieldPath: currentPath, } - for _, part := range parts[1:] { - if part == "invert" { - info.invert = true + + // 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 + } + } + } + + // 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 + } } } @@ -501,7 +516,7 @@ 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 camelCase name. + // 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 { @@ -510,7 +525,7 @@ func (p *UserPreferences) parseWorker(config map[string]any) { if idx, found := index[name]; found { info := infos[idx] field := getFieldByPath(v, info.fieldPath) - if info.invert { + if info.rawInvert { if b, ok := value.(bool); ok { value = !b } @@ -523,13 +538,16 @@ func (p *UserPreferences) parseWorker(config map[string]any) { // Process path-based config (VS Code style nested paths). // These run after unstable, so stable config values take precedence. for _, info := range infos { - val, ok := getNestedValue(config, info.path) + if info.configPath == "" { + continue + } + val, ok := getNestedValue(config, info.configPath) if !ok { continue } field := getFieldByPath(v, info.fieldPath) - if info.invert { + if info.configInvert { if b, ok := val.(bool); ok { val = !b } @@ -596,16 +614,26 @@ func (p *UserPreferences) MarshalJSONTo(enc *jsontext.Encoder) error { if val == nil { continue } - if info.invert { - if b, ok := val.(bool); ok { - val = !b + + // Prefer config path if available, otherwise use unstable section + if info.configPath != "" { + if info.configInvert { + if b, ok := val.(bool); ok { + val = !b + } + } + 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) } - - setNestedValue(config, info.path, val) } - return json.MarshalEncode(enc, config) + return json.MarshalEncode(enc, config, json.Deterministic(true)) } func serializeField(field reflect.Value) any { diff --git a/internal/ls/lsutil/userpreferences_test.go b/internal/ls/lsutil/userpreferences_test.go index 7071c4b809..feb6f074d1 100644 --- a/internal/ls/lsutil/userpreferences_test.go +++ b/internal/ls/lsutil/userpreferences_test.go @@ -85,6 +85,85 @@ func TestUserPreferencesRoundtrip(t *testing.T) { }) } +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() @@ -176,7 +255,7 @@ func TestUserPreferencesParseUnstable(t *testing.T) { name: "stable config overrides unstable", json: `{ "unstable": { - "quoteStyle": "double" + "quotePreference": "double" }, "preferences": { "quoteStyle": "single" @@ -198,12 +277,12 @@ func TestUserPreferencesParseUnstable(t *testing.T) { }, }, { - name: "any field can be passed via unstable by its camelCase name", + name: "any field can be passed via unstable by its raw name", json: `{ "unstable": { - "quoteStyle": "double", - "autoImports": true, - "excludeLibrarySymbols": true + "quotePreference": "double", + "includeCompletionsForModuleExports": true, + "excludeLibrarySymbolsInNavTo": true } }`, expected: &UserPreferences{ @@ -212,6 +291,27 @@ func TestUserPreferencesParseUnstable(t *testing.T) { 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 { diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 89d9465c8b..01665321ef 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -162,50 +162,16 @@ Config File Names:: "params": { "settings": { "typescript": { - "referencesCodeLens": { - "enabled": true, - "showOnAllFunctions": true - }, "implementationsCodeLens": { "enabled": true, - "showOnInterfaceMethods": true, - "showOnAllClassMethods": true - }, - "workspaceSymbols": { - "excludeLibrarySymbols": false - }, - "preferences": { - "importModuleSpecifier": "", - "importModuleSpecifierEnding": "", - "includePackageJsonAutoImports": "", - "preferTypeOnlyAutoImports": false, - "organizeImports": { - "accentCollation": false, - "caseFirst": "default", - "typeOrder": "auto", - "unicodeCollation": "ordinal", - "locale": "", - "numericCollation": false - }, - "quoteStyle": "", - "jsxAttributeCompletionStyle": "" - }, - "unstable": { - "disableLineTextInReferences": false, - "reportStyleChecksAsWarnings": false, - "maximumHoverLength": 0, - "allowRenameOfImportPath": false, - "provideRefactorNotApplicableReason": false, - "displayPartsForJSDoc": false, - "lazyConfiguredProjectsFromExternalProject": false, - "allowTextChangesInNewFiles": false, - "disableSuggestions": false + "showOnAllClassMethods": true, + "showOnInterfaceMethods": true }, "inlayHints": { - "functionLikeReturnTypes": { + "enumMemberValues": { "enabled": false }, - "enumMemberValues": { + "functionLikeReturnTypes": { "enabled": false }, "parameterNames": { @@ -215,13 +181,47 @@ Config File Names:: "parameterTypes": { "enabled": false }, + "propertyDeclarationTypes": { + "enabled": false + }, "variableTypes": { "enabled": false, "suppressWhenTypeMatchesName": true - }, - "propertyDeclarationTypes": { - "enabled": false } + }, + "preferences": { + "importModuleSpecifier": "", + "importModuleSpecifierEnding": "", + "includePackageJsonAutoImports": "", + "jsxAttributeCompletionStyle": "", + "organizeImports": { + "accentCollation": false, + "caseFirst": "default", + "locale": "", + "numericCollation": false, + "typeOrder": "auto", + "unicodeCollation": "ordinal" + }, + "preferTypeOnlyAutoImports": false, + "quoteStyle": "" + }, + "referencesCodeLens": { + "enabled": true, + "showOnAllFunctions": true + }, + "unstable": { + "allowRenameOfImportPath": false, + "allowTextChangesInNewFiles": false, + "disableLineTextInReferences": false, + "disableSuggestions": false, + "displayPartsForJSDoc": false, + "lazyConfiguredProjectsFromExternalProject": false, + "maximumHoverLength": 0, + "provideRefactorNotApplicableReason": false, + "reportStyleChecksAsWarnings": false + }, + "workspaceSymbols": { + "excludeLibrarySymbols": false } } } @@ -622,26 +622,18 @@ Config:: "params": { "settings": { "typescript": { - "unstable": { - "maximumHoverLength": 0, - "includeCompletionsWithSnippetText": true, - "allowRenameOfImportPath": true, - "reportStyleChecksAsWarnings": true, - "allowTextChangesInNewFiles": false, - "provideRefactorNotApplicableReason": true, - "disableSuggestions": false, - "disableLineTextInReferences": true, - "displayPartsForJSDoc": true, - "lazyConfiguredProjectsFromExternalProject": false - }, - "suggest": { - "autoImports": true, - "includeCompletionsForImportStatements": true + "implementationsCodeLens": { + "enabled": false, + "showOnAllClassMethods": false, + "showOnInterfaceMethods": false }, "inlayHints": { "enumMemberValues": { "enabled": false }, + "functionLikeReturnTypes": { + "enabled": false + }, "parameterNames": { "enabled": "", "suppressWhenArgumentMatchesName": true @@ -649,44 +641,52 @@ Config:: "parameterTypes": { "enabled": false }, - "variableTypes": { - "enabled": false, - "suppressWhenTypeMatchesName": true - }, "propertyDeclarationTypes": { "enabled": false }, - "functionLikeReturnTypes": { - "enabled": false + "variableTypes": { + "enabled": false, + "suppressWhenTypeMatchesName": true } }, - "referencesCodeLens": { - "enabled": false, - "showOnAllFunctions": false - }, - "implementationsCodeLens": { - "showOnAllClassMethods": false, - "enabled": false, - "showOnInterfaceMethods": false - }, - "workspaceSymbols": { - "excludeLibrarySymbols": true - }, "preferences": { + "importModuleSpecifier": "", "importModuleSpecifierEnding": "", "includePackageJsonAutoImports": "", - "preferTypeOnlyAutoImports": false, + "jsxAttributeCompletionStyle": "", "organizeImports": { - "numericCollation": false, "accentCollation": false, "caseFirst": "default", + "locale": "", + "numericCollation": false, "typeOrder": "auto", - "unicodeCollation": "ordinal", - "locale": "" + "unicodeCollation": "ordinal" }, - "quoteStyle": "", - "jsxAttributeCompletionStyle": "", - "importModuleSpecifier": "" + "preferTypeOnlyAutoImports": false, + "quoteStyle": "" + }, + "referencesCodeLens": { + "enabled": false, + "showOnAllFunctions": false + }, + "suggest": { + "autoImports": true, + "includeCompletionsForImportStatements": true + }, + "unstable": { + "allowRenameOfImportPath": true, + "allowTextChangesInNewFiles": false, + "disableLineTextInReferences": true, + "disableSuggestions": false, + "displayPartsForJSDoc": true, + "includeCompletionsWithSnippetText": true, + "lazyConfiguredProjectsFromExternalProject": false, + "maximumHoverLength": 0, + "provideRefactorNotApplicableReason": true, + "reportStyleChecksAsWarnings": true + }, + "workspaceSymbols": { + "excludeLibrarySymbols": true } } }