github.com/powerman/golang-tools@v0.1.11-0.20220410185822-5ad214d8d803/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" 13 "path/filepath" 14 "regexp" 15 "strings" 16 "sync" 17 18 "github.com/powerman/golang-tools/internal/jsonrpc2" 19 "github.com/powerman/golang-tools/internal/lsp/command" 20 "github.com/powerman/golang-tools/internal/lsp/protocol" 21 "github.com/powerman/golang-tools/internal/span" 22 errors "golang.org/x/xerrors" 23 ) 24 25 // Editor is a fake editor client. It keeps track of client state and can be 26 // used for writing LSP tests. 27 type Editor struct { 28 Config EditorConfig 29 30 // Server, client, and sandbox are concurrency safe and written only 31 // at construction time, so do not require synchronization. 32 Server protocol.Server 33 serverConn jsonrpc2.Conn 34 client *Client 35 sandbox *Sandbox 36 defaultEnv map[string]string 37 38 // Since this editor is intended just for testing, we use very coarse 39 // locking. 40 mu sync.Mutex 41 // Editor state. 42 buffers map[string]buffer 43 // Capabilities / Options 44 serverCapabilities protocol.ServerCapabilities 45 46 // Call metrics for the purpose of expectations. This is done in an ad-hoc 47 // manner for now. Perhaps in the future we should do something more 48 // systematic. Guarded with a separate mutex as calls may need to be accessed 49 // asynchronously via callbacks into the Editor. 50 callsMu sync.Mutex 51 calls CallCounts 52 } 53 54 type CallCounts struct { 55 DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose uint64 56 } 57 58 type buffer struct { 59 windowsLineEndings bool 60 version int 61 path string 62 lines []string 63 dirty bool 64 } 65 66 func (b buffer) text() string { 67 eol := "\n" 68 if b.windowsLineEndings { 69 eol = "\r\n" 70 } 71 return strings.Join(b.lines, eol) 72 } 73 74 // EditorConfig configures the editor's LSP session. This is similar to 75 // source.UserOptions, but we use a separate type here so that we expose only 76 // that configuration which we support. 77 // 78 // The zero value for EditorConfig should correspond to its defaults. 79 type EditorConfig struct { 80 Env map[string]string 81 BuildFlags []string 82 83 // CodeLenses is a map defining whether codelens are enabled, keyed by the 84 // codeLens command. CodeLenses which are not present in this map are left in 85 // their default state. 86 CodeLenses map[string]bool 87 88 // SymbolMatcher is the config associated with the "symbolMatcher" gopls 89 // config option. 90 SymbolMatcher, SymbolStyle *string 91 92 // LimitWorkspaceScope is true if the user does not want to expand their 93 // workspace scope to the entire module. 94 LimitWorkspaceScope bool 95 96 // WorkspaceFolders is the workspace folders to configure on the LSP server, 97 // relative to the sandbox workdir. 98 // 99 // As a special case, if WorkspaceFolders is nil the editor defaults to 100 // configuring a single workspace folder corresponding to the workdir root. 101 // To explicitly send no workspace folders, use an empty (non-nil) slice. 102 WorkspaceFolders []string 103 104 // AllExperiments sets the "allExperiments" configuration, which enables 105 // all of gopls's opt-in settings. 106 AllExperiments bool 107 108 // Whether to send the current process ID, for testing data that is joined to 109 // the PID. This can only be set by one test. 110 SendPID bool 111 112 // Whether to edit files with windows line endings. 113 WindowsLineEndings bool 114 115 // Map of language ID -> regexp to match, used to set the file type of new 116 // buffers. Applied as an overlay on top of the following defaults: 117 // "go" -> ".*\.go" 118 // "go.mod" -> "go\.mod" 119 // "go.sum" -> "go\.sum" 120 // "gotmpl" -> ".*tmpl" 121 FileAssociations map[string]string 122 123 // Settings holds arbitrary additional settings to apply to the gopls config. 124 // TODO(rfindley): replace existing EditorConfig fields with Settings. 125 Settings map[string]interface{} 126 127 ImportShortcut string 128 DirectoryFilters []string 129 VerboseOutput bool 130 ExperimentalUseInvalidMetadata bool 131 } 132 133 // NewEditor Creates a new Editor. 134 func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { 135 return &Editor{ 136 buffers: make(map[string]buffer), 137 sandbox: sandbox, 138 defaultEnv: sandbox.GoEnv(), 139 Config: config, 140 } 141 } 142 143 // Connect configures the editor to communicate with an LSP server on conn. It 144 // is not concurrency safe, and should be called at most once, before using the 145 // editor. 146 // 147 // It returns the editor, so that it may be called as follows: 148 // editor, err := NewEditor(s).Connect(ctx, conn) 149 func (e *Editor) Connect(ctx context.Context, conn jsonrpc2.Conn, hooks ClientHooks) (*Editor, error) { 150 e.serverConn = conn 151 e.Server = protocol.ServerDispatcher(conn) 152 e.client = &Client{editor: e, hooks: hooks} 153 conn.Go(ctx, 154 protocol.Handlers( 155 protocol.ClientHandler(e.client, 156 jsonrpc2.MethodNotFound))) 157 if err := e.initialize(ctx, e.Config.WorkspaceFolders); err != nil { 158 return nil, err 159 } 160 e.sandbox.Workdir.AddWatcher(e.onFileChanges) 161 return e, nil 162 } 163 164 func (e *Editor) Stats() CallCounts { 165 e.callsMu.Lock() 166 defer e.callsMu.Unlock() 167 return e.calls 168 } 169 170 // Shutdown issues the 'shutdown' LSP notification. 171 func (e *Editor) Shutdown(ctx context.Context) error { 172 if e.Server != nil { 173 if err := e.Server.Shutdown(ctx); err != nil { 174 return errors.Errorf("Shutdown: %w", err) 175 } 176 } 177 return nil 178 } 179 180 // Exit issues the 'exit' LSP notification. 181 func (e *Editor) Exit(ctx context.Context) error { 182 if e.Server != nil { 183 // Not all LSP clients issue the exit RPC, but we do so here to ensure that 184 // we gracefully handle it on multi-session servers. 185 if err := e.Server.Exit(ctx); err != nil { 186 return errors.Errorf("Exit: %w", err) 187 } 188 } 189 return nil 190 } 191 192 // Close issues the shutdown and exit sequence an editor should. 193 func (e *Editor) Close(ctx context.Context) error { 194 if err := e.Shutdown(ctx); err != nil { 195 return err 196 } 197 if err := e.Exit(ctx); err != nil { 198 return err 199 } 200 // called close on the editor should result in the connection closing 201 select { 202 case <-e.serverConn.Done(): 203 // connection closed itself 204 return nil 205 case <-ctx.Done(): 206 return errors.Errorf("connection not closed: %w", ctx.Err()) 207 } 208 } 209 210 // Client returns the LSP client for this editor. 211 func (e *Editor) Client() *Client { 212 return e.client 213 } 214 215 func (e *Editor) overlayEnv() map[string]string { 216 env := make(map[string]string) 217 for k, v := range e.defaultEnv { 218 v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) 219 env[k] = v 220 } 221 for k, v := range e.Config.Env { 222 v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", e.sandbox.Workdir.RootURI().SpanURI().Filename()) 223 env[k] = v 224 } 225 return env 226 } 227 228 func (e *Editor) configuration() map[string]interface{} { 229 config := map[string]interface{}{ 230 "verboseWorkDoneProgress": true, 231 "env": e.overlayEnv(), 232 "expandWorkspaceToModule": !e.Config.LimitWorkspaceScope, 233 "completionBudget": "10s", 234 } 235 236 for k, v := range e.Config.Settings { 237 config[k] = v 238 } 239 240 if e.Config.BuildFlags != nil { 241 config["buildFlags"] = e.Config.BuildFlags 242 } 243 if e.Config.DirectoryFilters != nil { 244 config["directoryFilters"] = e.Config.DirectoryFilters 245 } 246 if e.Config.ExperimentalUseInvalidMetadata { 247 config["experimentalUseInvalidMetadata"] = true 248 } 249 if e.Config.CodeLenses != nil { 250 config["codelenses"] = e.Config.CodeLenses 251 } 252 if e.Config.SymbolMatcher != nil { 253 config["symbolMatcher"] = *e.Config.SymbolMatcher 254 } 255 if e.Config.SymbolStyle != nil { 256 config["symbolStyle"] = *e.Config.SymbolStyle 257 } 258 if e.Config.AllExperiments { 259 config["allExperiments"] = true 260 } 261 262 if e.Config.VerboseOutput { 263 config["verboseOutput"] = true 264 } 265 266 if e.Config.ImportShortcut != "" { 267 config["importShortcut"] = e.Config.ImportShortcut 268 } 269 270 config["diagnosticsDelay"] = "10ms" 271 272 // ExperimentalWorkspaceModule is only set as a mode, not a configuration. 273 return config 274 } 275 276 func (e *Editor) initialize(ctx context.Context, workspaceFolders []string) error { 277 params := &protocol.ParamInitialize{} 278 params.ClientInfo.Name = "fakeclient" 279 params.ClientInfo.Version = "v1.0.0" 280 281 if workspaceFolders == nil { 282 workspaceFolders = []string{string(e.sandbox.Workdir.RelativeTo)} 283 } 284 for _, folder := range workspaceFolders { 285 params.WorkspaceFolders = append(params.WorkspaceFolders, protocol.WorkspaceFolder{ 286 URI: string(e.sandbox.Workdir.URI(folder)), 287 Name: filepath.Base(folder), 288 }) 289 } 290 291 params.Capabilities.Workspace.Configuration = true 292 params.Capabilities.Window.WorkDoneProgress = true 293 // TODO: set client capabilities 294 params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} 295 params.InitializationOptions = e.configuration() 296 if e.Config.SendPID { 297 params.ProcessID = int32(os.Getpid()) 298 } 299 300 params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true 301 params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true 302 // copied from lsp/semantic.go to avoid import cycle in tests 303 params.Capabilities.TextDocument.SemanticTokens.TokenTypes = []string{ 304 "namespace", "type", "class", "enum", "interface", 305 "struct", "typeParameter", "parameter", "variable", "property", "enumMember", 306 "event", "function", "method", "macro", "keyword", "modifier", "comment", 307 "string", "number", "regexp", "operator", 308 } 309 310 // This is a bit of a hack, since the fake editor doesn't actually support 311 // watching changed files that match a specific glob pattern. However, the 312 // editor does send didChangeWatchedFiles notifications, so set this to 313 // true. 314 params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true 315 316 params.Trace = "messages" 317 // TODO: support workspace folders. 318 if e.Server != nil { 319 resp, err := e.Server.Initialize(ctx, params) 320 if err != nil { 321 return errors.Errorf("initialize: %w", err) 322 } 323 e.mu.Lock() 324 e.serverCapabilities = resp.Capabilities 325 e.mu.Unlock() 326 327 if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 328 return errors.Errorf("initialized: %w", err) 329 } 330 } 331 // TODO: await initial configuration here, or expect gopls to manage that? 332 return nil 333 } 334 335 // onFileChanges is registered to be called by the Workdir on any writes that 336 // go through the Workdir API. It is called synchronously by the Workdir. 337 func (e *Editor) onFileChanges(ctx context.Context, evts []FileEvent) { 338 if e.Server == nil { 339 return 340 } 341 342 // e may be locked when onFileChanges is called, but it is important that we 343 // synchronously increment this counter so that we can subsequently assert on 344 // the number of expected DidChangeWatchedFiles calls. 345 e.callsMu.Lock() 346 e.calls.DidChangeWatchedFiles++ 347 e.callsMu.Unlock() 348 349 // Since e may be locked, we must run this mutation asynchronously. 350 go func() { 351 e.mu.Lock() 352 defer e.mu.Unlock() 353 var lspevts []protocol.FileEvent 354 for _, evt := range evts { 355 // Always send an on-disk change, even for events that seem useless 356 // because they're shadowed by an open buffer. 357 lspevts = append(lspevts, evt.ProtocolEvent) 358 359 if buf, ok := e.buffers[evt.Path]; ok { 360 // Following VS Code, don't honor deletions or changes to dirty buffers. 361 if buf.dirty || evt.ProtocolEvent.Type == protocol.Deleted { 362 continue 363 } 364 365 content, err := e.sandbox.Workdir.ReadFile(evt.Path) 366 if err != nil { 367 continue // A race with some other operation. 368 } 369 // No need to update if the buffer content hasn't changed. 370 if content == buf.text() { 371 continue 372 } 373 // During shutdown, this call will fail. Ignore the error. 374 _ = e.setBufferContentLocked(ctx, evt.Path, false, lines(content), nil) 375 } 376 } 377 e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ 378 Changes: lspevts, 379 }) 380 }() 381 } 382 383 // OpenFile creates a buffer for the given workdir-relative file. 384 func (e *Editor) OpenFile(ctx context.Context, path string) error { 385 content, err := e.sandbox.Workdir.ReadFile(path) 386 if err != nil { 387 return err 388 } 389 return e.createBuffer(ctx, path, false, content) 390 } 391 392 // CreateBuffer creates a new unsaved buffer corresponding to the workdir path, 393 // containing the given textual content. 394 func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { 395 return e.createBuffer(ctx, path, true, content) 396 } 397 398 func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content string) error { 399 buf := buffer{ 400 windowsLineEndings: e.Config.WindowsLineEndings, 401 version: 1, 402 path: path, 403 lines: lines(content), 404 dirty: dirty, 405 } 406 e.mu.Lock() 407 defer e.mu.Unlock() 408 e.buffers[path] = buf 409 410 item := protocol.TextDocumentItem{ 411 URI: e.sandbox.Workdir.URI(buf.path), 412 LanguageID: e.languageID(buf.path), 413 Version: int32(buf.version), 414 Text: buf.text(), 415 } 416 417 if e.Server != nil { 418 if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ 419 TextDocument: item, 420 }); err != nil { 421 return errors.Errorf("DidOpen: %w", err) 422 } 423 e.callsMu.Lock() 424 e.calls.DidOpen++ 425 e.callsMu.Unlock() 426 } 427 return nil 428 } 429 430 var defaultFileAssociations = map[string]*regexp.Regexp{ 431 "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! 432 "go.mod": regexp.MustCompile(`^go\.mod$`), 433 "go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`), 434 "go.work": regexp.MustCompile(`^go\.work$`), 435 "gotmpl": regexp.MustCompile(`^.*tmpl$`), 436 } 437 438 func (e *Editor) languageID(p string) string { 439 base := path.Base(p) 440 for lang, re := range e.Config.FileAssociations { 441 re := regexp.MustCompile(re) 442 if re.MatchString(base) { 443 return lang 444 } 445 } 446 for lang, re := range defaultFileAssociations { 447 if re.MatchString(base) { 448 return lang 449 } 450 } 451 return "" 452 } 453 454 // lines returns line-ending agnostic line representation of content. 455 func lines(content string) []string { 456 lines := strings.Split(content, "\n") 457 for i, l := range lines { 458 lines[i] = strings.TrimSuffix(l, "\r") 459 } 460 return lines 461 } 462 463 // CloseBuffer removes the current buffer (regardless of whether it is saved). 464 func (e *Editor) CloseBuffer(ctx context.Context, path string) error { 465 e.mu.Lock() 466 _, ok := e.buffers[path] 467 if !ok { 468 e.mu.Unlock() 469 return ErrUnknownBuffer 470 } 471 delete(e.buffers, path) 472 e.mu.Unlock() 473 474 if e.Server != nil { 475 if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ 476 TextDocument: e.textDocumentIdentifier(path), 477 }); err != nil { 478 return errors.Errorf("DidClose: %w", err) 479 } 480 e.callsMu.Lock() 481 e.calls.DidClose++ 482 e.callsMu.Unlock() 483 } 484 return nil 485 } 486 487 func (e *Editor) textDocumentIdentifier(path string) protocol.TextDocumentIdentifier { 488 return protocol.TextDocumentIdentifier{ 489 URI: e.sandbox.Workdir.URI(path), 490 } 491 } 492 493 // SaveBuffer writes the content of the buffer specified by the given path to 494 // the filesystem. 495 func (e *Editor) SaveBuffer(ctx context.Context, path string) error { 496 if err := e.OrganizeImports(ctx, path); err != nil { 497 return errors.Errorf("organizing imports before save: %w", err) 498 } 499 if err := e.FormatBuffer(ctx, path); err != nil { 500 return errors.Errorf("formatting before save: %w", err) 501 } 502 return e.SaveBufferWithoutActions(ctx, path) 503 } 504 505 func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error { 506 e.mu.Lock() 507 defer e.mu.Unlock() 508 buf, ok := e.buffers[path] 509 if !ok { 510 return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path)) 511 } 512 content := buf.text() 513 includeText := false 514 syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions) 515 if ok { 516 includeText = syncOptions.Save.IncludeText 517 } 518 519 docID := e.textDocumentIdentifier(buf.path) 520 if e.Server != nil { 521 if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ 522 TextDocument: docID, 523 Reason: protocol.Manual, 524 }); err != nil { 525 return errors.Errorf("WillSave: %w", err) 526 } 527 } 528 if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil { 529 return errors.Errorf("writing %q: %w", path, err) 530 } 531 532 buf.dirty = false 533 e.buffers[path] = buf 534 535 if e.Server != nil { 536 params := &protocol.DidSaveTextDocumentParams{ 537 TextDocument: docID, 538 } 539 if includeText { 540 params.Text = &content 541 } 542 if err := e.Server.DidSave(ctx, params); err != nil { 543 return errors.Errorf("DidSave: %w", err) 544 } 545 e.callsMu.Lock() 546 e.calls.DidSave++ 547 e.callsMu.Unlock() 548 } 549 return nil 550 } 551 552 // contentPosition returns the (Line, Column) position corresponding to offset 553 // in the buffer referenced by path. 554 func contentPosition(content string, offset int) (Pos, error) { 555 scanner := bufio.NewScanner(strings.NewReader(content)) 556 start := 0 557 line := 0 558 for scanner.Scan() { 559 end := start + len([]rune(scanner.Text())) + 1 560 if offset < end { 561 return Pos{Line: line, Column: offset - start}, nil 562 } 563 start = end 564 line++ 565 } 566 if err := scanner.Err(); err != nil { 567 return Pos{}, errors.Errorf("scanning content: %w", err) 568 } 569 // Scan() will drop the last line if it is empty. Correct for this. 570 if (strings.HasSuffix(content, "\n") || content == "") && offset == start { 571 return Pos{Line: line, Column: 0}, nil 572 } 573 return Pos{}, fmt.Errorf("position %d out of bounds in %q (line = %d, start = %d)", offset, content, line, start) 574 } 575 576 // ErrNoMatch is returned if a regexp search fails. 577 var ( 578 ErrNoMatch = errors.New("no match") 579 ErrUnknownBuffer = errors.New("unknown buffer") 580 ) 581 582 // regexpRange returns the start and end of the first occurrence of either re 583 // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. 584 func regexpRange(content, re string) (Pos, Pos, error) { 585 content = normalizeEOL(content) 586 var start, end int 587 rec, err := regexp.Compile(re) 588 if err != nil { 589 return Pos{}, Pos{}, err 590 } 591 indexes := rec.FindStringSubmatchIndex(content) 592 if indexes == nil { 593 return Pos{}, Pos{}, ErrNoMatch 594 } 595 switch len(indexes) { 596 case 2: 597 // no subgroups: return the range of the regexp expression 598 start, end = indexes[0], indexes[1] 599 case 4: 600 // one subgroup: return its range 601 start, end = indexes[2], indexes[3] 602 default: 603 return Pos{}, Pos{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) 604 } 605 startPos, err := contentPosition(content, start) 606 if err != nil { 607 return Pos{}, Pos{}, err 608 } 609 endPos, err := contentPosition(content, end) 610 if err != nil { 611 return Pos{}, Pos{}, err 612 } 613 return startPos, endPos, nil 614 } 615 616 func normalizeEOL(content string) string { 617 return strings.Join(lines(content), "\n") 618 } 619 620 // RegexpRange returns the first range in the buffer bufName matching re. See 621 // RegexpSearch for more information on matching. 622 func (e *Editor) RegexpRange(bufName, re string) (Pos, Pos, error) { 623 e.mu.Lock() 624 defer e.mu.Unlock() 625 buf, ok := e.buffers[bufName] 626 if !ok { 627 return Pos{}, Pos{}, ErrUnknownBuffer 628 } 629 return regexpRange(buf.text(), re) 630 } 631 632 // RegexpSearch returns the position of the first match for re in the buffer 633 // bufName. For convenience, RegexpSearch supports the following two modes: 634 // 1. If re has no subgroups, return the position of the match for re itself. 635 // 2. If re has one subgroup, return the position of the first subgroup. 636 // It returns an error re is invalid, has more than one subgroup, or doesn't 637 // match the buffer. 638 func (e *Editor) RegexpSearch(bufName, re string) (Pos, error) { 639 start, _, err := e.RegexpRange(bufName, re) 640 return start, err 641 } 642 643 // RegexpReplace edits the buffer corresponding to path by replacing the first 644 // instance of re, or its first subgroup, with the replace text. See 645 // RegexpSearch for more explanation of these two modes. 646 // It returns an error if re is invalid, has more than one subgroup, or doesn't 647 // match the buffer. 648 func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error { 649 e.mu.Lock() 650 defer e.mu.Unlock() 651 buf, ok := e.buffers[path] 652 if !ok { 653 return ErrUnknownBuffer 654 } 655 content := buf.text() 656 start, end, err := regexpRange(content, re) 657 if err != nil { 658 return err 659 } 660 return e.editBufferLocked(ctx, path, []Edit{{ 661 Start: start, 662 End: end, 663 Text: replace, 664 }}) 665 } 666 667 // EditBuffer applies the given test edits to the buffer identified by path. 668 func (e *Editor) EditBuffer(ctx context.Context, path string, edits []Edit) error { 669 e.mu.Lock() 670 defer e.mu.Unlock() 671 return e.editBufferLocked(ctx, path, edits) 672 } 673 674 func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error { 675 e.mu.Lock() 676 defer e.mu.Unlock() 677 lines := lines(content) 678 return e.setBufferContentLocked(ctx, path, true, lines, nil) 679 } 680 681 // HasBuffer reports whether the file name is open in the editor. 682 func (e *Editor) HasBuffer(name string) bool { 683 e.mu.Lock() 684 defer e.mu.Unlock() 685 _, ok := e.buffers[name] 686 return ok 687 } 688 689 // BufferText returns the content of the buffer with the given name. 690 func (e *Editor) BufferText(name string) string { 691 e.mu.Lock() 692 defer e.mu.Unlock() 693 return e.buffers[name].text() 694 } 695 696 // BufferVersion returns the current version of the buffer corresponding to 697 // name (or 0 if it is not being edited). 698 func (e *Editor) BufferVersion(name string) int { 699 e.mu.Lock() 700 defer e.mu.Unlock() 701 return e.buffers[name].version 702 } 703 704 func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []Edit) error { 705 buf, ok := e.buffers[path] 706 if !ok { 707 return fmt.Errorf("unknown buffer %q", path) 708 } 709 content := make([]string, len(buf.lines)) 710 copy(content, buf.lines) 711 content, err := editContent(content, edits) 712 if err != nil { 713 return err 714 } 715 return e.setBufferContentLocked(ctx, path, true, content, edits) 716 } 717 718 func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []string, fromEdits []Edit) error { 719 buf, ok := e.buffers[path] 720 if !ok { 721 return fmt.Errorf("unknown buffer %q", path) 722 } 723 buf.lines = content 724 buf.version++ 725 buf.dirty = dirty 726 e.buffers[path] = buf 727 // A simple heuristic: if there is only one edit, send it incrementally. 728 // Otherwise, send the entire content. 729 var evts []protocol.TextDocumentContentChangeEvent 730 if len(fromEdits) == 1 { 731 evts = append(evts, fromEdits[0].toProtocolChangeEvent()) 732 } else { 733 evts = append(evts, protocol.TextDocumentContentChangeEvent{ 734 Text: buf.text(), 735 }) 736 } 737 params := &protocol.DidChangeTextDocumentParams{ 738 TextDocument: protocol.VersionedTextDocumentIdentifier{ 739 Version: int32(buf.version), 740 TextDocumentIdentifier: e.textDocumentIdentifier(buf.path), 741 }, 742 ContentChanges: evts, 743 } 744 if e.Server != nil { 745 if err := e.Server.DidChange(ctx, params); err != nil { 746 return errors.Errorf("DidChange: %w", err) 747 } 748 e.callsMu.Lock() 749 e.calls.DidChange++ 750 e.callsMu.Unlock() 751 } 752 return nil 753 } 754 755 // GoToDefinition jumps to the definition of the symbol at the given position 756 // in an open buffer. It returns the path and position of the resulting jump. 757 func (e *Editor) GoToDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) { 758 if err := e.checkBufferPosition(path, pos); err != nil { 759 return "", Pos{}, err 760 } 761 params := &protocol.DefinitionParams{} 762 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 763 params.Position = pos.ToProtocolPosition() 764 765 resp, err := e.Server.Definition(ctx, params) 766 if err != nil { 767 return "", Pos{}, errors.Errorf("definition: %w", err) 768 } 769 return e.extractFirstPathAndPos(ctx, resp) 770 } 771 772 // GoToTypeDefinition jumps to the type definition of the symbol at the given position 773 // in an open buffer. 774 func (e *Editor) GoToTypeDefinition(ctx context.Context, path string, pos Pos) (string, Pos, error) { 775 if err := e.checkBufferPosition(path, pos); err != nil { 776 return "", Pos{}, err 777 } 778 params := &protocol.TypeDefinitionParams{} 779 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 780 params.Position = pos.ToProtocolPosition() 781 782 resp, err := e.Server.TypeDefinition(ctx, params) 783 if err != nil { 784 return "", Pos{}, errors.Errorf("type definition: %w", err) 785 } 786 return e.extractFirstPathAndPos(ctx, resp) 787 } 788 789 // extractFirstPathAndPos returns the path and the position of the first location. 790 // It opens the file if needed. 791 func (e *Editor) extractFirstPathAndPos(ctx context.Context, locs []protocol.Location) (string, Pos, error) { 792 if len(locs) == 0 { 793 return "", Pos{}, nil 794 } 795 796 newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) 797 newPos := fromProtocolPosition(locs[0].Range.Start) 798 if !e.HasBuffer(newPath) { 799 if err := e.OpenFile(ctx, newPath); err != nil { 800 return "", Pos{}, errors.Errorf("OpenFile: %w", err) 801 } 802 } 803 return newPath, newPos, nil 804 } 805 806 // Symbol performs a workspace symbol search using query 807 func (e *Editor) Symbol(ctx context.Context, query string) ([]SymbolInformation, error) { 808 params := &protocol.WorkspaceSymbolParams{} 809 params.Query = query 810 811 resp, err := e.Server.Symbol(ctx, params) 812 if err != nil { 813 return nil, errors.Errorf("symbol: %w", err) 814 } 815 var res []SymbolInformation 816 for _, si := range resp { 817 ploc := si.Location 818 path := e.sandbox.Workdir.URIToPath(ploc.URI) 819 start := fromProtocolPosition(ploc.Range.Start) 820 end := fromProtocolPosition(ploc.Range.End) 821 rnge := Range{ 822 Start: start, 823 End: end, 824 } 825 loc := Location{ 826 Path: path, 827 Range: rnge, 828 } 829 res = append(res, SymbolInformation{ 830 Name: si.Name, 831 Kind: si.Kind, 832 Location: loc, 833 }) 834 } 835 return res, nil 836 } 837 838 // OrganizeImports requests and performs the source.organizeImports codeAction. 839 func (e *Editor) OrganizeImports(ctx context.Context, path string) error { 840 _, err := e.applyCodeActions(ctx, path, nil, nil, protocol.SourceOrganizeImports) 841 return err 842 } 843 844 // RefactorRewrite requests and performs the source.refactorRewrite codeAction. 845 func (e *Editor) RefactorRewrite(ctx context.Context, path string, rng *protocol.Range) error { 846 applied, err := e.applyCodeActions(ctx, path, rng, nil, protocol.RefactorRewrite) 847 if applied == 0 { 848 return errors.Errorf("no refactorings were applied") 849 } 850 return err 851 } 852 853 // ApplyQuickFixes requests and performs the quickfix codeAction. 854 func (e *Editor) ApplyQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) error { 855 applied, err := e.applyCodeActions(ctx, path, rng, diagnostics, protocol.SourceFixAll, protocol.QuickFix) 856 if applied == 0 { 857 return errors.Errorf("no quick fixes were applied") 858 } 859 return err 860 } 861 862 // ApplyCodeAction applies the given code action. 863 func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error { 864 for _, change := range action.Edit.DocumentChanges { 865 path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) 866 if int32(e.buffers[path].version) != change.TextDocument.Version { 867 // Skip edits for old versions. 868 continue 869 } 870 edits := convertEdits(change.Edits) 871 if err := e.EditBuffer(ctx, path, edits); err != nil { 872 return errors.Errorf("editing buffer %q: %w", path, err) 873 } 874 } 875 // Execute any commands. The specification says that commands are 876 // executed after edits are applied. 877 if action.Command != nil { 878 if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ 879 Command: action.Command.Command, 880 Arguments: action.Command.Arguments, 881 }); err != nil { 882 return err 883 } 884 } 885 // Some commands may edit files on disk. 886 return e.sandbox.Workdir.CheckForFileChanges(ctx) 887 } 888 889 // GetQuickFixes returns the available quick fix code actions. 890 func (e *Editor) GetQuickFixes(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { 891 return e.getCodeActions(ctx, path, rng, diagnostics, protocol.QuickFix, protocol.SourceFixAll) 892 } 893 894 func (e *Editor) applyCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) { 895 actions, err := e.getCodeActions(ctx, path, rng, diagnostics, only...) 896 if err != nil { 897 return 0, err 898 } 899 applied := 0 900 for _, action := range actions { 901 if action.Title == "" { 902 return 0, errors.Errorf("empty title for code action") 903 } 904 var match bool 905 for _, o := range only { 906 if action.Kind == o { 907 match = true 908 break 909 } 910 } 911 if !match { 912 continue 913 } 914 applied++ 915 if err := e.ApplyCodeAction(ctx, action); err != nil { 916 return 0, err 917 } 918 } 919 return applied, nil 920 } 921 922 func (e *Editor) getCodeActions(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { 923 if e.Server == nil { 924 return nil, nil 925 } 926 params := &protocol.CodeActionParams{} 927 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 928 params.Context.Only = only 929 if diagnostics != nil { 930 params.Context.Diagnostics = diagnostics 931 } 932 if rng != nil { 933 params.Range = *rng 934 } 935 return e.Server.CodeAction(ctx, params) 936 } 937 938 func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { 939 if e.Server == nil { 940 return nil, nil 941 } 942 var match bool 943 // Ensure that this command was actually listed as a supported command. 944 for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { 945 if command == params.Command { 946 match = true 947 break 948 } 949 } 950 if !match { 951 return nil, fmt.Errorf("unsupported command %q", params.Command) 952 } 953 result, err := e.Server.ExecuteCommand(ctx, params) 954 if err != nil { 955 return nil, err 956 } 957 // Some commands use the go command, which writes directly to disk. 958 // For convenience, check for those changes. 959 if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil { 960 return nil, err 961 } 962 return result, nil 963 } 964 965 func convertEdits(protocolEdits []protocol.TextEdit) []Edit { 966 var edits []Edit 967 for _, lspEdit := range protocolEdits { 968 edits = append(edits, fromProtocolTextEdit(lspEdit)) 969 } 970 return edits 971 } 972 973 // FormatBuffer gofmts a Go file. 974 func (e *Editor) FormatBuffer(ctx context.Context, path string) error { 975 if e.Server == nil { 976 return nil 977 } 978 e.mu.Lock() 979 version := e.buffers[path].version 980 e.mu.Unlock() 981 params := &protocol.DocumentFormattingParams{} 982 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 983 resp, err := e.Server.Formatting(ctx, params) 984 if err != nil { 985 return errors.Errorf("textDocument/formatting: %w", err) 986 } 987 e.mu.Lock() 988 defer e.mu.Unlock() 989 if versionAfter := e.buffers[path].version; versionAfter != version { 990 return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter) 991 } 992 edits := convertEdits(resp) 993 if len(edits) == 0 { 994 return nil 995 } 996 return e.editBufferLocked(ctx, path, edits) 997 } 998 999 func (e *Editor) checkBufferPosition(path string, pos Pos) error { 1000 e.mu.Lock() 1001 defer e.mu.Unlock() 1002 buf, ok := e.buffers[path] 1003 if !ok { 1004 return fmt.Errorf("buffer %q is not open", path) 1005 } 1006 if !inText(pos, buf.lines) { 1007 return fmt.Errorf("position %v is invalid in buffer %q", pos, path) 1008 } 1009 return nil 1010 } 1011 1012 // RunGenerate runs `go generate` non-recursively in the workdir-relative dir 1013 // path. It does not report any resulting file changes as a watched file 1014 // change, so must be followed by a call to Workdir.CheckForFileChanges once 1015 // the generate command has completed. 1016 // TODO(rFindley): this shouldn't be necessary anymore. Delete it. 1017 func (e *Editor) RunGenerate(ctx context.Context, dir string) error { 1018 if e.Server == nil { 1019 return nil 1020 } 1021 absDir := e.sandbox.Workdir.AbsPath(dir) 1022 cmd, err := command.NewGenerateCommand("", command.GenerateArgs{ 1023 Dir: protocol.URIFromSpanURI(span.URIFromPath(absDir)), 1024 Recursive: false, 1025 }) 1026 if err != nil { 1027 return err 1028 } 1029 params := &protocol.ExecuteCommandParams{ 1030 Command: cmd.Command, 1031 Arguments: cmd.Arguments, 1032 } 1033 if _, err := e.ExecuteCommand(ctx, params); err != nil { 1034 return fmt.Errorf("running generate: %v", err) 1035 } 1036 // Unfortunately we can't simply poll the workdir for file changes here, 1037 // because server-side command may not have completed. In regtests, we can 1038 // Await this state change, but here we must delegate that responsibility to 1039 // the caller. 1040 return nil 1041 } 1042 1043 // CodeLens executes a codelens request on the server. 1044 func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) { 1045 if e.Server == nil { 1046 return nil, nil 1047 } 1048 e.mu.Lock() 1049 _, ok := e.buffers[path] 1050 e.mu.Unlock() 1051 if !ok { 1052 return nil, fmt.Errorf("buffer %q is not open", path) 1053 } 1054 params := &protocol.CodeLensParams{ 1055 TextDocument: e.textDocumentIdentifier(path), 1056 } 1057 lens, err := e.Server.CodeLens(ctx, params) 1058 if err != nil { 1059 return nil, err 1060 } 1061 return lens, nil 1062 } 1063 1064 // Completion executes a completion request on the server. 1065 func (e *Editor) Completion(ctx context.Context, path string, pos Pos) (*protocol.CompletionList, error) { 1066 if e.Server == nil { 1067 return nil, nil 1068 } 1069 e.mu.Lock() 1070 _, ok := e.buffers[path] 1071 e.mu.Unlock() 1072 if !ok { 1073 return nil, fmt.Errorf("buffer %q is not open", path) 1074 } 1075 params := &protocol.CompletionParams{ 1076 TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 1077 TextDocument: e.textDocumentIdentifier(path), 1078 Position: pos.ToProtocolPosition(), 1079 }, 1080 } 1081 completions, err := e.Server.Completion(ctx, params) 1082 if err != nil { 1083 return nil, err 1084 } 1085 return completions, nil 1086 } 1087 1088 // AcceptCompletion accepts a completion for the given item at the given 1089 // position. 1090 func (e *Editor) AcceptCompletion(ctx context.Context, path string, pos Pos, item protocol.CompletionItem) error { 1091 if e.Server == nil { 1092 return nil 1093 } 1094 e.mu.Lock() 1095 defer e.mu.Unlock() 1096 _, ok := e.buffers[path] 1097 if !ok { 1098 return fmt.Errorf("buffer %q is not open", path) 1099 } 1100 return e.editBufferLocked(ctx, path, convertEdits(append([]protocol.TextEdit{ 1101 *item.TextEdit, 1102 }, item.AdditionalTextEdits...))) 1103 } 1104 1105 // Symbols executes a workspace/symbols request on the server. 1106 func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInformation, error) { 1107 if e.Server == nil { 1108 return nil, nil 1109 } 1110 params := &protocol.WorkspaceSymbolParams{Query: sym} 1111 ans, err := e.Server.Symbol(ctx, params) 1112 return ans, err 1113 } 1114 1115 // References executes a reference request on the server. 1116 func (e *Editor) References(ctx context.Context, path string, pos Pos) ([]protocol.Location, error) { 1117 if e.Server == nil { 1118 return nil, nil 1119 } 1120 e.mu.Lock() 1121 _, ok := e.buffers[path] 1122 e.mu.Unlock() 1123 if !ok { 1124 return nil, fmt.Errorf("buffer %q is not open", path) 1125 } 1126 params := &protocol.ReferenceParams{ 1127 TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 1128 TextDocument: e.textDocumentIdentifier(path), 1129 Position: pos.ToProtocolPosition(), 1130 }, 1131 Context: protocol.ReferenceContext{ 1132 IncludeDeclaration: true, 1133 }, 1134 } 1135 locations, err := e.Server.References(ctx, params) 1136 if err != nil { 1137 return nil, err 1138 } 1139 return locations, nil 1140 } 1141 1142 func (e *Editor) Rename(ctx context.Context, path string, pos Pos, newName string) error { 1143 if e.Server == nil { 1144 return nil 1145 } 1146 params := &protocol.RenameParams{ 1147 TextDocument: e.textDocumentIdentifier(path), 1148 Position: pos.ToProtocolPosition(), 1149 NewName: newName, 1150 } 1151 wsEdits, err := e.Server.Rename(ctx, params) 1152 if err != nil { 1153 return err 1154 } 1155 for _, change := range wsEdits.DocumentChanges { 1156 if err := e.applyProtocolEdit(ctx, change); err != nil { 1157 return err 1158 } 1159 } 1160 return nil 1161 } 1162 1163 func (e *Editor) applyProtocolEdit(ctx context.Context, change protocol.TextDocumentEdit) error { 1164 path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) 1165 if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { 1166 return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) 1167 } 1168 if !e.HasBuffer(path) { 1169 err := e.OpenFile(ctx, path) 1170 if os.IsNotExist(err) { 1171 // TODO: it's unclear if this is correct. Here we create the buffer (with 1172 // version 1), then apply edits. Perhaps we should apply the edits before 1173 // sending the didOpen notification. 1174 e.CreateBuffer(ctx, path, "") 1175 err = nil 1176 } 1177 if err != nil { 1178 return err 1179 } 1180 } 1181 fakeEdits := convertEdits(change.Edits) 1182 return e.EditBuffer(ctx, path, fakeEdits) 1183 } 1184 1185 // CodeAction executes a codeAction request on the server. 1186 func (e *Editor) CodeAction(ctx context.Context, path string, rng *protocol.Range, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { 1187 if e.Server == nil { 1188 return nil, nil 1189 } 1190 e.mu.Lock() 1191 _, ok := e.buffers[path] 1192 e.mu.Unlock() 1193 if !ok { 1194 return nil, fmt.Errorf("buffer %q is not open", path) 1195 } 1196 params := &protocol.CodeActionParams{ 1197 TextDocument: e.textDocumentIdentifier(path), 1198 Context: protocol.CodeActionContext{ 1199 Diagnostics: diagnostics, 1200 }, 1201 } 1202 if rng != nil { 1203 params.Range = *rng 1204 } 1205 lens, err := e.Server.CodeAction(ctx, params) 1206 if err != nil { 1207 return nil, err 1208 } 1209 return lens, nil 1210 } 1211 1212 // Hover triggers a hover at the given position in an open buffer. 1213 func (e *Editor) Hover(ctx context.Context, path string, pos Pos) (*protocol.MarkupContent, Pos, error) { 1214 if err := e.checkBufferPosition(path, pos); err != nil { 1215 return nil, Pos{}, err 1216 } 1217 params := &protocol.HoverParams{} 1218 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 1219 params.Position = pos.ToProtocolPosition() 1220 1221 resp, err := e.Server.Hover(ctx, params) 1222 if err != nil { 1223 return nil, Pos{}, errors.Errorf("hover: %w", err) 1224 } 1225 if resp == nil { 1226 return nil, Pos{}, nil 1227 } 1228 return &resp.Contents, fromProtocolPosition(resp.Range.Start), nil 1229 } 1230 1231 func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { 1232 if e.Server == nil { 1233 return nil, nil 1234 } 1235 params := &protocol.DocumentLinkParams{} 1236 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 1237 return e.Server.DocumentLink(ctx, params) 1238 } 1239 1240 func (e *Editor) DocumentHighlight(ctx context.Context, path string, pos Pos) ([]protocol.DocumentHighlight, error) { 1241 if e.Server == nil { 1242 return nil, nil 1243 } 1244 if err := e.checkBufferPosition(path, pos); err != nil { 1245 return nil, err 1246 } 1247 params := &protocol.DocumentHighlightParams{} 1248 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 1249 params.Position = pos.ToProtocolPosition() 1250 1251 return e.Server.DocumentHighlight(ctx, params) 1252 }