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