diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go index 819f1e0793..fb28841caa 100644 --- a/internal/lsp/fake/edit.go +++ b/internal/lsp/fake/edit.go @@ -18,6 +18,13 @@ type Pos struct { Line, Column int } +// Range corresponds to protocol.Range, but uses the editor friend Pos +// instead of UTF-16 oriented protocol.Position +type Range struct { + Start Pos + End Pos +} + func (p Pos) toProtocolPosition() protocol.Position { return protocol.Position{ Line: float64(p.Line), @@ -38,6 +45,21 @@ type Edit struct { Text string } +// Location is the editor friendly equivalent of protocol.Location +type Location struct { + Path string + Range Range +} + +// SymbolInformation is an editor friendly version of +// protocol.SymbolInformation, with location information transformed to byte +// offsets. Field names correspond to the protocol type. +type SymbolInformation struct { + Name string + Kind protocol.SymbolKind + Location Location +} + // NewEdit creates an edit replacing all content between // (startLine, startColumn) and (endLine, endColumn) with text. func NewEdit(startLine, startColumn, endLine, endColumn int, text string) Edit { diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go index 9ae5b84666..14ab7ef3eb 100644 --- a/internal/lsp/fake/editor.go +++ b/internal/lsp/fake/editor.go @@ -526,6 +526,38 @@ func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (stri return newPath, newPos, nil } +// Symbol performs a workspace symbol search using query +func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) { + params := &protocol.WorkspaceSymbolParams{} + params.Query = query + + resp, err := e.server.Symbol(ctx, params) + if err != nil { + return nil, fmt.Errorf("symbol: %w", err) + } + var res []SymbolInformation + for _, si := range resp { + ploc := si.Location + path := e.sandbox.Workdir.URIToPath(ploc.URI) + start := fromProtocolPosition(ploc.Range.Start) + end := fromProtocolPosition(ploc.Range.End) + rnge := Range{ + Start: start, + End: end, + } + loc := Location{ + Path: path, + Range: rnge, + } + res = append(res, SymbolInformation{ + Name: si.Name, + Kind: si.Kind, + Location: loc, + }) + } + return res, nil +} + // OrganizeImports requests and performs the source.organizeImports codeAction. func (e *Editor) OrganizeImports(ctx context.Context, path string) error { return e.codeAction(ctx, path, nil, protocol.SourceOrganizeImports) diff --git a/internal/lsp/regtest/symbol_helper_test.go b/internal/lsp/regtest/symbol_helper_test.go new file mode 100644 index 0000000000..c4ece70e13 --- /dev/null +++ b/internal/lsp/regtest/symbol_helper_test.go @@ -0,0 +1,114 @@ +// Copyright 2020 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 regtest + +import ( + "encoding/json" + "fmt" + + "golang.org/x/tools/internal/lsp/fake" + "golang.org/x/tools/internal/lsp/protocol" +) + +// expSymbolInformation and the types it references are pointer-based versions +// of fake.SymbolInformation, used to make it easier to partially assert +// against values of type fake.SymbolInformation + +// expSymbolInformation is a pointer-based version of fake.SymbolInformation +type expSymbolInformation struct { + Name *string + Kind *protocol.SymbolKind + Location *expLocation +} + +func (e *expSymbolInformation) matchAgainst(sis []fake.SymbolInformation) bool { + for _, si := range sis { + if e.match(si) { + return true + } + } + return false +} + +func (e *expSymbolInformation) match(si fake.SymbolInformation) bool { + if e.Name != nil && *e.Name != si.Name { + return false + } + if e.Kind != nil && *e.Kind != si.Kind { + return false + } + if e.Location != nil && !e.Location.match(si.Location) { + return false + } + return true +} + +func (e *expSymbolInformation) String() string { + byts, err := json.MarshalIndent(e, "", " ") + if err != nil { + panic(fmt.Errorf("failed to json.Marshal *expSymbolInformation: %v", err)) + } + return string(byts) +} + +// expLocation is a pointer-based version of fake.Location +type expLocation struct { + Path *string + Range *expRange +} + +func (e *expLocation) match(l fake.Location) bool { + if e.Path != nil && *e.Path != l.Path { + return false + } + if e.Range != nil && !e.Range.match(l.Range) { + return false + } + return true +} + +// expRange is a pointer-based version of fake.Range +type expRange struct { + Start *expPos + End *expPos +} + +func (e *expRange) match(l fake.Range) bool { + if e.Start != nil && !e.Start.match(l.Start) { + return false + } + if e.End != nil && !e.End.match(l.End) { + return false + } + return true +} + +// expPos is a pointer-based version of fake.Pos +type expPos struct { + Line *int + Column *int +} + +func (e *expPos) match(l fake.Pos) bool { + if e.Line != nil && *e.Line != l.Line { + return false + } + if e.Column != nil && *e.Column != l.Column { + return false + } + return true +} + +func pString(s string) *string { + return &s +} + +func pInt(i int) *int { + return &i +} + +func pKind(k protocol.SymbolKind) *protocol.SymbolKind { + return &k +} diff --git a/internal/lsp/regtest/symbol_test.go b/internal/lsp/regtest/symbol_test.go new file mode 100644 index 0000000000..094c49f80b --- /dev/null +++ b/internal/lsp/regtest/symbol_test.go @@ -0,0 +1,58 @@ +// Copyright 2020 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 regtest + +import ( + "testing" + + "golang.org/x/tools/internal/lsp/fake" +) + +const symbolSetup = ` +-- go.mod -- +module mod.com + +go 1.12 +-- main.go -- +package main + +import "fmt" + +func main() { + fmt.Println(Message) +} +-- const.go -- +package main + +const Message = "Hello World." +` + +// TestSymbolPos tests that, at a basic level, we get the correct position +// information for symbols matches that are returned. +func TestSymbolPos(t *testing.T) { + matcher := "caseSensitive" + opts := []RunOption{ + WithEditorConfig(fake.EditorConfig{SymbolMatcher: &matcher}), + } + + runner.Run(t, symbolSetup, func(t *testing.T, env *Env) { + res := env.Symbol("main") + exp := &expSymbolInformation{ + Name: pString("main"), + Location: &expLocation{ + Path: pString("main.go"), + Range: &expRange{ + Start: &expPos{ + Line: pInt(4), + Column: pInt(5), + }, + }, + }, + } + if !exp.matchAgainst(res) { + t.Fatalf("failed to find match for main function") + } + }, opts...) +} diff --git a/internal/lsp/regtest/wrappers.go b/internal/lsp/regtest/wrappers.go index 4464f43cff..0422f64416 100644 --- a/internal/lsp/regtest/wrappers.go +++ b/internal/lsp/regtest/wrappers.go @@ -106,6 +106,16 @@ func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) { return n, p } +// Symbol returns symbols matching query +func (e *Env) Symbol(query string) []fake.SymbolInformation { + e.T.Helper() + r, err := e.Editor.Symbol(e.Ctx, query) + if err != nil { + e.T.Fatal(err) + } + return r +} + // FormatBuffer formats the editor buffer, calling t.Fatal on any error. func (e *Env) FormatBuffer(name string) { e.T.Helper()