| // 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/ast" |
| "go/parser" |
| "go/token" |
| "io/ioutil" |
| "log" |
| "net" |
| "os" |
| "strings" |
| "sync" |
| |
| "golang.org/x/tools/go/packages" |
| "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/protocol" |
| "golang.org/x/tools/internal/lsp/source" |
| "golang.org/x/tools/internal/span" |
| "golang.org/x/tools/internal/tool" |
| ) |
| |
| // 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 |
| |
| // An initial, common go/packages configuration |
| Config packages.Config |
| |
| // The base cache to use for sessions from this application. |
| Cache source.Cache |
| |
| // Support for remote lsp server |
| Remote string `flag:"remote" help:"*EXPERIMENTAL* - forward all commands to a remote lsp"` |
| |
| // Enable verbose logging |
| Verbose bool `flag:"v" help:"Verbose output"` |
| } |
| |
| // Returns a new Application ready to run. |
| func New(config *packages.Config) *Application { |
| app := &Application{ |
| Cache: cache.New(), |
| } |
| if config != nil { |
| app.Config = *config |
| } |
| return app |
| } |
| |
| // Name implements tool.Application returning the binary name. |
| func (app *Application) Name() string { return "gopls" } |
| |
| // 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(), ` |
| Available commands are: |
| `) |
| for _, c := range app.commands() { |
| 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 { |
| app.Serve.app = app |
| if len(args) == 0 { |
| tool.Main(ctx, &app.Serve, args) |
| return nil |
| } |
| if app.Config.Dir == "" { |
| if wd, err := os.Getwd(); err == nil { |
| app.Config.Dir = wd |
| } |
| } |
| app.Config.Mode = packages.LoadSyntax |
| app.Config.Tests = true |
| if app.Config.Fset == nil { |
| app.Config.Fset = token.NewFileSet() |
| } |
| app.Config.Context = ctx |
| app.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { |
| return parser.ParseFile(fset, filename, src, parser.AllErrors|parser.ParseComments) |
| } |
| command, args := args[0], args[1:] |
| for _, c := range app.commands() { |
| if c.Name() == command { |
| tool.Main(ctx, c, args) |
| return nil |
| } |
| } |
| 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 { |
| return []tool.Application{ |
| &app.Serve, |
| &bug{}, |
| &check{app: app}, |
| &format{app: app}, |
| &query{app: app}, |
| &version{app: app}, |
| } |
| } |
| |
| var ( |
| internalMu sync.Mutex |
| internalConnections = make(map[string]*connection) |
| ) |
| |
| func (app *Application) connect(ctx context.Context) (*connection, error) { |
| switch app.Remote { |
| case "": |
| connection := newConnection(app) |
| connection.Server = lsp.NewClientServer(app.Cache, connection.Client) |
| return connection, connection.initialize(ctx) |
| case "internal": |
| internalMu.Lock() |
| defer internalMu.Unlock() |
| if c := internalConnections[app.Config.Dir]; c != nil { |
| return c, nil |
| } |
| connection := newConnection(app) |
| ctx := context.Background() //TODO:a way of shutting down the internal server |
| cr, sw, _ := os.Pipe() |
| sr, cw, _ := os.Pipe() |
| var jc *jsonrpc2.Conn |
| jc, connection.Server, _ = protocol.NewClient(jsonrpc2.NewHeaderStream(cr, cw), connection.Client) |
| go jc.Run(ctx) |
| go lsp.NewServer(app.Cache, jsonrpc2.NewHeaderStream(sr, sw)).Run(ctx) |
| if err := connection.initialize(ctx); err != nil { |
| return nil, err |
| } |
| internalConnections[app.Config.Dir] = connection |
| return connection, nil |
| default: |
| connection := newConnection(app) |
| conn, err := net.Dial("tcp", app.Remote) |
| if err != nil { |
| return nil, err |
| } |
| stream := jsonrpc2.NewHeaderStream(conn, conn) |
| var jc *jsonrpc2.Conn |
| jc, connection.Server, _ = protocol.NewClient(stream, connection.Client) |
| go jc.Run(ctx) |
| return connection, connection.initialize(ctx) |
| } |
| } |
| |
| func (c *connection) initialize(ctx context.Context) error { |
| params := &protocol.InitializeParams{} |
| params.RootURI = string(span.FileURI(c.Client.app.Config.Dir)) |
| params.Capabilities.Workspace.Configuration = true |
| params.Capabilities.TextDocument.Hover.ContentFormat = []protocol.MarkupKind{protocol.PlainText} |
| 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 |
| |
| filesMu sync.Mutex |
| files map[span.URI]*cmdFile |
| } |
| |
| type cmdFile struct { |
| uri span.URI |
| mapper *protocol.ColumnMapper |
| err error |
| added bool |
| hasDiagnostics chan struct{} |
| diagnosticsMu sync.Mutex |
| diagnostics []protocol.Diagnostic |
| } |
| |
| func newConnection(app *Application) *connection { |
| return &connection{ |
| Client: &cmdClient{ |
| app: app, |
| fset: token.NewFileSet(), |
| files: make(map[span.URI]*cmdFile), |
| }, |
| } |
| } |
| |
| 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.ConfigurationParams) ([]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.Config.Env { |
| l := strings.SplitN(value, "=", 2) |
| if len(l) != 2 { |
| continue |
| } |
| env[l[0]] = l[1] |
| } |
| results[i] = map[string]interface{}{ |
| "env": env, |
| "noDocsOnHover": true, |
| } |
| } |
| 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 { |
| c.filesMu.Lock() |
| defer c.filesMu.Unlock() |
| uri := span.URI(p.URI) |
| file := c.getFile(ctx, uri) |
| file.diagnosticsMu.Lock() |
| defer file.diagnosticsMu.Unlock() |
| hadDiagnostics := file.diagnostics != nil |
| file.diagnostics = p.Diagnostics |
| if file.diagnostics == nil { |
| file.diagnostics = []protocol.Diagnostic{} |
| } |
| if !hadDiagnostics { |
| close(file.hasDiagnostics) |
| } |
| return nil |
| } |
| |
| func (c *cmdClient) getFile(ctx context.Context, uri span.URI) *cmdFile { |
| file, found := c.files[uri] |
| if !found { |
| file = &cmdFile{ |
| uri: uri, |
| hasDiagnostics: make(chan struct{}), |
| } |
| c.files[uri] = file |
| } |
| if file.mapper == nil { |
| fname, err := uri.Filename() |
| if err != nil { |
| file.err = fmt.Errorf("%v: %v", uri, err) |
| return file |
| } |
| content, err := ioutil.ReadFile(fname) |
| if err != nil { |
| file.err = fmt.Errorf("%v: %v", uri, err) |
| return file |
| } |
| f := c.fset.AddFile(fname, -1, len(content)) |
| f.SetLinesForContent(content) |
| file.mapper = protocol.NewColumnMapper(uri, fname, c.fset, f, 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) |
| if !file.added { |
| file.added = true |
| p := &protocol.DidOpenTextDocumentParams{} |
| p.TextDocument.URI = string(uri) |
| p.TextDocument.Text = string(file.mapper.Content) |
| if err := c.Server.DidOpen(ctx, p); err != nil { |
| file.err = fmt.Errorf("%v: %v", uri, err) |
| } |
| } |
| return file |
| } |
| |
| func (c *connection) terminate(ctx context.Context) { |
| if 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) |
| } |