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