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