Skip to content

Commit e7bd227

Browse files
ShoshinNikitagopherbot
authored andcommitted
gopls/internal/golang: fix folding range for function calls
Make folding ranges for function calls consistent with folding ranges for function bodies and composite literals: - keep the closing parenthesis visible - do not generate a folding range if either the first or last argument is on the same line as the opening or closing parenthesis In order to achieve this, calculate folding ranges by analyzing whitespace in the source instead of analyzing AST. Fixes golang/go#70467 Change-Id: I92924970c452e7c64efe4c19ad67560d83c2bd86 GitHub-Last-Rev: 4e50b03 GitHub-Pull-Request: #546 Reviewed-on: https://go-review.googlesource.com/c/tools/+/630056 Reviewed-by: Robert Findley <[email protected]> Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Dmitri Shuralyov <[email protected]>
1 parent e71702b commit e7bd227

File tree

5 files changed

+327
-52
lines changed

5 files changed

+327
-52
lines changed

gopls/internal/golang/folding_range.go

Lines changed: 73 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package golang
66

77
import (
8+
"bytes"
89
"context"
910
"go/ast"
1011
"go/token"
@@ -73,94 +74,125 @@ func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *Fold
7374
// TODO(suzmue): include trailing empty lines before the closing
7475
// parenthesis/brace.
7576
var kind protocol.FoldingRangeKind
77+
// start and end define the range of content to fold away.
7678
var start, end token.Pos
7779
switch n := n.(type) {
7880
case *ast.BlockStmt:
7981
// Fold between positions of or lines between "{" and "}".
80-
var startList, endList token.Pos
81-
if num := len(n.List); num != 0 {
82-
startList, endList = n.List[0].Pos(), n.List[num-1].End()
83-
}
84-
start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly)
82+
start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly)
8583
case *ast.CaseClause:
8684
// Fold from position of ":" to end.
8785
start, end = n.Colon+1, n.End()
8886
case *ast.CommClause:
8987
// Fold from position of ":" to end.
9088
start, end = n.Colon+1, n.End()
9189
case *ast.CallExpr:
92-
// Fold from position of "(" to position of ")".
93-
start, end = n.Lparen+1, n.Rparen
90+
// Fold between positions of or lines between "(" and ")".
91+
start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly)
9492
case *ast.FieldList:
9593
// Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace.
96-
var startList, endList token.Pos
97-
if num := len(n.List); num != 0 {
98-
startList, endList = n.List[0].Pos(), n.List[num-1].End()
99-
}
100-
start, end = validLineFoldingRange(pgf.Tok, n.Opening, n.Closing, startList, endList, lineFoldingOnly)
94+
start, end = getLineFoldingRange(pgf, n.Opening, n.Closing, lineFoldingOnly)
10195
case *ast.GenDecl:
10296
// If this is an import declaration, set the kind to be protocol.Imports.
10397
if n.Tok == token.IMPORT {
10498
kind = protocol.Imports
10599
}
106100
// Fold between positions of or lines between "(" and ")".
107-
var startSpecs, endSpecs token.Pos
108-
if num := len(n.Specs); num != 0 {
109-
startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End()
110-
}
111-
start, end = validLineFoldingRange(pgf.Tok, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly)
101+
start, end = getLineFoldingRange(pgf, n.Lparen, n.Rparen, lineFoldingOnly)
112102
case *ast.BasicLit:
113103
// Fold raw string literals from position of "`" to position of "`".
114104
if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' {
115105
start, end = n.Pos(), n.End()
116106
}
117107
case *ast.CompositeLit:
118108
// Fold between positions of or lines between "{" and "}".
119-
var startElts, endElts token.Pos
120-
if num := len(n.Elts); num != 0 {
121-
startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End()
122-
}
123-
start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly)
109+
start, end = getLineFoldingRange(pgf, n.Lbrace, n.Rbrace, lineFoldingOnly)
124110
}
125111

