Skip to content

Commit 5eefd05

Browse files
hartzellianthehat
authored andcommitted
tools/gopls: add command line support for rename
This commit adds support for calling rename from the gopls command line, e.g. $ gopls rename -w ~/tmp/foo/main.go:8:6 $ gopls rename -w ~/tmp/foo/main.go:#53 Optional arguments are: - -w, which writes the changes back to the original file; and - -d, which prints a unified diff to stdout With no arguments, the changed files are printed to stdout. It: - adds internal/lsp/cmd/rename.go, which implements the command; - adds "rename" to the list of commands in internal/lsp/cmd/cmd.go; - removes the dummy test from internal/lsp/cmd/cmd_test.go; and - adds internal/lsp/cmd/rename_test.go, which uses the existing "golden" data to implement its tests. Updates #32875 Change-Id: I5cab5a40b4aa26357b26b0caf4ed54dbd2284d0f GitHub-Last-Rev: fe853d3 GitHub-Pull-Request: #157 Reviewed-on: https://go-review.googlesource.com/c/tools/+/194878 Run-TryBot: Ian Cottrell <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Ian Cottrell <[email protected]>
1 parent 1081e67 commit 5eefd05

File tree

9 files changed

+628
-4
lines changed

9 files changed

+628
-4
lines changed

internal/lsp/cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func (app *Application) commands() []tool.Application {
140140
&check{app: app},
141141
&format{app: app},
142142
&query{app: app},
143+
&rename{app: app},
143144
&version{app: app},
144145
}
145146
}

internal/lsp/cmd/cmd_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,6 @@ func (r *runner) Reference(t *testing.T, data tests.References) {
6262
//TODO: add command line references tests when it works
6363
}
6464

65-
func (r *runner) Rename(t *testing.T, data tests.Renames) {
66-
//TODO: add command line rename tests when it works
67-
}
68-
6965
func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) {
7066
//TODO: add command line prepare rename tests when it works
7167
}

