902 lines
34 KiB
Go
902 lines
34 KiB
Go
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
|
|
}
|