internal/lsp: add version and bug commands

Also log gopls version information on startup in server mode.

Change-Id: If7bf85d19f993430709b1fae83083e6fdfe1b2ca
Reviewed-on: https://go-review.googlesource.com/c/tools/+/175199
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/browser/README.md b/internal/lsp/browser/README.md
new file mode 100644
index 0000000..e5f04df
--- /dev/null
+++ b/internal/lsp/browser/README.md
@@ -0,0 +1 @@
+This package is a copy of cmd/internal/browser from the go distribution
\ No newline at end of file
diff --git a/internal/lsp/browser/browser.go b/internal/lsp/browser/browser.go
new file mode 100644
index 0000000..6867c85
--- /dev/null
+++ b/internal/lsp/browser/browser.go
@@ -0,0 +1,67 @@
+// Copyright 2016 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 browser provides utilities for interacting with users' browsers.
+package browser
+
+import (
+	"os"
+	"os/exec"
+	"runtime"
+	"time"
+)
+
+// Commands returns a list of possible commands to use to open a url.
+func Commands() [][]string {
+	var cmds [][]string
+	if exe := os.Getenv("BROWSER"); exe != "" {
+		cmds = append(cmds, []string{exe})
+	}
+	switch runtime.GOOS {
+	case "darwin":
+		cmds = append(cmds, []string{"/usr/bin/open"})
+	case "windows":
+		cmds = append(cmds, []string{"cmd", "/c", "start"})
+	default:
+		if os.Getenv("DISPLAY") != "" {
+			// xdg-open is only for use in a desktop environment.
+			cmds = append(cmds, []string{"xdg-open"})
+		}
+	}
+	cmds = append(cmds,
+		[]string{"chrome"},
+		[]string{"google-chrome"},
+		[]string{"chromium"},
+		[]string{"firefox"},
+	)
+	return cmds
+}
+
+// Open tries to open url in a browser and reports whether it succeeded.
+func Open(url string) bool {
+	for _, args := range Commands() {
+		cmd := exec.Command(args[0], append(args[1:], url)...)
+		if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) {
+			return true
+		}
+	}
+	return false
+}
+
+// appearsSuccessful reports whether the command appears to have run successfully.
+// If the command runs longer than the timeout, it's deemed successful.
+// If the command runs within the timeout, it's deemed successful if it exited cleanly.
+func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool {
+	errc := make(chan error, 1)
+	go func() {
+		errc <- cmd.Wait()
+	}()
+
+	select {
+	case <-time.After(timeout):
+		return true
+	case err := <-errc:
+		return err == nil
+	}
+}
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index f1baea7..1059f04 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -118,9 +118,11 @@
 func (app *Application) commands() []tool.Application {
 	return []tool.Application{
 		&app.Serve,
+		&bug{},
 		&check{app: app},
 		&format{app: app},
 		&query{app: app},
+		&version{app: app},
 	}
 }
 
diff --git a/internal/lsp/cmd/info.go b/internal/lsp/cmd/info.go
new file mode 100644
index 0000000..2514405
--- /dev/null
+++ b/internal/lsp/cmd/info.go
@@ -0,0 +1,84 @@
+// Copyright 2019 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 (
+	"bytes"
+	"context"
+	"flag"
+	"fmt"
+	"net/url"
+	"os"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp"
+	"golang.org/x/tools/internal/lsp/browser"
+)
+
+// version implements the version command.
+type version struct {
+	app *Application
+}
+
+// bug implements the bug command.
+type bug struct{}
+
+func (v *version) Name() string      { return "version" }
+func (v *version) Usage() string     { return "" }
+func (v *version) ShortHelp() string { return "print the gopls version information" }
+func (v *version) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), ``)
+	f.PrintDefaults()
+}
+
+// Run collects some basic information and then prepares an issue ready to
+// be reported.
+func (v *version) Run(ctx context.Context, args ...string) error {
+	lsp.PrintVersionInfo(os.Stdout, v.app.Verbose, false)
+	return nil
+}
+
+func (b *bug) Name() string      { return "bug" }
+func (b *bug) Usage() string     { return "" }
+func (b *bug) ShortHelp() string { return "report a bug in gopls" }
+func (b *bug) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), ``)
+	f.PrintDefaults()
+}
+
+const goplsBugPrefix = "gopls: "
+const goplsBugHeader = `Please answer these questions before submitting your issue. Thanks!
+
+#### What did you do?
+If possible, provide a recipe for reproducing the error.
+A complete runnable program is good.
+A link on play.golang.org is better.
+A failing unit test is the best.
+
+#### What did you expect to see?
+
+
+#### What did you see instead?
+
+
+`
+
+// Run collects some basic information and then prepares an issue ready to
+// be reported.
+func (b *bug) Run(ctx context.Context, args ...string) error {
+	buf := &bytes.Buffer{}
+	fmt.Fprint(buf, goplsBugHeader)
+	lsp.PrintVersionInfo(buf, true, true)
+	body := buf.String()
+	title := strings.Join(args, " ")
+	if !strings.HasPrefix(title, goplsBugPrefix) {
+		title = goplsBugPrefix + title
+	}
+	if !browser.Open("https://github.com/golang/go/issues/new?title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body)) {
+		fmt.Print("Please file a new issue at golang.org/issue/new using this template:\n\n")
+		fmt.Print(body)
+	}
+	return nil
+}
diff --git a/internal/lsp/general.go b/internal/lsp/general.go
index 212a039..09f0a4e 100644
--- a/internal/lsp/general.go
+++ b/internal/lsp/general.go
@@ -5,6 +5,7 @@
 package lsp
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"os"
@@ -139,6 +140,9 @@
 			}
 		}
 	}
