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