Skip to content

Commit d72a64a

Browse files
rneatherwayfindleyr
authored andcommitted
gopls/internal/lsp: add selection range request
selectionRange defines the textDocument/selectionRange feature, which, given a list of positions within a file, reports a linked list of enclosing syntactic blocks, innermost first. See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange. This feature can be used by a client to implement "expand selection" in a language-aware fashion. Multiple input positions are supported to allow for multiple cursors, and the entire path up to the whole document is returned for each cursor to avoid multiple round-trips when the user is likely to issue this command multiple times in quick succession. Fixes golang/go#36679 Change-Id: I4852db4b40be24b3dc13e4d9d9238c1a9ac5f824 GitHub-Last-Rev: 0a11741 GitHub-Pull-Request: #416 Reviewed-on: https://go-review.googlesource.com/c/tools/+/452315 Reviewed-by: Alan Donovan <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Robert Findley <rfindley@google.com> Run-TryBot: Robert Findley <rfindley@google.com>
1 parent 18f76ec commit d72a64a

File tree

10 files changed

+204
-2
lines changed

10 files changed

+204
-2
lines changed

gopls/internal/lsp/cmd/test/cmdtest.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) {
118118
// TODO: inlayHints not supported on command line
119119
}
120120

121+
func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {}
122+
121123
func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
122124
rStdout, wStdout, err := os.Pipe()
123125
if err != nil {

gopls/internal/lsp/lsp_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,71 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {
12821282
}
12831283
}
12841284

1285+
func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {
1286+
uri := spn.URI()
1287+
sm, err := r.data.Mapper(uri)
1288+
if err != nil {
1289+
t.Fatal(err)
1290+
}
1291+
loc, err := sm.Location(spn)
1292+
if err != nil {
1293+
t.Error(err)
1294+
}
1295+
1296+
ranges, err := r.server.selectionRange(r.ctx, &protocol.SelectionRangeParams{
1297+
TextDocument: protocol.TextDocumentIdentifier{
1298+
URI: protocol.URIFromSpanURI(uri),
1299+
},
1300+
Positions: []protocol.Position{loc.Range.Start},
1301+
})
1302+
if err != nil {
1303+
t.Fatal(err)
1304+
}
1305+
1306+
sb := &strings.Builder{}
1307+
for i, path := range ranges {
1308+
fmt.Fprintf(sb, "Ranges %d: ", i)
1309+
rng := path
1310+
for {
1311+
s, err := sm.Offset(rng.Range.Start)
1312+
if err != nil {
1313+
t.Error(err)
1314+
}
1315+
e, err := sm.Offset(rng.Range.End)
1316+
if err != nil {
1317+
t.Error(err)
1318+
}
1319+
1320+
var snippet string
1321+
if e-s < 30 {
1322+
snippet = string(sm.Content[s:e])
1323+
} else {
1324+
snippet = string(sm.Content[s:s+15]) + "..." + string(sm.Content[e-15:e])
1325+
}
1326+
1327+
fmt.Fprintf(sb, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n"))
1328+
1329+
if rng.Parent == nil {
1330+
break
1331+
}
1332+
rng = *rng.Parent
1333+
}
1334+
sb.WriteRune('\n')
1335+
}
1336+
got := sb.String()
1337+
1338+
testName := "selectionrange_" + tests.SpanName(spn)
1339+
want := r.data.Golden(t, testName, uri.Filename(), func() ([]byte, error) {
1340+
return []byte(got), nil
1341+
})
1342+
if want == nil {
1343+
t.Fatalf("golden file %q not found", uri.Filename())
1344+
}
1345+
if diff := compare.Text(got, string(want)); diff != "" {
1346+
t.Errorf("%s mismatch\n%s", testName, diff)
1347+
}
1348+
}
1349+
12851350
func TestBytesOffset(t *testing.T) {
12861351
tests := []struct {
12871352
text string

gopls/internal/lsp/selection_range.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2022 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 lsp
6+
7+
import (
8+
"context"
9+
10+
"golang.org/x/tools/go/ast/astutil"
11+
"golang.org/x/tools/gopls/internal/lsp/protocol"
12+
"golang.org/x/tools/gopls/internal/lsp/source"
13+
"golang.org/x/tools/internal/event"
14+
)
15+
16+
// selectionRange defines the textDocument/selectionRange feature,
17+
// which, given a list of positions within a file,
18+
// reports a linked list of enclosing syntactic blocks, innermost first.
19+
//
20+
// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange.
21+
//
22+
// This feature can be used by a client to implement "expand selection" in a
23+
// language-aware fashion. Multiple input positions are supported to allow
24+
// for multiple cursors, and the entire path up to the whole document is
25+
// returned for each cursor to avoid multiple round-trips when the user is
26+
// likely to issue this command multiple times in quick succession.
27+
func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
28+
ctx, done := event.Start(ctx, "lsp.Server.documentSymbol")
29+
defer done()
30+
31+
snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
32+
defer release()
33+
if !ok {
34+
return nil, err
35+
}
36+
37+
pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
result := make([]protocol.SelectionRange, len(params.Positions))
43+
for i, protocolPos := range params.Positions {
44+
pos, err := pgf.Mapper.Pos(protocolPos)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)
50+
51+
tail := &result[i] // tail of the Parent linked list, built head first
52+
53+
for j, node := range path {
54+
rng, err := pgf.Mapper.PosRange(node.Pos(), node.End())
55+
if err != nil {
56+
return nil, err
57+
}
58+
59+
// Add node to tail.
60+
if j > 0 {
61+
tail.Parent = &protocol.SelectionRange{}
62+
tail = tail.Parent
63+
}
64+
tail.Range = rng
65+
}
66+
}
67+
68+
return result, nil
69+
}

gopls/internal/lsp/server_gen.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ func (s *Server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymb
244244
return nil, notImplemented("ResolveWorkspaceSymbol")
245245
}
246246

247-
func (s *Server) SelectionRange(context.Context, *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
248-
return nil, notImplemented("SelectionRange")
247+
func (s *Server) SelectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
248+
return s.selectionRange(ctx, params)
249249
}
250250

251251
func (s *Server) SemanticTokensFull(ctx context.Context, p *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) {

gopls/internal/lsp/source/source_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,10 @@ func (r *runner) SemanticTokens(t *testing.T, spn span.Span) {
497497
t.Skip("nothing to test in source")
498498
}
499499

500+
func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {
501+
t.Skip("nothing to test in source")
502+
}
503+
500504
func (r *runner) Import(t *testing.T, spn span.Span) {
501505
fh, err := r.snapshot.GetFile(r.ctx, spn.URI())
502506
if err != nil {
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package foo
2+
3+
import "time"
4+
5+
func Bar(x, y int, t time.Time) int {
6+
zs := []int{1, 2, 3} //@selectionrange("1")
7+
8+
for _, z := range zs {
9+
x = x + z + y + zs[1] //@selectionrange("1")
10+
}
11+
12+
return x + y //@selectionrange("+")
13+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- selectionrange_foo_12_11 --
2+
Ranges 0:
3+
11:8-11:13 "x + y"
4+
11:1-11:13 "return x + y"
5+
4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}"
6+
4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}"
7+
0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}"
8+
9+
-- selectionrange_foo_6_14 --
10+
Ranges 0:
11+
5:13-5:14 "1"
12+
5:7-5:21 "[]int{1, 2, 3}"
13+
5:1-5:21 "zs := []int{1, 2, 3}"
14+
4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}"
15+
4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}"
16+
0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}"
17+
18+
-- selectionrange_foo_9_22 --
19+
Ranges 0:
20+
8:21-8:22 "1"
21+
8:18-8:23 "zs[1]"
22+
8:6-8:23 "x + z + y + zs[1]"
23+
8:2-8:23 "x = x + z + y + zs[1]"
24+
7:22-9:2 "{\\n\t\tx = x + z +...onrange(\"1\")\\n\t}"
25+
7:1-9:2 "for _, z := ran...onrange(\"1\")\\n\t}"
26+
4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}"
27+
4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}"
28+
0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}"
29+

