|
5 | 5 | package golang
|
6 | 6 |
|
7 | 7 | import (
|
| 8 | + "bytes" |
8 | 9 | "context"
|
9 | 10 | "go/ast"
|
10 | 11 | "go/token"
|
@@ -73,94 +74,125 @@ func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *Fold
|
73 | 74 | // TODO(suzmue): include trailing empty lines before the closing
|
74 | 75 | // parenthesis/brace.
|
75 | 76 | var kind protocol.FoldingRangeKind
|
| 77 | + // start and end define the range of content to fold away. |
76 | 78 | var start, end token.Pos
|
77 | 79 | switch n := n.(type) {
|
78 | 80 | case *ast.BlockStmt:
|
79 | 81 | // 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) |
85 | 83 | case *ast.CaseClause:
|
86 | 84 | // Fold from position of ":" to end.
|
87 | 85 | start, end = n.Colon+1, n.End()
|
88 | 86 | case *ast.CommClause:
|
89 | 87 | // Fold from position of ":" to end.
|
90 | 88 | start, end = n.Colon+1, n.End()
|
91 | 89 | 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) |
94 | 92 | case *ast.FieldList:
|
95 | 93 | // 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) |
101 | 95 | case *ast.GenDecl:
|
102 | 96 | // If this is an import declaration, set the kind to be protocol.Imports.
|
103 | 97 | if n.Tok == token.IMPORT {
|
104 | 98 | kind = protocol.Imports
|
105 | 99 | }
|
106 | 100 | // 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) |
112 | 102 | case *ast.BasicLit:
|
113 | 103 | // Fold raw string literals from position of "`" to position of "`".
|
114 | 104 | if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' {
|
115 | 105 | start, end = n.Pos(), n.End()
|
116 | 106 | }
|
117 | 107 | case *ast.CompositeLit:
|
118 | 108 | // 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) |
124 | 110 | }
|
125 | 111 |
|
126 | 112 | // Check that folding positions are valid.
|
127 | 113 | if !start.IsValid() || !end.IsValid() {
|
128 | 114 | return nil
|
129 | 115 | }
|
| 116 | + if start == end { |
| 117 | + // Nothing to fold. |
| 118 | + return nil |
| 119 | + } |
130 | 120 | // in line folding mode, do not fold if the start and end lines are the same.
|
131 | 121 | if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) {
|
132 | 122 | return nil
|
133 | 123 | }
|
134 | 124 | mrng, err := pgf.PosMappedRange(start, end)
|
135 | 125 | if err != nil {
|
136 | 126 | bug.Errorf("%w", err) // can't happen
|
| 127 | + return nil |
137 | 128 | }
|
138 | 129 | return &FoldingRangeInfo{
|
139 | 130 | MappedRange: mrng,
|
140 | 131 | Kind: kind,
|
141 | 132 | }
|
142 | 133 | }
|
143 | 134 |
|
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 | + } |
151 | 151 |
|
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.Errorf("%w", err) // can't happen |
| 176 | + return false |
159 | 177 | }
|
| 178 | + return len(bytes.TrimSpace(pgf.Src[start:end])) == 0 |
| 179 | + } |
160 | 180 |
|
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 |
| 184 | + } |
| 185 | + nextLineStart := pgf.Tok.LineStart(nextLine) |
| 186 | + if !isOnlySpaceBetween(open+1, nextLineStart) { |
| 187 | + return token.NoPos, token.NoPos |
162 | 188 | }
|
163 |
| - return open + 1, close |
| 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 |
164 | 196 | }
|
165 | 197 |
|
166 | 198 | // commentsFoldingRange returns the folding ranges for all comment blocks in file.
|
|
0 commit comments