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  }