Skip to content

Commit 114c575

Browse files
committed
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent 88604bc commit 114c575

File tree

10 files changed

+411
-2
lines changed

10 files changed

+411
-2
lines changed

internal/lsp/cmd/cmd_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests
4444
//TODO: add command line completions tests when it works
4545
}
4646

47+
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
48+
//TODO: add command line folding range tests when it works
49+
}
50+
4751
func (r *runner) Highlight(t *testing.T, data tests.Highlights) {
4852
//TODO: add command line highlight tests when it works
4953
}

internal/lsp/folding_range.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package lsp
2+
3+
import (
4+
"context"
5+
6+
"golang.org/x/tools/internal/lsp/protocol"
7+
"golang.org/x/tools/internal/lsp/source"
8+
"golang.org/x/tools/internal/span"
9+
)
10+
11+
func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
12+
uri := span.NewURI(params.TextDocument.URI)
13+
view := s.session.ViewOf(uri)
14+
f, err := getGoFile(ctx, view, uri)
15+
if err != nil {
16+
return nil, err
17+
}
18+
m, err := getMapper(ctx, f)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
ranges, err := source.FoldingRange(ctx, view, f)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return source.ToProtocolFoldingRanges(m, ranges)
28+
}

internal/lsp/general.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.InitializePara
9999
DefinitionProvider: true,
100100
DocumentFormattingProvider: true,
101101
DocumentSymbolProvider: true,
102+
FoldingRangeProvider: true,
102103
HoverProvider: true,
103104
DocumentHighlightProvider: true,
104105
DocumentLinkProvider: &protocol.DocumentLinkOptions{},

internal/lsp/lsp_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,103 @@ func summarizeCompletionItems(i int, want []source.CompletionItem, got []protoco
254254
return msg.String()
255255
}
256256