internal/lsp/cmd/rename.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Copyright 2019 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package cmd
6+
7+
import (
8+
"context"
9+
"flag"
10+
"fmt"
11+
"io/ioutil"
12+
"os"
13+
"path/filepath"
14+
"sort"
15+
"strings"
16+
17+
"golang.org/x/tools/internal/lsp/diff"
18+
"golang.org/x/tools/internal/lsp/protocol"
19+
"golang.org/x/tools/internal/lsp/source"
20+
"golang.org/x/tools/internal/span"
21+
"golang.org/x/tools/internal/tool"
22+
errors "golang.org/x/xerrors"
23+
)
24+
25+
// rename implements the rename verb for gopls.
26+
type rename struct {
27+
Diff bool `flag:"d" help:"display diffs instead of rewriting files"`
28+
Write bool `flag:"w" help:"write result to (source) file instead of stdout"`
29+
30+
app *Application
31+
}
32+
33+
func (r *rename) Name() string { return "rename" }
34+
func (r *rename) Usage() string { return "<position>" }
35+
func (r *rename) ShortHelp() string { return "rename selected identifier" }
36+
func (r *rename) DetailedHelp(f *flag.FlagSet) {
37+
fmt.Fprint(f.Output(), `
38+
Example:
39+
40+
$ # 1-based location (:line:column or :#position) of the thing to change
41+
$ gopls rename helper/helper.go:8:6
42+
$ gopls rename helper/helper.go:#53
43+
44+
gopls rename flags are:
45+
`)
46+
f.PrintDefaults()
47+
}
48+
49+
// Run renames the specified identifier and either;
50+
// - if -w is specified, updates the file(s) in place;
51+
// - if -d is specified, prints out unified diffs of the changes; or
52+
// - otherwise, prints the new versions to stdout.
53+
func (r *rename) Run(ctx context.Context, args ...string) error {
54+
if len(args) != 2 {
55+
return tool.CommandLineErrorf("definition expects 2 arguments (position, new name)")
56+
}
57+
conn, err := r.app.connect(ctx)
58+
if err != nil {
59+
return err
60+
}
61+
defer conn.terminate(ctx)
62+
63+
from := span.Parse(args[0])
64+
file := conn.AddFile(ctx, from.URI())
65+
if file.err != nil {
66+
return file.err
67+
}
68+
69+
loc, err := file.mapper.Location(from)
70+
if err != nil {
71+
return err
72+
}
73+
74+
p := protocol.RenameParams{
75+
TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
76+
Position: loc.Range.Start,
77+
NewName: args[1],
78+
}
79+
we, err := conn.Rename(ctx, &p)
80+
if err != nil {
81+
return err
82+
}
83+
84+
// Make output order predictable
85+
var keys []string
86+
for u, _ := range *we.Changes {
87+
keys = append(keys, u)
88+
}
89+
sort.Strings(keys)
90+
changeCount := len(keys)
91+
92+
for _, u := range keys {
93+
edits := (*we.Changes)[u]
94+
uri := span.NewURI(u)
95+
cmdFile := conn.AddFile(ctx, uri)
96+
filename := cmdFile.uri.Filename()
97+
98+
// convert LSP-style edits to []diff.TextEdit cuz Spans are handy
99+
renameEdits, err := source.FromProtocolEdits(cmdFile.mapper, edits)
100+
if err != nil {
101+
return errors.Errorf("%v: %v", edits, err)
102+
}
103+
104+
newContent := diff.ApplyEdits(string(cmdFile.mapper.Content), renameEdits)
105+
106+
switch {
107+
case r.Write:
108+
fmt.Fprintln(os.Stderr, filename)
109+
err := os.Rename(filename, filename+".orig")
110+
if err != nil {
111+
return errors.Errorf("%v: %v", edits, err)
112+
}
113+
ioutil.WriteFile(filename, []byte(newContent), 0644)
114+
case r.Diff:
115+
// myersEdits := diff.ComputeEdits(cmdFile.uri, string(cmdFile.mapper.Content), string(newContent))
116+
myersEdits := toMyersTextEdits(renameEdits, cmdFile.mapper)
117+
diffs := diff.ToUnified(filename+".orig", filename, string(cmdFile.mapper.Content), myersEdits)
118+
fmt.Print(diffs)
119+
default:
120+
fmt.Printf("%s:\n", filepath.Base(filename))
121+
fmt.Print(string(newContent))
122+
if changeCount > 1 { // if this wasn't last change, print newline
123+
fmt.Println()
124+
}
125+
changeCount -= 1
126+
}
127+
}
128+
return nil
129+
}
130+
131+
type editPair [2]diff.TextEdit // container for a del/ins TextEdit pair
132+
133+
// toMyersTextEdits converts the "word-oriented" textEdits returned by
134+
// source.Rename into the "line-oriented" textEdits that
135+
// diff.ToUnified() (aka myers.toUnified()) expects.
136+
func toMyersTextEdits(edits []diff.TextEdit, mapper *protocol.ColumnMapper) []diff.TextEdit {
137+
var myersEdits []diff.TextEdit
138+
139+
if len(edits) == 0 {
140+
return myersEdits
141+
}
142+
143+
contentByLine := strings.Split(string(mapper.Content), "\n")
144+
145+
// gather all of the edits on a line, create an editPair from them,
146+
// and append it to the list of pairs
147+
var pairs []editPair
148+
var pending []diff.TextEdit
149+
currentLine := edits[0].Span.Start().Line()
150+
for i := 0; i < len(edits); i++ {
151+
if edits[i].Span.Start().Line() != currentLine {
152+
pairs = append(pairs, toEditPair(pending, contentByLine[currentLine-1]))
153+
currentLine = edits[i].Span.Start().Line()
154+
pending = pending[:0] // clear it, leaking not a problem...
155+
}
156+
pending = append(pending, edits[i])
157+
}
158+
pairs = append(pairs, toEditPair(pending, contentByLine[currentLine-1]))
159+
160+
// reorder contiguous del/ins pairs into blocks of del and ins
161+
myersEdits = reorderEdits(pairs)
162+
return myersEdits
163+
}
164+
165+
// toEditPair takes one or more "word" diff.TextEdit(s) that occur
166+
// on a single line and creates a single equivalent
167+
// delete-line/insert-line pair of diff.TextEdit.
168+
func toEditPair(edits []diff.TextEdit, before string) editPair {
169+
// interleave retained bits of old line with new text from edits
170+
p := 0 // position in old line
171+
after := ""
172+
for i := 0; i < len(edits); i++ {
173+
after += before[p:edits[i].Span.Start().Column()-1] + edits[i].NewText
174+
p = edits[i].Span.End().Column() - 1
175+
}
176+
after += before[p:] + "\n"
177+
178+
// seems we can get away w/out providing offsets
179+
u := edits[0].Span.URI()
180+
l := edits[0].Span.Start().Line()
181+
newEdits := editPair{
182+
diff.TextEdit{Span: span.New(u, span.NewPoint(l, 1, -1), span.NewPoint(l+1, 1, -1))},
183+
diff.TextEdit{Span: span.New(u, span.NewPoint(l+1, 1, -1), span.NewPoint(l+1, 1, -1)), NewText: after},
184+
}
185+
return newEdits
186+
}
187+
188+
// reorderEdits reorders blocks of delete/insert pairs so that all of
189+
// the deletes come first, resetting the spans for the insert records
190+
// to keep them "sorted". It assumes that each entry is a "del/ins"
191+
// pair.
192+
func reorderEdits(e []editPair) []diff.TextEdit {
193+
var r []diff.TextEdit // reordered edits
194+
var p []diff.TextEdit // pending insert edits, waiting for end of dels
195+
196+
r = append(r, e[0][0])
197+
p = append(p, e[0][1])
198+
199+
for i := 1; i < len(e); i++ {
200+
if e[i][0].Span.Start().Line() != r[len(r)-1].Span.Start().Line()+1 {
201+
unpend(&r, &p)
202+
p = p[:0] // clear it, leaking not a problem...
203+
}
204+
r = append(r, e[i][0])
205+
p = append(p, e[i][1])
206+
}
207+
unpend(&r, &p)
208+
209+
return r
210+
}
211+
212+
// unpend sets the spans of the pending TextEdits to point to the last
213+
// line in the associated block of deletes then appends them to r.
214+
func unpend(r, p *[]diff.TextEdit) {
215+
for j := 0; j < len(*p); j++ {
216+
prev := (*r)[len(*r)-1]
217+
(*p)[j].Span = span.New(prev.Span.URI(), prev.Span.End(), prev.Span.End())
218+
}
219+
*r = append(*r, (*p)...)
220+
}

