internal/lsp: abstract the diff library so it can be substituted

this moves the actual diff algorithm into a different package and then provides hooks so it can be easily replaced with an alternate algorithm.

Change-Id: Ia0359f58878493599ea0e0fda8920f21100e16f1
Reviewed-on: https://go-review.googlesource.com/c/tools/+/190898
Run-TryBot: Ian Cottrell <iancottrell@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-08-19 19:28:08 -04:00 committed by Ian Cottrell
parent d9ab56aa29
commit 85edb9ef32
18 changed files with 192 additions and 143 deletions

View File

@ -9,12 +9,10 @@ import (
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strings"
"golang.org/x/tools/internal/lsp" "golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol" "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/span"
errors "golang.org/x/xerrors" errors "golang.org/x/xerrors"
) )
@ -82,9 +80,7 @@ func (f *format) Run(ctx context.Context, args ...string) error {
if err != nil { if err != nil {
return errors.Errorf("%v: %v", spn, err) return errors.Errorf("%v: %v", spn, err)
} }
ops := source.EditsToDiff(sedits) formatted := diff.ApplyEdits(string(file.mapper.Content), sedits)
lines := diff.SplitLines(string(file.mapper.Content))
formatted := strings.Join(diff.ApplyEdits(lines, ops), "")
printIt := true printIt := true
if f.List { if f.List {
printIt = false printIt = false
@ -100,7 +96,7 @@ func (f *format) Run(ctx context.Context, args ...string) error {
} }
if f.Diff { if f.Diff {
printIt = false printIt = false
u := diff.ToUnified(filename+".orig", filename, lines, ops) u := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits)
fmt.Print(u) fmt.Print(u)
} }
if printIt { if printIt {

View File

@ -0,0 +1,32 @@
// 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 diff supports a pluggable diff algorithm.
package diff
import (
"sort"
"golang.org/x/tools/internal/span"
)
// TextEdit represents a change to a section of a document.
// The text within the specified span should be replaced by the supplied new text.
type TextEdit struct {
Span span.Span
NewText string
}
var (
ComputeEdits func(uri span.URI, before, after string) []TextEdit
ApplyEdits func(before string, edits []TextEdit) string
ToUnified func(from, to string, before string, edits []TextEdit) string
)
func SortTextEdits(d []TextEdit) {
// Use a stable sort to maintain the order of edits inserted at the same position.
sort.SliceStable(d, func(i int, j int) bool {
return span.Compare(d[i].Span, d[j].Span) < 0
})
}

View File

@ -0,0 +1,80 @@
// 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 diff
import (
"fmt"
"strings"
"golang.org/x/tools/internal/lsp/diff/myers"
"golang.org/x/tools/internal/span"
)
func init() {
ComputeEdits = myersComputeEdits
ApplyEdits = myersApplyEdits
ToUnified = myersToUnified
}
func myersComputeEdits(uri span.URI, before, after string) []TextEdit {
u := myers.SplitLines(before)
f := myers.SplitLines(after)
return myersDiffToEdits(uri, myers.Operations(u, f))
}
func myersApplyEdits(before string, edits []TextEdit) string {
ops := myersEditsToDiff(edits)
return strings.Join(myers.ApplyEdits(myers.SplitLines(before), ops), "")
}
func myersToUnified(from, to string, before string, edits []TextEdit) string {
u := myers.SplitLines(before)
ops := myersEditsToDiff(edits)
return fmt.Sprint(myers.ToUnified(from, to, u, ops))
}
func myersDiffToEdits(uri span.URI, ops []*myers.Op) []TextEdit {
edits := make([]TextEdit, 0, len(ops))
for _, op := range ops {
s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0))
switch op.Kind {
case myers.Delete:
// Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{Span: s})
case myers.Insert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
if content := strings.Join(op.Content, ""); content != "" {
edits = append(edits, TextEdit{Span: s, NewText: content})
}
}
}
return edits
}
func myersEditsToDiff(edits []TextEdit) []*myers.Op {
iToJ := 0
ops := make([]*myers.Op, len(edits))
for i, edit := range edits {
i1 := edit.Span.Start().Line() - 1
i2 := edit.Span.End().Line() - 1
kind := myers.Insert
if edit.NewText == "" {
kind = myers.Delete
}
ops[i] = &myers.Op{
Kind: kind,
Content: myers.SplitLines(edit.NewText),
I1: i1,
I2: i2,
J1: i1 + iToJ,
}
if kind == myers.Insert {
iToJ += len(ops[i].Content)
} else {
iToJ -= i2 - i1
}
}
return ops
}

View File

@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// Package diff implements the Myers diff algorithm. // Package myers implements the Myers diff algorithm.
package diff package myers
import "strings" import "strings"

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package diff_test package myers_test
import ( import (
"flag" "flag"
@ -14,7 +14,7 @@ import (
"strings" "strings"
"testing" "testing"
"golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/lsp/diff/myers"
) )
const ( const (
@ -28,22 +28,22 @@ var verifyDiff = flag.Bool("verify-diff", false, "Check that the unified diff ou
func TestDiff(t *testing.T) { func TestDiff(t *testing.T) {
for _, test := range []struct { for _, test := range []struct {
a, b string a, b string
lines []*diff.Op lines []*myers.Op
operations []*diff.Op operations []*myers.Op
unified string unified string
nodiff bool nodiff bool
}{ }{
{ {
a: "A\nB\nC\n", a: "A\nB\nC\n",
b: "A\nB\nC\n", b: "A\nB\nC\n",
operations: []*diff.Op{}, operations: []*myers.Op{},
unified: ` unified: `
`[1:]}, { `[1:]}, {
a: "A\n", a: "A\n",
b: "B\n", b: "B\n",
operations: []*diff.Op{ operations: []*myers.Op{
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0}, &myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
&diff.Op{Kind: diff.Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0}, &myers.Op{Kind: myers.Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0},
}, },
unified: ` unified: `
@@ -1 +1 @@ @@ -1 +1 @@
@ -52,9 +52,9 @@ func TestDiff(t *testing.T) {
`[1:]}, { `[1:]}, {
a: "A", a: "A",
b: "B", b: "B",
operations: []*diff.Op{ operations: []*myers.Op{
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0}, &myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
&diff.Op{Kind: diff.Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0}, &myers.Op{Kind: myers.Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0},
}, },
unified: ` unified: `
@@ -1 +1 @@ @@ -1 +1 @@
@ -65,12 +65,12 @@ func TestDiff(t *testing.T) {
`[1:]}, { `[1:]}, {
a: "A\nB\nC\nA\nB\nB\nA\n", a: "A\nB\nC\nA\nB\nB\nA\n",
b: "C\nB\nA\nB\nA\nC\n", b: "C\nB\nA\nB\nA\nC\n",
operations: []*diff.Op{ operations: []*myers.Op{
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0}, &myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
&diff.Op{Kind: diff.Delete, I1: 1, I2: 2, J1: 0}, &myers.Op{Kind: myers.Delete, I1: 1, I2: 2, J1: 0},
&diff.Op{Kind: diff.Insert, Content: []string{"B\n"}, I1: 3, I2: 3, J1: 1}, &myers.Op{Kind: myers.Insert, Content: []string{"B\n"}, I1: 3, I2: 3, J1: 1},
&diff.Op{Kind: diff.Delete, I1: 5, I2: 6, J1: 4}, &myers.Op{Kind: myers.Delete, I1: 5, I2: 6, J1: 4},
&diff.Op{Kind: diff.Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5}, &myers.Op{Kind: myers.Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
}, },
unified: ` unified: `
@@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
@ -89,10 +89,10 @@ func TestDiff(t *testing.T) {
{ {
a: "A\nB\n", a: "A\nB\n",
b: "A\nC\n\n", b: "A\nC\n\n",
operations: []*diff.Op{ operations: []*myers.Op{
&diff.Op{Kind: diff.Delete, I1: 1, I2: 2, J1: 1}, &myers.Op{Kind: myers.Delete, I1: 1, I2: 2, J1: 1},
&diff.Op{Kind: diff.Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1}, &myers.Op{Kind: myers.Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
&diff.Op{Kind: diff.Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2}, &myers.Op{Kind: myers.Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
}, },
unified: ` unified: `
@@ -1,2 +1,3 @@ @@ -1,2 +1,3 @@
@ -120,9 +120,9 @@ func TestDiff(t *testing.T) {
+K +K
`[1:]}, `[1:]},
} { } {
a := diff.SplitLines(test.a) a := myers.SplitLines(test.a)
b := diff.SplitLines(test.b) b := myers.SplitLines(test.b)
ops := diff.Operations(a, b) ops := myers.Operations(a, b)
if test.operations != nil { if test.operations != nil {
if len(ops) != len(test.operations) { if len(ops) != len(test.operations) {
t.Fatalf("expected %v operations, got %v", len(test.operations), len(ops)) t.Fatalf("expected %v operations, got %v", len(test.operations), len(ops))
@ -134,7 +134,7 @@ func TestDiff(t *testing.T) {
} }
} }
} }
applied := diff.ApplyEdits(a, ops) applied := myers.ApplyEdits(a, ops)
for i, want := range applied { for i, want := range applied {
got := b[i] got := b[i]
if got != want { if got != want {
@ -142,7 +142,7 @@ func TestDiff(t *testing.T) {
} }
} }
if test.unified != "" { if test.unified != "" {
diff := diff.ToUnified(fileA, fileB, a, ops) diff := myers.ToUnified(fileA, fileB, a, ops)
got := fmt.Sprint(diff) got := fmt.Sprint(diff)
if !strings.HasPrefix(got, unifiedPrefix) { if !strings.HasPrefix(got, unifiedPrefix) {
t.Errorf("expected prefix:\n%s\ngot:\n%s", unifiedPrefix, got) t.Errorf("expected prefix:\n%s\ngot:\n%s", unifiedPrefix, got)
@ -166,7 +166,7 @@ func TestDiff(t *testing.T) {
} }
func getDiffOutput(a, b string) (string, error) { func getDiffOutput(a, b string) (string, error) {
fileA, err := ioutil.TempFile("", "diff.in") fileA, err := ioutil.TempFile("", "myers.in")
if err != nil { if err != nil {
return "", err return "", err
} }
@ -177,7 +177,7 @@ func getDiffOutput(a, b string) (string, error) {
if err := fileA.Close(); err != nil { if err := fileA.Close(); err != nil {
return "", err return "", err
} }
fileB, err := ioutil.TempFile("", "diff.in") fileB, err := ioutil.TempFile("", "myers.in")
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package diff package myers
import ( import (
"fmt" "fmt"

View File

@ -7,6 +7,7 @@ package lsp
import ( import (
"context" "context"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
@ -51,7 +52,7 @@ func spanToRange(ctx context.Context, view source.View, spn span.Span) (source.G
return f, m, rng, nil return f, m, rng, nil
} }
func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]protocol.TextEdit, error) { func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) {
if edits == nil { if edits == nil {
return nil, nil return nil, nil
} }
@ -69,17 +70,17 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]proto
return result, nil return result, nil
} }
func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]source.TextEdit, error) { func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) {
if edits == nil { if edits == nil {
return nil, nil return nil, nil
} }
result := make([]source.TextEdit, len(edits)) result := make([]diff.TextEdit, len(edits))
for i, edit := range edits { for i, edit := range edits {
spn, err := m.RangeSpan(edit.Range) spn, err := m.RangeSpan(edit.Range)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result[i] = source.TextEdit{ result[i] = diff.TextEdit{
Span: spn, Span: spn,
NewText: edit.NewText, NewText: edit.NewText,
} }

View File

@ -287,8 +287,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
ops := source.EditsToDiff(sedits) got := diff.ApplyEdits(string(m.Content), sedits)
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
if gofmted != got { if gofmted != got {
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got)
} }
@ -334,8 +333,7 @@ func (r *runner) Import(t *testing.T, data tests.Imports) {
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
ops := source.EditsToDiff(sedits) got := diff.ApplyEdits(string(m.Content), sedits)
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
if goimported != got { if goimported != got {
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got) t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
} }
@ -549,7 +547,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) {
} }
} }
func applyEdits(contents string, edits []source.TextEdit) string { func applyEdits(contents string, edits []diff.TextEdit) string {
res := contents res := contents
// Apply the edits from the end of the file forward // Apply the edits from the end of the file forward

View File

@ -13,6 +13,7 @@ import (
"golang.org/x/tools/go/ast/astutil" "golang.org/x/tools/go/ast/astutil"
"golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/fuzzy" "golang.org/x/tools/internal/lsp/fuzzy"
"golang.org/x/tools/internal/lsp/snippet" "golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
@ -41,7 +42,7 @@ type CompletionItem struct {
// Additional text edits should be used to change text unrelated to the current cursor position // Additional text edits should be used to change text unrelated to the current cursor position
// (for example adding an import statement at the top of the file if the completion item will // (for example adding an import statement at the top of the file if the completion item will
// insert an unqualified type). // insert an unqualified type).
AdditionalTextEdits []TextEdit AdditionalTextEdits []diff.TextEdit
// Depth is how many levels were searched to find this completion. // Depth is how many levels were searched to find this completion.
// For example when completing "foo<>", "fooBar" is depth 0, and // For example when completing "foo<>", "fooBar" is depth 0, and

View File

@ -13,6 +13,7 @@ import (
"go/types" "go/types"
"strings" "strings"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/snippet" "golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log" "golang.org/x/tools/internal/telemetry/log"
@ -35,7 +36,7 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
kind CompletionItemKind kind CompletionItemKind
plainSnippet *snippet.Builder plainSnippet *snippet.Builder
placeholderSnippet *snippet.Builder placeholderSnippet *snippet.Builder
addlEdits []TextEdit addlEdits []diff.TextEdit
) )
// expandFuncCall mutates the completion label, detail, and snippets // expandFuncCall mutates the completion label, detail, and snippets

View File

@ -35,6 +35,7 @@ import (
"golang.org/x/tools/go/analysis/passes/unsafeptr" "golang.org/x/tools/go/analysis/passes/unsafeptr"
"golang.org/x/tools/go/analysis/passes/unusedresult" "golang.org/x/tools/go/analysis/passes/unusedresult"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
@ -55,7 +56,7 @@ type Diagnostic struct {
type SuggestedFixes struct { type SuggestedFixes struct {
Title string Title string
Edits []TextEdit Edits []diff.TextEdit
} }
type DiagnosticSeverity int type DiagnosticSeverity int

View File

@ -21,7 +21,7 @@ import (
) )
// Format formats a file with a given range. // Format formats a file with a given range.
func Format(ctx context.Context, f GoFile, rng span.Range) ([]TextEdit, error) { func Format(ctx context.Context, f GoFile, rng span.Range) ([]diff.TextEdit, error) {
ctx, done := trace.StartSpan(ctx, "source.Format") ctx, done := trace.StartSpan(ctx, "source.Format")
defer done() defer done()
@ -74,7 +74,7 @@ func formatSource(ctx context.Context, file File) ([]byte, error) {
} }
// Imports formats a file using the goimports tool. // Imports formats a file using the goimports tool.
func Imports(ctx context.Context, view View, f GoFile, rng span.Range) ([]TextEdit, error) { func Imports(ctx context.Context, view View, f GoFile, rng span.Range) ([]diff.TextEdit, error) {
ctx, done := trace.StartSpan(ctx, "source.Imports") ctx, done := trace.StartSpan(ctx, "source.Imports")
defer done() defer done()
data, _, err := f.Handle(ctx).Read(ctx) data, _, err := f.Handle(ctx).Read(ctx)
@ -112,14 +112,14 @@ func Imports(ctx context.Context, view View, f GoFile, rng span.Range) ([]TextEd
type ImportFix struct { type ImportFix struct {
Fix *imports.ImportFix Fix *imports.ImportFix
Edits []TextEdit Edits []diff.TextEdit
} }
// AllImportsFixes formats f for each possible fix to the imports. // AllImportsFixes formats f for each possible fix to the imports.
// In addition to returning the result of applying all edits, // In addition to returning the result of applying all edits,
// it returns a list of fixes that could be applied to the file, with the // it returns a list of fixes that could be applied to the file, with the
// corresponding TextEdits that would be needed to apply that fix. // corresponding TextEdits that would be needed to apply that fix.
func AllImportsFixes(ctx context.Context, view View, f GoFile, rng span.Range) (edits []TextEdit, editsPerFix []*ImportFix, err error) { func AllImportsFixes(ctx context.Context, view View, f GoFile, rng span.Range) (edits []diff.TextEdit, editsPerFix []*ImportFix, err error) {
ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes") ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes")
defer done() defer done()
data, _, err := f.Handle(ctx).Read(ctx) data, _, err := f.Handle(ctx).Read(ctx)
@ -224,7 +224,7 @@ func hasListErrors(errors []packages.Error) bool {
return false return false
} }
func computeTextEdits(ctx context.Context, file File, formatted string) (edits []TextEdit) { func computeTextEdits(ctx context.Context, file File, formatted string) (edits []diff.TextEdit) {
ctx, done := trace.StartSpan(ctx, "source.computeTextEdits") ctx, done := trace.StartSpan(ctx, "source.computeTextEdits")
defer done() defer done()
data, _, err := file.Handle(ctx).Read(ctx) data, _, err := file.Handle(ctx).Read(ctx)
@ -232,7 +232,5 @@ func computeTextEdits(ctx context.Context, file File, formatted string) (edits [
log.Error(ctx, "Cannot compute text edits", err) log.Error(ctx, "Cannot compute text edits", err)
return nil return nil
} }
u := diff.SplitLines(string(data)) return diff.ComputeEdits(file.URI(), string(data), formatted)
f := diff.SplitLines(formatted)
return DiffToEdits(file.URI(), diff.Operations(u, f))
} }

View File

@ -13,6 +13,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
@ -35,7 +36,7 @@ import (
// import pathpkg "path" // import pathpkg "path"
// //
// addNamedImport only returns edits that affect the import declarations. // addNamedImport only returns edits that affect the import declarations.
func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits []TextEdit, err error) { func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits []diff.TextEdit, err error) {
if alreadyImports(f, name, path) { if alreadyImports(f, name, path) {
return nil, nil return nil, nil
} }
@ -178,7 +179,7 @@ func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits
return nil, err return nil, err
} }
edits = append(edits, TextEdit{ edits = append(edits, diff.TextEdit{
Span: spn, Span: spn,
NewText: newText, NewText: newText,
}) })

View File

@ -8,6 +8,8 @@ import (
"go/parser" "go/parser"
"go/token" "go/token"
"testing" "testing"
"golang.org/x/tools/internal/lsp/diff"
) )
var fset = token.NewFileSet() var fset = token.NewFileSet()
@ -125,7 +127,7 @@ package main // Here is a comment after`,
name: "package statement multiline comments", name: "package statement multiline comments",
pkg: "os", pkg: "os",
in: `package main /* This is a multiline comment in: `package main /* This is a multiline comment
and it extends and it extends
further down*/`, further down*/`,
want: []importInfo{ want: []importInfo{
importInfo{ importInfo{
@ -137,7 +139,7 @@ further down*/`,
{ {
name: "import c", name: "import c",
pkg: "os", pkg: "os",
in: `package main in: `package main
import "C" import "C"
`, `,
@ -155,7 +157,7 @@ import "C"
{ {
name: "existing imports", name: "existing imports",
pkg: "os", pkg: "os",
in: `package main in: `package main
import "io" import "io"
`, `,
@ -173,7 +175,7 @@ import "io"
{ {
name: "existing imports with comment", name: "existing imports with comment",
pkg: "os", pkg: "os",
in: `package main in: `package main
import "io" // A comment import "io" // A comment
`, `,
@ -191,7 +193,7 @@ import "io" // A comment
{ {
name: "existing imports multiline comment", name: "existing imports multiline comment",
pkg: "os", pkg: "os",
in: `package main in: `package main
import "io" /* A comment import "io" /* A comment
that that
@ -212,7 +214,7 @@ extends */
name: "renamed import", name: "renamed import",
renamedPkg: "o", renamedPkg: "o",
pkg: "os", pkg: "os",
in: `package main in: `package main
`, `,
want: []importInfo{ want: []importInfo{
importInfo{ importInfo{
@ -314,7 +316,7 @@ func compareImports(t *testing.T, prefix string, got []*ast.ImportSpec, want []i
} }
} }
func applyEdits(contents string, edits []TextEdit) string { func applyEdits(contents string, edits []diff.TextEdit) string {
res := contents res := contents
// Apply the edits from the end of the file forward // Apply the edits from the end of the file forward

View File

@ -14,6 +14,7 @@ import (
"regexp" "regexp"
"golang.org/x/tools/go/types/typeutil" "golang.org/x/tools/go/types/typeutil"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/trace" "golang.org/x/tools/internal/telemetry/trace"
"golang.org/x/tools/refactor/satisfy" "golang.org/x/tools/refactor/satisfy"
@ -35,7 +36,7 @@ type renamer struct {
} }
// Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package. // Rename returns a map of TextEdits for each file modified when renaming a given identifier within a package.
func (i *IdentifierInfo) Rename(ctx context.Context, newName string) (map[span.URI][]TextEdit, error) { func (i *IdentifierInfo) Rename(ctx context.Context, newName string) (map[span.URI][]diff.TextEdit, error) {
ctx, done := trace.StartSpan(ctx, "source.Rename") ctx, done := trace.StartSpan(ctx, "source.Rename")
defer done() defer done()
@ -93,14 +94,14 @@ func (i *IdentifierInfo) Rename(ctx context.Context, newName string) (map[span.U
// Sort edits for each file. // Sort edits for each file.
for _, edits := range changes { for _, edits := range changes {
sortTextEdits(edits) diff.SortTextEdits(edits)
} }
return changes, nil return changes, nil
} }
// Rename all references to the identifier. // Rename all references to the identifier.
func (r *renamer) update() (map[span.URI][]TextEdit, error) { func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) {
result := make(map[span.URI][]TextEdit) result := make(map[span.URI][]diff.TextEdit)
seen := make(map[span.Span]bool) seen := make(map[span.Span]bool)
docRegexp, err := regexp.Compile(`\b` + r.from + `\b`) docRegexp, err := regexp.Compile(`\b` + r.from + `\b`)
@ -129,7 +130,7 @@ func (r *renamer) update() (map[span.URI][]TextEdit, error) {
} }
// Replace the identifier with r.to. // Replace the identifier with r.to.
edit := TextEdit{ edit := diff.TextEdit{
Span: refSpan, Span: refSpan,
NewText: r.to, NewText: r.to,
} }
@ -153,7 +154,7 @@ func (r *renamer) update() (map[span.URI][]TextEdit, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
result[spn.URI()] = append(result[spn.URI()], TextEdit{ result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
Span: spn, Span: spn,
NewText: r.to, NewText: r.to,
}) })
@ -194,7 +195,7 @@ func (r *renamer) docComment(pkg Package, id *ast.Ident) *ast.CommentGroup {
} }
// updatePkgName returns the updates to rename a pkgName in the import spec // updatePkgName returns the updates to rename a pkgName in the import spec
func (r *renamer) updatePkgName(pkgName *types.PkgName) (*TextEdit, error) { func (r *renamer) updatePkgName(pkgName *types.PkgName) (*diff.TextEdit, error) {
// Modify ImportSpec syntax to add or remove the Name as needed. // Modify ImportSpec syntax to add or remove the Name as needed.
pkg := r.packages[pkgName.Pkg()] pkg := r.packages[pkgName.Pkg()]
_, path, _ := pathEnclosingInterval(r.ctx, r.fset, pkg, pkgName.Pos(), pkgName.Pos()) _, path, _ := pathEnclosingInterval(r.ctx, r.fset, pkg, pkgName.Pos(), pkgName.Pos())
@ -229,7 +230,7 @@ func (r *renamer) updatePkgName(pkgName *types.PkgName) (*TextEdit, error) {
format.Node(&buf, r.fset, updated) format.Node(&buf, r.fset, updated)
newText := buf.String() newText := buf.String()
return &TextEdit{ return &diff.TextEdit{
Span: spn, Span: spn,
NewText: newText, NewText: newText,
}, nil }, nil

View File

@ -289,13 +289,12 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
} }
continue continue
} }
ops := source.EditsToDiff(edits)
data, _, err := f.Handle(ctx).Read(ctx) data, _, err := f.Handle(ctx).Read(ctx)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
continue continue
} }
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(data)), ops), "") got := diff.ApplyEdits(string(data), edits)
if gofmted != got { if gofmted != got {
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got)
} }
@ -331,13 +330,12 @@ func (r *runner) Import(t *testing.T, data tests.Imports) {
} }
continue continue
} }
ops := source.EditsToDiff(edits)
data, _, err := f.Handle(ctx).Read(ctx) data, _, err := f.Handle(ctx).Read(ctx)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
continue continue
} }
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(data)), ops), "") got := diff.ApplyEdits(string(data), edits)
if goimported != got { if goimported != got {
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got) t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
} }
@ -538,7 +536,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) {
} }
} }
func applyEdits(contents string, edits []source.TextEdit) string { func applyEdits(contents string, edits []diff.TextEdit) string {
res := contents res := contents
// Apply the edits from the end of the file forward // Apply the edits from the end of the file forward

View File

@ -2,7 +2,9 @@ package source
import ( import (
"go/token" "go/token"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
@ -16,7 +18,7 @@ func getCodeActions(fset *token.FileSet, diag analysis.Diagnostic) ([]SuggestedF
if err != nil { if err != nil {
return nil, err return nil, err
} }
ca.Edits = append(ca.Edits, TextEdit{span, string(te.NewText)}) ca.Edits = append(ca.Edits, diff.TextEdit{Span: span, NewText: string(te.NewText)})
} }
cas = append(cas, ca) cas = append(cas, ca)
} }

View File

@ -10,13 +10,10 @@ import (
"go/ast" "go/ast"
"go/token" "go/token"
"go/types" "go/types"
"sort"
"strings"
"golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages" "golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
@ -304,63 +301,3 @@ type Package interface {
// GetActionGraph returns the action graph for the given package. // GetActionGraph returns the action graph for the given package.
GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*Action, error) GetActionGraph(ctx context.Context, a *analysis.Analyzer) (*Action, error)
} }
// TextEdit represents a change to a section of a document.
// The text within the specified span should be replaced by the supplied new text.
type TextEdit struct {
Span span.Span
NewText string
}
// DiffToEdits converts from a sequence of diff operations to a sequence of
// source.TextEdit
func DiffToEdits(uri span.URI, ops []*diff.Op) []TextEdit {
edits := make([]TextEdit, 0, len(ops))
for _, op := range ops {
s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0))
switch op.Kind {
case diff.Delete:
// Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{Span: s})
case diff.Insert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
if content := strings.Join(op.Content, ""); content != "" {
edits = append(edits, TextEdit{Span: s, NewText: content})
}
}
}
return edits
}
func EditsToDiff(edits []TextEdit) []*diff.Op {
iToJ := 0
ops := make([]*diff.Op, len(edits))
for i, edit := range edits {
i1 := edit.Span.Start().Line() - 1
i2 := edit.Span.End().Line() - 1
kind := diff.Insert
if edit.NewText == "" {
kind = diff.Delete
}
ops[i] = &diff.Op{
Kind: kind,
Content: diff.SplitLines(edit.NewText),
I1: i1,
I2: i2,
J1: i1 + iToJ,
}
if kind == diff.Insert {
iToJ += len(ops[i].Content)
} else {
iToJ -= i2 - i1
}
}
return ops
}
func sortTextEdits(d []TextEdit) {
// Use a stable sort to maintain the order of edits inserted at the same position.
sort.SliceStable(d, func(i int, j int) bool {
return span.Compare(d[i].Span, d[j].Span) < 0
})
}