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")
}
}