internal/lsp: add foldingRange support

Support textDocument/foldingRange request. Provide folding ranges for
multiline comment blocks, declarations, block statements, field lists,
case clauses, and call expressions.

Fixes golang/go#32987

Change-Id: I9c76e850ffa0e5bb65bee273d8ee40577c342f92
Reviewed-on: https://go-review.googlesource.com/c/tools/+/192257
Run-TryBot: Suzy Mueller <suzmue@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Suzy Mueller 2019-08-28 21:48:29 -04:00
Родитель 88604bcfcf
Коммит 114c575556
10 изменённых файлов: 411 добавлений и 2 удалений

Просмотреть файл

@ -44,6 +44,10 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
//TODO: add command line completions tests when it works
}
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
//TODO: add command line folding range tests when it works
}
func (r *runner) Highlight(t *testing.T, data tests.Highlights) {
//TODO: add command line highlight tests when it works
}

Просмотреть файл

@ -0,0 +1,28 @@
package lsp
import (
"context"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
uri := span.NewURI(params.TextDocument.URI)
view := s.session.ViewOf(uri)
f, err := getGoFile(ctx, view, uri)
if err != nil {
return nil, err
}
m, err := getMapper(ctx, f)
if err != nil {
return nil, err
}
ranges, err := source.FoldingRange(ctx, view, f)
if err != nil {
return nil, err
}
return source.ToProtocolFoldingRanges(m, ranges)
}

Просмотреть файл

@ -99,6 +99,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
DefinitionProvider: true,
DocumentFormattingProvider: true,
DocumentSymbolProvider: true,
FoldingRangeProvider: true,
HoverProvider: true,
DocumentHighlightProvider: true,
DocumentLinkProvider: &protocol.DocumentLinkOptions{},

Просмотреть файл

