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