Skip to content

Commit 4a6d376

Browse files
feat: dynamic formatting configuration (#47)
* added tests * minor style tweaks * adding https://github.com/mitchellh/mapstructure * switch from reflection -> mapstructure for decoding * neovim editor integration docs * wip * go mod tidy
1 parent 3bbe4f0 commit 4a6d376

File tree

8 files changed

+290
-2
lines changed

8 files changed

+290
-2
lines changed

editor/vim/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,32 @@ The LSP integration will depend on the vim plugin you're using
88
* `neoclide/coc.nvim`:
99
* Inside vim, run: `:CocConfig` (to edit `~/.vim/coc-settings.json`)
1010
* Copy [coc-settings.json](coc-settings.json) content
11+
* `neovim/nvim-lspconfig`:
12+
* Install jsonnet-language-server, either manually via `go install github.com/grafana/jsonnet-language-server@latest` or via
13+
[williamboman/mason.nvim](https://github.com/williamboman/mason.nvim)
14+
* Configure settings via [neovim/nvim-lspconfig](https://github.com/neovim/nvim-lspconfig)
15+
```lua
16+
require'lspconfig'.jsonnet_ls.setup{
17+
ext_vars = {
18+
foo = 'bar',
19+
},
20+
formatting = {
21+
-- default values
22+
Indent = 2,
23+
MaxBlankLines = 2,
24+
StringStyle = 'single',
25+
CommentStyle = 'slash',
26+
PrettyFieldNames = true,
27+
PadArrays = false,
28+
PadObjects = true,
29+
SortImports = true,
30+
UseImplicitPlus = true,
31+
StripEverything = false,
32+
StripComments = false,
33+
StripAllButComments = false,
34+
},
35+
}
36+
```
1137

1238
Some adjustments you may need to review for above example configs:
1339
* Both are preset to run `jsonnet-language-server -t`, i.e. with

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/grafana/tanka v0.19.0
99
github.com/hexops/gotextdiff v1.0.3
1010
github.com/jdbaldry/go-language-server-protocol v0.0.0-20211013214444-3022da0884b2
11+
github.com/mitchellh/mapstructure v1.5.0
1112
github.com/sirupsen/logrus v1.8.1
1213
github.com/stretchr/testify v1.7.0
1314
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
4747
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
4848
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
4949
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
50+
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
51+
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
5052
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
5153
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
5254
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=

pkg/server/configuration.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package server
33
import (
44
"context"
55
"fmt"
6+
"reflect"
67

78
"github.com/google/go-jsonnet"
9+
"github.com/google/go-jsonnet/formatter"
810
"github.com/jdbaldry/go-language-server-protocol/jsonrpc2"
911
"github.com/jdbaldry/go-language-server-protocol/lsp/protocol"
12+
"github.com/mitchellh/mapstructure"
1013
)
1114

1215
func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error {
@@ -24,6 +27,13 @@ func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.Di
2427
}
2528
s.extVars = newVars
2629

30+
case "formatting":
31+
newFmtOpts, err := s.parseFormattingOpts(sv)
32+
if err != nil {
33+
return fmt.Errorf("%w: formatting options parsing failed: %v", jsonrpc2.ErrInvalidParams, err)
34+
}
35+
s.fmtOpts = newFmtOpts
36+
2737
default:
2838
return fmt.Errorf("%w: unsupported settings key: %q", jsonrpc2.ErrInvalidParams, sk)
2939
}
@@ -48,9 +58,76 @@ func (s *server) parseExtVars(unparsed interface{}) (map[string]string, error) {
4858
return extVars, nil
4959
}
5060

61+
func (s *server) parseFormattingOpts(unparsed interface{}) (formatter.Options, error) {
62+
newOpts, ok := unparsed.(map[string]interface{})
63+
if !ok {
64+
return formatter.Options{}, fmt.Errorf("unsupported settings value for formatting. expected json object. got: %T", unparsed)
65+
}
66+
67+
opts := formatter.DefaultOptions()
68+
config := mapstructure.DecoderConfig{
69+
Result: &opts,
70+
DecodeHook: mapstructure.ComposeDecodeHookFunc(
71+
stringStyleDecodeFunc,
72+
commentStyleDecodeFunc,
73+
),
74+
}
75+
decoder, err := mapstructure.NewDecoder(&config)
76+
if err != nil {
77+
return formatter.Options{}, fmt.Errorf("decoder construction failed: %v", err)
78+
}
79+
80+
if err := decoder.Decode(newOpts); err != nil {
81+
return formatter.Options{}, fmt.Errorf("map decode failed: %v", err)
82+
}
83+
return opts, nil
84+
}
85+
5186
func resetExtVars(vm *jsonnet.VM, vars map[string]string) {
5287
vm.ExtReset()
5388
for vk, vv := range vars {
5489
vm.ExtVar(vk, vv)
5590
}
5691
}
92+
93+
func stringStyleDecodeFunc(from, to reflect.Type, unparsed interface{}) (interface{}, error) {
94+
if to != reflect.TypeOf(formatter.StringStyleDouble) {
95+
return unparsed, nil
96+
}
97+
if from.Kind() != reflect.String {
98+
return nil, fmt.Errorf("expected string, got: %v", from.Kind())
99+
}
100+
101+
// will not panic because of the kind == string check above
102+
switch str := unparsed.(string); str {
103+
case "double":
104+
return formatter.StringStyleDouble, nil
105+
case "single":
106+
return formatter.StringStyleSingle, nil
107+
case "leave":
108+
return formatter.StringStyleLeave, nil
109+
default:
110+
return nil, fmt.Errorf("expected one of 'double', 'single', 'leave', got: %q", str)
111+
}
112+
}
113+
114+
func commentStyleDecodeFunc(from, to reflect.Type, unparsed interface{}) (interface{}, error) {
115+
if to != reflect.TypeOf(formatter.CommentStyleHash) {
116+
return unparsed, nil
117+
}
118+
if from.Kind() != reflect.String {
119+
return nil, fmt.Errorf("expected string, got: %v", from.Kind())
120+
}
121+
122+
// will not panic because of the kind == string check above
123+
switch str := unparsed.(string); str {
124+
case "hash":
125+
return formatter.CommentStyleHash, nil
126+
case "slash":
127+
return formatter.CommentStyleSlash, nil
128+
case "leave":
129+
return formatter.CommentStyleLeave, nil
130+
default:
131+
return nil, fmt.Errorf("expected one of 'hash', 'slash', 'leave', got: %q", str)
132+
}
133+
}

pkg/server/configuration_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"testing"
77

8+
"github.com/google/go-jsonnet/formatter"
89
"github.com/jdbaldry/go-language-server-protocol/lsp/protocol"
910
"github.com/stretchr/testify/assert"
1011
)
@@ -117,3 +118,97 @@ func TestConfiguration(t *testing.T) {
117118
})
118119
}
119120
}
121+
122+
func TestConfiguration_Formatting(t *testing.T) {
123+
type kase struct {
124+
name string
125+
settings interface{}
126+
expectedOptions formatter.Options
127+
expectedErr error
128+
}
129+
130+
testCases := []kase{
131+
{
132+
name: "formatting opts",
133+
settings: map[string]interface{}{
134+
"formatting": map[string]interface{}{
135+
"Indent": 4,
136+
"MaxBlankLines": 10,
137+
"StringStyle": "single",
138+
"CommentStyle": "leave",
139+
"PrettyFieldNames": true,
140+
"PadArrays": false,
141+
"PadObjects": true,
142+
"SortImports": false,
143+
"UseImplicitPlus": true,
144+
"StripEverything": false,
145+
"StripComments": false,
146+
// not setting StripAllButComments
147+
},
148+
},
149+
expectedOptions: func() formatter.Options {
150+
opts := formatter.DefaultOptions()
151+
opts.Indent = 4
152+
opts.MaxBlankLines = 10
153+
opts.StringStyle = formatter.StringStyleSingle
154+
opts.CommentStyle = formatter.CommentStyleLeave
155+
opts.PrettyFieldNames = true
156+
opts.PadArrays = false
157+
opts.PadObjects = true
158+
opts.SortImports = false
159+
opts.UseImplicitPlus = true
160+
opts.StripEverything = false
161+
opts.StripComments = false
162+
return opts
163+
}(),
164+
},
165+
{
166+
name: "invalid string style",
167+
settings: map[string]interface{}{
168+
"formatting": map[string]interface{}{
169+
"StringStyle": "invalid",
170+
},
171+
},
172+
expectedErr: errors.New("JSON RPC invalid params: formatting options parsing failed: map decode failed: 1 error(s) decoding:\n\n* error decoding 'StringStyle': expected one of 'double', 'single', 'leave', got: \"invalid\""),
173+
},
174+
{
175+
name: "invalid comment style",
176+
settings: map[string]interface{}{
177+
"formatting": map[string]interface{}{
178+
"CommentStyle": "invalid",
179+
},
180+
},
181+
expectedErr: errors.New("JSON RPC invalid params: formatting options parsing failed: map decode failed: 1 error(s) decoding:\n\n* error decoding 'CommentStyle': expected one of 'hash', 'slash', 'leave', got: \"invalid\""),
182+
},
183+
{
184+
name: "does not override default values",
185+
settings: map[string]interface{}{
186+
"formatting": map[string]interface{}{},
187+
},
188+
expectedOptions: formatter.DefaultOptions(),
189+
},
190+
}
191+
192+
for _, tc := range testCases {
193+
t.Run(tc.name, func(t *testing.T) {
194+
s, _ := testServerWithFile(t, nil, "")
195+
196+
err := s.DidChangeConfiguration(
197+
context.TODO(),
198+
&protocol.DidChangeConfigurationParams{
199+
Settings: tc.settings,
200+
},
201+
)
202+
if tc.expectedErr == nil && err != nil {
203+
t.Fatalf("DidChangeConfiguration produced unexpected error: %v", err)
204+
} else if tc.expectedErr != nil && err == nil {
205+
t.Fatalf("expected DidChangeConfiguration to produce error but it did not")
206+
} else if tc.expectedErr != nil && err != nil {
207+
assert.EqualError(t, err, tc.expectedErr.Error())
208+
return
209+
}
210+
211+
assert.Equal(t, tc.expectedOptions, s.fmtOpts)
212+
})
213+
}
214+
}

