// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package source import ( "context" "go/ast" "go/token" "sort" "strings" "golang.org/x/tools/internal/lsp/protocol" ) // FoldingRangeInfo holds range and kind info of folding for an ast.Node type FoldingRangeInfo struct { MappedRange Kind protocol.FoldingRangeKind } // FoldingRange gets all of the folding range for f. func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { // TODO(suzmue): consider limiting the number of folding ranges returned, and // implement a way to prioritize folding ranges in that case. pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) if err != nil { return nil, err } // With parse errors, we wouldn't be able to produce accurate folding info. // LSP protocol (3.16) currently does not have a way to handle this case // (https://github.com/microsoft/language-server-protocol/issues/1200). // We cannot return an error either because we are afraid some editors // may not handle errors nicely. As a workaround, we now return an empty // result and let the client handle this case by double check the file // contents (i.e. if the file is not empty and the folding range result // is empty, raise an internal error). if pgf.ParseErr != nil { return nil, nil } // Get folding ranges for comments separately as they are not walked by ast.Inspect. ranges = append(ranges, commentsFoldingRange(pgf.Tok, pgf.Mapper, pgf.File)...) visit := func(n ast.Node) bool { rng := foldingRangeFunc(pgf.Tok, pgf.Mapper, n, lineFoldingOnly) if rng != nil { ranges = append(ranges, rng) } return true } // Walk the ast and collect folding ranges. ast.Inspect(pgf.File, visit) sort.Slice(ranges, func(i, j int) bool { irng, _ := ranges[i].Range() jrng, _ := ranges[j].Range() return protocol.CompareRange(irng, jrng) < 0 }) return ranges, nil } // foldingRangeFunc calculates the line folding range for ast.Node n func foldingRangeFunc(tokFile *token.File, m *protocol.ColumnMapper, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { // TODO(suzmue): include trailing empty lines before the closing // parenthesis/brace. var kind protocol.FoldingRangeKind var start, end token.Pos switch n := n.(type) { case *ast.BlockStmt: // Fold between positions of or lines between "{" and "}". var startList, endList token.Pos if num := len(n.List); num != 0 { startList, endList = n.List[0].Pos(), n.List[num-1].End() } start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) case *ast.CaseClause: // Fold from position of ":" to end. start, end = n.Colon+1, n.End() case *ast.CommClause: // Fold from position of ":" to end. start, end = n.Colon+1, n.End() case *ast.CallExpr: // Fold from position of "(" to position of ")". start, end = n.Lparen+1, n.Rparen case *ast.FieldList: // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace. var startList, endList token.Pos if num := len(n.List); num != 0 { startList, endList = n.List[0].Pos(), n.List[num-1].End() } start, end = validLineFoldingRange(tokFile, n.Opening, n.Closing, startList, endList, lineFoldingOnly) case *ast.GenDecl: // If this is an import declaration, set the kind to be protocol.Imports. if n.Tok == token.IMPORT { kind = protocol.Imports } // Fold between positions of or lines between "(" and ")". var startSpecs, endSpecs token.Pos if num := len(n.Specs); num != 0 { startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End() } start, end = validLineFoldingRange(tokFile, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) case *ast.BasicLit: // Fold raw string literals from position of "`" to position of "`". if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { start, end = n.Pos(), n.End() } case *ast.CompositeLit: // Fold between positions of or lines between "{" and "}". var startElts, endElts token.Pos if num := len(n.Elts); num != 0 { startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() } start, end = validLineFoldingRange(tokFile, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) } // Check that folding positions are valid. if !start.IsValid() || !end.IsValid() { return nil } // in line folding mode, do not fold if the start and end lines are the same. if lineFoldingOnly && tokFile.Line(start) == tokFile.Line(end) { return nil } return &FoldingRangeInfo{ MappedRange: NewMappedRange(tokFile, m, start, end), Kind: kind, } } // validLineFoldingRange returns start and end token.Pos for folding range if the range is valid. // returns token.NoPos otherwise, which fails token.IsValid check func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { if lineFoldingOnly { if !open.IsValid() || !close.IsValid() { return token.NoPos, token.NoPos } // Don't want to fold if the start/end is on the same line as the open/close // as an example, the example below should *not* fold: // var x = [2]string{"d", // "e" } if tokFile.Line(open) == tokFile.Line(start) || tokFile.Line(close) == tokFile.Line(end) { return token.NoPos, token.NoPos } return open + 1, end } return open + 1, close } // commentsFoldingRange returns the folding ranges for all comment blocks in file. // The folding range starts at the end of the first line of the comment block, and ends at the end of the // comment block and has kind protocol.Comment. func commentsFoldingRange(tokFile *token.File, m *protocol.ColumnMapper, file *ast.File) (comments []*FoldingRangeInfo) { for _, commentGrp := range file.Comments { startGrpLine, endGrpLine := tokFile.Line(commentGrp.Pos()), tokFile.Line(commentGrp.End()) if startGrpLine == endGrpLine { // Don't fold single line comments. continue } firstComment := commentGrp.List[0] startPos, endLinePos := firstComment.Pos(), firstComment.End() startCmmntLine, endCmmntLine := tokFile.Line(startPos), tokFile.Line(endLinePos) if startCmmntLine != endCmmntLine { // If the first comment spans multiple lines, then we want to have the // folding range start at the end of the first line. endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) } comments = append(comments, &FoldingRangeInfo{ // Fold from the end of the first line comment to the end of the comment block. MappedRange: NewMappedRange(tokFile, m, endLinePos, commentGrp.End()), Kind: protocol.Comment, }) } return comments }