golang.org/x/tools/gopls@v0.15.3/internal/cmd/cmd.go (about) 1 // Copyright 2018 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 cmd handles the gopls command line. 6 // It contains a handler for each of the modes, along with all the flag handling 7 // and the command line output format. 8 package cmd 9 10 import ( 11 "context" 12 "flag" 13 "fmt" 14 "log" 15 "os" 16 "path/filepath" 17 "reflect" 18 "sort" 19 "strings" 20 "sync" 21 "text/tabwriter" 22 "time" 23 24 "golang.org/x/tools/gopls/internal/cache" 25 "golang.org/x/tools/gopls/internal/debug" 26 "golang.org/x/tools/gopls/internal/filecache" 27 "golang.org/x/tools/gopls/internal/lsprpc" 28 "golang.org/x/tools/gopls/internal/protocol" 29 "golang.org/x/tools/gopls/internal/protocol/command" 30 "golang.org/x/tools/gopls/internal/server" 31 "golang.org/x/tools/gopls/internal/settings" 32 "golang.org/x/tools/gopls/internal/util/browser" 33 bugpkg "golang.org/x/tools/gopls/internal/util/bug" 34 "golang.org/x/tools/gopls/internal/util/constraints" 35 "golang.org/x/tools/internal/diff" 36 "golang.org/x/tools/internal/jsonrpc2" 37 "golang.org/x/tools/internal/tool" 38 ) 39 40 // Application is the main application as passed to tool.Main 41 // It handles the main command line parsing and dispatch to the sub commands. 42 type Application struct { 43 // Core application flags 44 45 // Embed the basic profiling flags supported by the tool package 46 tool.Profile 47 48 // We include the server configuration directly for now, so the flags work 49 // even without the verb. 50 // TODO: Remove this when we stop allowing the serve verb by default. 51 Serve Serve 52 53 // the options configuring function to invoke when building a server 54 options func(*settings.Options) 55 56 // Support for remote LSP server. 57 Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."` 58 59 // Verbose enables verbose logging. 60 Verbose bool `flag:"v,verbose" help:"verbose output"` 61 62 // VeryVerbose enables a higher level of verbosity in logging output. 63 VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"` 64 65 // Control ocagent export of telemetry 66 OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"` 67 68 // PrepareOptions is called to update the options when a new view is built. 69 // It is primarily to allow the behavior of gopls to be modified by hooks. 70 PrepareOptions func(*settings.Options) 71 72 // editFlags holds flags that control how file edit operations 73 // are applied, in particular when the server makes an ApplyEdits 74 // downcall to the client. Present only for commands that apply edits. 75 editFlags *EditFlags 76 } 77 78 // EditFlags defines flags common to {fix,format,imports,rename} 79 // that control how edits are applied to the client's files. 80 // 81 // The type is exported for flag reflection. 82 // 83 // The -write, -diff, and -list flags are orthogonal but any 84 // of them suppresses the default behavior, which is to print 85 // the edited file contents. 86 type EditFlags struct { 87 Write bool `flag:"w,write" help:"write edited content to source files"` 88 Preserve bool `flag:"preserve" help:"with -write, make copies of original files"` 89 Diff bool `flag:"d,diff" help:"display diffs instead of edited file content"` 90 List bool `flag:"l,list" help:"display names of edited files"` 91 } 92 93 func (app *Application) verbose() bool { 94 return app.Verbose || app.VeryVerbose 95 } 96 97 // New returns a new Application ready to run. 98 func New(options func(*settings.Options)) *Application { 99 app := &Application{ 100 options: options, 101 OCAgent: "off", //TODO: Remove this line to default the exporter to on 102 103 Serve: Serve{ 104 RemoteListenTimeout: 1 * time.Minute, 105 }, 106 } 107 app.Serve.app = app 108 return app 109 } 110 111 // Name implements tool.Application returning the binary name. 112 func (app *Application) Name() string { return "gopls" } 113 114 // Usage implements tool.Application returning empty extra argument usage. 115 func (app *Application) Usage() string { return "" } 116 117 // ShortHelp implements tool.Application returning the main binary help. 118 func (app *Application) ShortHelp() string { 119 return "" 120 } 121 122 // DetailedHelp implements tool.Application returning the main binary help. 123 // This includes the short help for all the sub commands. 124 func (app *Application) DetailedHelp(f *flag.FlagSet) { 125 w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) 126 defer w.Flush() 127 128 fmt.Fprint(w, ` 129 gopls is a Go language server. 130 131 It is typically used with an editor to provide language features. When no 132 command is specified, gopls will default to the 'serve' command. The language 133 features can also be accessed via the gopls command-line interface. 134 135 Usage: 136 gopls help [<subject>] 137 138 Command: 139 `) 140 fmt.Fprint(w, "\nMain\t\n") 141 for _, c := range app.mainCommands() { 142 fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) 143 } 144 fmt.Fprint(w, "\t\nFeatures\t\n") 145 for _, c := range app.featureCommands() { 146 fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) 147 } 148 if app.verbose() { 149 fmt.Fprint(w, "\t\nInternal Use Only\t\n") 150 for _, c := range app.internalCommands() { 151 fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) 152 } 153 } 154 fmt.Fprint(w, "\nflags:\n") 155 printFlagDefaults(f) 156 } 157 158 // this is a slightly modified version of flag.PrintDefaults to give us control 159 func printFlagDefaults(s *flag.FlagSet) { 160 var flags [][]*flag.Flag 161 seen := map[flag.Value]int{} 162 s.VisitAll(func(f *flag.Flag) { 163 if i, ok := seen[f.Value]; !ok { 164 seen[f.Value] = len(flags) 165 flags = append(flags, []*flag.Flag{f}) 166 } else { 167 flags[i] = append(flags[i], f) 168 } 169 }) 170 for _, entry := range flags { 171 sort.SliceStable(entry, func(i, j int) bool { 172 return len(entry[i].Name) < len(entry[j].Name) 173 }) 174 var b strings.Builder 175 for i, f := range entry { 176 switch i { 177 case 0: 178 b.WriteString(" -") 179 default: 180 b.WriteString(",-") 181 } 182 b.WriteString(f.Name) 183 } 184 185 f := entry[0] 186 name, usage := flag.UnquoteUsage(f) 187 if len(name) > 0 { 188 b.WriteString("=") 189 b.WriteString(name) 190 } 191 // Boolean flags of one ASCII letter are so common we 192 // treat them specially, putting their usage on the same line. 193 if b.Len() <= 4 { // space, space, '-', 'x'. 194 b.WriteString("\t") 195 } else { 196 // Four spaces before the tab triggers good alignment 197 // for both 4- and 8-space tab stops. 198 b.WriteString("\n \t") 199 } 200 b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) 201 if !isZeroValue(f, f.DefValue) { 202 if reflect.TypeOf(f.Value).Elem().Name() == "stringValue" { 203 fmt.Fprintf(&b, " (default %q)", f.DefValue) 204 } else { 205 fmt.Fprintf(&b, " (default %v)", f.DefValue) 206 } 207 } 208 fmt.Fprint(s.Output(), b.String(), "\n") 209 } 210 } 211 212 // isZeroValue is copied from the flags package 213 func isZeroValue(f *flag.Flag, value string) bool { 214 // Build a zero value of the flag's Value type, and see if the 215 // result of calling its String method equals the value passed in. 216 // This works unless the Value type is itself an interface type. 217 typ := reflect.TypeOf(f.Value) 218 var z reflect.Value 219 if typ.Kind() == reflect.Ptr { 220 z = reflect.New(typ.Elem()) 221 } else { 222 z = reflect.Zero(typ) 223 } 224 return value == z.Interface().(flag.Value).String() 225 } 226 227 // Run takes the args after top level flag processing, and invokes the correct 228 // sub command as specified by the first argument. 229 // If no arguments are passed it will invoke the server sub command, as a 230 // temporary measure for compatibility. 231 func (app *Application) Run(ctx context.Context, args ...string) error { 232 // In the category of "things we can do while waiting for the Go command": 233 // Pre-initialize the filecache, which takes ~50ms to hash the gopls 234 // executable, and immediately runs a gc. 235 filecache.Start() 236 237 ctx = debug.WithInstance(ctx, app.OCAgent) 238 if len(args) == 0 { 239 s := flag.NewFlagSet(app.Name(), flag.ExitOnError) 240 return tool.Run(ctx, s, &app.Serve, args) 241 } 242 command, args := args[0], args[1:] 243 for _, c := range app.Commands() { 244 if c.Name() == command { 245 s := flag.NewFlagSet(app.Name(), flag.ExitOnError) 246 return tool.Run(ctx, s, c, args) 247 } 248 } 249 return tool.CommandLineErrorf("Unknown command %v", command) 250 } 251 252 // Commands returns the set of commands supported by the gopls tool on the 253 // command line. 254 // The command is specified by the first non flag argument. 255 func (app *Application) Commands() []tool.Application { 256 var commands []tool.Application 257 commands = append(commands, app.mainCommands()...) 258 commands = append(commands, app.featureCommands()...) 259 commands = append(commands, app.internalCommands()...) 260 return commands 261 } 262 263 func (app *Application) mainCommands() []tool.Application { 264 return []tool.Application{ 265 &app.Serve, 266 &version{app: app}, 267 &bug{app: app}, 268 &help{app: app}, 269 &apiJSON{app: app}, 270 &licenses{app: app}, 271 } 272 } 273 274 func (app *Application) internalCommands() []tool.Application { 275 return []tool.Application{ 276 &vulncheck{app: app}, 277 } 278 } 279 280 func (app *Application) featureCommands() []tool.Application { 281 return []tool.Application{ 282 &callHierarchy{app: app}, 283 &check{app: app}, 284 &codelens{app: app}, 285 &definition{app: app}, 286 &execute{app: app}, 287 &foldingRanges{app: app}, 288 &format{app: app}, 289 &highlight{app: app}, 290 &implementation{app: app}, 291 &imports{app: app}, 292 newRemote(app, ""), 293 newRemote(app, "inspect"), 294 &links{app: app}, 295 &prepareRename{app: app}, 296 &references{app: app}, 297 &rename{app: app}, 298 &semtok{app: app}, 299 &signature{app: app}, 300 &stats{app: app}, 301 &suggestedFix{app: app}, 302 &symbols{app: app}, 303 304 &workspaceSymbol{app: app}, 305 } 306 } 307 308 var ( 309 internalMu sync.Mutex 310 internalConnections = make(map[string]*connection) 311 ) 312 313 // connect creates and initializes a new in-process gopls session. 314 // 315 // If onProgress is set, it is called for each new progress notification. 316 func (app *Application) connect(ctx context.Context, onProgress func(*protocol.ProgressParams)) (*connection, error) { 317 switch { 318 case app.Remote == "": 319 client := newClient(app, onProgress) 320 options := settings.DefaultOptions(app.options) 321 server := server.New(cache.NewSession(ctx, cache.New(nil)), client, options) 322 conn := newConnection(server, client) 323 if err := conn.initialize(protocol.WithClient(ctx, client), app.options); err != nil { 324 return nil, err 325 } 326 return conn, nil 327 328 default: 329 return app.connectRemote(ctx, app.Remote) 330 } 331 } 332 333 func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) { 334 conn, err := lsprpc.ConnectToRemote(ctx, remote) 335 if err != nil { 336 return nil, err 337 } 338 stream := jsonrpc2.NewHeaderStream(conn) 339 cc := jsonrpc2.NewConn(stream) 340 server := protocol.ServerDispatcher(cc) 341 client := newClient(app, nil) 342 connection := newConnection(server, client) 343 ctx = protocol.WithClient(ctx, connection.client) 344 cc.Go(ctx, 345 protocol.Handlers( 346 protocol.ClientHandler(client, jsonrpc2.MethodNotFound))) 347 return connection, connection.initialize(ctx, app.options) 348 } 349 350 func (c *connection) initialize(ctx context.Context, options func(*settings.Options)) error { 351 wd, err := os.Getwd() 352 if err != nil { 353 return fmt.Errorf("finding workdir: %v", err) 354 } 355 params := &protocol.ParamInitialize{} 356 params.RootURI = protocol.URIFromPath(wd) 357 params.Capabilities.Workspace.Configuration = true 358 359 // Make sure to respect configured options when sending initialize request. 360 opts := settings.DefaultOptions(options) 361 // If you add an additional option here, you must update the map key in connect. 362 params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{ 363 ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, 364 } 365 params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport 366 params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{} 367 params.Capabilities.TextDocument.SemanticTokens.Formats = []protocol.TokenFormat{"relative"} 368 params.Capabilities.TextDocument.SemanticTokens.Requests.Range = &protocol.Or_ClientSemanticTokensRequestOptions_range{Value: true} 369 //params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true 370 params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true} 371 params.Capabilities.TextDocument.SemanticTokens.TokenTypes = protocol.SemanticTypes() 372 params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = protocol.SemanticModifiers() 373 374 // If the subcommand has registered a progress handler, report the progress 375 // capability. 376 if c.client.onProgress != nil { 377 params.Capabilities.Window.WorkDoneProgress = true 378 } 379 380 params.InitializationOptions = map[string]interface{}{ 381 "symbolMatcher": string(opts.SymbolMatcher), 382 } 383 if _, err := c.Server.Initialize(ctx, params); err != nil { 384 return err 385 } 386 if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 387 return err 388 } 389 return nil 390 } 391 392 type connection struct { 393 protocol.Server 394 client *cmdClient 395 } 396 397 // cmdClient defines the protocol.Client interface behavior of the gopls CLI tool. 398 type cmdClient struct { 399 app *Application 400 onProgress func(*protocol.ProgressParams) 401 402 filesMu sync.Mutex // guards files map and each cmdFile.diagnostics 403 files map[protocol.DocumentURI]*cmdFile 404 } 405 406 type cmdFile struct { 407 uri protocol.DocumentURI 408 mapper *protocol.Mapper 409 err error 410 diagnostics []protocol.Diagnostic 411 } 412 413 func newClient(app *Application, onProgress func(*protocol.ProgressParams)) *cmdClient { 414 return &cmdClient{ 415 app: app, 416 onProgress: onProgress, 417 files: make(map[protocol.DocumentURI]*cmdFile), 418 } 419 } 420 421 func newConnection(server protocol.Server, client *cmdClient) *connection { 422 return &connection{ 423 Server: server, 424 client: client, 425 } 426 } 427 428 func (c *cmdClient) CodeLensRefresh(context.Context) error { return nil } 429 430 func (c *cmdClient) FoldingRangeRefresh(context.Context) error { return nil } 431 432 func (c *cmdClient) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } 433 434 func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { 435 fmt.Fprintf(os.Stderr, "%s: %s\n", p.Type, p.Message) 436 return nil 437 } 438 439 func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { 440 return nil, nil 441 } 442 443 func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { 444 // This logic causes server logging to be double-prefixed with a timestamp. 445 // 2023/11/08 10:50:21 Error:2023/11/08 10:50:21 <actual message> 446 // TODO(adonovan): print just p.Message, plus a newline if needed? 447 switch p.Type { 448 case protocol.Error: 449 log.Print("Error:", p.Message) 450 case protocol.Warning: 451 log.Print("Warning:", p.Message) 452 case protocol.Info: 453 if c.app.verbose() { 454 log.Print("Info:", p.Message) 455 } 456 case protocol.Log: 457 if c.app.verbose() { 458 log.Print("Log:", p.Message) 459 } 460 default: 461 if c.app.verbose() { 462 log.Print(p.Message) 463 } 464 } 465 return nil 466 } 467 468 func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } 469 470 func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { 471 return nil 472 } 473 474 func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error { 475 return nil 476 } 477 478 func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { 479 return nil, nil 480 } 481 482 func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { 483 results := make([]interface{}, len(p.Items)) 484 for i, item := range p.Items { 485 if item.Section != "gopls" { 486 continue 487 } 488 m := map[string]interface{}{ 489 "analyses": map[string]any{ 490 "fillreturns": true, 491 "nonewvars": true, 492 "noresultvalues": true, 493 "undeclaredname": true, 494 }, 495 } 496 if c.app.VeryVerbose { 497 m["verboseOutput"] = true 498 } 499 results[i] = m 500 } 501 return results, nil 502 } 503 504 func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { 505 if err := c.applyWorkspaceEdit(&p.Edit); err != nil { 506 return &protocol.ApplyWorkspaceEditResult{FailureReason: err.Error()}, nil 507 } 508 return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil 509 } 510 511 // applyWorkspaceEdit applies a complete WorkspaceEdit to the client's 512 // files, honoring the preferred edit mode specified by cli.app.editMode. 513 // (Used by rename and by ApplyEdit downcalls.) 514 func (cli *cmdClient) applyWorkspaceEdit(edit *protocol.WorkspaceEdit) error { 515 var orderedURIs []protocol.DocumentURI 516 edits := map[protocol.DocumentURI][]protocol.TextEdit{} 517 for _, c := range edit.DocumentChanges { 518 if c.TextDocumentEdit != nil { 519 uri := c.TextDocumentEdit.TextDocument.URI 520 edits[uri] = append(edits[uri], protocol.AsTextEdits(c.TextDocumentEdit.Edits)...) 521 orderedURIs = append(orderedURIs, uri) 522 } 523 if c.RenameFile != nil { 524 return fmt.Errorf("client does not support file renaming (%s -> %s)", 525 c.RenameFile.OldURI, 526 c.RenameFile.NewURI) 527 } 528 } 529 sortSlice(orderedURIs) 530 for _, uri := range orderedURIs { 531 f := cli.openFile(uri) 532 if f.err != nil { 533 return f.err 534 } 535 if err := applyTextEdits(f.mapper, edits[uri], cli.app.editFlags); err != nil { 536 return err 537 } 538 } 539 return nil 540 } 541 542 func sortSlice[T constraints.Ordered](slice []T) { 543 sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) 544 } 545 546 // applyTextEdits applies a list of edits to the mapper file content, 547 // using the preferred edit mode. It is a no-op if there are no edits. 548 func applyTextEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, flags *EditFlags) error { 549 if len(edits) == 0 { 550 return nil 551 } 552 newContent, renameEdits, err := protocol.ApplyEdits(mapper, edits) 553 if err != nil { 554 return err 555 } 556 557 filename := mapper.URI.Path() 558 559 if flags.List { 560 fmt.Println(filename) 561 } 562 563 if flags.Write { 564 if flags.Preserve { 565 if err := os.Rename(filename, filename+".orig"); err != nil { 566 return err 567 } 568 } 569 if err := os.WriteFile(filename, newContent, 0644); err != nil { 570 return err 571 } 572 } 573 574 if flags.Diff { 575 unified, err := diff.ToUnified(filename+".orig", filename, string(mapper.Content), renameEdits, diff.DefaultContextLines) 576 if err != nil { 577 return err 578 } 579 fmt.Print(unified) 580 } 581 582 // No flags: just print edited file content. 583 // TODO(adonovan): how is this ever useful with multiple files? 584 if !(flags.List || flags.Write || flags.Diff) { 585 os.Stdout.Write(newContent) 586 } 587 588 return nil 589 } 590 591 func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error { 592 // Don't worry about diagnostics without versions. 593 if p.Version == 0 { 594 return nil 595 } 596 597 c.filesMu.Lock() 598 defer c.filesMu.Unlock() 599 600 file := c.getFile(p.URI) 601 file.diagnostics = append(file.diagnostics, p.Diagnostics...) 602 603 // Perform a crude in-place deduplication. 604 // TODO(golang/go#60122): replace the gopls.diagnose_files 605 // command with support for textDocument/diagnostic, 606 // so that we don't need to do this de-duplication. 607 type key [6]interface{} 608 seen := make(map[key]bool) 609 out := file.diagnostics[:0] 610 for _, d := range file.diagnostics { 611 var codeHref string 612 if desc := d.CodeDescription; desc != nil { 613 codeHref = desc.Href 614 } 615 k := key{d.Range, d.Severity, d.Code, codeHref, d.Source, d.Message} 616 if !seen[k] { 617 seen[k] = true 618 out = append(out, d) 619 } 620 } 621 file.diagnostics = out 622 623 return nil 624 } 625 626 func (c *cmdClient) Progress(_ context.Context, params *protocol.ProgressParams) error { 627 if c.onProgress != nil { 628 c.onProgress(params) 629 } 630 return nil 631 } 632 633 func (c *cmdClient) ShowDocument(ctx context.Context, params *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { 634 var success bool 635 if params.External { 636 // Open URI in external browser. 637 success = browser.Open(params.URI) 638 } else { 639 // Open file in editor, optionally taking focus and selecting a range. 640 // (cmdClient has no editor. Should it fork+exec $EDITOR?) 641 log.Printf("Server requested that client editor open %q (takeFocus=%t, selection=%+v)", 642 params.URI, params.TakeFocus, params.Selection) 643 success = true 644 } 645 return &protocol.ShowDocumentResult{Success: success}, nil 646 } 647 648 func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { 649 return nil 650 } 651 652 func (c *cmdClient) DiagnosticRefresh(context.Context) error { 653 return nil 654 } 655 656 func (c *cmdClient) InlayHintRefresh(context.Context) error { 657 return nil 658 } 659 660 func (c *cmdClient) SemanticTokensRefresh(context.Context) error { 661 return nil 662 } 663 664 func (c *cmdClient) InlineValueRefresh(context.Context) error { 665 return nil 666 } 667 668 func (c *cmdClient) getFile(uri protocol.DocumentURI) *cmdFile { 669 file, found := c.files[uri] 670 if !found || file.err != nil { 671 file = &cmdFile{ 672 uri: uri, 673 } 674 c.files[uri] = file 675 } 676 if file.mapper == nil { 677 content, err := os.ReadFile(uri.Path()) 678 if err != nil { 679 file.err = fmt.Errorf("getFile: %v: %v", uri, err) 680 return file 681 } 682 file.mapper = protocol.NewMapper(uri, content) 683 } 684 return file 685 } 686 687 func (c *cmdClient) openFile(uri protocol.DocumentURI) *cmdFile { 688 c.filesMu.Lock() 689 defer c.filesMu.Unlock() 690 return c.getFile(uri) 691 } 692 693 // TODO(adonovan): provide convenience helpers to: 694 // - map a (URI, protocol.Range) to a MappedRange; 695 // - parse a command-line argument to a MappedRange. 696 func (c *connection) openFile(ctx context.Context, uri protocol.DocumentURI) (*cmdFile, error) { 697 file := c.client.openFile(uri) 698 if file.err != nil { 699 return nil, file.err 700 } 701 702 p := &protocol.DidOpenTextDocumentParams{ 703 TextDocument: protocol.TextDocumentItem{ 704 URI: uri, 705 LanguageID: "go", 706 Version: 1, 707 Text: string(file.mapper.Content), 708 }, 709 } 710 if err := c.Server.DidOpen(ctx, p); err != nil { 711 // TODO(adonovan): is this assignment concurrency safe? 712 file.err = fmt.Errorf("%v: %v", uri, err) 713 return nil, file.err 714 } 715 return file, nil 716 } 717 718 func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { 719 // use range to avoid limits on full 720 resp, err := c.Server.SemanticTokensRange(ctx, p) 721 if err != nil { 722 return nil, err 723 } 724 return resp, nil 725 } 726 727 func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.DocumentURI) error { 728 cmd, err := command.NewDiagnoseFilesCommand("Diagnose files", command.DiagnoseFilesArgs{ 729 Files: files, 730 }) 731 if err != nil { 732 return err 733 } 734 _, err = c.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ 735 Command: cmd.Command, 736 Arguments: cmd.Arguments, 737 }) 738 return err 739 } 740 741 func (c *connection) terminate(ctx context.Context) { 742 //TODO: do we need to handle errors on these calls? 743 c.Shutdown(ctx) 744 //TODO: right now calling exit terminates the process, we should rethink that 745 //server.Exit(ctx) 746 } 747 748 // Implement io.Closer. 749 func (c *cmdClient) Close() error { 750 return nil 751 } 752 753 // -- conversions to span (UTF-8) domain -- 754 755 // locationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. 756 // Precondition: the URIs of Location and Mapper match. 757 func (f *cmdFile) locationSpan(loc protocol.Location) (span, error) { 758 // TODO(adonovan): check that l.URI matches m.URI. 759 return f.rangeSpan(loc.Range) 760 } 761 762 // rangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. 763 // The resulting span has valid Positions and Offsets. 764 func (f *cmdFile) rangeSpan(r protocol.Range) (span, error) { 765 start, end, err := f.mapper.RangeOffsets(r) 766 if err != nil { 767 return span{}, err 768 } 769 return f.offsetSpan(start, end) 770 } 771 772 // offsetSpan converts a byte-offset interval to a (UTF-8) span. 773 // The resulting span contains line, column, and offset information. 774 func (f *cmdFile) offsetSpan(start, end int) (span, error) { 775 if start > end { 776 return span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) 777 } 778 startPoint, err := offsetPoint(f.mapper, start) 779 if err != nil { 780 return span{}, fmt.Errorf("start: %v", err) 781 } 782 endPoint, err := offsetPoint(f.mapper, end) 783 if err != nil { 784 return span{}, fmt.Errorf("end: %v", err) 785 } 786 return newSpan(f.mapper.URI, startPoint, endPoint), nil 787 } 788 789 // offsetPoint converts a byte offset to a span (UTF-8) point. 790 // The resulting point contains line, column, and offset information. 791 func offsetPoint(m *protocol.Mapper, offset int) (point, error) { 792 if !(0 <= offset && offset <= len(m.Content)) { 793 return point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) 794 } 795 line, col8 := m.OffsetLineCol8(offset) 796 return newPoint(line, col8, offset), nil 797 } 798 799 // -- conversions from span (UTF-8) domain -- 800 801 // spanLocation converts a (UTF-8) span to a protocol (UTF-16) range. 802 // Precondition: the URIs of spanLocation and Mapper match. 803 func (f *cmdFile) spanLocation(s span) (protocol.Location, error) { 804 rng, err := f.spanRange(s) 805 if err != nil { 806 return protocol.Location{}, err 807 } 808 return f.mapper.RangeLocation(rng), nil 809 } 810 811 // spanRange converts a (UTF-8) span to a protocol (UTF-16) range. 812 // Precondition: the URIs of span and Mapper match. 813 func (f *cmdFile) spanRange(s span) (protocol.Range, error) { 814 // Assert that we aren't using the wrong mapper. 815 // We check only the base name, and case insensitively, 816 // because we can't assume clean paths, no symbolic links, 817 // case-sensitive directories. The authoritative answer 818 // requires querying the file system, and we don't want 819 // to do that. 820 if !strings.EqualFold(filepath.Base(string(f.mapper.URI)), filepath.Base(string(s.URI()))) { 821 return protocol.Range{}, bugpkg.Errorf("mapper is for file %q instead of %q", f.mapper.URI, s.URI()) 822 } 823 start, err := pointPosition(f.mapper, s.Start()) 824 if err != nil { 825 return protocol.Range{}, fmt.Errorf("start: %w", err) 826 } 827 end, err := pointPosition(f.mapper, s.End()) 828 if err != nil { 829 return protocol.Range{}, fmt.Errorf("end: %w", err) 830 } 831 return protocol.Range{Start: start, End: end}, nil 832 } 833 834 // pointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. 835 func pointPosition(m *protocol.Mapper, p point) (protocol.Position, error) { 836 if p.HasPosition() { 837 return m.LineCol8Position(p.Line(), p.Column()) 838 } 839 if p.HasOffset() { 840 return m.OffsetPosition(p.Offset()) 841 } 842 return protocol.Position{}, fmt.Errorf("point has neither offset nor line/column") 843 }