| // 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: |
| if len(keys) > 1 { |
| 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)...) |
| } |