diff --git a/formatter/body.go b/formatter/body.go index a3f21ec..831d842 100644 --- a/formatter/body.go +++ b/formatter/body.go @@ -2,6 +2,7 @@ package formatter import ( "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/syntax" ) @@ -25,7 +26,7 @@ func (f *formatter) structLike(v *syntax.Struct) doc.Doc { // bracedBody formats struct, union, and exception declarations. The header // renders as a token run up to and including the open brace; the brace // text itself is emitted by bracedGroup. -func (f *formatter) bracedBody(fields []*syntax.Field, open, close int, closeTrailing bool, c Construct) doc.Doc { +func (f *formatter) bracedBody(fields []*syntax.Field, open, close int, closeTrailing bool, c options.Construct) doc.Doc { bodyID := f.id() sepMode := f.opts.Separator.Get(c) forced := f.opts.Break.Get(c) || sepForcesBreak(sepsOfFields(fields), sepMode) @@ -61,15 +62,15 @@ func (f *formatter) scanKind(start, end int, kind syntax.TokenKind) int { } // constructOf returns the construct for the struct-like kind. -func (f *formatter) constructOf(kind syntax.StructKind) Construct { +func (f *formatter) constructOf(kind syntax.StructKind) options.Construct { switch kind { case syntax.TokenUnion: - return ConstructUnion + return options.ConstructUnion case syntax.TokenException: - return ConstructException + return options.ConstructException } - return ConstructStruct + return options.ConstructStruct } // bracedGroup assembles "{ body }" from the prebuilt body list: flat as @@ -112,8 +113,8 @@ func (f *formatter) bracedGroup(body doc.Doc, bodyID, n, open, close int, closeT // bracedEnumBody is bracedBody for enum values. func (f *formatter) bracedEnumBody(values []*syntax.EnumValue, open, close int, closeTrailing bool) doc.Doc { bodyID := f.id() - sepMode := f.opts.Separator.Get(ConstructEnum) - forced := f.opts.Break.Get(ConstructEnum) || sepForcesBreak(sepsOfValues(values), sepMode) + sepMode := f.opts.Separator.Get(options.ConstructEnum) + forced := f.opts.Break.Get(options.ConstructEnum) || sepForcesBreak(sepsOfValues(values), sepMode) return f.bracedGroup(f.enumValueList(values, bodyID), bodyID, len(values), open, close, closeTrailing, forced) } @@ -205,8 +206,8 @@ func (f *formatter) functionBody(v *syntax.Function) doc.Doc { // Comments or blank lines in the arguments force the multiline layout: // the flat argument group would drop them. - argsMode := f.opts.Separator.Get(ConstructArguments) - if f.fieldsForcedBroken(v.Args) || sepForcesBreak(sepsOfFields(v.Args), argsMode) || f.opts.Break.Get(ConstructArguments) { + argsMode := f.opts.Separator.Get(options.ConstructArguments) + if f.fieldsForcedBroken(v.Args) || sepForcesBreak(sepsOfFields(v.Args), argsMode) || f.opts.Break.Get(options.ConstructArguments) { return f.functionBrokenArgs(v, header) } @@ -249,11 +250,11 @@ func (f *formatter) parenGroup(fields []*syntax.Field, open, close int, forced b // throwsGroup renders the throws clause with the same folding as the // arguments, so it stays flat when it fits even if the arguments broke. func (f *formatter) throwsGroup(v *syntax.Function) doc.Doc { - forced := f.fieldsForcedBroken(v.Throws.Fields) || sepForcesBreak(sepsOfFields(v.Throws.Fields), f.opts.Separator.Get(ConstructThrows)) || f.opts.Break.Get(ConstructThrows) + forced := f.fieldsForcedBroken(v.Throws.Fields) || sepForcesBreak(sepsOfFields(v.Throws.Fields), f.opts.Separator.Get(options.ConstructThrows)) || f.opts.Break.Get(options.ConstructThrows) p := f.Parts(2) p = append(p, f.Text(" throws ")) - p = append(p, f.parenGroup(v.Throws.Fields, v.Throws.TokStart(), v.Throws.TokEnd(), forced, f.opts.Separator.Get(ConstructThrows))) + p = append(p, f.parenGroup(v.Throws.Fields, v.Throws.TokStart(), v.Throws.TokEnd(), forced, f.opts.Separator.Get(options.ConstructThrows))) return f.Concat(p...) } @@ -335,7 +336,7 @@ func (f *formatter) functionBrokenArgs(v *syntax.Function, header doc.Doc) doc.D parts := []doc.Doc{ header, - f.parenGroup(v.Args, open, f.parenClose(v.Args, open), true, f.opts.Separator.Get(ConstructArguments)), + f.parenGroup(v.Args, open, f.parenClose(v.Args, open), true, f.opts.Separator.Get(options.ConstructArguments)), } if v.Throws != nil { parts = append(parts, f.throwsGroup(v)) diff --git a/formatter/config.go b/formatter/config.go new file mode 100644 index 0000000..88c61df --- /dev/null +++ b/formatter/config.go @@ -0,0 +1,86 @@ +package formatter + +import ( + "github.com/karitham/thrift-ls/options" +) + +// FromConfig converts a validated configuration patch to formatter options. +// The formatter owns this translation because the config strings ("field", +// "comma", ...) name formatting concepts only it defines. +func FromConfig(p options.Patch) (Options, error) { + if err := p.Validate(); err != nil { + return Options{}, err + } + + o := DefaultOptions() + if p.PrintWidth != nil { + o.PrintWidth = *p.PrintWidth + } + + if p.Indent != nil { + o.Indent = p.Indent.Value + o.TabWidth = p.Indent.Width + } + + if p.TabWidth != nil { + o.TabWidth = *p.TabWidth + } + + if p.Align != nil { + if mode, ok := alignMode(*p.Align); ok { + o.Align = mode + } + } + + if p.Separators != nil { + for _, c := range options.AllConstructs { + if v := p.Separators.Get(c); v != nil { + if mode, ok := separatorMode(*v); ok { + o.Separator.Set(c, mode) + } + } + } + } + + if p.Break != nil { + for _, c := range options.AllConstructs { + if v := p.Break.Get(c); v != nil { + o.Break.Set(c, *v) + } + } + } + + return o, nil +} + +// alignMode maps a config value to an align mode. The second result reports +// whether the value is a known align mode. +func alignMode(s string) (AlignMode, bool) { + switch s { + case "field": + return AlignField, true + case "assign": + return AlignAssign, true + case "disable": + return AlignDisable, true + default: + return 0, false + } +} + +// separatorMode maps a config value to a separator mode. The second result +// reports whether the value is a known separator mode. +func separatorMode(s string) (SeparatorMode, bool) { + switch s { + case "comma": + return SeparatorComma, true + case "semicolon": + return SeparatorSemicolon, true + case "none": + return SeparatorNone, true + case "preserve": + return SeparatorPreserve, true + default: + return 0, false + } +} diff --git a/formatter/config_test.go b/formatter/config_test.go new file mode 100644 index 0000000..7e012f2 --- /dev/null +++ b/formatter/config_test.go @@ -0,0 +1,117 @@ +package formatter + +import ( + "testing" + + "github.com/karitham/thrift-ls/options" +) + +func TestFromConfig(t *testing.T) { + indent := options.Indent{Value: " ", Width: 2} + printWidth := 100 + p := options.Patch{Indent: &indent, PrintWidth: &printWidth} + + o, err := FromConfig(p) + if err != nil { + t.Fatalf("FromConfig: %v", err) + } + + if o.PrintWidth != 100 || o.Indent != " " || o.TabWidth != 2 { + t.Errorf("got %+v", o) + } + + if o.Align != AlignField || o.Separator.Get(options.ConstructStruct) != SeparatorPreserve { + t.Errorf("defaults wrong: %+v", o) + } + + comma := "comma" + align := "assign" + separators := options.Separators{Structs: &comma} + p = options.Patch{Separators: &separators, Align: &align} + + o, err = FromConfig(p) + if err != nil { + t.Fatalf("FromConfig: %v", err) + } + + if o.Separator.Get(options.ConstructStruct) != SeparatorComma || o.Align != AlignAssign { + t.Errorf("got %+v", o) + } +} + +// TestFromConfigSeparatorModes maps every config value to the separator +// modes, per construct. +func TestFromConfigSeparatorModes(t *testing.T) { + tests := []struct { + value string + want SeparatorMode + }{ + {"comma", SeparatorComma}, + {"none", SeparatorNone}, + {"semicolon", SeparatorSemicolon}, + {"preserve", SeparatorPreserve}, + } + for _, tt := range tests { + t.Run(tt.value, func(t *testing.T) { + value := tt.value + p := options.Patch{Separators: &options.Separators{ + Structs: &value, Unions: &value, Exceptions: &value, + Enums: &value, Arguments: &value, Throws: &value, + Lists: &value, Maps: &value, Sets: &value, + }} + + o, err := FromConfig(p) + if err != nil { + t.Fatalf("FromConfig: %v", err) + } + + for _, c := range options.AllConstructs { + if o.Separator.Get(c) != tt.want { + t.Errorf("value %q: construct %s = %v, want %v", tt.value, c, o.Separator.Get(c), tt.want) + } + } + }) + } + + // The option maps independently per construct. + semicolon, comma := "semicolon", "comma" + separators := options.Separators{Structs: &semicolon, Enums: &semicolon, Arguments: &comma, Throws: &comma} + p := options.Patch{Separators: &separators} + + o, err := FromConfig(p) + if err != nil { + t.Fatalf("FromConfig: %v", err) + } + + if o.Separator.Get(options.ConstructStruct) != SeparatorSemicolon || o.Separator.Get(options.ConstructArguments) != SeparatorComma { + t.Errorf("independent mapping failed: %+v", o) + } +} + +// TestFromConfigBreak maps the break group to the formatter options. +func TestFromConfigBreak(t *testing.T) { + trueVal, falseVal := true, false + + p := options.Patch{Break: &options.Break{Structs: &trueVal, Enums: &falseVal}} + + o, err := FromConfig(p) + if err != nil { + t.Fatalf("FromConfig: %v", err) + } + + if !o.Break.Get(options.ConstructStruct) || o.Break.Get(options.ConstructEnum) { + t.Errorf("break mapping wrong: %+v", o) + } + + // Zero patch keeps the defaults (no forced breaks). + o, err = FromConfig(options.Patch{}) + if err != nil { + t.Fatalf("FromConfig: %v", err) + } + + for _, c := range options.AllConstructs { + if o.Break.Get(c) { + t.Errorf("breaks should default to false for %s: %+v", c, o) + } + } +} diff --git a/formatter/field.go b/formatter/field.go index e8dc532..a95ed2a 100644 --- a/formatter/field.go +++ b/formatter/field.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/syntax" ) @@ -69,11 +70,11 @@ func (f *formatter) enumValueList(values []*syntax.EnumValue, bodyID int) doc.Do for i, value := range values { if i > 0 { - parts = append(parts, f.fieldSep(values[i-1].Sep, f.opts.Separator.Get(ConstructEnum))) + parts = append(parts, f.fieldSep(values[i-1].Sep, f.opts.Separator.Get(options.ConstructEnum))) } parts = append(parts, f.blankLines(value, doc.HardLine)...) - parts = append(parts, f.enumValue(value, f.alignmentForEnum(values, i, f.opts.Separator.Get(ConstructEnum)), bodyID)) + parts = append(parts, f.enumValue(value, f.alignmentForEnum(values, i, f.opts.Separator.Get(options.ConstructEnum)), bodyID)) } return f.Concat(parts...) @@ -448,7 +449,7 @@ func (f *formatter) fieldPads(v *syntax.Field, a *columnAlign) ([]padEntry, stri // enumValue assembles an enum value with comments, aligning '=' signs when // the body breaks. func (f *formatter) enumValue(v *syntax.EnumValue, align *columnAlign, bodyID int) doc.Doc { - sepMode := f.opts.Separator.Get(ConstructEnum) + sepMode := f.opts.Separator.Get(options.ConstructEnum) broken := f.Concat( f.enumValueContent(v, align, true, sepMode), diff --git a/formatter/format.go b/formatter/format.go index 15f7134..7960e6a 100644 --- a/formatter/format.go +++ b/formatter/format.go @@ -16,6 +16,7 @@ import ( "sync" "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/syntax" ) @@ -48,115 +49,6 @@ const ( SeparatorNone ) -// Construct identifies a construct with per-construct options. -type Construct uint8 - -const ( - ConstructStruct Construct = iota - ConstructUnion - ConstructException - ConstructEnum - ConstructArguments - ConstructThrows - ConstructList - ConstructMap - ConstructSet -) - -// PerConstruct holds one option value per construct. The JSON tags make the -// per-construct option maps config-compatible ("structs", "arguments", ...), -// so the options layer and the CLI share this single source of truth. -type PerConstruct[T any] struct { - Structs T `json:"structs"` - Unions T `json:"unions"` - Exceptions T `json:"exceptions"` - Enums T `json:"enums"` - Arguments T `json:"arguments"` - Throws T `json:"throws"` - Lists T `json:"lists"` - Maps T `json:"maps"` - Sets T `json:"sets"` -} - -// Get returns the value for the construct. -func (p PerConstruct[T]) Get(c Construct) T { - switch c { - case ConstructUnion: - return p.Unions - case ConstructException: - return p.Exceptions - case ConstructEnum: - return p.Enums - case ConstructArguments: - return p.Arguments - case ConstructThrows: - return p.Throws - case ConstructList: - return p.Lists - case ConstructMap: - return p.Maps - case ConstructSet: - return p.Sets - } - - return p.Structs -} - -// Set assigns the value for the construct. -func (p *PerConstruct[T]) Set(c Construct, v T) { - switch c { - case ConstructUnion: - p.Unions = v - case ConstructException: - p.Exceptions = v - case ConstructEnum: - p.Enums = v - case ConstructArguments: - p.Arguments = v - case ConstructThrows: - p.Throws = v - case ConstructList: - p.Lists = v - case ConstructMap: - p.Maps = v - case ConstructSet: - p.Sets = v - default: - p.Structs = v - } -} - -// AllConstructs lists every construct, in config order. -var AllConstructs = []Construct{ - ConstructStruct, ConstructUnion, ConstructException, - ConstructEnum, ConstructArguments, ConstructThrows, - ConstructList, ConstructMap, ConstructSet, -} - -// String returns the config key of the construct. -func (c Construct) String() string { - switch c { - case ConstructUnion: - return "unions" - case ConstructException: - return "exceptions" - case ConstructEnum: - return "enums" - case ConstructArguments: - return "arguments" - case ConstructThrows: - return "throws" - case ConstructList: - return "lists" - case ConstructMap: - return "maps" - case ConstructSet: - return "sets" - } - - return "structs" -} - // Options controls formatting behavior. Zero values mean defaults. type Options struct { // PrintWidth is the target line width. Must be positive. @@ -170,10 +62,10 @@ type Options struct { Align AlignMode // Separator controls trailing separators per construct (default // SeparatorPreserve). - Separator PerConstruct[SeparatorMode] + Separator options.PerConstruct[SeparatorMode] // Break forces the multiline layout per construct, even when the body // fits on one line. - Break PerConstruct[bool] + Break options.PerConstruct[bool] // NoTrailingNewline suppresses the final newline that is otherwise // appended to the formatted output. NoTrailingNewline bool @@ -186,7 +78,7 @@ func DefaultOptions() Options { Indent: " ", TabWidth: 4, Align: AlignField, - Separator: PerConstruct[SeparatorMode]{ + Separator: options.PerConstruct[SeparatorMode]{ Structs: SeparatorPreserve, Unions: SeparatorPreserve, Exceptions: SeparatorPreserve, diff --git a/formatter/format_fuzz_test.go b/formatter/format_fuzz_test.go index ef9036d..8e8285f 100644 --- a/formatter/format_fuzz_test.go +++ b/formatter/format_fuzz_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/syntax" ) @@ -68,7 +69,7 @@ func FuzzFormat(f *testing.F) { // In preserve mode every field and enum separator survives. allPreserve := true - for _, c := range AllConstructs { + for _, c := range options.AllConstructs { if opts.Separator.Get(c) != SeparatorPreserve { allPreserve = false } @@ -136,7 +137,7 @@ func fuzzOpts(src string) Options { o.Align = AlignDisable } - for i, c := range AllConstructs { + for i, c := range options.AllConstructs { o.Separator.Set(c, SeparatorMode((h[i%4]+i)%4)) o.Break.Set(c, h[(i+1)%4]%2 == 0) } diff --git a/formatter/format_test.go b/formatter/format_test.go index a7511f6..c1f7bb7 100644 --- a/formatter/format_test.go +++ b/formatter/format_test.go @@ -4,6 +4,7 @@ import ( "strconv" "testing" + "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/syntax" ) @@ -55,7 +56,7 @@ func testOpts(width int) Options { // commaOpts returns testOpts at width with the given struct separator. func commaOpts(width int, mode SeparatorMode) Options { o := testOpts(width) - o.Separator.Set(ConstructStruct, mode) + o.Separator.Set(options.ConstructStruct, mode) return o } @@ -663,9 +664,9 @@ func TestFormatSeparators(t *testing.T) { name: "fields semicolon, functions comma", opts: func() Options { o := testOpts(30) - o.Separator.Set(ConstructStruct, SeparatorSemicolon) - o.Separator.Set(ConstructArguments, SeparatorComma) - o.Separator.Set(ConstructThrows, SeparatorComma) + o.Separator.Set(options.ConstructStruct, SeparatorSemicolon) + o.Separator.Set(options.ConstructArguments, SeparatorComma) + o.Separator.Set(options.ConstructThrows, SeparatorComma) return o }(), @@ -694,8 +695,8 @@ func TestFormatSeparators(t *testing.T) { name: "function comma add forces commas on throws", opts: func() Options { o := testOpts(30) - o.Separator.Set(ConstructArguments, SeparatorComma) - o.Separator.Set(ConstructThrows, SeparatorComma) + o.Separator.Set(options.ConstructArguments, SeparatorComma) + o.Separator.Set(options.ConstructThrows, SeparatorComma) return o }(), @@ -706,8 +707,8 @@ func TestFormatSeparators(t *testing.T) { name: "function comma remove drops argument separators", opts: func() Options { o := testOpts(30) - o.Separator.Set(ConstructArguments, SeparatorNone) - o.Separator.Set(ConstructThrows, SeparatorNone) + o.Separator.Set(options.ConstructArguments, SeparatorNone) + o.Separator.Set(options.ConstructThrows, SeparatorNone) return o }(), @@ -733,7 +734,7 @@ func TestFormatAlwaysBreak(t *testing.T) { name: "break structs forces multiline", opts: func() Options { o := testOpts(80) - o.Break.Set(ConstructStruct, true) + o.Break.Set(options.ConstructStruct, true) return o }(), @@ -744,7 +745,7 @@ func TestFormatAlwaysBreak(t *testing.T) { name: "break enums forces multiline", opts: func() Options { o := testOpts(80) - o.Break.Set(ConstructEnum, true) + o.Break.Set(options.ConstructEnum, true) return o }(), @@ -755,7 +756,7 @@ func TestFormatAlwaysBreak(t *testing.T) { name: "break structs keeps empty bodies flat", opts: func() Options { o := testOpts(80) - o.Break.Set(ConstructStruct, true) + o.Break.Set(options.ConstructStruct, true) return o }(), @@ -766,7 +767,7 @@ func TestFormatAlwaysBreak(t *testing.T) { name: "break structs does not affect enums", opts: func() Options { o := testOpts(80) - o.Break.Set(ConstructStruct, true) + o.Break.Set(options.ConstructStruct, true) return o }(), @@ -1261,10 +1262,10 @@ func TestFormatThrowsFoldsWithBrokenArgs(t *testing.T) { func TestFormatSeparatorsPerConstruct(t *testing.T) { opts := testOpts(80) - opts.Separator.Set(ConstructStruct, SeparatorSemicolon) - opts.Separator.Set(ConstructUnion, SeparatorSemicolon) - opts.Separator.Set(ConstructException, SeparatorSemicolon) - opts.Separator.Set(ConstructEnum, SeparatorComma) + opts.Separator.Set(options.ConstructStruct, SeparatorSemicolon) + opts.Separator.Set(options.ConstructUnion, SeparatorSemicolon) + opts.Separator.Set(options.ConstructException, SeparatorSemicolon) + opts.Separator.Set(options.ConstructEnum, SeparatorComma) src := "struct S {\n 1: i32 a\n 2: i32 b\n}\n\nunion U {\n 1: i32 a\n 2: i32 b\n}\n\nexception X {\n 1: i32 a\n 2: i32 b\n}\n\nenum E {\n A\n B\n}" want := "struct S { 1: i32 a; 2: i32 b }\n\nunion U { 1: i32 a; 2: i32 b }\n\nexception X { 1: i32 a; 2: i32 b }\n\nenum E { A, B }\n" @@ -1335,8 +1336,8 @@ func TestFormatConstsOptions(t *testing.T) { name: "lists forced comma with break", src: "const list a = [1, 2]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorComma) - o.Break.Set(ConstructList, true) + o.Separator.Set(options.ConstructList, SeparatorComma) + o.Break.Set(options.ConstructList, true) }), want: "const list a = [\n 1,\n 2,\n]\n", }, @@ -1344,7 +1345,7 @@ func TestFormatConstsOptions(t *testing.T) { name: "lists forced semicolon flat", src: "const list a = [1, 2]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorSemicolon) + o.Separator.Set(options.ConstructList, SeparatorSemicolon) }), want: "const list a = [1; 2; ]\n", }, @@ -1352,7 +1353,7 @@ func TestFormatConstsOptions(t *testing.T) { name: "lists none drops separators", src: "const list a = [1, 2]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorNone) + o.Separator.Set(options.ConstructList, SeparatorNone) }), want: "const list a = [1 2]\n", }, @@ -1360,8 +1361,8 @@ func TestFormatConstsOptions(t *testing.T) { name: "maps forced comma with break", src: "const map m = {\"a\": 1, \"b\": 2}", opts: opts(func(o *Options) { - o.Separator.Set(ConstructMap, SeparatorComma) - o.Break.Set(ConstructMap, true) + o.Separator.Set(options.ConstructMap, SeparatorComma) + o.Break.Set(options.ConstructMap, true) }), want: "const map m = {\n \"a\": 1,\n \"b\": 2,\n}\n", }, @@ -1371,8 +1372,8 @@ func TestFormatConstsOptions(t *testing.T) { name: "sets forced comma with break", src: "const set s = [1, 2]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructSet, SeparatorComma) - o.Break.Set(ConstructSet, true) + o.Separator.Set(options.ConstructSet, SeparatorComma) + o.Break.Set(options.ConstructSet, true) }), want: "const set s = [\n 1,\n 2,\n]\n", }, @@ -1380,7 +1381,7 @@ func TestFormatConstsOptions(t *testing.T) { name: "sets semicolon separators", src: "const set s = [1, 2]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructSet, SeparatorSemicolon) + o.Separator.Set(options.ConstructSet, SeparatorSemicolon) }), want: "const set s = [1; 2; ]\n", }, @@ -1388,8 +1389,8 @@ func TestFormatConstsOptions(t *testing.T) { name: "trailing separator never leaves a blank before the close", src: "const list a = [\n 1,\n 2,\n]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorComma) - o.Break.Set(ConstructList, true) + o.Separator.Set(options.ConstructList, SeparatorComma) + o.Break.Set(options.ConstructList, true) }), want: "const list a = [\n 1,\n 2,\n]\n", }, @@ -1397,8 +1398,8 @@ func TestFormatConstsOptions(t *testing.T) { name: "line comment after trailing separator owns its line end", src: "const list a = [1; #0\n]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorPreserve) - o.Break.Set(ConstructList, true) + o.Separator.Set(options.ConstructList, SeparatorPreserve) + o.Break.Set(options.ConstructList, true) }), want: "const list a = [\n 1; #0\n]\n", }, @@ -1406,7 +1407,7 @@ func TestFormatConstsOptions(t *testing.T) { name: "suppressed separator keeps the comment inline", src: "const list a = [1, // c\n2]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorNone) + o.Separator.Set(options.ConstructList, SeparatorNone) }), want: "const list a = [\n 1 // c\n 2\n]\n", }, @@ -1414,7 +1415,7 @@ func TestFormatConstsOptions(t *testing.T) { name: "nested containers stay flat with a comment at the item boundary", src: "const list a = [[0]#\n]", opts: opts(func(o *Options) { - o.Separator.Set(ConstructList, SeparatorNone) + o.Separator.Set(options.ConstructList, SeparatorNone) }), want: "const list a = [\n [0] #\n]\n", }, diff --git a/formatter/sepspace_test.go b/formatter/sepspace_test.go index 2ba124c..032cec2 100644 --- a/formatter/sepspace_test.go +++ b/formatter/sepspace_test.go @@ -3,6 +3,8 @@ package formatter import ( "strings" "testing" + + "github.com/karitham/thrift-ls/options" ) // sweepSeparatorSpace asserts the invariant behind the "space before @@ -222,10 +224,10 @@ func TestNoSpaceBeforeSeparator(t *testing.T) { for _, w := range widths { o := DefaultOptions() o.PrintWidth = w - o.Separator.Set(ConstructStruct, m.mode) - o.Separator.Set(ConstructEnum, m.mode) - o.Separator.Set(ConstructArguments, m.mode) - o.Separator.Set(ConstructThrows, m.mode) + o.Separator.Set(options.ConstructStruct, m.mode) + o.Separator.Set(options.ConstructEnum, m.mode) + o.Separator.Set(options.ConstructArguments, m.mode) + o.Separator.Set(options.ConstructThrows, m.mode) o.Align = a.align label := src.name + "/" + m.name + "/" + a.name diff --git a/formatter/value.go b/formatter/value.go index 7f82cec..2246e7e 100644 --- a/formatter/value.go +++ b/formatter/value.go @@ -2,6 +2,7 @@ package formatter import ( "github.com/karitham/thrift-ls/doc" + "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/syntax" ) @@ -14,18 +15,18 @@ func isListSep(kind syntax.TokenKind) bool { // containerConstruct returns the per-construct key of a declared container // type. Set values are written with the list literal syntax, so the // declared type is the only way to select the sets construct. -func containerConstruct(t *syntax.FieldType) Construct { +func containerConstruct(t *syntax.FieldType) options.Construct { if t == nil { - return ConstructList + return options.ConstructList } switch t.Kind { case syntax.TypeSet: - return ConstructSet + return options.ConstructSet case syntax.TypeMap: - return ConstructMap + return options.ConstructMap default: - return ConstructList + return options.ConstructList } } @@ -37,7 +38,7 @@ func containerConstruct(t *syntax.FieldType) Construct { // a token run, so comments inside the value are preserved. isLast reports // whether the value ends the enclosing declaration, in which case its // trailing trivia belongs to the declaration's trailing comments. -func (f *formatter) constValue(v *syntax.ConstValue, isLast bool, c Construct) doc.Doc { +func (f *formatter) constValue(v *syntax.ConstValue, isLast bool, c options.Construct) doc.Doc { if v == nil { return f.Concat() } @@ -56,10 +57,10 @@ func (f *formatter) constValue(v *syntax.ConstValue, isLast bool, c Construct) d // constList formats "[ items ]" as a foldable group honoring the c // construct's separator and break options. -func (f *formatter) constList(v *syntax.ConstValue, isLast bool, c Construct) doc.Doc { +func (f *formatter) constList(v *syntax.ConstValue, isLast bool, c options.Construct) doc.Doc { items := make([]constItem, len(v.List)) for i, item := range v.List { - items[i] = constItem{start: item.TokStart(), end: item.TokEnd(), doc: f.constValue(item, false, ConstructList)} + items[i] = constItem{start: item.TokStart(), end: item.TokEnd(), doc: f.constValue(item, false, options.ConstructList)} } return f.constItems(items, v.TokStart(), v.TokEnd(), c, isLast) @@ -77,7 +78,7 @@ func (f *formatter) constMap(v *syntax.ConstValue, isLast bool) doc.Doc { } } - return f.constItems(items, v.TokStart(), v.TokEnd(), ConstructMap, isLast) + return f.constItems(items, v.TokStart(), v.TokEnd(), options.ConstructMap, isLast) } // constItem is one list/map entry: its formatted doc and the token span of @@ -93,7 +94,7 @@ type constItem struct { // options. The separator between entries and the trailing separator follow // the mode; the closing bracket gets exactly one break, so a trailing // separator never leaves a blank line before it. -func (f *formatter) constItems(items []constItem, open, close int, c Construct, isLast bool) doc.Doc { +func (f *formatter) constItems(items []constItem, open, close int, c options.Construct, isLast bool) doc.Doc { sepMode := f.opts.Separator.Get(c) openOpts := emitOpts{trailing: true} diff --git a/lsp/server.go b/lsp/server.go index 60f3ecf..ae5dc4f 100644 --- a/lsp/server.go +++ b/lsp/server.go @@ -72,7 +72,7 @@ func NewServer(c *cache.Cache, client protocol.Client, opts Options) *Server { // setWorkspaceSettings stores the workspace settings overlay; invalid // settings are rejected and the previous document stays in effect. func (s *Server) setWorkspaceSettings(overlay options.Patch) { - if _, err := overlay.Formatter(); err != nil { + if err := overlay.Validate(); err != nil { logError("workspace settings rejected", err) return @@ -157,13 +157,13 @@ func (s *Server) formatOptions(view *cache.View) formatter.Options { overlay := s.workspaceOverlay s.optsMu.RUnlock() - fopts, err := overlay.Apply(view.Config()).Formatter() + fopts, err := formatter.FromConfig(overlay.Apply(view.Config())) if err != nil { // Both layers were validated when stored; this is unreachable // unless a view config was corrupted. logError("formatter options rejected", err) - fopts, _ = view.Config().Formatter() + fopts, _ = formatter.FromConfig(view.Config()) } return fopts diff --git a/main.go b/main.go index d2f8eb7..1275dab 100644 --- a/main.go +++ b/main.go @@ -113,17 +113,17 @@ func lspFlags() []cli.Flag { // constructFlags maps the per-construct format flag names to constructs. var constructFlags = []struct { name string - construct formatter.Construct + construct options.Construct }{ - {"struct", formatter.ConstructStruct}, - {"union", formatter.ConstructUnion}, - {"exception", formatter.ConstructException}, - {"enum", formatter.ConstructEnum}, - {"argument", formatter.ConstructArguments}, - {"throws", formatter.ConstructThrows}, - {"list", formatter.ConstructList}, - {"map", formatter.ConstructMap}, - {"set", formatter.ConstructSet}, + {"struct", options.ConstructStruct}, + {"union", options.ConstructUnion}, + {"exception", options.ConstructException}, + {"enum", options.ConstructEnum}, + {"argument", options.ConstructArguments}, + {"throws", options.ConstructThrows}, + {"list", options.ConstructList}, + {"map", options.ConstructMap}, + {"set", options.ConstructSet}, } // formatFlags are the flags of the format subcommand. @@ -195,7 +195,7 @@ func lspAction(ctx context.Context, cmd *cli.Command) error { // Validate early: a broken --config or working-directory config must // fail before serving; per-folder configs are re-resolved later. - if _, err := patch.Formatter(); err != nil { + if err := patch.Validate(); err != nil { return err } @@ -574,7 +574,7 @@ func formatFile(file string, w io.Writer, write, diffOut bool, configPath string patch := options.Effective(cfg) patch = cli.Apply(patch) - fopts, err := patch.Formatter() + fopts, err := formatter.FromConfig(patch) if err != nil { return err } diff --git a/options/construct.go b/options/construct.go new file mode 100644 index 0000000..f7f4986 --- /dev/null +++ b/options/construct.go @@ -0,0 +1,112 @@ +package options + +// Construct identifies one formatting construct that per-construct options +// apply to: the container bodies (structs, unions, exceptions, enums, +// arguments, throws) and the collection types (lists, maps, sets). +type Construct int + +const ( + ConstructStruct Construct = iota + ConstructUnion + ConstructException + ConstructEnum + ConstructArguments + ConstructThrows + ConstructList + ConstructMap + ConstructSet +) + +// PerConstruct holds one option value per construct. The JSON tags make the +// per-construct option maps config-compatible ("structs", "arguments", ...), +// so the options layer and the CLI share this single source of truth. +type PerConstruct[T any] struct { + Structs T `json:"structs"` + Unions T `json:"unions"` + Exceptions T `json:"exceptions"` + Enums T `json:"enums"` + Arguments T `json:"arguments"` + Throws T `json:"throws"` + Lists T `json:"lists"` + Maps T `json:"maps"` + Sets T `json:"sets"` +} + +// Get returns the value for the construct. +func (p PerConstruct[T]) Get(c Construct) T { + switch c { + case ConstructUnion: + return p.Unions + case ConstructException: + return p.Exceptions + case ConstructEnum: + return p.Enums + case ConstructArguments: + return p.Arguments + case ConstructThrows: + return p.Throws + case ConstructList: + return p.Lists + case ConstructMap: + return p.Maps + case ConstructSet: + return p.Sets + } + + return p.Structs +} + +// Set assigns the value for the construct. +func (p *PerConstruct[T]) Set(c Construct, v T) { + switch c { + case ConstructUnion: + p.Unions = v + case ConstructException: + p.Exceptions = v + case ConstructEnum: + p.Enums = v + case ConstructArguments: + p.Arguments = v + case ConstructThrows: + p.Throws = v + case ConstructList: + p.Lists = v + case ConstructMap: + p.Maps = v + case ConstructSet: + p.Sets = v + default: + p.Structs = v + } +} + +// AllConstructs lists every construct, in config order. +var AllConstructs = []Construct{ + ConstructStruct, ConstructUnion, ConstructException, + ConstructEnum, ConstructArguments, ConstructThrows, + ConstructList, ConstructMap, ConstructSet, +} + +// String returns the config key of the construct. +func (c Construct) String() string { + switch c { + case ConstructUnion: + return "unions" + case ConstructException: + return "exceptions" + case ConstructEnum: + return "enums" + case ConstructArguments: + return "arguments" + case ConstructThrows: + return "throws" + case ConstructList: + return "lists" + case ConstructMap: + return "maps" + case ConstructSet: + return "sets" + } + + return "structs" +} diff --git a/options/options.go b/options/options.go index 56e229b..25600d2 100644 --- a/options/options.go +++ b/options/options.go @@ -16,22 +16,19 @@ import ( "os" "path/filepath" "strings" - - "github.com/karitham/thrift-ls/formatter" ) // ConfigFileName is the JSON config file name. const ConfigFileName = "thrift-ls.json" // Separators configures trailing separators per construct. A nil value is -// unset. It is an alias of the formatter's per-construct container, so -// adding a construct adds the config key, CLI flags, and validation in one -// place. -type Separators = formatter.PerConstruct[*string] +// unset. It is an alias of the per-construct container, so adding a +// construct adds the config key, CLI flags, and validation in one place. +type Separators = PerConstruct[*string] // Break configures layouts that are forced multiline per construct. A nil // value is unset. -type Break = formatter.PerConstruct[*bool] +type Break = PerConstruct[*bool] // Patch is a partial set of options; nil fields are unset. type Patch struct { @@ -81,16 +78,16 @@ func (p Patch) Apply(base Patch) Patch { // overlayPerConstruct copies the set fields of src onto dst, creating dst // when it is nil. -func overlayPerConstruct[T *E, E any](dst, src *formatter.PerConstruct[T]) *formatter.PerConstruct[T] { +func overlayPerConstruct[T *E, E any](dst, src *PerConstruct[T]) *PerConstruct[T] { if src == nil { return dst } if dst == nil { - dst = &formatter.PerConstruct[T]{} + dst = &PerConstruct[T]{} } - for _, c := range formatter.AllConstructs { + for _, c := range AllConstructs { if v := src.Get(c); v != nil { dst.Set(c, v) } @@ -134,15 +131,15 @@ func (p Patch) Validate() error { } if p.Align != nil { - if _, ok := alignMode(*p.Align); !ok { + if !validAlign(*p.Align) { return fmt.Errorf("align must be one of \"field\", \"assign\", \"disable\", got %q", *p.Align) } } if p.Separators != nil { - for _, c := range formatter.AllConstructs { + for _, c := range AllConstructs { if v := p.Separators.Get(c); v != nil { - if _, ok := separatorMode(*v); !ok { + if !validSeparator(*v) { return fmt.Errorf("separators.%s must be one of \"comma\", \"semicolon\", \"none\", \"preserve\" (keep as written), got %q", c, *v) } } @@ -156,83 +153,24 @@ func (p Patch) Validate() error { return nil } -// Formatter converts the patch to formatter options, validating first. -func (p Patch) Formatter() (formatter.Options, error) { - if err := p.Validate(); err != nil { - return formatter.Options{}, err - } - - o := formatter.DefaultOptions() - if p.PrintWidth != nil { - o.PrintWidth = *p.PrintWidth - } - - if p.Indent != nil { - o.Indent = p.Indent.Value - o.TabWidth = p.Indent.Width - } - - if p.TabWidth != nil { - o.TabWidth = *p.TabWidth - } - - if p.Align != nil { - if mode, ok := alignMode(*p.Align); ok { - o.Align = mode - } - } - - if p.Separators != nil { - for _, c := range formatter.AllConstructs { - if v := p.Separators.Get(c); v != nil { - if mode, ok := separatorMode(*v); ok { - o.Separator.Set(c, mode) - } - } - } - } - - if p.Break != nil { - for _, c := range formatter.AllConstructs { - if v := p.Break.Get(c); v != nil { - o.Break.Set(c, *v) - } - } +// validAlign reports whether s is a known align config value. +func validAlign(s string) bool { + switch s { + case "field", "assign", "disable": + return true } - return o, nil + return false } -// alignMode maps a config value to a formatter align mode. The second -// result reports whether the value is a known align mode. -func alignMode(s string) (formatter.AlignMode, bool) { +// validSeparator reports whether s is a known separator config value. +func validSeparator(s string) bool { switch s { - case "field": - return formatter.AlignField, true - case "assign": - return formatter.AlignAssign, true - case "disable": - return formatter.AlignDisable, true - default: - return 0, false + case "comma", "semicolon", "none", "preserve": + return true } -} -// separatorMode maps a config value to a formatter separator mode. The -// second result reports whether the value is a known separator mode. -func separatorMode(s string) (formatter.SeparatorMode, bool) { - switch s { - case "comma": - return formatter.SeparatorComma, true - case "semicolon": - return formatter.SeparatorSemicolon, true - case "none": - return formatter.SeparatorNone, true - case "preserve": - return formatter.SeparatorPreserve, true - default: - return 0, false - } + return false } // Indent is a resolved indentation: the string emitted for one level and diff --git a/options/options_test.go b/options/options_test.go index 5a1a81a..67d5c88 100644 --- a/options/options_test.go +++ b/options/options_test.go @@ -5,8 +5,6 @@ import ( "os" "path/filepath" "testing" - - "github.com/karitham/thrift-ls/formatter" ) func TestParseIndentValue(t *testing.T) { @@ -117,37 +115,6 @@ func TestPatchValidate(t *testing.T) { } } -func TestPatchFormatter(t *testing.T) { - indent := Indent{Value: " ", Width: 2} - p := Patch{Indent: &indent, PrintWidth: new(100)} - - o, err := p.Formatter() - if err != nil { - t.Fatalf("Formatter: %v", err) - } - - if o.PrintWidth != 100 || o.Indent != " " || o.TabWidth != 2 { - t.Errorf("got %+v", o) - } - - if o.Align != formatter.AlignField || o.Separator.Get(formatter.ConstructStruct) != formatter.SeparatorPreserve { - t.Errorf("defaults wrong: %+v", o) - } - - comma := "comma" - align := "assign" - p = Patch{Separators: &Separators{Structs: &comma}, Align: &align} - - o, err = p.Formatter() - if err != nil { - t.Fatalf("Formatter: %v", err) - } - - if o.Separator.Get(formatter.ConstructStruct) != formatter.SeparatorComma || o.Align != formatter.AlignAssign { - t.Errorf("got %+v", o) - } -} - func TestFindConfig(t *testing.T) { dir := t.TempDir() @@ -259,15 +226,12 @@ func TestLoadRejectsUnknownOverrideKeys(t *testing.T) { // TestPatchSeparatorModes maps every config value to the formatter modes. func TestPatchSeparatorModes(t *testing.T) { tests := []struct { - value string - field formatter.SeparatorMode - function formatter.SeparatorMode + value string }{ - {"comma", formatter.SeparatorComma, formatter.SeparatorComma}, - {"none", formatter.SeparatorNone, formatter.SeparatorNone}, - {"semicolon", formatter.SeparatorSemicolon, formatter.SeparatorSemicolon}, - {"preserve", formatter.SeparatorPreserve, formatter.SeparatorPreserve}, - {"preserve", formatter.SeparatorPreserve, formatter.SeparatorPreserve}, + {"comma"}, + {"none"}, + {"semicolon"}, + {"preserve"}, } for _, tt := range tests { t.Run(tt.value, func(t *testing.T) { @@ -277,57 +241,21 @@ func TestPatchSeparatorModes(t *testing.T) { Lists: &tt.value, Maps: &tt.value, Sets: &tt.value, }} - o, err := p.Formatter() - if err != nil { - t.Fatalf("Formatter: %v", err) - } - - for _, c := range formatter.AllConstructs { - if o.Separator.Get(c) != tt.field { - t.Errorf("value %q: construct %s = %v, want %v", tt.value, c, o.Separator.Get(c), tt.field) - } + if err := p.Validate(); err != nil { + t.Fatalf("Validate: %v", err) } }) } - // The two options map independently. - semicolon, comma := "semicolon", "comma" - p := Patch{Separators: &Separators{Structs: &semicolon, Enums: &semicolon, Arguments: &comma, Throws: &comma}} - - o, err := p.Formatter() - if err != nil { - t.Fatalf("Formatter: %v", err) - } - - if o.Separator.Get(formatter.ConstructStruct) != formatter.SeparatorSemicolon || o.Separator.Get(formatter.ConstructArguments) != formatter.SeparatorComma { - t.Errorf("independent mapping failed: %+v", o) - } -} - -// TestPatchBreak maps the break group to the formatter options. -func TestPatchBreak(t *testing.T) { - trueVal, falseVal := true, false - - p := Patch{Break: &Break{Structs: &trueVal, Enums: &falseVal}} - - o, err := p.Formatter() - if err != nil { - t.Fatalf("Formatter: %v", err) - } - - if !o.Break.Get(formatter.ConstructStruct) || o.Break.Get(formatter.ConstructEnum) { - t.Errorf("break mapping wrong: %+v", o) - } - - // Zero patch keeps the defaults (no forced breaks). - o, err = (Patch{}).Formatter() - if err != nil { - t.Fatalf("Formatter: %v", err) + // Invalid values are rejected. + bogus := "bogus" + p := Patch{Align: &bogus} + if err := p.Validate(); err == nil { + t.Fatal("Validate accepted an unknown align value") } - for _, c := range formatter.AllConstructs { - if o.Break.Get(c) { - t.Errorf("breaks should default to false for %s: %+v", c, o) - } + p = Patch{Separators: &Separators{Structs: &bogus}} + if err := p.Validate(); err == nil { + t.Fatal("Validate accepted an unknown separator value") } }