Skip to content

Commit

Permalink
gopls/internal/lsp: add selection range request
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
rneatherway authored and findleyr committed Dec 13, 2022
1 parent 18f76ec commit d72a64a
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 2 deletions.
2 changes: 2 additions & 0 deletions gopls/internal/lsp/cmd/test/cmdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func (r *runner) InlayHints(t *testing.T, spn span.Span) {
// TODO: inlayHints not supported on command line
}

func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {}

func (r *runner) runGoplsCmd(t testing.TB, args ...string) (string, string) {
rStdout, wStdout, err := os.Pipe()
if err != nil {
Expand Down
65 changes: 65 additions & 0 deletions gopls/internal/lsp/lsp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,71 @@ func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) {
}
}

func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {
uri := spn.URI()
sm, err := r.data.Mapper(uri)
if err != nil {
t.Fatal(err)
}
loc, err := sm.Location(spn)
if err != nil {
t.Error(err)
}

ranges, err := r.server.selectionRange(r.ctx, &protocol.SelectionRangeParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.URIFromSpanURI(uri),
},
Positions: []protocol.Position{loc.Range.Start},
})
if err != nil {
t.Fatal(err)
}

sb := &strings.Builder{}
for i, path := range ranges {
fmt.Fprintf(sb, "Ranges %d: ", i)
rng := path
for {
s, err := sm.Offset(rng.Range.Start)
if err != nil {
t.Error(err)
}
e, err := sm.Offset(rng.Range.End)
if err != nil {
t.Error(err)
}

var snippet string
if e-s < 30 {
snippet = string(sm.Content[s:e])
} else {
snippet = string(sm.Content[s:s+15]) + "..." + string(sm.Content[e-15:e])
}

fmt.Fprintf(sb, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n"))

if rng.Parent == nil {
break
}
rng = *rng.Parent
}
sb.WriteRune('\n')
}
got := sb.String()

testName := "selectionrange_" + tests.SpanName(spn)
want := r.data.Golden(t, testName, uri.Filename(), func() ([]byte, error) {
return []byte(got), nil
})
if want == nil {
t.Fatalf("golden file %q not found", uri.Filename())
}
if diff := compare.Text(got, string(want)); diff != "" {
t.Errorf("%s mismatch\n%s", testName, diff)
}
}

func TestBytesOffset(t *testing.T) {
tests := []struct {
text string
Expand Down
69 changes: 69 additions & 0 deletions gopls/internal/lsp/selection_range.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2022 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 lsp

import (
"context"

"golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/event"
)

// 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.
func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
ctx, done := event.Start(ctx, "lsp.Server.documentSymbol")
defer done()

snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind)
defer release()
if !ok {
return nil, err
}

pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull)
if err != nil {
return nil, err
}

result := make([]protocol.SelectionRange, len(params.Positions))
for i, protocolPos := range params.Positions {
pos, err := pgf.Mapper.Pos(protocolPos)
if err != nil {
return nil, err
}

path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos)

tail := &result[i] // tail of the Parent linked list, built head first

for j, node := range path {
rng, err := pgf.Mapper.PosRange(node.Pos(), node.End())
if err != nil {
return nil, err
}

// Add node to tail.
if j > 0 {
tail.Parent = &protocol.SelectionRange{}
tail = tail.Parent
}
tail.Range = rng
}
}

return result, nil
}
4 changes: 2 additions & 2 deletions gopls/internal/lsp/server_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,8 @@ func (s *Server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymb
return nil, notImplemented("ResolveWorkspaceSymbol")
}

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

