Skip to content

gopls/internal/lsp: add selection range request #416

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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