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.
17 "golang.org/x/tools/internal/jsonrpc2"
18 "golang.org/x/tools/internal/lsp/command"
19 "golang.org/x/tools/internal/lsp/protocol"
20 "golang.org/x/tools/internal/span"
21 errors "golang.org/x/xerrors"
24 // Editor is a fake editor client. It keeps track of client state and can be
25 // used for writing LSP tests.
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
35 defaultEnv map[string]string
37 // Since this editor is intended just for testing, we use very coarse
41 buffers map[string]buffer
42 // Capabilities / Options
43 serverCapabilities protocol.ServerCapabilities
45 // Call metrics for the purpose of expectations. This is done in an ad-hoc
46 // manner for now. Perhaps in the future we should do something more
47 // systematic. Guarded with a separate mutex as calls may need to be accessed
48 // asynchronously via callbacks into the Editor.
53 type CallCounts struct {
54 DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64
64 func (b buffer) text() string {
65 return strings.Join(b.lines, "\n")
68 // EditorConfig configures the editor's LSP session. This is similar to
69 // source.UserOptions, but we use a separate type here so that we expose only
70 // that configuration which we support.
72 // The zero value for EditorConfig should correspond to its defaults.
73 type EditorConfig struct {
77 // CodeLenses is a map defining whether codelens are enabled, keyed by the
78 // codeLens command. CodeLenses which are not present in this map are left in
79 // their default state.
80 CodeLenses map[string]bool
82 // SymbolMatcher is the config associated with the "symbolMatcher" gopls
84 SymbolMatcher, SymbolStyle *string
86 // LimitWorkspaceScope is true if the user does not want to expand their
87 // workspace scope to the entire module.
88 LimitWorkspaceScope bool
90 // WorkspaceFolders is the workspace folders to configure on the LSP server,
91 // relative to the sandbox workdir.
93 // As a special case, if WorkspaceFolders is nil the editor defaults to
94 // configuring a single workspace folder corresponding to the workdir root.
95 // To explicitly send no workspace folders, use an empty (non-nil) slice.
96 WorkspaceFolders []string
98 // EnableStaticcheck enables staticcheck analyzers.
99 EnableStaticcheck bool
101 // AllExperiments sets the "allExperiments" configuration, which enables
102 // all of gopls's opt-in settings.
105 // Whether to send the current process ID, for testing data that is joined to
106 // the PID. This can only be set by one test.
109 DirectoryFilters []string
113 ImportShortcut string
116 // NewEditor Creates a new Editor.
117 func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
119 buffers: make(map[string]buffer),
121 defaultEnv: sandbox.GoEnv(),
126 // Connect configures the editor to communicate with an LSP server on conn. It
127 // is not concurrency safe, and should be called at most once, before using the
130 // It returns the editor, so that it may be called as follows:
131 // editor, err := NewEditor(s).Connect(ctx, conn)
132 func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
134 e.Server = protocol.ServerDispatcher(conn)
135 e.client = &Client{editor: e, hooks: hooks}
138 protocol.ClientHandler(e.client,
139 jsonrpc2.MethodNotFound)))
140 if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil {
143 e.sandbox.Workdir.AddWatcher(e.onFileChanges)
147 func (e *Editor) Stats() CallCounts {
149 defer e.callsMu.Unlock()
153 // Shutdown issues the 'shutdown' LSP notification.
154 func (e *Editor) Shutdown(ctx context.Context) error {
156 if err := e.Server.Shutdown(ctx); err != nil {
157 return errors.Errorf("Shutdown: %w", err)
163 // Exit issues the 'exit' LSP notification.
164 func (e *Editor) Exit(ctx context.Context) error {
166 // Not all LSP clients issue the exit RPC, but we do so here to ensure that
167 // we gracefully handle it on multi-session servers.
168 if err := e.Server.Exit(ctx); err != nil {
169 return errors.Errorf("Exit: %w", err)
175 // Close issues the shutdown and exit sequence an editor should.
176 func (e *Editor) Close(ctx context.Context) error {
177 if err := e.Shutdown(ctx); err != nil {
180 if err := e.Exit(ctx); err != nil {
183 // called close on the editor should result in the connection closing
185 case <-e.serverConn.Done():
186 // connection closed itself
189 return errors.Errorf("connection not closed: %w", ctx.Err())
193 // Client returns the LSP client for this editor.
194 func (e *Editor) Client() *Client {
198 func (e *Editor) overlayEnv() map[string]string {
199 env := make(map[string]string)
200 for k, v := range e.defaultEnv {
203 for k, v := range e.Config.Env {
209 func (e *Editor) configuration() map[string]interface{} {
210 config := map[string]interface{}{
211 "verboseWorkDoneProgress": true,
212 "env": e.overlayEnv(),
213 "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
214 "completionBudget": "10s",
217 if e.Config.BuildFlags != nil {
218 config["buildFlags"] = e.Config.BuildFlags
220 if e.Config.DirectoryFilters != nil {
221 config["directoryFilters"] = e.Config.DirectoryFilters
223 if e.Config.CodeLenses != nil {
224 config["codelenses"] = e.Config.CodeLenses
226 if e.Config.SymbolMatcher != nil {
227 config["symbolMatcher"] = *e.Config.SymbolMatcher
229 if e.Config.SymbolStyle != nil {
230 config["symbolStyle"] = *e.Config.SymbolStyle
232 if e.Config.EnableStaticcheck {
233 config["staticcheck"] = true
235 if e.Config.AllExperiments {
236 config["allExperiments"] = true
239 if e.Config.VerboseOutput {
240 config["verboseOutput"] = true
243 if e.Config.ImportShortcut != "" {
244 config["importShortcut"] = e.Config.ImportShortcut
247 // TODO(rFindley): change to the new settings name once it is no longer
248 // designated experimental.
249 config["experimentalDiagnosticsDelay"] = "10ms"
251 // ExperimentalWorkspaceModule is only set as a mode, not a configuration.
255 func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error {
256 params := &protocol.ParamInitialize{}
257 params.ClientInfo.Name = "fakeclient"
258 params.ClientInfo.Version = "v1.0.0"
260 if workspaceFolders == nil {
261 workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)}
263 for _, folder := range workspaceFolders {
264 params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{
265 URI: string(e.sandbox.Workdir.URI(folder)),
266 Name: filepath.Base(folder),
270 params.Capabilities.Workspace.Configuration = true
271 params.Capabilities.Window.WorkDoneProgress = true
272 // TODO: set client capabilities
273 params.InitializationOptions = e.configuration()
274 if e.Config.SendPID {
275 params.ProcessID = int32(os.Getpid())
278 // This is a bit of a hack, since the fake editor doesn't actually support
279 // watching changed files that match a specific glob pattern. However, the
280 // editor does send didChangeWatchedFiles notifications, so set this to
282 params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
284 params.Trace = "messages"
285 // TODO: support workspace folders.
287 resp, err := e.Server.Initialize(ctx, params)
289 return errors.Errorf("initialize: %w", err)
292 e.serverCapabilities = resp.Capabilities
295 if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
296 return errors.Errorf("initialized: %w", err)
299 // TODO: await initial configuration here, or expect gopls to manage that?
303 // onFileChanges is registered to be called by the Workdir on any writes that
304 // go through the Workdir API. It is called synchronously by the Workdir.
305 func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
310 // e may be locked when onFileChanges is called, but it is important that we
311 // synchronously increment this counter so that we can subsequently assert on
312 // the number of expected DidChangeWatchedFiles calls.
314 e.calls.DidChangeWatchedFiles++
317 // Since e may be locked, we must run this mutation asynchronously.
321 var lspevts []protocol.FileEvent
322 for _, evt := range evts {
323 // Always send an on-disk change, even for events that seem useless
324 // because they're shadowed by an open buffer.
325 lspevts = append(lspevts, evt.ProtocolEvent)
327 if buf, ok := e.buffers[evt.Path]; ok {
328 // Following VS Code, don't honor deletions or changes to dirty buffers.
329 if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted {
333 content, err := e.sandbox.Workdir.ReadFile(evt.Path)
335 continue // A race with some other operation.
337 // No need to update if the buffer content hasn't changed.
338 if content == strings.Join(buf.lines, "\n") {
341 // During shutdown, this call will fail. Ignore the error.
342 _ = e.setBufferContentLocked(ctx, evt.Path, false, strings.Split(content, "\n"), nil)
345 e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
351 // OpenFile creates a buffer for the given workdir-relative file.
352 func (e *Editor) OpenFile(ctx context.Context, path string) error {
353 content, err := e.sandbox.Workdir.ReadFile(path)
357 return e.createBuffer(ctx, path, false, content)
360 func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
361 uri := wd.URI(buf.path)
363 if strings.HasSuffix(buf.path, ".go") {
364 // TODO: what about go.mod files? What is their language ID?
367 return protocol.TextDocumentItem{
369 LanguageID: languageID,
370 Version: int32(buf.version),
375 // CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
376 // containing the given textual content.
377 func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
378 return e.createBuffer(ctx, path, true, content)
381 func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error {
385 lines: strings.Split(content, "\n"),
390 e.buffers[path] = buf
391 item := textDocumentItem(e.sandbox.Workdir, buf)
394 if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
397 return errors.Errorf("DidOpen: %w", err)
406 // CloseBuffer removes the current buffer (regardless of whether it is saved).
407 func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
409 _, ok := e.buffers[path]
412 return ErrUnknownBuffer
414 delete(e.buffers, path)
418 if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
419 TextDocument: e.textDocumentIdentifier(path),
421 return errors.Errorf("DidClose: %w", err)
430 func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
431 return protocol.TextDocumentIdentifier{
432 URI: e.sandbox.Workdir.URI(path),
436 // SaveBuffer writes the content of the buffer specified by the given path to
438 func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
439 if err := e.OrganizeImports(ctx, path); err != nil {
440 return errors.Errorf("organizing imports before save: %w", err)
442 if err := e.FormatBuffer(ctx, path); err != nil {
443 return errors.Errorf("formatting before save: %w", err)
445 return e.SaveBufferWithoutActions(ctx, path)
448 func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error {
451 buf, ok := e.buffers[path]
453 return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
455 content := buf.text()
457 syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
459 includeText = syncOptions.Save.IncludeText
462 docID := e.textDocumentIdentifier(buf.path)
464 if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
466 Reason: protocol.Manual,
468 return errors.Errorf("WillSave: %w", err)
471 if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
472 return errors.Errorf("writing %q: %w", path, err)
476 e.buffers[path] = buf
479 params := &protocol.DidSaveTextDocumentParams{
483 params.Text = &content
485 if err := e.Server.DidSave(ctx, params); err != nil {
486 return errors.Errorf("DidSave: %w", err)
495 // contentPosition returns the (Line, Column) position corresponding to offset
496 // in the buffer referenced by path.
497 func contentPosition(content string, offset int) (Pos, error) {
498 scanner := bufio.NewScanner(strings.NewReader(content))
502 end := start + len([]rune(scanner.Text())) + 1
504 return Pos{Line: line, Column: offset - start}, nil
509 if err := scanner.Err(); err != nil {
510 return Pos{}, errors.Errorf("scanning content: %w", err)
512 // Scan() will drop the last line if it is empty. Correct for this.
513 if (strings.HasSuffix(content, "\n") || content == "") && offset == start {
514 return Pos{Line: line, Column: 0}, nil
516 return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start)
519 // ErrNoMatch is returned if a regexp search fails.
521 ErrNoMatch = errors.New("no match")
522 ErrUnknownBuffer = errors.New("unknown buffer")
525 // regexpRange returns the start and end of the first occurrence of either re
526 // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match.
527 func regexpRange(content, re string) (Pos, Pos, error) {
529 rec, err := regexp.Compile(re)
531 return Pos{}, Pos{}, err
533 indexes := rec.FindStringSubmatchIndex(content)
535 return Pos{}, Pos{}, ErrNoMatch
537 switch len(indexes) {
539 // no subgroups: return the range of the regexp expression
540 start, end = indexes[0], indexes[1]
542 // one subgroup: return its range
543 start, end = indexes[2], indexes[3]
545 return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1)
547 startPos, err := contentPosition(content, start)
549 return Pos{}, Pos{}, err
551 endPos, err := contentPosition(content, end)
553 return Pos{}, Pos{}, err
555 return startPos, endPos, nil
558 // RegexpRange returns the first range in the buffer bufName matching re. See
559 // RegexpSearch for more information on matching.
560 func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) {
563 buf, ok := e.buffers[bufName]
565 return Pos{}, Pos{}, ErrUnknownBuffer
567 return regexpRange(buf.text(), re)
570 // RegexpSearch returns the position of the first match for re in the buffer
571 // bufName. For convenience, RegexpSearch supports the following two modes:
572 // 1. If re has no subgroups, return the position of the match for re itself.
573 // 2. If re has one subgroup, return the position of the first subgroup.
574 // It returns an error re is invalid, has more than one subgroup, or doesn't
576 func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) {
577 start, _, err := e.RegexpRange(bufName, re)
581 // RegexpReplace edits the buffer corresponding to path by replacing the first
582 // instance of re, or its first subgroup, with the replace text. See
583 // RegexpSearch for more explanation of these two modes.
584 // It returns an error if re is invalid, has more than one subgroup, or doesn't
586 func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error {
589 buf, ok := e.buffers[path]
591 return ErrUnknownBuffer
593 content := buf.text()
594 start, end, err := regexpRange(content, re)
598 return e.editBufferLocked(ctx, path, []Edit{{
605 // EditBuffer applies the given test edits to the buffer identified by path.
606 func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
609 return e.editBufferLocked(ctx, path, edits)
612 func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error {
615 lines := strings.Split(content, "\n")
616 return e.setBufferContentLocked(ctx, path, true, lines, nil)
619 // HasBuffer reports whether the file name is open in the editor.
620 func (e *Editor) HasBuffer(name string) bool {
623 _, ok := e.buffers[name]
627 // BufferText returns the content of the buffer with the given name.
628 func (e *Editor) BufferText(name string) string {
631 return e.buffers[name].text()
634 // BufferVersion returns the current version of the buffer corresponding to
635 // name (or 0 if it is not being edited).
636 func (e *Editor) BufferVersion(name string) int {
639 return e.buffers[name].version
642 func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error {
643 buf, ok := e.buffers[path]
645 return fmt.Errorf("unknown buffer %q", path)
647 content := make([]string, len(buf.lines))
648 copy(content, buf.lines)
649 content, err := editContent(content, edits)
653 return e.setBufferContentLocked(ctx, path, true, content, edits)
656 func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []string, fromEdits []Edit) error {
657 buf, ok := e.buffers[path]
659 return fmt.Errorf("unknown buffer %q", path)
664 e.buffers[path] = buf
665 // A simple heuristic: if there is only one edit, send it incrementally.
666 // Otherwise, send the entire content.
667 var evts []protocol.TextDocumentContentChangeEvent
668 if len(fromEdits) == 1 {
669 evts = append(evts, fromEdits[0].toProtocolChangeEvent())
671 evts = append(evts, protocol.TextDocumentContentChangeEvent{
675 params := &protocol.DidChangeTextDocumentParams{
676 TextDocument: protocol.VersionedTextDocumentIdentifier{
677 Version: int32(buf.version),
678 TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
680 ContentChanges: evts,
683 if err := e.Server.DidChange(ctx, params); err != nil {
684 return errors.Errorf("DidChange: %w", err)
693 // GoToDefinition jumps to the definition of the symbol at the given position
694 // in an open buffer.
695 func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
696 if err := e.checkBufferPosition(path, pos); err != nil {
697 return "", Pos{}, err
699 params := &protocol.DefinitionParams{}
700 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
701 params.Position = pos.ToProtocolPosition()
703 resp, err := e.Server.Definition(ctx, params)
705 return "", Pos{}, errors.Errorf("definition: %w", err)
708 return "", Pos{}, nil
710 newPath := e.sandbox.Workdir.URIToPath(resp[0].URI)
711 newPos := fromProtocolPosition(resp[0].Range.Start)
712 if !e.HasBuffer(newPath) {
713 if err := e.OpenFile(ctx, newPath); err != nil {
714 return "", Pos{}, errors.Errorf("OpenFile: %w", err)
717 return newPath, newPos, nil
720 // Symbol performs a workspace symbol search using query
721 func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) {
722 params := &protocol.WorkspaceSymbolParams{}
725 resp, err := e.Server.Symbol(ctx, params)
727 return nil, errors.Errorf("symbol: %w", err)
729 var res []SymbolInformation
730 for _, si := range resp {
732 path := e.sandbox.Workdir.URIToPath(ploc.URI)
733 start := fromProtocolPosition(ploc.Range.Start)
734 end := fromProtocolPosition(ploc.Range.End)
743 res = append(res, SymbolInformation{
752 // OrganizeImports requests and performs the source.organizeImports codeAction.
753 func (e *Editor) OrganizeImports(ctx context.Context, path string) error {
754 _, err := e.codeAction(ctx, path, nil, nil, protocol.SourceOrganizeImports)
758 // RefactorRewrite requests and performs the source.refactorRewrite codeAction.
759 func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error {
760 applied, err := e.codeAction(ctx, path, rng, nil, protocol.RefactorRewrite)
762 return errors.Errorf("no refactorings were applied")
767 // ApplyQuickFixes requests and performs the quickfix codeAction.
768 func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error {
769 applied, err := e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
771 return errors.Errorf("no quick fixes were applied")
776 // GetQuickFixes returns the available quick fix code actions.
777 func (e *Editor) GetQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) {
778 return e.getCodeActions(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
781 func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) {
782 actions, err := e.getCodeActions(ctx, path, rng, diagnostics, only...)
787 for _, action := range actions {
788 if action.Title == "" {
789 return 0, errors.Errorf("empty title for code action")
792 for _, o := range only {
793 if action.Kind == o {
802 for _, change := range action.Edit.DocumentChanges {
803 path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
804 if int32(e.buffers[path].version) != change.TextDocument.Version {
805 // Skip edits for old versions.
808 edits := convertEdits(change.Edits)
809 if err := e.EditBuffer(ctx, path, edits); err != nil {
810 return 0, errors.Errorf("editing buffer %q: %w", path, err)
813 // Execute any commands. The specification says that commands are
814 // executed after edits are applied.
815 if action.Command != nil {
816 if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
817 Command: action.Command.Command,
818 Arguments: action.Command.Arguments,
823 // Some commands may edit files on disk.
824 if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
831 func (e *Editor) getCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) {
835 params := &protocol.CodeActionParams{}
836 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
837 params.Context.Only = only
838 if diagnostics != nil {
839 params.Context.Diagnostics = diagnostics
844 return e.Server.CodeAction(ctx, params)
847 func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
852 // Ensure that this command was actually listed as a supported command.
853 for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
854 if command == params.Command {
860 return nil, fmt.Errorf("unsupported command %q", params.Command)
862 result, err := e.Server.ExecuteCommand(ctx, params)
866 // Some commands use the go command, which writes directly to disk.
867 // For convenience, check for those changes.
868 if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
874 func convertEdits(protocolEdits []protocol.TextEdit) []Edit {
876 for _, lspEdit := range protocolEdits {
877 edits = append(edits, fromProtocolTextEdit(lspEdit))
882 // FormatBuffer gofmts a Go file.
883 func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
888 version := e.buffers[path].version
890 params := &protocol.DocumentFormattingParams{}
891 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
892 resp, err := e.Server.Formatting(ctx, params)
894 return errors.Errorf("textDocument/formatting: %w", err)
898 if versionAfter := e.buffers[path].version; versionAfter != version {
899 return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter)
901 edits := convertEdits(resp)
905 return e.editBufferLocked(ctx, path, edits)
908 func (e *Editor) checkBufferPosition(path string, pos Pos) error {
911 buf, ok := e.buffers[path]
913 return fmt.Errorf("buffer %q is not open", path)
915 if !inText(pos, buf.lines) {
916 return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
921 // RunGenerate runs `go generate` non-recursively in the workdir-relative dir
922 // path. It does not report any resulting file changes as a watched file
923 // change, so must be followed by a call to Workdir.CheckForFileChanges once
924 // the generate command has completed.
925 // TODO(rFindley): this shouldn't be necessary anymore. Delete it.
926 func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
930 absDir := e.sandbox.Workdir.AbsPath(dir)
931 cmd, err := command.NewGenerateCommand("", command.GenerateArgs{
932 Dir: protocol.URIFromSpanURI(span.URIFromPath(absDir)),
938 params := &protocol.ExecuteCommandParams{
939 Command: cmd.Command,
940 Arguments: cmd.Arguments,
942 if _, err := e.ExecuteCommand(ctx, params); err != nil {
943 return fmt.Errorf("running generate: %v", err)
945 // Unfortunately we can't simply poll the workdir for file changes here,
946 // because server-side command may not have completed. In regtests, we can
947 // Await this state change, but here we must delegate that responsibility to
952 // CodeLens executes a codelens request on the server.
953 func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
958 _, ok := e.buffers[path]
961 return nil, fmt.Errorf("buffer %q is not open", path)
963 params := &protocol.CodeLensParams{
964 TextDocument: e.textDocumentIdentifier(path),
966 lens, err := e.Server.CodeLens(ctx, params)
973 // Completion executes a completion request on the server.
974 func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) {
979 _, ok := e.buffers[path]
982 return nil, fmt.Errorf("buffer %q is not open", path)
984 params := &protocol.CompletionParams{
985 TextDocumentPositionParams: protocol.TextDocumentPositionParams{
986 TextDocument: e.textDocumentIdentifier(path),
987 Position: pos.ToProtocolPosition(),
990 completions, err := e.Server.Completion(ctx, params)
994 return completions, nil
997 // AcceptCompletion accepts a completion for the given item at the given
999 func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, item protocol.CompletionItem) error {
1000 if e.Server == nil {
1005 _, ok := e.buffers[path]
1007 return fmt.Errorf("buffer %q is not open", path)
1009 return e.editBufferLocked(ctx, path, convertEdits(append([]protocol.TextEdit{
1011 }, item.AdditionalTextEdits...)))
1014 // References executes a reference request on the server.
1015 func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
1016 if e.Server == nil {
1020 _, ok := e.buffers[path]
1023 return nil, fmt.Errorf("buffer %q is not open", path)
1025 params := &protocol.ReferenceParams{
1026 TextDocumentPositionParams: protocol.TextDocumentPositionParams{
1027 TextDocument: e.textDocumentIdentifier(path),
1028 Position: pos.ToProtocolPosition(),
1030 Context: protocol.ReferenceContext{
1031 IncludeDeclaration: true,
1034 locations, err := e.Server.References(ctx, params)
1038 return locations, nil
1041 // CodeAction executes a codeAction request on the server.
1042 func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range) ([]protocol.CodeAction, error) {
1043 if e.Server == nil {
1047 _, ok := e.buffers[path]
1050 return nil, fmt.Errorf("buffer %q is not open", path)
1052 params := &protocol.CodeActionParams{
1053 TextDocument: e.textDocumentIdentifier(path),
1058 lens, err := e.Server.CodeAction(ctx, params)
1065 // Hover triggers a hover at the given position in an open buffer.
1066 func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) {
1067 if err := e.checkBufferPosition(path, pos); err != nil {
1068 return nil, Pos{}, err
1070 params := &protocol.HoverParams{}
1071 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
1072 params.Position = pos.ToProtocolPosition()
1074 resp, err := e.Server.Hover(ctx, params)
1076 return nil, Pos{}, errors.Errorf("hover: %w", err)
1079 return nil, Pos{}, nil
1081 return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
1084 func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) {
1085 if e.Server == nil {
1088 params := &protocol.DocumentLinkParams{}
1089 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
1090 return e.Server.DocumentLink(ctx, params)