|
| 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 | +} |
0 commit comments