github.com/jd-ly/tools@v0.5.7/internal/lsp/fake/editor.go (about)

     1  // Copyright 2020 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 fake
     6  
     7  import (
     8  	"bufio"
     9  	"context"
    10  	"fmt"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strings"
    15  	"sync"
    16  
    17  	"github.com/jd-ly/tools/internal/jsonrpc2"
    18  	"github.com/jd-ly/tools/internal/lsp/protocol"
    19  	"github.com/jd-ly/tools/internal/lsp/source"
    20  	"github.com/jd-ly/tools/internal/span"
    21  	errors "golang.org/x/xerrors"
    22  )
    23  
    24  // Editor is a fake editor client.  It keeps track of client state and can be
    25  // used for writing LSP tests.
    26  type Editor struct {
    27  	Config EditorConfig
    28  
    29  	// Server, client, and sandbox are concurrency safe and written only
    30  	// at construction time, so do not require synchronization.
    31  	Server     protocol.Server
    32  	serverConn jsonrpc2.Conn
    33  	client     *Client
    34  	sandbox    *Sandbox
    35  	defaultEnv map[string]string
    36  
    37  	// Since this editor is intended just for testing, we use very coarse
    38  	// locking.
    39  	mu sync.Mutex
    40  	// Editor state.
    41  	buffers map[string]buffer
    42  	// Capabilities / Options
    43  	serverCapabilities protocol.ServerCapabilities
    44  }
    45  
    46  type buffer struct {
    47  	version int
    48  	path    string
    49  	content []string
    50  	dirty   bool
    51  }
    52  
    53  func (b buffer) text() string {
    54  	return strings.Join(b.content, "\n")
    55  }
    56  
    57  // EditorConfig configures the editor's LSP session. This is similar to
    58  // source.UserOptions, but we use a separate type here so that we expose only
    59  // that configuration which we support.
    60  //
    61  // The zero value for EditorConfig should correspond to its defaults.
    62  type EditorConfig struct {
    63  	Env        map[string]string
    64  	BuildFlags []string
    65  
    66  	// CodeLenses is a map defining whether codelens are enabled, keyed by the
    67  	// codeLens command. CodeLenses which are not present in this map are left in
    68  	// their default state.
    69  	CodeLenses map[string]bool
    70  
    71  	// SymbolMatcher is the config associated with the "symbolMatcher" gopls
    72  	// config option.
    73  	SymbolMatcher, SymbolStyle *string
    74  
    75  	// LimitWorkspaceScope is true if the user does not want to expand their
    76  	// workspace scope to the entire module.
    77  	LimitWorkspaceScope bool
    78  
    79  	// WithoutWorkspaceFolders is used to simulate opening a single file in the
    80  	// editor, without a workspace root. In that case, the client sends neither
    81  	// workspace folders nor a root URI.
    82  	WithoutWorkspaceFolders bool
    83  
    84  	// WorkspaceRoot specifies the root path of the workspace folder used when
    85  	// initializing gopls in the sandbox. If empty, the Workdir is used.
    86  	WorkspaceRoot string
    87  
    88  	// EnableStaticcheck enables staticcheck analyzers.
    89  	EnableStaticcheck bool
    90  
    91  	// AllExperiments sets the "allExperiments" configuration, which enables
    92  	// all of gopls's opt-in settings.
    93  	AllExperiments bool
    94  
    95  	// Whether to send the current process ID, for testing data that is joined to
    96  	// the PID. This can only be set by one test.
    97  	SendPID bool
    98  
    99  	VerboseOutput bool
   100  }
   101  
   102  // NewEditor Creates a new Editor.
   103  func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
   104  	return &Editor{
   105  		buffers:    make(map[string]buffer),
   106  		sandbox:    sandbox,
   107  		defaultEnv: sandbox.GoEnv(),
   108  		Config:     config,
   109  	}
   110  }
   111  
   112  // Connect configures the editor to communicate with an LSP server on conn. It
   113  // is not concurrency safe, and should be called at most once, before using the
   114  // editor.
   115  //
   116  // It returns the editor, so that it may be called as follows:
   117  //   editor, err := NewEditor(s).Connect(ctx, conn)
   118  func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
   119  	e.serverConn = conn
   120  	e.Server = protocol.ServerDispatcher(conn)
   121  	e.client = &Client{editor: e, hooks: hooks}
   122  	conn.Go(ctx,
   123  		protocol.Handlers(
   124  			protocol.ClientHandler(e.client,
   125  				jsonrpc2.MethodNotFound)))
   126  	if err := e.initialize(ctx, e.Config.WithoutWorkspaceFolders, e.Config.WorkspaceRoot); err != nil {
   127  		return nil, err
   128  	}
   129  	e.sandbox.Workdir.AddWatcher(e.onFileChanges)
   130  	return e, nil
   131  }
   132  
   133  // Shutdown issues the 'shutdown' LSP notification.
   134  func (e *Editor) Shutdown(ctx context.Context) error {
   135  	if e.Server != nil {
   136  		if err := e.Server.Shutdown(ctx); err != nil {
   137  			return errors.Errorf("Shutdown: %w", err)
   138  		}
   139  	}
   140  	return nil
   141  }
   142  
   143  // Exit issues the 'exit' LSP notification.
   144  func (e *Editor) Exit(ctx context.Context) error {
   145  	if e.Server != nil {
   146  		// Not all LSP clients issue the exit RPC, but we do so here to ensure that
   147  		// we gracefully handle it on multi-session servers.
   148  		if err := e.Server.Exit(ctx); err != nil {
   149  			return errors.Errorf("Exit: %w", err)
   150  		}
   151  	}
   152  	return nil
   153  }
   154  
   155  // Close issues the shutdown and exit sequence an editor should.
   156  func (e *Editor) Close(ctx context.Context) error {
   157  	if err := e.Shutdown(ctx); err != nil {
   158  		return err
   159  	}
   160  	if err := e.Exit(ctx); err != nil {
   161  		return err
   162  	}
   163  	// called close on the editor should result in the connection closing
   164  	select {
   165  	case <-e.serverConn.Done():
   166  		// connection closed itself
   167  		return nil
   168  	case <-ctx.Done():
   169  		return errors.Errorf("connection not closed: %w", ctx.Err())
   170  	}
   171  }
   172  
   173  // Client returns the LSP client for this editor.
   174  func (e *Editor) Client() *Client {
   175  	return e.client
   176  }
   177  
   178  func (e *Editor) overlayEnv() map[string]string {
   179  	env := make(map[string]string)
   180  	for k, v := range e.defaultEnv {
   181  		env[k] = v
   182  	}
   183  	for k, v := range e.Config.Env {
   184  		env[k] = v
   185  	}
   186  	return env
   187  }
   188  
   189  func (e *Editor) configuration() map[string]interface{} {
   190  	config := map[string]interface{}{
   191  		"verboseWorkDoneProgress": true,
   192  		"env":                     e.overlayEnv(),
   193  		"expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
   194  		"completionBudget":        "10s",
   195  	}
   196  
   197  	if e.Config.BuildFlags != nil {
   198  		config["buildFlags"] = e.Config.BuildFlags
   199  	}
   200  
   201  	if e.Config.CodeLenses != nil {
   202  		config["codelenses"] = e.Config.CodeLenses
   203  	}
   204  	if e.Config.SymbolMatcher != nil {
   205  		config["symbolMatcher"] = *e.Config.SymbolMatcher
   206  	}
   207  	if e.Config.SymbolStyle != nil {
   208  		config["symbolStyle"] = *e.Config.SymbolStyle
   209  	}
   210  	if e.Config.EnableStaticcheck {
   211  		config["staticcheck"] = true
   212  	}
   213  	if e.Config.AllExperiments {
   214  		config["allExperiments"] = true
   215  	}
   216  
   217  	if e.Config.VerboseOutput {
   218  		config["verboseOutput"] = true
   219  	}
   220  
   221  	// TODO(rFindley): change to the new settings name once it is no longer
   222  	// designated experimental.
   223  	config["experimentalDiagnosticsDelay"] = "10ms"
   224  
   225  	// ExperimentalWorkspaceModule is only set as a mode, not a configuration.
   226  	return config
   227  }
   228  
   229  func (e *Editor) initialize(ctx context.Context, withoutWorkspaceFolders bool, editorRootPath string) error {
   230  	params := &protocol.ParamInitialize{}
   231  	params.ClientInfo.Name = "fakeclient"
   232  	params.ClientInfo.Version = "v1.0.0"
   233  	if !withoutWorkspaceFolders {
   234  		rootURI := e.sandbox.Workdir.RootURI()
   235  		if editorRootPath != "" {
   236  			rootURI = toURI(e.sandbox.Workdir.AbsPath(editorRootPath))
   237  		}
   238  		params.WorkspaceFolders = []protocol.WorkspaceFolder{{
   239  			URI:  string(rootURI),
   240  			Name: filepath.Base(rootURI.SpanURI().Filename()),
   241  		}}
   242  	}
   243  	params.Capabilities.Workspace.Configuration = true
   244  	params.Capabilities.Window.WorkDoneProgress = true
   245  	// TODO: set client capabilities
   246  	params.InitializationOptions = e.configuration()
   247  	if e.Config.SendPID {
   248  		params.ProcessID = float64(os.Getpid())
   249  	}
   250  
   251  	// This is a bit of a hack, since the fake editor doesn't actually support
   252  	// watching changed files that match a specific glob pattern. However, the
   253  	// editor does send didChangeWatchedFiles notifications, so set this to
   254  	// true.
   255  	params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
   256  
   257  	params.Trace = "messages"
   258  	// TODO: support workspace folders.
   259  	if e.Server != nil {
   260  		resp, err := e.Server.Initialize(ctx, params)
   261  		if err != nil {
   262  			return errors.Errorf("initialize: %w", err)
   263  		}
   264  		e.mu.Lock()
   265  		e.serverCapabilities = resp.Capabilities
   266  		e.mu.Unlock()
   267  
   268  		if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
   269  			return errors.Errorf("initialized: %w", err)
   270  		}
   271  	}
   272  	// TODO: await initial configuration here, or expect gopls to manage that?
   273  	return nil
   274  }
   275  
   276  func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
   277  	if e.Server == nil {
   278  		return
   279  	}
   280  	e.mu.Lock()
   281  	var lspevts []protocol.FileEvent
   282  	for _, evt := range evts {
   283  		// Always send an on-disk change, even for events that seem useless
   284  		// because they're shadowed by an open buffer.
   285  		lspevts = append(lspevts, evt.ProtocolEvent)
   286  
   287  		if buf, ok := e.buffers[evt.Path]; ok {
   288  			// Following VS Code, don't honor deletions or changes to dirty buffers.
   289  			if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted {
   290  				continue
   291  			}
   292  
   293  			content, err := e.sandbox.Workdir.ReadFile(evt.Path)
   294  			if err != nil {
   295  				continue // A race with some other operation.
   296  			}
   297  			// During shutdown, this call will fail. Ignore the error.
   298  			_ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil)
   299  		}
   300  	}
   301  	e.mu.Unlock()
   302  	e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
   303  		Changes: lspevts,
   304  	})
   305  }
   306  
   307  // OpenFile creates a buffer for the given workdir-relative file.
   308  func (e *Editor) OpenFile(ctx context.Context, path string) error {
   309  	content, err := e.sandbox.Workdir.ReadFile(path)
   310  	if err != nil {
   311  		return err
   312  	}
   313  	return e.createBuffer(ctx, path, false, content)
   314  }
   315  
   316  func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
   317  	uri := wd.URI(buf.path)
   318  	languageID := ""
   319  	if strings.HasSuffix(buf.path, ".go") {
   320  		// TODO: what about go.mod files? What is their language ID?
   321  		languageID = "go"
   322  	}
   323  	return protocol.TextDocumentItem{
   324  		URI:        uri,
   325  		LanguageID: languageID,
   326  		Version:    float64(buf.version),
   327  		Text:       buf.text(),
   328  	}
   329  }
   330  
   331  // CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
   332  // containing the given textual content.
   333  func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
   334  	return e.createBuffer(ctx, path, true, content)
   335  }
   336  
   337  func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error {
   338  	buf := buffer{
   339  		version: 1,
   340  		path:    path,
   341  		content: strings.Split(content, "\n"),
   342  		dirty:   dirty,
   343  	}
   344  	e.mu.Lock()
   345  	e.buffers[path] = buf
   346  	item := textDocumentItem(e.sandbox.Workdir, buf)
   347  	e.mu.Unlock()
   348  
   349  	if e.Server != nil {
   350  		if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
   351  			TextDocument: item,
   352  		}); err != nil {
   353  			return errors.Errorf("DidOpen: %w", err)
   354  		}
   355  	}
   356  	return nil
   357  }
   358  
   359  // CloseBuffer removes the current buffer (regardless of whether it is saved).
   360  func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
   361  	e.mu.Lock()
   362  	_, ok := e.buffers[path]
   363  	if !ok {
   364  		e.mu.Unlock()
   365  		return ErrUnknownBuffer
   366  	}
   367  	delete(e.buffers, path)
   368  	e.mu.Unlock()
   369  
   370  	if e.Server != nil {
   371  		if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
   372  			TextDocument: e.textDocumentIdentifier(path),
   373  		}); err != nil {
   374  			return errors.Errorf("DidClose: %w", err)
   375  		}
   376  	}
   377  	return nil
   378  }
   379  
   380  func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
   381  	return protocol.TextDocumentIdentifier{
   382  		URI: e.sandbox.Workdir.URI(path),
   383  	}
   384  }
   385  
   386  // SaveBuffer writes the content of the buffer specified by the given path to
   387  // the filesystem.
   388  func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
   389  	if err := e.OrganizeImports(ctx, path); err != nil {
   390  		return errors.Errorf("organizing imports before save: %w", err)
   391  	}
   392  	if err := e.FormatBuffer(ctx, path); err != nil {
   393  		return errors.Errorf("formatting before save: %w", err)
   394  	}
   395  	return e.SaveBufferWithoutActions(ctx, path)
   396  }
   397  
   398  func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error {
   399  	e.mu.Lock()
   400  	defer e.mu.Unlock()
   401  	buf, ok := e.buffers[path]
   402  	if !ok {
   403  		return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
   404  	}
   405  	content := buf.text()
   406  	includeText := false
   407  	syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
   408  	if ok {
   409  		includeText = syncOptions.Save.IncludeText
   410  	}
   411  
   412  	docID := e.textDocumentIdentifier(buf.path)
   413  	if e.Server != nil {
   414  		if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
   415  			TextDocument: docID,
   416  			Reason:       protocol.Manual,
   417  		}); err != nil {
   418  			return errors.Errorf("WillSave: %w", err)
   419  		}
   420  	}
   421  	if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
   422  		return errors.Errorf("writing %q: %w", path, err)
   423  	}
   424  
   425  	buf.dirty = false
   426  	e.buffers[path] = buf
   427  
   428  	if e.Server != nil {
   429  		params := &protocol.DidSaveTextDocumentParams{
   430  			TextDocument: protocol.VersionedTextDocumentIdentifier{
   431  				Version:                float64(buf.version),
   432  				TextDocumentIdentifier: docID,
   433  			},
   434  		}
   435  		if includeText {
   436  			params.Text = &content
   437  		}
   438  		if err := e.Server.DidSave(ctx, params); err != nil {
   439  			return errors.Errorf("DidSave: %w", err)
   440  		}
   441  	}
   442  	return nil
   443  }
   444  
   445  // contentPosition returns the (Line, Column) position corresponding to offset
   446  // in the buffer referenced by path.
   447  func contentPosition(content string, offset int) (Pos, error) {
   448  	scanner := bufio.NewScanner(strings.NewReader(content))
   449  	start := 0
   450  	line := 0
   451  	for scanner.Scan() {
   452  		end := start + len([]rune(scanner.Text())) + 1
   453  		if offset < end {
   454  			return Pos{Line: line, Column: offset - start}, nil
   455  		}
   456  		start = end
   457  		line++
   458  	}
   459  	if err := scanner.Err(); err != nil {
   460  		return Pos{}, errors.Errorf("scanning content: %w", err)
   461  	}
   462  	// Scan() will drop the last line if it is empty. Correct for this.
   463  	if (strings.HasSuffix(content, "\n") || content == "") && offset == start {
   464  		return Pos{Line: line, Column: 0}, nil
   465  	}
   466  	return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start)
   467  }
   468  
   469  // ErrNoMatch is returned if a regexp search fails.
   470  var (
   471  	ErrNoMatch       = errors.New("no match")
   472  	ErrUnknownBuffer = errors.New("unknown buffer")
   473  )
   474  
   475  // regexpRange returns the start and end of the first occurrence of either re
   476  // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match.
   477  func regexpRange(content, re string) (Pos, Pos, error) {
   478  	var start, end int
   479  	rec, err := regexp.Compile(re)
   480  	if err != nil {
   481  		return Pos{}, Pos{}, err
   482  	}
   483  	indexes := rec.FindStringSubmatchIndex(content)
   484  	if indexes == nil {
   485  		return Pos{}, Pos{}, ErrNoMatch
   486  	}
   487  	switch len(indexes) {
   488  	case 2:
   489  		// no subgroups: return the range of the regexp expression
   490  		start, end = indexes[0], indexes[1]
   491  	case 4:
   492  		// one subgroup: return its range
   493  		start, end = indexes[2], indexes[3]
   494  	default:
   495  		return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1)
   496  	}
   497  	startPos, err := contentPosition(content, start)
   498  	if err != nil {
   499  		return Pos{}, Pos{}, err
   500  	}
   501  	endPos, err := contentPosition(content, end)
   502  	if err != nil {
   503  		return Pos{}, Pos{}, err
   504  	}
   505  	return startPos, endPos, nil
   506  }
   507  
   508  // RegexpRange returns the first range in the buffer bufName matching re. See
   509  // RegexpSearch for more information on matching.
   510  func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) {
   511  	e.mu.Lock()
   512  	defer e.mu.Unlock()
   513  	buf, ok := e.buffers[bufName]
   514  	if !ok {
   515  		return Pos{}, Pos{}, ErrUnknownBuffer
   516  	}
   517  	return regexpRange(buf.text(), re)
   518  }
   519  
   520  // RegexpSearch returns the position of the first match for re in the buffer
   521  // bufName. For convenience, RegexpSearch supports the following two modes:
   522  //  1. If re has no subgroups, return the position of the match for re itself.
   523  //  2. If re has one subgroup, return the position of the first subgroup.
   524  // It returns an error re is invalid, has more than one subgroup, or doesn't
   525  // match the buffer.
   526  func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) {
   527  	start, _, err := e.RegexpRange(bufName, re)
   528  	return start, err
   529  }
   530  
   531  // RegexpReplace edits the buffer corresponding to path by replacing the first
   532  // instance of re, or its first subgroup, with the replace text. See
   533  // RegexpSearch for more explanation of these two modes.
   534  // It returns an error if re is invalid, has more than one subgroup, or doesn't
   535  // match the buffer.
   536  func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error {
   537  	e.mu.Lock()
   538  	defer e.mu.Unlock()
   539  	buf, ok := e.buffers[path]
   540  	if !ok {
   541  		return ErrUnknownBuffer
   542  	}
   543  	content := buf.text()
   544  	start, end, err := regexpRange(content, re)
   545  	if err != nil {
   546  		return err
   547  	}
   548  	return e.editBufferLocked(ctx, path, []Edit{{
   549  		Start: start,
   550  		End:   end,
   551  		Text:  replace,
   552  	}})
   553  }
   554  
   555  // EditBuffer applies the given test edits to the buffer identified by path.
   556  func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
   557  	e.mu.Lock()
   558  	defer e.mu.Unlock()
   559  	return e.editBufferLocked(ctx, path, edits)
   560  }
   561  
   562  func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error {
   563  	e.mu.Lock()
   564  	defer e.mu.Unlock()
   565  	lines := strings.Split(content, "\n")
   566  	return e.setBufferContentLocked(ctx, path, true, lines, nil)
   567  }
   568  
   569  // BufferText returns the content of the buffer with the given name.
   570  func (e *Editor) BufferText(name string) string {
   571  	e.mu.Lock()
   572  	defer e.mu.Unlock()
   573  	return e.buffers[name].text()
   574  }
   575  
   576  // BufferVersion returns the current version of the buffer corresponding to
   577  // name (or 0 if it is not being edited).
   578  func (e *Editor) BufferVersion(name string) int {
   579  	e.mu.Lock()
   580  	defer e.mu.Unlock()
   581  	return e.buffers[name].version
   582  }
   583  
   584  func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error {
   585  	buf, ok := e.buffers[path]
   586  	if !ok {
   587  		return fmt.Errorf("unknown buffer %q", path)
   588  	}
   589  	content := make([]string, len(buf.content))
   590  	copy(content, buf.content)
   591  	content, err := editContent(content, edits)
   592  	if err != nil {
   593  		return err
   594  	}
   595  	return e.setBufferContentLocked(ctx, path, true, content, edits)
   596  }
   597  
   598  func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []string, fromEdits []Edit) error {
   599  	buf, ok := e.buffers[path]
   600  	if !ok {
   601  		return fmt.Errorf("unknown buffer %q", path)
   602  	}
   603  	buf.content = content
   604  	buf.version++
   605  	buf.dirty = dirty
   606  	e.buffers[path] = buf
   607  	// A simple heuristic: if there is only one edit, send it incrementally.
   608  	// Otherwise, send the entire content.
   609  	var evts []protocol.TextDocumentContentChangeEvent
   610  	if len(fromEdits) == 1 {
   611  		evts = append(evts, fromEdits[0].toProtocolChangeEvent())
   612  	} else {
   613  		evts = append(evts, protocol.TextDocumentContentChangeEvent{
   614  			Text: buf.text(),
   615  		})
   616  	}
   617  	params := &protocol.DidChangeTextDocumentParams{
   618  		TextDocument: protocol.VersionedTextDocumentIdentifier{
   619  			Version:                float64(buf.version),
   620  			TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
   621  		},
   622  		ContentChanges: evts,
   623  	}
   624  	if e.Server != nil {
   625  		if err := e.Server.DidChange(ctx, params); err != nil {
   626  			return errors.Errorf("DidChange: %w", err)
   627  		}
   628  	}
   629  	return nil
   630  }
   631  
   632  // GoToDefinition jumps to the definition of the symbol at the given position
   633  // in an open buffer.
   634  func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
   635  	if err := e.checkBufferPosition(path, pos); err != nil {
   636  		return "", Pos{}, err
   637  	}
   638  	params := &protocol.DefinitionParams{}
   639  	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
   640  	params.Position = pos.ToProtocolPosition()
   641  
   642  	resp, err := e.Server.Definition(ctx, params)
   643  	if err != nil {
   644  		return "", Pos{}, errors.Errorf("definition: %w", err)
   645  	}
   646  	if len(resp) == 0 {
   647  		return "", Pos{}, nil
   648  	}
   649  	newPath := e.sandbox.Workdir.URIToPath(resp[0].URI)
   650  	newPos := fromProtocolPosition(resp[0].Range.Start)
   651  	if err := e.OpenFile(ctx, newPath); err != nil {
   652  		return "", Pos{}, errors.Errorf("OpenFile: %w", err)
   653  	}
   654  	return newPath, newPos, nil
   655  }
   656  
   657  // Symbol performs a workspace symbol search using query
   658  func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) {
   659  	params := &protocol.WorkspaceSymbolParams{}
   660  	params.Query = query
   661  
   662  	resp, err := e.Server.Symbol(ctx, params)
   663  	if err != nil {
   664  		return nil, errors.Errorf("symbol: %w", err)
   665  	}
   666  	var res []SymbolInformation
   667  	for _, si := range resp {
   668  		ploc := si.Location
   669  		path := e.sandbox.Workdir.URIToPath(ploc.URI)
   670  		start := fromProtocolPosition(ploc.Range.Start)
   671  		end := fromProtocolPosition(ploc.Range.End)
   672  		rnge := Range{
   673  			Start: start,
   674  			End:   end,
   675  		}
   676  		loc := Location{
   677  			Path:  path,
   678  			Range: rnge,
   679  		}
   680  		res = append(res, SymbolInformation{
   681  			Name:     si.Name,
   682  			Kind:     si.Kind,
   683  			Location: loc,
   684  		})
   685  	}
   686  	return res, nil
   687  }
   688  
   689  // OrganizeImports requests and performs the source.organizeImports codeAction.
   690  func (e *Editor) OrganizeImports(ctx context.Context, path string) error {
   691  	return e.codeAction(ctx, path, nil, nil, protocol.SourceOrganizeImports)
   692  }
   693  
   694  // RefactorRewrite requests and performs the source.refactorRewrite codeAction.
   695  func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error {
   696  	return e.codeAction(ctx, path, rng, nil, protocol.RefactorRewrite)
   697  }
   698  
   699  // ApplyQuickFixes requests and performs the quickfix codeAction.
   700  func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error {
   701  	return e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
   702  }
   703  
   704  func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
   705  	if e.Server == nil {
   706  		return nil
   707  	}
   708  	params := &protocol.CodeActionParams{}
   709  	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
   710  	params.Context.Only = only
   711  	if diagnostics != nil {
   712  		params.Context.Diagnostics = diagnostics
   713  	}
   714  	if rng != nil {
   715  		params.Range = *rng
   716  	}
   717  	actions, err := e.Server.CodeAction(ctx, params)
   718  	if err != nil {
   719  		return errors.Errorf("textDocument/codeAction: %w", err)
   720  	}
   721  	for _, action := range actions {
   722  		var match bool
   723  		for _, o := range only {
   724  			if action.Kind == o {
   725  				match = true
   726  				break
   727  			}
   728  		}
   729  		if !match {
   730  			continue
   731  		}
   732  		for _, change := range action.Edit.DocumentChanges {
   733  			path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
   734  			if float64(e.buffers[path].version) != change.TextDocument.Version {
   735  				// Skip edits for old versions.
   736  				continue
   737  			}
   738  			edits := convertEdits(change.Edits)
   739  			if err := e.EditBuffer(ctx, path, edits); err != nil {
   740  				return errors.Errorf("editing buffer %q: %w", path, err)
   741  			}
   742  		}
   743  		// Execute any commands. The specification says that commands are
   744  		// executed after edits are applied.
   745  		if action.Command != nil {
   746  			if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
   747  				Command:   action.Command.Command,
   748  				Arguments: action.Command.Arguments,
   749  			}); err != nil {
   750  				return err
   751  			}
   752  		}
   753  	}
   754  	return nil
   755  }
   756  
   757  func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
   758  	if e.Server == nil {
   759  		return nil, nil
   760  	}
   761  	var match bool
   762  	// Ensure that this command was actually listed as a supported command.
   763  	for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
   764  		if command == params.Command {
   765  			match = true
   766  			break
   767  		}
   768  	}
   769  	if !match {
   770  		return nil, fmt.Errorf("unsupported command %q", params.Command)
   771  	}
   772  	result, err := e.Server.ExecuteCommand(ctx, params)
   773  	if err != nil {
   774  		return nil, err
   775  	}
   776  	// Some commands use the go command, which writes directly to disk.
   777  	// For convenience, check for those changes.
   778  	if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
   779  		return nil, err
   780  	}
   781  	return result, nil
   782  }
   783  
   784  func convertEdits(protocolEdits []protocol.TextEdit) []Edit {
   785  	var edits []Edit
   786  	for _, lspEdit := range protocolEdits {
   787  		edits = append(edits, fromProtocolTextEdit(lspEdit))
   788  	}
   789  	return edits
   790  }
   791  
   792  // FormatBuffer gofmts a Go file.
   793  func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
   794  	if e.Server == nil {
   795  		return nil
   796  	}
   797  	e.mu.Lock()
   798  	version := e.buffers[path].version
   799  	e.mu.Unlock()
   800  	params := &protocol.DocumentFormattingParams{}
   801  	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
   802  	resp, err := e.Server.Formatting(ctx, params)
   803  	if err != nil {
   804  		return errors.Errorf("textDocument/formatting: %w", err)
   805  	}
   806  	e.mu.Lock()
   807  	defer e.mu.Unlock()
   808  	if versionAfter := e.buffers[path].version; versionAfter != version {
   809  		return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter)
   810  	}
   811  	edits := convertEdits(resp)
   812  	if len(edits) == 0 {
   813  		return nil
   814  	}
   815  	return e.editBufferLocked(ctx, path, edits)
   816  }
   817  
   818  func (e *Editor) checkBufferPosition(path string, pos Pos) error {
   819  	e.mu.Lock()
   820  	defer e.mu.Unlock()
   821  	buf, ok := e.buffers[path]
   822  	if !ok {
   823  		return fmt.Errorf("buffer %q is not open", path)
   824  	}
   825  	if !inText(pos, buf.content) {
   826  		return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
   827  	}
   828  	return nil
   829  }
   830  
   831  // RunGenerate runs `go generate` non-recursively in the workdir-relative dir
   832  // path. It does not report any resulting file changes as a watched file
   833  // change, so must be followed by a call to Workdir.CheckForFileChanges once
   834  // the generate command has completed.
   835  func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
   836  	if e.Server == nil {
   837  		return nil
   838  	}
   839  	absDir := e.sandbox.Workdir.AbsPath(dir)
   840  	jsonArgs, err := source.MarshalArgs(span.URIFromPath(absDir), false)
   841  	if err != nil {
   842  		return err
   843  	}
   844  	params := &protocol.ExecuteCommandParams{
   845  		Command:   source.CommandGenerate.ID(),
   846  		Arguments: jsonArgs,
   847  	}
   848  	if _, err := e.ExecuteCommand(ctx, params); err != nil {
   849  		return fmt.Errorf("running generate: %v", err)
   850  	}
   851  	// Unfortunately we can't simply poll the workdir for file changes here,
   852  	// because server-side command may not have completed. In regtests, we can
   853  	// Await this state change, but here we must delegate that responsibility to
   854  	// the caller.
   855  	return nil
   856  }
   857  
   858  // CodeLens executes a codelens request on the server.
   859  func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
   860  	if e.Server == nil {
   861  		return nil, nil
   862  	}
   863  	e.mu.Lock()
   864  	_, ok := e.buffers[path]
   865  	e.mu.Unlock()
   866  	if !ok {
   867  		return nil, fmt.Errorf("buffer %q is not open", path)
   868  	}
   869  	params := &protocol.CodeLensParams{
   870  		TextDocument: e.textDocumentIdentifier(path),
   871  	}
   872  	lens, err := e.Server.CodeLens(ctx, params)
   873  	if err != nil {
   874  		return nil, err
   875  	}
   876  	return lens, nil
   877  }
   878  
   879  // Completion executes a completion request on the server.
   880  func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) {
   881  	if e.Server == nil {
   882  		return nil, nil
   883  	}
   884  	e.mu.Lock()
   885  	_, ok := e.buffers[path]
   886  	e.mu.Unlock()
   887  	if !ok {
   888  		return nil, fmt.Errorf("buffer %q is not open", path)
   889  	}
   890  	params := &protocol.CompletionParams{
   891  		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
   892  			TextDocument: e.textDocumentIdentifier(path),
   893  			Position:     pos.ToProtocolPosition(),
   894  		},
   895  	}
   896  	completions, err := e.Server.Completion(ctx, params)
   897  	if err != nil {
   898  		return nil, err
   899  	}
   900  	return completions, nil
   901  }
   902  
   903  // References executes a reference request on the server.
   904  func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
   905  	if e.Server == nil {
   906  		return nil, nil
   907  	}
   908  	e.mu.Lock()
   909  	_, ok := e.buffers[path]
   910  	e.mu.Unlock()
   911  	if !ok {
   912  		return nil, fmt.Errorf("buffer %q is not open", path)
   913  	}
   914  	params := &protocol.ReferenceParams{
   915  		TextDocumentPositionParams: protocol.TextDocumentPositionParams{
   916  			TextDocument: e.textDocumentIdentifier(path),
   917  			Position:     pos.ToProtocolPosition(),
   918  		},
   919  		Context: protocol.ReferenceContext{
   920  			IncludeDeclaration: true,
   921  		},
   922  	}
   923  	locations, err := e.Server.References(ctx, params)
   924  	if err != nil {
   925  		return nil, err
   926  	}
   927  	return locations, nil
   928  }
   929  
   930  // CodeAction executes a codeAction request on the server.
   931  func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range) ([]protocol.CodeAction, error) {
   932  	if e.Server == nil {
   933  		return nil, nil
   934  	}
   935  	e.mu.Lock()
   936  	_, ok := e.buffers[path]
   937  	e.mu.Unlock()
   938  	if !ok {
   939  		return nil, fmt.Errorf("buffer %q is not open", path)
   940  	}
   941  	params := &protocol.CodeActionParams{
   942  		TextDocument: e.textDocumentIdentifier(path),
   943  	}
   944  	if rng != nil {
   945  		params.Range = *rng
   946  	}
   947  	lens, err := e.Server.CodeAction(ctx, params)
   948  	if err != nil {
   949  		return nil, err
   950  	}
   951  	return lens, nil
   952  }
   953  
   954  // Hover triggers a hover at the given position in an open buffer.
   955  func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) {
   956  	if err := e.checkBufferPosition(path, pos); err != nil {
   957  		return nil, Pos{}, err
   958  	}
   959  	params := &protocol.HoverParams{}
   960  	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
   961  	params.Position = pos.ToProtocolPosition()
   962  
   963  	resp, err := e.Server.Hover(ctx, params)
   964  	if err != nil {
   965  		return nil, Pos{}, errors.Errorf("hover: %w", err)
   966  	}
   967  	if resp == nil {
   968  		return nil, Pos{}, nil
   969  	}
   970  	return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
   971  }
   972  
   973  func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) {
   974  	if e.Server == nil {
   975  		return nil, nil
   976  	}
   977  	params := &protocol.DocumentLinkParams{}
   978  	params.TextDocument.URI = e.sandbox.Workdir.URI(path)
   979  	return e.Server.DocumentLink(ctx, params)
   980  }