diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go index 4c85c215..3155f425 100644 --- a/internal/lsp/cmd/cmd.go +++ b/internal/lsp/cmd/cmd.go @@ -114,8 +114,9 @@ func (app *Application) Run(ctx context.Context, args ...string) error { 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 5389664c..0738b111 100644 --- a/internal/lsp/cmd/cmd_test.go +++ b/internal/lsp/cmd/cmd_test.go @@ -7,7 +7,6 @@ package cmd_test import ( "io/ioutil" "os" - "strings" "testing" "golang.org/x/tools/go/packages/packagestest" @@ -43,10 +42,6 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.Co //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 @@ func captureStdOut(t testing.TB, f func()) string { 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 00000000..02f9bc3a --- /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 "" } +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 00000000..380c6c53 --- /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 00000000..a01634c7 --- /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 00000000..e69de29b 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 00000000..1f356fb5 --- /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 00000000..e69de29b