Skip to content

Commit ee5afb8

Browse files
committed
internal/lsp: added a code action which generates key-value pairs of individual
fields and default values between a struct's enclosing braces Fixes #37576
1 parent ed308ab commit ee5afb8

15 files changed

+286
-0
lines changed

internal/lsp/cmd/test/fillstruct.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package cmdtest
2+
3+
import (
4+
"testing"
5+
6+
"golang.org/x/tools/internal/span"
7+
)
8+
9+
func (r *runner) FillStruct(t *testing.T, spn span.Span) {}

internal/lsp/code_action.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
143143
})
144144
}
145145
}
146+
fillActions, err := source.FillStruct(ctx, snapshot, fh, params.Range)
147+
if err != nil {
148+
return nil, err
149+
}
150+
codeActions = append(codeActions, fillActions...)
146151
default:
147152
// Unsupported file kind for a code action.
148153
return nil, nil

internal/lsp/lsp_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,44 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
369369
}
370370
}
371371

372+
func (r *runner) FillStruct(t *testing.T, spn span.Span) {
373+
uri := spn.URI()
374+
m, err := r.data.Mapper(uri)
375+
if err != nil {
376+
t.Fatal(err)
377+
}
378+
rng, err := m.Range(spn)
379+
if err != nil {
380+
t.Fatal(err)
381+
}
382+
actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{
383+
TextDocument: protocol.TextDocumentIdentifier{
384+
URI: protocol.URIFromSpanURI(uri),
385+
},
386+
Range: rng,
387+
})
388+
389+
if len(actions) == 0 {
390+
return
391+
}
392+
if actions[0].Kind != protocol.RefactorRewrite {
393+
t.Fatal("expected refactor rewrite kind")
394+
}
395+
res, err := applyWorkspaceEdits(r, actions[0].Edit)
396+
if err != nil {
397+
t.Fatal(err)
398+
}
399+
400+
for u, got := range res {
401+
fixed := string(r.data.Golden(tests.SpanName(spn), u.Filename(), func() ([]byte, error) {
402+
return []byte(got), nil
403+
}))
404+
if fixed != got {
405+
t.Errorf("fill struct failed for %s, expected:\n%#v\ngot:\n%#v", u.Filename(), fixed, got)
406+
}
407+
}
408+
}
409+
372410
func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {
373411
uri := spn.URI()
374412
view, err := r.server.session.ViewOf(uri)

internal/lsp/source/fill_struct.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package source
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"go/format"
11+
"go/types"
12+
"strings"
13+
14+
"golang.org/x/tools/go/ast/astutil"
15+
"golang.org/x/tools/internal/lsp/protocol"
16+
)
17+
18+
// FillStruct completes all of targeted struct's fields with their default values.
19+
func FillStruct(ctx context.Context, snapshot Snapshot, fh FileHandle, protoRng protocol.Range) ([]protocol.CodeAction, error) {
20+
21+
pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
22+
if err != nil {
23+
return nil, fmt.Errorf("getting file for struct fill code action: %v", err)
24+
}
25+
file, src, m, _, err := pgh.Cached()
26+
if err != nil {
27+
return nil, err
28+
}
29+
spn, err := m.PointSpan(protoRng.Start)
30+
if err != nil {
31+
return nil, err
32+
}
33+
spanRng, err := spn.Range(m.Converter)
34+
if err != nil {
35+
return nil, err
36+
}
37+
path, _ := astutil.PathEnclosingInterval(file, spanRng.Start, spanRng.End)
38+
if path == nil {
39+
return nil, nil
40+
}
41+
42+
ecl := enclosingCompositeLiteral(path, spanRng.Start, pkg.GetTypesInfo())
43+
if ecl == nil || !ecl.isStruct() {
44+
return nil, nil
45+
}
46+
47+
// If in F{ Bar<> : V} or anywhere in F{Bar : V, ...}
48+
// we should not fill the struct.
49+
if ecl.inKey || len(ecl.cl.Elts) != 0 {
50+
return nil, nil
51+
}
52+
53+
var codeActions []protocol.CodeAction
54+
qfFunc := qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
55+
switch obj := ecl.clType.(type) {
56+
case *types.Struct:
57+
fieldCount := obj.NumFields()
58+
if fieldCount == 0 {
59+
return nil, nil
60+
}
61+
var edit string
62+
for i := 0; i < fieldCount; i++ {
63+
field := obj.Field(i)
64+
// Ignore fields that are not accessible in the current package.
65+
if field.Pkg() != nil && field.Pkg() != pkg.GetTypes() && !field.Exported() {
66+
continue
67+
}
68+
69+
label := field.Name()
70+
value := formatZeroValue(field.Type(), qfFunc)
71+
text := "\n" + label + " : " + value + ","
72+
edit += text
73+
}
74+
edit += "\n"
75+
76+
// the range of all text between '<>', inclusive. E.g. {<> ... <}>
77+
mappedRange := newMappedRange(snapshot.View().Session().Cache().FileSet(), m, ecl.cl.Lbrace, ecl.cl.Rbrace+1)
78+
protoRange, err := mappedRange.Range()
79+
if err != nil {
80+
return nil, err
81+
}
82+
// consider formatting from the first character of the line the lbrace is on.
83+
// ToOffset is 1-based
84+
beginOffset, err := m.Converter.ToOffset(int(protoRange.Start.Line)+1, 1)
85+
if err != nil {
86+
return nil, err
87+
}
88+
89+
endOffset, err := m.Converter.ToOffset(int(protoRange.Start.Line)+1, int(protoRange.Start.Character)+1)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
// An increment to make sure the lbrace is included in the slice.
95+
endOffset++
96+
// Append the edits. Then append the closing brace.
97+
sourceCode := string(src[beginOffset:endOffset]) + edit + "}"
98+
99+
buf, err := format.Source([]byte(sourceCode))
100+
if err != nil {
101+
return nil, err
102+
}
103+
104+
// it is guranteed that a left brace exists.
105+
edit = string(buf[strings.IndexByte(string(buf), '{'):])
106+
107+
codeActions = append(codeActions, protocol.CodeAction{
108+
Title: "Fill struct",
109+
Kind: protocol.RefactorRewrite,
110+
Edit: protocol.WorkspaceEdit{
111+
DocumentChanges: []protocol.TextDocumentEdit{
112+
{
113+
TextDocument: protocol.VersionedTextDocumentIdentifier{
114+
Version: fh.Identity().Version,
115+
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
116+
URI: protocol.URIFromSpanURI(fh.Identity().URI),
117+
},
118+
},
119+
Edits: []protocol.TextEdit{
120+
{
121+
Range: protoRange,
122+
NewText: edit,
123+
},
124+
},
125+
},
126+
},
127+
},
128+
})
129+
}
130+
131+
return codeActions, nil
132+
}

