Skip to content

Commit 8dcb013

Browse files
committed
Implement split/group code action
1 parent 9164f2a commit 8dcb013

File tree

7 files changed

+608
-4
lines changed

7 files changed

+608
-4
lines changed

gopls/internal/lsp/source/fix.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ var fixers = map[settings.Fix]Fixer{
5151
settings.InvertIfCondition: singleFile(invertIfCondition),
5252
settings.StubMethods: stubMethodsFixer,
5353
settings.UndeclaredName: singleFile(undeclaredname.SuggestedFix),
54+
settings.SplitLines: singleFile(splitLines),
55+
settings.GroupLines: singleFile(groupLines),
5456
}
5557

5658
// A singleFileFixer is a Fixer that inspects only a single file,

gopls/internal/lsp/source/lines.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package source
2+
3+
import (
4+
"bytes"
5+
"go/ast"
6+
"go/token"
7+
"go/types"
8+
"strings"
9+
"unicode"
10+
11+
"golang.org/x/tools/go/analysis"
12+
"golang.org/x/tools/go/ast/astutil"
13+
"golang.org/x/tools/gopls/internal/util/safetoken"
14+
)
15+
16+
func CanSplitLines(
17+
file *ast.File,
18+
fset *token.FileSet,
19+
start, end token.Pos,
20+
) (string, bool, error) {
21+
msg, lines, numElts, target := findSplitGroupTarget(file, fset, start, end)
22+
if target == nil {
23+
return "", false, nil
24+
}
25+
26+
// minus two to discount the parens/brackets.
27+
if lines-2 == numElts {
28+
return "", false, nil
29+
}
30+
31+
return "Split " + msg + " into separate lines", true, nil
32+
}
33+
34+
func CanGroupLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) {
35+
msg, lines, _, target := findSplitGroupTarget(file, fset, start, end)
36+
if target == nil {
37+
return "", false, nil
38+
}
39+
40+
if lines == 1 {
41+
return "", false, nil
42+
}
43+
44+
return "Group " + msg + " into one line", true, nil
45+
}
46+
47+
func splitLines(
48+
fset *token.FileSet,
49+
start token.Pos,
50+
end token.Pos,
51+
src []byte,
52+
file *ast.File,
53+
_ *types.Package,
54+
_ *types.Info,
55+
) (*token.FileSet, *analysis.SuggestedFix, error) {
56+
_, _, _, target := findSplitGroupTarget(file, fset, start, end)
57+
58+
// get the original line indent of target.
59+
split := bytes.Split(src, []byte("\n"))
60+
targetLineNumber := safetoken.StartPosition(fset, target.Pos()).Line
61+
firstLine := string(split[targetLineNumber-1])
62+
trimmed := strings.TrimFunc(string(firstLine), unicode.IsSpace)
63+
64+
firstLineIndent := firstLine[:strings.Index(firstLine, trimmed)]
65+
eltIndent := firstLineIndent + "\t"
66+
67+
return fset, processLines(fset, target, src, file, ",\n", "\n", ",\n"+firstLineIndent, eltIndent), nil
68+
}
69+
70+
func groupLines(
71+
fset *token.FileSet,
72+
start, end token.Pos,
73+
src []byte,
74+
file *ast.File,
75+
_ *types.Package,
76+
_ *types.Info,
77+
) (*token.FileSet, *analysis.SuggestedFix, error) {
78+
_, _, _, target := findSplitGroupTarget(file, fset, start, end)
79+
return fset, processLines(fset, target, src, file, ", ", "", "", ""), nil
80+
}
81+
82+
func processLines(fset *token.FileSet, target ast.Node, src []byte, file *ast.File, sep,
83+
prefix,
84+
suffix, indent string,
85+
) *analysis.SuggestedFix {
86+
var replPos, replEnd token.Pos
87+
var lines []string
88+
89+
switch node := target.(type) {
90+
case *ast.FieldList:
91+
replPos, replEnd = node.Opening+1, node.Closing
92+
93+
for _, field := range node.List {
94+
pos := safetoken.StartPosition(fset, field.Pos())
95+
end := safetoken.EndPosition(fset, field.End())
96+
lines = append(lines, indent+string(src[pos.Offset:end.Offset]))
97+
}
98+
case *ast.CallExpr:
99+
replPos, replEnd = node.Lparen+1, node.Rparen
100+
101+
for _, arg := range node.Args {
102+
pos := safetoken.StartPosition(fset, arg.Pos())
103+
end := safetoken.EndPosition(fset, arg.End())
104+
lines = append(lines, indent+string(src[pos.Offset:end.Offset]))
105+
}
106+
case *ast.CompositeLit:
107+
replPos, replEnd = node.Lbrace+1, node.Rbrace
108+
109+
for _, arg := range node.Elts {
110+
pos := safetoken.StartPosition(fset, arg.Pos())
111+
end := safetoken.EndPosition(fset, arg.End())
112+
lines = append(lines, indent+string(src[pos.Offset:end.Offset]))
113+
}
114+
}
115+
116+
return &analysis.SuggestedFix{
117+
TextEdits: []analysis.TextEdit{{
118+
Pos: replPos,
119+
End: replEnd,
120+
NewText: []byte(prefix + strings.Join(lines, sep) + suffix),
121+
}},
122+
}
123+
}
124+
125+
func findSplitGroupTarget(
126+
file *ast.File,
127+
fset *token.FileSet,
128+
start, end token.Pos,
129+
) (targetName string, numLines int, targetElts int, target ast.Node) {
130+
isValidTarget := func(opening token.Pos, closing token.Pos, numElts int) bool {
131+
// current cursor is inside the parens/bracket
132+
isInside := opening < start && end < closing
133+
134+
// todo: retain /*-style comments.
135+
// it doesnt have //-style comment in between
136+
// we prefer not to do anything here because it will break the code once it got grouped.
137+
for _, comment := range file.Comments {
138+
if comment.End() < start {
139+
continue
140+
}
141+
142+
if closing < comment.Pos() {
143+
break
144+
}
145+
146+
for _, c := range comment.List {
147+
if strings.HasPrefix(c.Text, "//") {
148+
return false
149+
}
150+
}
151+
}
152+
153+
// and it has more than 1 element
154+
return isInside && numElts > 1
155+
}
156+
157+
// find the closest enclosing parens/bracket from the cursor.
158+
path, _ := astutil.PathEnclosingInterval(file, start, end)
159+
for _, p := range path {
160+
switch node := p.(type) {
161+
// Case 1: target struct method declarations.
162+
// function (...) someMethod(a int, b int, c int) (d int, e, int) {}
163+
case *ast.FuncDecl:
164+
fl := node.Type.Params
165+
if isValidTarget(fl.Opening, fl.Closing, len(fl.List)) {
166+
return "parameters", countLines(fset, fl.Opening, fl.Closing), len(fl.List), fl
167+
}
168+
169+
fl = node.Type.Results
170+
if fl != nil && isValidTarget(fl.Opening, fl.Closing, len(fl.List)) {
171+
return "return values", countLines(fset, fl.Opening, fl.Closing), len(fl.List), fl
172+
}
173+
174+
// Case 2: target function signature args and result.
175+
// type someFunc func (a int, b int, c int) (d int, e int)
176+
case *ast.FuncType:
177+
fl := node.Params
178+
if isValidTarget(fl.Opening, fl.Closing, len(fl.List)) {
179+
return "parameters", countLines(fset, fl.Opening, fl.Closing), len(fl.List), fl
180+
}
181+
182+
fl = node.Results
183+
if fl != nil && isValidTarget(fl.Opening, fl.Closing, len(fl.List)) {
184+
return "return values", countLines(fset, fl.Opening, fl.Closing), len(fl.List), fl
185+
}
186+
187+
// Case 3: target function calls.
188+
// someFunction(a, b, c)
189+
case *ast.CallExpr:
190+
if isValidTarget(node.Lparen, node.Rparen, len(node.Args)) {
191+
return "parameters", countLines(fset, node.Lparen, node.Rparen), len(node.Args), node
192+
}
193+
194+
// Case 4: target composite lit instantiation (structs, maps, arrays).
195+
// A{b: 1, c: 2, d: 3}
196+
case *ast.CompositeLit:
197+
if isValidTarget(node.Lbrace, node.Rbrace, len(node.Elts)) {
198+
return "elements", countLines(fset, node.Lbrace, node.Rbrace), len(node.Elts), node
199+
}
200+
}
201+
}
202+
203+
return "", 0, 0, nil
204+
}
205+
206+
func countLines(fset *token.FileSet, start, end token.Pos) int {
207+
startPos := safetoken.StartPosition(fset, start)
208+
endPos := safetoken.EndPosition(fset, end)
209+
return endPos.Line - startPos.Line + 1
210+
}

