diff --git a/cmd/golsp/forward/main.go b/cmd/golsp/forward/main.go new file mode 100644 index 0000000000..1139fe9d98 --- /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=]" } +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 e4d1ffc589..1665d5f4ab 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 0767b6891c..5725be6bf6 100644 --- a/cmd/golsp/integration/vscode/src/extension.ts +++ b/cmd/golsp/integration/vscode/src/extension.ts @@ -12,9 +12,10 @@ import path = require('path'); 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 f5041d906c..2320838079 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -23,7 +23,7 @@ type Application struct { // 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 ed3b6e0b5b..f6ab95a299 100644 --- a/internal/lsp/cmd/server.go +++ b/internal/lsp/cmd/server.go @@ -21,19 +21,20 @@ import ( "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 @@ 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 { +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 @@ func (s *server) Run(ctx context.Context, args ...string) error { 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 cdd07f2cb3..4460816402 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -6,7 +6,9 @@ package lsp import ( "context" + "fmt" "go/token" + "net" "os" "sync" @@ -26,6 +28,28 @@ func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) 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