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