github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/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 "go/token" 15 "io/ioutil" 16 "log" 17 "os" 18 "reflect" 19 "sort" 20 "strings" 21 "sync" 22 "text/tabwriter" 23 "time" 24 25 "github.com/jhump/golang-x-tools/internal/jsonrpc2" 26 "github.com/jhump/golang-x-tools/internal/lsp" 27 "github.com/jhump/golang-x-tools/internal/lsp/cache" 28 "github.com/jhump/golang-x-tools/internal/lsp/debug" 29 "github.com/jhump/golang-x-tools/internal/lsp/lsprpc" 30 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 31 "github.com/jhump/golang-x-tools/internal/lsp/source" 32 "github.com/jhump/golang-x-tools/internal/span" 33 "github.com/jhump/golang-x-tools/internal/tool" 34 "github.com/jhump/golang-x-tools/internal/xcontext" 35 errors "golang.org/x/xerrors" 36 ) 37 38 // Application is the main application as passed to tool.Main 39 // It handles the main command line parsing and dispatch to the sub commands. 40 type Application struct { 41 // Core application flags 42 43 // Embed the basic profiling flags supported by the tool package 44 tool.Profile 45 46 // We include the server configuration directly for now, so the flags work 47 // even without the verb. 48 // TODO: Remove this when we stop allowing the serve verb by default. 49 Serve Serve 50 51 // the options configuring function to invoke when building a server 52 options func(*source.Options) 53 54 // The name of the binary, used in help and telemetry. 55 name string 56 57 // The working directory to run commands in. 58 wd string 59 60 // The environment variables to use. 61 env []string 62 63 // Support for remote LSP server. 64 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."` 65 66 // Verbose enables verbose logging. 67 Verbose bool `flag:"v,verbose" help:"verbose output"` 68 69 // VeryVerbose enables a higher level of verbosity in logging output. 70 VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"` 71 72 // Control ocagent export of telemetry 73 OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"` 74 75 // PrepareOptions is called to update the options when a new view is built. 76 // It is primarily to allow the behavior of gopls to be modified by hooks. 77 PrepareOptions func(*source.Options) 78 } 79 80 func (app *Application) verbose() bool { 81 return app.Verbose || app.VeryVerbose 82 } 83 84 // New returns a new Application ready to run. 85 func New(name, wd string, env []string, options func(*source.Options)) *Application { 86 if wd == "" { 87 wd, _ = os.Getwd() 88 } 89 app := &Application{ 90 options: options, 91 name: name, 92 wd: wd, 93 env: env, 94 OCAgent: "off", //TODO: Remove this line to default the exporter to on 95 96 Serve: Serve{ 97 RemoteListenTimeout: 1 * time.Minute, 98 }, 99 } 100 app.Serve.app = app 101 return app 102 } 103 104 // Name implements tool.Application returning the binary name. 105 func (app *Application) Name() string { return app.name } 106 107 // Usage implements tool.Application returning empty extra argument usage. 108 func (app *Application) Usage() string { return "" } 109 110 // ShortHelp implements tool.Application returning the main binary help. 111 func (app *Application) ShortHelp() string { 112 return "" 113 } 114 115 // DetailedHelp implements tool.Application returning the main binary help. 116 // This includes the short help for all the sub commands. 117 func (app *Application) DetailedHelp(f *flag.FlagSet) { 118 w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) 119 defer w.Flush() 120 121 fmt.Fprint(w, ` 122 gopls is a Go language server. 123 124 It is typically used with an editor to provide language features. When no 125 command is specified, gopls will default to the 'serve' command. The language 126 features can also be accessed via the gopls command-line interface. 127 128 Usage: 129 gopls help [<subject>] 130 131 Command: 132 `) 133 fmt.Fprint(w, "\nMain\t\n") 134 for _, c := range app.mainCommands() { 135 fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) 136 } 137 fmt.Fprint(w, "\t\nFeatures\t\n") 138 for _, c := range app.featureCommands() { 139 fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) 140 } 141 fmt.Fprint(w, "\nflags:\n") 142 printFlagDefaults(f) 143 } 144 145 // this is a slightly modified version of flag.PrintDefaults to give us control 146 func printFlagDefaults(s *flag.FlagSet) { 147 var flags [][]*flag.Flag 148 seen := map[flag.Value]int{} 149 s.VisitAll(func(f *flag.Flag) { 150 if i, ok := seen[f.Value]; !ok { 151 seen[f.Value] = len(flags) 152 flags = append(flags, []*flag.Flag{f}) 153 } else { 154 flags[i] = append(flags[i], f) 155 } 156 }) 157 for _, entry := range flags { 158 sort.SliceStable(entry, func(i, j int) bool { 159 return len(entry[i].Name) < len(entry[j].Name) 160 }) 161 var b strings.Builder 162 for i, f := range entry { 163 switch i { 164 case 0: 165 b.WriteString(" -") 166 default: 167 b.WriteString(",-") 168 } 169 b.WriteString(f.Name) 170 } 171 172 f := entry[0] 173 name, usage := flag.UnquoteUsage(f) 174 if len(name) > 0 { 175 b.WriteString("=") 176 b.WriteString(name) 177 } 178 // Boolean flags of one ASCII letter are so common we 179 // treat them specially, putting their usage on the same line. 180 if b.Len() <= 4 { // space, space, '-', 'x'. 181 b.WriteString("\t") 182 } else { 183 // Four spaces before the tab triggers good alignment 184 // for both 4- and 8-space tab stops. 185 b.WriteString("\n \t") 186 } 187 b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) 188 if !isZeroValue(f, f.DefValue) { 189 if reflect.TypeOf(f.Value).Elem().Name() == "stringValue" { 190 fmt.Fprintf(&b, " (default %q)", f.DefValue) 191 } else { 192 fmt.Fprintf(&b, " (default %v)", f.DefValue) 193 } 194 } 195 fmt.Fprint(s.Output(), b.String(), "\n") 196 } 197 } 198 199 // isZeroValue is copied from the flags package 200 func isZeroValue(f *flag.Flag, value string) bool { 201 // Build a zero value of the flag's Value type, and see if the 202 // result of calling its String method equals the value passed in. 203 // This works unless the Value type is itself an interface type. 204 typ := reflect.TypeOf(f.Value) 205 var z reflect.Value 206 if typ.Kind() == reflect.Ptr { 207 z = reflect.New(typ.Elem()) 208 } else { 209 z = reflect.Zero(typ) 210 } 211 return value == z.Interface().(flag.Value).String() 212 } 213 214 // Run takes the args after top level flag processing, and invokes the correct 215 // sub command as specified by the first argument. 216 // If no arguments are passed it will invoke the server sub command, as a 217 // temporary measure for compatibility. 218 func (app *Application) Run(ctx context.Context, args ...string) error { 219 ctx = debug.WithInstance(ctx, app.wd, app.OCAgent) 220 if len(args) == 0 { 221 s := flag.NewFlagSet(app.Name(), flag.ExitOnError) 222 return tool.Run(ctx, s, &app.Serve, args) 223 } 224 command, args := args[0], args[1:] 225 for _, c := range app.Commands() { 226 if c.Name() == command { 227 s := flag.NewFlagSet(app.Name(), flag.ExitOnError) 228 return tool.Run(ctx, s, c, args) 229 } 230 } 231 return tool.CommandLineErrorf("Unknown command %v", command) 232 } 233 234 // commands returns the set of commands supported by the gopls tool on the 235 // command line. 236 // The command is specified by the first non flag argument. 237 func (app *Application) Commands() []tool.Application { 238 var commands []tool.Application 239 commands = append(commands, app.mainCommands()...) 240 commands = append(commands, app.featureCommands()...) 241 return commands 242 } 243 244 func (app *Application) mainCommands() []tool.Application { 245 return []tool.Application{ 246 &app.Serve, 247 &version{app: app}, 248 &bug{app: app}, 249 &apiJSON{app: app}, 250 &licenses{app: app}, 251 } 252 } 253 254 func (app *Application) featureCommands() []tool.Application { 255 return []tool.Application{ 256 &callHierarchy{app: app}, 257 &check{app: app}, 258 &definition{app: app}, 259 &foldingRanges{app: app}, 260 &format{app: app}, 261 &highlight{app: app}, 262 &implementation{app: app}, 263 &imports{app: app}, 264 newRemote(app, ""), 265 newRemote(app, "inspect"), 266 &links{app: app}, 267 &prepareRename{app: app}, 268 &references{app: app}, 269 &rename{app: app}, 270 &semtok{app: app}, 271 &signature{app: app}, 272 &suggestedFix{app: app}, 273 &symbols{app: app}, 274 newWorkspace(app), 275 &workspaceSymbol{app: app}, 276 } 277 } 278 279 var ( 280 internalMu sync.Mutex 281 internalConnections = make(map[string]*connection) 282 ) 283 284 func (app *Application) connect(ctx context.Context) (*connection, error) { 285 switch { 286 case app.Remote == "": 287 connection := newConnection(app) 288 connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client) 289 ctx = protocol.WithClient(ctx, connection.Client) 290 return connection, connection.initialize(ctx, app.options) 291 case strings.HasPrefix(app.Remote, "internal@"): 292 internalMu.Lock() 293 defer internalMu.Unlock() 294 opts := source.DefaultOptions().Clone() 295 if app.options != nil { 296 app.options(opts) 297 } 298 key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher) 299 if c := internalConnections[key]; c != nil { 300 return c, nil 301 } 302 remote := app.Remote[len("internal@"):] 303 ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server 304 connection, err := app.connectRemote(ctx, remote) 305 if err != nil { 306 return nil, err 307 } 308 internalConnections[key] = connection 309 return connection, nil 310 default: 311 return app.connectRemote(ctx, app.Remote) 312 } 313 } 314 315 // CloseTestConnections terminates shared connections used in command tests. It 316 // should only be called from tests. 317 func CloseTestConnections(ctx context.Context) { 318 for _, c := range internalConnections { 319 c.Shutdown(ctx) 320 c.Exit(ctx) 321 } 322 } 323 324 func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) { 325 connection := newConnection(app) 326 conn, err := lsprpc.ConnectToRemote(ctx, remote) 327 if err != nil { 328 return nil, err 329 } 330 stream := jsonrpc2.NewHeaderStream(conn) 331 cc := jsonrpc2.NewConn(stream) 332 connection.Server = protocol.ServerDispatcher(cc) 333 ctx = protocol.WithClient(ctx, connection.Client) 334 cc.Go(ctx, 335 protocol.Handlers( 336 protocol.ClientHandler(connection.Client, 337 jsonrpc2.MethodNotFound))) 338 return connection, connection.initialize(ctx, app.options) 339 } 340 341 var matcherString = map[source.SymbolMatcher]string{ 342 source.SymbolFuzzy: "fuzzy", 343 source.SymbolCaseSensitive: "caseSensitive", 344 source.SymbolCaseInsensitive: "caseInsensitive", 345 } 346 347 func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error { 348 params := &protocol.ParamInitialize{} 349 params.RootURI = protocol.URIFromPath(c.Client.app.wd) 350 params.Capabilities.Workspace.Configuration = true 351 352 // Make sure to respect configured options when sending initialize request. 353 opts := source.DefaultOptions().Clone() 354 if options != nil { 355 options(opts) 356 } 357 // If you add an additional option here, you must update the map key in connect. 358 params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{ 359 ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, 360 } 361 params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport 362 params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{} 363 params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"} 364 params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true 365 params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true 366 params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes() 367 params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers() 368 params.InitializationOptions = map[string]interface{}{ 369 "symbolMatcher": matcherString[opts.SymbolMatcher], 370 } 371 if _, err := c.Server.Initialize(ctx, params); err != nil { 372 return err 373 } 374 if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 375 return err 376 } 377 return nil 378 } 379 380 type connection struct { 381 protocol.Server 382 Client *cmdClient 383 } 384 385 type cmdClient struct { 386 protocol.Server 387 app *Application 388 fset *token.FileSet 389 390 diagnosticsMu sync.Mutex 391 diagnosticsDone chan struct{} 392 393 filesMu sync.Mutex 394 files map[span.URI]*cmdFile 395 } 396 397 type cmdFile struct { 398 uri span.URI 399 mapper *protocol.ColumnMapper 400 err error 401 added bool 402 diagnostics []protocol.Diagnostic 403 } 404 405 func newConnection(app *Application) *connection { 406 return &connection{ 407 Client: &cmdClient{ 408 app: app, 409 fset: token.NewFileSet(), 410 files: make(map[span.URI]*cmdFile), 411 }, 412 } 413 } 414 415 // fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file. 416 func fileURI(uri protocol.DocumentURI) span.URI { 417 sURI := uri.SpanURI() 418 if !sURI.IsFile() { 419 panic(fmt.Sprintf("%q is not a file URI", uri)) 420 } 421 return sURI 422 } 423 424 func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil } 425 426 func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { 427 return nil, nil 428 } 429 430 func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { 431 switch p.Type { 432 case protocol.Error: 433 log.Print("Error:", p.Message) 434 case protocol.Warning: 435 log.Print("Warning:", p.Message) 436 case protocol.Info: 437 if c.app.verbose() { 438 log.Print("Info:", p.Message) 439 } 440 case protocol.Log: 441 if c.app.verbose() { 442 log.Print("Log:", p.Message) 443 } 444 default: 445 if c.app.verbose() { 446 log.Print(p.Message) 447 } 448 } 449 return nil 450 } 451 452 func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } 453 454 func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { 455 return nil 456 } 457 458 func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error { 459 return nil 460 } 461 462 func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { 463 return nil, nil 464 } 465 466 func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { 467 results := make([]interface{}, len(p.Items)) 468 for i, item := range p.Items { 469 if item.Section != "gopls" { 470 continue 471 } 472 env := map[string]interface{}{} 473 for _, value := range c.app.env { 474 l := strings.SplitN(value, "=", 2) 475 if len(l) != 2 { 476 continue 477 } 478 env[l[0]] = l[1] 479 } 480 m := map[string]interface{}{ 481 "env": env, 482 "analyses": map[string]bool{ 483 "fillreturns": true, 484 "nonewvars": true, 485 "noresultvalues": true, 486 "undeclaredname": true, 487 }, 488 } 489 if c.app.VeryVerbose { 490 m["verboseOutput"] = true 491 } 492 results[i] = m 493 } 494 return results, nil 495 } 496 497 func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { 498 return &protocol.ApplyWorkspaceEditResult{Applied: false, FailureReason: "not implemented"}, nil 499 } 500 501 func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error { 502 if p.URI == "gopls://diagnostics-done" { 503 close(c.diagnosticsDone) 504 } 505 // Don't worry about diagnostics without versions. 506 if p.Version == 0 { 507 return nil 508 } 509 510 c.filesMu.Lock() 511 defer c.filesMu.Unlock() 512 513 file := c.getFile(ctx, fileURI(p.URI)) 514 file.diagnostics = p.Diagnostics 515 return nil 516 } 517 518 func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error { 519 return nil 520 } 521 522 func (c *cmdClient) ShowDocument(context.Context, *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { 523 return nil, nil 524 } 525 526 func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { 527 return nil 528 } 529 530 func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { 531 file, found := c.files[uri] 532 if !found || file.err != nil { 533 file = &cmdFile{ 534 uri: uri, 535 } 536 c.files[uri] = file 537 } 538 if file.mapper == nil { 539 fname := uri.Filename() 540 content, err := ioutil.ReadFile(fname) 541 if err != nil { 542 file.err = errors.Errorf("getFile: %v: %v", uri, err) 543 return file 544 } 545 f := c.fset.AddFile(fname, -1, len(content)) 546 f.SetLinesForContent(content) 547 converter := span.NewContentConverter(fname, content) 548 file.mapper = &protocol.ColumnMapper{ 549 URI: uri, 550 Converter: converter, 551 Content: content, 552 } 553 } 554 return file 555 } 556 557 func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile { 558 c.Client.filesMu.Lock() 559 defer c.Client.filesMu.Unlock() 560 561 file := c.Client.getFile(ctx, uri) 562 // This should never happen. 563 if file == nil { 564 return &cmdFile{ 565 uri: uri, 566 err: fmt.Errorf("no file found for %s", uri), 567 } 568 } 569 if file.err != nil || file.added { 570 return file 571 } 572 file.added = true 573 p := &protocol.DidOpenTextDocumentParams{ 574 TextDocument: protocol.TextDocumentItem{ 575 URI: protocol.URIFromSpanURI(uri), 576 LanguageID: "go", 577 Version: 1, 578 Text: string(file.mapper.Content), 579 }, 580 } 581 if err := c.Server.DidOpen(ctx, p); err != nil { 582 file.err = errors.Errorf("%v: %v", uri, err) 583 } 584 return file 585 } 586 587 func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { 588 // use range to avoid limits on full 589 resp, err := c.Server.SemanticTokensRange(ctx, p) 590 if err != nil { 591 return nil, err 592 } 593 return resp, nil 594 } 595 596 func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error { 597 var untypedFiles []interface{} 598 for _, file := range files { 599 untypedFiles = append(untypedFiles, string(file)) 600 } 601 c.Client.diagnosticsMu.Lock() 602 defer c.Client.diagnosticsMu.Unlock() 603 604 c.Client.diagnosticsDone = make(chan struct{}) 605 _, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles}) 606 if err != nil { 607 close(c.Client.diagnosticsDone) 608 return err 609 } 610 611 <-c.Client.diagnosticsDone 612 return nil 613 } 614 615 func (c *connection) terminate(ctx context.Context) { 616 if strings.HasPrefix(c.Client.app.Remote, "internal@") { 617 // internal connections need to be left alive for the next test 618 return 619 } 620 //TODO: do we need to handle errors on these calls? 621 c.Shutdown(ctx) 622 //TODO: right now calling exit terminates the process, we should rethink that 623 //server.Exit(ctx) 624 } 625 626 // Implement io.Closer. 627 func (c *cmdClient) Close() error { 628 return nil 629 }