diff --git a/cmd/golsp/main.go b/cmd/golsp/main.go index b129b6a858..365ef14bba 100644 --- a/cmd/golsp/main.go +++ b/cmd/golsp/main.go @@ -10,148 +10,12 @@ package main // import "golang.org/x/tools/cmd/golsp" import ( "context" - "encoding/json" - "flag" - "fmt" - "io" - "log" "os" - "path/filepath" - "runtime" - "runtime/pprof" - "runtime/trace" - "strings" - "time" - "golang.org/x/tools/internal/jsonrpc2" - "golang.org/x/tools/internal/lsp" -) - -var ( - cpuprofile = flag.String("cpuprofile", "", "write CPU profile to this file") - memprofile = flag.String("memprofile", "", "write memory profile to this file") - traceFlag = flag.String("trace", "", "write trace log to this file") - logfile = flag.String("logfile", "", "filename to log to. if value is \"auto\", then logging to a default output file is enabled") - - // Flags for compatitibility with VSCode. - mode = flag.String("mode", "", "no effect") + "golang.org/x/tools/internal/lsp/cmd" + "golang.org/x/tools/internal/tool" ) func main() { - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "usage: golsp [flags]\n") - flag.PrintDefaults() - } - flag.Parse() - if flag.NArg() > 0 { - flag.Usage() - os.Exit(2) - } - - if *cpuprofile != "" { - f, err := os.Create(*cpuprofile) - if err != nil { - log.Fatal(err) - } - if err := pprof.StartCPUProfile(f); err != nil { - log.Fatal(err) - } - // NB: profile won't be written in case of error. - defer pprof.StopCPUProfile() - } - - if *traceFlag != "" { - f, err := os.Create(*traceFlag) - if err != nil { - log.Fatal(err) - } - if err := trace.Start(f); err != nil { - log.Fatal(err) - } - // NB: trace log won't be written in case of error. - defer func() { - trace.Stop() - log.Printf("To view the trace, run:\n$ go tool trace view %s", *traceFlag) - }() - } - - if *memprofile != "" { - f, err := os.Create(*memprofile) - if err != nil { - log.Fatal(err) - } - // NB: memprofile won't be written in case of error. - defer func() { - runtime.GC() // get up-to-date statistics - if err := pprof.WriteHeapProfile(f); err != nil { - log.Fatalf("Writing memory profile: %v", err) - } - f.Close() - }() - } - - out := os.Stderr - if *logfile != "" { - filename := *logfile - if filename == "auto" { - filename = filepath.Join(os.TempDir(), fmt.Sprintf("golsp-%d.log", os.Getpid())) - } - f, err := os.Create(filename) - if err != nil { - log.Fatalf("Unable to create log file: %v", err) - } - defer f.Close() - log.SetOutput(io.MultiWriter(os.Stderr, f)) - out = f - } - if err := lsp.RunServer( - context.Background(), - jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout), - func(direction jsonrpc2.Direction, id *jsonrpc2.ID, elapsed time.Duration, method string, payload *json.RawMessage, err *jsonrpc2.Error) { - - const eol = "\r\n\r\n\r\n" - if err != nil { - fmt.Fprintf(out, "[Error - %v] %s %s%s %v%s", time.Now().Format("3:04:05 PM"), - direction, method, id, err, eol) - return - } - outx := new(strings.Builder) - fmt.Fprintf(outx, "[Trace - %v] ", time.Now().Format("3:04:05 PM")) - switch direction { - case jsonrpc2.Send: - fmt.Fprint(outx, "Received ") - case jsonrpc2.Receive: - fmt.Fprint(outx, "Sending ") - } - switch { - case id == nil: - fmt.Fprint(outx, "notification ") - case elapsed >= 0: - fmt.Fprint(outx, "response ") - default: - fmt.Fprint(outx, "request ") - } - fmt.Fprintf(outx, "'%s", method) - switch { - case id == nil: - // do nothing - case id.Name != "": - fmt.Fprintf(outx, " - (%s)", id.Name) - default: - fmt.Fprintf(outx, " - (%d)", id.Number) - } - fmt.Fprint(outx, "'") - if elapsed >= 0 { - fmt.Fprintf(outx, " in %vms", elapsed.Nanoseconds()/1000) - } - params := string(*payload) - if params == "null" { - params = "{}" - } - fmt.Fprintf(outx, ".\r\nParams: %s%s", params, eol) - fmt.Fprintf(out, "%s", outx.String()) - }, - ); err != nil { - log.Fatal(err) - } + tool.Main(context.Background(), &cmd.Application{}, os.Args[1:]) } diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go new file mode 100644 index 0000000000..a959583003 --- /dev/null +++ b/internal/lsp/cmd/cmd.go @@ -0,0 +1,81 @@ +// 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 golsp 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" + "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 { + // Embed the basic profiling flags supported by the tool package + tool.Profile + + // we also include the server directly for now, so the flags work even without + // the verb. We should remove this when we stop allowing the server verb by + // default + Server server +} + +// Name implements tool.Application returning the binary name. +func (app *Application) Name() string { return "golsp" } + +// Usage implements tool.Application returning empty extra argument usage. +func (app *Application) Usage() string { return " [mode-flags] [mode-args]" } + +// ShortHelp implements tool.Application returning the main binary help. +func (app *Application) ShortHelp() string { + return "The Go Language Smartness Provider." +} + +// 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 modes are: +`) + for _, c := range app.modes() { + fmt.Fprintf(f.Output(), " %s : %v\n", c.Name(), c.ShortHelp()) + } + fmt.Fprint(f.Output(), ` +golsp 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 compatability. +func (app *Application) Run(ctx context.Context, args ...string) error { + if len(args) == 0 { + tool.Main(ctx, &app.Server, args) + return nil + } + mode, args := args[0], args[1:] + for _, m := range app.modes() { + if m.Name() == mode { + tool.Main(ctx, m, args) + return nil + } + } + return tool.CommandLineErrorf("Unknown mode %v", mode) +} + +// modes returns the set of command modes supported by the golsp tool on the +// command line. +// The mode is specified by the first non flag argument. +func (app *Application) modes() []tool.Application { + return []tool.Application{ + &app.Server, + } +} diff --git a/internal/lsp/cmd/server.go b/internal/lsp/cmd/server.go new file mode 100644 index 0000000000..81807abd7e --- /dev/null +++ b/internal/lsp/cmd/server.go @@ -0,0 +1,110 @@ +// 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 + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "log" + "os" + "path/filepath" + "strings" + "time" + + "golang.org/x/tools/internal/jsonrpc2" + "golang.org/x/tools/internal/lsp" + "golang.org/x/tools/internal/tool" +) + +// server is a struct that exposes the configurable parts of the LSP server as +// flags, in the right form for tool.Main to consume. +type server struct { + Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"` + Mode string `flag:"mode" help:"no effect"` +} + +func (s *server) Name() string { return "server" } +func (s *server) Usage() string { return "" } +func (s *server) ShortHelp() string { + return "run a server for Go code using the Language Server Protocol" +} +func (s *server) DetailedHelp(f *flag.FlagSet) { + fmt.Fprint(f.Output(), ` +The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as +a child of an editor process. +`) +} + +// Run configures a server based on the flags, and then runs it. +// It blocks until the server shuts down. +func (s *server) Run(ctx context.Context, args ...string) error { + if len(args) > 0 { + return tool.CommandLineErrorf("server does not take arguments, got %v", args) + } + out := os.Stderr + if s.Logfile != "" { + filename := s.Logfile + if filename == "auto" { + filename = filepath.Join(os.TempDir(), fmt.Sprintf("golsp-%d.log", os.Getpid())) + } + f, err := os.Create(filename) + if err != nil { + return fmt.Errorf("Unable to create log file: %v", err) + } + defer f.Close() + log.SetOutput(io.MultiWriter(os.Stderr, f)) + out = f + } + return lsp.RunServer( + ctx, + jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout), + func(direction jsonrpc2.Direction, id *jsonrpc2.ID, elapsed time.Duration, method string, payload *json.RawMessage, err *jsonrpc2.Error) { + const eol = "\r\n\r\n\r\n" + if err != nil { + fmt.Fprintf(out, "[Error - %v] %s %s%s %v%s", time.Now().Format("3:04:05 PM"), + direction, method, id, err, eol) + return + } + outx := new(strings.Builder) + fmt.Fprintf(outx, "[Trace - %v] ", time.Now().Format("3:04:05 PM")) + switch direction { + case jsonrpc2.Send: + fmt.Fprint(outx, "Received ") + case jsonrpc2.Receive: + fmt.Fprint(outx, "Sending ") + } + switch { + case id == nil: + fmt.Fprint(outx, "notification ") + case elapsed >= 0: + fmt.Fprint(outx, "response ") + default: + fmt.Fprint(outx, "request ") + } + fmt.Fprintf(outx, "'%s", method) + switch { + case id == nil: + // do nothing + case id.Name != "": + fmt.Fprintf(outx, " - (%s)", id.Name) + default: + fmt.Fprintf(outx, " - (%d)", id.Number) + } + fmt.Fprint(outx, "'") + if elapsed >= 0 { + fmt.Fprintf(outx, " in %vms", elapsed.Nanoseconds()/1000) + } + params := string(*payload) + if params == "null" { + params = "{}" + } + fmt.Fprintf(outx, ".\r\nParams: %s%s", params, eol) + fmt.Fprintf(out, "%s", outx.String()) + }, + ) +}