1
0
mirror of https://github.com/golang/go synced 2024-09-30 20:28:32 -06:00

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@ package lsp
import (
"context"
"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"
@ -51,7 +52,7 @@ func spanToRange(ctx context.Context, view source.View, spn span.Span) (source.G
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 {
return nil, nil
}
@ -69,17 +70,17 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]proto
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 {
return nil, nil
}
result := make([]source.TextEdit, len(edits))
result := make([]diff.TextEdit, len(edits))
for i, edit := range edits {
spn, err := m.RangeSpan(edit.Range)
if err != nil {
return nil, err
}
result[i] = source.TextEdit{
result[i] = diff.TextEdit{
Span: spn,
NewText: edit.NewText,
}

View File

@ -287,8 +287,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
if err != nil {
t.Error(err)
}
ops := source.EditsToDiff(sedits)
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
got := diff.ApplyEdits(string(m.Content), sedits)
if 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 {
t.Error(err)
}
ops := source.EditsToDiff(sedits)
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
got := diff.ApplyEdits(string(m.Content), sedits)
if 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
// 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/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/fuzzy"
"golang.org/x/tools/internal/lsp/snippet"
"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
// (for example adding an import statement at the top of the file if the completion item will
// insert an unqualified type).
AdditionalTextEdits []TextEdit
AdditionalTextEdits []diff.TextEdit
// Depth is how many levels were searched to find this completion.
// For example when completing "foo<>", "fooBar" is depth 0, and

View File

@ -13,6 +13,7 @@ import (
"go/types"
"strings"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/lsp/snippet"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
@ -35,7 +36,7 @@ func (c *completer) item(cand candidate) (CompletionItem, error) {
kind CompletionItemKind
plainSnippet *snippet.Builder
placeholderSnippet *snippet.Builder
addlEdits []TextEdit
addlEdits []diff.TextEdit
)
// 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/unusedresult"
"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/telemetry"
"golang.org/x/tools/internal/span"
@ -55,7 +56,7 @@ type Diagnostic struct {
type SuggestedFixes struct {
Title string
Edits []TextEdit
Edits []diff.TextEdit
}
type DiagnosticSeverity int

View File

@ -21,7 +21,7 @@ import (
)
// 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")
defer done()
@ -74,7 +74,7 @@ func formatSource(ctx context.Context, file File) ([]byte, error) {
}
// 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")
defer done()
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 {
Fix *imports.ImportFix
Edits []TextEdit
Edits []diff.TextEdit
}
// AllImportsFixes formats f for each possible fix to the imports.
// 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
// 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")
defer done()
data, _, err := f.Handle(ctx).Read(ctx)
@ -224,7 +224,7 @@ func hasListErrors(errors []packages.Error) bool {
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")
defer done()
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)
return nil
}
u := diff.SplitLines(string(data))
f := diff.SplitLines(formatted)
return DiffToEdits(file.URI(), diff.Operations(u, f))
return diff.ComputeEdits(file.URI(), string(data), formatted)
}

View File

@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span"
)
@ -35,7 +36,7 @@ import (
// import pathpkg "path"
//
// 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) {
return nil, nil
}
@ -178,7 +179,7 @@ func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits
return nil, err
}
edits = append(edits, TextEdit{
edits = append(edits, diff.TextEdit{
Span: spn,
NewText: newText,
})

View File

@ -8,6 +8,8 @@ import (
"go/parser"
"go/token"
"testing"
"golang.org/x/tools/internal/lsp/diff"
)
var fset = token.NewFileSet()
@ -125,7 +127,7 @@ package main // Here is a comment after`,
name: "package statement multiline comments",
pkg: "os",
in: `package main /* This is a multiline comment
and it extends
and it extends
further down*/`,
want: []importInfo{
importInfo{
@ -137,7 +139,7 @@ further down*/`,
{
name: "import c",
pkg: "os",
in: `package main
in: `package main
import "C"
`,
@ -155,7 +157,7 @@ import "C"
{
name: "existing imports",
pkg: "os",
in: `package main
in: `package main
import "io"
`,
@ -173,7 +175,7 @@ import "io"
{
name: "existing imports with comment",
pkg: "os",
in: `package main
in: `package main
import "io" // A comment
`,
@ -191,7 +193,7 @@ import "io" // A comment
{
name: "existing imports multiline comment",
pkg: "os",
in: `package main
in: `package main
import "io" /* A comment
that
@ -212,7 +214,7 @@ extends */
name: "renamed import",
renamedPkg: "o",
pkg: "os",
in: `package main
in: `package main
`,
want: []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
// Apply the edits from the end of the file forward

View File

@ -14,6 +14,7 @@ import (
"regexp"
"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/telemetry/trace"
"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.
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")
defer done()
@ -93,14 +94,14 @@ func (i *IdentifierInfo) Rename(ctx context.Context, newName string) (map[span.U
// Sort edits for each file.
for _, edits := range changes {
sortTextEdits(edits)
diff.SortTextEdits(edits)
}
return changes, nil
}
// Rename all references to the identifier.
func (r *renamer) update() (map[span.URI][]TextEdit, error) {
result := make(map[span.URI][]TextEdit)
func (r *renamer) update() (map[span.URI][]diff.TextEdit, error) {
result := make(map[span.URI][]diff.TextEdit)
seen := make(map[span.Span]bool)
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.
edit := TextEdit{
edit := diff.TextEdit{
Span: refSpan,
NewText: r.to,
}
@ -153,7 +154,7 @@ func (r *renamer) update() (map[span.URI][]TextEdit, error) {
if err != nil {
return nil, err
}
result[spn.URI()] = append(result[spn.URI()], TextEdit{
result[spn.URI()] = append(result[spn.URI()], diff.TextEdit{
Span: spn,
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
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.
pkg := r.packages[pkgName.Pkg()]
_, 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)
newText := buf.String()
return &TextEdit{
return &diff.TextEdit{
Span: spn,
NewText: newText,
}, nil

View File

@ -289,13 +289,12 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
}
continue
}
ops := source.EditsToDiff(edits)
data, _, err := f.Handle(ctx).Read(ctx)
if err != nil {
t.Error(err)
continue
}
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(data)), ops), "")
got := diff.ApplyEdits(string(data), edits)
if 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
}
ops := source.EditsToDiff(edits)
data, _, err := f.Handle(ctx).Read(ctx)
if err != nil {
t.Error(err)
continue
}
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(data)), ops), "")
got := diff.ApplyEdits(string(data), edits)
if 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
// Apply the edits from the end of the file forward

View File

@ -2,7 +2,9 @@ package source
import (
"go/token"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span"
)
@ -16,7 +18,7 @@ func getCodeActions(fset *token.FileSet, diag analysis.Diagnostic) ([]SuggestedF
if err != nil {
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)
}

View File

@ -10,13 +10,10 @@ import (
"go/ast"
"go/token"
"go/types"
"sort"
"strings"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span"
)
@ -304,63 +301,3 @@ type Package interface {
// GetActionGraph returns the action graph for the given package.
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
})
}