internal/lsp: add the format command line

Change-Id: If7c4135b6b81b4f691d0f5eae8b49a1aca028346
Reviewed-on: https://go-review.googlesource.com/c/tools/+/171031
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/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 4c85c21..3155f42 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -114,8 +114,9 @@
 func (app *Application) commands() []tool.Application {
 	return []tool.Application{
 		&app.Serve,
-		&query{app: app},
 		&check{app: app},
+		&format{app: app},
+		&query{app: app},
 	}
 }
 
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index 5389664..0738b11 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -7,7 +7,6 @@
 import (
 	"io/ioutil"
 	"os"
-	"strings"
 	"testing"
 
 	"golang.org/x/tools/go/packages/packagestest"
@@ -43,10 +42,6 @@
 	//TODO: add command line completions tests when it works
 }
 
-func (r *runner) Format(t *testing.T, data tests.Formats) {
-	//TODO: add command line formatting tests when it works
-}
-
 func (r *runner) Highlight(t *testing.T, data tests.Highlights) {
 	//TODO: add command line highlight tests when it works
 }
@@ -76,5 +71,5 @@
 	if err != nil {
 		t.Fatal(err)
 	}
-	return strings.TrimSpace(string(data))
+	return string(data)
 }
diff --git a/internal/lsp/cmd/format.go b/internal/lsp/cmd/format.go
new file mode 100644
index 0000000..02f9bc3
--- /dev/null
+++ b/internal/lsp/cmd/format.go
@@ -0,0 +1,108 @@
+// 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 (
+	"context"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"strings"
+
+	"golang.org/x/tools/internal/lsp"
+	"golang.org/x/tools/internal/lsp/diff"
+	"golang.org/x/tools/internal/lsp/protocol"
+	"golang.org/x/tools/internal/lsp/source"
+	"golang.org/x/tools/internal/span"
+)
+
+// format implements the format verb for gopls.
+type format struct {
+	Diff  bool `flag:"d" help:"display diffs instead of rewriting files"`
+	Write bool `flag:"w" help:"write result to (source) file instead of stdout"`
+	List  bool `flag:"l" help:"list files whose formatting differs from gofmt's"`
+
+	app *Application
+}
+
+func (c *format) Name() string      { return "format" }
+func (c *format) Usage() string     { return "<filerange>" }
+func (c *format) ShortHelp() string { return "format the code according to the go standard" }
+func (c *format) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), `
+The arguments supplied may be simple file names, or ranges within files.
+
+Example: reformat this file:
+
+  $ gopls format -w internal/lsp/cmd/check.go
+
+	gopls format flags are:
+`)
+	f.PrintDefaults()
+}
+
+// Run performs the check on the files specified by args and prints the
+// results to stdout.
+func (f *format) Run(ctx context.Context, args ...string) error {
+	if len(args) == 0 {
+		// no files, so no results
+		return nil
+	}
+	client := &baseClient{}
+	// now we ready to kick things off
+	server, err := f.app.connect(ctx, client)
+	if err != nil {
+		return err
+	}
+	for _, arg := range args {
+		spn := span.Parse(arg)
+		m, err := client.AddFile(ctx, spn.URI())
+		if err != nil {
+			return err
+		}
+		filename, _ := spn.URI().Filename() // this cannot fail, already checked in AddFile above
+		loc, err := m.Location(spn)
+		if err != nil {
+			return err
+		}
+		p := protocol.DocumentRangeFormattingParams{
+			TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
+			Range:        loc.Range,
+		}
+		edits, err := server.RangeFormatting(ctx, &p)
+		if err != nil {
+			return fmt.Errorf("%v: %v", spn, err)
+		}
+		sedits, err := lsp.FromProtocolEdits(m, edits)
+		if err != nil {
+			return fmt.Errorf("%v: %v", spn, err)
+		}
+		ops := source.EditsToDiff(sedits)
+		lines := diff.SplitLines(string(m.Content))
+		formatted := strings.Join(diff.ApplyEdits(lines, ops), "")
+		printIt := true
+		if f.List {
+			printIt = false
+			if len(edits) > 0 {
+				fmt.Println(filename)
+			}
+		}
+		if f.Write {
+			printIt = false
+			if len(edits) > 0 {
+				ioutil.WriteFile(filename, []byte(formatted), 0644)
+			}
+		}
+		if f.Diff {
+			printIt = false
+			u := diff.ToUnified(filename, filename, lines, ops)
+			fmt.Print(u)
+		}
+		if printIt {
+			fmt.Print(formatted)
+		}
+	}
+	return nil
+}
diff --git a/internal/lsp/cmd/format_test.go b/internal/lsp/cmd/format_test.go
new file mode 100644
index 0000000..380c6c5
--- /dev/null
+++ b/internal/lsp/cmd/format_test.go
@@ -0,0 +1,103 @@
+// 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_test
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"io/ioutil"
+	"os/exec"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/internal/lsp/cmd"
+	"golang.org/x/tools/internal/lsp/tests"
+	"golang.org/x/tools/internal/tool"
+)
+
+var formatModes = [][]string{
+	[]string{},
+	[]string{"-d"},
+}
+
+func (r *runner) Format(t *testing.T, data tests.Formats) {
+	for _, spn := range data {
+		for _, mode := range formatModes {
+			isDiff := false
+			tag := "gofmt"
+			for _, arg := range mode {
+				tag += arg
+				if arg == "-d" {
+					isDiff = true
+				}
+			}
+			uri := spn.URI()
+			filename, err := uri.Filename()
+			if err != nil {
+				t.Fatal(err)
+			}
+			args := append(mode, filename)
+			expect := string(r.data.Golden(tag, filename, func(golden string) error {
+				cmd := exec.Command("gofmt", args...)
+				buf := &bytes.Buffer{}
+				cmd.Stdout = buf
+				cmd.Run() // ignore error, sometimes we have intentionally ungofmt-able files
+				contents := buf.String()
+				// strip the unwanted diff line
+				if isDiff {
+					if strings.HasPrefix(contents, "diff -u") {
+						if i := strings.IndexRune(contents, '\n'); i >= 0 && i < len(contents)-1 {
+							contents = contents[i+1:]
+						}
+					}
+					contents, _ = stripFileHeader(contents)
+				}
+				return ioutil.WriteFile(golden, []byte(contents), 0666)
+			}))
+			if expect == "" {
+				//TODO: our error handling differs, for now just skip unformattable files
+				continue
+			}
+			app := &cmd.Application{}
+			app.Config = r.data.Config
+			got := captureStdOut(t, func() {
+				tool.Main(context.Background(), app, append([]string{"format"}, args...))
+			})
+			if isDiff {
+				got, err = stripFileHeader(got)
+				if err != nil {
+					t.Errorf("%v: got: %v\n%v", filename, err, got)
+					continue
+				}
+			}
+			// check the first two lines are the expected file header
+			if expect != got {
+				t.Errorf("format failed with %#v expected:\n%s\ngot:\n%s", args, expect, got)
+			}
+		}
+	}
+}
+
+func stripFileHeader(s string) (string, error) {
+	s = strings.TrimSpace(s)
+	if !strings.HasPrefix(s, "---") {
+		return s, fmt.Errorf("missing original")
+	}
+	if i := strings.IndexRune(s, '\n'); i >= 0 && i < len(s)-1 {
+		s = s[i+1:]
+	} else {
+		return s, fmt.Errorf("no EOL for original")
+	}
+	if !strings.HasPrefix(s, "+++") {
+		return s, fmt.Errorf("missing output")
+	}
+	if i := strings.IndexRune(s, '\n'); i >= 0 && i < len(s)-1 {
+		s = s[i+1:]
+	} else {
+		return s, fmt.Errorf("no EOL for output")
+	}
+	return s, nil
+}
diff --git a/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go b/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go
new file mode 100644
index 0000000..a01634c
--- /dev/null
+++ b/internal/lsp/testdata/format/bad_format.gofmt-d.golden.go
@@ -0,0 +1,17 @@
+@@ -1,16 +1,13 @@
+ package format //@format("package")
+ 
+ import (
+-	"runtime"
+ 	"fmt"
+ 	"log"
++	"runtime"
+ )
+ 
+ func hello() {
+ 
+-
+-
+-
+ 	var x int //@diag("x", "LSP", "x declared but not used")
+ }
\ No newline at end of file
diff --git a/internal/lsp/testdata/format/good_format.gofmt-d.golden.go b/internal/lsp/testdata/format/good_format.gofmt-d.golden.go
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/internal/lsp/testdata/format/good_format.gofmt-d.golden.go
diff --git a/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go b/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go
new file mode 100644
index 0000000..1f356fb
--- /dev/null
+++ b/internal/lsp/testdata/format/newline_format.gofmt-d.golden.go
@@ -0,0 +1,5 @@
+@@ -1,2 +1,2 @@
+ package format //@format("package")
+-func _() {}
+\ No newline at end of file
++func _()       {}
\ No newline at end of file
diff --git a/internal/lsp/testdata/noparse_format/noparse_format.gofmt-d.golden.go b/internal/lsp/testdata/noparse_format/noparse_format.gofmt-d.golden.go
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/internal/lsp/testdata/noparse_format/noparse_format.gofmt-d.golden.go