--- /dev/null
+// Copyright 2018 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package cmd handles the gopls command line.
+// It contains a handler for each of the modes, along with all the flag handling
+// and the command line output format.
+package cmd
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "go/token"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/tools/internal/jsonrpc2"
+ "golang.org/x/tools/internal/lsp"
+ "golang.org/x/tools/internal/lsp/cache"
+ "golang.org/x/tools/internal/lsp/debug"
+ "golang.org/x/tools/internal/lsp/lsprpc"
+ "golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/lsp/source"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/tool"
+ "golang.org/x/tools/internal/xcontext"
+ errors "golang.org/x/xerrors"
+)
+
+// Application is the main application as passed to tool.Main
+// It handles the main command line parsing and dispatch to the sub commands.
+type Application struct {
+ // Core application flags
+
+ // Embed the basic profiling flags supported by the tool package
+ tool.Profile
+
+ // We include the server configuration directly for now, so the flags work
+ // even without the verb.
+ // TODO: Remove this when we stop allowing the serve verb by default.
+ Serve Serve
+
+ // the options configuring function to invoke when building a server
+ options func(*source.Options)
+
+ // The name of the binary, used in help and telemetry.
+ name string
+
+ // The working directory to run commands in.
+ wd string
+
+ // The environment variables to use.
+ env []string
+
+ // Support for remote LSP server.
+ 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."`
+
+ // Verbose enables verbose logging.
+ Verbose bool `flag:"v" help:"verbose output"`
+
+ // VeryVerbose enables a higher level of verbosity in logging output.
+ VeryVerbose bool `flag:"vv" help:"very verbose output"`
+
+ // Control ocagent export of telemetry
+ OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"`
+
+ // PrepareOptions is called to update the options when a new view is built.
+ // It is primarily to allow the behavior of gopls to be modified by hooks.
+ PrepareOptions func(*source.Options)
+}
+
+func (app *Application) verbose() bool {
+ return app.Verbose || app.VeryVerbose
+}
+
+// New returns a new Application ready to run.
+func New(name, wd string, env []string, options func(*source.Options)) *Application {
+ if wd == "" {
+ wd, _ = os.Getwd()
+ }
+ app := &Application{
+ options: options,
+ name: name,
+ wd: wd,
+ env: env,
+ OCAgent: "off", //TODO: Remove this line to default the exporter to on
+
+ Serve: Serve{
+ RemoteListenTimeout: 1 * time.Minute,
+ },
+ }
+ return app
+}
+
+// Name implements tool.Application returning the binary name.
+func (app *Application) Name() string { return app.name }
+
+// Usage implements tool.Application returning empty extra argument usage.
+func (app *Application) Usage() string { return "<command> [command-flags] [command-args]" }
+
+// ShortHelp implements tool.Application returning the main binary help.
+func (app *Application) ShortHelp() string {
+ return "The Go Language source tools."
+}
+
+// DetailedHelp implements tool.Application returning the main binary help.
+// This includes the short help for all the sub commands.
+func (app *Application) DetailedHelp(f *flag.FlagSet) {
+ fmt.Fprint(f.Output(), `
+gopls is a Go language server. It is typically used with an editor to provide
+language features. When no command is specified, gopls will default to the 'serve'
+command. The language features can also be accessed via the gopls command-line interface.
+
+Available commands are:
+`)
+ fmt.Fprint(f.Output(), `
+main:
+`)
+ for _, c := range app.mainCommands() {
+ fmt.Fprintf(f.Output(), " %s : %v\n", c.Name(), c.ShortHelp())
+ }
+ fmt.Fprint(f.Output(), `
+features:
+`)
+ for _, c := range app.featureCommands() {
+ fmt.Fprintf(f.Output(), " %s : %v\n", c.Name(), c.ShortHelp())
+ }
+ fmt.Fprint(f.Output(), `
+gopls flags are:
+`)
+ f.PrintDefaults()
+}
+
+// Run takes the args after top level flag processing, and invokes the correct
+// sub command as specified by the first argument.
+// If no arguments are passed it will invoke the server sub command, as a
+// temporary measure for compatibility.
+func (app *Application) Run(ctx context.Context, args ...string) error {
+ ctx = debug.WithInstance(ctx, app.wd, app.OCAgent)
+ app.Serve.app = app
+ if len(args) == 0 {
+ return tool.Run(ctx, &app.Serve, args)
+ }
+ command, args := args[0], args[1:]
+ for _, c := range app.commands() {
+ if c.Name() == command {
+ return tool.Run(ctx, c, args)
+ }
+ }
+ return tool.CommandLineErrorf("Unknown command %v", command)
+}
+
+// commands returns the set of commands supported by the gopls tool on the
+// command line.
+// The command is specified by the first non flag argument.
+func (app *Application) commands() []tool.Application {
+ var commands []tool.Application
+ commands = append(commands, app.mainCommands()...)
+ commands = append(commands, app.featureCommands()...)
+ return commands
+}
+
+func (app *Application) mainCommands() []tool.Application {
+ return []tool.Application{
+ &app.Serve,
+ &version{app: app},
+ &bug{},
+ &apiJSON{},
+ }
+}
+
+func (app *Application) featureCommands() []tool.Application {
+ return []tool.Application{
+ &callHierarchy{app: app},
+ &check{app: app},
+ &definition{app: app},
+ &foldingRanges{app: app},
+ &format{app: app},
+ &highlight{app: app},
+ &implementation{app: app},
+ &imports{app: app},
+ &inspect{app: app},
+ &links{app: app},
+ &prepareRename{app: app},
+ &references{app: app},
+ &rename{app: app},
+ &semtok{app: app},
+ &signature{app: app},
+ &suggestedFix{app: app},
+ &symbols{app: app},
+ &workspace{app: app},
+ &workspaceSymbol{app: app},
+ }
+}
+
+var (
+ internalMu sync.Mutex
+ internalConnections = make(map[string]*connection)
+)
+
+func (app *Application) connect(ctx context.Context) (*connection, error) {
+ switch {
+ case app.Remote == "":
+ connection := newConnection(app)
+ connection.Server = lsp.NewServer(cache.New(ctx, app.options).NewSession(ctx), connection.Client)
+ ctx = protocol.WithClient(ctx, connection.Client)
+ return connection, connection.initialize(ctx, app.options)
+ case strings.HasPrefix(app.Remote, "internal@"):
+ internalMu.Lock()
+ defer internalMu.Unlock()
+ opts := source.DefaultOptions().Clone()
+ if app.options != nil {
+ app.options(opts)
+ }
+ key := fmt.Sprintf("%s %v", app.wd, opts)
+ if c := internalConnections[key]; c != nil {
+ return c, nil
+ }
+ remote := app.Remote[len("internal@"):]
+ ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server
+ connection, err := app.connectRemote(ctx, remote)
+ if err != nil {
+ return nil, err
+ }
+ internalConnections[key] = connection
+ return connection, nil
+ default:
+ return app.connectRemote(ctx, app.Remote)
+ }
+}
+
+// CloseTestConnections terminates shared connections used in command tests. It
+// should only be called from tests.
+func CloseTestConnections(ctx context.Context) {
+ for _, c := range internalConnections {
+ c.Shutdown(ctx)
+ c.Exit(ctx)
+ }
+}
+
+func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) {
+ connection := newConnection(app)
+ network, addr := parseAddr(remote)
+ conn, err := lsprpc.ConnectToRemote(ctx, network, addr)
+ if err != nil {
+ return nil, err
+ }
+ stream := jsonrpc2.NewHeaderStream(conn)
+ cc := jsonrpc2.NewConn(stream)
+ connection.Server = protocol.ServerDispatcher(cc)
+ ctx = protocol.WithClient(ctx, connection.Client)
+ cc.Go(ctx,
+ protocol.Handlers(
+ protocol.ClientHandler(connection.Client,
+ jsonrpc2.MethodNotFound)))
+ return connection, connection.initialize(ctx, app.options)
+}
+
+var matcherString = map[source.SymbolMatcher]string{
+ source.SymbolFuzzy: "fuzzy",
+ source.SymbolCaseSensitive: "caseSensitive",
+ source.SymbolCaseInsensitive: "caseInsensitive",
+}
+
+func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error {
+ params := &protocol.ParamInitialize{}
+ params.RootURI = protocol.URIFromPath(c.Client.app.wd)
+ params.Capabilities.Workspace.Configuration = true
+
+ // Make sure to respect configured options when sending initialize request.
+ opts := source.DefaultOptions().Clone()
+ if options != nil {
+ options(opts)
+ }
+ params.Capabilities.TextDocument.Hover = protocol.HoverClientCapabilities{
+ ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat},
+ }
+ params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport
+ params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{}
+ params.Capabilities.TextDocument.SemanticTokens.Formats = []string{"relative"}
+ params.Capabilities.TextDocument.SemanticTokens.Requests.Range = true
+ params.Capabilities.TextDocument.SemanticTokens.Requests.Full = true
+ params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes()
+ params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers()
+ params.InitializationOptions = map[string]interface{}{
+ "symbolMatcher": matcherString[opts.SymbolMatcher],
+ }
+ if _, err := c.Server.Initialize(ctx, params); err != nil {
+ return err
+ }
+ if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil {
+ return err
+ }
+ return nil
+}
+
+type connection struct {
+ protocol.Server
+ Client *cmdClient
+}
+
+type cmdClient struct {
+ protocol.Server
+ app *Application
+ fset *token.FileSet
+
+ diagnosticsMu sync.Mutex
+ diagnosticsDone chan struct{}
+
+ filesMu sync.Mutex
+ files map[span.URI]*cmdFile
+}
+
+type cmdFile struct {
+ uri span.URI
+ mapper *protocol.ColumnMapper
+ err error
+ added bool
+ diagnostics []protocol.Diagnostic
+}
+
+func newConnection(app *Application) *connection {
+ return &connection{
+ Client: &cmdClient{
+ app: app,
+ fset: token.NewFileSet(),
+ files: make(map[span.URI]*cmdFile),
+ },
+ }
+}
+
+// fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file.
+func fileURI(uri protocol.DocumentURI) span.URI {
+ sURI := uri.SpanURI()
+ if !sURI.IsFile() {
+ panic(fmt.Sprintf("%q is not a file URI", uri))
+ }
+ return sURI
+}
+
+func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil }
+
+func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) {
+ return nil, nil
+}
+
+func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error {
+ switch p.Type {
+ case protocol.Error:
+ log.Print("Error:", p.Message)
+ case protocol.Warning:
+ log.Print("Warning:", p.Message)
+ case protocol.Info:
+ if c.app.verbose() {
+ log.Print("Info:", p.Message)
+ }
+ case protocol.Log:
+ if c.app.verbose() {
+ log.Print("Log:", p.Message)
+ }
+ default:
+ if c.app.verbose() {
+ log.Print(p.Message)
+ }
+ }
+ return nil
+}
+
+func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil }
+
+func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error {
+ return nil
+}
+
+func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error {
+ return nil
+}
+
+func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) {
+ return nil, nil
+}
+
+func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) {
+ results := make([]interface{}, len(p.Items))
+ for i, item := range p.Items {
+ if item.Section != "gopls" {
+ continue
+ }
+ env := map[string]interface{}{}
+ for _, value := range c.app.env {
+ l := strings.SplitN(value, "=", 2)
+ if len(l) != 2 {
+ continue
+ }
+ env[l[0]] = l[1]
+ }
+ m := map[string]interface{}{
+ "env": env,
+ "analyses": map[string]bool{
+ "fillreturns": true,
+ "nonewvars": true,
+ "noresultvalues": true,
+ "undeclaredname": true,
+ },
+ }
+ if c.app.VeryVerbose {
+ m["verboseOutput"] = true
+ }
+ results[i] = m
+ }
+ return results, nil
+}
+
+func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResponse, error) {
+ return &protocol.ApplyWorkspaceEditResponse{Applied: false, FailureReason: "not implemented"}, nil
+}
+
+func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error {
+ if p.URI == "gopls://diagnostics-done" {
+ close(c.diagnosticsDone)
+ }
+ // Don't worry about diagnostics without versions.
+ if p.Version == 0 {
+ return nil
+ }
+
+ c.filesMu.Lock()
+ defer c.filesMu.Unlock()
+
+ file := c.getFile(ctx, fileURI(p.URI))
+ file.diagnostics = p.Diagnostics
+ return nil
+}
+
+func (c *cmdClient) Progress(context.Context, *protocol.ProgressParams) error {
+ return nil
+}
+
+func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error {
+ return nil
+}
+
+func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile {
+ file, found := c.files[uri]
+ if !found || file.err != nil {
+ file = &cmdFile{
+ uri: uri,
+ }
+ c.files[uri] = file
+ }
+ if file.mapper == nil {
+ fname := uri.Filename()
+ content, err := ioutil.ReadFile(fname)
+ if err != nil {
+ file.err = errors.Errorf("getFile: %v: %v", uri, err)
+ return file
+ }
+ f := c.fset.AddFile(fname, -1, len(content))
+ f.SetLinesForContent(content)
+ converter := span.NewContentConverter(fname, content)
+ file.mapper = &protocol.ColumnMapper{
+ URI: uri,
+ Converter: converter,
+ Content: content,
+ }
+ }
+ return file
+}
+
+func (c *connection) AddFile(ctx context.Context, uri span.URI) *cmdFile {
+ c.Client.filesMu.Lock()
+ defer c.Client.filesMu.Unlock()
+
+ file := c.Client.getFile(ctx, uri)
+ // This should never happen.
+ if file == nil {
+ return &cmdFile{
+ uri: uri,
+ err: fmt.Errorf("no file found for %s", uri),
+ }
+ }
+ if file.err != nil || file.added {
+ return file
+ }
+ file.added = true
+ p := &protocol.DidOpenTextDocumentParams{
+ TextDocument: protocol.TextDocumentItem{
+ URI: protocol.URIFromSpanURI(uri),
+ LanguageID: source.DetectLanguage("", file.uri.Filename()).String(),
+ Version: 1,
+ Text: string(file.mapper.Content),
+ },
+ }
+ if err := c.Server.DidOpen(ctx, p); err != nil {
+ file.err = errors.Errorf("%v: %v", uri, err)
+ }
+ return file
+}
+
+func (c *connection) semanticTokens(ctx context.Context, file span.URI) (*protocol.SemanticTokens, error) {
+ p := &protocol.SemanticTokensParams{
+ TextDocument: protocol.TextDocumentIdentifier{
+ URI: protocol.URIFromSpanURI(file),
+ },
+ }
+ resp, err := c.Server.SemanticTokensFull(ctx, p)
+ if err != nil {
+ return nil, err
+ }
+ return resp, nil
+}
+
+func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error {
+ var untypedFiles []interface{}
+ for _, file := range files {
+ untypedFiles = append(untypedFiles, string(file))
+ }
+ c.Client.diagnosticsMu.Lock()
+ defer c.Client.diagnosticsMu.Unlock()
+
+ c.Client.diagnosticsDone = make(chan struct{})
+ _, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles})
+ <-c.Client.diagnosticsDone
+ return err
+}
+
+func (c *connection) terminate(ctx context.Context) {
+ if strings.HasPrefix(c.Client.app.Remote, "internal@") {
+ // internal connections need to be left alive for the next test
+ return
+ }
+ //TODO: do we need to handle errors on these calls?
+ c.Shutdown(ctx)
+ //TODO: right now calling exit terminates the process, we should rethink that
+ //server.Exit(ctx)
+}
+
+// Implement io.Closer.
+func (c *cmdClient) Close() error {
+ return nil
+}