@ -254,6 +254,103 @@ func summarizeCompletionItems(i int, want []source.CompletionItem, got []protoco
return msg.String()
}
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
for _, spn := range data {
uri := spn.URI()
filename := uri.Filename()
ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(uri),
},
})
if err != nil {
t.Error(err)
continue
}
f, err := getGoFile(r.ctx, r.server.session.ViewOf(uri), uri)
if err != nil {
t.Fatal(err)
}
m, err := getMapper(r.ctx, f)
if err != nil {
t.Fatal(err)
}
// Fold all ranges.
got, err := foldRanges(m, string(m.Content), ranges)
if err != nil {
t.Error(err)
continue
}
want := string(r.data.Golden("foldingRange", spn.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
t.Errorf("foldingRanges failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
}
// Filter by kind.
kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment}
for _, kind := range kinds {
var kindOnly []protocol.FoldingRange
for _, fRng := range ranges {
if fRng.Kind == string(kind) {
kindOnly = append(kindOnly, fRng)
}
}
got, err := foldRanges(m, string(m.Content), kindOnly)
if err != nil {
t.Error(err)
continue
}
want := string(r.data.Golden("foldingRange-"+string(kind), spn.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
t.Errorf("foldingRanges-%s failed for %s, expected:\n%v\ngot:\n%v", string(kind), filename, want, got)
}
}
}
}
func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.FoldingRange) (string, error) {
// TODO(suzmue): Allow folding ranges to intersect for these tests, do a folding by level,
// or per individual fold.
foldedText := "<>"
res := contents
// Apply the edits from the end of the file forward
// to preserve the offsets
for i := len(ranges) - 1; i >= 0; i-- {
fRange := ranges[i]
spn, err := m.RangeSpan(protocol.Range{
Start: protocol.Position{
Line: fRange.StartLine,
Character: fRange.StartCharacter,
},
End: protocol.Position{
Line: fRange.EndLine,
Character: fRange.EndCharacter,
},
})
if err != nil {
return "", err
}
start := spn.Start().Offset()
end := spn.End().Offset()
tmp := res[0:start] + foldedText
res = tmp + res[end:]
}
return res, nil
}
func (r *runner) Format(t *testing.T, data tests.Formats) {
for _, spn := range data {
uri := spn.URI()

Просмотреть файл

@ -260,8 +260,8 @@ func (s *Server) Declaration(context.Context, *protocol.TextDocumentPositionPara
return nil, notImplemented("Declaration")
}
func (s *Server) FoldingRange(context.Context, *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
return nil, notImplemented("FoldingRange")
func (s *Server) FoldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
return s.foldingRange(ctx, params)
}
func (s *Server) LogTraceNotification(context.Context, *protocol.LogTraceParams) error {

Просмотреть файл

@ -0,0 +1,118 @@
package source
import (
"context"
"go/ast"
"go/token"
"sort"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
)
type FoldingRangeInfo struct {
Range span.Range
Kind protocol.FoldingRangeKind
}
// FoldingRange gets all of the folding range for f.
func FoldingRange(ctx context.Context, view View, f GoFile) (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.
file, err := f.GetAST(ctx, ParseFull)
if err != nil {
return nil, err
}
// Get folding ranges for comments separately as they are not walked by ast.Inspect.
ranges = append(ranges, commentsFoldingRange(f.FileSet(), file)...)
visit := func(n ast.Node) bool {
var kind protocol.FoldingRangeKind
var start, end token.Pos
switch n := n.(type) {
case *ast.BlockStmt:
// Fold from position of "{" to position of "}".
start, end = n.Lbrace+1, n.Rbrace
case *ast.CaseClause:
// 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 from position of opening parenthesis/brace, to position of
// closing parenthesis/brace.
start, end = n.Opening+1, n.Closing
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 from position of "(" to position of ")".
start, end = n.Lparen+1, n.Rparen
}
if start.IsValid() && end.IsValid() {
ranges = append(ranges, FoldingRangeInfo{
Range: span.NewRange(f.FileSet(), start, end),
Kind: kind,
})
}
return true
}
// Walk the ast and collect folding ranges.
ast.Inspect(file, visit)
sort.Slice(ranges, func(i, j int) bool {
if ranges[i].Range.Start < ranges[j].Range.Start {
return true
} else if ranges[i].Range.Start > ranges[j].Range.Start {
return false
}
return ranges[i].Range.End < ranges[j].Range.End
})
return ranges, nil
}
// commentsFoldingRange returns the folding ranges for all comment blocks in file.
// The folding range starts at the end of the first comment, and ends at the end of the
// comment block and has kind protocol.Comment.
func commentsFoldingRange(fset *token.FileSet, file *ast.File) []FoldingRangeInfo {
var comments []FoldingRangeInfo
for _, commentGrp := range file.Comments {
// Don't fold single comments.
if len(commentGrp.List) <= 1 {
continue
}
comments = append(comments, FoldingRangeInfo{
// Fold from the end of the first line comment to the end of the comment block.
Range: span.NewRange(fset, commentGrp.List[0].End(), commentGrp.End()),
Kind: protocol.Comment,
})
}
return comments
}
func ToProtocolFoldingRanges(m *protocol.ColumnMapper, ranges []FoldingRangeInfo) ([]protocol.FoldingRange, error) {
var res []protocol.FoldingRange
for _, r := range ranges {
spn, err := r.Range.Span()
if err != nil {
return nil, err
}
rng, err := m.Range(spn)
if err != nil {
return nil, err
}
res = append(res, protocol.FoldingRange{
StartLine: rng.Start.Line,
StartCharacter: rng.Start.Character,
EndLine: rng.End.Line,
EndCharacter: rng.End.Character,
Kind: string(r.Kind),
})
}
return res, nil
}

Просмотреть файл

@ -257,6 +257,89 @@ func summarizeCompletionItems(i int, want []source.CompletionItem, got []source.
return msg.String()
}
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
for _, spn := range data {
uri := spn.URI()
filename := uri.Filename()
f, err := r.view.GetFile(r.ctx, uri)
if err != nil {
t.Fatalf("failed for %v: %v", spn, err)
}
ranges, err := source.FoldingRange(r.ctx, r.view, f.(source.GoFile))
if err != nil {
t.Error(err)
continue
}
data, _, err := f.Handle(r.ctx).Read(r.ctx)
if err != nil {
t.Error(err)
continue
}
// Fold all ranges.
got, err := foldRanges(string(data), ranges)
if err != nil {
t.Error(err)
continue
}
want := string(r.data.Golden("foldingRange", spn.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
t.Errorf("foldingRanges failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
}
// Filter by kind.
kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment}
for _, kind := range kinds {
var kindOnly []source.FoldingRangeInfo
for _, fRng := range ranges {
if fRng.Kind == kind {
kindOnly = append(kindOnly, fRng)
}
}
got, err := foldRanges(string(data), kindOnly)
if err != nil {
t.Error(err)
continue
}
want := string(r.data.Golden("foldingRange-"+string(kind), spn.URI().Filename(), func() ([]byte, error) {
return []byte(got), nil
}))
if want != got {
t.Errorf("foldingRanges-%s failed for %s, expected:\n%v\ngot:\n%v", string(kind), filename, want, got)
}
}
}
}
func foldRanges(contents string, ranges []source.FoldingRangeInfo) (string, error) {
// TODO(suzmue): Allow folding ranges to intersect for these tests.
foldedText := "<>"
res := contents
// Apply the folds from the end of the file forward
// to preserve the offsets.
for i := len(ranges) - 1; i >= 0; i-- {
fRange := ranges[i]
spn, err := fRange.Range.Span()
if err != nil {
return "", err
}
start := spn.Start().Offset()
end := spn.End().Offset()
tmp := res[0:start] + foldedText
res = tmp + res[end:]
}
return res, nil
}
func (r *runner) Format(t *testing.T, data tests.Formats) {
ctx := r.ctx
for _, spn := range data {

17
internal/lsp/testdata/folding/a.go поставляемый Normal file
Просмотреть файл

@ -0,0 +1,17 @@
package folding //@fold("package")
import (
_ "fmt"
_ "log"
)
import _ "os"
// bar is a function.
// With a multiline doc comment.
func bar() string {
return `
this string
is not indented`
}

44
internal/lsp/testdata/folding/a.go.golden поставляемый Normal file
Просмотреть файл

@ -0,0 +1,44 @@
-- foldingRange --
package folding //@fold("package")
import (<>)
import _ "os"
// bar is a function.<>
func bar(<>) string {<>}
-- foldingRange-comment --
package folding //@fold("package")
import (
_ "fmt"
_ "log"
)
import _ "os"
// bar is a function.<>
func bar() string {
return `
this string
is not indented`
}
-- foldingRange-imports --
package folding //@fold("package")
import (<>)
import _ "os"
// bar is a function.
// With a multiline doc comment.
func bar() string {
return `
this string
is not indented`
}

Просмотреть файл

@ -36,6 +36,7 @@ const (
ExpectedImportCount = 2
ExpectedDefinitionsCount = 39
ExpectedTypeDefinitionsCount = 2
ExpectedFoldingRangesCount = 1
ExpectedHighlightsCount = 2
ExpectedReferencesCount = 5
ExpectedRenamesCount = 20
@ -57,6 +58,7 @@ type Diagnostics map[span.URI][]source.Diagnostic
type CompletionItems map[token.Pos]*source.CompletionItem
type Completions map[span.Span][]token.Pos
type CompletionSnippets map[span.Span]CompletionSnippet
type FoldingRanges []span.Span
type Formats []span.Span
type Imports []span.Span
type Definitions map[span.Span]Definition
@ -75,6 +77,7 @@ type Data struct {
CompletionItems CompletionItems
Completions Completions
CompletionSnippets CompletionSnippets
FoldingRanges FoldingRanges
Formats Formats
Imports Imports
Definitions Definitions
@ -95,6 +98,7 @@ type Data struct {
type Tests interface {
Diagnostics(*testing.T, Diagnostics)
Completion(*testing.T, Completions, CompletionSnippets, CompletionItems)
FoldingRange(*testing.T, FoldingRanges)
Format(*testing.T, Formats)
Import(*testing.T, Imports)
Definition(*testing.T, Definitions)
@ -222,6 +226,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
"diag": data.collectDiagnostics,
"item": data.collectCompletionItems,
"complete": data.collectCompletions,
"fold": data.collectFoldingRanges,
"format": data.collectFormats,
"import": data.collectImports,
"godef": data.collectDefinitions,
@ -278,6 +283,14 @@ func Run(t *testing.T, tests Tests, data *Data) {
tests.Diagnostics(t, data.Diagnostics)
})
t.Run("FoldingRange", func(t *testing.T) {
t.Helper()
if len(data.FoldingRanges) != ExpectedFoldingRangesCount {
t.Errorf("got %v folding ranges expected %v", len(data.FoldingRanges), ExpectedFoldingRangesCount)
}
tests.FoldingRange(t, data.FoldingRanges)
})
t.Run("Format", func(t *testing.T) {
t.Helper()
if len(data.Formats) != ExpectedFormatCount {
@ -547,6 +560,10 @@ func (data *Data) collectCompletionItems(pos token.Pos, args []string) {
}
}
func (data *Data) collectFoldingRanges(spn span.Span) {
data.FoldingRanges = append(data.FoldingRanges, spn)
}
func (data *Data) collectFormats(spn span.Span) {
data.Formats = append(data.Formats, spn)
}