gopls/internal/server/code_action.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,30 @@ func refactorRewrite(snapshot *cache.Snapshot, pkg *cache.Package, pgf *source.P
475475
commands = append(commands, cmd)
476476
}
477477

478+
if msg, ok, _ := source.CanSplitLines(pgf.File, pkg.FileSet(), start, end); ok {
479+
cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{
480+
URI: pgf.URI,
481+
Fix: string(settings.SplitLines),
482+
Range: rng,
483+
})
484+
if err != nil {
485+
return nil, err
486+
}
487+
commands = append(commands, cmd)
488+
}
489+
490+
if msg, ok, _ := source.CanGroupLines(pgf.File, pkg.FileSet(), start, end); ok {
491+
cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{
492+
URI: pgf.URI,
493+
Fix: string(settings.GroupLines),
494+
Range: rng,
495+
})
496+
if err != nil {
497+
return nil, err
498+
}
499+
commands = append(commands, cmd)
500+
}
501+
478502
// N.B.: an inspector only pays for itself after ~5 passes, which means we're
479503
// currently not getting a good deal on this inspection.
480504
//

gopls/internal/settings/analyzer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const (
2323
InlineCall Fix = "inline_call"
2424
InvertIfCondition Fix = "invert_if_condition"
2525
AddEmbedImport Fix = "add_embed_import"
26+
SplitLines Fix = "split_lines"
27+
GroupLines Fix = "group_lines"
2628
)
2729

2830
// Analyzer augments a go/analysis analyzer with additional LSP configuration.

0 commit comments

Comments
 (0)