From fef4cfeb67a355083205fee8c1c9ed1f4ac4b04c Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 9 Feb 2026 11:57:57 -0800 Subject: [PATCH 01/16] initial InitializationOptions.formatting parsing implementation --- internal/ls/lsutil/formatcodeoptions.go | 33 +++++ internal/lsp/lsproto/_generate/generate.mts | 149 +++++++++++++++++++- internal/lsp/lsproto/lsp_generated.go | 59 ++++++++ internal/lsp/server.go | 9 ++ 4 files changed, 249 insertions(+), 1 deletion(-) diff --git a/internal/ls/lsutil/formatcodeoptions.go b/internal/ls/lsutil/formatcodeoptions.go index a62c6c27d69..de7465dfafe 100644 --- a/internal/ls/lsutil/formatcodeoptions.go +++ b/internal/ls/lsutil/formatcodeoptions.go @@ -87,6 +87,39 @@ type FormatCodeSettings struct { IndentSwitchCase core.Tristate } +func FromInitFormatOptions(opt *lsproto.FormatOptions) *FormatCodeSettings { + return &FormatCodeSettings{ + EditorSettings: EditorSettings{ + IndentSize: int(opt.IndentSize), + TabSize: int(opt.TabSize), + NewLineCharacter: core.GetNewLineKind(tsoptions.ParseString(opt.NewLineCharacter)).GetNewLineCharacter(), + ConvertTabsToSpaces: opt.ConvertTabsToSpaces, + IndentStyle: parseIndentStyle(opt.IndentStyle), + TrimTrailingWhitespace: opt.TrimTrailingWhitespace, + }, + InsertSpaceAfterCommaDelimiter: core.BoolToTristate(opt.InsertSpaceAfterCommaDelimiter), + InsertSpaceAfterSemicolonInForStatements: core.BoolToTristate(opt.InsertSpaceAfterSemicolonInForStatements), + InsertSpaceBeforeAndAfterBinaryOperators: core.BoolToTristate(opt.InsertSpaceBeforeAndAfterBinaryOperators), + InsertSpaceAfterConstructor: core.BoolToTristate(opt.InsertSpaceAfterConstructor), + InsertSpaceAfterKeywordsInControlFlowStatements: core.BoolToTristate(opt.InsertSpaceAfterKeywordsInControlFlowStatements), + InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.BoolToTristate(opt.InsertSpaceAfterFunctionKeywordForAnonymousFunctions), + InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis), + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets), + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces), + InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces), + InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces), + InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces), + InsertSpaceAfterTypeAssertion: core.BoolToTristate(opt.InsertSpaceAfterTypeAssertion), + InsertSpaceBeforeFunctionParenthesis: core.BoolToTristate(opt.InsertSpaceBeforeFunctionParenthesis), + PlaceOpenBraceOnNewLineForFunctions: core.BoolToTristate(opt.PlaceOpenBraceOnNewLineForFunctions), + PlaceOpenBraceOnNewLineForControlBlocks: core.BoolToTristate(opt.PlaceOpenBraceOnNewLineForControlBlocks), + InsertSpaceBeforeTypeAnnotation: core.BoolToTristate(opt.InsertSpaceBeforeTypeAnnotation), + IndentMultiLineObjectLiteralBeginningOnBlankLine: core.BoolToTristate(opt.IndentMultiLineObjectLiteralBeginningOnBlankLine), + Semicolons: parseSemicolonPreference(opt.Semicolons), + IndentSwitchCase: core.BoolToTristate(opt.IndentSwitchCase), + } +} + func FromLSFormatOptions(f *FormatCodeSettings, opt *lsproto.FormattingOptions) *FormatCodeSettings { updatedSettings := f.Copy() updatedSettings.TabSize = int(opt.TabSize) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 08526fe0449..f84313622b8 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -48,9 +48,155 @@ const customStructures: Structure[] = [ optional: true, documentation: "The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`.", }, + { + name: "typescript", + type: { kind: "reference", name: "FormatOptions" }, + optional: true, + documentation: "Formatting options provided at initialization.", + }, ], documentation: "InitializationOptions contains user-provided initialization options.", }, + { + name: "FormatOptions", + properties: [ + { + name: "InsertSpaceAfterCommaDelimiter", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterSemicolonInForStatements", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceBeforeAndAfterBinaryOperators", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterConstructor", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterKeywordsInControlFlowStatements", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterFunctionKeywordForAnonymousFunctions", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceAfterTypeAssertion", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceBeforeFunctionParenthesis", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "PlaceOpenBraceOnNewLineForFunctions", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "PlaceOpenBraceOnNewLineForControlBlocks", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "InsertSpaceBeforeTypeAnnotation", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "IndentMultiLineObjectLiteralBeginningOnBlankLine", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "IndentSwitchCase", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "Semicolons", + type: { kind: "base", name: "string" }, + omitzeroValue: true, + }, + { + name: "BaseIndentSize", + type: { kind: "base", name: "integer" }, + omitzeroValue: true, + }, + { + name: "IndentSize", + type: { kind: "base", name: "integer" }, + omitzeroValue: true, + }, + { + name: "TabSize", + type: { kind: "base", name: "integer" }, + omitzeroValue: true, + }, + { + name: "NewLineCharacter", + type: { kind: "base", name: "string" }, + omitzeroValue: true, + }, + { + name: "ConvertTabsToSpaces", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + { + name: "IndentStyle", + type: { kind: "base", name: "string" }, + omitzeroValue: true, + }, + { + name: "TrimTrailingWhitespace", + type: { kind: "base", name: "boolean" }, + omitzeroValue: true, + }, + ], + }, { name: "AutoImportFix", properties: [ @@ -748,7 +894,7 @@ function handleOrType(orType: OrType): GoType { let memberNames = nonNullTypes.map(type => { if (type.kind === "reference") { - return type.name; + return type.name.slice(type.name.lastIndexOf(".") + 1); } else if (type.kind === "base") { return titleCase(type.name); @@ -1124,6 +1270,7 @@ function generateCode() { writeLine(""); writeLine(`\t"github.com/go-json-experiment/json"`); writeLine(`\t"github.com/go-json-experiment/json/jsontext"`); + // writeLine(`\t"github.com/microsoft/typescript-go/internal/core"`); writeLine(`)`); writeLine(""); writeLine("// Meta model version " + model.metaData.version); diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 71477b95a07..8a848d85643 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21626,6 +21626,65 @@ type InitializationOptions struct { // The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`. CodeLensShowLocationsCommandName *string `json:"codeLensShowLocationsCommandName,omitzero"` + + // Formatting options provided at initialization. + Typescript *FormatOptions `json:"typescript,omitzero"` +} + +type FormatOptions struct { + InsertSpaceAfterCommaDelimiter bool `json:"InsertSpaceAfterCommaDelimiter,omitzero"` + + InsertSpaceAfterSemicolonInForStatements bool `json:"InsertSpaceAfterSemicolonInForStatements,omitzero"` + + InsertSpaceBeforeAndAfterBinaryOperators bool `json:"InsertSpaceBeforeAndAfterBinaryOperators,omitzero"` + + InsertSpaceAfterConstructor bool `json:"InsertSpaceAfterConstructor,omitzero"` + + InsertSpaceAfterKeywordsInControlFlowStatements bool `json:"InsertSpaceAfterKeywordsInControlFlowStatements,omitzero"` + + InsertSpaceAfterFunctionKeywordForAnonymousFunctions bool `json:"InsertSpaceAfterFunctionKeywordForAnonymousFunctions,omitzero"` + + InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis bool `json:"InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis,omitzero"` + + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets bool `json:"InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets,omitzero"` + + InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces,omitzero"` + + InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces,omitzero"` + + InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces,omitzero"` + + InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces,omitzero"` + + InsertSpaceAfterTypeAssertion bool `json:"InsertSpaceAfterTypeAssertion,omitzero"` + + InsertSpaceBeforeFunctionParenthesis bool `json:"InsertSpaceBeforeFunctionParenthesis,omitzero"` + + PlaceOpenBraceOnNewLineForFunctions bool `json:"PlaceOpenBraceOnNewLineForFunctions,omitzero"` + + PlaceOpenBraceOnNewLineForControlBlocks bool `json:"PlaceOpenBraceOnNewLineForControlBlocks,omitzero"` + + InsertSpaceBeforeTypeAnnotation bool `json:"InsertSpaceBeforeTypeAnnotation,omitzero"` + + IndentMultiLineObjectLiteralBeginningOnBlankLine bool `json:"IndentMultiLineObjectLiteralBeginningOnBlankLine,omitzero"` + + IndentSwitchCase bool `json:"IndentSwitchCase,omitzero"` + + Semicolons string `json:"Semicolons,omitzero"` + + BaseIndentSize int32 `json:"BaseIndentSize,omitzero"` + + IndentSize int32 `json:"IndentSize,omitzero"` + + TabSize int32 `json:"TabSize,omitzero"` + + NewLineCharacter string `json:"NewLineCharacter,omitzero"` + + ConvertTabsToSpaces bool `json:"ConvertTabsToSpaces,omitzero"` + + IndentStyle string `json:"IndentStyle,omitzero"` + + TrimTrailingWhitespace bool `json:"TrimTrailingWhitespace,omitzero"` } // AutoImportFix contains information about an auto-import suggestion. diff --git a/internal/lsp/server.go b/internal/lsp/server.go index aa211c174b8..3d481192ffb 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -275,6 +275,15 @@ func (s *Server) RefreshCodeLens(ctx context.Context) error { func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, error) { caps := lsproto.GetClientCapabilities(ctx) if !caps.Workspace.Configuration { + if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && s.initializeParams.InitializationOptions.Typescript != nil { + // return user preferences passed in `initializationOptions` + newUserPrefs := lsutil.NewDefaultUserPreferences() + newUserPrefs.FormatCodeSettings = lsutil.FromInitFormatOptions(s.initializeParams.InitializationOptions.Typescript) + s.logger.Logf( + "received formatting options from initialization: %v", s.initializeParams.InitializationOptions.Typescript, + ) + return lsutil.NewUserConfig(newUserPrefs), nil + } // if no configuration request capapbility, return default config return lsutil.NewUserConfig(nil), nil } From 2ff673e9182c49cd3f647aac5ca41ed6f4835679 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 9 Feb 2026 14:15:33 -0800 Subject: [PATCH 02/16] change to any --- internal/lsp/lsproto/_generate/generate.mts | 4 ++-- internal/lsp/lsproto/lsp_generated.go | 4 ++-- internal/lsp/server.go | 12 ++++++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index f84313622b8..55d3ad31232 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -50,9 +50,9 @@ const customStructures: Structure[] = [ }, { name: "typescript", - type: { kind: "reference", name: "FormatOptions" }, + type: { kind: "reference", name: "any" }, optional: true, - documentation: "Formatting options provided at initialization.", + documentation: "userPreferences and/or formatting options or provided at initialization.", }, ], documentation: "InitializationOptions contains user-provided initialization options.", diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 8a848d85643..036408bc3ba 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21627,8 +21627,8 @@ type InitializationOptions struct { // The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`. CodeLensShowLocationsCommandName *string `json:"codeLensShowLocationsCommandName,omitzero"` - // Formatting options provided at initialization. - Typescript *FormatOptions `json:"typescript,omitzero"` + // userPreferences and/or formatting options or provided at initialization. + Typescript *any `json:"typescript,omitzero"` } type FormatOptions struct { diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 3d481192ffb..5aeefb02aaa 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -277,12 +277,16 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, if !caps.Workspace.Configuration { if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && s.initializeParams.InitializationOptions.Typescript != nil { // return user preferences passed in `initializationOptions` - newUserPrefs := lsutil.NewDefaultUserPreferences() - newUserPrefs.FormatCodeSettings = lsutil.FromInitFormatOptions(s.initializeParams.InitializationOptions.Typescript) + // newUserPrefs := lsutil.NewDefaultUserPreferences() + // newUserPrefs.FormatCodeSettings = lsutil.FromInitFormatOptions(s.initializeParams.InitializationOptions.Typescript) + s.logger.Logf( - "received formatting options from initialization: %v", s.initializeParams.InitializationOptions.Typescript, + "received formatting options from initialization: %T\n%+v", + s.initializeParams.InitializationOptions.Typescript, + s.initializeParams.InitializationOptions.Typescript, ) - return lsutil.NewUserConfig(newUserPrefs), nil + // Any options recieved via initialization options will be used for both `js` and `ts`options + return lsutil.ParseNewUserConfig([]any{s.initializeParams.InitializationOptions.Typescript}), nil } // if no configuration request capapbility, return default config return lsutil.NewUserConfig(nil), nil From ceba8ff9bed8489a949ebdd36f620f0da02d1b6d Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 9 Feb 2026 17:24:09 -0800 Subject: [PATCH 03/16] tested --- internal/ls/lsutil/formatcodeoptions.go | 33 ----- internal/ls/lsutil/userpreferences.go | 5 +- internal/lsp/lsproto/_generate/generate.mts | 141 -------------------- internal/lsp/server.go | 20 +-- 4 files changed, 15 insertions(+), 184 deletions(-) diff --git a/internal/ls/lsutil/formatcodeoptions.go b/internal/ls/lsutil/formatcodeoptions.go index 38d4de5bfa0..04162a71d64 100644 --- a/internal/ls/lsutil/formatcodeoptions.go +++ b/internal/ls/lsutil/formatcodeoptions.go @@ -88,39 +88,6 @@ type FormatCodeSettings struct { IndentSwitchCase core.Tristate } -func FromInitFormatOptions(opt *lsproto.FormatOptions) *FormatCodeSettings { - return &FormatCodeSettings{ - EditorSettings: EditorSettings{ - IndentSize: int(opt.IndentSize), - TabSize: int(opt.TabSize), - NewLineCharacter: core.GetNewLineKind(tsoptions.ParseString(opt.NewLineCharacter)).GetNewLineCharacter(), - ConvertTabsToSpaces: opt.ConvertTabsToSpaces, - IndentStyle: parseIndentStyle(opt.IndentStyle), - TrimTrailingWhitespace: opt.TrimTrailingWhitespace, - }, - InsertSpaceAfterCommaDelimiter: core.BoolToTristate(opt.InsertSpaceAfterCommaDelimiter), - InsertSpaceAfterSemicolonInForStatements: core.BoolToTristate(opt.InsertSpaceAfterSemicolonInForStatements), - InsertSpaceBeforeAndAfterBinaryOperators: core.BoolToTristate(opt.InsertSpaceBeforeAndAfterBinaryOperators), - InsertSpaceAfterConstructor: core.BoolToTristate(opt.InsertSpaceAfterConstructor), - InsertSpaceAfterKeywordsInControlFlowStatements: core.BoolToTristate(opt.InsertSpaceAfterKeywordsInControlFlowStatements), - InsertSpaceAfterFunctionKeywordForAnonymousFunctions: core.BoolToTristate(opt.InsertSpaceAfterFunctionKeywordForAnonymousFunctions), - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis), - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets), - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces), - InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces), - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces), - InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces: core.BoolToTristate(opt.InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces), - InsertSpaceAfterTypeAssertion: core.BoolToTristate(opt.InsertSpaceAfterTypeAssertion), - InsertSpaceBeforeFunctionParenthesis: core.BoolToTristate(opt.InsertSpaceBeforeFunctionParenthesis), - PlaceOpenBraceOnNewLineForFunctions: core.BoolToTristate(opt.PlaceOpenBraceOnNewLineForFunctions), - PlaceOpenBraceOnNewLineForControlBlocks: core.BoolToTristate(opt.PlaceOpenBraceOnNewLineForControlBlocks), - InsertSpaceBeforeTypeAnnotation: core.BoolToTristate(opt.InsertSpaceBeforeTypeAnnotation), - IndentMultiLineObjectLiteralBeginningOnBlankLine: core.BoolToTristate(opt.IndentMultiLineObjectLiteralBeginningOnBlankLine), - Semicolons: parseSemicolonPreference(opt.Semicolons), - IndentSwitchCase: core.BoolToTristate(opt.IndentSwitchCase), - } -} - func FromLSFormatOptions(f *FormatCodeSettings, opt *lsproto.FormattingOptions) *FormatCodeSettings { updatedSettings := f.Copy() updatedSettings.TabSize = int(opt.TabSize) diff --git a/internal/ls/lsutil/userpreferences.go b/internal/ls/lsutil/userpreferences.go index f97f48212ae..0de5c224c80 100644 --- a/internal/ls/lsutil/userpreferences.go +++ b/internal/ls/lsutil/userpreferences.go @@ -618,8 +618,11 @@ 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 } diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 8872bb2a0fa..e623b2b3f78 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -57,146 +57,6 @@ const customStructures: Structure[] = [ ], documentation: "InitializationOptions contains user-provided initialization options.", }, - { - name: "FormatOptions", - properties: [ - { - name: "InsertSpaceAfterCommaDelimiter", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterSemicolonInForStatements", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceBeforeAndAfterBinaryOperators", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterConstructor", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterKeywordsInControlFlowStatements", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterFunctionKeywordForAnonymousFunctions", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceAfterTypeAssertion", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceBeforeFunctionParenthesis", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "PlaceOpenBraceOnNewLineForFunctions", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "PlaceOpenBraceOnNewLineForControlBlocks", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "InsertSpaceBeforeTypeAnnotation", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "IndentMultiLineObjectLiteralBeginningOnBlankLine", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "IndentSwitchCase", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "Semicolons", - type: { kind: "base", name: "string" }, - omitzeroValue: true, - }, - { - name: "BaseIndentSize", - type: { kind: "base", name: "integer" }, - omitzeroValue: true, - }, - { - name: "IndentSize", - type: { kind: "base", name: "integer" }, - omitzeroValue: true, - }, - { - name: "TabSize", - type: { kind: "base", name: "integer" }, - omitzeroValue: true, - }, - { - name: "NewLineCharacter", - type: { kind: "base", name: "string" }, - omitzeroValue: true, - }, - { - name: "ConvertTabsToSpaces", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - { - name: "IndentStyle", - type: { kind: "base", name: "string" }, - omitzeroValue: true, - }, - { - name: "TrimTrailingWhitespace", - type: { kind: "base", name: "boolean" }, - omitzeroValue: true, - }, - ], - }, { name: "AutoImportFix", properties: [ @@ -1302,7 +1162,6 @@ function generateCode() { writeLine(""); writeLine(`\t"github.com/go-json-experiment/json"`); writeLine(`\t"github.com/go-json-experiment/json/jsontext"`); - // writeLine(`\t"github.com/microsoft/typescript-go/internal/core"`); writeLine(`)`); writeLine(""); writeLine("// Meta model version " + model.metaData.version); diff --git a/internal/lsp/server.go b/internal/lsp/server.go index e25dbf381c8..a9292db8b21 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -283,17 +283,13 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, caps := lsproto.GetClientCapabilities(ctx) if !caps.Workspace.Configuration { if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && s.initializeParams.InitializationOptions.Typescript != nil { - // return user preferences passed in `initializationOptions` - // newUserPrefs := lsutil.NewDefaultUserPreferences() - // newUserPrefs.FormatCodeSettings = lsutil.FromInitFormatOptions(s.initializeParams.InitializationOptions.Typescript) - s.logger.Logf( - "received formatting options from initialization: %T\n%+v", - s.initializeParams.InitializationOptions.Typescript, - s.initializeParams.InitializationOptions.Typescript, + "received formatting options from initialization: %T\n%+v", + *s.initializeParams.InitializationOptions.Typescript, + *s.initializeParams.InitializationOptions.Typescript, ) - // Any options recieved via initialization options will be used for both `js` and `ts`options - return lsutil.ParseNewUserConfig([]any{s.initializeParams.InitializationOptions.Typescript}), nil + // Any options received via initializationOptions will be used for both `js` and `ts`options + return lsutil.ParseNewUserConfig([]any{*s.initializeParams.InitializationOptions.Typescript}), nil } // if no configuration request capapbility, return default config return lsutil.NewUserConfig(nil), nil @@ -314,6 +310,12 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, if err != nil { return &lsutil.UserConfig{}, fmt.Errorf("configure request failed: %w", err) } + s.logger.Logf( + "received options from workspace/configuration request:\njs/ts: %+v\n\ntypescript: %+v\n\njavascript: %+v\n", + configs[0], + configs[1], + configs[2], + ) return lsutil.ParseNewUserConfig(configs), nil } From 1914c0cc4402a1eae54970cc240bff66ed845b56 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:13:57 -0800 Subject: [PATCH 04/16] Resolve/merge configuration on the client --- _extension/src/client.ts | 10 + _extension/src/configurationMiddleware.ts | 255 ++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 _extension/src/configurationMiddleware.ts diff --git a/_extension/src/client.ts b/_extension/src/client.ts index 5fb494539a0..e49763fce0e 100644 --- a/_extension/src/client.ts +++ b/_extension/src/client.ts @@ -16,6 +16,10 @@ import { } from "vscode-languageclient/node"; import { codeLensShowLocationsCommandName } from "./commands"; +import { + configurationMiddleware, + sendNotificationMiddleware, +} from "./configurationMiddleware"; import { registerTagClosingFeature } from "./languageFeatures/tagClosing"; import * as tr from "./telemetryReporting"; import { @@ -63,6 +67,12 @@ export class Client implements vscode.Disposable { codeLensShowLocationsCommandName, }, errorHandler: new ReportingErrorHandler(this.telemetryReporter, 5), + middleware: { + workspace: { + ...configurationMiddleware, + }, + sendNotification: sendNotificationMiddleware, + }, diagnosticPullOptions: { onChange: true, onSave: true, diff --git a/_extension/src/configurationMiddleware.ts b/_extension/src/configurationMiddleware.ts new file mode 100644 index 00000000000..e2dd537c81c --- /dev/null +++ b/_extension/src/configurationMiddleware.ts @@ -0,0 +1,255 @@ +import * as vscode from "vscode"; + +import type { ConfigurationMiddleware } from "vscode-languageclient/node"; +import type { MessageSignature } from "vscode-languageserver-protocol"; + +/** + * Configuration middleware for the TypeScript language server. + * + * The default vscode-languageclient handler uses `getConfiguration().get()`, which + * returns "fully resolved" values including VS Code defaults. This is problematic + * because the server has its own defaults, and receiving VS Code's prevents the + * server from distinguishing user-set values from defaults. + * + * This module uses `inspect()` to retrieve only explicitly-set values (user/workspace/ + * workspace-folder settings) from all three configuration sections, then merges them + * with the correct precedence: js/ts > typescript > javascript. + * + * Both the `workspace/configuration` (pull) and `workspace/didChangeConfiguration` + * (push) middlewares return/send the same merged object for every requested section. + */ + +// Sections merged together. Earlier sections take precedence over later ones. +const configSections = ["js/ts", "typescript", "javascript"]; + +/** + * Build a single merged configuration object from all config sections. + * + * For each key, the value is chosen with this precedence (highest first): + * 1. js/ts explicit 2. typescript explicit 3. javascript explicit + * 4. js/ts default 5. typescript default 6. javascript default + * + * This ensures user-set values always win, and declared-but-unset settings + * still get their default with the right section precedence. + */ +function getMergedExplicitConfiguration(resource: vscode.Uri | undefined): Record { + const configs = configSections.map(section => getInspectedConfiguration(section, resource)); + + // Layer from lowest to highest precedence. + let merged: Record = {}; + + // Defaults: javascript < typescript < js/ts + for (let i = configs.length - 1; i >= 0; i--) { + if (configs[i].defaults !== null) { + merged = deepMerge(merged, configs[i].defaults!); + } + } + + // Explicit values: javascript < typescript < js/ts + for (let i = configs.length - 1; i >= 0; i--) { + if (configs[i].explicit !== null) { + merged = deepMerge(merged, configs[i].explicit!); + } + } + + return merged; +} + +/** + * Given a configuration section name (e.g., "typescript"), use vscode's + * inspect API to collect both explicitly-set values and default values, + * returning them as separate nested objects. + */ +function getInspectedConfiguration( + section: string, + resource: vscode.Uri | undefined, +): { explicit: Record | null; defaults: Record | null; } { + const config = vscode.workspace.getConfiguration(section, resource); + const explicit: Record = {}; + const defaults: Record = {}; + let hasExplicit = false; + let hasDefaults = false; + + const allKeys = collectConfigurationKeys(config); + + for (const key of allKeys) { + const inspection = config.inspect(key); + if (!inspection) { + continue; + } + + // Pick the most specific explicitly-set value. + const explicitValue = inspection.workspaceFolderValue + ?? inspection.workspaceValue + ?? inspection.globalValue + ?? inspection.workspaceFolderLanguageValue + ?? inspection.workspaceLanguageValue + ?? inspection.globalLanguageValue; + + if (explicitValue !== undefined) { + setNestedValue(explicit, key, toJSONObject(explicitValue)); + hasExplicit = true; + } + else if (inspection.defaultValue !== undefined) { + setNestedValue(defaults, key, toJSONObject(inspection.defaultValue)); + hasDefaults = true; + } + } + + return { + explicit: hasExplicit ? explicit : null, + defaults: hasDefaults ? defaults : null, + }; +} + +/** + * Collect all leaf key paths from a workspace configuration section. + */ +function collectConfigurationKeys(config: vscode.WorkspaceConfiguration): string[] { + const keys: string[] = []; + const configMethods = new Set(["get", "has", "inspect", "update"]); + + function walk(obj: any, prefix: string) { + if (obj === null || obj === undefined || typeof obj !== "object" || Array.isArray(obj)) { + return; + } + for (const key of Object.keys(obj)) { + if (configMethods.has(key)) { + continue; + } + const fullKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + walk(value, fullKey); + } + else { + keys.push(fullKey); + } + } + } + + walk(config, ""); + return keys; +} + +function setNestedValue(obj: Record, dottedKey: string, value: any): void { + const parts = dottedKey.split("."); + let current = obj; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!(part in current) || typeof current[part] !== "object" || current[part] === null) { + current[part] = {}; + } + current = current[part]; + } + current[parts[parts.length - 1]] = value; +} + +/** + * Deep merge b into a. Values in b take precedence over values in a. + * Returns a new object; does not mutate inputs. + */ +function deepMerge(a: Record, b: Record): Record { + const result: Record = { ...a }; + for (const key of Object.keys(b)) { + if ( + key in result + && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key]) + && b[key] !== null && typeof b[key] === "object" && !Array.isArray(b[key]) + ) { + result[key] = deepMerge(result[key], b[key]); + } + else { + result[key] = b[key]; + } + } + return result; +} + +function toJSONObject(obj: any): any { + if (obj === null || obj === undefined) { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(toJSONObject); + } + if (typeof obj === "object") { + const res: Record = Object.create(null); + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + res[key] = toJSONObject(obj[key]); + } + } + return res; + } + return obj; +} + +const configSectionsSet: ReadonlySet = new Set(configSections); + +/** + * Intercepts workspace/configuration requests. For items requesting one of + * the JS/TS config sections, returns the merged explicit configuration + * (js/ts > typescript > javascript). For any other section, delegates to + * the default handler via `next`. + */ +export const configurationMiddleware: ConfigurationMiddleware = { + async configuration(params, token, next) { + const hasNonJsTs = params.items.some( + item => item.section === undefined || !configSectionsSet.has(item.section), + ); + + // If all items are JS/TS sections, no need to call next. + let defaultResults: any[] | undefined; + if (hasNonJsTs) { + const res = await next(params, token); + if (Array.isArray(res)) { + defaultResults = res; + } + } + + // Cache merged config per resource URI to avoid redundant recalculation. + const mergedCache = new Map>(); + function getMergedCached(resource: vscode.Uri | undefined): Record { + const key = resource?.toString() ?? ""; + let cached = mergedCache.get(key); + if (cached === undefined) { + cached = getMergedExplicitConfiguration(resource); + mergedCache.set(key, cached); + } + return cached; + } + + const result: any[] = params.items.map((item, i) => { + if (item.section !== undefined && configSectionsSet.has(item.section)) { + const resource = item.scopeUri ? vscode.Uri.parse(item.scopeUri) : undefined; + return getMergedCached(resource); + } + return defaultResults?.[i] ?? null; + }); + + return result; + }, +}; + +/** + * Intercepts outgoing workspace/didChangeConfiguration notifications. + * Replaces the default settings (which include VS Code defaults) with + * the merged explicit configuration, keyed by section name. + */ +export function sendNotificationMiddleware( + type: string | MessageSignature, + next: (type: string | MessageSignature, params?: any) => Promise, + params: any, +): Promise { + const method = typeof type === "string" ? type : type.method; + if (method === "workspace/didChangeConfiguration") { + const merged = getMergedExplicitConfiguration(undefined); + const settings: Record = {}; + for (const section of configSections) { + settings[section] = merged; + } + return next(type, { settings }); + } + return next(type, params); +} From e9481a0aaf7952959ef36153d26009c08ae73836 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:17:56 -0800 Subject: [PATCH 05/16] fmt --- _extension/src/configurationMiddleware.ts | 510 +++++++++++----------- 1 file changed, 255 insertions(+), 255 deletions(-) diff --git a/_extension/src/configurationMiddleware.ts b/_extension/src/configurationMiddleware.ts index e2dd537c81c..f869baa577f 100644 --- a/_extension/src/configurationMiddleware.ts +++ b/_extension/src/configurationMiddleware.ts @@ -1,255 +1,255 @@ -import * as vscode from "vscode"; - -import type { ConfigurationMiddleware } from "vscode-languageclient/node"; -import type { MessageSignature } from "vscode-languageserver-protocol"; - -/** - * Configuration middleware for the TypeScript language server. - * - * The default vscode-languageclient handler uses `getConfiguration().get()`, which - * returns "fully resolved" values including VS Code defaults. This is problematic - * because the server has its own defaults, and receiving VS Code's prevents the - * server from distinguishing user-set values from defaults. - * - * This module uses `inspect()` to retrieve only explicitly-set values (user/workspace/ - * workspace-folder settings) from all three configuration sections, then merges them - * with the correct precedence: js/ts > typescript > javascript. - * - * Both the `workspace/configuration` (pull) and `workspace/didChangeConfiguration` - * (push) middlewares return/send the same merged object for every requested section. - */ - -// Sections merged together. Earlier sections take precedence over later ones. -const configSections = ["js/ts", "typescript", "javascript"]; - -/** - * Build a single merged configuration object from all config sections. - * - * For each key, the value is chosen with this precedence (highest first): - * 1. js/ts explicit 2. typescript explicit 3. javascript explicit - * 4. js/ts default 5. typescript default 6. javascript default - * - * This ensures user-set values always win, and declared-but-unset settings - * still get their default with the right section precedence. - */ -function getMergedExplicitConfiguration(resource: vscode.Uri | undefined): Record { - const configs = configSections.map(section => getInspectedConfiguration(section, resource)); - - // Layer from lowest to highest precedence. - let merged: Record = {}; - - // Defaults: javascript < typescript < js/ts - for (let i = configs.length - 1; i >= 0; i--) { - if (configs[i].defaults !== null) { - merged = deepMerge(merged, configs[i].defaults!); - } - } - - // Explicit values: javascript < typescript < js/ts - for (let i = configs.length - 1; i >= 0; i--) { - if (configs[i].explicit !== null) { - merged = deepMerge(merged, configs[i].explicit!); - } - } - - return merged; -} - -/** - * Given a configuration section name (e.g., "typescript"), use vscode's - * inspect API to collect both explicitly-set values and default values, - * returning them as separate nested objects. - */ -function getInspectedConfiguration( - section: string, - resource: vscode.Uri | undefined, -): { explicit: Record | null; defaults: Record | null; } { - const config = vscode.workspace.getConfiguration(section, resource); - const explicit: Record = {}; - const defaults: Record = {}; - let hasExplicit = false; - let hasDefaults = false; - - const allKeys = collectConfigurationKeys(config); - - for (const key of allKeys) { - const inspection = config.inspect(key); - if (!inspection) { - continue; - } - - // Pick the most specific explicitly-set value. - const explicitValue = inspection.workspaceFolderValue - ?? inspection.workspaceValue - ?? inspection.globalValue - ?? inspection.workspaceFolderLanguageValue - ?? inspection.workspaceLanguageValue - ?? inspection.globalLanguageValue; - - if (explicitValue !== undefined) { - setNestedValue(explicit, key, toJSONObject(explicitValue)); - hasExplicit = true; - } - else if (inspection.defaultValue !== undefined) { - setNestedValue(defaults, key, toJSONObject(inspection.defaultValue)); - hasDefaults = true; - } - } - - return { - explicit: hasExplicit ? explicit : null, - defaults: hasDefaults ? defaults : null, - }; -} - -/** - * Collect all leaf key paths from a workspace configuration section. - */ -function collectConfigurationKeys(config: vscode.WorkspaceConfiguration): string[] { - const keys: string[] = []; - const configMethods = new Set(["get", "has", "inspect", "update"]); - - function walk(obj: any, prefix: string) { - if (obj === null || obj === undefined || typeof obj !== "object" || Array.isArray(obj)) { - return; - } - for (const key of Object.keys(obj)) { - if (configMethods.has(key)) { - continue; - } - const fullKey = prefix ? `${prefix}.${key}` : key; - const value = obj[key]; - if (value !== null && typeof value === "object" && !Array.isArray(value)) { - walk(value, fullKey); - } - else { - keys.push(fullKey); - } - } - } - - walk(config, ""); - return keys; -} - -function setNestedValue(obj: Record, dottedKey: string, value: any): void { - const parts = dottedKey.split("."); - let current = obj; - for (let i = 0; i < parts.length - 1; i++) { - const part = parts[i]; - if (!(part in current) || typeof current[part] !== "object" || current[part] === null) { - current[part] = {}; - } - current = current[part]; - } - current[parts[parts.length - 1]] = value; -} - -/** - * Deep merge b into a. Values in b take precedence over values in a. - * Returns a new object; does not mutate inputs. - */ -function deepMerge(a: Record, b: Record): Record { - const result: Record = { ...a }; - for (const key of Object.keys(b)) { - if ( - key in result - && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key]) - && b[key] !== null && typeof b[key] === "object" && !Array.isArray(b[key]) - ) { - result[key] = deepMerge(result[key], b[key]); - } - else { - result[key] = b[key]; - } - } - return result; -} - -function toJSONObject(obj: any): any { - if (obj === null || obj === undefined) { - return obj; - } - if (Array.isArray(obj)) { - return obj.map(toJSONObject); - } - if (typeof obj === "object") { - const res: Record = Object.create(null); - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - res[key] = toJSONObject(obj[key]); - } - } - return res; - } - return obj; -} - -const configSectionsSet: ReadonlySet = new Set(configSections); - -/** - * Intercepts workspace/configuration requests. For items requesting one of - * the JS/TS config sections, returns the merged explicit configuration - * (js/ts > typescript > javascript). For any other section, delegates to - * the default handler via `next`. - */ -export const configurationMiddleware: ConfigurationMiddleware = { - async configuration(params, token, next) { - const hasNonJsTs = params.items.some( - item => item.section === undefined || !configSectionsSet.has(item.section), - ); - - // If all items are JS/TS sections, no need to call next. - let defaultResults: any[] | undefined; - if (hasNonJsTs) { - const res = await next(params, token); - if (Array.isArray(res)) { - defaultResults = res; - } - } - - // Cache merged config per resource URI to avoid redundant recalculation. - const mergedCache = new Map>(); - function getMergedCached(resource: vscode.Uri | undefined): Record { - const key = resource?.toString() ?? ""; - let cached = mergedCache.get(key); - if (cached === undefined) { - cached = getMergedExplicitConfiguration(resource); - mergedCache.set(key, cached); - } - return cached; - } - - const result: any[] = params.items.map((item, i) => { - if (item.section !== undefined && configSectionsSet.has(item.section)) { - const resource = item.scopeUri ? vscode.Uri.parse(item.scopeUri) : undefined; - return getMergedCached(resource); - } - return defaultResults?.[i] ?? null; - }); - - return result; - }, -}; - -/** - * Intercepts outgoing workspace/didChangeConfiguration notifications. - * Replaces the default settings (which include VS Code defaults) with - * the merged explicit configuration, keyed by section name. - */ -export function sendNotificationMiddleware( - type: string | MessageSignature, - next: (type: string | MessageSignature, params?: any) => Promise, - params: any, -): Promise { - const method = typeof type === "string" ? type : type.method; - if (method === "workspace/didChangeConfiguration") { - const merged = getMergedExplicitConfiguration(undefined); - const settings: Record = {}; - for (const section of configSections) { - settings[section] = merged; - } - return next(type, { settings }); - } - return next(type, params); -} +import * as vscode from "vscode"; + +import type { ConfigurationMiddleware } from "vscode-languageclient/node"; +import type { MessageSignature } from "vscode-languageserver-protocol"; + +/** + * Configuration middleware for the TypeScript language server. + * + * The default vscode-languageclient handler uses `getConfiguration().get()`, which + * returns "fully resolved" values including VS Code defaults. This is problematic + * because the server has its own defaults, and receiving VS Code's prevents the + * server from distinguishing user-set values from defaults. + * + * This module uses `inspect()` to retrieve only explicitly-set values (user/workspace/ + * workspace-folder settings) from all three configuration sections, then merges them + * with the correct precedence: js/ts > typescript > javascript. + * + * Both the `workspace/configuration` (pull) and `workspace/didChangeConfiguration` + * (push) middlewares return/send the same merged object for every requested section. + */ + +// Sections merged together. Earlier sections take precedence over later ones. +const configSections = ["js/ts", "typescript", "javascript"]; + +/** + * Build a single merged configuration object from all config sections. + * + * For each key, the value is chosen with this precedence (highest first): + * 1. js/ts explicit 2. typescript explicit 3. javascript explicit + * 4. js/ts default 5. typescript default 6. javascript default + * + * This ensures user-set values always win, and declared-but-unset settings + * still get their default with the right section precedence. + */ +function getMergedExplicitConfiguration(resource: vscode.Uri | undefined): Record { + const configs = configSections.map(section => getInspectedConfiguration(section, resource)); + + // Layer from lowest to highest precedence. + let merged: Record = {}; + + // Defaults: javascript < typescript < js/ts + for (let i = configs.length - 1; i >= 0; i--) { + if (configs[i].defaults !== null) { + merged = deepMerge(merged, configs[i].defaults!); + } + } + + // Explicit values: javascript < typescript < js/ts + for (let i = configs.length - 1; i >= 0; i--) { + if (configs[i].explicit !== null) { + merged = deepMerge(merged, configs[i].explicit!); + } + } + + return merged; +} + +/** + * Given a configuration section name (e.g., "typescript"), use vscode's + * inspect API to collect both explicitly-set values and default values, + * returning them as separate nested objects. + */ +function getInspectedConfiguration( + section: string, + resource: vscode.Uri | undefined, +): { explicit: Record | null; defaults: Record | null; } { + const config = vscode.workspace.getConfiguration(section, resource); + const explicit: Record = {}; + const defaults: Record = {}; + let hasExplicit = false; + let hasDefaults = false; + + const allKeys = collectConfigurationKeys(config); + + for (const key of allKeys) { + const inspection = config.inspect(key); + if (!inspection) { + continue; + } + + // Pick the most specific explicitly-set value. + const explicitValue = inspection.workspaceFolderValue + ?? inspection.workspaceValue + ?? inspection.globalValue + ?? inspection.workspaceFolderLanguageValue + ?? inspection.workspaceLanguageValue + ?? inspection.globalLanguageValue; + + if (explicitValue !== undefined) { + setNestedValue(explicit, key, toJSONObject(explicitValue)); + hasExplicit = true; + } + else if (inspection.defaultValue !== undefined) { + setNestedValue(defaults, key, toJSONObject(inspection.defaultValue)); + hasDefaults = true; + } + } + + return { + explicit: hasExplicit ? explicit : null, + defaults: hasDefaults ? defaults : null, + }; +} + +/** + * Collect all leaf key paths from a workspace configuration section. + */ +function collectConfigurationKeys(config: vscode.WorkspaceConfiguration): string[] { + const keys: string[] = []; + const configMethods = new Set(["get", "has", "inspect", "update"]); + + function walk(obj: any, prefix: string) { + if (obj === null || obj === undefined || typeof obj !== "object" || Array.isArray(obj)) { + return; + } + for (const key of Object.keys(obj)) { + if (configMethods.has(key)) { + continue; + } + const fullKey = prefix ? `${prefix}.${key}` : key; + const value = obj[key]; + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + walk(value, fullKey); + } + else { + keys.push(fullKey); + } + } + } + + walk(config, ""); + return keys; +} + +function setNestedValue(obj: Record, dottedKey: string, value: any): void { + const parts = dottedKey.split("."); + let current = obj; + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]; + if (!(part in current) || typeof current[part] !== "object" || current[part] === null) { + current[part] = {}; + } + current = current[part]; + } + current[parts[parts.length - 1]] = value; +} + +/** + * Deep merge b into a. Values in b take precedence over values in a. + * Returns a new object; does not mutate inputs. + */ +function deepMerge(a: Record, b: Record): Record { + const result: Record = { ...a }; + for (const key of Object.keys(b)) { + if ( + key in result + && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key]) + && b[key] !== null && typeof b[key] === "object" && !Array.isArray(b[key]) + ) { + result[key] = deepMerge(result[key], b[key]); + } + else { + result[key] = b[key]; + } + } + return result; +} + +function toJSONObject(obj: any): any { + if (obj === null || obj === undefined) { + return obj; + } + if (Array.isArray(obj)) { + return obj.map(toJSONObject); + } + if (typeof obj === "object") { + const res: Record = Object.create(null); + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + res[key] = toJSONObject(obj[key]); + } + } + return res; + } + return obj; +} + +const configSectionsSet: ReadonlySet = new Set(configSections); + +/** + * Intercepts workspace/configuration requests. For items requesting one of + * the JS/TS config sections, returns the merged explicit configuration + * (js/ts > typescript > javascript). For any other section, delegates to + * the default handler via `next`. + */ +export const configurationMiddleware: ConfigurationMiddleware = { + async configuration(params, token, next) { + const hasNonJsTs = params.items.some( + item => item.section === undefined || !configSectionsSet.has(item.section), + ); + + // If all items are JS/TS sections, no need to call next. + let defaultResults: any[] | undefined; + if (hasNonJsTs) { + const res = await next(params, token); + if (Array.isArray(res)) { + defaultResults = res; + } + } + + // Cache merged config per resource URI to avoid redundant recalculation. + const mergedCache = new Map>(); + function getMergedCached(resource: vscode.Uri | undefined): Record { + const key = resource?.toString() ?? ""; + let cached = mergedCache.get(key); + if (cached === undefined) { + cached = getMergedExplicitConfiguration(resource); + mergedCache.set(key, cached); + } + return cached; + } + + const result: any[] = params.items.map((item, i) => { + if (item.section !== undefined && configSectionsSet.has(item.section)) { + const resource = item.scopeUri ? vscode.Uri.parse(item.scopeUri) : undefined; + return getMergedCached(resource); + } + return defaultResults?.[i] ?? null; + }); + + return result; + }, +}; + +/** + * Intercepts outgoing workspace/didChangeConfiguration notifications. + * Replaces the default settings (which include VS Code defaults) with + * the merged explicit configuration, keyed by section name. + */ +export function sendNotificationMiddleware( + type: string | MessageSignature, + next: (type: string | MessageSignature, params?: any) => Promise, + params: any, +): Promise { + const method = typeof type === "string" ? type : type.method; + if (method === "workspace/didChangeConfiguration") { + const merged = getMergedExplicitConfiguration(undefined); + const settings: Record = {}; + for (const section of configSections) { + settings[section] = merged; + } + return next(type, { settings }); + } + return next(type, params); +} From 1b96c37d5ac2b8e2082ab57a0a04544324d020c6 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:33:49 -0800 Subject: [PATCH 06/16] PR feedback --- _extension/src/configurationMiddleware.ts | 42 ++++++++++++++++------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/_extension/src/configurationMiddleware.ts b/_extension/src/configurationMiddleware.ts index f869baa577f..c2fbfb6c45e 100644 --- a/_extension/src/configurationMiddleware.ts +++ b/_extension/src/configurationMiddleware.ts @@ -11,9 +11,11 @@ import type { MessageSignature } from "vscode-languageserver-protocol"; * because the server has its own defaults, and receiving VS Code's prevents the * server from distinguishing user-set values from defaults. * - * This module uses `inspect()` to retrieve only explicitly-set values (user/workspace/ - * workspace-folder settings) from all three configuration sections, then merges them - * with the correct precedence: js/ts > typescript > javascript. + * This module instead uses `inspect()` to retrieve both explicitly-set values (user/ + * workspace/workspace-folder settings) and VS Code default values from all three + * configuration sections, then merges them with the correct precedence: + * sections: js/ts > typescript > javascript + * values: explicit > default * * Both the `workspace/configuration` (pull) and `workspace/didChangeConfiguration` * (push) middlewares return/send the same merged object for every requested section. @@ -32,7 +34,7 @@ const configSections = ["js/ts", "typescript", "javascript"]; * This ensures user-set values always win, and declared-but-unset settings * still get their default with the right section precedence. */ -function getMergedExplicitConfiguration(resource: vscode.Uri | undefined): Record { +function getMergedConfiguration(resource: vscode.Uri | undefined): Record { const configs = configSections.map(section => getInspectedConfiguration(section, resource)); // Layer from lowest to highest precedence. @@ -79,12 +81,14 @@ function getInspectedConfiguration( } // Pick the most specific explicitly-set value. - const explicitValue = inspection.workspaceFolderValue - ?? inspection.workspaceValue - ?? inspection.globalValue - ?? inspection.workspaceFolderLanguageValue + // Language-specific overrides (e.g. [typescript]) take precedence + // over non-language values at the same scope. + const explicitValue = inspection.workspaceFolderLanguageValue + ?? inspection.workspaceFolderValue ?? inspection.workspaceLanguageValue - ?? inspection.globalLanguageValue; + ?? inspection.workspaceValue + ?? inspection.globalLanguageValue + ?? inspection.globalValue; if (explicitValue !== undefined) { setNestedValue(explicit, key, toJSONObject(explicitValue)); @@ -132,17 +136,25 @@ function collectConfigurationKeys(config: vscode.WorkspaceConfiguration): string return keys; } +const prototypeKeys = new Set(["__proto__", "constructor", "prototype"]); + function setNestedValue(obj: Record, dottedKey: string, value: any): void { const parts = dottedKey.split("."); let current = obj; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; + if (prototypeKeys.has(part)) { + return; + } if (!(part in current) || typeof current[part] !== "object" || current[part] === null) { current[part] = {}; } current = current[part]; } - current[parts[parts.length - 1]] = value; + const lastPart = parts[parts.length - 1]; + if (!prototypeKeys.has(lastPart)) { + current[lastPart] = value; + } } /** @@ -214,7 +226,7 @@ export const configurationMiddleware: ConfigurationMiddleware = { const key = resource?.toString() ?? ""; let cached = mergedCache.get(key); if (cached === undefined) { - cached = getMergedExplicitConfiguration(resource); + cached = getMergedConfiguration(resource); mergedCache.set(key, cached); } return cached; @@ -235,7 +247,11 @@ export const configurationMiddleware: ConfigurationMiddleware = { /** * Intercepts outgoing workspace/didChangeConfiguration notifications. * Replaces the default settings (which include VS Code defaults) with - * the merged explicit configuration, keyed by section name. + * the merged configuration, keyed by section name. + * + * This is typed as returning `Promise` rather than `void` because the + * `didChangeConfiguration` notification is misannotated in vscode-languageclient + * as returning void, so we must go through `sendNotification` instead. */ export function sendNotificationMiddleware( type: string | MessageSignature, @@ -244,7 +260,7 @@ export function sendNotificationMiddleware( ): Promise { const method = typeof type === "string" ? type : type.method; if (method === "workspace/didChangeConfiguration") { - const merged = getMergedExplicitConfiguration(undefined); + const merged = getMergedConfiguration(undefined); const settings: Record = {}; for (const section of configSections) { settings[section] = merged; From 88d467bb8d92ad48974a6ccc4e30319dbd1de2cd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:06:54 -0800 Subject: [PATCH 07/16] whatever you want, codeql --- _extension/src/configurationMiddleware.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/_extension/src/configurationMiddleware.ts b/_extension/src/configurationMiddleware.ts index c2fbfb6c45e..752ba02c570 100644 --- a/_extension/src/configurationMiddleware.ts +++ b/_extension/src/configurationMiddleware.ts @@ -164,6 +164,9 @@ function setNestedValue(obj: Record, dottedKey: string, value: any) function deepMerge(a: Record, b: Record): Record { const result: Record = { ...a }; for (const key of Object.keys(b)) { + if (prototypeKeys.has(key)) { + continue; + } if ( key in result && result[key] !== null && typeof result[key] === "object" && !Array.isArray(result[key]) From 942627697054288a074369a750086b7a67e3bb26 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:08:07 -0800 Subject: [PATCH 08/16] whatever you want, codeql 2 --- _extension/src/configurationMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_extension/src/configurationMiddleware.ts b/_extension/src/configurationMiddleware.ts index 752ba02c570..d01504a948f 100644 --- a/_extension/src/configurationMiddleware.ts +++ b/_extension/src/configurationMiddleware.ts @@ -147,7 +147,7 @@ function setNestedValue(obj: Record, dottedKey: string, value: any) return; } if (!(part in current) || typeof current[part] !== "object" || current[part] === null) { - current[part] = {}; + current[part] = Object.create(null); } current = current[part]; } From e7ed013ac505ed7e1da5b2bf3be81a31457d2625 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:56:13 -0800 Subject: [PATCH 09/16] Throwing spaghetti at the wall --- _extension/src/configurationMiddleware.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/_extension/src/configurationMiddleware.ts b/_extension/src/configurationMiddleware.ts index d01504a948f..91cc40e1fcf 100644 --- a/_extension/src/configurationMiddleware.ts +++ b/_extension/src/configurationMiddleware.ts @@ -38,7 +38,8 @@ function getMergedConfiguration(resource: vscode.Uri | undefined): Record getInspectedConfiguration(section, resource)); // Layer from lowest to highest precedence. - let merged: Record = {}; + // Use Object.create(null) so the object has no prototype to pollute. + let merged: Record = Object.create(null); // Defaults: javascript < typescript < js/ts for (let i = configs.length - 1; i >= 0; i--) { @@ -67,8 +68,9 @@ function getInspectedConfiguration( resource: vscode.Uri | undefined, ): { explicit: Record | null; defaults: Record | null; } { const config = vscode.workspace.getConfiguration(section, resource); - const explicit: Record = {}; - const defaults: Record = {}; + // Use Object.create(null) so these objects have no prototype to pollute. + const explicit: Record = Object.create(null); + const defaults: Record = Object.create(null); let hasExplicit = false; let hasDefaults = false; @@ -162,7 +164,9 @@ function setNestedValue(obj: Record, dottedKey: string, value: any) * Returns a new object; does not mutate inputs. */ function deepMerge(a: Record, b: Record): Record { - const result: Record = { ...a }; + // Use Object.create(null) so the result has no prototype to pollute. + const result: Record = Object.create(null); + Object.assign(result, a); for (const key of Object.keys(b)) { if (prototypeKeys.has(key)) { continue; From 7780e2c208147878f31dbf8da6632c79059c7a18 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 13 Feb 2026 13:36:58 -0800 Subject: [PATCH 10/16] vscode parsing update --- internal/fourslash/fourslash.go | 9 ++- internal/ls/lsutil/configuration.go | 58 +++++++++++-------- internal/ls/lsutil/formatcodeoptions.go | 13 +++++ internal/lsp/lsproto/lsp_generated.go | 56 ------------------ internal/lsp/server.go | 52 +++++++++++++---- internal/project/session_test.go | 4 +- .../state/codeLensAcrossProjects.baseline | 4 +- 7 files changed, 98 insertions(+), 98 deletions(-) diff --git a/internal/fourslash/fourslash.go b/internal/fourslash/fourslash.go index 034c2ec4ca8..89234bef6ec 100644 --- a/internal/fourslash/fourslash.go +++ b/internal/fourslash/fourslash.go @@ -807,14 +807,13 @@ func (f *FourslashTest) GetOptions() *lsutil.UserPreferences { func (f *FourslashTest) Configure(t *testing.T, config *lsutil.UserPreferences) { // !!! - // Callers to this function may need to consider - // sending a more specific configuration for 'javascript' - // or 'js/ts' as well. For now, we only send 'typescript', - // and most tests probably just want this. + // We send 'js/ts' by default because that is what we expect the primary config to be in vscode and VS (one + // set of preferences for both languages). This should be fine in fourslash since tests that need + // multiple options usually send reconfiguration commands for each `verify` anyways f.userPreferences = config sendNotification(t, f, lsproto.WorkspaceDidChangeConfigurationInfo, &lsproto.DidChangeConfigurationParams{ Settings: map[string]any{ - "typescript": config, + "js/ts": config, }, }) } diff --git a/internal/ls/lsutil/configuration.go b/internal/ls/lsutil/configuration.go index 5d3c17ff29e..dc30d2c2eb9 100644 --- a/internal/ls/lsutil/configuration.go +++ b/internal/ls/lsutil/configuration.go @@ -78,31 +78,43 @@ func (c *UserConfig) GetPreferences(activeFile string) *UserPreferences { return NewDefaultUserPreferences() } -func ParseNewUserConfig(items []any) *UserConfig { - defaultPref := NewUserConfig(NewDefaultUserPreferences()) +func ParseNewUserConfig(items map[string]any) *UserConfig { + defaultPref := NewDefaultUserPreferences() + if editorItem, ok := items["editor"]; ok && editorItem != nil { + if editorSettings, ok := editorItem.(map[string]any); ok { + defaultPref.FormatCodeSettings = defaultPref.FormatCodeSettings.ParseEditorSettings(editorSettings) + } + } + if jsTsItem, ok := items["js/ts"]; ok && jsTsItem != nil { + // if "js/ts" is provided, we assume they are already resolved and merged + switch jsTsSettings := jsTsItem.(type) { + case map[string]any: + return NewUserConfig(defaultPref.ParseWorker(jsTsSettings)) + case *UserPreferences: + // case for fourslash -- fourslash sends the entire userPreferences over in "js/ts" + return NewUserConfig(jsTsSettings) + } + } + + // set typescript and javascript preferences separately c := &UserConfig{} - for i, item := range items { - if item == nil { - // continue - } else if config, ok := item.(map[string]any); ok { - switch i { - case 0: - // if provided, parse and set "js/ts" as base config - defaultPref = NewUserConfig(defaultPref.ts.ParseWorker(config)) - c = defaultPref.Copy() - continue - case 1: - // typescript - c.ts = defaultPref.ts.ParseWorker(config) - case 2: - // javascript - c.js = defaultPref.js.ParseWorker(config) - } - } else if item, ok := item.(*UserPreferences); ok { - // case for fourslash -- fourslash sends the entire userPreferences over - // !!! support format and js/ts distinction? - return NewUserConfig(item) + if tsItem, ok := items["typescript"]; ok && tsItem != nil { + switch tsSettings := tsItem.(type) { + case map[string]any: + c.ts = defaultPref.Copy().ParseWorker(tsSettings) + case *UserPreferences: + c.ts = tsSettings } } + + if jsItem, ok := items["javascript"]; ok && jsItem != nil { + switch jsSettings := jsItem.(type) { + case map[string]any: + c.js = defaultPref.Copy().ParseWorker(jsSettings) + case *UserPreferences: + c.js = jsSettings + } + } + return c } diff --git a/internal/ls/lsutil/formatcodeoptions.go b/internal/ls/lsutil/formatcodeoptions.go index 04162a71d64..15e27c02972 100644 --- a/internal/ls/lsutil/formatcodeoptions.go +++ b/internal/ls/lsutil/formatcodeoptions.go @@ -107,6 +107,19 @@ func (settings *FormatCodeSettings) ToLSFormatOptions() *lsproto.FormattingOptio } } +func (settings *FormatCodeSettings) ParseEditorSettings(editorSettings map[string]any) *FormatCodeSettings { + if editorSettings == nil { + return settings + } + for name, value := range editorSettings { + switch strings.ToLower(name) { + case "baseindentsize", "indentsize", "tabsize", "newlinecharacter", "converttabstospaces", "indentstyle", "trimtrailingwhitespace": + settings.Set(name, value) + } + } + return settings +} + func (settings *FormatCodeSettings) Parse(prefs any) bool { formatSettingsMap, ok := prefs.(map[string]any) formatSettingsParsed := false diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 63ac52694f9..d1a341a1434 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21630,62 +21630,6 @@ type InitializationOptions struct { Typescript *any `json:"typescript,omitzero"` } -type FormatOptions struct { - InsertSpaceAfterCommaDelimiter bool `json:"InsertSpaceAfterCommaDelimiter,omitzero"` - - InsertSpaceAfterSemicolonInForStatements bool `json:"InsertSpaceAfterSemicolonInForStatements,omitzero"` - - InsertSpaceBeforeAndAfterBinaryOperators bool `json:"InsertSpaceBeforeAndAfterBinaryOperators,omitzero"` - - InsertSpaceAfterConstructor bool `json:"InsertSpaceAfterConstructor,omitzero"` - - InsertSpaceAfterKeywordsInControlFlowStatements bool `json:"InsertSpaceAfterKeywordsInControlFlowStatements,omitzero"` - - InsertSpaceAfterFunctionKeywordForAnonymousFunctions bool `json:"InsertSpaceAfterFunctionKeywordForAnonymousFunctions,omitzero"` - - InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis bool `json:"InsertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis,omitzero"` - - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets bool `json:"InsertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets,omitzero"` - - InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingNonemptyBraces,omitzero"` - - InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingEmptyBraces,omitzero"` - - InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces,omitzero"` - - InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces bool `json:"InsertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces,omitzero"` - - InsertSpaceAfterTypeAssertion bool `json:"InsertSpaceAfterTypeAssertion,omitzero"` - - InsertSpaceBeforeFunctionParenthesis bool `json:"InsertSpaceBeforeFunctionParenthesis,omitzero"` - - PlaceOpenBraceOnNewLineForFunctions bool `json:"PlaceOpenBraceOnNewLineForFunctions,omitzero"` - - PlaceOpenBraceOnNewLineForControlBlocks bool `json:"PlaceOpenBraceOnNewLineForControlBlocks,omitzero"` - - InsertSpaceBeforeTypeAnnotation bool `json:"InsertSpaceBeforeTypeAnnotation,omitzero"` - - IndentMultiLineObjectLiteralBeginningOnBlankLine bool `json:"IndentMultiLineObjectLiteralBeginningOnBlankLine,omitzero"` - - IndentSwitchCase bool `json:"IndentSwitchCase,omitzero"` - - Semicolons string `json:"Semicolons,omitzero"` - - BaseIndentSize int32 `json:"BaseIndentSize,omitzero"` - - IndentSize int32 `json:"IndentSize,omitzero"` - - TabSize int32 `json:"TabSize,omitzero"` - - NewLineCharacter string `json:"NewLineCharacter,omitzero"` - - ConvertTabsToSpaces bool `json:"ConvertTabsToSpaces,omitzero"` - - IndentStyle string `json:"IndentStyle,omitzero"` - - TrimTrailingWhitespace bool `json:"TrimTrailingWhitespace,omitzero"` -} - // AutoImportFix contains information about an auto-import suggestion. type AutoImportFix struct { Kind AutoImportFixKind `json:"kind,omitzero"` diff --git a/internal/lsp/server.go b/internal/lsp/server.go index ee88483b1f9..e808fc0600c 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -288,7 +288,9 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, *s.initializeParams.InitializationOptions.Typescript, ) // Any options received via initializationOptions will be used for both `js` and `ts`options - return lsutil.ParseNewUserConfig([]any{*s.initializeParams.InitializationOptions.Typescript}), nil + if config, ok := (*s.initializeParams.InitializationOptions.Typescript).(map[string]any); ok { + return lsutil.NewUserConfig(lsutil.NewDefaultUserPreferences().ParseWorker(config)), nil + } } // if no configuration request capapbility, return default config return lsutil.NewUserConfig(nil), nil @@ -304,18 +306,35 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, { Section: ptrTo("javascript"), }, + { + Section: ptrTo("editor"), + }, }, }) if err != nil { return &lsutil.UserConfig{}, fmt.Errorf("configure request failed: %w", err) } + configMap := map[string]any{} + for i, config := range configs { + switch i { + case 0: + configMap["js/ts"] = config + case 1: + configMap["typescript"] = config + case 2: + configMap["javascript"] = config + case 3: + configMap["editor"] = config + } + } s.logger.Logf( - "received options from workspace/configuration request:\njs/ts: %+v\n\ntypescript: %+v\n\njavascript: %+v\n", - configs[0], - configs[1], - configs[2], + "received options from workspace/configuration request:\njs/ts: %+v\n\ntypescript: %+v\n\njavascript: %+v\n\neditor: %+v\n", + configMap["js/ts"], + configMap["typescript"], + configMap["javascript"], + configMap["editor"], ) - return lsutil.ParseNewUserConfig(configs), nil + return lsutil.ParseNewUserConfig(configMap), nil } func (s *Server) Run(ctx context.Context) error { @@ -1016,7 +1035,7 @@ func (s *Server) handleInitialized(ctx context.Context, params *lsproto.Initiali RegisterOptions: &lsproto.RegisterOptions{ DidChangeConfiguration: &lsproto.DidChangeConfigurationRegistrationOptions{ Section: &lsproto.StringOrStrings{ - Strings: &[]string{"js/ts", "typescript", "javascript"}, + Strings: &[]string{"js/ts", "typescript", "javascript", "editor"}, }, }, }, @@ -1052,10 +1071,23 @@ func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, para if params.Settings == nil { return nil } else if settings, ok := params.Settings.([]any); ok { - s.session.Configure(lsutil.ParseNewUserConfig(settings)) + settingsMap := map[string]any{} + for i, item := range []string{"js/ts", "typescript", "javascript", "editor"} { + if i < len(settings) { + settingsMap[item] = settings[i] + } + } + s.session.Configure(lsutil.ParseNewUserConfig(settingsMap)) } else if settings, ok := params.Settings.(map[string]any); ok { - // fourslash case - s.session.Configure(lsutil.ParseNewUserConfig([]any{settings["js/ts"], settings["typescript"], settings["javascript"]})) + for key := range settings { + switch key { + case "js/ts", "typescript", "javascript", "editor": + continue + default: + delete(settings, key) + } + } + s.session.Configure(lsutil.ParseNewUserConfig(settings)) } return nil } diff --git a/internal/project/session_test.go b/internal/project/session_test.go index 1db7aeae104..28bb13bccc2 100644 --- a/internal/project/session_test.go +++ b/internal/project/session_test.go @@ -922,7 +922,7 @@ func TestSession(t *testing.T) { "OrganizeImportsIgnoreCase": true, } // set "typescript" options only - session.Configure(lsutil.ParseNewUserConfig([]any{nil, configMap1, nil})) + session.Configure(lsutil.ParseNewUserConfig(map[string]any{"typescript": configMap1})) actualConfig1 := session.Config() expectedPrefs1 := lsutil.NewDefaultUserPreferences() expectedPrefs1.UseAliasesForRename = core.TSTrue @@ -939,7 +939,7 @@ func TestSession(t *testing.T) { "OrganizeImportsIgnoreCase": false, } // set "javascript" options only - session.Configure(lsutil.ParseNewUserConfig([]any{nil, nil, configMap2})) + session.Configure(lsutil.ParseNewUserConfig(map[string]any{"javascript": configMap2})) actualConfig2 := session.Config() expectedPrefs2 := lsutil.NewDefaultUserPreferences() expectedPrefs2.UseAliasesForRename = core.TSFalse diff --git a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline index 9600c980c61..a8df600b277 100644 --- a/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline +++ b/testdata/baselines/reference/fourslash/state/codeLensAcrossProjects.baseline @@ -161,7 +161,7 @@ Config File Names:: "method": "workspace/didChangeConfiguration", "params": { "settings": { - "typescript": { + "js/ts": { "FormatCodeSettings": null, "QuotePreference": "", "LazyConfiguredProjectsFromExternalProject": false, @@ -609,7 +609,7 @@ Config:: "method": "workspace/didChangeConfiguration", "params": { "settings": { - "typescript": { + "js/ts": { "FormatCodeSettings": { "BaseIndentSize": 0, "IndentSize": 4, From 15909a1f1829c96f32a7ab18d9f8a8bc843420fc Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 13 Feb 2026 14:23:42 -0800 Subject: [PATCH 11/16] update server.go --- internal/lsp/server.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 3668ae0127b..8f98db0f121 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -310,7 +310,7 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, Section: new("javascript"), }, { - Section: ptrTo("editor"), + Section: new("editor"), }, }, }) @@ -1115,17 +1115,8 @@ func (s *Server) handleExit(ctx context.Context, params any) error { } func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, params *lsproto.DidChangeConfigurationParams) error { - // !!! only implemented because needed for fourslash if params.Settings == nil { return nil - } else if settings, ok := params.Settings.([]any); ok { - settingsMap := map[string]any{} - for i, item := range []string{"js/ts", "typescript", "javascript", "editor"} { - if i < len(settings) { - settingsMap[item] = settings[i] - } - } - s.session.Configure(lsutil.ParseNewUserConfig(settingsMap)) } else if settings, ok := params.Settings.(map[string]any); ok { for key := range settings { switch key { From 892e2b0ba7e653269ceadf9e957e26259f46e7e4 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 13 Feb 2026 15:05:50 -0800 Subject: [PATCH 12/16] rename fields for initialization options and remove excess code --- internal/lsp/lsproto/_generate/generate.mts | 2 +- internal/lsp/lsproto/lsp_generated.go | 2 +- internal/lsp/server.go | 18 +++++------------- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 2bd9d975bf8..4f7bbbee6f8 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -49,7 +49,7 @@ const customStructures: Structure[] = [ documentation: "The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`.", }, { - name: "typescript", + name: "userPreferences", type: { kind: "reference", name: "any" }, optional: true, documentation: "userPreferences and/or formatting options or provided at initialization.", diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index d1a341a1434..6eed12843ed 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21627,7 +21627,7 @@ type InitializationOptions struct { CodeLensShowLocationsCommandName *string `json:"codeLensShowLocationsCommandName,omitzero"` // userPreferences and/or formatting options or provided at initialization. - Typescript *any `json:"typescript,omitzero"` + UserPreferences *any `json:"userPreferences,omitzero"` } // AutoImportFix contains information about an auto-import suggestion. diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 8f98db0f121..d15e0bc444b 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -284,14 +284,14 @@ func (s *Server) RefreshCodeLens(ctx context.Context) error { func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, error) { caps := lsproto.GetClientCapabilities(ctx) if !caps.Workspace.Configuration { - if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && s.initializeParams.InitializationOptions.Typescript != nil { + if s.initializeParams != nil && s.initializeParams.InitializationOptions != nil && s.initializeParams.InitializationOptions.UserPreferences != nil { s.logger.Logf( "received formatting options from initialization: %T\n%+v", - *s.initializeParams.InitializationOptions.Typescript, - *s.initializeParams.InitializationOptions.Typescript, + *s.initializeParams.InitializationOptions.UserPreferences, + *s.initializeParams.InitializationOptions.UserPreferences, ) - // Any options received via initializationOptions will be used for both `js` and `ts`options - if config, ok := (*s.initializeParams.InitializationOptions.Typescript).(map[string]any); ok { + // Any options received via initializationOptions will be used for both `js` and `ts` options + if config, ok := (*s.initializeParams.InitializationOptions.UserPreferences).(map[string]any); ok { return lsutil.NewUserConfig(lsutil.NewDefaultUserPreferences().ParseWorker(config)), nil } } @@ -1118,14 +1118,6 @@ func (s *Server) handleDidChangeWorkspaceConfiguration(ctx context.Context, para if params.Settings == nil { return nil } else if settings, ok := params.Settings.(map[string]any); ok { - for key := range settings { - switch key { - case "js/ts", "typescript", "javascript", "editor": - continue - default: - delete(settings, key) - } - } s.session.Configure(lsutil.ParseNewUserConfig(settings)) } return nil From 016028619de0e1bc378bfbf64c72a52aa111d347 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Fri, 13 Feb 2026 16:29:46 -0800 Subject: [PATCH 13/16] update generate --- internal/lsp/lsproto/_generate/generate.mts | 4 ++-- internal/lsp/lsproto/lsp_generated.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 4f7bbbee6f8..afdc9410710 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -52,7 +52,7 @@ const customStructures: Structure[] = [ name: "userPreferences", type: { kind: "reference", name: "any" }, optional: true, - documentation: "userPreferences and/or formatting options or provided at initialization.", + documentation: "userPreferences and/or formatting options provided at initialization.", }, ], documentation: "InitializationOptions contains user-provided initialization options.", @@ -786,7 +786,7 @@ function handleOrType(orType: OrType): GoType { let memberNames = nonNullTypes.map(type => { if (type.kind === "reference") { - return type.name.slice(type.name.lastIndexOf(".") + 1); + return type.name } else if (type.kind === "base") { return titleCase(type.name); diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 6eed12843ed..851a3cb6847 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21626,7 +21626,7 @@ type InitializationOptions struct { // The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`. CodeLensShowLocationsCommandName *string `json:"codeLensShowLocationsCommandName,omitzero"` - // userPreferences and/or formatting options or provided at initialization. + // userPreferences and/or formatting options provided at initialization. UserPreferences *any `json:"userPreferences,omitzero"` } From 20a27fb0cf8be91c2d0f2a2e28dd3484205e616c Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 16 Feb 2026 15:54:32 -0800 Subject: [PATCH 14/16] format --- internal/lsp/lsproto/_generate/generate.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index afdc9410710..4884a6d6fa1 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -786,7 +786,7 @@ function handleOrType(orType: OrType): GoType { let memberNames = nonNullTypes.map(type => { if (type.kind === "reference") { - return type.name + return type.name; } else if (type.kind === "base") { return titleCase(type.name); From 776163d953a3370d3dd4538839957877bec0a427 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Mon, 16 Feb 2026 16:01:56 -0800 Subject: [PATCH 15/16] typos --- internal/lsp/lsproto/_generate/generate.mts | 2 +- internal/lsp/server.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/lsp/lsproto/_generate/generate.mts b/internal/lsp/lsproto/_generate/generate.mts index 4884a6d6fa1..150502187e6 100644 --- a/internal/lsp/lsproto/_generate/generate.mts +++ b/internal/lsp/lsproto/_generate/generate.mts @@ -52,7 +52,7 @@ const customStructures: Structure[] = [ name: "userPreferences", type: { kind: "reference", name: "any" }, optional: true, - documentation: "userPreferences and/or formatting options provided at initialization.", + documentation: "userPreferences and/or formatting options if provided at initialization.", }, ], documentation: "InitializationOptions contains user-provided initialization options.", diff --git a/internal/lsp/server.go b/internal/lsp/server.go index d15e0bc444b..66ae1d02071 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -295,7 +295,7 @@ func (s *Server) RequestConfiguration(ctx context.Context) (*lsutil.UserConfig, return lsutil.NewUserConfig(lsutil.NewDefaultUserPreferences().ParseWorker(config)), nil } } - // if no configuration request capapbility, return default config + // if no configuration request capability, return default config return lsutil.NewUserConfig(nil), nil } configs, err := sendClientRequest(ctx, s, lsproto.WorkspaceConfigurationInfo, &lsproto.ConfigurationParams{ From 5916c4aecc4da059e4db702dd106069347f2cbb3 Mon Sep 17 00:00:00 2001 From: Isabel Duan Date: Thu, 19 Feb 2026 11:51:47 -0800 Subject: [PATCH 16/16] generate --- internal/lsp/lsproto/lsp_generated.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/lsp/lsproto/lsp_generated.go b/internal/lsp/lsproto/lsp_generated.go index 851a3cb6847..6cfef994de3 100644 --- a/internal/lsp/lsproto/lsp_generated.go +++ b/internal/lsp/lsproto/lsp_generated.go @@ -21626,7 +21626,7 @@ type InitializationOptions struct { // The client-side command name that resolved references/implementations `CodeLens` should trigger. Arguments passed will be `(DocumentUri, Position, Location[])`. CodeLensShowLocationsCommandName *string `json:"codeLensShowLocationsCommandName,omitzero"` - // userPreferences and/or formatting options provided at initialization. + // userPreferences and/or formatting options if provided at initialization. UserPreferences *any `json:"userPreferences,omitzero"` }