|
| 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 | +} |
0 commit comments