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: fe853d325ef91f8f911987790fcba7a5a777b6ce
GitHub-Pull-Request: golang/tools#157
Reviewed-on: https://go-review.googlesource.com/c/tools/+/194878
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cmd/cmd.go b/internal/lsp/cmd/cmd.go
index 3f95add..9ef778b 100644
--- a/internal/lsp/cmd/cmd.go
+++ b/internal/lsp/cmd/cmd.go
@@ -140,6 +140,7 @@
 		&check{app: app},
 		&format{app: app},
 		&query{app: app},
+		&rename{app: app},
 		&version{app: app},
 	}
 }
diff --git a/internal/lsp/cmd/cmd_test.go b/internal/lsp/cmd/cmd_test.go
index b9e2fa6..4dad9db 100644
--- a/internal/lsp/cmd/cmd_test.go
+++ b/internal/lsp/cmd/cmd_test.go
@@ -62,10 +62,6 @@
 	//TODO: add command line references tests when it works
 }
 
-func (r *runner) Rename(t *testing.T, data tests.Renames) {
-	//TODO: add command line rename tests when it works
-}
-
 func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) {
 	//TODO: add command line prepare rename tests when it works
 }
diff --git a/internal/lsp/cmd/rename.go b/internal/lsp/cmd/rename.go
new file mode 100644
index 0000000..71eb0d1
--- /dev/null
+++ b/internal/lsp/cmd/rename.go
@@ -0,0 +1,220 @@
+// 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"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+
+	"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"
+	"golang.org/x/tools/internal/tool"
+	errors "golang.org/x/xerrors"
+)
+
+// rename implements the rename verb for gopls.
+type rename 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"`
+
+	app *Application
+}
+
+func (r *rename) Name() string      { return "rename" }
+func (r *rename) Usage() string     { return "<position>" }
+func (r *rename) ShortHelp() string { return "rename selected identifier" }
+func (r *rename) DetailedHelp(f *flag.FlagSet) {
+	fmt.Fprint(f.Output(), `
+Example:
+
+  $ # 1-based location (:line:column or :#position) of the thing to change
+  $ gopls rename helper/helper.go:8:6
+  $ gopls rename helper/helper.go:#53
+
+	gopls rename flags are:
+`)
+	f.PrintDefaults()
+}
+
+// Run renames the specified identifier and either;
+// - if -w is specified, updates the file(s) in place;
+// - if -d is specified, prints out unified diffs of the changes; or
+// - otherwise, prints the new versions to stdout.
+func (r *rename) Run(ctx context.Context, args ...string) error {
+	if len(args) != 2 {
+		return tool.CommandLineErrorf("definition expects 2 arguments (position, new name)")
+	}
+	conn, err := r.app.connect(ctx)
+	if err != nil {
+		return err
+	}
+	defer conn.terminate(ctx)
+
+	from := span.Parse(args[0])
+	file := conn.AddFile(ctx, from.URI())
+	if file.err != nil {
+		return file.err
+	}
+
+	loc, err := file.mapper.Location(from)
+	if err != nil {
+		return err
+	}
+
+	p := protocol.RenameParams{
+		TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI},
+		Position:     loc.Range.Start,
+		NewName:      args[1],
+	}
+	we, err := conn.Rename(ctx, &p)
+	if err != nil {
+		return err
+	}
+
+	// Make output order predictable
+	var keys []string
+	for u, _ := range *we.Changes {
+		keys = append(keys, u)
+	}
+	sort.Strings(keys)
+	changeCount := len(keys)
+
+	for _, u := range keys {
+		edits := (*we.Changes)[u]
+		uri := span.NewURI(u)
+		cmdFile := conn.AddFile(ctx, uri)
+		filename := cmdFile.uri.Filename()
+
+		// convert LSP-style edits to []diff.TextEdit cuz Spans are handy
+		renameEdits, err := source.FromProtocolEdits(cmdFile.mapper, edits)
+		if err != nil {
+			return errors.Errorf("%v: %v", edits, err)
+		}
+
+		newContent := diff.ApplyEdits(string(cmdFile.mapper.Content), renameEdits)
+
+		switch {
+		case r.Write:
+			fmt.Fprintln(os.Stderr, filename)
+			err := os.Rename(filename, filename+".orig")
+			if err != nil {
+				return errors.Errorf("%v: %v", edits, err)
+			}
+			ioutil.WriteFile(filename, []byte(newContent), 0644)
+		case r.Diff:
+			// myersEdits := diff.ComputeEdits(cmdFile.uri, string(cmdFile.mapper.Content), string(newContent))
+			myersEdits := toMyersTextEdits(renameEdits, cmdFile.mapper)
+			diffs := diff.ToUnified(filename+".orig", filename, string(cmdFile.mapper.Content), myersEdits)
+			fmt.Print(diffs)
+		default:
+			fmt.Printf("%s:\n", filepath.Base(filename))
+			fmt.Print(string(newContent))
+			if changeCount > 1 { // if this wasn't last change, print newline
+				fmt.Println()
+			}
+			changeCount -= 1
+		}
+	}
+	return nil
+}
+
+type editPair [2]diff.TextEdit // container for a del/ins TextEdit pair
+
+// toMyersTextEdits converts the "word-oriented" textEdits returned by
+// source.Rename into the "line-oriented" textEdits that
+// diff.ToUnified() (aka myers.toUnified()) expects.
+func toMyersTextEdits(edits []diff.TextEdit, mapper *protocol.ColumnMapper) []diff.TextEdit {
+	var myersEdits []diff.TextEdit
+
+	if len(edits) == 0 {
+		return myersEdits
+	}
+
+	contentByLine := strings.Split(string(mapper.Content), "\n")
+
+	// gather all of the edits on a line, create an editPair from them,
+	// and append it to the list of pairs
+	var pairs []editPair
+	var pending []diff.TextEdit
+	currentLine := edits[0].Span.Start().Line()
+	for i := 0; i < len(edits); i++ {
+		if edits[i].Span.Start().Line() != currentLine {
+			pairs = append(pairs, toEditPair(pending, contentByLine[currentLine-1]))
+			currentLine = edits[i].Span.Start().Line()
+			pending = pending[:0] // clear it, leaking not a problem...
+		}
+		pending = append(pending, edits[i])
+	}
+	pairs = append(pairs, toEditPair(pending, contentByLine[currentLine-1]))
+
+	// reorder contiguous del/ins pairs into blocks of del and ins
+	myersEdits = reorderEdits(pairs)
+	return myersEdits
+}
+
+// toEditPair takes one or more "word" diff.TextEdit(s) that occur
+// on a single line and creates a single equivalent
+// delete-line/insert-line pair of diff.TextEdit.
+func toEditPair(edits []diff.TextEdit, before string) editPair {
+	// interleave retained bits of old line with new text from edits
+	p := 0 // position in old line
+	after := ""
+	for i := 0; i < len(edits); i++ {
+		after += before[p:edits[i].Span.Start().Column()-1] + edits[i].NewText
+		p = edits[i].Span.End().Column() - 1
+	}
+	after += before[p:] + "\n"
+
+	// seems we can get away w/out providing offsets
+	u := edits[0].Span.URI()
+	l := edits[0].Span.Start().Line()
+	newEdits := editPair{
+		diff.TextEdit{Span: span.New(u, span.NewPoint(l, 1, -1), span.NewPoint(l+1, 1, -1))},
+		diff.TextEdit{Span: span.New(u, span.NewPoint(l+1, 1, -1), span.NewPoint(l+1, 1, -1)), NewText: after},
+	}
+	return newEdits
+}
+
+// reorderEdits reorders blocks of delete/insert pairs so that all of
+// the deletes come first, resetting the spans for the insert records
+// to keep them "sorted".  It assumes that each entry is a "del/ins"
+// pair.
+func reorderEdits(e []editPair) []diff.TextEdit {
+	var r []diff.TextEdit // reordered edits
+	var p []diff.TextEdit // pending insert edits, waiting for end of dels
+
+	r = append(r, e[0][0])
+	p = append(p, e[0][1])
+
+	for i := 1; i < len(e); i++ {
+		if e[i][0].Span.Start().Line() != r[len(r)-1].Span.Start().Line()+1 {
+			unpend(&r, &p)
+			p = p[:0] // clear it, leaking not a problem...
+		}
+		r = append(r, e[i][0])
+		p = append(p, e[i][1])
+	}
+	unpend(&r, &p)
+
+	return r
+}
+
+// unpend sets the spans of the pending TextEdits to point to the last
+// line in the associated block of deletes then appends them to r.
+func unpend(r, p *[]diff.TextEdit) {
+	for j := 0; j < len(*p); j++ {
+		prev := (*r)[len(*r)-1]
+		(*p)[j].Span = span.New(prev.Span.URI(), prev.Span.End(), prev.Span.End())
+	}
+	*r = append(*r, (*p)...)
+}
diff --git a/internal/lsp/cmd/rename_test.go b/internal/lsp/cmd/rename_test.go
new file mode 100644
index 0000000..73d3296
--- /dev/null
+++ b/internal/lsp/cmd/rename_test.go
@@ -0,0 +1,66 @@
+// 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 (
+	"fmt"
+	"sort"
+	"strings"
+	"testing"
+
+	"golang.org/x/tools/internal/lsp/cmd"
+	"golang.org/x/tools/internal/lsp/tests"
+	"golang.org/x/tools/internal/span"
+	"golang.org/x/tools/internal/tool"
+)
+
+var renameModes = [][]string{
+	[]string{},
+	[]string{"-d"},
+}
+
+func (r *runner) Rename(t *testing.T, data tests.Renames) {
+	sortedSpans := sortSpans(data) // run the tests in a repeatable order
+	for _, spn := range sortedSpans {
+		tag := data[spn]
+		filename := spn.URI().Filename()
+		for _, mode := range renameModes {
+			goldenTag := data[spn] + strings.Join(mode, "") + "-rename"
+			expect := string(r.data.Golden(goldenTag, filename, func() ([]byte, error) {
+				return []byte{}, nil
+			}))
+
+			app := cmd.New("gopls-test", r.data.Config.Dir, r.data.Config.Env)
+			loc := fmt.Sprintf("%v", spn)
+			args := []string{"-remote=internal", "rename"}
+			if strings.Join(mode, "") != "" {
+				args = append(args, strings.Join(mode, ""))
+			}
+			args = append(args, loc, tag)
+			var err error
+			got := captureStdOut(t, func() {
+				err = tool.Run(r.ctx, app, args)
+			})
+			if err != nil {
+				got = err.Error()
+			}
+			got = normalizePaths(r.data, got)
+			if expect != got {
+				t.Errorf("rename failed with %#v expected:\n%s\ngot:\n%s", args, expect, got)
+			}
+		}
+	}
+}
+
+func sortSpans(data map[span.Span]string) []span.Span {
+	spans := make([]span.Span, 0, len(data))
+	for spn, _ := range data {
+		spans = append(spans, spn)
+	}
+	sort.Slice(spans, func(i, j int) bool {
+		return span.Compare(spans[i], spans[j]) < 0
+	})
+	return spans
+}
diff --git a/internal/lsp/testdata/rename/a/random.go.golden b/internal/lsp/testdata/rename/a/random.go.golden
index b1a20ff..135e43a 100644
--- a/internal/lsp/testdata/rename/a/random.go.golden
+++ b/internal/lsp/testdata/rename/a/random.go.golden
@@ -628,3 +628,284 @@
 	}
 }
 
