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