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