internal/lsp/source/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func DefaultOptions() Options {
6969
protocol.SourceFixAll: true,
7070
protocol.SourceOrganizeImports: true,
7171
protocol.QuickFix: true,
72+
protocol.RefactorRewrite: true,
7273
},
7374
Mod: {
7475
protocol.SourceOrganizeImports: true,

internal/lsp/source/source_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ func (r *runner) Import(t *testing.T, spn span.Span) {
475475
}
476476

477477
func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []string) {}
478+
func (r *runner) FillStruct(t *testing.T, spn span.Span) {}
478479

479480
func (r *runner) Definition(t *testing.T, spn span.Span, d tests.Definition) {
480481
_, srcRng, err := spanToRange(r.data, d.Src)

internal/lsp/testdata/indirect/summary.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 0
2525
SignaturesCount = 0
2626
LinksCount = 0
2727
ImplementationsCount = 0
28+
FillStructCount = 0
2829

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package fillstruct
2+
3+
type StructA struct {
4+
unexportedIntField int
5+
ExportedIntField int
6+
}
7+
8+
func fill() {
9+
a := StructA{} //@fillstruct("}")
10+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- fill_struct_9_15 --
2+
package fillstruct
3+
4+
type StructA struct {
5+
unexportedIntField int
6+
ExportedIntField int
7+
}
8+
9+
func fill() {
10+
a := StructA{
11+
unexportedIntField: 0,
12+
ExportedIntField: 0,
13+
} //@fillstruct("}")
14+
}
15+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package fillstruct
2+
3+
type StructC struct {
4+
StructB
5+
}
6+
7+
func nested() {
8+
c := StructC{
9+
StructB: StructB{}, //@fillstruct("}")
10+
}
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- fill_struct_nested_9_20 --
2+
package fillstruct
3+
4+
type StructC struct {
5+
StructB
6+
}
7+
8+
func nested() {
9+
c := StructC{
10+
StructB: StructB{
11+
ExportedIntField: 0,
12+
}, //@fillstruct("}")
13+
}
14+
}
15+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package fillstruct
2+
3+
type StructB struct {
4+
ExportedIntField int
5+
}
6+
7+
func spaces() {
8+
b := StructB{
9+
10+
//@fillstruct("")
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- fill_struct_spaces_10_1 --
2+
package fillstruct
3+
4+
type StructB struct {
5+
ExportedIntField int
6+
}
7+
8+
func spaces() {
9+
b := StructB{
10+
ExportedIntField: 0,
11+
}
12+
}
13+

internal/lsp/testdata/lsp/summary.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ CaseSensitiveWorkspaceSymbolsCount = 2
2525
SignaturesCount = 32
2626
LinksCount = 8
2727
ImplementationsCount = 14
28+
FillStructCount = 4
2829

internal/lsp/tests/tests.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type FoldingRanges []span.Span
5757
type Formats []span.Span
5858
type Imports []span.Span
5959
type SuggestedFixes map[span.Span][]string
60+
type FillStructActions []span.Span
6061
type Definitions map[span.Span]Definition
6162
type Implementations map[span.Span][]span.Span
6263
type Highlights map[span.Span][]span.Span
@@ -87,6 +88,7 @@ type Data struct {
8788
Formats Formats
8889
Imports Imports
8990
SuggestedFixes SuggestedFixes
91+
FillStruct FillStructActions
9092
Definitions Definitions
9193
Implementations Implementations
9294
Highlights Highlights
@@ -140,6 +142,7 @@ type Tests interface {
140142
CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})
141143
SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp)
142144
Link(*testing.T, span.URI, []Link)
145+
FillStruct(*testing.T, span.Span)
143146
}
144147

145148
type Definition struct {
@@ -331,6 +334,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data {
331334
Filename: goldFile,
332335
Archive: archive,
333336
}
337+
fmt.Printf("Trimmed : %s, goldfile: %s, ArchiveCount : %d\n", trimmed, goldFile, len(archive.Files))
334338
} else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment {
335339
delete(m.Files, fragment)
336340
m.Files[trimmed] = operation
@@ -414,6 +418,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data {
414418
"signature": datum.collectSignatures,
415419
"link": datum.collectLinks,
416420
"suggestedfix": datum.collectSuggestedFixes,
421+
"fillstruct": datum.collectfillStruct,
417422
}); err != nil {
418423
t.Fatal(err)
419424
}
@@ -588,6 +593,17 @@ func Run(t *testing.T, tests Tests, data *Data) {
588593
}
589594
})
590595

596+
t.Run("FillStruct", func(t *testing.T) {
597+
t.Helper()
598+
599+
for _, spn := range data.FillStruct {
600+
t.Run(SpanName(spn), func(t *testing.T) {
601+
t.Helper()
602+
tests.FillStruct(t, spn)
603+
})
604+
}
605+
})
606+
591607
t.Run("SuggestedFix", func(t *testing.T) {
592608
t.Helper()
593609
for spn, actionKinds := range data.SuggestedFixes {
@@ -802,6 +818,7 @@ func checkData(t *testing.T, data *Data) {
802818
fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures))
803819
fmt.Fprintf(buf, "LinksCount = %v\n", linksCount)
804820
fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations))
821+
fmt.Fprintf(buf, "FillStructCount = %v\n", len(data.FillStruct))
805822

806823
want := string(data.Golden("summary", summaryFile, func() ([]byte, error) {
807824
return buf.Bytes(), nil
@@ -877,6 +894,7 @@ func (data *Data) Golden(tag string, target string, update func() ([]byte, error
877894
}
878895
file.Data = append(contents, '\n') // add trailing \n for txtar
879896
golden.Modified = true
897+
880898
}
881899
if file == nil {
882900
data.t.Fatalf("could not find golden contents %v: %v", fragment, tag)
@@ -1010,6 +1028,10 @@ func (data *Data) collectSuggestedFixes(spn span.Span, actionKind string) {
10101028
data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], actionKind)
10111029
}
10121030

1031+
func (data *Data) collectfillStruct(spn span.Span) {
1032+
data.FillStruct = append(data.FillStruct, spn)
1033+
}
1034+
10131035
func (data *Data) collectDefinitions(src, target span.Span) {
10141036
data.Definitions[src] = Definition{
10151037
Src: src,

0 commit comments

Comments
 (0)