github.com/april1989/origin-go-tools@v0.0.32/internal/lsp/source/folding_range.go (about)

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