257+
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
258+
for _, spn := range data {
259+
uri := spn.URI()
260+
filename := uri.Filename()
261+
262+
ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{
263+
TextDocument: protocol.TextDocumentIdentifier{
264+
URI: protocol.NewURI(uri),
265+
},
266+
})
267+
if err != nil {
268+
t.Error(err)
269+
continue
270+
}
271+
272+
f, err := getGoFile(r.ctx, r.server.session.ViewOf(uri), uri)
273+
if err != nil {
274+
t.Fatal(err)
275+
}
276+
m, err := getMapper(r.ctx, f)
277+
if err != nil {
278+
t.Fatal(err)
279+
}
280+
281+
// Fold all ranges.
282+
got, err := foldRanges(m, string(m.Content), ranges)
283+
if err != nil {
284+
t.Error(err)
285+
continue
286+
}
287+
want := string(r.data.Golden("foldingRange", spn.URI().Filename(), func() ([]byte, error) {
288+
return []byte(got), nil
289+
}))
290+
291+
if want != got {
292+
t.Errorf("foldingRanges failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
293+
}
294+
295+
// Filter by kind.
296+
kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment}
297+
for _, kind := range kinds {
298+
var kindOnly []protocol.FoldingRange
299+
for _, fRng := range ranges {
300+
if fRng.Kind == string(kind) {
301+
kindOnly = append(kindOnly, fRng)
302+
}
303+
}
304+
305+
got, err := foldRanges(m, string(m.Content), kindOnly)
306+
if err != nil {
307+
t.Error(err)
308+
continue
309+
}
310+
want := string(r.data.Golden("foldingRange-"+string(kind), spn.URI().Filename(), func() ([]byte, error) {
311+
return []byte(got), nil
312+
}))
313+
314+
if want != got {
315+
t.Errorf("foldingRanges-%s failed for %s, expected:\n%v\ngot:\n%v", string(kind), filename, want, got)
316+
}
317+
318+
}
319+
320+
}
321+
}
322+
323+
func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.FoldingRange) (string, error) {
324+
// TODO(suzmue): Allow folding ranges to intersect for these tests, do a folding by level,
325+
// or per individual fold.
326+
foldedText := "<>"
327+
res := contents
328+
// Apply the edits from the end of the file forward
329+
// to preserve the offsets
330+
for i := len(ranges) - 1; i >= 0; i-- {
331+
fRange := ranges[i]
332+
spn, err := m.RangeSpan(protocol.Range{
333+
Start: protocol.Position{
334+
Line: fRange.StartLine,
335+
Character: fRange.StartCharacter,
336+
},
337+
End: protocol.Position{
338+
Line: fRange.EndLine,
339+
Character: fRange.EndCharacter,
340+
},
341+
})
342+
if err != nil {
343+
return "", err
344+
}
345+
start := spn.Start().Offset()
346+
end := spn.End().Offset()
347+
348+
tmp := res[0:start] + foldedText
349+
res = tmp + res[end:]
350+
}
351+
return res, nil
352+
}
353+
257354
func (r *runner) Format(t *testing.T, data tests.Formats) {
258355
for _, spn := range data {
259356
uri := spn.URI()

internal/lsp/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,8 @@ func (s *Server) Declaration(context.Context, *protocol.TextDocumentPositionPara
260260
return nil, notImplemented("Declaration")
261261
}
262262

263-
func (s *Server) FoldingRange(context.Context, *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
264-
return nil, notImplemented("FoldingRange")
263+
func (s *Server) FoldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
264+
return s.foldingRange(ctx, params)
265265
}
266266

267267
func (s *Server) LogTraceNotification(context.Context, *protocol.LogTraceParams) error {

internal/lsp/source/folding_range.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package source
2+
3+
import (
4+
"context"
5+
"go/ast"
6+
"go/token"
7+
"sort"
8+
9+
"golang.org/x/tools/internal/lsp/protocol"
10+
"golang.org/x/tools/internal/span"
11+
)
12+
13+
type FoldingRangeInfo struct {
14+
Range span.Range
15+
Kind protocol.FoldingRangeKind
16+
}
17+
18+
// FoldingRange gets all of the folding range for f.
19+
func FoldingRange(ctx context.Context, view View, f GoFile) (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+
file, err := f.GetAST(ctx, ParseFull)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
// Get folding ranges for comments separately as they are not walked by ast.Inspect.
28+
ranges = append(ranges, commentsFoldingRange(f.FileSet(), file)...)
29+
30+
visit := func(n ast.Node) bool {
31+
var kind protocol.FoldingRangeKind
32+
var start, end token.Pos
33+
switch n := n.(type) {
34+
case *ast.BlockStmt:
35+
// Fold from position of "{" to position of "}".
36+
start, end = n.Lbrace+1, n.Rbrace
37+
case *ast.CaseClause:
38+
// Fold from position of ":" to end.
39+
start, end = n.Colon+1, n.End()
40+
case *ast.CallExpr:
41+
// Fold from position of "(" to position of ")".
42+
start, end = n.Lparen+1, n.Rparen
43+
case *ast.FieldList:
44+
// Fold from position of opening parenthesis/brace, to position of
45+
// closing parenthesis/brace.
46+
start, end = n.Opening+1, n.Closing
47+
case *ast.GenDecl:
48+
// If this is an import declaration, set the kind to be protocol.Imports.
49+
if n.Tok == token.IMPORT {
50+
kind = protocol.Imports
51+
}
52+
// Fold from position of "(" to position of ")".
53+
start, end = n.Lparen+1, n.Rparen
54+
}
55+
56+
if start.IsValid() && end.IsValid() {
57+
ranges = append(ranges, FoldingRangeInfo{
58+
Range: span.NewRange(f.FileSet(), start, end),
59+
Kind: kind,
60+
})
61+
}
62+
return true
63+
}
64+
65+
// Walk the ast and collect folding ranges.
66+
ast.Inspect(file, visit)
67+
68+
sort.Slice(ranges, func(i, j int) bool {
69+
if ranges[i].Range.Start < ranges[j].Range.Start {
70+
return true
71+
} else if ranges[i].Range.Start > ranges[j].Range.Start {
72+
return false
73+
}
74+
return ranges[i].Range.End < ranges[j].Range.End
75+
})
76+
return ranges, nil
77+
}
78+
79+
// commentsFoldingRange returns the folding ranges for all comment blocks in file.
80+
// The folding range starts at the end of the first comment, and ends at the end of the
81+
// comment block and has kind protocol.Comment.
82+
func commentsFoldingRange(fset *token.FileSet, file *ast.File) []FoldingRangeInfo {
83+
var comments []FoldingRangeInfo
84+
for _, commentGrp := range file.Comments {
85+
// Don't fold single comments.
86+
if len(commentGrp.List) <= 1 {
87+
continue
88+
}
89+
comments = append(comments, FoldingRangeInfo{
90+
// Fold from the end of the first line comment to the end of the comment block.
91+
Range: span.NewRange(fset, commentGrp.List[0].End(), commentGrp.End()),
92+
Kind: protocol.Comment,
93+
})
94+
}
95+
return comments
96+
}
97+
98+
func ToProtocolFoldingRanges(m *protocol.ColumnMapper, ranges []FoldingRangeInfo) ([]protocol.FoldingRange, error) {
99+
var res []protocol.FoldingRange
100+
for _, r := range ranges {
101+
spn, err := r.Range.Span()
102+
if err != nil {
103+
return nil, err
104+
}
105+
rng, err := m.Range(spn)
106+
if err != nil {
107+
return nil, err
108+
}
109+
res = append(res, protocol.FoldingRange{
110+
StartLine: rng.Start.Line,
111+
StartCharacter: rng.Start.Character,
112+
EndLine: rng.End.Line,
113+
EndCharacter: rng.End.Character,
114+
Kind: string(r.Kind),
115+
})
116+
}
117+
return res, nil
118+
}

internal/lsp/source/source_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,89 @@ func summarizeCompletionItems(i int, want []source.CompletionItem, got []source.
257257
return msg.String()
258258
}
259259

260+
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
261+
for _, spn := range data {
262+
uri := spn.URI()
263+
filename := uri.Filename()
264+
265+
f, err := r.view.GetFile(r.ctx, uri)
266+
if err != nil {
267+
t.Fatalf("failed for %v: %v", spn, err)
268+
}
269+
270+
ranges, err := source.FoldingRange(r.ctx, r.view, f.(source.GoFile))
271+
if err != nil {
272+
t.Error(err)
273+
continue
274+
}
275+
data, _, err := f.Handle(r.ctx).Read(r.ctx)
276+
if err != nil {
277+
t.Error(err)
278+
continue
279+
}
280+
// Fold all ranges.
281+
got, err := foldRanges(string(data), ranges)
282+
if err != nil {
283+
t.Error(err)
284+
continue
285+
}
286+
want := string(r.data.Golden("foldingRange", spn.URI().Filename(), func() ([]byte, error) {
287+
return []byte(got), nil
288+
}))
289+
290+
if want != got {
291+
t.Errorf("foldingRanges failed for %s, expected:\n%v\ngot:\n%v", filename, want, got)
292+
}
293+
294+
// Filter by kind.
295+
kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment}
296+
for _, kind := range kinds {
297+
var kindOnly []source.FoldingRangeInfo
298+
for _, fRng := range ranges {
299+
if fRng.Kind == kind {
300+
kindOnly = append(kindOnly, fRng)
301+
}
302+
}
303+
304+
got, err := foldRanges(string(data), kindOnly)
305+
if err != nil {
306+
t.Error(err)
307+
continue
308+
}
309+
want := string(r.data.Golden("foldingRange-"+string(kind), spn.URI().Filename(), func() ([]byte, error) {
310+
return []byte(got), nil
311+
}))
312+
313+
if want != got {
314+
t.Errorf("foldingRanges-%s failed for %s, expected:\n%v\ngot:\n%v", string(kind), filename, want, got)
315+
}
316+
317+
}
318+
319+
}
320+
}
321+
322+
func foldRanges(contents string, ranges []source.FoldingRangeInfo) (string, error) {
323+
// TODO(suzmue): Allow folding ranges to intersect for these tests.
324+
foldedText := "<>"
325+
res := contents
326+
// Apply the folds from the end of the file forward
327+
// to preserve the offsets.
328+
for i := len(ranges) - 1; i >= 0; i-- {
329+
fRange := ranges[i]
330+
spn, err := fRange.Range.Span()
331+
if err != nil {
332+
return "", err
333+
}
334+
start := spn.Start().Offset()
335+
end := spn.End().Offset()
336+
337+
tmp := res[0:start] + foldedText
338+
res = tmp + res[end:]
339+
}
340+
return res, nil
341+
}
342+
260343
func (r *runner) Format(t *testing.T, data tests.Formats) {
261344
ctx := r.ctx
262345
for _, spn := range data {

internal/lsp/testdata/folding/a.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package folding //@fold("package")
2+
3+
import (
4+
_ "fmt"
5+
_ "log"
6+
)
7+
8+
import _ "os"
9+
10+
// bar is a function.
11+
// With a multiline doc comment.
12+
func bar() string {
13+
return `
14+
this string
15+
is not indented`
16+
17+
}

0 commit comments

Comments
 (0)