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.
13 "golang.org/x/tools/internal/lsp/protocol"
16 // FoldingRangeInfo holds range and kind info of folding for an ast.Node
17 type FoldingRangeInfo struct {
19 Kind protocol.FoldingRangeKind
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)
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 {
43 fset := snapshot.FileSet()
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)...)
48 visit := func(n ast.Node) bool {
49 rng := foldingRangeFunc(fset, pgf.Mapper, n, lineFoldingOnly)
51 ranges = append(ranges, rng)
55 // Walk the ast and collect folding ranges.
56 ast.Inspect(pgf.File, visit)
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
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
71 var kind protocol.FoldingRangeKind
72 var start, end token.Pos
73 switch n := n.(type) {
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()
80 start, end = validLineFoldingRange(fset, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly)
82 // Fold from position of ":" to end.
83 start, end = n.Colon+1, n.End()
85 // Fold from position of ":" to end.
86 start, end = n.Colon+1, n.End()
88 // Fold from position of "(" to position of ")".
89 start, end = n.Lparen+1, n.Rparen
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()
96 start, end = validLineFoldingRange(fset, n.Opening, n.Closing, startList, endList, lineFoldingOnly)
98 // If this is an import declaration, set the kind to be protocol.Imports.
99 if n.Tok == token.IMPORT {
100 kind = protocol.Imports
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()
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()
114 start, end = validLineFoldingRange(fset, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly)
117 // Check that folding positions are valid.
118 if !start.IsValid() || !end.IsValid() {
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 {
125 return &FoldingRangeInfo{
126 MappedRange: NewMappedRange(fset, m, start, end),
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) {
135 if !open.IsValid() || !close.IsValid() {
136 return token.NoPos, token.NoPos
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",
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
150 return open + 1, close
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 {
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,