package lsutil import ( "reflect" "slices" "strings" "sync" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/json" "github.com/microsoft/typescript-go/internal/modulespecifiers" "github.com/microsoft/typescript-go/internal/vfs/vfsmatch" ) func NewDefaultUserPreferences() UserPreferences { return UserPreferences{ FormatCodeSettings: GetDefaultFormatCodeSettings(), IncludeCompletionsForModuleExports: core.TSTrue, IncludeCompletionsForImportStatements: core.TSTrue, EnableAutoClosingTags: core.TSTrue, EnableJSDocCompletions: core.TSTrue, GenerateReturnInDocTemplate: core.TSTrue, AllowRenameOfImportPath: core.TSTrue, ProvideRefactorNotApplicableReason: core.TSTrue, EnableFormatting: core.TSTrue, EnableValidation: core.TSTrue, DisplayPartsForJSDoc: core.TSTrue, DisableLineTextInReferences: core.TSTrue, ReportStyleChecksAsWarnings: core.TSTrue, ExcludeLibrarySymbolsInNavTo: core.TSTrue, } } // UserPreferences represents TypeScript language service preferences. // // Fields are populated using two tags: // - `raw:"name"` or `raw:"name,invert"` - TypeScript/raw name for unstable section lookup // - `config:"path.to.setting"` or `config:"path.to.setting,invert"` - VS Code nested config path // // At least one tag must be present on each preference field. // The `,invert` modifier inverts boolean values (e.g., VS Code's "suppress" -> our "include"). type UserPreferences struct { FormatCodeSettings FormatCodeSettings QuotePreference QuotePreference `raw:"quotePreference" config:"preferences.quoteStyle"` LazyConfiguredProjectsFromExternalProject core.Tristate `raw:"lazyConfiguredProjectsFromExternalProject"` // !!! // A positive integer indicating the maximum length of a hover text before it is truncated. // // Default: `500` 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 `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 `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 `raw:"includeAutomaticOptionalChainCompletions" config:"suggest.includeAutomaticOptionalChainCompletions"` // 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 `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 `raw:"includeCompletionsWithObjectLiteralMethodSnippets" config:"suggest.objectLiteralMethodSnippets.enabled"` // !!! JsxAttributeCompletionStyle JsxAttributeCompletionStyle `raw:"jsxAttributeCompletionStyle" config:"preferences.jsxAttributeCompletionStyle"` EnableAutoClosingTags core.Tristate `raw:"autoClosingTags" config:"autoClosingTags.enabled" fallbackConfig:"autoClosingTags"` EnableJSDocCompletions core.Tristate `raw:"completeJSDocs" config:"suggest.jsdoc.enabled" fallbackConfig:"suggest.completeJSDocs"` GenerateReturnInDocTemplate core.Tristate `raw:"generateReturnInDocTemplate" config:"suggest.jsdoc.generateReturns"` // ------- AutoImports -------- ImportModuleSpecifierPreference modulespecifiers.ImportModuleSpecifierPreference `raw:"importModuleSpecifierPreference" config:"preferences.importModuleSpecifier"` // !!! // Determines whether we import `foo/index.ts` as "foo", "foo/index", or "foo/index.js" ImportModuleSpecifierEnding modulespecifiers.ImportModuleSpecifierEndingPreference `raw:"importModuleSpecifierEnding" config:"preferences.importModuleSpecifierEnding"` // !!! AutoImportSpecifierExcludeRegexes []string `raw:"autoImportSpecifierExcludeRegexes" config:"preferences.autoImportSpecifierExcludeRegexes"` // !!! AutoImportFileExcludePatterns []string `raw:"autoImportFileExcludePatterns" config:"preferences.autoImportFileExcludePatterns"` AutoImportEntrypointDirectorySearch core.Tristate `raw:"autoImportEntrypointDirectorySearch" config:"preferences.autoImportEntrypointDirectorySearch"` PreferTypeOnlyAutoImports core.Tristate `raw:"preferTypeOnlyAutoImports" config:"preferences.preferTypeOnlyAutoImports"` // ------- OrganizeImports ------- // Indicates which deterministic preset should be used to sort imports. // "auto" detects the existing ordinal case sensitivity where possible. OrganizeImportsSort OrganizeImportsSort `raw:"organizeImportsSort" config:"preferences.organizeImports.sort"` // !!! // Indicates whether imports should be organized in a case-insensitive manner. // // Default: TSUnknown ("auto" in strada), will perform detection 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" natural sorting. This implementation is locale-agnostic and approximates the practical // import-sorting behavior rather than the full Unicode Collation Algorithm. // // Default: Ordinal OrganizeImportsCollation OrganizeImportsCollation `raw:"organizeImportsCollation" config:"preferences.organizeImports.unicodeCollation"` // !!! // Indicates the locale to use for "unicode" collation in legacy clients. This is accepted for compatibility, but // currently ignored because organize-import sorting is deterministic and locale-agnostic. // // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `"en"` 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 core.Tristate `raw:"organizeImportsNumericCollation" config:"preferences.organizeImports.numericCollation"` // !!! // Indicates whether accents and other diacritic marks are considered unequal for the purpose of sorting. // // This preference is ignored if organizeImportsCollation is not `unicode`. // // Default: `true` OrganizeImportsAccentCollation core.Tristate `raw:"organizeImportsAccentCollation" config:"preferences.organizeImports.accentCollation"` // !!! // Indicates whether upper case or lower case should sort first. // // This permission 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 `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 `raw:"organizeImportsTypeOrder" config:"preferences.organizeImports.typeOrder"` // !!! // ------- MoveToFile ------- AllowTextChangesInNewFiles core.Tristate `raw:"allowTextChangesInNewFiles"` // !!! // ------- Rename ------- UseAliasesForRename core.Tristate `raw:"providePrefixAndSuffixTextForRename" config:"preferences.useAliasesForRenames"` AllowRenameOfImportPath core.Tristate `raw:"allowRenameOfImportPath"` // ------- CodeFixes/Refactors ------- ProvideRefactorNotApplicableReason core.Tristate `raw:"provideRefactorNotApplicableReason"` // !!! // ------- InlayHints ------- InlayHints InlayHintsPreferences // ------- CodeLens ------- CodeLens CodeLensUserPreferences // ------- Definition ------- PreferGoToSourceDefinition bool `raw:"preferGoToSourceDefinition"` // ------- Symbols ------- ExcludeLibrarySymbolsInNavTo core.Tristate `raw:"excludeLibrarySymbolsInNavTo" config:"workspaceSymbols.excludeLibrarySymbols"` // ------- Misc ------- EnableFormatting core.Tristate `raw:"formatEnabled" config:"format.enabled" fallbackConfig:"format.enable"` EnableValidation core.Tristate `raw:"validateEnabled" config:"validate.enabled" fallbackConfig:"validate.enable"` DisableSuggestions core.Tristate `raw:"disableSuggestions"` // !!! DisableLineTextInReferences core.Tristate `raw:"disableLineTextInReferences"` // !!! DisplayPartsForJSDoc core.Tristate `raw:"displayPartsForJSDoc"` // !!! ReportStyleChecksAsWarnings core.Tristate `raw:"reportStyleChecksAsWarnings" config:"reportStyleChecksAsWarnings"` // ------- ATA ------- // DisableAutomaticTypeAcquisition is the deprecated setting from typescript.disableAutomaticTypeAcquisition. DisableAutomaticTypeAcquisition core.Tristate `raw:"disableAutomaticTypeAcquisition" config:"disableAutomaticTypeAcquisition"` // AutomaticTypeAcquisitionEnabled is the unified setting from tsserver.automaticTypeAcquisition.enabled under the js/ts section. // When set, it takes precedence over DisableAutomaticTypeAcquisition. AutomaticTypeAcquisitionEnabled core.Tristate `raw:"automaticTypeAcquisitionEnabled" config:"tsserver.automaticTypeAcquisition.enabled"` // TODO: add tsserver.web.typeAcquisition.enabled under the js/ts section for the web variant when web support is implemented. // ------- Project Configuration ------- // CustomConfigFileName specifies a custom config file name to use before defaulting to tsconfig.json/jsconfig.json. CustomConfigFileName string `raw:"customConfigFileName" config:"customConfigFileName"` } // IsATADisabled returns whether Automatic Type Acquisition is disabled based on user preferences. // It checks the unified setting (tsserver.automaticTypeAcquisition.enabled) first, // then falls back to the deprecated setting (disableAutomaticTypeAcquisition). func (p UserPreferences) IsATADisabled() bool { if !p.AutomaticTypeAcquisitionEnabled.IsUnknown() { return !p.AutomaticTypeAcquisitionEnabled.IsTrue() } return p.DisableAutomaticTypeAcquisition.IsTrue() } type InlayHintsPreferences struct { IncludeInlayParameterNameHints IncludeInlayParameterNameHints `raw:"includeInlayParameterNameHints" config:"inlayHints.parameterNames.enabled"` IncludeInlayParameterNameHintsWhenArgumentMatchesName core.Tristate `raw:"includeInlayParameterNameHintsWhenArgumentMatchesName" config:"inlayHints.parameterNames.suppressWhenArgumentMatchesName,invert"` IncludeInlayFunctionParameterTypeHints core.Tristate `raw:"includeInlayFunctionParameterTypeHints" config:"inlayHints.parameterTypes.enabled"` IncludeInlayVariableTypeHints core.Tristate `raw:"includeInlayVariableTypeHints" config:"inlayHints.variableTypes.enabled"` IncludeInlayVariableTypeHintsWhenTypeMatchesName core.Tristate `raw:"includeInlayVariableTypeHintsWhenTypeMatchesName" config:"inlayHints.variableTypes.suppressWhenTypeMatchesName,invert"` IncludeInlayPropertyDeclarationTypeHints core.Tristate `raw:"includeInlayPropertyDeclarationTypeHints" config:"inlayHints.propertyDeclarationTypes.enabled"` IncludeInlayFunctionLikeReturnTypeHints core.Tristate `raw:"includeInlayFunctionLikeReturnTypeHints" config:"inlayHints.functionLikeReturnTypes.enabled"` IncludeInlayEnumMemberValueHints core.Tristate `raw:"includeInlayEnumMemberValueHints" config:"inlayHints.enumMemberValues.enabled"` } type CodeLensUserPreferences struct { ReferencesCodeLensEnabled core.Tristate `raw:"referencesCodeLensEnabled" config:"referencesCodeLens.enabled"` ImplementationsCodeLensEnabled core.Tristate `raw:"implementationsCodeLensEnabled" config:"implementationsCodeLens.enabled"` ReferencesCodeLensShowOnAllFunctions core.Tristate `raw:"referencesCodeLensShowOnAllFunctions" config:"referencesCodeLens.showOnAllFunctions"` ImplementationsCodeLensShowOnInterfaceMethods core.Tristate `raw:"implementationsCodeLensShowOnInterfaceMethods" config:"implementationsCodeLens.showOnInterfaceMethods"` ImplementationsCodeLensShowOnAllClassMethods core.Tristate `raw:"implementationsCodeLensShowOnAllClassMethods" config:"implementationsCodeLens.showOnAllClassMethods"` } // --- Enum Types --- type QuotePreference string const ( QuotePreferenceUnknown QuotePreference = "" QuotePreferenceAuto QuotePreference = "auto" QuotePreferenceDouble QuotePreference = "double" QuotePreferenceSingle QuotePreference = "single" ) type JsxAttributeCompletionStyle string const ( JsxAttributeCompletionStyleUnknown JsxAttributeCompletionStyle = "" JsxAttributeCompletionStyleAuto JsxAttributeCompletionStyle = "auto" JsxAttributeCompletionStyleBraces JsxAttributeCompletionStyle = "braces" JsxAttributeCompletionStyleNone JsxAttributeCompletionStyle = "none" ) type IncludeInlayParameterNameHints string const ( IncludeInlayParameterNameHintsNone IncludeInlayParameterNameHints = "" IncludeInlayParameterNameHintsAll IncludeInlayParameterNameHints = "all" IncludeInlayParameterNameHintsLiterals IncludeInlayParameterNameHints = "literals" ) type OrganizeImportsSort int const ( OrganizeImportsSortAuto OrganizeImportsSort = iota OrganizeImportsSortOrdinal OrganizeImportsSortOrdinalIgnoreCase OrganizeImportsSortNatural OrganizeImportsSortNaturalIgnoreCase ) type OrganizeImportsCollation bool const ( OrganizeImportsCollationOrdinal OrganizeImportsCollation = false OrganizeImportsCollationUnicode OrganizeImportsCollation = true ) type OrganizeImportsCaseFirst int const ( OrganizeImportsCaseFirstFalse OrganizeImportsCaseFirst = 0 OrganizeImportsCaseFirstLower OrganizeImportsCaseFirst = 1 OrganizeImportsCaseFirstUpper OrganizeImportsCaseFirst = 2 ) type OrganizeImportsTypeOrder int const ( OrganizeImportsTypeOrderAuto OrganizeImportsTypeOrder = 0 OrganizeImportsTypeOrderLast OrganizeImportsTypeOrder = 1 OrganizeImportsTypeOrderInline OrganizeImportsTypeOrder = 2 OrganizeImportsTypeOrderFirst OrganizeImportsTypeOrder = 3 ) // --- 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[IndentStyle](): func(val any) any { return parseIndentStyle(val) }, reflect.TypeFor[SemicolonPreference](): func(val any) any { return parseSemicolonPreference(val) }, 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[OrganizeImportsSort](): func(val any) any { if s, ok := val.(string); ok { switch strings.ToLower(s) { case "ordinal": return OrganizeImportsSortOrdinal case "ordinalignorecase": return OrganizeImportsSortOrdinalIgnoreCase case "natural": return OrganizeImportsSortNatural case "naturalignorecase": return OrganizeImportsSortNaturalIgnoreCase } } return OrganizeImportsSortAuto }, 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. // For types which do not serialize as-is (tristate, enums, etc). var typeSerializers = map[reflect.Type]func(any) any{ reflect.TypeFor[core.Tristate](): func(val any) any { switch val.(core.Tristate) { case core.TSTrue: return true case core.TSFalse: return false default: return nil } }, reflect.TypeFor[OrganizeImportsSort](): func(val any) any { switch val.(OrganizeImportsSort) { case OrganizeImportsSortOrdinal: return "ordinal" case OrganizeImportsSortOrdinalIgnoreCase: return "ordinalIgnoreCase" case OrganizeImportsSortNatural: return "natural" case OrganizeImportsSortNaturalIgnoreCase: return "naturalIgnoreCase" default: return "auto" } }, 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" } }, // These enums distinguish an unset zero value (e.g. "") from their effective // default (e.g. "auto"): the parser promotes unset/unknown input to the // non-zero default. Plain string serialization would therefore write "" for // an unset field and the parser would read it back as the non-zero default, // breaking round-tripping. Mirror the core.Tristate serializer above and omit // the unset value (return nil) so it decodes back to the zero value. (Enums // whose default already is their zero value, like the OrganizeImports* ones, // round-trip without this.) // // TODO: These three are the only parsers whose fallback is a non-zero value; // every other parser returns its zero value as the fallback. They should be // made consistent: change the parser fallback to return the zero value and // remove this serializer (relying on the default string serialization, which // already omits ""). The consumer must then treat the zero value as the // effective default. The two module-specifier enums are safe to convert (all // read sites already treat the "" zero identically to the promoted default). reflect.TypeFor[JsxAttributeCompletionStyle](): func(val any) any { // TODO: make consistent with other enums (see note above). Unlike the // module-specifier enums, the consumer in completions.go distinguishes // JsxAttributeCompletionStyleUnknown from ...Auto, so converting this one // requires updating that consumer to treat the zero value as "auto". if v := val.(JsxAttributeCompletionStyle); v != JsxAttributeCompletionStyleUnknown { return string(v) } return nil }, reflect.TypeFor[modulespecifiers.ImportModuleSpecifierPreference](): func(val any) any { // TODO: make consistent with other enums (see note above): have the parser // return the zero value (None) as its fallback and drop this serializer. if v := val.(modulespecifiers.ImportModuleSpecifierPreference); v != "" { return string(v) } return nil }, reflect.TypeFor[modulespecifiers.ImportModuleSpecifierEndingPreference](): func(val any) any { // TODO: make consistent with other enums (see note above): have the parser // return the zero value (None) as its fallback and drop this serializer. if v := val.(modulespecifiers.ImportModuleSpecifierEndingPreference); v != "" { return string(v) } return nil }, } // configPathParsers provides field-specific config value parsers that override the default // type-based parser when the VS Code config value format differs from the Go field type. var configPathParsers = map[string]func(any) any{ // VS Code sends caseSensitivity as a string ("auto"/"caseSensitive"/"caseInsensitive"), // but OrganizeImportsIgnoreCase is a core.Tristate. "preferences.organizeImports.caseSensitivity": func(val any) any { if s, ok := val.(string); ok { switch strings.ToLower(s) { case "caseinsensitive": return core.TSTrue case "casesensitive": return core.TSFalse } } if b, ok := val.(bool); ok { if b { return core.TSTrue } return core.TSFalse } return core.TSUnknown }, } type fieldInfo struct { rawName string // raw name for unstable section lookup (e.g., "quotePreference") configPath string // dotted path for config (e.g., "preferences.quoteStyle") fallbackConfigPaths []configPathInfo 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 } type configPathInfo struct { path string invert bool } var fieldInfoCache = sync.OnceValue(func() []fieldInfo { return collectFieldInfos(reflect.TypeFor[UserPreferences](), nil) }) // unstableNameIndex maps raw names to fieldInfo index for unstable section lookup. var unstableNameIndex = sync.OnceValue(func() map[string]int { infos := fieldInfoCache() index := make(map[string]int, len(infos)) for i, info := range infos { if info.rawName != "" { index[info.rawName] = i } } return index }) 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) rawTag := field.Tag.Get("raw") configTag := field.Tag.Get("config") fallbackConfigTag := field.Tag.Get("fallbackConfig") if rawTag == "" && configTag == "" { // Embedded struct without tags - recurse into it if field.Type.Kind() == reflect.Struct { infos = append(infos, collectFieldInfos(field.Type, currentPath)...) continue } panic("raw or config tag required for field " + field.Name) } info := fieldInfo{ fieldPath: currentPath, } // 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 != "" { configPath := parseConfigPathTag(configTag) info.configPath = configPath.path info.configInvert = configPath.invert } if fallbackConfigTag != "" { for tag := range strings.SplitSeq(fallbackConfigTag, ";") { info.fallbackConfigPaths = append(info.fallbackConfigPaths, parseConfigPathTag(tag)) } } infos = append(infos, info) } return infos } func parseConfigPathTag(tag string) configPathInfo { parts := strings.Split(tag, ",") info := configPathInfo{path: parts[0]} for _, part := range parts[1:] { if part == "invert" { info.invert = true } } return info } func getNestedValue(config map[string]any, path string) (any, bool) { parts := strings.Split(path, ".") current := any(config) for _, part := range parts { m, ok := current.(map[string]any) if !ok { return nil, false } current, ok = m[part] if !ok { return nil, false } } return current, true } func setNestedValue(config map[string]any, path string, value any) { parts := strings.Split(path, ".") current := config for _, part := range parts[:len(parts)-1] { next, ok := current[part].(map[string]any) if !ok { next = make(map[string]any) current[part] = next } current = next } current[parts[len(parts)-1]] = value } func setRawFieldsFromConfig(v reflect.Value, infos []fieldInfo, settings map[string]any) { index := unstableNameIndex() for name, value := range settings { if idx, found := index[name]; found { info := infos[idx] field := getFieldByPath(v, info.fieldPath) if info.rawInvert { if b, ok := value.(bool); ok { value = !b } } setFieldFromValue(field, value) } } } func (p UserPreferences) withConfig(config map[string]any) UserPreferences { v := reflect.ValueOf(&p).Elem() infos := fieldInfoCache() // Raw UserPreferences can be provided directly, notably via LSP initializationOptions. setRawFieldsFromConfig(v, infos, config) // 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 { setRawFieldsFromConfig(v, infos, unstable) } // Process path-based config (VS Code style nested paths). // These run after unstable, so stable config values take precedence. for _, info := range infos { if info.configPath == "" { continue } configPath := configPathInfo{path: info.configPath, invert: info.configInvert} val, ok := getNestedValue(config, configPath.path) if !ok { for _, fallbackConfigPath := range info.fallbackConfigPaths { val, ok = getNestedValue(config, fallbackConfigPath.path) if ok { configPath = fallbackConfigPath break } } } if !ok { continue } field := getFieldByPath(v, info.fieldPath) if configPath.invert { if b, ok := val.(bool); ok { val = !b } } if parser, ok := configPathParsers[configPath.path]; ok { field.Set(reflect.ValueOf(parser(val))) continue } setFieldFromValue(field, val) } // Validate CustomConfigFileName for path traversal if p.CustomConfigFileName != "" { name := strings.TrimSpace(p.CustomConfigFileName) if strings.ContainsAny(name, "/\\") || name == ".." || name == "." { p.CustomConfigFileName = "" } else { p.CustomConfigFileName = name } } return p } func getFieldByPath(v reflect.Value, path []int) reflect.Value { for _, idx := range path { v = v.Field(idx) } return v } func setFieldFromValue(field reflect.Value, val any) { if val == nil { return } // Check custom parsers first (for types like Tristate, enums, etc.) if parser, ok := typeParsers[field.Type()]; ok { field.Set(reflect.ValueOf(parser(val))) return } switch field.Kind() { case reflect.Bool: if b, ok := val.(bool); ok { field.SetBool(b) } case reflect.Int: switch v := val.(type) { case int: field.SetInt(int64(v)) case float64: field.SetInt(int64(v)) } case reflect.String: if s, ok := val.(string); ok { field.SetString(s) } case reflect.Slice: if arr, ok := val.([]any); ok { result := reflect.MakeSlice(field.Type(), 0, len(arr)) for _, item := range arr { if s, ok := item.(string); ok { result = reflect.Append(result, reflect.ValueOf(s)) } } field.Set(result) } } } func (p *UserPreferences) MarshalJSONTo(enc *json.Encoder) error { config := make(map[string]any) v := reflect.ValueOf(p).Elem() for _, info := range fieldInfoCache() { field := getFieldByPath(v, info.fieldPath) val := serializeField(field) if val == nil { continue } // Prefer config path if available, otherwise use unstable section if info.configPath != "" { if info.configInvert { if b, ok := val.(bool); ok { val = !b } } setNestedValue(config, info.configPath, val) } else if info.rawName != "" { if info.rawInvert { if b, ok := val.(bool); ok { val = !b } } setNestedValue(config, "unstable."+info.rawName, val) } } return json.MarshalEncode(enc, config, json.Deterministic(true)) } func serializeField(field reflect.Value) any { // Check custom serializers first (for types like Tristate, enums, etc.) if serializer, ok := typeSerializers[field.Type()]; ok { return serializer(field.Interface()) } switch field.Kind() { case reflect.Bool: return field.Bool() case reflect.Int: // Zero means "unset" for these preference fields. Omit it so a partial // config does not clobber defaults with zeros when round-tripped through // withConfig. i := field.Int() if i == 0 { return nil } return int(i) case reflect.String: // Zero ("") means "unset"; omit it for the same reason as int above. s := field.String() if s == "" { return nil } return s 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 (p *UserPreferences) UnmarshalJSONFrom(dec *json.Decoder) error { var config map[string]any if err := json.UnmarshalDecode(dec, &config); err != nil { return err } // Start with defaults, then overlay parsed values *p = NewDefaultUserPreferences().withConfig(config) return nil } // --- Helper methods --- func (p UserPreferences) ModuleSpecifierPreferences() modulespecifiers.UserPreferences { return modulespecifiers.UserPreferences{ ImportModuleSpecifierPreference: p.ImportModuleSpecifierPreference, ImportModuleSpecifierEnding: p.ImportModuleSpecifierEnding, AutoImportSpecifierExcludeRegexes: p.AutoImportSpecifierExcludeRegexes, } } func (p UserPreferences) ParsedAutoImportFileExcludePatterns(useCaseSensitiveFileNames bool) *vfsmatch.SpecMatcher { return vfsmatch.NewSpecMatcher(p.AutoImportFileExcludePatterns, "", vfsmatch.UsageExclude, useCaseSensitiveFileNames) } func (p UserPreferences) IsModuleSpecifierExcluded(moduleSpecifier string) bool { return modulespecifiers.IsExcludedByRegex(moduleSpecifier, p.AutoImportSpecifierExcludeRegexes) } func ParseUserPreferences(items map[string]any) UserPreferences { prefs := NewDefaultUserPreferences() // Apply editor settings first (tabSize, indentSize, etc.) as raw-name defaults, // then overlay language-specific settings with increasing precedence: // editor < javascript < typescript < js/ts if editorItem, ok := items["editor"]; ok && editorItem != nil { if editorSettings, ok := editorItem.(map[string]any); ok { prefs = prefs.withConfig(map[string]any{"unstable": editorSettings}) } } // Apply javascript, then typescript, then js/ts (highest precedence). for _, section := range []string{"javascript", "typescript", "js/ts"} { if item, ok := items[section]; ok && item != nil { if settings, ok := item.(map[string]any); ok { prefs = prefs.withConfig(settings) } } } return prefs }