cmd/golsp: add a debugging tool to connect with golsp on a port

This change allows golsp to be run on a port, with an intermediary
command passing the data through. This allows for improved logging.
Also, add necessary changes to VSCode integration to allow changing the
name of the command for golsp.

Change-Id: I20dca1a50296636e57e022342ee70f0610ad1531
Reviewed-on: https://go-review.googlesource.com/c/157497
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/cmd/golsp/forward/main.go b/cmd/golsp/forward/main.go
new file mode 100644
index 0000000..1139fe9
--- /dev/null
+++ b/cmd/golsp/forward/main.go
@@ -0,0 +1,59 @@
+// The forward command writes and reads to a golsp server on a network socket.
+package main
+
+import (
+	"context"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+
+	"golang.org/x/tools/internal/lsp/cmd"
+	"golang.org/x/tools/internal/tool"
+)
+
+func main() {
+	tool.Main(context.Background(), &app{&cmd.Server{}}, os.Args[1:])
+}
+
+type app struct {
+	*cmd.Server
+}
+
+func (*app) Name() string               { return "forward" }
+func (*app) Usage() string              { return "[-port=<value>]" }
+func (*app) ShortHelp() string          { return "An intermediary between an editor and GoLSP." }
+func (*app) DetailedHelp(*flag.FlagSet) {}
+
+func (a *app) Run(ctx context.Context, args ...string) error {
+	if a.Server.Port == 0 {
+		a.ShortHelp()
+		os.Exit(0)
+	}
+	conn, err := net.Dial("tcp", fmt.Sprintf(":%v", a.Server.Port))
+	if err != nil {
+		log.Print(err)
+		os.Exit(0)
+	}
+
+	go func(conn net.Conn) {
+		_, err := io.Copy(conn, os.Stdin)
+		if err != nil {
+			log.Print(err)
+			os.Exit(0)
+		}
+	}(conn)
+
+	go func(conn net.Conn) {
+		_, err := io.Copy(os.Stdout, conn)
+		if err != nil {
+			log.Print(err)
+			os.Exit(0)
+		}
+	}(conn)
+
+	for {
+	}
+}
diff --git a/cmd/golsp/integration/vscode/package.json b/cmd/golsp/integration/vscode/package.json
index e4d1ffc..1665d5f 100644
--- a/cmd/golsp/integration/vscode/package.json
+++ b/cmd/golsp/integration/vscode/package.json
@@ -45,6 +45,12 @@
                     "default": [],
                     "description": "Flags to pass to golsp",
                     "scope": "resource"
+                },
+                "golsp.command": {
+                    "type": "string",
+                    "default": "golsp",
+                    "description": "Name of the GoLSP binary",
+                    "scope": "resource"
                 }
             }
         }
diff --git a/cmd/golsp/integration/vscode/src/extension.ts b/cmd/golsp/integration/vscode/src/extension.ts
index 0767b68..5725be6 100644
--- a/cmd/golsp/integration/vscode/src/extension.ts
+++ b/cmd/golsp/integration/vscode/src/extension.ts
@@ -12,9 +12,10 @@
 export function activate(ctx: vscode.ExtensionContext): void {
   let document = vscode.window.activeTextEditor.document;
   let config = vscode.workspace.getConfiguration('golsp', document.uri);
+  let golspCommand: string = config['command'];
   let golspFlags: string[] = config['flags'];
   let serverOptions:
-      lsp.ServerOptions = {command: getBinPath('golsp'), args: golspFlags};
+      lsp.ServerOptions = {command: getBinPath(golspCommand), args: golspFlags};
   let clientOptions: lsp.LanguageClientOptions = {
     initializationOptions: {},
     documentSelector: ['go'],
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index f5041d9..2320838 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -23,7 +23,7 @@
 	// 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
+	Server Server
 }
 
 // Name implements tool.Application returning the binary name.
diff --git a/internal/lsp/cmd/server.go b/internal/lsp/cmd/server.go
index ed3b6e0..f6ab95a 100644
--- a/internal/lsp/cmd/server.go
+++ b/internal/lsp/cmd/server.go
@@ -21,19 +21,20 @@
 	"golang.org/x/tools/internal/tool"
 )
 
-// server is a struct that exposes the configurable parts of the LSP server as
+// 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 {
+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"`
+	Port    int    `flag:"port" help:"port on which to run golsp for debugging purposes"`
 }
 
-func (s *server) Name() string  { return "server" }
-func (s *server) Usage() string { return "" }
-func (s *server) ShortHelp() string {
+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) {
+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.
@@ -42,7 +43,7 @@
 
 // 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 {
+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)
 	}
@@ -60,52 +61,54 @@
 		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 {
-				msec := int(elapsed.Round(time.Millisecond) / time.Millisecond)
-				fmt.Fprintf(outx, " in %dms", msec)
-			}
-			params := string(*payload)
-			if params == "null" {
-				params = "{}"
-			}
-			fmt.Fprintf(outx, ".\r\nParams: %s%s", params, eol)
-			fmt.Fprintf(out, "%s", outx.String())
-		},
-	)
+	logger := 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 {
+			msec := int(elapsed.Round(time.Millisecond) / time.Millisecond)
+			fmt.Fprintf(outx, " in %dms", msec)
+		}
+		params := string(*payload)
+		if params == "null" {
+			params = "{}"
+		}
+		fmt.Fprintf(outx, ".\r\nParams: %s%s", params, eol)
+		fmt.Fprintf(out, "%s", outx.String())
+	}
+	// For debugging purposes only.
+	if s.Port != 0 {
+		return lsp.RunServerOnPort(ctx, s.Port, logger)
+	}
+	stream := jsonrpc2.NewHeaderStream(os.Stdin, os.Stdout)
+	return lsp.RunServer(ctx, stream, logger)
 }
diff --git a/internal/lsp/server.go b/internal/lsp/server.go
index cdd07f2..4460816 100644
--- a/internal/lsp/server.go
+++ b/internal/lsp/server.go
@@ -6,7 +6,9 @@
 
 import (
 	"context"
+	"fmt"
 	"go/token"
+	"net"
 	"os"
 	"sync"
 
@@ -26,6 +28,28 @@
 	return conn.Wait(ctx)
 }
 
+// RunServerOnPort starts an LSP server on the given port and does not exit.
+// This function exists for debugging purposes.
+func RunServerOnPort(ctx context.Context, port int, opts ...interface{}) error {
+	s := &server{}
+	ln, err := net.Listen("tcp", fmt.Sprintf(":%v", port))
+	if err != nil {
+		return err
+	}
+	for {
+		conn, err := ln.Accept()
+		if err != nil {
+			return err
+		}
+		stream := jsonrpc2.NewHeaderStream(conn, conn)
+		go func() {
+			conn, client := protocol.RunServer(ctx, stream, s, opts...)
+			s.client = client
+			conn.Wait(ctx)
+		}()
+	}
+}
+
 type server struct {
 	client protocol.Client