golang.org/x/tools/gopls@v0.15.3/internal/golang/folding_range.go (about) 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 golang 6 7 import ( 8 "context" 9 "go/ast" 10 "go/token" 11 "sort" 12 "strings" 13 14 "golang.org/x/tools/gopls/internal/cache" 15 "golang.org/x/tools/gopls/internal/file" 16 "golang.org/x/tools/gopls/internal/protocol" 17 "golang.org/x/tools/gopls/internal/util/bug" 18 "golang.org/x/tools/gopls/internal/util/safetoken" 19 ) 20 21 // FoldingRangeInfo holds range and kind info of folding for an ast.Node 22 type FoldingRangeInfo struct { 23 MappedRange protocol.MappedRange 24 Kind protocol.FoldingRangeKind 25 } 26 27 // FoldingRange gets all of the folding range for f. 28 func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { 29 // TODO(suzmue): consider limiting the number of folding ranges returned, and 30 // implement a way to prioritize folding ranges in that case. 31 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 32 if err != nil { 33 return nil, err 34 } 35 36 // With parse errors, we wouldn't be able to produce accurate folding info. 37 // LSP protocol (3.16) currently does not have a way to handle this case 38 // (https://github.com/microsoft/language-server-protocol/issues/1200). 39 // We cannot return an error either because we are afraid some editors 40 // may not handle errors nicely. As a workaround, we now return an empty 41 // result and let the client handle this case by double check the file 42 // contents (i.e. if the file is not empty and the folding range result 43 // is empty, raise an internal error). 44 if pgf.ParseErr != nil { 45 return nil, nil 46 } 47 48 // Get folding ranges for comments separately as they are not walked by ast.Inspect. 49 ranges = append(ranges, commentsFoldingRange(pgf)...) 50 51 visit := func(n ast.Node) bool { 52 rng := foldingRangeFunc(pgf, n, lineFoldingOnly) 53 if rng != nil { 54 ranges = append(ranges, rng) 55 } 56 return true 57 } 58 // Walk the ast and collect folding ranges. 59 ast.Inspect(pgf.File, visit) 60 61 sort.Slice(ranges, func(i, j int) bool { 62 irng := ranges[i].MappedRange.Range() 63 jrng := ranges[j].MappedRange.Range() 64 return protocol.CompareRange(irng, jrng) < 0 65 }) 66 67 return ranges, nil 68 } 69 70 // foldingRangeFunc calculates the line folding range for ast.Node n 71 func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { 72 // TODO(suzmue): include trailing empty lines before the closing 73 // parenthesis/brace. 74 var kind protocol.FoldingRangeKind 75 var start, end token.Pos 76 switch n := n.(type) { 77 case *ast.BlockStmt: 78 // Fold between positions of or lines between "{" and "}". 79 var startList, endList token.Pos 80 if num := len(n.List); num != 0 { 81 startList, endList = n.List[0].Pos(), n.List[num-1].End() 82 } 83 start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) 84 case *ast.CaseClause: 85 // Fold from position of ":" to end. 86 start, end = n.Colon+1, n.End() 87 case *ast.CommClause: 88 // Fold from position of ":" to end. 89 start, end = n.Colon+1, n.End() 90 case *ast.CallExpr: 91 // Fold from position of "(" to position of ")". 92 start, end = n.Lparen+1, n.Rparen 93 case *ast.FieldList: 94 // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace. 95 var startList, endList token.Pos 96 if num := len(n.List); num != 0 { 97 startList, endList = n.List[0].Pos(), n.List[num-1].End() 98 } 99 start, end = validLineFoldingRange(pgf.Tok, n.Opening, n.Closing, startList, endList, lineFoldingOnly) 100 case *ast.GenDecl: 101 // If this is an import declaration, set the kind to be protocol.Imports. 102 if n.Tok == token.IMPORT { 103 kind = protocol.Imports 104 } 105 // Fold between positions of or lines between "(" and ")". 106 var startSpecs, endSpecs token.Pos 107 if num := len(n.Specs); num != 0 { 108 startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End() 109 } 110 start, end = validLineFoldingRange(pgf.Tok, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) 111 case *ast.BasicLit: 112 // Fold raw string literals from position of "`" to position of "`". 113 if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { 114 start, end = n.Pos(), n.End() 115 } 116 case *ast.CompositeLit: 117 // Fold between positions of or lines between "{" and "}". 118 var startElts, endElts token.Pos 119 if num := len(n.Elts); num != 0 { 120 startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() 121 } 122 start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) 123 } 124 125 // Check that folding positions are valid. 126 if !start.IsValid() || !end.IsValid() { 127 return nil 128 } 129 // in line folding mode, do not fold if the start and end lines are the same. 130 if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) { 131 return nil 132 } 133 mrng, err := pgf.PosMappedRange(start, end) 134 if err != nil { 135 bug.Errorf("%w", err) // can't happen 136 } 137 return &FoldingRangeInfo{ 138 MappedRange: mrng, 139 Kind: kind, 140 } 141 } 142 143 // validLineFoldingRange returns start and end token.Pos for folding range if the range is valid. 144 // returns token.NoPos otherwise, which fails token.IsValid check 145 func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { 146 if lineFoldingOnly { 147 if !open.IsValid() || !close.IsValid() { 148 return token.NoPos, token.NoPos 149 } 150 151 // Don't want to fold if the start/end is on the same line as the open/close 152 // as an example, the example below should *not* fold: 153 // var x = [2]string{"d", 154 // "e" } 155 if safetoken.Line(tokFile, open) == safetoken.Line(tokFile, start) || 156 safetoken.Line(tokFile, close) == safetoken.Line(tokFile, end) { 157 return token.NoPos, token.NoPos 158 } 159 160 return open + 1, end 161 } 162 return open + 1, close 163 } 164 165 // commentsFoldingRange returns the folding ranges for all comment blocks in file. 166 // The folding range starts at the end of the first line of the comment block, and ends at the end of the 167 // comment block and has kind protocol.Comment. 168 func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) { 169 tokFile := pgf.Tok 170 for _, commentGrp := range pgf.File.Comments { 171 startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End()) 172 if startGrpLine == endGrpLine { 173 // Don't fold single line comments. 174 continue 175 } 176 177 firstComment := commentGrp.List[0] 178 startPos, endLinePos := firstComment.Pos(), firstComment.End() 179 startCmmntLine, endCmmntLine := safetoken.Line(tokFile, startPos), safetoken.Line(tokFile, endLinePos) 180 if startCmmntLine != endCmmntLine { 181 // If the first comment spans multiple lines, then we want to have the 182 // folding range start at the end of the first line. 183 endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) 184 } 185 mrng, err := pgf.PosMappedRange(endLinePos, commentGrp.End()) 186 if err != nil { 187 bug.Errorf("%w", err) // can't happen 188 } 189 comments = append(comments, &FoldingRangeInfo{ 190 // Fold from the end of the first line comment to the end of the comment block. 191 MappedRange: mrng, 192 Kind: protocol.Comment, 193 }) 194 } 195 return comments 196 }