+	buf := &bytes.Buffer{}
+	PrintVersionInfo(buf, true, false)
+	s.log.Infof(ctx, "%s", buf)
 	return nil
 }
 
diff --git a/internal/lsp/info.1.11.go b/internal/lsp/info.1.11.go
new file mode 100644
index 0000000..cd24339
--- /dev/null
+++ b/internal/lsp/info.1.11.go
@@ -0,0 +1,16 @@
+// Copyright 2019 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.
+
+// +build !go1.12
+
+package lsp
+
+import (
+	"fmt"
+	"io"
+)
+
+func printBuildInfo(w io.Writer, verbose bool) {
+	fmt.Fprintf(w, "no module information, gopls not build with go 1.11 or earlier\n")
+}
diff --git a/internal/lsp/info.go b/internal/lsp/info.go
new file mode 100644
index 0000000..476ebb8
--- /dev/null
+++ b/internal/lsp/info.go
@@ -0,0 +1,38 @@
+// Copyright 2019 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.
+
+// +build go1.12
+
+package lsp
+
+import (
+	"fmt"
+	"io"
+	"runtime/debug"
+)
+
+func printBuildInfo(w io.Writer, verbose bool) {
+	if info, ok := debug.ReadBuildInfo(); ok {
+		fmt.Fprintf(w, "%v\n", info.Path)
+		printModuleInfo(w, &info.Main)
+		if verbose {
+			for _, dep := range info.Deps {
+				printModuleInfo(w, dep)
+			}
+		}
+	} else {
+		fmt.Fprintf(w, "no module information, gopls not built in module mode\n")
+	}
+}
+
+func printModuleInfo(w io.Writer, m *debug.Module) {
+	fmt.Fprintf(w, "    %s@%s", m.Path, m.Version)
+	if m.Sum != "" {
+		fmt.Fprintf(w, " %s", m.Sum)
+	}
+	if m.Replace != nil {
+		fmt.Fprintf(w, " => %v", m.Replace.Path)
+	}
+	fmt.Fprintf(w, "\n")
+}
diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go
index 9777e7d..a29edd5 100644
--- a/internal/lsp/text_synchronization.go
+++ b/internal/lsp/text_synchronization.go
@@ -34,7 +34,6 @@
 		}
 		text = change.Text
 	}
-	s.log.Debugf(ctx, "didChange: %s", params.TextDocument.URI)
 	return s.cacheAndDiagnose(ctx, span.NewURI(params.TextDocument.URI), text)
 }
 
diff --git a/internal/lsp/util.go b/internal/lsp/util.go
index 48d83fc..244e2cb 100644
--- a/internal/lsp/util.go
+++ b/internal/lsp/util.go
@@ -7,12 +7,45 @@
 import (
 	"context"
 	"fmt"
+	"io"
+	"os/exec"
 
 	"golang.org/x/tools/internal/lsp/protocol"
 	"golang.org/x/tools/internal/lsp/source"
 	"golang.org/x/tools/internal/span"
 )
 
+// This writes the version and environment information to a writer.
+func PrintVersionInfo(w io.Writer, verbose bool, markdown bool) {
+	if !verbose {
+		printBuildInfo(w, false)
+		return
+	}
+	fmt.Fprint(w, "#### Build info\n\n")
+	if markdown {
+		fmt.Fprint(w, "```\n")
+	}
+	printBuildInfo(w, true)
+	fmt.Fprint(w, "\n")
+	if markdown {
+		fmt.Fprint(w, "```\n")
+	}
+	fmt.Fprint(w, "\n#### Go info\n\n")
+	if markdown {
+		fmt.Fprint(w, "```\n")
+	}
+	cmd := exec.Command("go", "version")
+	cmd.Stdout = w
+	cmd.Run()
+	fmt.Fprint(w, "\n")
+	cmd = exec.Command("go", "env")
+	cmd.Stdout = w
+	cmd.Run()
+	if markdown {
+		fmt.Fprint(w, "```\n")
+	}
+}
+
 func newColumnMap(ctx context.Context, v source.View, uri span.URI) (source.File, *protocol.ColumnMapper, error) {
 	f, err := v.GetFile(ctx, uri)
 	if err != nil {