126112
// Check that folding positions are valid.
127113
if !start.IsValid() || !end.IsValid() {
128114
return nil
129115
}
116+
if start == end {
117+
// Nothing to fold.
118+
return nil
119+
}
130120
// in line folding mode, do not fold if the start and end lines are the same.
131121
if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) {
132122
return nil
133123
}
134124
mrng, err := pgf.PosMappedRange(start, end)
135125
if err != nil {
136-
bug.Errorf("%w", err) // can't happen
126+
bug.Reportf("failed to create mapped range: %s", err) // can't happen
127+
return nil
137128
}
138129
return &FoldingRangeInfo{
139130
MappedRange: mrng,
140131
Kind: kind,
141132
}
142133
}
143134

144-
// validLineFoldingRange returns start and end token.Pos for folding range if the range is valid.
145-
// returns token.NoPos otherwise, which fails token.IsValid check
146-
func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) {
147-
if lineFoldingOnly {
148-
if !open.IsValid() || !close.IsValid() {
149-
return token.NoPos, token.NoPos
150-
}
135+
// getLineFoldingRange returns the folding range for nodes with parentheses/braces/brackets
136+
// that potentially can take up multiple lines.
137+
func getLineFoldingRange(pgf *parsego.File, open, close token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) {
138+
if !open.IsValid() || !close.IsValid() {
139+
return token.NoPos, token.NoPos
140+
}
141+
if open+1 == close {
142+
// Nothing to fold: (), {} or [].
143+
return token.NoPos, token.NoPos
144+
}
145+
146+
if !lineFoldingOnly {
147+
// Can fold between opening and closing parenthesis/brace
148+
// even if they are on the same line.
149+
return open + 1, close
150+
}
151151

152-
// Don't want to fold if the start/end is on the same line as the open/close
153-
// as an example, the example below should *not* fold:
154-
// var x = [2]string{"d",
155-
// "e" }
156-
if safetoken.Line(tokFile, open) == safetoken.Line(tokFile, start) ||
157-
safetoken.Line(tokFile, close) == safetoken.Line(tokFile, end) {
158-
return token.NoPos, token.NoPos
152+
// Clients with "LineFoldingOnly" set to true can fold only full lines.
153+
// So, we return a folding range only when the closing parenthesis/brace
154+
// and the end of the last argument/statement/element are on different lines.
155+
//
156+
// We could skip the check for the opening parenthesis/brace and start of
157+
// the first argument/statement/element. For example, the following code
158+
//
159+
// var x = []string{"a",
160+
// "b",
161+
// "c" }
162+
//
163+
// can be folded to
164+
//
165+
// var x = []string{"a", ...
166+
// "c" }
167+
//
168+
// However, this might look confusing. So, check the lines of "open" and
169+
// "start" positions as well.
170+
171+
// isOnlySpaceBetween returns true if there are only space characters between "from" and "to".
172+
isOnlySpaceBetween := func(from token.Pos, to token.Pos) bool {
173+
start, end, err := safetoken.Offsets(pgf.Tok, from, to)
174+
if err != nil {
175+
bug.Reportf("failed to get offsets: %s", err) // can't happen
176+
return false
159177
}
178+
return len(bytes.TrimSpace(pgf.Src[start:end])) == 0
179+
}
160180

161-
return open + 1, end
181+
nextLine := safetoken.Line(pgf.Tok, open) + 1
182+
if nextLine > pgf.Tok.LineCount() {
183+
return token.NoPos, token.NoPos
162184
}
163-
return open + 1, close
185+
nextLineStart := pgf.Tok.LineStart(nextLine)
186+
if !isOnlySpaceBetween(open+1, nextLineStart) {
187+
return token.NoPos, token.NoPos
188+
}
189+
190+
prevLineEnd := pgf.Tok.LineStart(safetoken.Line(pgf.Tok, close)) - 1 // there must be a previous line
191+
if !isOnlySpaceBetween(prevLineEnd, close) {
192+
return token.NoPos, token.NoPos
193+
}
194+
195+
return open + 1, prevLineEnd
164196
}
165197

166198
// commentsFoldingRange returns the folding ranges for all comment blocks in file.
@@ -185,7 +217,8 @@ func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) {
185217
}
186218
mrng, err := pgf.PosMappedRange(endLinePos, commentGrp.End())
187219
if err != nil {
188-
bug.Errorf("%w", err) // can't happen
220+
bug.Reportf("failed to create mapped range: %s", err) // can't happen
221+
continue
189222
}
190223
comments = append(comments, &FoldingRangeInfo{
191224
// Fold from the end of the first line comment to the end of the comment block.

gopls/internal/test/marker/testdata/foldingrange/a.txt

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,17 @@ package folding //@foldingrange(raw)
66
import (
77
"fmt"
88
_ "log"
9+
"sort"
10+
"time"
911
)
1012

1113
import _ "os"
1214

1315
// bar is a function.
1416
// With a multiline doc comment.
15-
func bar() string {
17+
func bar() (
18+
string,
19+
) {
1620
/* This is a single line comment */
1721
switch {
1822
case true:
@@ -76,19 +80,74 @@ func bar() string {
7680
this string
7781
is not indented`
7882
}
83+
84+
func _() {
85+
slice := []int{1, 2, 3}
86+
sort.Slice(slice, func(i, j int) bool {
87+
a, b := slice[i], slice[j]
88+
return a < b
89+
})
90+
91+
sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] })
92+
93+
sort.Slice(
94+
slice,
95+
func(i, j int) bool {
96+
return slice[i] < slice[j]
97+
},
98+
)
99+
100+
fmt.Println(
101+
1, 2, 3,
102+
4,
103+
)
104+
105+
fmt.Println(1, 2, 3,
106+
4, 5, 6,
107+
7, 8, 9,
108+
10)
109+
110+
// Call with ellipsis.
111+
_ = fmt.Errorf(
112+
"test %d %d",
113+
[]any{1, 2, 3}...,
114+
)
115+
116+
// Check multiline string.
117+
fmt.Println(
118+
`multi
119+
line
120+
string
121+
`,
122+
1, 2, 3,
123+
)
124+
125+
// Call without arguments.
126+
_ = time.Now()
127+
}
128+
129+
func _(
130+
a int, b int,
131+
c int,
132+
) {
133+
}
79134
-- @raw --
80135
package folding //@foldingrange(raw)
81136

82137
import (<0 kind="imports">
83138
"fmt"
84139
_ "log"
140+
"sort"
141+
"time"
85142
</0>)
86143

87144
import _ "os"
88145

89146
// bar is a function.<1 kind="comment">
90147
// With a multiline doc comment.</1>
91-
func bar(<2 kind=""></2>) string {<3 kind="">
148+
func bar() (<2 kind="">
149+
string,
150+
</2>) {<3 kind="">
92151
/* This is a single line comment */
93152
switch {<4 kind="">
94153
case true:<5 kind="">
@@ -152,3 +211,54 @@ func bar(<2 kind=""></2>) string {<3 kind="">
152211
this string
153212
is not indented`</34>
154213
</3>}
214+
215+
func _() {<35 kind="">
216+
slice := []int{<36 kind="">1, 2, 3</36>}
217+
sort.Slice(<37 kind="">slice, func(<38 kind="">i, j int</38>) bool {<39 kind="">
218+
a, b := slice[i], slice[j]
219+
return a < b
220+
</39>}</37>)
221+
222+
sort.Slice(<40 kind="">slice, func(<41 kind="">i, j int</41>) bool {<42 kind=""> return slice[i] < slice[j] </42>}</40>)
223+
224+
sort.Slice(<43 kind="">
225+
slice,
226+
func(<44 kind="">i, j int</44>) bool {<45 kind="">
227+
return slice[i] < slice[j]
228+
</45>},
229+
</43>)
230+
231+
fmt.Println(<46 kind="">
232+
1, 2, 3,
233+
4,
234+
</46>)
235+
236+
fmt.Println(<47 kind="">1, 2, 3,
237+
4, 5, 6,
238+
7, 8, 9,
239+
10</47>)
240+
241+
// Call with ellipsis.
242+
_ = fmt.Errorf(<48 kind="">
243+
"test %d %d",
244+
[]any{<49 kind="">1, 2, 3</49>}...,
245+
</48>)
246+
247+
// Check multiline string.
248+
fmt.Println(<50 kind="">
249+
<51 kind="">`multi
250+
line
251+
string
252+
`</51>,
253+
1, 2, 3,
254+
</50>)
255+
256+
// Call without arguments.
257+
_ = time.Now()
258+
</35>}
259+
260+
func _(<52 kind="">
261+
a int, b int,
262+
c int,
263+
</52>) {<53 kind="">
264+
</53>}

0 commit comments

Comments
 (0)