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/protocol"
19 "golang.org/x/tools/internal/lsp/source"
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
52 func (b buffer) text() string {
53 return strings.Join(b.content, "\n")
56 // EditorConfig configures the editor's LSP session. This is similar to
57 // source.UserOptions, but we use a separate type here so that we expose only
58 // that configuration which we support.
60 // The zero value for EditorConfig should correspond to its defaults.
61 type EditorConfig struct {
65 // CodeLens is a map defining whether codelens are enabled, keyed by the
66 // codeLens command. CodeLens which are not present in this map are left in
67 // their default state.
68 CodeLens map[string]bool
70 // SymbolMatcher is the config associated with the "symbolMatcher" gopls
72 SymbolMatcher, SymbolStyle *string
74 // LimitWorkspaceScope is true if the user does not want to expand their
75 // workspace scope to the entire module.
76 LimitWorkspaceScope bool
78 // WithoutWorkspaceFolders is used to simulate opening a single file in the
79 // editor, without a workspace root. In that case, the client sends neither
80 // workspace folders nor a root URI.
81 WithoutWorkspaceFolders bool
83 // EditorRootPath specifies the root path of the workspace folder used when
84 // initializing gopls in the sandbox. If empty, the Workdir is used.
87 // EnableStaticcheck enables staticcheck analyzers.
88 EnableStaticcheck bool
90 // AllExperiments sets the "allExperiments" configuration, which enables
91 // all of gopls's opt-in settings.
94 // Whether to send the current process ID, for testing data that is joined to
95 // the PID. This can only be set by one test.
99 // NewEditor Creates a new Editor.
100 func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
102 buffers: make(map[string]buffer),
104 defaultEnv: sandbox.GoEnv(),
109 // Connect configures the editor to communicate with an LSP server on conn. It
110 // is not concurrency safe, and should be called at most once, before using the
113 // It returns the editor, so that it may be called as follows:
114 // editor, err := NewEditor(s).Connect(ctx, conn)
115 func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) {
117 e.Server = protocol.ServerDispatcher(conn)
118 e.client = &Client{editor: e, hooks: hooks}
121 protocol.ClientHandler(e.client,
122 jsonrpc2.MethodNotFound)))
123 if err := e.initialize(ctx, e.Config.WithoutWorkspaceFolders, e.Config.EditorRootPath); err != nil {
126 e.sandbox.Workdir.AddWatcher(e.onFileChanges)
130 // Shutdown issues the 'shutdown' LSP notification.
131 func (e *Editor) Shutdown(ctx context.Context) error {
133 if err := e.Server.Shutdown(ctx); err != nil {
134 return errors.Errorf("Shutdown: %w", err)
140 // Exit issues the 'exit' LSP notification.
141 func (e *Editor) Exit(ctx context.Context) error {
143 // Not all LSP clients issue the exit RPC, but we do so here to ensure that
144 // we gracefully handle it on multi-session servers.
145 if err := e.Server.Exit(ctx); err != nil {
146 return errors.Errorf("Exit: %w", err)
152 // Close issues the shutdown and exit sequence an editor should.
153 func (e *Editor) Close(ctx context.Context) error {
154 if err := e.Shutdown(ctx); err != nil {
157 if err := e.Exit(ctx); err != nil {
160 // called close on the editor should result in the connection closing
162 case <-e.serverConn.Done():
163 // connection closed itself
166 return errors.Errorf("connection not closed: %w", ctx.Err())
170 // Client returns the LSP client for this editor.
171 func (e *Editor) Client() *Client {
175 func (e *Editor) overlayEnv() map[string]string {
176 env := make(map[string]string)
177 for k, v := range e.defaultEnv {
180 for k, v := range e.Config.Env {
186 func (e *Editor) configuration() map[string]interface{} {
187 config := map[string]interface{}{
188 "verboseWorkDoneProgress": true,
189 "env": e.overlayEnv(),
190 "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope,
191 "completionBudget": "10s",
194 if e.Config.BuildFlags != nil {
195 config["buildFlags"] = e.Config.BuildFlags
198 if e.Config.CodeLens != nil {
199 config["codelens"] = e.Config.CodeLens
201 if e.Config.SymbolMatcher != nil {
202 config["symbolMatcher"] = *e.Config.SymbolMatcher
204 if e.Config.SymbolStyle != nil {
205 config["symbolStyle"] = *e.Config.SymbolStyle
207 if e.Config.EnableStaticcheck {
208 config["staticcheck"] = true
210 if e.Config.AllExperiments {
211 config["allExperiments"] = true
214 // TODO(rFindley): uncomment this if/when diagnostics delay is on by
215 // default... and probably change to the new settings name.
216 // config["experimentalDiagnosticsDelay"] = "10ms"
218 // ExperimentalWorkspaceModule is only set as a mode, not a configuration.
222 func (e *Editor) initialize(ctx context.Context, withoutWorkspaceFolders bool, editorRootPath string) error {
223 params := &protocol.ParamInitialize{}
224 params.ClientInfo.Name = "fakeclient"
225 params.ClientInfo.Version = "v1.0.0"
226 if !withoutWorkspaceFolders {
227 rootURI := e.sandbox.Workdir.RootURI()
228 if editorRootPath != "" {
229 rootURI = toURI(e.sandbox.Workdir.AbsPath(editorRootPath))
231 params.WorkspaceFolders = []protocol.WorkspaceFolder{{
232 URI: string(rootURI),
233 Name: filepath.Base(rootURI.SpanURI().Filename()),
236 params.Capabilities.Workspace.Configuration = true
237 params.Capabilities.Window.WorkDoneProgress = true
238 // TODO: set client capabilities
239 params.InitializationOptions = e.configuration()
240 if e.Config.SendPID {
241 params.ProcessID = float64(os.Getpid())
244 // This is a bit of a hack, since the fake editor doesn't actually support
245 // watching changed files that match a specific glob pattern. However, the
246 // editor does send didChangeWatchedFiles notifications, so set this to
248 params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true
250 params.Trace = "messages"
251 // TODO: support workspace folders.
253 resp, err := e.Server.Initialize(ctx, params)
255 return errors.Errorf("initialize: %w", err)
258 e.serverCapabilities = resp.Capabilities
261 if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
262 return errors.Errorf("initialized: %w", err)
265 // TODO: await initial configuration here, or expect gopls to manage that?
269 func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) {
273 var lspevts []protocol.FileEvent
274 for _, evt := range evts {
275 lspevts = append(lspevts, evt.ProtocolEvent)
277 e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{
282 // OpenFile creates a buffer for the given workdir-relative file.
283 func (e *Editor) OpenFile(ctx context.Context, path string) error {
284 content, err := e.sandbox.Workdir.ReadFile(path)
288 return e.CreateBuffer(ctx, path, content)
291 func newBuffer(path, content string) buffer {
295 content: strings.Split(content, "\n"),
299 func textDocumentItem(wd *Workdir, buf buffer) protocol.TextDocumentItem {
300 uri := wd.URI(buf.path)
302 if strings.HasSuffix(buf.path, ".go") {
303 // TODO: what about go.mod files? What is their language ID?
306 return protocol.TextDocumentItem{
308 LanguageID: languageID,
309 Version: float64(buf.version),
314 // CreateBuffer creates a new unsaved buffer corresponding to the workdir path,
315 // containing the given textual content.
316 func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error {
317 buf := newBuffer(path, content)
319 e.buffers[path] = buf
320 item := textDocumentItem(e.sandbox.Workdir, buf)
324 if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{
327 return errors.Errorf("DidOpen: %w", err)
333 // CloseBuffer removes the current buffer (regardless of whether it is saved).
334 func (e *Editor) CloseBuffer(ctx context.Context, path string) error {
336 _, ok := e.buffers[path]
339 return ErrUnknownBuffer
341 delete(e.buffers, path)
345 if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{
346 TextDocument: e.textDocumentIdentifier(path),
348 return errors.Errorf("DidClose: %w", err)
354 func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier {
355 return protocol.TextDocumentIdentifier{
356 URI: e.sandbox.Workdir.URI(path),
360 // SaveBuffer writes the content of the buffer specified by the given path to
362 func (e *Editor) SaveBuffer(ctx context.Context, path string) error {
363 if err := e.OrganizeImports(ctx, path); err != nil {
364 return errors.Errorf("organizing imports before save: %w", err)
366 if err := e.FormatBuffer(ctx, path); err != nil {
367 return errors.Errorf("formatting before save: %w", err)
369 return e.SaveBufferWithoutActions(ctx, path)
372 func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error {
374 buf, ok := e.buffers[path]
377 return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path))
379 content := buf.text()
381 syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions)
383 includeText = syncOptions.Save.IncludeText
387 docID := e.textDocumentIdentifier(buf.path)
389 if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{
391 Reason: protocol.Manual,
393 return errors.Errorf("WillSave: %w", err)
396 if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil {
397 return errors.Errorf("writing %q: %w", path, err)
400 params := &protocol.DidSaveTextDocumentParams{
401 TextDocument: protocol.VersionedTextDocumentIdentifier{
402 Version: float64(buf.version),
403 TextDocumentIdentifier: docID,
407 params.Text = &content
409 if err := e.Server.DidSave(ctx, params); err != nil {
410 return errors.Errorf("DidSave: %w", err)
416 // contentPosition returns the (Line, Column) position corresponding to offset
417 // in the buffer referenced by path.
418 func contentPosition(content string, offset int) (Pos, error) {
419 scanner := bufio.NewScanner(strings.NewReader(content))
423 end := start + len([]rune(scanner.Text())) + 1
425 return Pos{Line: line, Column: offset - start}, nil
430 if err := scanner.Err(); err != nil {
431 return Pos{}, errors.Errorf("scanning content: %w", err)
433 // Scan() will drop the last line if it is empty. Correct for this.
434 if (strings.HasSuffix(content, "\n") || content == "") && offset == start {
435 return Pos{Line: line, Column: 0}, nil
437 return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start)
440 // ErrNoMatch is returned if a regexp search fails.
442 ErrNoMatch = errors.New("no match")
443 ErrUnknownBuffer = errors.New("unknown buffer")
446 // regexpRange returns the start and end of the first occurrence of either re
447 // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match.
448 func regexpRange(content, re string) (Pos, Pos, error) {
450 rec, err := regexp.Compile(re)
452 return Pos{}, Pos{}, err
454 indexes := rec.FindStringSubmatchIndex(content)
456 return Pos{}, Pos{}, ErrNoMatch
458 switch len(indexes) {
460 // no subgroups: return the range of the regexp expression
461 start, end = indexes[0], indexes[1]
463 // one subgroup: return its range
464 start, end = indexes[2], indexes[3]
466 return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1)
468 startPos, err := contentPosition(content, start)
470 return Pos{}, Pos{}, err
472 endPos, err := contentPosition(content, end)
474 return Pos{}, Pos{}, err
476 return startPos, endPos, nil
479 // RegexpRange returns the first range in the buffer bufName matching re. See
480 // RegexpSearch for more information on matching.
481 func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) {
484 buf, ok := e.buffers[bufName]
486 return Pos{}, Pos{}, ErrUnknownBuffer
488 return regexpRange(buf.text(), re)
491 // RegexpSearch returns the position of the first match for re in the buffer
492 // bufName. For convenience, RegexpSearch supports the following two modes:
493 // 1. If re has no subgroups, return the position of the match for re itself.
494 // 2. If re has one subgroup, return the position of the first subgroup.
495 // It returns an error re is invalid, has more than one subgroup, or doesn't
497 func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) {
498 start, _, err := e.RegexpRange(bufName, re)
502 // RegexpReplace edits the buffer corresponding to path by replacing the first
503 // instance of re, or its first subgroup, with the replace text. See
504 // RegexpSearch for more explanation of these two modes.
505 // It returns an error if re is invalid, has more than one subgroup, or doesn't
507 func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error {
510 buf, ok := e.buffers[path]
512 return ErrUnknownBuffer
514 content := buf.text()
515 start, end, err := regexpRange(content, re)
519 return e.editBufferLocked(ctx, path, []Edit{{
526 // EditBuffer applies the given test edits to the buffer identified by path.
527 func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error {
530 return e.editBufferLocked(ctx, path, edits)
533 func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error {
536 lines := strings.Split(content, "\n")
537 return e.setBufferContentLocked(ctx, path, lines, nil)
540 // BufferText returns the content of the buffer with the given name.
541 func (e *Editor) BufferText(name string) string {
544 return e.buffers[name].text()
547 // BufferVersion returns the current version of the buffer corresponding to
548 // name (or 0 if it is not being edited).
549 func (e *Editor) BufferVersion(name string) int {
552 return e.buffers[name].version
555 func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error {
556 buf, ok := e.buffers[path]
558 return fmt.Errorf("unknown buffer %q", path)
560 content := make([]string, len(buf.content))
561 copy(content, buf.content)
562 content, err := editContent(content, edits)
566 return e.setBufferContentLocked(ctx, path, content, edits)
569 func (e *Editor) setBufferContentLocked(ctx context.Context, path string, content []string, fromEdits []Edit) error {
570 buf, ok := e.buffers[path]
572 return fmt.Errorf("unknown buffer %q", path)
574 buf.content = content
576 e.buffers[path] = buf
577 // A simple heuristic: if there is only one edit, send it incrementally.
578 // Otherwise, send the entire content.
579 var evts []protocol.TextDocumentContentChangeEvent
580 if len(fromEdits) == 1 {
581 evts = append(evts, fromEdits[0].toProtocolChangeEvent())
583 evts = append(evts, protocol.TextDocumentContentChangeEvent{
587 params := &protocol.DidChangeTextDocumentParams{
588 TextDocument: protocol.VersionedTextDocumentIdentifier{
589 Version: float64(buf.version),
590 TextDocumentIdentifier: e.textDocumentIdentifier(buf.path),
592 ContentChanges: evts,
595 if err := e.Server.DidChange(ctx, params); err != nil {
596 return errors.Errorf("DidChange: %w", err)
602 // GoToDefinition jumps to the definition of the symbol at the given position
603 // in an open buffer.
604 func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) {
605 if err := e.checkBufferPosition(path, pos); err != nil {
606 return "", Pos{}, err
608 params := &protocol.DefinitionParams{}
609 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
610 params.Position = pos.ToProtocolPosition()
612 resp, err := e.Server.Definition(ctx, params)
614 return "", Pos{}, errors.Errorf("definition: %w", err)
617 return "", Pos{}, nil
619 newPath := e.sandbox.Workdir.URIToPath(resp[0].URI)
620 newPos := fromProtocolPosition(resp[0].Range.Start)
621 if err := e.OpenFile(ctx, newPath); err != nil {
622 return "", Pos{}, errors.Errorf("OpenFile: %w", err)
624 return newPath, newPos, nil
627 // Symbol performs a workspace symbol search using query
628 func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) {
629 params := &protocol.WorkspaceSymbolParams{}
632 resp, err := e.Server.Symbol(ctx, params)
634 return nil, errors.Errorf("symbol: %w", err)
636 var res []SymbolInformation
637 for _, si := range resp {
639 path := e.sandbox.Workdir.URIToPath(ploc.URI)
640 start := fromProtocolPosition(ploc.Range.Start)
641 end := fromProtocolPosition(ploc.Range.End)
650 res = append(res, SymbolInformation{
659 // OrganizeImports requests and performs the source.organizeImports codeAction.
660 func (e *Editor) OrganizeImports(ctx context.Context, path string) error {
661 return e.codeAction(ctx, path, nil, nil, protocol.SourceOrganizeImports)
664 // RefactorRewrite requests and performs the source.refactorRewrite codeAction.
665 func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error {
666 return e.codeAction(ctx, path, rng, nil, protocol.RefactorRewrite)
669 // ApplyQuickFixes requests and performs the quickfix codeAction.
670 func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error {
671 return e.codeAction(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll)
674 func (e *Editor) codeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) error {
678 params := &protocol.CodeActionParams{}
679 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
680 params.Context.Only = only
681 if diagnostics != nil {
682 params.Context.Diagnostics = diagnostics
687 actions, err := e.Server.CodeAction(ctx, params)
689 return errors.Errorf("textDocument/codeAction: %w", err)
691 for _, action := range actions {
693 for _, o := range only {
694 if action.Kind == o {
702 for _, change := range action.Edit.DocumentChanges {
703 path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI)
704 if float64(e.buffers[path].version) != change.TextDocument.Version {
705 // Skip edits for old versions.
708 edits := convertEdits(change.Edits)
709 if err := e.EditBuffer(ctx, path, edits); err != nil {
710 return errors.Errorf("editing buffer %q: %w", path, err)
713 // Execute any commands. The specification says that commands are
714 // executed after edits are applied.
715 if action.Command != nil {
716 if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{
717 Command: action.Command.Command,
718 Arguments: action.Command.Arguments,
727 func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) {
732 // Ensure that this command was actually listed as a supported command.
733 for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands {
734 if command == params.Command {
740 return nil, fmt.Errorf("unsupported command %q", params.Command)
742 return e.Server.ExecuteCommand(ctx, params)
745 func convertEdits(protocolEdits []protocol.TextEdit) []Edit {
747 for _, lspEdit := range protocolEdits {
748 edits = append(edits, fromProtocolTextEdit(lspEdit))
753 // FormatBuffer gofmts a Go file.
754 func (e *Editor) FormatBuffer(ctx context.Context, path string) error {
759 version := e.buffers[path].version
761 params := &protocol.DocumentFormattingParams{}
762 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
763 resp, err := e.Server.Formatting(ctx, params)
765 return errors.Errorf("textDocument/formatting: %w", err)
769 if versionAfter := e.buffers[path].version; versionAfter != version {
770 return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter)
772 edits := convertEdits(resp)
773 return e.editBufferLocked(ctx, path, edits)
776 func (e *Editor) checkBufferPosition(path string, pos Pos) error {
779 buf, ok := e.buffers[path]
781 return fmt.Errorf("buffer %q is not open", path)
783 if !inText(pos, buf.content) {
784 return fmt.Errorf("position %v is invalid in buffer %q", pos, path)
789 // RunGenerate runs `go generate` non-recursively in the workdir-relative dir
790 // path. It does not report any resulting file changes as a watched file
791 // change, so must be followed by a call to Workdir.CheckForFileChanges once
792 // the generate command has completed.
793 func (e *Editor) RunGenerate(ctx context.Context, dir string) error {
797 absDir := e.sandbox.Workdir.AbsPath(dir)
798 jsonArgs, err := source.MarshalArgs(span.URIFromPath(absDir), false)
802 params := &protocol.ExecuteCommandParams{
803 Command: source.CommandGenerate.ID(),
806 if _, err := e.ExecuteCommand(ctx, params); err != nil {
807 return fmt.Errorf("running generate: %v", err)
809 // Unfortunately we can't simply poll the workdir for file changes here,
810 // because server-side command may not have completed. In regtests, we can
811 // Await this state change, but here we must delegate that responsibility to
816 // CodeLens executes a codelens request on the server.
817 func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) {
822 _, ok := e.buffers[path]
825 return nil, fmt.Errorf("buffer %q is not open", path)
827 params := &protocol.CodeLensParams{
828 TextDocument: e.textDocumentIdentifier(path),
830 lens, err := e.Server.CodeLens(ctx, params)
837 // Completion executes a completion request on the server.
838 func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) {
843 _, ok := e.buffers[path]
846 return nil, fmt.Errorf("buffer %q is not open", path)
848 params := &protocol.CompletionParams{
849 TextDocumentPositionParams: protocol.TextDocumentPositionParams{
850 TextDocument: e.textDocumentIdentifier(path),
851 Position: pos.ToProtocolPosition(),
854 completions, err := e.Server.Completion(ctx, params)
858 return completions, nil
861 // References executes a reference request on the server.
862 func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) {
867 _, ok := e.buffers[path]
870 return nil, fmt.Errorf("buffer %q is not open", path)
872 params := &protocol.ReferenceParams{
873 TextDocumentPositionParams: protocol.TextDocumentPositionParams{
874 TextDocument: e.textDocumentIdentifier(path),
875 Position: pos.ToProtocolPosition(),
877 Context: protocol.ReferenceContext{
878 IncludeDeclaration: true,
881 locations, err := e.Server.References(ctx, params)
885 return locations, nil
888 // CodeAction executes a codeAction request on the server.
889 func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range) ([]protocol.CodeAction, error) {
894 _, ok := e.buffers[path]
897 return nil, fmt.Errorf("buffer %q is not open", path)
899 params := &protocol.CodeActionParams{
900 TextDocument: e.textDocumentIdentifier(path),
905 lens, err := e.Server.CodeAction(ctx, params)
912 // Hover triggers a hover at the given position in an open buffer.
913 func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) {
914 if err := e.checkBufferPosition(path, pos); err != nil {
915 return nil, Pos{}, err
917 params := &protocol.HoverParams{}
918 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
919 params.Position = pos.ToProtocolPosition()
921 resp, err := e.Server.Hover(ctx, params)
923 return nil, Pos{}, errors.Errorf("hover: %w", err)
926 return nil, Pos{}, nil
928 return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil
931 func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) {
935 params := &protocol.DocumentLinkParams{}
936 params.TextDocument.URI = e.sandbox.Workdir.URI(path)
937 return e.Server.DocumentLink(ctx, params)