github.com/v2fly/tools@v0.100.0/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/v2fly/tools/internal/jsonrpc2" 23 "github.com/v2fly/tools/internal/lsp" 24 "github.com/v2fly/tools/internal/lsp/cache" 25 "github.com/v2fly/tools/internal/lsp/debug" 26 "github.com/v2fly/tools/internal/lsp/lsprpc" 27 "github.com/v2fly/tools/internal/lsp/protocol" 28 "github.com/v2fly/tools/internal/lsp/source" 29 "github.com/v2fly/tools/internal/span" 30 "github.com/v2fly/tools/internal/tool" 31 "github.com/v2fly/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 &apiJSON{}, 174 &licenses{app: app}, 175 } 176 } 177 178 func (app *Application) featureCommands() []tool.Application { 179 return []tool.Application{ 180 &callHierarchy{app: app}, 181 &check{app: app}, 182 &definition{app: app}, 183 &foldingRanges{app: app}, 184 &format{app: app}, 185 &highlight{app: app}, 186 &implementation{app: app}, 187 &imports{app: app}, 188 newRemote(app, ""), 189 newRemote(app, "inspect"), 190 &links{app: app}, 191 &prepareRename{app: app}, 192 &references{app: app}, 193 &rename{app: app}, 194 &semtok{app: app}, 195 &signature{app: app}, 196 &suggestedFix{app: app}, 197 &symbols{app: app}, 198 newWorkspace(app), 199 &workspaceSymbol{app: app}, 200 } 201 } 202 203 var ( 204 internalMu sync.Mutex 205 internalConnections = make(map[string]*connection) 206 ) 207 208 func (app *Application) connect(ctx context.Context) (*connection, error) { 209 switch { 210 case app.Remote == "": 211 connection := newConnection(app) 212 connection.Server = lsp.NewServer(cache.New(app.options).NewSession(ctx), connection.Client) 213 ctx = protocol.WithClient(ctx, connection.Client) 214 return connection, connection.initialize(ctx, app.options) 215 case strings.HasPrefix(app.Remote, "internal@"): 216 internalMu.Lock() 217 defer internalMu.Unlock() 218 opts := source.DefaultOptions().Clone() 219 if app.options != nil { 220 app.options(opts) 221 } 222 key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher) 223 if c := internalConnections[key]; c != nil { 224 return c, nil 225 } 226 remote := app.Remote[len("internal@"):] 227 ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server 228 connection, err := app.connectRemote(ctx, remote) 229 if err != nil { 230 return nil, err 231 } 232 internalConnections[key] = connection 233 return connection, nil 234 default: 235 return app.connectRemote(ctx, app.Remote) 236 } 237 } 238 239 // CloseTestConnections terminates shared connections used in command tests. It 240 // should only be called from tests. 241 func CloseTestConnections(ctx context.Context) { 242 for _, c := range internalConnections { 243 c.Shutdown(ctx) 244 c.Exit(ctx) 245 } 246 } 247 248 func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) { 249 connection := newConnection(app) 250 conn, err := lsprpc.ConnectToRemote(ctx, remote) 251 if err != nil { 252 return nil, err 253 } 254 stream := jsonrpc2.NewHeaderStream(conn) 255 cc := jsonrpc2.NewConn(stream) 256 connection.Server = protocol.ServerDispatcher(cc) 257 ctx = protocol.WithClient(ctx, connection.Client) 258 cc.Go(ctx, 259 protocol.Handlers( 260 protocol.ClientHandler(connection.Client, 261 jsonrpc2.MethodNotFound))) 262 return connection, connection.initialize(ctx, app.options) 263 } 264 265 var matcherString = map[source.SymbolMatcher]string{ 266 source.SymbolFuzzy: "fuzzy", 267 source.SymbolCaseSensitive: "caseSensitive", 268 source.SymbolCaseInsensitive: "caseInsensitive", 269 } 270 271 func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error { 272 params := &protocol.ParamInitialize{} 273 params.RootURI = protocol.URIFromPath(c.Client.app.wd) 274 params.Capabilities.Workspace.Configuration = true 275 276 // Make sure to respect configured options when sending initialize request. 277 opts := source.DefaultOptions().Clone() 278 if options != nil { 279 options(opts) 280 } 281 // If you add an additional option here, you must update the map key in connect. 282 params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{ 283 ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, 284 } 285 params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport 286 params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{} 287 params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"} 288 params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true 289 params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true 290 params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes() 291 params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers() 292 params.InitializationOptions = map[string]interface{}{ 293 "symbolMatcher": matcherString[opts.SymbolMatcher], 294 } 295 if _, err := c.Server.Initialize(ctx, params); err != nil { 296 return err 297 } 298 if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 299 return err 300 } 301 return nil 302 } 303 304 type connection struct { 305 protocol.Server 306 Client *cmdClient 307 } 308 309 type cmdClient struct { 310 protocol.Server 311 app *Application 312 fset *token.FileSet 313 314 diagnosticsMu sync.Mutex 315 diagnosticsDone chan struct{} 316 317 filesMu sync.Mutex 318 files map[span.URI]*cmdFile 319 } 320 321 type cmdFile struct { 322 uri span.URI 323 mapper *protocol.ColumnMapper 324 err error 325 added bool 326 diagnostics []protocol.Diagnostic 327 } 328 329 func newConnection(app *Application) *connection { 330 return &connection{ 331 Client: &cmdClient{ 332 app: app, 333 fset: token.NewFileSet(), 334 files: make(map[span.URI]*cmdFile), 335 }, 336 } 337 } 338 339 // fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file. 340 func fileURI(uri protocol.DocumentURI) span.URI { 341 sURI := uri.SpanURI() 342 if !sURI.IsFile() { 343 panic(fmt.Sprintf("%q is not a file URI", uri)) 344 } 345 return sURI 346 } 347 348 func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil } 349 350 func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { 351 return nil, nil 352 } 353 354 func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { 355 switch p.Type { 356 case protocol.Error: 357 log.Print("Error:", p.Message) 358 case protocol.Warning: 359 log.Print("Warning:", p.Message) 360 case protocol.Info: 361 if c.app.verbose() { 362 log.Print("Info:", p.Message) 363 } 364 case protocol.Log: 365 if c.app.verbose() { 366 log.Print("Log:", p.Message) 367 } 368 default: 369 if c.app.verbose() { 370 log.Print(p.Message) 371 } 372 } 373 return nil 374 } 375 376 func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } 377 378 func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { 379 return nil 380 } 381 382 func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error { 383 return nil 384 } 385 386 func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { 387 return nil, nil 388 } 389 390 func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { 391 results := make([]interface{}, len(p.Items)) 392 for i, item := range p.Items { 393 if item.Section != "gopls" { 394 continue 395 } 396 env := map[string]interface{}{} 397 for _, value := range c.app.env { 398 l := strings.SplitN(value, "=", 2) 399 if len(l) != 2 { 400 continue 401 } 402 env[l[0]] = l[1] 403 } 404 m := map[string]interface{}{ 405 "env": env, 406 "analyses": map[string]bool{ 407 "fillreturns": true, 408 "nonewvars": true, 409 "noresultvalues": true, 410 "undeclaredname": true, 411 }, 412 } 413 if c.app.VeryVerbose { 414 m["verboseOutput"] = true 415 } 416 results[i] = m 417 } 418 return results, nil 419 } 420 421 func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) { 422 return &protocol.ApplyWorkspaceEditResponse{Applied: false, FailureReason: "not implemented"}, nil 423 } 424 425 func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error { 426 if p.URI == "gopls://diagnostics-done" { 427 close(c.diagnosticsDone) 428 } 429 // Don't worry about diagnostics without versions. 430 if p.Version == 0 { 431 return nil 432 } 433 434 c.filesMu.Lock() 435 defer c.filesMu.Unlock() 436 437 file := c.getFile(ctx, fileURI(p.URI)) 438 file.diagnostics = p.Diagnostics 439 return nil 440 } 441 442 func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error { 443 return nil 444 } 445 446 func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { 447 return nil 448 } 449 450 func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { 451 file, found := c.files[uri] 452 if !found || file.err != nil { 453 file = &cmdFile{ 454 uri: uri, 455 } 456 c.files[uri] = file 457 } 458 if file.mapper == nil { 459 fname := uri.Filename() 460 content, err := ioutil.ReadFile(fname) 461 if err != nil { 462 file.err = errors.Errorf("getFile: %v: %v", uri, err) 463 return file 464 } 465 f := c.fset.AddFile(fname, -1, len(content)) 466 f.SetLinesForContent(content) 467 converter := span.NewContentConverter(fname, content) 468 file.mapper = &protocol.ColumnMapper{ 469 URI: uri, 470 Converter: converter, 471 Content: content, 472 } 473 } 474 return file 475 } 476 477 func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile { 478 c.Client.filesMu.Lock() 479 defer c.Client.filesMu.Unlock() 480 481 file := c.Client.getFile(ctx, uri) 482 // This should never happen. 483 if file == nil { 484 return &cmdFile{ 485 uri: uri, 486 err: fmt.Errorf("no file found for %s", uri), 487 } 488 } 489 if file.err != nil || file.added { 490 return file 491 } 492 file.added = true 493 p := &protocol.DidOpenTextDocumentParams{ 494 TextDocument: protocol.TextDocumentItem{ 495 URI: protocol.URIFromSpanURI(uri), 496 LanguageID: source.DetectLanguage("", file.uri.Filename()).String(), 497 Version: 1, 498 Text: string(file.mapper.Content), 499 }, 500 } 501 if err := c.Server.DidOpen(ctx, p); err != nil { 502 file.err = errors.Errorf("%v: %v", uri, err) 503 } 504 return file 505 } 506 507 func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { 508 // use range to avoid limits on full 509 resp, err := c.Server.SemanticTokensRange(ctx, p) 510 if err != nil { 511 return nil, err 512 } 513 return resp, nil 514 } 515 516 func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error { 517 var untypedFiles []interface{} 518 for _, file := range files { 519 untypedFiles = append(untypedFiles, string(file)) 520 } 521 c.Client.diagnosticsMu.Lock() 522 defer c.Client.diagnosticsMu.Unlock() 523 524 c.Client.diagnosticsDone = make(chan struct{}) 525 _, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles}) 526 <-c.Client.diagnosticsDone 527 return err 528 } 529 530 func (c *connection) terminate(ctx context.Context) { 531 if strings.HasPrefix(c.Client.app.Remote, "internal@") { 532 // internal connections need to be left alive for the next test 533 return 534 } 535 //TODO: do we need to handle errors on these calls? 536 c.Shutdown(ctx) 537 //TODO: right now calling exit terminates the process, we should rethink that 538 //server.Exit(ctx) 539 } 540 541 // Implement io.Closer. 542 func (c *cmdClient) Close() error { 543 return nil 544 }