+-- fmty-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -2,7 +2,7 @@
+ 
+ import (
+ 	lg "log"
+-	"fmt" //@rename("fmt", "fmty")
++	fmty "fmt" //@rename("fmt", "fmty")
+ 	f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
+ )
+ 
+@@ -33,7 +33,7 @@
+ 
+ 	switch y := x.(type) { //@rename("y", "y0")
+ 	case int:
+-		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
++		fmty.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+ 		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+
+-- f2name-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -3,7 +3,7 @@
+ import (
+ 	lg "log"
+ 	"fmt" //@rename("fmt", "fmty")
+-	f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
++	f2name "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
+ )
+ 
+ func Random() int {
+@@ -37,6 +37,6 @@
+ 	case string:
+ 		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		f2name.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
+-- f2y-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -3,7 +3,7 @@
+ import (
+ 	lg "log"
+ 	"fmt" //@rename("fmt", "fmty")
+-	f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
++	f2y "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
+ )
+ 
+ func Random() int {
+@@ -37,6 +37,6 @@
+ 	case string:
+ 		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		f2y.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
+-- z-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -11,8 +11,8 @@
+ 	return y
+ }
+ 
+-func Random2(y int) int { //@rename("y", "z")
+-	return y
++func Random2(z int) int { //@rename("y", "z")
++	return z
+ }
+ 
+ type Pos struct {
+
+-- myX-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -16,11 +16,11 @@
+ }
+ 
+ type Pos struct {
+-	x, y int
++	myX, y int
+ }
+ 
+ func (p *Pos) Sum() int {
+-	return p.x + p.y //@rename("x", "myX")
++	return p.myX + p.y //@rename("x", "myX")
+ }
+ 
+ func _() {
+
+-- pos-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -24,8 +24,8 @@
+ }
+ 
+ func _() {
+-	var p Pos   //@rename("p", "pos")
+-	_ = p.Sum() //@rename("Sum", "GetSum")
++	var pos Pos   //@rename("p", "pos")
++	_ = pos.Sum() //@rename("Sum", "GetSum")
+ }
+ 
+ func sw() {
+
+-- GetSum-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -19,13 +19,13 @@
+ 	x, y int
+ }
+ 
+-func (p *Pos) Sum() int {
++func (p *Pos) GetSum() int {
+ 	return p.x + p.y //@rename("x", "myX")
+ }
+ 
+ func _() {
+ 	var p Pos   //@rename("p", "pos")
+-	_ = p.Sum() //@rename("Sum", "GetSum")
++	_ = p.GetSum() //@rename("Sum", "GetSum")
+ }
+ 
+ func sw() {
+
+-- y0-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -31,12 +31,12 @@
+ func sw() {
+ 	var x interface{}
+ 
+-	switch y := x.(type) { //@rename("y", "y0")
++	switch y0 := x.(type) { //@rename("y", "y0")
+ 	case int:
+-		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
++		fmt.Printf("%d", y0) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+-		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
++		lg.Printf("%s", y0) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		f2.Printf("%v", y0) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
+-- format-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -2,7 +2,7 @@
+ 
+ import (
+ 	lg "log"
+-	"fmt" //@rename("fmt", "fmty")
++	format "fmt" //@rename("fmt", "fmty")
+ 	f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
+ )
+ 
+@@ -33,7 +33,7 @@
+ 
+ 	switch y := x.(type) { //@rename("y", "y0")
+ 	case int:
+-		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
++		format.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+ 		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+
+-- y1-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -31,12 +31,12 @@
+ func sw() {
+ 	var x interface{}
+ 
+-	switch y := x.(type) { //@rename("y", "y0")
++	switch y1 := x.(type) { //@rename("y", "y0")
+ 	case int:
+-		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
++		fmt.Printf("%d", y1) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+-		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
++		lg.Printf("%s", y1) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		f2.Printf("%v", y1) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
+-- log-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -1,7 +1,7 @@
+ package a
+ 
+ import (
+-	lg "log"
++	"log"
+ 	"fmt" //@rename("fmt", "fmty")
+ 	f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
+ )
+@@ -35,7 +35,7 @@
+ 	case int:
+ 		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+-		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
++		log.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+ 		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+
+-- y2-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -31,12 +31,12 @@
+ func sw() {
+ 	var x interface{}
+ 
+-	switch y := x.(type) { //@rename("y", "y0")
++	switch y2 := x.(type) { //@rename("y", "y0")
+ 	case int:
+-		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
++		fmt.Printf("%d", y2) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+-		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
++		lg.Printf("%s", y2) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		f2.Printf("%v", y2) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
+-- fmt2-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -3,7 +3,7 @@
+ import (
+ 	lg "log"
+ 	"fmt" //@rename("fmt", "fmty")
+-	f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
++	fmt2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y")
+ )
+ 
+ func Random() int {
+@@ -37,6 +37,6 @@
+ 	case string:
+ 		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		fmt2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
+-- y3-d-rename --
+--- rename/a/random.go.orig
++++ rename/a/random.go
+@@ -31,12 +31,12 @@
+ func sw() {
+ 	var x interface{}
+ 
+-	switch y := x.(type) { //@rename("y", "y0")
++	switch y3 := x.(type) { //@rename("y", "y0")
+ 	case int:
+-		fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format")
++		fmt.Printf("%d", y3) //@rename("y", "y1"),rename("fmt", "format")
+ 	case string:
+-		lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log")
++		lg.Printf("%s", y3) //@rename("y", "y2"),rename("lg","log")
+ 	default:
+-		f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2")
++		f2.Printf("%v", y3) //@rename("y", "y3"),rename("f2","fmt2")
+ 	}
+ }
+
diff --git a/internal/lsp/testdata/rename/b/b.go.golden b/internal/lsp/testdata/rename/b/b.go.golden
index 5fd037a..e7b6de5 100644
--- a/internal/lsp/testdata/rename/b/b.go.golden
+++ b/internal/lsp/testdata/rename/b/b.go.golden
@@ -1,2 +1,4 @@
 -- uint-rename --
 cannot rename builtin "int"
+-- uint-d-rename --
+cannot rename builtin "int"
diff --git a/internal/lsp/testdata/rename/bad/bad.go.golden b/internal/lsp/testdata/rename/bad/bad.go.golden
index 7f45813..8f8140b 100644
--- a/internal/lsp/testdata/rename/bad/bad.go.golden
+++ b/internal/lsp/testdata/rename/bad/bad.go.golden
@@ -1,2 +1,4 @@
 -- rFunc-rename --
 renaming "sFunc" to "rFunc" not possible because "golang.org/x/tools/internal/lsp/rename/bad" has errors
+-- rFunc-d-rename --
+renaming "sFunc" to "rFunc" not possible because "golang.org/x/tools/internal/lsp/rename/bad" has errors
diff --git a/internal/lsp/testdata/rename/testy/testy.go.golden b/internal/lsp/testdata/rename/testy/testy.go.golden
index 66693ba..fb9e117 100644
--- a/internal/lsp/testdata/rename/testy/testy.go.golden
+++ b/internal/lsp/testdata/rename/testy/testy.go.golden
@@ -18,3 +18,26 @@
 	foo := 42 //@rename("foo", "bar")
 }
 
+-- testyType-d-rename --
+--- rename/testy/testy.go.orig
++++ rename/testy/testy.go
+@@ -1,6 +1,6 @@
+ package testy
+ 
+-type tt int //@rename("tt", "testyType")
++type testyType int //@rename("tt", "testyType")
+ 
+ func a() {
+ 	foo := 42 //@rename("foo", "bar")
+
+-- bar-d-rename --
+--- rename/testy/testy.go.orig
++++ rename/testy/testy.go
+@@ -3,5 +3,5 @@
+ type tt int //@rename("tt", "testyType")
+ 
+ func a() {
+-	foo := 42 //@rename("foo", "bar")
++	bar := 42 //@rename("foo", "bar")
+ }
+
diff --git a/internal/lsp/testdata/rename/testy/testy_test.go.golden b/internal/lsp/testdata/rename/testy/testy_test.go.golden
index f54e5d7..6aacd22 100644
--- a/internal/lsp/testdata/rename/testy/testy_test.go.golden
+++ b/internal/lsp/testdata/rename/testy/testy_test.go.golden
@@ -29,3 +29,36 @@
 	a()       //@rename("a", "b")
 }
 
+-- testyX-d-rename --
+--- rename/testy/testy_test.go.orig
++++ rename/testy/testy_test.go
+@@ -3,6 +3,6 @@
+ import "testing"
+ 
+ func TestSomething(t *testing.T) {
+-	var x int //@rename("x", "testyX")
++	var testyX int //@rename("x", "testyX")
+ 	a()       //@rename("a", "b")
+ }
+
+-- b-d-rename --
+--- rename/testy/testy.go.orig
++++ rename/testy/testy.go
+@@ -2,6 +2,6 @@
+ 
+ type tt int //@rename("tt", "testyType")
+ 
+-func a() {
++func b() {
+ 	foo := 42 //@rename("foo", "bar")
+ }
+--- rename/testy/testy_test.go.orig
++++ rename/testy/testy_test.go
+@@ -4,5 +4,5 @@
+ 
+ func TestSomething(t *testing.T) {
+ 	var x int //@rename("x", "testyX")
+-	a()       //@rename("a", "b")
++	b()       //@rename("a", "b")
+ }
+