internal/lsp/cmd/rename_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2019 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package cmd_test
6+
7+
import (
8+
"fmt"
9+
"sort"
10+
"strings"
11+
"testing"
12+
13+
"golang.org/x/tools/internal/lsp/cmd"
14+
"golang.org/x/tools/internal/lsp/tests"
15+
"golang.org/x/tools/internal/span"
16+
"golang.org/x/tools/internal/tool"
17+
)
18+
19+
var renameModes = [][]string{
20+
[]string{},
21+
[]string{"-d"},
22+
}
23+
24+
func (r *runner) Rename(t *testing.T, data tests.Renames) {
25+
sortedSpans := sortSpans(data) // run the tests in a repeatable order
26+
for _, spn := range sortedSpans {
27+
tag := data[spn]
28+
filename := spn.URI().Filename()
29+
for _, mode := range renameModes {
30+
goldenTag := data[spn] + strings.Join(mode, "") + "-rename"
31+
expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) {
32+
return []byte{}, nil
33+
}))
34+
35+
app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Config.Env)
36+
loc := fmt.Sprintf("%v", spn)
37+
args := []string{"-remote=internal", "rename"}
38+
if strings.Join(mode, "") != "" {
39+
args = append(args, strings.Join(mode, ""))
40+
}
41+
args = append(args, loc, tag)
42+
var err error
43+
got := captureStdOut(t, func() {
44+
err = tool.Run(r.ctx, app, args)
45+
})
46+
if err != nil {
47+
got = err.Error()
48+
}
49+
got = normalizePaths(r.data, got)
50+
if expect != got {
51+
t.Errorf("rename failed with %#v expected:\n%s\ngot:\n%s", args, expect, got)
52+
}
53+
}
54+
}
55+
}
56+
57+
func sortSpans(data map[span.Span]string) []span.Span {
58+
spans := make([]span.Span, 0, len(data))
59+
for spn, _ := range data {
60+
spans = append(spans, spn)
61+
}
62+
sort.Slice(spans, func(i, j int) bool {
63+
return span.Compare(spans[i], spans[j]) < 0
64+
})
65+
return spans
66+
}

0 commit comments

Comments
 (0)