+++ /dev/null
-// 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 (
- "bufio"
- "context"
- "fmt"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
-
- "golang.org/x/tools/internal/jsonrpc2"
- "golang.org/x/tools/internal/lsp/protocol"
- "golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/span"
- errors "golang.org/x/xerrors"
-)
-
-// Editor is a fake editor client. It keeps track of client state and can be
-// used for writing LSP tests.
-type Editor struct {
- Config EditorConfig
-
- // Server, client, and sandbox are concurrency safe and written only
- // at construction time, so do not require synchronization.
- Server protocol.Server
- serverConn jsonrpc2.Conn
- client *Client
- sandbox *Sandbox
- defaultEnv map[string]string
-
- // Since this editor is intended just for testing, we use very coarse
- // locking.
- mu sync.Mutex
- // Editor state.
- buffers map[string]buffer
- // 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")
-}
-
-// 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 map[string]string
- BuildFlags []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
-
- // SymbolMatcher is the config associated with the "symbolMatcher" gopls
- // config option.
- SymbolMatcher, SymbolStyle *string
-
- // LimitWorkspaceScope is true if the user does not want to expand their
- // workspace scope to the entire module.
- LimitWorkspaceScope bool
-
- // WithoutWorkspaceFolders is used to simulate opening a single file in the
- // editor, without a workspace root. In that case, the client sends neither
- // workspace folders nor a root URI.
- WithoutWorkspaceFolders bool
-
- // EditorRootPath specifies the root path of the workspace folder used when
- // initializing gopls in the sandbox. If empty, the Workdir is used.
- EditorRootPath string
-
- // EnableStaticcheck enables staticcheck analyzers.
- EnableStaticcheck bool
-
- // AllExperiments sets the "allExperiments" configuration, which enables
- // all of gopls's opt-in settings.
- AllExperiments bool
-}
-
-// NewEditor Creates a new Editor.
-func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
- return &Editor{
- buffers: make(map[string]buffer),
- sandbox: sandbox,
- defaultEnv: sandbox.GoEnv(),
- Config: config,
- }
-}
-
-// 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.
-//
-// It returns the editor, so that it may be called as follows:
-// editor, err := NewEditor(s).Connect(ctx, conn)
-func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
- e.serverConn = conn
- e.Server = protocol.ServerDispatcher(conn)
- e.client = &Client{editor: e, hooks: hooks}
- conn.Go(ctx,
- protocol.Handlers(
- protocol.ClientHandler(e.client,
- jsonrpc2.MethodNotFound)))
- if err := e.initialize(ctx, e.Config.WithoutWorkspaceFolders, e.Config.EditorRootPath); err != nil {
- return nil, err
- }
- e.sandbox.Workdir.AddWatcher(e.onFileChanges)
- return e, nil
-}
-
-// Shutdown issues the 'shutdown' LSP notification.
-func (e *Editor) Shutdown(ctx context.Context) error {
- if e.Server != nil {
- if err := e.Server.Shutdown(ctx); err != nil {
- return errors.Errorf("Shutdown: %w", err)
- }
- }
- return nil
-}
-
-// Exit issues the 'exit' LSP notification.
-func (e *Editor) Exit(ctx context.Context) error {
- if e.Server != nil {
- // Not all LSP clients issue the exit RPC, but we do so here to ensure that
- // we gracefully handle it on multi-session servers.
- if err := e.Server.Exit(ctx); err != nil {
- return errors.Errorf("Exit: %w", err)
- }
- }
- return nil
-}
-
-// 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 errors.Errorf("connection not closed: %w", ctx.Err())
- }
-}
-
-// Client returns the LSP client for this editor.
-func (e *Editor) Client() *Client {
- return e.client
-}
-
-func (e *Editor) overlayEnv() map[string]string {
- env := make(map[string]string)
- for k, v := range e.defaultEnv {
- env[k] = v
- }
- for k, v := range e.Config.Env {
- env[k] = v
- }
- return env
-}
-
-func (e *Editor) configuration() map[string]interface{} {
- config := map[string]interface{}{
- "verboseWorkDoneProgress": true,
- "env": e.overlayEnv(),
- "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
- "completionBudget": "10s",
- }
-
- if e.Config.BuildFlags != nil {
- config["buildFlags"] = e.Config.BuildFlags
- }
-
- if e.Config.CodeLens != nil {
- config["codelens"] = e.Config.CodeLens
- }
- if e.Config.SymbolMatcher != nil {
- config["symbolMatcher"] = *e.Config.SymbolMatcher
- }
- if e.Config.SymbolStyle != nil {
- config["symbolStyle"] = *e.Config.SymbolStyle
- }
- if e.Config.EnableStaticcheck {
- config["staticcheck"] = true
- }
- if e.Config.AllExperiments {
- config["allExperiments"] = true
- }
-
- // TODO(rFindley): uncomment this if/when diagnostics delay is on by
- // default... and probably change to the new settings name.
- // config["experimentalDiagnosticsDelay"] = "10ms"
-
- // ExperimentalWorkspaceModule is only set as a mode, not a configuration.
- return config
-}
-
-func (e *Editor) initialize(ctx context.Context, withoutWorkspaceFolders bool, editorRootPath string) error {
- params := &protocol.ParamInitialize{}
- params.ClientInfo.Name = "fakeclient"
- params.ClientInfo.Version = "v1.0.0"
- if !withoutWorkspaceFolders {
- rootURI := e.sandbox.Workdir.RootURI()
- if editorRootPath != "" {
- rootURI = toURI(e.sandbox.Workdir.AbsPath(editorRootPath))
- }
- params.WorkspaceFolders = []protocol.WorkspaceFolder{{
- URI: string(rootURI),
- Name: filepath.Base(rootURI.SpanURI().Filename()),
- }}
- }
- params.Capabilities.Workspace.Configuration = true
- params.Capabilities.Window.WorkDoneProgress = true
- // TODO: set client capabilities
- params.InitializationOptions = e.configuration()
-
- // This is a bit of a hack, since the fake editor doesn't actually support
- // watching changed files that match a specific glob pattern. However, the
- // editor does send didChangeWatchedFiles notifications, so set this to
- // true.
- params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
-
- params.Trace = "messages"
- // TODO: support workspace folders.
- if e.Server != nil {
- resp, err := e.Server.Initialize(ctx, params)
- if err != nil {
- return errors.Errorf("initialize: %w", err)
- }
- e.mu.Lock()
- e.serverCapabilities = resp.Capabilities
- e.mu.Unlock()
-
- if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
- return errors.Errorf("initialized: %w", err)
- }
- }
- // TODO: await initial configuration here, or expect gopls to manage that?
- return nil
-}
-
-func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
- if e.Server == nil {
- return
- }
- var lspevts []protocol.FileEvent
- for _, evt := range evts {
- lspevts = append(lspevts, evt.ProtocolEvent)
- }
- e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
- Changes: lspevts,
- })
-}
-
-// OpenFile creates a buffer for the given workdir-relative file.
-func (e *Editor) OpenFile(ctx context.Context, path string) error {
- content, err := e.sandbox.Workdir.ReadFile(path)
- if err != nil {
- return err
- }
- return e.CreateBuffer(ctx, path, content)
-}
-
-func newBuffer(path, content string) buffer {
- return buffer{
- version: 1,
- path: path,
- content: strings.Split(content, "\n"),
- }
-}
-
-func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
- uri := wd.URI(buf.path)
- 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(),
- }
-}
-
-// CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
-// containing the given textual content.
-func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
- buf := newBuffer(path, content)
- e.mu.Lock()
- e.buffers[path] = buf
- item := textDocumentItem(e.sandbox.Workdir, buf)
- e.mu.Unlock()
-
- if e.Server != nil {
- if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
- TextDocument: item,
- }); err != nil {
- return errors.Errorf("DidOpen: %w", err)
- }
- }
- 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()
- return ErrUnknownBuffer
- }
- delete(e.buffers, path)
- e.mu.Unlock()
-
- if e.Server != nil {
- if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
- TextDocument: e.textDocumentIdentifier(path),
- }); err != nil {
- return errors.Errorf("DidClose: %w", err)
- }
- }
- return nil
-}
-
-func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
- return protocol.TextDocumentIdentifier{
- URI: e.sandbox.Workdir.URI(path),
- }
-}
-
-// SaveBuffer writes the content of the buffer specified by the given path to
-// the filesystem.
-func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
- if err := e.OrganizeImports(ctx, path); err != nil {
- return errors.Errorf("organizing imports before save: %w", err)
- }
- if err := e.FormatBuffer(ctx, path); err != nil {
- return errors.Errorf("formatting before save: %w", err)
- }
- return e.SaveBufferWithoutActions(ctx, path)
-}
-
-func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error {
- 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()
-
- docID := e.textDocumentIdentifier(buf.path)
- if e.Server != nil {
- if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
- TextDocument: docID,
- Reason: protocol.Manual,
- }); err != nil {
- return errors.Errorf("WillSave: %w", err)
- }
- }
- if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
- return errors.Errorf("writing %q: %w", path, err)
- }
- if e.Server != nil {
- params := &protocol.DidSaveTextDocumentParams{
- TextDocument: protocol.VersionedTextDocumentIdentifier{
- Version: float64(buf.version),
- TextDocumentIdentifier: docID,
- },
- }
- if includeText {
- params.Text = &content
- }
- if err := e.Server.DidSave(ctx, params); err != nil {
- return errors.Errorf("DidSave: %w", err)
- }
- }
- return nil
-}
-
-// 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 {
- return Pos{}, errors.Errorf("scanning content: %w", err)
- }
- // Scan() will drop the last line if it is empty. Correct for this.
- if (strings.HasSuffix(content, "\n") || content == "") && 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
-}
-
-// RegexpRange returns the first range in the buffer bufName matching re. See
-// RegexpSearch for more information on matching.
-func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) {
- e.mu.Lock()
- defer e.mu.Unlock()
- buf, ok := e.buffers[bufName]
- if !ok {
- return Pos{}, Pos{}, ErrUnknownBuffer
- }
- return regexpRange(buf.text(), re)
-}
-
-// 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) {
- start, _, err := e.RegexpRange(bufName, 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,
- }})
-}
-
-// EditBuffer applies the given test edits to the buffer identified by path.
-func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.editBufferLocked(ctx, path, edits)
-}
-
-// BufferText returns the content of the buffer with the given name.
-func (e *Editor) BufferText(name string) string {
- e.mu.Lock()
- defer e.mu.Unlock()
- return e.buffers[name].text()
-}
-
-// 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
-}
-
-func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error {
- buf, ok := e.buffers[path]
- if !ok {
- return fmt.Errorf("unknown buffer %q", path)
- }
- var (
- content = make([]string, len(buf.content))
- err error
- evts []protocol.TextDocumentContentChangeEvent
- )
- copy(content, buf.content)
- content, err = editContent(content, edits)
- if err != nil {
- return err
- }
-
- buf.content = content
- buf.version++
- e.buffers[path] = buf
- // 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(),
- })
- }
- params := &protocol.DidChangeTextDocumentParams{
- TextDocument: protocol.VersionedTextDocumentIdentifier{
- Version: float64(buf.version),
- TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
- },
- ContentChanges: evts,
- }
- if e.Server != nil {
- if err := e.Server.DidChange(ctx, params); err != nil {
- return errors.Errorf("DidChange: %w", err)
- }
- }
- return nil
-}
-
-// 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{}
- params.TextDocument.URI = e.sandbox.Workdir.URI(path)
- params.Position = pos.ToProtocolPosition()
-
- resp, err := e.Server.Definition(ctx, params)
- if err != nil {
- return "", Pos{}, errors.Errorf("definition: %w", err)
- }
- if len(resp) == 0 {
- return "", Pos{}, nil
- }
- newPath := e.sandbox.Workdir.URIToPath(resp[0].URI)
- newPos := fromProtocolPosition(resp[0].Range.Start)
- if err := e.OpenFile(ctx, newPath); err != nil {
- return "", Pos{}, errors.Errorf("OpenFile: %w", err)
- }
- return newPath, newPos, nil
-}
-
-// Symbol performs a workspace symbol search using query
-func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) {
- params := &protocol.WorkspaceSymbolParams{}
- params.Query = query
-
- resp, err := e.Server.Symbol(ctx, params)
- if err != nil {
- return nil, errors.Errorf("symbol: %w", err)
- }
- var res []SymbolInformation
- for _, si := range resp {
- ploc := si.Location
- path := e.sandbox.Workdir.URIToPath(ploc.URI)
- start := fromProtocolPosition(ploc.Range.Start)
- end := fromProtocolPosition(ploc.Range.End)
- rnge := Range{
- Start: start,
- End: end,
- }
- loc := Location{
- Path: path,
- Range: rnge,
- }
- res = append(res, SymbolInformation{
- Name: si.Name,
- Kind: si.Kind,
- Location: loc,
- })
- }
- return res, nil
-}
-
-// OrganizeImports requests and performs the source.organizeImports codeAction.
-func (e *Editor) OrganizeImports(ctx context.Context, path string) error {
- return e.codeAction(ctx, path, nil, nil, protocol.SourceOrganizeImports)
-}
-
-// RefactorRewrite requests and performs the source.refactorRewrite codeAction.
-func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error {
- return e.codeAction(ctx, path, rng, nil, protocol.RefactorRewrite)
-}
-
-// ApplyQuickFixes requests and performs the quickfix codeAction.
-func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error {
- return e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
-}
-
-func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
- if e.Server == nil {
- return nil
- }
- params := &protocol.CodeActionParams{}
- params.TextDocument.URI = e.sandbox.Workdir.URI(path)
- params.Context.Only = only
- if diagnostics != nil {
- params.Context.Diagnostics = diagnostics
- }
- if rng != nil {
- params.Range = *rng
- }
- actions, err := e.Server.CodeAction(ctx, params)
- if err != nil {
- return errors.Errorf("textDocument/codeAction: %w", err)
- }
- for _, action := range actions {
- var match bool
- for _, o := range only {
- if action.Kind == o {
- match = true
- break
- }
- }
- if !match {
- continue
- }
- for _, change := range action.Edit.DocumentChanges {
- path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
- if float64(e.buffers[path].version) != change.TextDocument.Version {
- // Skip edits for old versions.
- continue
- }
- edits := convertEdits(change.Edits)
- if err := e.EditBuffer(ctx, path, edits); err != nil {
- return errors.Errorf("editing buffer %q: %w", path, err)
- }
- }
- // Execute any commands. The specification says that commands are
- // executed after edits are applied.
- if action.Command != nil {
- if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
- Command: action.Command.Command,
- Arguments: action.Command.Arguments,
- }); err != nil {
- return err
- }
- }
- }
- return nil
-}
-
-func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
- if e.Server == nil {
- return nil, nil
- }
- var match bool
- // Ensure that this command was actually listed as a supported command.
- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
- if command == params.Command {
- match = true
- break
- }
- }
- if !match {
- return nil, fmt.Errorf("unsupported command %q", params.Command)
- }
- return e.Server.ExecuteCommand(ctx, params)
-}
-
-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 {
- if e.Server == nil {
- return nil
- }
- e.mu.Lock()
- version := e.buffers[path].version
- e.mu.Unlock()
- params := &protocol.DocumentFormattingParams{}
- params.TextDocument.URI = e.sandbox.Workdir.URI(path)
- resp, err := e.Server.Formatting(ctx, params)
- if err != nil {
- return errors.Errorf("textDocument/formatting: %w", err)
- }
- 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)
- }
- edits := convertEdits(resp)
- return e.editBufferLocked(ctx, path, edits)
-}
-
-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
-}
-
-// RunGenerate runs `go generate` non-recursively in the workdir-relative dir
-// path. It does not report any resulting file changes as a watched file
-// change, so must be followed by a call to Workdir.CheckForFileChanges once
-// the generate command has completed.
-func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
- if e.Server == nil {
- return nil
- }
- absDir := e.sandbox.Workdir.AbsPath(dir)
- jsonArgs, err := source.MarshalArgs(span.URIFromPath(absDir), false)
- if err != nil {
- return err
- }
- params := &protocol.ExecuteCommandParams{
- Command: source.CommandGenerate.ID(),
- Arguments: jsonArgs,
- }
- if _, err := e.ExecuteCommand(ctx, params); err != nil {
- return fmt.Errorf("running generate: %v", err)
- }
- // Unfortunately we can't simply poll the workdir for file changes here,
- // 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
-}
-
-// CodeLens executes a codelens request on the server.
-func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
- if e.Server == nil {
- 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),
- }
- lens, err := e.Server.CodeLens(ctx, params)
- if err != nil {
- return nil, err
- }
- return lens, nil
-}
-
-// Completion executes a completion request on the server.
-func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) {
- if e.Server == nil {
- 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.CompletionParams{
- TextDocumentPositionParams: protocol.TextDocumentPositionParams{
- TextDocument: e.textDocumentIdentifier(path),
- Position: pos.ToProtocolPosition(),
- },
- }
- completions, err := e.Server.Completion(ctx, params)
- if err != nil {
- return nil, err
- }
- return completions, nil
-}
-
-// References executes a reference request on the server.
-func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
- if e.Server == nil {
- 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.ReferenceParams{
- TextDocumentPositionParams: protocol.TextDocumentPositionParams{
- TextDocument: e.textDocumentIdentifier(path),
- Position: pos.ToProtocolPosition(),
- },
- Context: protocol.ReferenceContext{
- IncludeDeclaration: true,
- },
- }
- locations, err := e.Server.References(ctx, params)
- if err != nil {
- return nil, err
- }
- return locations, nil
-}
-
-// CodeAction executes a codeAction request on the server.
-func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range) ([]protocol.CodeAction, error) {
- if e.Server == nil {
- 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),
- }
- if rng != nil {
- params.Range = *rng
- }
- lens, err := e.Server.CodeAction(ctx, params)
- if err != nil {
- return nil, err
- }
- return lens, nil
-}
-
-// 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{}, errors.Errorf("hover: %w", err)
- }
- if resp == nil {
- return nil, Pos{}, nil
- }
- return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
-}
-
-func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) {
- if e.Server == nil {
- return nil, nil
- }
- params := &protocol.DocumentLinkParams{}
- params.TextDocument.URI = e.sandbox.Workdir.URI(path)
- return e.Server.DocumentLink(ctx, params)
-}