gopls/internal/lsp/testdata/summary.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ WorkspaceSymbolsCount = 20
2828
SignaturesCount = 33
2929
LinksCount = 7
3030
ImplementationsCount = 14
31+
SelectionRangesCount = 3
3132

gopls/internal/lsp/testdata/summary_go1.18.txt.golden

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ WorkspaceSymbolsCount = 20
2828
SignaturesCount = 33
2929
LinksCount = 7
3030
ImplementationsCount = 14
31+
SelectionRangesCount = 3
3132

gopls/internal/lsp/tests/tests.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ type Signatures = map[span.Span]*protocol.SignatureHelp
9393
type Links = map[span.URI][]Link
9494
type AddImport = map[span.URI]string
9595
type Hovers = map[span.Span]string
96+
type SelectionRanges = []span.Span
9697

9798
type Data struct {
9899
Config packages.Config
@@ -128,6 +129,7 @@ type Data struct {
128129
Links Links
129130
AddImport AddImport
130131
Hovers Hovers
132+
SelectionRanges SelectionRanges
131133

132134
fragments map[string]string
133135
dir string
@@ -177,6 +179,7 @@ type Tests interface {
177179
Link(*testing.T, span.URI, []Link)
178180
AddImport(*testing.T, span.URI, string)
179181
Hover(*testing.T, span.Span, string)
182+
SelectionRanges(*testing.T, span.Span)
180183
}
181184

182185
type Definition struct {
@@ -499,6 +502,7 @@ func load(t testing.TB, mode string, dir string) *Data {
499502
"incomingcalls": datum.collectIncomingCalls,
500503
"outgoingcalls": datum.collectOutgoingCalls,
501504
"addimport": datum.collectAddImports,
505+
"selectionrange": datum.collectSelectionRanges,
502506
}); err != nil {
503507
t.Fatal(err)
504508
}
@@ -947,6 +951,15 @@ func Run(t *testing.T, tests Tests, data *Data) {
947951
}
948952
})
949953

954+
t.Run("SelectionRanges", func(t *testing.T) {
955+
t.Helper()
956+
for _, span := range data.SelectionRanges {
957+
t.Run(SpanName(span), func(t *testing.T) {
958+
tests.SelectionRanges(t, span)
959+
})
960+
}
961+
})
962+
950963
if *UpdateGolden {
951964
for _, golden := range data.golden {
952965
if !golden.Modified {
@@ -1039,6 +1052,7 @@ func checkData(t *testing.T, data *Data) {
10391052
fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures))
10401053
fmt.Fprintf(buf, "LinksCount = %v\n", linksCount)
10411054
fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations))
1055+
fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges))
10421056

10431057
want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) {
10441058
return buf.Bytes(), nil
@@ -1240,6 +1254,10 @@ func (data *Data) collectDefinitions(src, target span.Span) {
12401254
}
12411255
}
12421256

1257+
func (data *Data) collectSelectionRanges(spn span.Span) {
1258+
data.SelectionRanges = append(data.SelectionRanges, spn)
1259+
}
1260+
12431261
func (data *Data) collectImplementations(src span.Span, targets []span.Span) {
12441262
data.Implementations[src] = targets
12451263
}

0 commit comments

Comments
 (0)