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