cuelang.org/go@v0.13.0/internal/golangorgx/gopls/test/integration/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 "bytes" 9 "context" 10 "encoding/json" 11 "errors" 12 "fmt" 13 "os" 14 "path" 15 "path/filepath" 16 "regexp" 17 "slices" 18 "strings" 19 "sync" 20 21 "cuelang.org/go/internal/golangorgx/gopls/protocol" 22 "cuelang.org/go/internal/golangorgx/gopls/protocol/command" 23 "cuelang.org/go/internal/golangorgx/gopls/test/integration/fake/glob" 24 "cuelang.org/go/internal/golangorgx/gopls/util/pathutil" 25 "cuelang.org/go/internal/golangorgx/tools/jsonrpc2" 26 "cuelang.org/go/internal/golangorgx/tools/jsonrpc2/servertest" 27 "cuelang.org/go/internal/golangorgx/tools/xcontext" 28 ) 29 30 // Editor is a fake client editor. It keeps track of client state and can be 31 // used for writing LSP tests. 32 type Editor struct { 33 34 // Server, client, and sandbox are concurrency safe and written only 35 // at construction time, so do not require synchronization. 36 Server protocol.Server 37 cancelConn func() 38 serverConn jsonrpc2.Conn 39 client *Client 40 sandbox *Sandbox 41 42 // TODO(rfindley): buffers should be keyed by protocol.DocumentURI. 43 mu sync.Mutex 44 config EditorConfig // editor configuration 45 buffers map[string]buffer // open buffers (relative path -> buffer content) 46 serverCapabilities protocol.ServerCapabilities // capabilities / options 47 semTokOpts protocol.SemanticTokensOptions 48 watchPatterns []*glob.Glob // glob patterns to watch 49 50 // Call metrics for the purpose of expectations. This is done in an ad-hoc 51 // manner for now. Perhaps in the future we should do something more 52 // systematic. Guarded with a separate mutex as calls may need to be accessed 53 // asynchronously via callbacks into the Editor. 54 callsMu sync.Mutex 55 calls CallCounts 56 } 57 58 // CallCounts tracks the number of protocol notifications of different types. 59 type CallCounts struct { 60 DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose, DidChangeConfiguration uint64 61 } 62 63 // buffer holds information about an open buffer in the editor. 64 type buffer struct { 65 version int // monotonic version; incremented on edits 66 path string // relative path in the workspace 67 mapper *protocol.Mapper // buffer content 68 dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) 69 } 70 71 func (b buffer) text() string { 72 return string(b.mapper.Content) 73 } 74 75 // EditorConfig configures the editor's LSP session. This is similar to 76 // golang.UserOptions, but we use a separate type here so that we expose only 77 // that configuration which we support. 78 // 79 // The zero value for EditorConfig is the default configuration. 80 type EditorConfig struct { 81 // ClientName sets the clientInfo.name for the LSP session (in the initialize request). 82 // 83 // Since this can only be set during initialization, changing this field via 84 // Editor.ChangeConfiguration has no effect. 85 ClientName string 86 87 // Env holds environment variables to apply on top of the default editor 88 // environment. When applying these variables, the special string 89 // $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working 90 // directory. 91 Env map[string]string 92 93 // WorkspaceFolders is the workspace folders to configure on the LSP server, 94 // relative to the sandbox workdir. 95 // 96 // As a special case, if WorkspaceFolders is nil the editor defaults to 97 // configuring a single workspace folder corresponding to the workdir root. 98 // To explicitly send no workspace folders, use an empty (non-nil) slice. 99 WorkspaceFolders []string 100 101 // RootURIAsDefaultFolder configures the RootURI initialization 102 // parameter to be set to the default workdir root. 103 RootURIAsDefaultFolder bool 104 105 // Whether to edit files with windows line endings. 106 WindowsLineEndings bool 107 108 // Map of language ID -> regexp to match, used to set the file type of new 109 // buffers. Applied as an overlay on top of the following defaults: 110 // "go" -> ".*\.go" 111 // "go.mod" -> "go\.mod" 112 // "go.sum" -> "go\.sum" 113 // "gotmpl" -> ".*tmpl" 114 FileAssociations map[string]string 115 116 // Settings holds user-provided configuration for the LSP server. 117 Settings map[string]any 118 119 // FolderSettings holds user-provided per-folder configuration, if any. 120 // 121 // It maps each folder (as a relative path to the sandbox workdir) to its 122 // configuration mapping (like Settings). 123 FolderSettings map[string]map[string]any 124 125 // CapabilitiesJSON holds JSON client capabilities to overlay over the 126 // editor's default client capabilities. 127 // 128 // Specifically, this JSON string will be unmarshalled into the editor's 129 // client capabilities struct, before sending to the server. 130 CapabilitiesJSON []byte 131 132 // If non-nil, MessageResponder is used to respond to ShowMessageRequest 133 // messages. 134 MessageResponder func(params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) 135 } 136 137 // NewEditor creates a new Editor. 138 func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { 139 return &Editor{ 140 buffers: make(map[string]buffer), 141 sandbox: sandbox, 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 // 152 // editor, err := NewEditor(s).Connect(ctx, conn, hooks) 153 func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks, skipApplyEdits bool) (*Editor, error) { 154 bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) 155 conn := connector.Connect(bgCtx) 156 e.cancelConn = cancelConn 157 158 e.serverConn = conn 159 e.Server = protocol.ServerDispatcher(conn) 160 e.client = &Client{editor: e, hooks: hooks, skipApplyEdits: skipApplyEdits} 161 conn.Go(bgCtx, 162 protocol.Handlers( 163 protocol.ClientHandler(e.client, 164 jsonrpc2.MethodNotFound))) 165 166 if err := e.initialize(ctx); err != nil { 167 return nil, err 168 } 169 e.sandbox.Workdir.AddWatcher(e.onFileChanges) 170 return e, nil 171 } 172 173 func (e *Editor) Stats() CallCounts { 174 e.callsMu.Lock() 175 defer e.callsMu.Unlock() 176 return e.calls 177 } 178 179 // Shutdown issues the 'shutdown' LSP notification. 180 func (e *Editor) Shutdown(ctx context.Context) error { 181 if e.Server != nil { 182 if err := e.Server.Shutdown(ctx); err != nil { 183 return fmt.Errorf("Shutdown: %w", err) 184 } 185 } 186 return nil 187 } 188 189 // Exit issues the 'exit' LSP notification. 190 func (e *Editor) Exit(ctx context.Context) error { 191 if e.Server != nil { 192 // Not all LSP clients issue the exit RPC, but we do so here to ensure that 193 // we gracefully handle it on multi-session servers. 194 if err := e.Server.Exit(ctx); err != nil { 195 return fmt.Errorf("Exit: %w", err) 196 } 197 } 198 return nil 199 } 200 201 // Close issues the shutdown and exit sequence an editor should. 202 func (e *Editor) Close(ctx context.Context) error { 203 if err := e.Shutdown(ctx); err != nil { 204 return err 205 } 206 if err := e.Exit(ctx); err != nil { 207 return err 208 } 209 defer func() { 210 e.cancelConn() 211 }() 212 213 // called close on the editor should result in the connection closing 214 select { 215 case <-e.serverConn.Done(): 216 // connection closed itself 217 return nil 218 case <-ctx.Done(): 219 return fmt.Errorf("connection not closed: %w", ctx.Err()) 220 } 221 } 222 223 // Client returns the LSP client for this editor. 224 func (e *Editor) Client() *Client { 225 return e.client 226 } 227 228 // makeSettings builds the settings map for use in LSP settings RPCs. 229 func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) map[string]any { 230 env := make(map[string]string) 231 for k, v := range sandbox.GoEnv() { 232 env[k] = v 233 } 234 for k, v := range config.Env { 235 env[k] = v 236 } 237 for k, v := range env { 238 v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().Path()) 239 env[k] = v 240 } 241 242 settings := map[string]any{ 243 "env": env, 244 245 // Use verbose progress reporting so that integration tests can assert on 246 // asynchronous operations being completed (such as diagnosing a snapshot). 247 "verboseWorkDoneProgress": true, 248 249 // Set an unlimited completion budget, so that tests don't flake because 250 // completions are too slow. 251 "completionBudget": "0s", 252 } 253 254 for k, v := range config.Settings { 255 if k == "env" { 256 panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead") 257 } 258 settings[k] = v 259 } 260 261 // If the server is requesting configuration for a specific scope, apply 262 // settings for the nearest folder that has customized settings, if any. 263 if scopeURI != nil { 264 var ( 265 scopePath = protocol.DocumentURI(*scopeURI).Path() 266 closestDir string // longest dir with settings containing the scope, if any 267 closestSettings map[string]any // settings for that dir, if any 268 ) 269 for relPath, settings := range config.FolderSettings { 270 dir := sandbox.Workdir.AbsPath(relPath) 271 if strings.HasPrefix(scopePath+string(filepath.Separator), dir+string(filepath.Separator)) && len(dir) > len(closestDir) { 272 closestDir = dir 273 closestSettings = settings 274 } 275 } 276 if closestSettings != nil { 277 for k, v := range closestSettings { 278 settings[k] = v 279 } 280 } 281 } 282 283 return settings 284 } 285 286 func (e *Editor) initialize(ctx context.Context) error { 287 config := e.Config() 288 289 params := &protocol.ParamInitialize{} 290 if e.config.ClientName != "" { 291 params.ClientInfo = &protocol.ClientInfo{ 292 Name: e.config.ClientName, 293 Version: "v1.0.0", 294 } 295 } 296 params.InitializationOptions = makeSettings(e.sandbox, config, nil) 297 params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) 298 if config.RootURIAsDefaultFolder { 299 params.RootURI = e.sandbox.Workdir.URI(string(e.sandbox.Workdir.RelativeTo)) 300 } 301 302 capabilities, err := clientCapabilities(config) 303 if err != nil { 304 return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err) 305 } 306 params.Capabilities = capabilities 307 308 trace := protocol.TraceValues("messages") 309 params.Trace = &trace 310 // TODO: support workspace folders. 311 if e.Server != nil { 312 resp, err := e.Server.Initialize(ctx, params) 313 if err != nil { 314 return fmt.Errorf("initialize: %w", err) 315 } 316 semTokOpts, err := marshalUnmarshal[protocol.SemanticTokensOptions](resp.Capabilities.SemanticTokensProvider) 317 if err != nil { 318 return fmt.Errorf("unmarshalling semantic tokens options: %v", err) 319 } 320 e.mu.Lock() 321 e.serverCapabilities = resp.Capabilities 322 e.semTokOpts = semTokOpts 323 e.mu.Unlock() 324 325 if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 326 return fmt.Errorf("initialized: %w", err) 327 } 328 } 329 // TODO: await initial configuration here, or expect gopls to manage that? 330 return nil 331 } 332 333 func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) { 334 var capabilities protocol.ClientCapabilities 335 // Set various client capabilities that are sought by gopls. 336 capabilities.Workspace.Configuration = true // support workspace/configuration 337 capabilities.TextDocument.Completion.CompletionItem.TagSupport = &protocol.CompletionItemTagOptions{} 338 capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} 339 capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true 340 capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true} 341 capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress 342 capabilities.TextDocument.SemanticTokens.TokenTypes = []string{ 343 "namespace", "type", "class", "enum", "interface", 344 "struct", "typeParameter", "parameter", "variable", "property", "enumMember", 345 "event", "function", "method", "macro", "keyword", "modifier", "comment", 346 "string", "number", "regexp", "operator", 347 } 348 capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ 349 "declaration", "definition", "readonly", "static", 350 "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", 351 } 352 // The LSP tests have historically enabled this flag, 353 // but really we should test both ways for older editors. 354 capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true 355 // Glob pattern watching is enabled. 356 capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true 357 // "rename" operations are used for package renaming. 358 // 359 // TODO(rfindley): add support for other resource operations (create, delete, ...) 360 capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{ 361 ResourceOperations: []protocol.ResourceOperationKind{ 362 "rename", 363 }, 364 } 365 366 // Apply capabilities overlay. 367 if cfg.CapabilitiesJSON != nil { 368 if err := json.Unmarshal(cfg.CapabilitiesJSON, &capabilities); err != nil { 369 return protocol.ClientCapabilities{}, fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err) 370 } 371 } 372 return capabilities, nil 373 } 374 375 // marshalUnmarshal is a helper to json Marshal and then Unmarshal as a 376 // different type. Used to work around cases where our protocol types are not 377 // specific. 378 func marshalUnmarshal[T any](v any) (T, error) { 379 var t T 380 data, err := json.Marshal(v) 381 if err != nil { 382 return t, err 383 } 384 err = json.Unmarshal(data, &t) 385 return t, err 386 } 387 388 // HasCommand reports whether the connected server supports the command with the given ID. 389 func (e *Editor) HasCommand(id string) bool { 390 for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { 391 if command == id { 392 return true 393 } 394 } 395 return false 396 } 397 398 // makeWorkspaceFolders creates a slice of workspace folders to use for 399 // this editing session, based on the editor configuration. 400 func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) { 401 if paths == nil { 402 paths = []string{string(sandbox.Workdir.RelativeTo)} 403 } 404 405 for _, path := range paths { 406 uri := string(sandbox.Workdir.URI(path)) 407 folders = append(folders, protocol.WorkspaceFolder{ 408 URI: uri, 409 Name: filepath.Base(uri), 410 }) 411 } 412 413 return folders 414 } 415 416 // onFileChanges is registered to be called by the Workdir on any writes that 417 // go through the Workdir API. It is called synchronously by the Workdir. 418 func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) { 419 if e.Server == nil { 420 return 421 } 422 423 // e may be locked when onFileChanges is called, but it is important that we 424 // synchronously increment this counter so that we can subsequently assert on 425 // the number of expected DidChangeWatchedFiles calls. 426 e.callsMu.Lock() 427 e.calls.DidChangeWatchedFiles++ 428 e.callsMu.Unlock() 429 430 // Since e may be locked, we must run this mutation asynchronously. 431 go func() { 432 e.mu.Lock() 433 defer e.mu.Unlock() 434 for _, evt := range evts { 435 // Always send an on-disk change, even for events that seem useless 436 // because they're shadowed by an open buffer. 437 path := e.sandbox.Workdir.URIToPath(evt.URI) 438 if buf, ok := e.buffers[path]; ok { 439 // Following VS Code, don't honor deletions or changes to dirty buffers. 440 if buf.dirty || evt.Type == protocol.Deleted { 441 continue 442 } 443 444 content, err := e.sandbox.Workdir.ReadFile(path) 445 if err != nil { 446 continue // A race with some other operation. 447 } 448 // No need to update if the buffer content hasn't changed. 449 if string(content) == buf.text() { 450 continue 451 } 452 // During shutdown, this call will fail. Ignore the error. 453 _ = e.setBufferContentLocked(ctx, path, false, content, nil) 454 } 455 } 456 var matchedEvts []protocol.FileEvent 457 for _, evt := range evts { 458 filename := filepath.ToSlash(evt.URI.Path()) 459 for _, g := range e.watchPatterns { 460 if g.Match(filename) { 461 matchedEvts = append(matchedEvts, evt) 462 break 463 } 464 } 465 } 466 467 // TODO(rfindley): don't send notifications while locked. 468 e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ 469 Changes: matchedEvts, 470 }) 471 }() 472 } 473 474 // OpenFile creates a buffer for the given workdir-relative file. 475 // 476 // If the file is already open, it is a no-op. 477 func (e *Editor) OpenFile(ctx context.Context, path string) error { 478 if e.HasBuffer(path) { 479 return nil 480 } 481 content, err := e.sandbox.Workdir.ReadFile(path) 482 if err != nil { 483 return err 484 } 485 if e.Config().WindowsLineEndings { 486 content = toWindowsLineEndings(content) 487 } 488 return e.createBuffer(ctx, path, false, content) 489 } 490 491 // toWindowsLineEndings checks whether content has windows line endings. 492 // 493 // If so, it returns content unmodified. If not, it returns a new byte slice modified to use CRLF line endings. 494 func toWindowsLineEndings(content []byte) []byte { 495 abnormal := false 496 for i, b := range content { 497 if b == '\n' && (i == 0 || content[i-1] != '\r') { 498 abnormal = true 499 break 500 } 501 } 502 if !abnormal { 503 return content 504 } 505 var buf bytes.Buffer 506 for i, b := range content { 507 if b == '\n' && (i == 0 || content[i-1] != '\r') { 508 buf.WriteByte('\r') 509 } 510 buf.WriteByte(b) 511 } 512 return buf.Bytes() 513 } 514 515 // CreateBuffer creates a new unsaved buffer corresponding to the workdir path, 516 // containing the given textual content. 517 func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { 518 return e.createBuffer(ctx, path, true, []byte(content)) 519 } 520 521 func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content []byte) error { 522 e.mu.Lock() 523 524 if _, ok := e.buffers[path]; ok { 525 e.mu.Unlock() 526 return fmt.Errorf("buffer %q already exists", path) 527 } 528 529 uri := e.sandbox.Workdir.URI(path) 530 buf := buffer{ 531 version: 1, 532 path: path, 533 mapper: protocol.NewMapper(uri, content), 534 dirty: dirty, 535 } 536 e.buffers[path] = buf 537 538 item := e.textDocumentItem(buf) 539 e.mu.Unlock() 540 541 return e.sendDidOpen(ctx, item) 542 } 543 544 // textDocumentItem builds a protocol.TextDocumentItem for the given buffer. 545 // 546 // Precondition: e.mu must be held. 547 func (e *Editor) textDocumentItem(buf buffer) protocol.TextDocumentItem { 548 return protocol.TextDocumentItem{ 549 URI: e.sandbox.Workdir.URI(buf.path), 550 LanguageID: languageID(buf.path, e.config.FileAssociations), 551 Version: int32(buf.version), 552 Text: buf.text(), 553 } 554 } 555 556 func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem) error { 557 if e.Server != nil { 558 if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ 559 TextDocument: item, 560 }); err != nil { 561 return fmt.Errorf("DidOpen: %w", err) 562 } 563 e.callsMu.Lock() 564 e.calls.DidOpen++ 565 e.callsMu.Unlock() 566 } 567 return nil 568 } 569 570 var defaultFileAssociations = map[string]*regexp.Regexp{ 571 "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! 572 "go.mod": regexp.MustCompile(`^go\.mod$`), 573 "go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`), 574 "go.work": regexp.MustCompile(`^go\.work$`), 575 "gotmpl": regexp.MustCompile(`^.*tmpl$`), 576 } 577 578 // languageID returns the language identifier for the path p given the user 579 // configured fileAssociations. 580 func languageID(p string, fileAssociations map[string]string) string { 581 base := path.Base(p) 582 for lang, re := range fileAssociations { 583 re := regexp.MustCompile(re) 584 if re.MatchString(base) { 585 return lang 586 } 587 } 588 for lang, re := range defaultFileAssociations { 589 if re.MatchString(base) { 590 return lang 591 } 592 } 593 return "" 594 } 595 596 // CloseBuffer removes the current buffer (regardless of whether it is saved). 597 func (e *Editor) CloseBuffer(ctx context.Context, path string) error { 598 e.mu.Lock() 599 _, ok := e.buffers[path] 600 if !ok { 601 e.mu.Unlock() 602 return ErrUnknownBuffer 603 } 604 delete(e.buffers, path) 605 e.mu.Unlock() 606 607 return e.sendDidClose(ctx, e.TextDocumentIdentifier(path)) 608 } 609 610 func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIdentifier) error { 611 if e.Server != nil { 612 if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ 613 TextDocument: doc, 614 }); err != nil { 615 return fmt.Errorf("DidClose: %w", err) 616 } 617 e.callsMu.Lock() 618 e.calls.DidClose++ 619 e.callsMu.Unlock() 620 } 621 return nil 622 } 623 624 func (e *Editor) TextDocumentIdentifier(path string) protocol.TextDocumentIdentifier { 625 return protocol.TextDocumentIdentifier{ 626 URI: e.sandbox.Workdir.URI(path), 627 } 628 } 629 630 // SaveBuffer writes the content of the buffer specified by the given path to 631 // the filesystem. 632 func (e *Editor) SaveBuffer(ctx context.Context, path string) error { 633 if err := e.OrganizeImports(ctx, path); err != nil { 634 return fmt.Errorf("organizing imports before save: %w", err) 635 } 636 if err := e.FormatBuffer(ctx, path); err != nil { 637 return fmt.Errorf("formatting before save: %w", err) 638 } 639 return e.SaveBufferWithoutActions(ctx, path) 640 } 641 642 func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error { 643 e.mu.Lock() 644 defer e.mu.Unlock() 645 buf, ok := e.buffers[path] 646 if !ok { 647 return fmt.Errorf("unknown buffer: %q", path) 648 } 649 content := buf.text() 650 includeText := false 651 syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions) 652 if ok { 653 includeText = syncOptions.Save.IncludeText 654 } 655 656 docID := e.TextDocumentIdentifier(buf.path) 657 if e.Server != nil { 658 if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ 659 TextDocument: docID, 660 Reason: protocol.Manual, 661 }); err != nil { 662 return fmt.Errorf("WillSave: %w", err) 663 } 664 } 665 if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil { 666 return fmt.Errorf("writing %q: %w", path, err) 667 } 668 669 buf.dirty = false 670 e.buffers[path] = buf 671 672 if e.Server != nil { 673 params := &protocol.DidSaveTextDocumentParams{ 674 TextDocument: docID, 675 } 676 if includeText { 677 params.Text = &content 678 } 679 if err := e.Server.DidSave(ctx, params); err != nil { 680 return fmt.Errorf("DidSave: %w", err) 681 } 682 e.callsMu.Lock() 683 e.calls.DidSave++ 684 e.callsMu.Unlock() 685 } 686 return nil 687 } 688 689 // ErrNoMatch is returned if a regexp search fails. 690 var ( 691 ErrNoMatch = errors.New("no match") 692 ErrUnknownBuffer = errors.New("unknown buffer") 693 ) 694 695 // regexpLocation returns the location of the first occurrence of either re 696 // or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. 697 func regexpLocation(mapper *protocol.Mapper, re string) (protocol.Location, error) { 698 var start, end int 699 rec, err := regexp.Compile(re) 700 if err != nil { 701 return protocol.Location{}, err 702 } 703 indexes := rec.FindSubmatchIndex(mapper.Content) 704 if indexes == nil { 705 return protocol.Location{}, ErrNoMatch 706 } 707 switch len(indexes) { 708 case 2: 709 // no subgroups: return the range of the regexp expression 710 start, end = indexes[0], indexes[1] 711 case 4: 712 // one subgroup: return its range 713 start, end = indexes[2], indexes[3] 714 default: 715 return protocol.Location{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) 716 } 717 return mapper.OffsetLocation(start, end) 718 } 719 720 // RegexpSearch returns the Location of the first match for re in the buffer 721 // bufName. For convenience, RegexpSearch supports the following two modes: 722 // 1. If re has no subgroups, return the position of the match for re itself. 723 // 2. If re has one subgroup, return the position of the first subgroup. 724 // 725 // It returns an error re is invalid, has more than one subgroup, or doesn't 726 // match the buffer. 727 func (e *Editor) RegexpSearch(bufName, re string) (protocol.Location, error) { 728 e.mu.Lock() 729 buf, ok := e.buffers[bufName] 730 e.mu.Unlock() 731 if !ok { 732 return protocol.Location{}, ErrUnknownBuffer 733 } 734 return regexpLocation(buf.mapper, re) 735 } 736 737 // RegexpReplace edits the buffer corresponding to path by replacing the first 738 // instance of re, or its first subgroup, with the replace text. See 739 // RegexpSearch for more explanation of these two modes. 740 // It returns an error if re is invalid, has more than one subgroup, or doesn't 741 // match the buffer. 742 func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error { 743 e.mu.Lock() 744 defer e.mu.Unlock() 745 buf, ok := e.buffers[path] 746 if !ok { 747 return ErrUnknownBuffer 748 } 749 loc, err := regexpLocation(buf.mapper, re) 750 if err != nil { 751 return err 752 } 753 edits := []protocol.TextEdit{{ 754 Range: loc.Range, 755 NewText: replace, 756 }} 757 patched, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) 758 if err != nil { 759 return fmt.Errorf("editing %q: %v", path, err) 760 } 761 return e.setBufferContentLocked(ctx, path, true, patched, edits) 762 } 763 764 // EditBuffer applies the given test edits to the buffer identified by path. 765 func (e *Editor) EditBuffer(ctx context.Context, path string, edits []protocol.TextEdit) error { 766 e.mu.Lock() 767 defer e.mu.Unlock() 768 return e.editBufferLocked(ctx, path, edits) 769 } 770 771 func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error { 772 e.mu.Lock() 773 defer e.mu.Unlock() 774 return e.setBufferContentLocked(ctx, path, true, []byte(content), nil) 775 } 776 777 // HasBuffer reports whether the file name is open in the editor. 778 func (e *Editor) HasBuffer(name string) bool { 779 e.mu.Lock() 780 defer e.mu.Unlock() 781 _, ok := e.buffers[name] 782 return ok 783 } 784 785 // BufferText returns the content of the buffer with the given name, or "" if 786 // the file at that path is not open. The second return value reports whether 787 // the file is open. 788 func (e *Editor) BufferText(name string) (string, bool) { 789 e.mu.Lock() 790 defer e.mu.Unlock() 791 buf, ok := e.buffers[name] 792 if !ok { 793 return "", false 794 } 795 return buf.text(), true 796 } 797 798 // Mapper returns the protocol.Mapper for the given buffer name, if it is open. 799 func (e *Editor) Mapper(name string) (*protocol.Mapper, error) { 800 e.mu.Lock() 801 defer e.mu.Unlock() 802 buf, ok := e.buffers[name] 803 if !ok { 804 return nil, fmt.Errorf("no mapper for %q", name) 805 } 806 return buf.mapper, nil 807 } 808 809 // BufferVersion returns the current version of the buffer corresponding to 810 // name (or 0 if it is not being edited). 811 func (e *Editor) BufferVersion(name string) int { 812 e.mu.Lock() 813 defer e.mu.Unlock() 814 return e.buffers[name].version 815 } 816 817 func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []protocol.TextEdit) error { 818 buf, ok := e.buffers[path] 819 if !ok { 820 return fmt.Errorf("unknown buffer %q", path) 821 } 822 content, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) 823 if err != nil { 824 return fmt.Errorf("editing %q: %v; edits:\n%v", path, err, edits) 825 } 826 return e.setBufferContentLocked(ctx, path, true, content, edits) 827 } 828 829 func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []byte, fromEdits []protocol.TextEdit) error { 830 buf, ok := e.buffers[path] 831 if !ok { 832 return fmt.Errorf("unknown buffer %q", path) 833 } 834 buf.mapper = protocol.NewMapper(buf.mapper.URI, content) 835 buf.version++ 836 buf.dirty = dirty 837 e.buffers[path] = buf 838 839 // A simple heuristic: if there is only one edit, send it incrementally. 840 // Otherwise, send the entire content. 841 var evt protocol.TextDocumentContentChangeEvent 842 if len(fromEdits) == 1 { 843 evt.Range = &fromEdits[0].Range 844 evt.Text = fromEdits[0].NewText 845 } else { 846 evt.Text = buf.text() 847 } 848 params := &protocol.DidChangeTextDocumentParams{ 849 TextDocument: protocol.VersionedTextDocumentIdentifier{ 850 Version: int32(buf.version), 851 TextDocumentIdentifier: e.TextDocumentIdentifier(buf.path), 852 }, 853 ContentChanges: []protocol.TextDocumentContentChangeEvent{evt}, 854 } 855 if e.Server != nil { 856 if err := e.Server.DidChange(ctx, params); err != nil { 857 return fmt.Errorf("DidChange: %w", err) 858 } 859 e.callsMu.Lock() 860 e.calls.DidChange++ 861 e.callsMu.Unlock() 862 } 863 return nil 864 } 865 866 // GoToDefinition jumps to the definition of the symbol at the given position 867 // in an open buffer. It returns the location of the resulting jump. 868 func (e *Editor) Definition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { 869 if err := e.checkBufferLocation(loc); err != nil { 870 return protocol.Location{}, err 871 } 872 params := &protocol.DefinitionParams{} 873 params.TextDocument.URI = loc.URI 874 params.Position = loc.Range.Start 875 876 resp, err := e.Server.Definition(ctx, params) 877 if err != nil { 878 return protocol.Location{}, fmt.Errorf("definition: %w", err) 879 } 880 return e.extractFirstLocation(ctx, resp) 881 } 882 883 // TypeDefinition jumps to the type definition of the symbol at the given 884 // location in an open buffer. 885 func (e *Editor) TypeDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { 886 if err := e.checkBufferLocation(loc); err != nil { 887 return protocol.Location{}, err 888 } 889 params := &protocol.TypeDefinitionParams{} 890 params.TextDocument.URI = loc.URI 891 params.Position = loc.Range.Start 892 893 resp, err := e.Server.TypeDefinition(ctx, params) 894 if err != nil { 895 return protocol.Location{}, fmt.Errorf("type definition: %w", err) 896 } 897 return e.extractFirstLocation(ctx, resp) 898 } 899 900 // extractFirstLocation returns the first location. 901 // It opens the file if needed. 902 func (e *Editor) extractFirstLocation(ctx context.Context, locs []protocol.Location) (protocol.Location, error) { 903 if len(locs) == 0 { 904 return protocol.Location{}, nil 905 } 906 907 newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) 908 if !e.HasBuffer(newPath) { 909 if err := e.OpenFile(ctx, newPath); err != nil { 910 return protocol.Location{}, fmt.Errorf("OpenFile: %w", err) 911 } 912 } 913 return locs[0], nil 914 } 915 916 // Symbol performs a workspace symbol search using query 917 func (e *Editor) Symbol(ctx context.Context, query string) ([]protocol.SymbolInformation, error) { 918 params := &protocol.WorkspaceSymbolParams{Query: query} 919 return e.Server.Symbol(ctx, params) 920 } 921 922 // OrganizeImports requests and performs the source.organizeImports codeAction. 923 func (e *Editor) OrganizeImports(ctx context.Context, path string) error { 924 loc := protocol.Location{URI: e.sandbox.Workdir.URI(path)} // zero Range => whole file 925 _, err := e.applyCodeActions(ctx, loc, nil, protocol.SourceOrganizeImports) 926 return err 927 } 928 929 // RefactorRewrite requests and performs the source.refactorRewrite codeAction. 930 func (e *Editor) RefactorRewrite(ctx context.Context, loc protocol.Location) error { 931 applied, err := e.applyCodeActions(ctx, loc, nil, protocol.RefactorRewrite) 932 if err != nil { 933 return err 934 } 935 if applied == 0 { 936 return fmt.Errorf("no refactorings were applied") 937 } 938 return nil 939 } 940 941 // ApplyQuickFixes requests and performs the quickfix codeAction. 942 func (e *Editor) ApplyQuickFixes(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) error { 943 applied, err := e.applyCodeActions(ctx, loc, diagnostics, protocol.SourceFixAll, protocol.QuickFix) 944 if applied == 0 { 945 return fmt.Errorf("no quick fixes were applied") 946 } 947 return err 948 } 949 950 // ApplyCodeAction applies the given code action. 951 func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error { 952 // Resolve the code actions if necessary and supported. 953 if action.Edit == nil { 954 editSupport, err := e.EditResolveSupport() 955 if err != nil { 956 return err 957 } 958 if editSupport { 959 ca, err := e.Server.ResolveCodeAction(ctx, &action) 960 if err != nil { 961 return err 962 } 963 action.Edit = ca.Edit 964 } 965 } 966 967 if action.Edit != nil { 968 for _, change := range action.Edit.DocumentChanges { 969 if change.TextDocumentEdit != nil { 970 path := e.sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) 971 if int32(e.buffers[path].version) != change.TextDocumentEdit.TextDocument.Version { 972 // Skip edits for old versions. 973 continue 974 } 975 if err := e.EditBuffer(ctx, path, protocol.AsTextEdits(change.TextDocumentEdit.Edits)); err != nil { 976 return fmt.Errorf("editing buffer %q: %w", path, err) 977 } 978 } 979 } 980 } 981 // Execute any commands. The specification says that commands are 982 // executed after edits are applied. 983 if action.Command != nil { 984 if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ 985 Command: action.Command.Command, 986 Arguments: action.Command.Arguments, 987 }); err != nil { 988 return err 989 } 990 } 991 // Some commands may edit files on disk. 992 return e.sandbox.Workdir.CheckForFileChanges(ctx) 993 } 994 995 // GetQuickFixes returns the available quick fix code actions. 996 func (e *Editor) GetQuickFixes(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { 997 return e.CodeActions(ctx, loc, diagnostics, protocol.QuickFix, protocol.SourceFixAll) 998 } 999 1000 func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) { 1001 actions, err := e.CodeActions(ctx, loc, diagnostics, only...) 1002 if err != nil { 1003 return 0, err 1004 } 1005 applied := 0 1006 for _, action := range actions { 1007 if action.Title == "" { 1008 return 0, fmt.Errorf("empty title for code action") 1009 } 1010 var match bool 1011 for _, o := range only { 1012 if action.Kind == o { 1013 match = true 1014 break 1015 } 1016 } 1017 if !match { 1018 continue 1019 } 1020 applied++ 1021 if err := e.ApplyCodeAction(ctx, action); err != nil { 1022 return 0, err 1023 } 1024 } 1025 return applied, nil 1026 } 1027 1028 func (e *Editor) CodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { 1029 if e.Server == nil { 1030 return nil, nil 1031 } 1032 params := &protocol.CodeActionParams{} 1033 params.TextDocument.URI = loc.URI 1034 params.Context.Only = only 1035 params.Range = loc.Range // may be zero => whole file 1036 if diagnostics != nil { 1037 params.Context.Diagnostics = diagnostics 1038 } 1039 return e.Server.CodeAction(ctx, params) 1040 } 1041 1042 func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { 1043 if e.Server == nil { 1044 return nil, nil 1045 } 1046 var match bool 1047 if e.serverCapabilities.ExecuteCommandProvider != nil { 1048 // Ensure that this command was actually listed as a supported command. 1049 for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { 1050 if command == params.Command { 1051 match = true 1052 break 1053 } 1054 } 1055 } 1056 if !match { 1057 return nil, fmt.Errorf("unsupported command %q", params.Command) 1058 } 1059 result, err := e.Server.ExecuteCommand(ctx, params) 1060 if err != nil { 1061 return nil, err 1062 } 1063 // Some commands use the go command, which writes directly to disk. 1064 // For convenience, check for those changes. 1065 if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil { 1066 return nil, fmt.Errorf("checking for file changes: %v", err) 1067 } 1068 return result, nil 1069 } 1070 1071 // FormatBuffer gofmts a Go file. 1072 func (e *Editor) FormatBuffer(ctx context.Context, path string) error { 1073 if e.Server == nil { 1074 return nil 1075 } 1076 e.mu.Lock() 1077 version := e.buffers[path].version 1078 e.mu.Unlock() 1079 params := &protocol.DocumentFormattingParams{} 1080 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 1081 edits, err := e.Server.Formatting(ctx, params) 1082 if err != nil { 1083 return fmt.Errorf("textDocument/formatting: %w", err) 1084 } 1085 e.mu.Lock() 1086 defer e.mu.Unlock() 1087 if versionAfter := e.buffers[path].version; versionAfter != version { 1088 return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter) 1089 } 1090 if len(edits) == 0 { 1091 return nil 1092 } 1093 return e.editBufferLocked(ctx, path, edits) 1094 } 1095 1096 func (e *Editor) checkBufferLocation(loc protocol.Location) error { 1097 e.mu.Lock() 1098 defer e.mu.Unlock() 1099 path := e.sandbox.Workdir.URIToPath(loc.URI) 1100 buf, ok := e.buffers[path] 1101 if !ok { 1102 return fmt.Errorf("buffer %q is not open", path) 1103 } 1104 1105 _, _, err := buf.mapper.RangeOffsets(loc.Range) 1106 return err 1107 } 1108 1109 // RunGenerate runs `go generate` non-recursively in the workdir-relative dir 1110 // path. It does not report any resulting file changes as a watched file 1111 // change, so must be followed by a call to Workdir.CheckForFileChanges once 1112 // the generate command has completed. 1113 // TODO(rFindley): this shouldn't be necessary anymore. Delete it. 1114 func (e *Editor) RunGenerate(ctx context.Context, dir string) error { 1115 if e.Server == nil { 1116 return nil 1117 } 1118 absDir := e.sandbox.Workdir.AbsPath(dir) 1119 cmd, err := command.NewGenerateCommand("", command.GenerateArgs{ 1120 Dir: protocol.URIFromPath(absDir), 1121 Recursive: false, 1122 }) 1123 if err != nil { 1124 return err 1125 } 1126 params := &protocol.ExecuteCommandParams{ 1127 Command: cmd.Command, 1128 Arguments: cmd.Arguments, 1129 } 1130 if _, err := e.ExecuteCommand(ctx, params); err != nil { 1131 return fmt.Errorf("running generate: %v", err) 1132 } 1133 // Unfortunately we can't simply poll the workdir for file changes here, 1134 // because server-side command may not have completed. In integration tests, we can 1135 // Await this state change, but here we must delegate that responsibility to 1136 // the caller. 1137 return nil 1138 } 1139 1140 // CodeLens executes a codelens request on the server. 1141 func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) { 1142 if e.Server == nil { 1143 return nil, nil 1144 } 1145 e.mu.Lock() 1146 _, ok := e.buffers[path] 1147 e.mu.Unlock() 1148 if !ok { 1149 return nil, fmt.Errorf("buffer %q is not open", path) 1150 } 1151 params := &protocol.CodeLensParams{ 1152 TextDocument: e.TextDocumentIdentifier(path), 1153 } 1154 lens, err := e.Server.CodeLens(ctx, params) 1155 if err != nil { 1156 return nil, err 1157 } 1158 return lens, nil 1159 } 1160 1161 // Completion executes a completion request on the server. 1162 func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protocol.CompletionList, error) { 1163 if e.Server == nil { 1164 return nil, nil 1165 } 1166 path := e.sandbox.Workdir.URIToPath(loc.URI) 1167 e.mu.Lock() 1168 _, ok := e.buffers[path] 1169 e.mu.Unlock() 1170 if !ok { 1171 return nil, fmt.Errorf("buffer %q is not open", path) 1172 } 1173 params := &protocol.CompletionParams{ 1174 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), 1175 } 1176 completions, err := e.Server.Completion(ctx, params) 1177 if err != nil { 1178 return nil, err 1179 } 1180 return completions, nil 1181 } 1182 1183 // AcceptCompletion accepts a completion for the given item at the given 1184 // position. 1185 func (e *Editor) AcceptCompletion(ctx context.Context, loc protocol.Location, item protocol.CompletionItem) error { 1186 if e.Server == nil { 1187 return nil 1188 } 1189 e.mu.Lock() 1190 defer e.mu.Unlock() 1191 path := e.sandbox.Workdir.URIToPath(loc.URI) 1192 _, ok := e.buffers[path] 1193 if !ok { 1194 return fmt.Errorf("buffer %q is not open", path) 1195 } 1196 return e.editBufferLocked(ctx, path, append([]protocol.TextEdit{ 1197 *item.TextEdit, 1198 }, item.AdditionalTextEdits...)) 1199 } 1200 1201 // Symbols executes a workspace/symbols request on the server. 1202 func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInformation, error) { 1203 if e.Server == nil { 1204 return nil, nil 1205 } 1206 params := &protocol.WorkspaceSymbolParams{Query: sym} 1207 ans, err := e.Server.Symbol(ctx, params) 1208 return ans, err 1209 } 1210 1211 // CodeLens executes a codelens request on the server. 1212 func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHint, error) { 1213 if e.Server == nil { 1214 return nil, nil 1215 } 1216 e.mu.Lock() 1217 _, ok := e.buffers[path] 1218 e.mu.Unlock() 1219 if !ok { 1220 return nil, fmt.Errorf("buffer %q is not open", path) 1221 } 1222 params := &protocol.InlayHintParams{ 1223 TextDocument: e.TextDocumentIdentifier(path), 1224 } 1225 hints, err := e.Server.InlayHint(ctx, params) 1226 if err != nil { 1227 return nil, err 1228 } 1229 return hints, nil 1230 } 1231 1232 // References returns references to the object at loc, as returned by 1233 // the connected LSP server. If no server is connected, it returns (nil, nil). 1234 func (e *Editor) References(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { 1235 if e.Server == nil { 1236 return nil, nil 1237 } 1238 path := e.sandbox.Workdir.URIToPath(loc.URI) 1239 e.mu.Lock() 1240 _, ok := e.buffers[path] 1241 e.mu.Unlock() 1242 if !ok { 1243 return nil, fmt.Errorf("buffer %q is not open", path) 1244 } 1245 params := &protocol.ReferenceParams{ 1246 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), 1247 Context: protocol.ReferenceContext{ 1248 IncludeDeclaration: true, 1249 }, 1250 } 1251 locations, err := e.Server.References(ctx, params) 1252 if err != nil { 1253 return nil, err 1254 } 1255 return locations, nil 1256 } 1257 1258 // Rename performs a rename of the object at loc to newName, using the 1259 // connected LSP server. If no server is connected, it returns nil. 1260 func (e *Editor) Rename(ctx context.Context, loc protocol.Location, newName string) error { 1261 if e.Server == nil { 1262 return nil 1263 } 1264 path := e.sandbox.Workdir.URIToPath(loc.URI) 1265 1266 // Verify that PrepareRename succeeds. 1267 prepareParams := &protocol.PrepareRenameParams{} 1268 prepareParams.TextDocument = e.TextDocumentIdentifier(path) 1269 prepareParams.Position = loc.Range.Start 1270 if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { 1271 return fmt.Errorf("preparing rename: %v", err) 1272 } 1273 1274 params := &protocol.RenameParams{ 1275 TextDocument: e.TextDocumentIdentifier(path), 1276 Position: loc.Range.Start, 1277 NewName: newName, 1278 } 1279 wsEdits, err := e.Server.Rename(ctx, params) 1280 if err != nil { 1281 return err 1282 } 1283 for _, change := range wsEdits.DocumentChanges { 1284 if err := e.applyDocumentChange(ctx, change); err != nil { 1285 return err 1286 } 1287 } 1288 return nil 1289 } 1290 1291 // Implementations returns implementations for the object at loc, as 1292 // returned by the connected LSP server. If no server is connected, it returns 1293 // (nil, nil). 1294 func (e *Editor) Implementations(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { 1295 if e.Server == nil { 1296 return nil, nil 1297 } 1298 path := e.sandbox.Workdir.URIToPath(loc.URI) 1299 e.mu.Lock() 1300 _, ok := e.buffers[path] 1301 e.mu.Unlock() 1302 if !ok { 1303 return nil, fmt.Errorf("buffer %q is not open", path) 1304 } 1305 params := &protocol.ImplementationParams{ 1306 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), 1307 } 1308 return e.Server.Implementation(ctx, params) 1309 } 1310 1311 func (e *Editor) SignatureHelp(ctx context.Context, loc protocol.Location) (*protocol.SignatureHelp, error) { 1312 if e.Server == nil { 1313 return nil, nil 1314 } 1315 path := e.sandbox.Workdir.URIToPath(loc.URI) 1316 e.mu.Lock() 1317 _, ok := e.buffers[path] 1318 e.mu.Unlock() 1319 if !ok { 1320 return nil, fmt.Errorf("buffer %q is not open", path) 1321 } 1322 params := &protocol.SignatureHelpParams{ 1323 TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), 1324 } 1325 return e.Server.SignatureHelp(ctx, params) 1326 } 1327 1328 func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { 1329 closed, opened, err := e.renameBuffers(oldPath, newPath) 1330 if err != nil { 1331 return err 1332 } 1333 1334 for _, c := range closed { 1335 if err := e.sendDidClose(ctx, c); err != nil { 1336 return err 1337 } 1338 } 1339 for _, o := range opened { 1340 if err := e.sendDidOpen(ctx, o); err != nil { 1341 return err 1342 } 1343 } 1344 1345 // Finally, perform the renaming on disk. 1346 if err := e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath); err != nil { 1347 return fmt.Errorf("renaming sandbox file: %w", err) 1348 } 1349 return nil 1350 } 1351 1352 // renameBuffers renames in-memory buffers affected by the renaming of 1353 // oldPath->newPath, returning the resulting text documents that must be closed 1354 // and opened over the LSP. 1355 func (e *Editor) renameBuffers(oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) { 1356 e.mu.Lock() 1357 defer e.mu.Unlock() 1358 1359 // In case either oldPath or newPath is absolute, convert to absolute paths 1360 // before checking for containment. 1361 oldAbs := e.sandbox.Workdir.AbsPath(oldPath) 1362 newAbs := e.sandbox.Workdir.AbsPath(newPath) 1363 1364 // Collect buffers that are affected by the given file or directory renaming. 1365 buffersToRename := make(map[string]string) // old path -> new path 1366 1367 for path := range e.buffers { 1368 abs := e.sandbox.Workdir.AbsPath(path) 1369 if oldAbs == abs || pathutil.InDir(oldAbs, abs) { 1370 rel, err := filepath.Rel(oldAbs, abs) 1371 if err != nil { 1372 return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err) 1373 } 1374 nabs := filepath.Join(newAbs, rel) 1375 newPath := e.sandbox.Workdir.RelPath(nabs) 1376 buffersToRename[path] = newPath 1377 } 1378 } 1379 1380 // Update buffers, and build protocol changes. 1381 for old, new := range buffersToRename { 1382 buf := e.buffers[old] 1383 delete(e.buffers, old) 1384 buf.version = 1 1385 buf.path = new 1386 e.buffers[new] = buf 1387 1388 closed = append(closed, e.TextDocumentIdentifier(old)) 1389 opened = append(opened, e.textDocumentItem(buf)) 1390 } 1391 1392 return closed, opened, nil 1393 } 1394 1395 func (e *Editor) applyDocumentChange(ctx context.Context, change protocol.DocumentChanges) error { 1396 if change.RenameFile != nil { 1397 oldPath := e.sandbox.Workdir.URIToPath(change.RenameFile.OldURI) 1398 newPath := e.sandbox.Workdir.URIToPath(change.RenameFile.NewURI) 1399 1400 return e.RenameFile(ctx, oldPath, newPath) 1401 } 1402 if change.TextDocumentEdit != nil { 1403 return e.applyTextDocumentEdit(ctx, *change.TextDocumentEdit) 1404 } 1405 panic("Internal error: one of RenameFile or TextDocumentEdit must be set") 1406 } 1407 1408 func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.TextDocumentEdit) error { 1409 path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) 1410 if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { 1411 return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) 1412 } 1413 if !e.HasBuffer(path) { 1414 err := e.OpenFile(ctx, path) 1415 if os.IsNotExist(err) { 1416 // TODO: it's unclear if this is correct. Here we create the buffer (with 1417 // version 1), then apply edits. Perhaps we should apply the edits before 1418 // sending the didOpen notification. 1419 e.CreateBuffer(ctx, path, "") 1420 err = nil 1421 } 1422 if err != nil { 1423 return err 1424 } 1425 } 1426 return e.EditBuffer(ctx, path, protocol.AsTextEdits(change.Edits)) 1427 } 1428 1429 // Config returns the current editor configuration. 1430 func (e *Editor) Config() EditorConfig { 1431 e.mu.Lock() 1432 defer e.mu.Unlock() 1433 return e.config 1434 } 1435 1436 func (e *Editor) SetConfig(cfg EditorConfig) { 1437 e.mu.Lock() 1438 e.config = cfg 1439 e.mu.Unlock() 1440 } 1441 1442 // ChangeConfiguration sets the new editor configuration, and if applicable 1443 // sends a didChangeConfiguration notification. 1444 // 1445 // An error is returned if the change notification failed to send. 1446 func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error { 1447 e.SetConfig(newConfig) 1448 if e.Server != nil { 1449 var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field 1450 if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil { 1451 return err 1452 } 1453 e.callsMu.Lock() 1454 e.calls.DidChangeConfiguration++ 1455 e.callsMu.Unlock() 1456 } 1457 return nil 1458 } 1459 1460 // ChangeWorkspaceFolders sets the new workspace folders, and sends a 1461 // didChangeWorkspaceFolders notification to the server. 1462 // 1463 // The given folders must all be unique. 1464 func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error { 1465 config := e.Config() 1466 1467 // capture existing folders so that we can compute the change. 1468 oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) 1469 newFolders := makeWorkspaceFolders(e.sandbox, folders) 1470 config.WorkspaceFolders = folders 1471 e.SetConfig(config) 1472 1473 if e.Server == nil { 1474 return nil 1475 } 1476 1477 var params protocol.DidChangeWorkspaceFoldersParams 1478 1479 // Keep track of old workspace folders that must be removed. 1480 toRemove := make(map[protocol.URI]protocol.WorkspaceFolder) 1481 for _, folder := range oldFolders { 1482 toRemove[folder.URI] = folder 1483 } 1484 1485 // Sanity check: if we see a folder twice the algorithm below doesn't work, 1486 // so track seen folders to ensure that we panic in that case. 1487 seen := make(map[protocol.URI]protocol.WorkspaceFolder) 1488 for _, folder := range newFolders { 1489 if _, ok := seen[folder.URI]; ok { 1490 panic(fmt.Sprintf("folder %s seen twice", folder.URI)) 1491 } 1492 1493 // If this folder already exists, we don't want to remove it. 1494 // Otherwise, we need to add it. 1495 if _, ok := toRemove[folder.URI]; ok { 1496 delete(toRemove, folder.URI) 1497 } else { 1498 params.Event.Added = append(params.Event.Added, folder) 1499 } 1500 } 1501 1502 for _, v := range toRemove { 1503 params.Event.Removed = append(params.Event.Removed, v) 1504 } 1505 1506 return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms) 1507 } 1508 1509 // CodeAction executes a codeAction request on the server. 1510 // If loc.Range is zero, the whole file is implied. 1511 func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { 1512 if e.Server == nil { 1513 return nil, nil 1514 } 1515 path := e.sandbox.Workdir.URIToPath(loc.URI) 1516 e.mu.Lock() 1517 _, ok := e.buffers[path] 1518 e.mu.Unlock() 1519 if !ok { 1520 return nil, fmt.Errorf("buffer %q is not open", path) 1521 } 1522 params := &protocol.CodeActionParams{ 1523 TextDocument: e.TextDocumentIdentifier(path), 1524 Context: protocol.CodeActionContext{ 1525 Diagnostics: diagnostics, 1526 }, 1527 Range: loc.Range, // may be zero 1528 } 1529 lens, err := e.Server.CodeAction(ctx, params) 1530 if err != nil { 1531 return nil, err 1532 } 1533 return lens, nil 1534 } 1535 1536 func (e *Editor) EditResolveSupport() (bool, error) { 1537 capabilities, err := clientCapabilities(e.Config()) 1538 if err != nil { 1539 return false, err 1540 } 1541 return capabilities.TextDocument.CodeAction.ResolveSupport != nil && slices.Contains(capabilities.TextDocument.CodeAction.ResolveSupport.Properties, "edit"), nil 1542 } 1543 1544 // Hover triggers a hover at the given position in an open buffer. 1545 func (e *Editor) Hover(ctx context.Context, loc protocol.Location) (*protocol.MarkupContent, protocol.Location, error) { 1546 if err := e.checkBufferLocation(loc); err != nil { 1547 return nil, protocol.Location{}, err 1548 } 1549 params := &protocol.HoverParams{} 1550 params.TextDocument.URI = loc.URI 1551 params.Position = loc.Range.Start 1552 1553 resp, err := e.Server.Hover(ctx, params) 1554 if err != nil { 1555 return nil, protocol.Location{}, fmt.Errorf("hover: %w", err) 1556 } 1557 if resp == nil { 1558 return nil, protocol.Location{}, nil 1559 } 1560 return &resp.Contents, protocol.Location{URI: loc.URI, Range: resp.Range}, nil 1561 } 1562 1563 func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { 1564 if e.Server == nil { 1565 return nil, nil 1566 } 1567 params := &protocol.DocumentLinkParams{} 1568 params.TextDocument.URI = e.sandbox.Workdir.URI(path) 1569 return e.Server.DocumentLink(ctx, params) 1570 } 1571 1572 func (e *Editor) DocumentHighlight(ctx context.Context, loc protocol.Location) ([]protocol.DocumentHighlight, error) { 1573 if e.Server == nil { 1574 return nil, nil 1575 } 1576 if err := e.checkBufferLocation(loc); err != nil { 1577 return nil, err 1578 } 1579 params := &protocol.DocumentHighlightParams{} 1580 params.TextDocument.URI = loc.URI 1581 params.Position = loc.Range.Start 1582 1583 return e.Server.DocumentHighlight(ctx, params) 1584 } 1585 1586 // SemanticTokensFull invokes textDocument/semanticTokens/full, and interprets 1587 // its result. 1588 func (e *Editor) SemanticTokensFull(ctx context.Context, path string) ([]SemanticToken, error) { 1589 p := &protocol.SemanticTokensParams{ 1590 TextDocument: protocol.TextDocumentIdentifier{ 1591 URI: e.sandbox.Workdir.URI(path), 1592 }, 1593 } 1594 resp, err := e.Server.SemanticTokensFull(ctx, p) 1595 if err != nil { 1596 return nil, err 1597 } 1598 content, ok := e.BufferText(path) 1599 if !ok { 1600 return nil, fmt.Errorf("buffer %s is not open", path) 1601 } 1602 return e.interpretTokens(resp.Data, content), nil 1603 } 1604 1605 // SemanticTokensRange invokes textDocument/semanticTokens/range, and 1606 // interprets its result. 1607 func (e *Editor) SemanticTokensRange(ctx context.Context, loc protocol.Location) ([]SemanticToken, error) { 1608 p := &protocol.SemanticTokensRangeParams{ 1609 TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, 1610 Range: loc.Range, 1611 } 1612 resp, err := e.Server.SemanticTokensRange(ctx, p) 1613 if err != nil { 1614 return nil, err 1615 } 1616 path := e.sandbox.Workdir.URIToPath(loc.URI) 1617 // As noted above: buffers should be keyed by protocol.DocumentURI. 1618 content, ok := e.BufferText(path) 1619 if !ok { 1620 return nil, fmt.Errorf("buffer %s is not open", path) 1621 } 1622 return e.interpretTokens(resp.Data, content), nil 1623 } 1624 1625 // A SemanticToken is an interpreted semantic token value. 1626 type SemanticToken struct { 1627 Token string 1628 TokenType string 1629 Mod string 1630 } 1631 1632 // Note: previously this function elided comment, string, and number tokens. 1633 // Instead, filtering of token types should be done by the caller. 1634 func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken { 1635 e.mu.Lock() 1636 legend := e.semTokOpts.Legend 1637 e.mu.Unlock() 1638 lines := strings.Split(contents, "\n") 1639 ans := []SemanticToken{} 1640 line, col := 1, 1 1641 for i := 0; i < len(x); i += 5 { 1642 line += int(x[i]) 1643 col += int(x[i+1]) 1644 if x[i] != 0 { // new line 1645 col = int(x[i+1]) + 1 // 1-based column numbers 1646 } 1647 sz := x[i+2] 1648 t := legend.TokenTypes[x[i+3]] 1649 l := x[i+4] 1650 var mods []string 1651 for i, mod := range legend.TokenModifiers { 1652 if l&(1<<i) != 0 { 1653 mods = append(mods, mod) 1654 } 1655 } 1656 // Preexisting note: "col is a utf-8 offset" 1657 // TODO(rfindley): is that true? Or is it UTF-16, like other columns in the LSP? 1658 tok := lines[line-1][col-1 : col-1+int(sz)] 1659 ans = append(ans, SemanticToken{tok, t, strings.Join(mods, " ")}) 1660 } 1661 return ans 1662 }