2020-02-02 10:53:30 -07:00
|
|
|
// 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 fake
|
|
|
|
|
|
|
|
import (
|
2020-02-28 12:56:20 -07:00
|
|
|
"bufio"
|
2020-02-02 10:53:30 -07:00
|
|
|
"context"
|
2020-02-28 12:56:20 -07:00
|
|
|
"errors"
|
2020-02-02 10:53:30 -07:00
|
|
|
"fmt"
|
2020-02-28 12:56:20 -07:00
|
|
|
"regexp"
|
2020-02-02 10:53:30 -07:00
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"golang.org/x/tools/internal/jsonrpc2"
|
|
|
|
"golang.org/x/tools/internal/lsp/protocol"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Editor is a fake editor client. It keeps track of client state and can be
|
|
|
|
// used for writing LSP tests.
|
|
|
|
type Editor struct {
|
2020-05-06 20:54:50 -06:00
|
|
|
Config EditorConfig
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
// Server, client, and sandbox are concurrency safe and written only
|
2020-05-06 20:54:50 -06:00
|
|
|
// at construction time, so do not require synchronization.
|
2020-06-05 07:17:05 -06:00
|
|
|
Server protocol.Server
|
|
|
|
serverConn jsonrpc2.Conn
|
|
|
|
client *Client
|
|
|
|
sandbox *Sandbox
|
2020-02-02 10:53:30 -07:00
|
|
|
|
|
|
|
// Since this editor is intended just for testing, we use very coarse
|
|
|
|
// locking.
|
|
|
|
mu sync.Mutex
|
|
|
|
// Editor state.
|
2020-05-07 10:25:33 -06:00
|
|
|
buffers map[string]buffer
|
2020-02-02 10:53:30 -07:00
|
|
|
// Capabilities / Options
|
|
|
|
serverCapabilities protocol.ServerCapabilities
|
|
|
|
}
|
|
|
|
|
|
|
|
type buffer struct {
|
|
|
|
version int
|
|
|
|
path string
|
|
|
|
content []string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b buffer) text() string {
|
|
|
|
return strings.Join(b.content, "\n")
|
|
|
|
}
|
|
|
|
|
2020-05-06 20:54:50 -06:00
|
|
|
// EditorConfig configures the editor's LSP session. This is similar to
|
|
|
|
// source.UserOptions, but we use a separate type here so that we expose only
|
|
|
|
// that configuration which we support.
|
|
|
|
//
|
|
|
|
// The zero value for EditorConfig should correspond to its defaults.
|
|
|
|
type EditorConfig struct {
|
|
|
|
Env []string
|
|
|
|
|
|
|
|
// CodeLens is a map defining whether codelens are enabled, keyed by the
|
|
|
|
// codeLens command. CodeLens which are not present in this map are left in
|
|
|
|
// their default state.
|
|
|
|
CodeLens map[string]bool
|
2020-04-08 15:56:24 -06:00
|
|
|
|
|
|
|
// SymbolMatcher is the config associated with the "symbolMatcher" gopls
|
|
|
|
// config option.
|
|
|
|
SymbolMatcher *string
|
2020-05-06 20:54:50 -06:00
|
|
|
}
|
|
|
|
|
2020-04-28 22:00:52 -06:00
|
|
|
// NewEditor Creates a new Editor.
|
2020-05-06 20:54:50 -06:00
|
|
|
func NewEditor(ws *Sandbox, config EditorConfig) *Editor {
|
2020-04-28 22:00:52 -06:00
|
|
|
return &Editor{
|
|
|
|
buffers: make(map[string]buffer),
|
|
|
|
sandbox: ws,
|
2020-05-06 20:54:50 -06:00
|
|
|
Config: config,
|
2020-04-28 22:00:52 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Connect configures the editor to communicate with an LSP server on conn. It
|
|
|
|
// is not concurrency safe, and should be called at most once, before using the
|
|
|
|
// editor.
|
2020-02-02 10:53:30 -07:00
|
|
|
//
|
2020-04-28 22:00:52 -06:00
|
|
|
// It returns the editor, so that it may be called as follows:
|
|
|
|
// editor, err := NewEditor(s).Connect(ctx, conn)
|
2020-04-29 11:17:22 -06:00
|
|
|
func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
|
2020-06-05 07:17:05 -06:00
|
|
|
e.serverConn = conn
|
2020-05-14 12:02:48 -06:00
|
|
|
e.Server = protocol.ServerDispatcher(conn)
|
2020-05-07 10:25:33 -06:00
|
|
|
e.client = &Client{editor: e, hooks: hooks}
|
2020-04-29 06:51:37 -06:00
|
|
|
conn.Go(ctx,
|
2020-04-02 10:43:08 -06:00
|
|
|
protocol.Handlers(
|
|
|
|
protocol.ClientHandler(e.client,
|
|
|
|
jsonrpc2.MethodNotFound)))
|
2020-02-02 10:53:30 -07:00
|
|
|
if err := e.initialize(ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-04-28 22:00:52 -06:00
|
|
|
e.sandbox.Workdir.AddWatcher(e.onFileChanges)
|
2020-02-02 10:53:30 -07:00
|
|
|
return e, nil
|
|
|
|
}
|
|
|
|
|
2020-02-18 18:53:06 -07:00
|
|
|
// Shutdown issues the 'shutdown' LSP notification.
|
|
|
|
func (e *Editor) Shutdown(ctx context.Context) error {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
if err := e.Server.Shutdown(ctx); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("Shutdown: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
2020-02-18 18:53:06 -07:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Exit issues the 'exit' LSP notification.
|
|
|
|
func (e *Editor) Exit(ctx context.Context) error {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
2020-02-02 10:53:30 -07:00
|
|
|
// Not all LSP clients issue the exit RPC, but we do so here to ensure that
|
|
|
|
// we gracefully handle it on multi-session servers.
|
2020-05-14 12:02:48 -06:00
|
|
|
if err := e.Server.Exit(ctx); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("Exit: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-05 07:17:05 -06:00
|
|
|
// Close issues the shutdown and exit sequence an editor should.
|
|
|
|
func (e *Editor) Close(ctx context.Context) error {
|
|
|
|
if err := e.Shutdown(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := e.Exit(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
// called close on the editor should result in the connection closing
|
|
|
|
select {
|
|
|
|
case <-e.serverConn.Done():
|
|
|
|
// connection closed itself
|
|
|
|
return nil
|
|
|
|
case <-ctx.Done():
|
|
|
|
return fmt.Errorf("connection not closed: %w", ctx.Err())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-02 10:53:30 -07:00
|
|
|
// Client returns the LSP client for this editor.
|
|
|
|
func (e *Editor) Client() *Client {
|
|
|
|
return e.client
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Editor) configuration() map[string]interface{} {
|
2020-05-06 20:54:50 -06:00
|
|
|
config := map[string]interface{}{
|
|
|
|
"verboseWorkDoneProgress": true,
|
|
|
|
}
|
|
|
|
|
|
|
|
envvars := e.sandbox.GoEnv()
|
|
|
|
envvars = append(envvars, e.Config.Env...)
|
2020-04-14 13:35:57 -06:00
|
|
|
env := map[string]interface{}{}
|
2020-05-06 20:54:50 -06:00
|
|
|
for _, value := range envvars {
|
2020-04-14 13:35:57 -06:00
|
|
|
kv := strings.SplitN(value, "=", 2)
|
|
|
|
env[kv[0]] = kv[1]
|
|
|
|
}
|
2020-05-06 20:54:50 -06:00
|
|
|
config["env"] = env
|
|
|
|
|
|
|
|
if e.Config.CodeLens != nil {
|
|
|
|
config["codelens"] = e.Config.CodeLens
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
2020-05-06 20:54:50 -06:00
|
|
|
|
2020-04-08 15:56:24 -06:00
|
|
|
if e.Config.SymbolMatcher != nil {
|
|
|
|
config["symbolMatcher"] = *e.Config.SymbolMatcher
|
|
|
|
}
|
|
|
|
|
2020-05-06 20:54:50 -06:00
|
|
|
return config
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Editor) initialize(ctx context.Context) error {
|
|
|
|
params := &protocol.ParamInitialize{}
|
|
|
|
params.ClientInfo.Name = "fakeclient"
|
|
|
|
params.ClientInfo.Version = "v1.0.0"
|
2020-04-28 22:00:52 -06:00
|
|
|
params.RootURI = e.sandbox.Workdir.RootURI()
|
2020-04-14 13:35:57 -06:00
|
|
|
params.Capabilities.Workspace.Configuration = true
|
2020-04-22 11:18:47 -06:00
|
|
|
params.Capabilities.Window.WorkDoneProgress = true
|
2020-04-14 13:35:57 -06:00
|
|
|
// TODO: set client capabilities
|
2020-04-22 11:18:47 -06:00
|
|
|
params.InitializationOptions = e.configuration()
|
2020-02-02 10:53:30 -07:00
|
|
|
|
|
|
|
params.Trace = "messages"
|
|
|
|
// TODO: support workspace folders.
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
resp, err := e.Server.Initialize(ctx, params)
|
2020-02-02 10:53:30 -07:00
|
|
|
if err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("initialize: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
e.mu.Lock()
|
|
|
|
e.serverCapabilities = resp.Capabilities
|
|
|
|
e.mu.Unlock()
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("initialized: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
2020-04-14 13:35:57 -06:00
|
|
|
// TODO: await initial configuration here, or expect gopls to manage that?
|
2020-02-02 10:53:30 -07:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server == nil {
|
2020-02-02 10:53:30 -07:00
|
|
|
return
|
|
|
|
}
|
|
|
|
var lspevts []protocol.FileEvent
|
|
|
|
for _, evt := range evts {
|
|
|
|
lspevts = append(lspevts, evt.ProtocolEvent)
|
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
|
2020-02-02 10:53:30 -07:00
|
|
|
Changes: lspevts,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-04-28 22:00:52 -06:00
|
|
|
// OpenFile creates a buffer for the given workdir-relative file.
|
2020-02-02 10:53:30 -07:00
|
|
|
func (e *Editor) OpenFile(ctx context.Context, path string) error {
|
2020-04-28 22:00:52 -06:00
|
|
|
content, err := e.sandbox.Workdir.ReadFile(path)
|
2020-02-02 10:53:30 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
buf := newBuffer(path, content)
|
|
|
|
e.mu.Lock()
|
|
|
|
e.buffers[path] = buf
|
2020-04-28 22:00:52 -06:00
|
|
|
item := textDocumentItem(e.sandbox.Workdir, buf)
|
2020-02-02 10:53:30 -07:00
|
|
|
e.mu.Unlock()
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
|
2020-02-02 10:53:30 -07:00
|
|
|
TextDocument: item,
|
|
|
|
}); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("DidOpen: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func newBuffer(path, content string) buffer {
|
|
|
|
return buffer{
|
|
|
|
version: 1,
|
|
|
|
path: path,
|
|
|
|
content: strings.Split(content, "\n"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 22:00:52 -06:00
|
|
|
func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
|
|
|
|
uri := wd.URI(buf.path)
|
2020-02-02 10:53:30 -07:00
|
|
|
languageID := ""
|
|
|
|
if strings.HasSuffix(buf.path, ".go") {
|
|
|
|
// TODO: what about go.mod files? What is their language ID?
|
|
|
|
languageID = "go"
|
|
|
|
}
|
|
|
|
return protocol.TextDocumentItem{
|
|
|
|
URI: uri,
|
|
|
|
LanguageID: languageID,
|
|
|
|
Version: float64(buf.version),
|
|
|
|
Text: buf.text(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-28 22:00:52 -06:00
|
|
|
// CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
|
|
|
|
// containing the given textual content.
|
2020-02-02 10:53:30 -07:00
|
|
|
func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
|
|
|
|
buf := newBuffer(path, content)
|
|
|
|
e.mu.Lock()
|
|
|
|
e.buffers[path] = buf
|
2020-04-28 22:00:52 -06:00
|
|
|
item := textDocumentItem(e.sandbox.Workdir, buf)
|
2020-02-02 10:53:30 -07:00
|
|
|
e.mu.Unlock()
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
|
2020-02-02 10:53:30 -07:00
|
|
|
TextDocument: item,
|
|
|
|
}); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("DidOpen: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CloseBuffer removes the current buffer (regardless of whether it is saved).
|
|
|
|
func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
|
|
|
|
e.mu.Lock()
|
|
|
|
_, ok := e.buffers[path]
|
|
|
|
if !ok {
|
|
|
|
e.mu.Unlock()
|
2020-02-28 12:56:20 -07:00
|
|
|
return ErrUnknownBuffer
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
delete(e.buffers, path)
|
|
|
|
e.mu.Unlock()
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
|
2020-05-06 20:54:50 -06:00
|
|
|
TextDocument: e.textDocumentIdentifier(path),
|
2020-02-02 10:53:30 -07:00
|
|
|
}); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("DidClose: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-05-06 20:54:50 -06:00
|
|
|
func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
|
|
|
|
return protocol.TextDocumentIdentifier{
|
|
|
|
URI: e.sandbox.Workdir.URI(path),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-27 11:35:46 -07:00
|
|
|
// SaveBuffer writes the content of the buffer specified by the given path to
|
2020-02-02 10:53:30 -07:00
|
|
|
// the filesystem.
|
2020-02-27 11:35:46 -07:00
|
|
|
func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
|
2020-02-27 15:20:53 -07:00
|
|
|
if err := e.OrganizeImports(ctx, path); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("organizing imports before save: %w", err)
|
2020-02-27 15:20:53 -07:00
|
|
|
}
|
|
|
|
if err := e.FormatBuffer(ctx, path); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("formatting before save: %w", err)
|
2020-02-27 15:20:53 -07:00
|
|
|
}
|
2020-05-08 16:19:30 -06:00
|
|
|
return e.SaveBufferWithoutActions(ctx, path)
|
|
|
|
}
|
2020-02-27 15:20:53 -07:00
|
|
|
|
2020-05-08 16:19:30 -06:00
|
|
|
func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error {
|
2020-02-02 10:53:30 -07:00
|
|
|
e.mu.Lock()
|
|
|
|
buf, ok := e.buffers[path]
|
|
|
|
if !ok {
|
|
|
|
e.mu.Unlock()
|
|
|
|
return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
|
|
|
|
}
|
|
|
|
content := buf.text()
|
|
|
|
includeText := false
|
|
|
|
syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
|
|
|
|
if ok {
|
|
|
|
includeText = syncOptions.Save.IncludeText
|
|
|
|
}
|
|
|
|
e.mu.Unlock()
|
|
|
|
|
2020-05-06 20:54:50 -06:00
|
|
|
docID := e.textDocumentIdentifier(buf.path)
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
|
2020-02-02 10:53:30 -07:00
|
|
|
TextDocument: docID,
|
|
|
|
Reason: protocol.Manual,
|
|
|
|
}); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("WillSave: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
2020-04-28 22:00:52 -06:00
|
|
|
if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("writing %q: %w", path, err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
2020-02-02 10:53:30 -07:00
|
|
|
params := &protocol.DidSaveTextDocumentParams{
|
|
|
|
TextDocument: protocol.VersionedTextDocumentIdentifier{
|
|
|
|
Version: float64(buf.version),
|
|
|
|
TextDocumentIdentifier: docID,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if includeText {
|
|
|
|
params.Text = &content
|
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
if err := e.Server.DidSave(ctx, params); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("DidSave: %w", err)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-02-28 12:56:20 -07:00
|
|
|
// contentPosition returns the (Line, Column) position corresponding to offset
|
|
|
|
// in the buffer referenced by path.
|
|
|
|
func contentPosition(content string, offset int) (Pos, error) {
|
|
|
|
scanner := bufio.NewScanner(strings.NewReader(content))
|
|
|
|
start := 0
|
|
|
|
line := 0
|
|
|
|
for scanner.Scan() {
|
|
|
|
end := start + len([]rune(scanner.Text())) + 1
|
|
|
|
if offset < end {
|
|
|
|
return Pos{Line: line, Column: offset - start}, nil
|
|
|
|
}
|
|
|
|
start = end
|
|
|
|
line++
|
|
|
|
}
|
|
|
|
if err := scanner.Err(); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return Pos{}, fmt.Errorf("scanning content: %w", err)
|
2020-02-28 12:56:20 -07:00
|
|
|
}
|
|
|
|
// Scan() will drop the last line if it is empty. Correct for this.
|
|
|
|
if strings.HasSuffix(content, "\n") && offset == start {
|
|
|
|
return Pos{Line: line, Column: 0}, nil
|
|
|
|
}
|
|
|
|
return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ErrNoMatch is returned if a regexp search fails.
|
|
|
|
var (
|
|
|
|
ErrNoMatch = errors.New("no match")
|
|
|
|
ErrUnknownBuffer = errors.New("unknown buffer")
|
|
|
|
)
|
|
|
|
|
|
|
|
// regexpRange returns the start and end of the first occurrence of either re
|
|
|
|
// or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match.
|
|
|
|
func regexpRange(content, re string) (Pos, Pos, error) {
|
|
|
|
var start, end int
|
|
|
|
rec, err := regexp.Compile(re)
|
|
|
|
if err != nil {
|
|
|
|
return Pos{}, Pos{}, err
|
|
|
|
}
|
|
|
|
indexes := rec.FindStringSubmatchIndex(content)
|
|
|
|
if indexes == nil {
|
|
|
|
return Pos{}, Pos{}, ErrNoMatch
|
|
|
|
}
|
|
|
|
switch len(indexes) {
|
|
|
|
case 2:
|
|
|
|
// no subgroups: return the range of the regexp expression
|
|
|
|
start, end = indexes[0], indexes[1]
|
|
|
|
case 4:
|
|
|
|
// one subgroup: return its range
|
|
|
|
start, end = indexes[2], indexes[3]
|
|
|
|
default:
|
|
|
|
return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1)
|
|
|
|
}
|
|
|
|
startPos, err := contentPosition(content, start)
|
|
|
|
if err != nil {
|
|
|
|
return Pos{}, Pos{}, err
|
|
|
|
}
|
|
|
|
endPos, err := contentPosition(content, end)
|
|
|
|
if err != nil {
|
|
|
|
return Pos{}, Pos{}, err
|
|
|
|
}
|
|
|
|
return startPos, endPos, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RegexpSearch returns the position of the first match for re in the buffer
|
|
|
|
// bufName. For convenience, RegexpSearch supports the following two modes:
|
|
|
|
// 1. If re has no subgroups, return the position of the match for re itself.
|
|
|
|
// 2. If re has one subgroup, return the position of the first subgroup.
|
|
|
|
// It returns an error re is invalid, has more than one subgroup, or doesn't
|
|
|
|
// match the buffer.
|
|
|
|
func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
buf, ok := e.buffers[bufName]
|
|
|
|
if !ok {
|
|
|
|
return Pos{}, ErrUnknownBuffer
|
|
|
|
}
|
|
|
|
start, _, err := regexpRange(buf.text(), re)
|
|
|
|
return start, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// RegexpReplace edits the buffer corresponding to path by replacing the first
|
|
|
|
// instance of re, or its first subgroup, with the replace text. See
|
|
|
|
// RegexpSearch for more explanation of these two modes.
|
|
|
|
// It returns an error if re is invalid, has more than one subgroup, or doesn't
|
|
|
|
// match the buffer.
|
|
|
|
func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
buf, ok := e.buffers[path]
|
|
|
|
if !ok {
|
|
|
|
return ErrUnknownBuffer
|
|
|
|
}
|
|
|
|
content := buf.text()
|
|
|
|
start, end, err := regexpRange(content, re)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return e.editBufferLocked(ctx, path, []Edit{{
|
|
|
|
Start: start,
|
|
|
|
End: end,
|
|
|
|
Text: replace,
|
|
|
|
}})
|
|
|
|
}
|
|
|
|
|
2020-02-02 10:53:30 -07:00
|
|
|
// EditBuffer applies the given test edits to the buffer identified by path.
|
|
|
|
func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
|
2020-02-27 15:20:53 -07:00
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
return e.editBufferLocked(ctx, path, edits)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
|
2020-02-27 15:20:53 -07:00
|
|
|
// BufferText returns the content of the buffer with the given name.
|
|
|
|
func (e *Editor) BufferText(name string) string {
|
2020-02-02 10:53:30 -07:00
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
2020-02-27 15:20:53 -07:00
|
|
|
return e.buffers[name].text()
|
|
|
|
}
|
|
|
|
|
internal/lsp/regtest: add functions to make diagnostic assertions easier
One of the tricky things about asserting on conditions in regtests is
the asynchronous nature of LSP. For example, as the LSP client we cannot
be sure when we've received all diagnostics for a given file.
Currently, regtests are implemented by awaiting specific diagnostic
expectations. This means that if gopls generates diagnostics that do
not match those expectations, we can only time out the test.
Ideally, we would want to know that gopls is done generating all diagnostics
for the current file state. This is not possible without knowing the
status of diagnostics for. Barring this, we would want to know that
diagnostics are done for the current file version. Unfortunately, that
also is not possible, because a new version of file B can affect
diagnostics in file A.
So in lieu of this information, this CL exposes a few tools that can be
used to improve the experience of writing new regtests.
- A new expectation is added: AnyDiagnosticAtCurrentVersion, that is
satisfied if any diagnostics have been received for the current
buffer version.
- ExpectDiagnostics is added to Env, to help check whether the current
diagnostics matches expectations.
Updates golang/go#38113
Change-Id: I48d2c3db87c13ac3ab424d01d9444cbc285af9e1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/226842
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
2020-04-01 12:56:48 -06:00
|
|
|
// BufferVersion returns the current version of the buffer corresponding to
|
|
|
|
// name (or 0 if it is not being edited).
|
|
|
|
func (e *Editor) BufferVersion(name string) int {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
return e.buffers[name].version
|
|
|
|
}
|
|
|
|
|
2020-02-27 15:20:53 -07:00
|
|
|
func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error {
|
2020-02-02 10:53:30 -07:00
|
|
|
buf, ok := e.buffers[path]
|
|
|
|
if !ok {
|
2020-02-27 15:20:53 -07:00
|
|
|
return fmt.Errorf("unknown buffer %q", path)
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
var (
|
|
|
|
content = make([]string, len(buf.content))
|
|
|
|
err error
|
|
|
|
evts []protocol.TextDocumentContentChangeEvent
|
|
|
|
)
|
|
|
|
copy(content, buf.content)
|
2020-02-27 15:20:53 -07:00
|
|
|
content, err = editContent(content, edits)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
2020-02-27 15:20:53 -07:00
|
|
|
|
2020-02-02 10:53:30 -07:00
|
|
|
buf.content = content
|
|
|
|
buf.version++
|
|
|
|
e.buffers[path] = buf
|
2020-02-27 15:20:53 -07:00
|
|
|
// A simple heuristic: if there is only one edit, send it incrementally.
|
|
|
|
// Otherwise, send the entire content.
|
|
|
|
if len(edits) == 1 {
|
|
|
|
evts = append(evts, edits[0].toProtocolChangeEvent())
|
|
|
|
} else {
|
|
|
|
evts = append(evts, protocol.TextDocumentContentChangeEvent{
|
|
|
|
Text: buf.text(),
|
|
|
|
})
|
|
|
|
}
|
2020-02-02 10:53:30 -07:00
|
|
|
params := &protocol.DidChangeTextDocumentParams{
|
|
|
|
TextDocument: protocol.VersionedTextDocumentIdentifier{
|
2020-05-06 20:54:50 -06:00
|
|
|
Version: float64(buf.version),
|
|
|
|
TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
|
2020-02-02 10:53:30 -07:00
|
|
|
},
|
|
|
|
ContentChanges: evts,
|
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server != nil {
|
|
|
|
if err := e.Server.DidChange(ctx, params); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("DidChange: %w", err)
|
2020-02-27 15:20:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2020-02-02 10:53:30 -07:00
|
|
|
}
|
|
|
|
|
2020-02-06 14:46:55 -07:00
|
|
|
// GoToDefinition jumps to the definition of the symbol at the given position
|
|
|
|
// in an open buffer.
|
|
|
|
func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
|
|
|
|
if err := e.checkBufferPosition(path, pos); err != nil {
|
|
|
|
return "", Pos{}, err
|
|
|
|
}
|
|
|
|
params := &protocol.DefinitionParams{}
|
2020-04-28 22:00:52 -06:00
|
|
|
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
|
2020-02-06 14:46:55 -07:00
|
|
|
params.Position = pos.toProtocolPosition()
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
resp, err := e.Server.Definition(ctx, params)
|
2020-02-06 14:46:55 -07:00
|
|
|
if err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return "", Pos{}, fmt.Errorf("definition: %w", err)
|
2020-02-06 14:46:55 -07:00
|
|
|
}
|
|
|
|
if len(resp) == 0 {
|
|
|
|
return "", Pos{}, nil
|
|
|
|
}
|
2020-04-28 22:00:52 -06:00
|
|
|
newPath := e.sandbox.Workdir.URIToPath(resp[0].URI)
|
2020-02-06 14:46:55 -07:00
|
|
|
newPos := fromProtocolPosition(resp[0].Range.Start)
|
2020-02-25 09:00:37 -07:00
|
|
|
if err := e.OpenFile(ctx, newPath); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return "", Pos{}, fmt.Errorf("OpenFile: %w", err)
|
2020-02-25 09:00:37 -07:00
|
|
|
}
|
2020-02-06 14:46:55 -07:00
|
|
|
return newPath, newPos, nil
|
|
|
|
}
|
|
|
|
|
2020-04-10 07:38:38 -06:00
|
|
|
// 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
|
|
|
|
|
2020-05-14 12:02:48 -06:00
|
|
|
resp, err := e.Server.Symbol(ctx, params)
|
2020-04-10 07:38:38 -06:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-02-27 15:20:53 -07:00
|
|
|
// OrganizeImports requests and performs the source.organizeImports codeAction.
|
|
|
|
func (e *Editor) OrganizeImports(ctx context.Context, path string) error {
|
2020-04-01 19:31:43 -06:00
|
|
|
return e.codeAction(ctx, path, nil, protocol.SourceOrganizeImports)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ApplyQuickFixes requests and performs the quickfix codeAction.
|
|
|
|
func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, diagnostics []protocol.Diagnostic) error {
|
2020-04-29 15:49:22 -06:00
|
|
|
return e.codeAction(ctx, path, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
|
2020-04-01 19:31:43 -06:00
|
|
|
}
|
|
|
|
|
2020-04-29 15:49:22 -06:00
|
|
|
func (e *Editor) codeAction(ctx context.Context, path string, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server == nil {
|
2020-02-27 15:20:53 -07:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
params := &protocol.CodeActionParams{}
|
2020-04-28 22:00:52 -06:00
|
|
|
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
|
2020-04-29 15:49:22 -06:00
|
|
|
params.Context.Only = only
|
2020-04-01 19:31:43 -06:00
|
|
|
if diagnostics != nil {
|
|
|
|
params.Context.Diagnostics = diagnostics
|
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
actions, err := e.Server.CodeAction(ctx, params)
|
2020-02-27 15:20:53 -07:00
|
|
|
if err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("textDocument/codeAction: %w", err)
|
2020-02-27 15:20:53 -07:00
|
|
|
}
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
for _, action := range actions {
|
2020-04-29 15:49:22 -06:00
|
|
|
var match bool
|
|
|
|
for _, o := range only {
|
|
|
|
if action.Kind == o {
|
|
|
|
match = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !match {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
for _, change := range action.Edit.DocumentChanges {
|
2020-04-28 22:00:52 -06:00
|
|
|
path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
|
2020-04-29 15:49:22 -06:00
|
|
|
if float64(e.buffers[path].version) != change.TextDocument.Version {
|
|
|
|
// Skip edits for old versions.
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
edits := convertEdits(change.Edits)
|
|
|
|
if err := e.editBufferLocked(ctx, path, edits); err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("editing buffer %q: %w", path, err)
|
2020-02-27 15:20:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func convertEdits(protocolEdits []protocol.TextEdit) []Edit {
|
|
|
|
var edits []Edit
|
|
|
|
for _, lspEdit := range protocolEdits {
|
|
|
|
edits = append(edits, fromProtocolTextEdit(lspEdit))
|
|
|
|
}
|
|
|
|
return edits
|
|
|
|
}
|
|
|
|
|
|
|
|
// FormatBuffer gofmts a Go file.
|
|
|
|
func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server == nil {
|
2020-02-27 15:20:53 -07:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
e.mu.Lock()
|
2020-04-14 14:37:58 -06:00
|
|
|
version := e.buffers[path].version
|
|
|
|
e.mu.Unlock()
|
2020-02-27 15:20:53 -07:00
|
|
|
params := &protocol.DocumentFormattingParams{}
|
2020-04-28 22:00:52 -06:00
|
|
|
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
|
2020-05-14 12:02:48 -06:00
|
|
|
resp, err := e.Server.Formatting(ctx, params)
|
2020-02-27 15:20:53 -07:00
|
|
|
if err != nil {
|
2020-05-01 08:05:39 -06:00
|
|
|
return fmt.Errorf("textDocument/formatting: %w", err)
|
2020-02-27 15:20:53 -07:00
|
|
|
}
|
2020-04-14 14:37:58 -06:00
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
if versionAfter := e.buffers[path].version; versionAfter != version {
|
|
|
|
return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter)
|
|
|
|
}
|
2020-02-27 15:20:53 -07:00
|
|
|
edits := convertEdits(resp)
|
|
|
|
return e.editBufferLocked(ctx, path, edits)
|
|
|
|
}
|
|
|
|
|
2020-02-06 14:46:55 -07:00
|
|
|
func (e *Editor) checkBufferPosition(path string, pos Pos) error {
|
|
|
|
e.mu.Lock()
|
|
|
|
defer e.mu.Unlock()
|
|
|
|
buf, ok := e.buffers[path]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("buffer %q is not open", path)
|
|
|
|
}
|
|
|
|
if !inText(pos, buf.content) {
|
|
|
|
return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2020-04-27 15:00:34 -06:00
|
|
|
|
|
|
|
// RunGenerate runs `go generate` non-recursively in the workdir-relative dir
|
|
|
|
// path. It does not report any resulting file changes as a watched file
|
2020-04-28 22:00:52 -06:00
|
|
|
// change, so must be followed by a call to Workdir.CheckForFileChanges once
|
2020-04-27 15:00:34 -06:00
|
|
|
// the generate command has completed.
|
|
|
|
func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server == nil {
|
2020-04-27 15:00:34 -06:00
|
|
|
return nil
|
|
|
|
}
|
2020-04-28 22:00:52 -06:00
|
|
|
absDir := e.sandbox.Workdir.filePath(dir)
|
2020-04-27 15:00:34 -06:00
|
|
|
params := &protocol.ExecuteCommandParams{
|
|
|
|
Command: "generate",
|
|
|
|
Arguments: []interface{}{absDir, false},
|
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
if _, err := e.Server.ExecuteCommand(ctx, params); err != nil {
|
2020-04-27 15:00:34 -06:00
|
|
|
return fmt.Errorf("running generate: %v", err)
|
|
|
|
}
|
2020-04-28 22:00:52 -06:00
|
|
|
// Unfortunately we can't simply poll the workdir for file changes here,
|
2020-04-27 15:00:34 -06:00
|
|
|
// because server-side command may not have completed. In regtests, we can
|
|
|
|
// Await this state change, but here we must delegate that responsibility to
|
|
|
|
// the caller.
|
|
|
|
return nil
|
|
|
|
}
|
2020-05-06 20:54:50 -06:00
|
|
|
|
|
|
|
// CodeLens execute a codelens request on the server.
|
|
|
|
func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
|
2020-05-14 12:02:48 -06:00
|
|
|
if e.Server == nil {
|
2020-05-06 20:54:50 -06:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
e.mu.Lock()
|
|
|
|
_, ok := e.buffers[path]
|
|
|
|
e.mu.Unlock()
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("buffer %q is not open", path)
|
|
|
|
}
|
|
|
|
params := &protocol.CodeLensParams{
|
|
|
|
TextDocument: e.textDocumentIdentifier(path),
|
|
|
|
}
|
2020-05-14 12:02:48 -06:00
|
|
|
lens, err := e.Server.CodeLens(ctx, params)
|
2020-05-06 20:54:50 -06:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return lens, nil
|
|
|
|
}
|
2020-05-09 06:54:23 -06:00
|
|
|
|
|
|
|
// CodeAction executes a codeAction request on the server.
|
|
|
|
func (e *Editor) CodeAction(ctx context.Context, path string) ([]protocol.CodeAction, error) {
|
2020-05-18 19:28:00 -06:00
|
|
|
if e.Server == nil {
|
2020-05-09 06:54:23 -06:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
e.mu.Lock()
|
|
|
|
_, ok := e.buffers[path]
|
|
|
|
e.mu.Unlock()
|
|
|
|
if !ok {
|
|
|
|
return nil, fmt.Errorf("buffer %q is not open", path)
|
|
|
|
}
|
|
|
|
params := &protocol.CodeActionParams{
|
|
|
|
TextDocument: e.textDocumentIdentifier(path),
|
|
|
|
}
|
2020-05-18 19:28:00 -06:00
|
|
|
lens, err := e.Server.CodeAction(ctx, params)
|
2020-05-09 06:54:23 -06:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return lens, nil
|
|
|
|
}
|
2020-05-08 11:42:14 -06:00
|
|
|
|
|
|
|
// Hover triggers a hover at the given position in an open buffer.
|
|
|
|
func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) {
|
|
|
|
if err := e.checkBufferPosition(path, pos); err != nil {
|
|
|
|
return nil, Pos{}, err
|
|
|
|
}
|
|
|
|
params := &protocol.HoverParams{}
|
|
|
|
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
|
|
|
|
params.Position = pos.toProtocolPosition()
|
|
|
|
|
|
|
|
resp, err := e.Server.Hover(ctx, params)
|
|
|
|
if err != nil {
|
|
|
|
return nil, Pos{}, fmt.Errorf("hover: %w", err)
|
|
|
|
}
|
|
|
|
if resp == nil {
|
|
|
|
return nil, Pos{}, nil
|
|
|
|
}
|
|
|
|
return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
|
|
|
|
}
|