Skip to content

Commit

Permalink
internal/lsp: refactor the command line handling
Browse files Browse the repository at this point in the history
This switched the golsp binary to support a sub-command model so it can grow
some guru like command line query capabilites

Change-Id: I1a7a49bb17701e62004bba636d6bee9de2481ffd
Reviewed-on: https://go-review.googlesource.com/c/154559
Reviewed-by: Rebecca Stambler <[email protected]>
  • Loading branch information
ianthehat committed Dec 17, 2018
1 parent 71d3d86 commit a072e66
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 139 deletions.
142 changes: 3 additions & 139 deletions cmd/golsp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:])
}
81 changes: 81 additions & 0 deletions internal/lsp/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -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> [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,
}
}
110 changes: 110 additions & 0 deletions internal/lsp/cmd/server.go
Original file line number Diff line number Diff line change
@@ -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())
},
)
}

0 comments on commit a072e66

Please sign in to comment.