pkg/server/formatting.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormat
1717
return nil, utils.LogErrorf("Formatting: %s: %w", errorRetrievingDocument, err)
1818
}
1919

20-
// TODO(#14): Formatting options should be user configurable.
21-
formatted, err := formatter.Format(params.TextDocument.URI.SpanURI().Filename(), doc.item.Text, formatter.DefaultOptions())
20+
formatted, err := formatter.Format(params.TextDocument.URI.SpanURI().Filename(), doc.item.Text, s.fmtOpts)
2221
if err != nil {
2322
log.Errorf("error formatting document: %v", err)
2423
return nil, nil

pkg/server/formatting_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package server
22

33
import (
4+
"context"
5+
"fmt"
46
"testing"
57

68
"github.com/jdbaldry/go-language-server-protocol/lsp/protocol"
79
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
811
)
912

1013
func TestGetTextEdits(t *testing.T) {
@@ -71,3 +74,85 @@ func TestGetTextEdits(t *testing.T) {
7174
})
7275
}
7376
}
77+
78+
func TestFormatting(t *testing.T) {
79+
type kase struct {
80+
name string
81+
settings interface{}
82+
fileContent string
83+
84+
expected []protocol.TextEdit
85+
}
86+
testCases := []kase{
87+
{
88+
name: "default settings",
89+
settings: nil,
90+
fileContent: "{foo: 'bar'}",
91+
expected: []protocol.TextEdit{
92+
{Range: makeRange(t, "0:0-1:0"), NewText: ""},
93+
{Range: makeRange(t, "1:0-1:0"), NewText: "{ foo: 'bar' }\n"},
94+
},
95+
},
96+
{
97+
name: "new lines with indentation",
98+
settings: map[string]interface{}{
99+
"formatting": map[string]interface{}{"Indent": 4},
100+
},
101+
fileContent: `
102+
{
103+
foo: 'bar',
104+
}`,
105+
expected: []protocol.TextEdit{
106+
{Range: makeRange(t, "0:0-1:0"), NewText: ""},
107+
{Range: makeRange(t, "2:0-3:0"), NewText: ""},
108+
{Range: makeRange(t, "3:0-4:0"), NewText: ""},
109+
{Range: makeRange(t, "4:0-4:0"), NewText: " foo: 'bar',\n"},
110+
{Range: makeRange(t, "4:0-4:0"), NewText: "}\n"},
111+
},
112+
},
113+
}
114+
115+
for _, tc := range testCases {
116+
t.Run(tc.name, func(t *testing.T) {
117+
s, fileURI := testServerWithFile(t, nil, tc.fileContent)
118+
119+
if tc.settings != nil {
120+
err := s.DidChangeConfiguration(
121+
context.TODO(),
122+
&protocol.DidChangeConfigurationParams{
123+
Settings: tc.settings,
124+
},
125+
)
126+
require.NoError(t, err, "expected settings to not return an error")
127+
}
128+
129+
edits, err := s.Formatting(context.TODO(), &protocol.DocumentFormattingParams{
130+
TextDocument: protocol.TextDocumentIdentifier{
131+
URI: fileURI,
132+
},
133+
})
134+
require.NoError(t, err, "expected Formatting to not return an error")
135+
assert.Equal(t, tc.expected, edits)
136+
})
137+
}
138+
}
139+
140+
// makeRange parses rangeStr of the form
141+
// <start-line>:<start-col>-<end-line>:<end-col> into a valid protocol.Range
142+
func makeRange(t *testing.T, rangeStr string) protocol.Range {
143+
ret := protocol.Range{
144+
Start: protocol.Position{Line: 0, Character: 0},
145+
End: protocol.Position{Line: 0, Character: 0},
146+
}
147+
n, err := fmt.Sscanf(
148+
rangeStr,
149+
"%d:%d-%d:%d",
150+
&ret.Start.Line,
151+
&ret.Start.Character,
152+
&ret.End.Line,
153+
&ret.End.Character,
154+
)
155+
require.NoError(t, err)
156+
require.Equal(t, 4, n)
157+
return ret
158+
}

pkg/server/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"path/filepath"
2222

2323
"github.com/google/go-jsonnet"
24+
"github.com/google/go-jsonnet/formatter"
2425
"github.com/grafana/jsonnet-language-server/pkg/stdlib"
2526
"github.com/grafana/jsonnet-language-server/pkg/utils"
2627
tankaJsonnet "github.com/grafana/tanka/pkg/jsonnet"
@@ -40,6 +41,7 @@ func NewServer(name, version string, client protocol.ClientCloser) *server {
4041
version: version,
4142
cache: newCache(),
4243
client: client,
44+
fmtOpts: formatter.DefaultOptions(),
4345
}
4446

4547
return server
@@ -54,6 +56,7 @@ type server struct {
5456
client protocol.ClientCloser
5557
getVM func(path string) (*jsonnet.VM, error)
5658
extVars map[string]string
59+
fmtOpts formatter.Options
5760

5861
// Feature flags
5962
EvalDiags bool

0 commit comments

Comments
 (0)