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