func (s *Server) SemanticTokensFull(ctx context.Context, p *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) {
Expand Down
4 changes: 4 additions & 0 deletions gopls/internal/lsp/source/source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,10 @@ func (r *runner) SemanticTokens(t *testing.T, spn span.Span) {
t.Skip("nothing to test in source")
}

func (r *runner) SelectionRanges(t *testing.T, spn span.Span) {
t.Skip("nothing to test in source")
}

func (r *runner) Import(t *testing.T, spn span.Span) {
fh, err := r.snapshot.GetFile(r.ctx, spn.URI())
if err != nil {
Expand Down
13 changes: 13 additions & 0 deletions gopls/internal/lsp/testdata/selectionrange/foo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package foo

import "time"

func Bar(x, y int, t time.Time) int {
zs := []int{1, 2, 3} //@selectionrange("1")

for _, z := range zs {
x = x + z + y + zs[1] //@selectionrange("1")
}

return x + y //@selectionrange("+")
}
29 changes: 29 additions & 0 deletions gopls/internal/lsp/testdata/selectionrange/foo.go.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- selectionrange_foo_12_11 --
Ranges 0:
11:8-11:13 "x + y"
11:1-11:13 "return x + y"
4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}"
4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}"
0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}"

-- selectionrange_foo_6_14 --
Ranges 0:
5:13-5:14 "1"
5:7-5:21 "[]int{1, 2, 3}"
5:1-5:21 "zs := []int{1, 2, 3}"
4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}"
4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}"
0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}"

-- selectionrange_foo_9_22 --
Ranges 0:
8:21-8:22 "1"
8:18-8:23 "zs[1]"
8:6-8:23 "x + z + y + zs[1]"
8:2-8:23 "x = x + z + y + zs[1]"
7:22-9:2 "{\\n\t\tx = x + z +...onrange(\"1\")\\n\t}"
7:1-9:2 "for _, z := ran...onrange(\"1\")\\n\t}"
4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}"
4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}"
0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}"

1 change: 1 addition & 0 deletions gopls/internal/lsp/testdata/summary.txt.golden
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ WorkspaceSymbolsCount = 20
SignaturesCount = 33
LinksCount = 7
ImplementationsCount = 14
SelectionRangesCount = 3

1 change: 1 addition & 0 deletions gopls/internal/lsp/testdata/summary_go1.18.txt.golden
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ WorkspaceSymbolsCount = 20
SignaturesCount = 33
LinksCount = 7
ImplementationsCount = 14
SelectionRangesCount = 3

18 changes: 18 additions & 0 deletions gopls/internal/lsp/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type Signatures = map[span.Span]*protocol.SignatureHelp
type Links = map[span.URI][]Link
type AddImport = map[span.URI]string
type Hovers = map[span.Span]string
type SelectionRanges = []span.Span

type Data struct {
Config packages.Config
Expand Down Expand Up @@ -128,6 +129,7 @@ type Data struct {
Links Links
AddImport AddImport
Hovers Hovers
SelectionRanges SelectionRanges

fragments map[string]string
dir string
Expand Down Expand Up @@ -177,6 +179,7 @@ type Tests interface {
Link(*testing.T, span.URI, []Link)
AddImport(*testing.T, span.URI, string)
Hover(*testing.T, span.Span, string)
SelectionRanges(*testing.T, span.Span)
}

type Definition struct {
Expand Down Expand Up @@ -499,6 +502,7 @@ func load(t testing.TB, mode string, dir string) *Data {
"incomingcalls": datum.collectIncomingCalls,
"outgoingcalls": datum.collectOutgoingCalls,
"addimport": datum.collectAddImports,
"selectionrange": datum.collectSelectionRanges,
}); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -947,6 +951,15 @@ func Run(t *testing.T, tests Tests, data *Data) {
}
})

t.Run("SelectionRanges", func(t *testing.T) {
t.Helper()
for _, span := range data.SelectionRanges {
t.Run(SpanName(span), func(t *testing.T) {
tests.SelectionRanges(t, span)
})
}
})

if *UpdateGolden {
for _, golden := range data.golden {
if !golden.Modified {
Expand Down Expand Up @@ -1039,6 +1052,7 @@ func checkData(t *testing.T, data *Data) {
fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures))
fmt.Fprintf(buf, "LinksCount = %v\n", linksCount)
fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations))
fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges))

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

func (data *Data) collectSelectionRanges(spn span.Span) {
data.SelectionRanges = append(data.SelectionRanges, spn)
}

func (data *Data) collectImplementations(src span.Span, targets []span.Span) {
data.Implementations[src] = targets
}
Expand Down

0 comments on commit d72a64a

Please sign in to comment.