Skip to content

Commit f84d949

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 44a64ad commit f84d949

File tree

5 files changed

+179
-1
lines changed

5 files changed

+179
-1
lines changed

internal/lsp/code_action.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
134134
})
135135
}
136136
}
137+
codeActions, err = source.StructFill(ctx, params, snapshot, fh, codeActions)
137138
default:
138139
// Unsupported file kind for a code action.
139140
return nil, nil

internal/lsp/source/completion.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ func (c *completer) getSurrounding() *Selection {
295295
func (c *completer) found(cand candidate) {
296296
obj := cand.obj
297297

298-
if obj.Pkg() != nil && obj.Pkg() != c.pkg.GetTypes() && !obj.Exported() {
298+
if !IsObjectExported(obj, c.pkg) {
299299
// obj is not accessible because it lives in another package and is not
300300
// exported. Don't treat it as a completion candidate.
301301
return
@@ -728,6 +728,15 @@ func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) {
728728
}
729729
}
730730

731+
func (c *completer) wantFillStructCompletions() bool {
732+
clInfo := c.enclosingCompositeLiteral
733+
if clInfo == nil {
734+
return false
735+
}
736+
737+
return clInfo.isStruct() && !(clInfo.inKey || clInfo.maybeInFieldName)
738+
}
739+
731740
func (c *completer) wantStructFieldCompletions() bool {
732741
clInfo := c.enclosingCompositeLiteral
733742
if clInfo == nil {
@@ -1162,6 +1171,30 @@ func (c *completer) inConstDecl() bool {
11621171
return false
11631172
}
11641173

1174+
func generateCompletionForStructFields(snip *snippet.Builder, field *types.Var, qualifer types.Qualifier, placeholder bool) (*snippet.Builder, string) {
1175+
var (
1176+
label = field.Name()
1177+
detail = typeDefaultValues(field, field.Type(), qualifer)
1178+
text = "\n" + label + " : " + detail + ","
1179+
)
1180+
1181+
// A plain snippet turns "Foo{Ba<>" into "Foo{Bar: <>".
1182+
snip.WriteText("\n" + label + ": ")
1183+
1184+
// Don't write placeholders if it is not offered.
1185+
if placeholder {
1186+
snip.WritePlaceholder(func(b *snippet.Builder) {
1187+
// A placeholder snippet turns "Foo{Ba<>" into "Foo{Bar: <*int*>".
1188+
b.WriteText(detail)
1189+
})
1190+
} else {
1191+
snip.WriteText(detail)
1192+
}
1193+
1194+
snip.WriteText(",")
1195+
return snip, text
1196+
}
1197+
11651198
// structLiteralFieldName finds completions for struct field names inside a struct literal.
11661199
func (c *completer) structLiteralFieldName() error {
11671200
clInfo := c.enclosingCompositeLiteral

internal/lsp/source/fill_struct.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2019 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/types"
11+
12+
"golang.org/x/tools/go/ast/astutil"
13+
"golang.org/x/tools/internal/lsp/protocol"
14+
"golang.org/x/tools/internal/lsp/snippet"
15+
)
16+
17+
func typeDefaultValues(field *types.Var, fieldType types.Type, qff types.Qualifier) string {
18+
var edit string
19+
switch typ := fieldType.(type) {
20+
case *types.Basic:
21+
switch typ.Info() {
22+
case types.IsInteger, types.IsUnsigned:
23+
edit = "0"
24+
case types.IsBoolean:
25+
edit = "false"
26+
case types.IsFloat:
27+
edit = "0.0"
28+
case types.IsString:
29+
edit = `""`
30+
}
31+
case *types.Named:
32+
switch underlying := typ.Underlying().(type) {
33+
34+
// To prevent such cases where
35+
// type NameA struct{}
36+
// type NameB NameA
37+
// type NameC NameB
38+
// we just return the field's typename.
39+
case *types.Struct:
40+
edit = types.TypeString(field.Type(), qff) + "{}"
41+
default:
42+
// get the base type of a named type recursively.
43+
edit = typeDefaultValues(field, underlying, qff)
44+
}
45+
default:
46+
edit = "nil"
47+
}
48+
return edit
49+
}
50+
51+
func StructFill(ctx context.Context, params *protocol.CodeActionParams, snapshot Snapshot, fh FileHandle, codeActions []protocol.CodeAction) ([]protocol.CodeAction, error) {
52+
rangeOfTarget := params.Range
53+
54+
pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestPackageHandle)
55+
if err != nil {
56+
return codeActions, fmt.Errorf("getting file for Completion: %v", err)
57+
}
58+
file, _, m, _, err := pgh.Cached()
59+
if err != nil {
60+
return codeActions, err
61+
}
62+
spn, err := m.PointSpan(rangeOfTarget.Start)
63+
if err != nil {
64+
return codeActions, err
65+
}
66+
rng, err := spn.Range(m.Converter)
67+
if err != nil {
68+
return codeActions, err
69+
}
70+
path, _ := astutil.PathEnclosingInterval(file, rng.Start, rng.End)
71+
if path == nil {
72+
return codeActions, nil
73+
}
74+
75+
ecl := enclosingCompositeLiteral(path, rng.Start, pkg.GetTypesInfo())
76+
if ecl == nil || !ecl.isStruct() {
77+
return codeActions, nil
78+
}
79+
80+
// If in F{ Bar<> : V} or anywhere in F{Bar : V, ...}
81+
// we should not fill the struct.
82+
if ecl.inKey || len(ecl.cl.Elts) != 0 {
83+
return codeActions, nil
84+
}
85+
86+
// the range to replace all text between enclosing braces {<> ... <>}
87+
newMappedRange := newMappedRange(snapshot.View().Session().Cache().FileSet(), m, ecl.cl.Lbrace+1, ecl.cl.Rbrace)
88+
newProtoRange, err := newMappedRange.Range()
89+
if err != nil {
90+
return codeActions, err
91+
}
92+
qfFunc := qualifier(file, pkg.GetTypes(), pkg.GetTypesInfo())
93+
switch obj := ecl.clType.(type) {
94+
case *types.Struct:
95+
96+
fieldCount := obj.NumFields()
97+
if fieldCount == 0 {
98+
return codeActions, nil
99+
}
100+
var edit, insert string
101+
for i := 0; i < fieldCount; i++ {
102+
field := obj.Field(i)
103+
if !IsObjectExported(field, pkg) {
104+
continue
105+
}
106+
107+
// Code action does not support placeholders (?) yet
108+
_, insert = generateCompletionForStructFields(&snippet.Builder{}, field, qfFunc, false)
109+
edit += insert
110+
}
111+
edit += "\n"
112+
codeActions = append(codeActions, protocol.CodeAction{
113+
Title: "Fill struct",
114+
Kind: protocol.RefactorRewrite,
115+
Edit: protocol.WorkspaceEdit{
116+
DocumentChanges: []protocol.TextDocumentEdit{
117+
{
118+
TextDocument: protocol.VersionedTextDocumentIdentifier{
119+
Version: fh.Identity().Version,
120+
TextDocumentIdentifier: protocol.TextDocumentIdentifier{
121+
URI: protocol.URIFromSpanURI(fh.Identity().URI),
122+
},
123+
},
124+
Edits: []protocol.TextEdit{
125+
{
126+
Range: newProtoRange,
127+
NewText: edit,
128+
},
129+
},
130+
},
131+
},
132+
},
133+
})
134+
}
135+
136+
return codeActions, nil
137+
}

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/util.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,3 +774,9 @@ func formatZeroValue(T types.Type, qf types.Qualifier) string {
774774
return types.TypeString(T, qf) + "{}"
775775
}
776776
}
777+
778+
// IsObjectExported checks if an obj is accessible, or not because it lives in another package and is not
779+
// exported.
780+
func IsObjectExported(obj types.Object, targetPkg Package) bool {
781+
return obj.Pkg() == nil || obj.Pkg() == targetPkg.GetTypes() || obj.Exported()
782+
}

0 commit comments

Comments
 (0)