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>
diff --git a/internal/lsp/cmd/format.go b/internal/lsp/cmd/format.go
index 93d041d..bbe9f7a 100644
--- a/internal/lsp/cmd/format.go
+++ b/internal/lsp/cmd/format.go
@@ -9,12 +9,10 @@
 	"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 @@
 		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 @@
 		}
 		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 {
diff --git a/internal/lsp/diff/hooks.go b/internal/lsp/diff/hooks.go
new file mode 100644
index 0000000..16c51d9
--- /dev/null
+++ b/internal/lsp/diff/hooks.go
@@ -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
+	})
+}
diff --git a/internal/lsp/diff/myers.go b/internal/lsp/diff/myers.go
new file mode 100644
index 0000000..eab3e2c
--- /dev/null
+++ b/internal/lsp/diff/myers.go
@@ -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
+}
diff --git a/internal/lsp/diff/diff.go b/internal/lsp/diff/myers/diff.go
similarity index 98%
rename from internal/lsp/diff/diff.go
rename to internal/lsp/diff/myers/diff.go
index cf7158d..ddf04ca 100644
--- a/internal/lsp/diff/diff.go
+++ b/internal/lsp/diff/myers/diff.go
@@ -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"
 
diff --git a/internal/lsp/diff/diff_test.go b/internal/lsp/diff/myers/diff_test.go
similarity index 70%
rename from internal/lsp/diff/diff_test.go
rename to internal/lsp/diff/myers/diff_test.go
index 1fe4292..f60f73b 100644
--- a/internal/lsp/diff/diff_test.go
+++ b/internal/lsp/diff/myers/diff_test.go
@@ -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 @@
 	"strings"
 	"testing"
 
-	"golang.org/x/tools/internal/lsp/diff"
+	"golang.org/x/tools/internal/lsp/diff/myers"
 )
 
 const (
@@ -28,22 +28,22 @@
 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 @@
 `[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 @@
 `[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 @@
 		{
 			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 @@
 +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 @@
 				}
 			}
 		}
-		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 @@
 			}
 		}
 		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 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 @@
 	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
 	}
diff --git a/internal/lsp/diff/unified.go b/internal/lsp/diff/myers/unified.go
similarity index 99%
rename from internal/lsp/diff/unified.go
rename to internal/lsp/diff/myers/unified.go
index 427a871..8339b10 100644
--- a/internal/lsp/diff/unified.go
+++ b/internal/lsp/diff/myers/unified.go
@@ -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"
diff --git a/internal/lsp/format.go b/internal/lsp/format.go
index 6e5f400..95c9fd3 100644
--- a/internal/lsp/format.go
+++ b/internal/lsp/format.go
@@ -7,6 +7,7 @@
 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 @@
 	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 @@
 	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,
 		}
diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go
index 248f293..59dbe0b 100644
--- a/internal/lsp/lsp_test.go
+++ b/internal/lsp/lsp_test.go
@@ -287,8 +287,7 @@
 		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 @@
 		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 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
diff --git a/internal/lsp/source/completion.go b/internal/lsp/source/completion.go
index 762195b..4506ce0 100644
--- a/internal/lsp/source/completion.go
+++ b/internal/lsp/source/completion.go
@@ -13,6 +13,7 @@
 
 	"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 @@
 	// 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
diff --git a/internal/lsp/source/completion_format.go b/internal/lsp/source/completion_format.go
index 3d02feb..115120d 100644
--- a/internal/lsp/source/completion_format.go
+++ b/internal/lsp/source/completion_format.go
@@ -13,6 +13,7 @@
 	"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 @@
 		kind               CompletionItemKind
 		plainSnippet       *snippet.Builder
 		placeholderSnippet *snippet.Builder
-		addlEdits          []TextEdit
+		addlEdits          []diff.TextEdit
 	)
 
 	// expandFuncCall mutates the completion label, detail, and snippets
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index 442ae78..5829209 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -35,6 +35,7 @@
 	"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 SuggestedFixes struct {
 	Title string
-	Edits []TextEdit
+	Edits []diff.TextEdit
 }
 
 type DiagnosticSeverity int
diff --git a/internal/lsp/source/format.go b/internal/lsp/source/format.go
index 9141869..a1448cd 100644
--- a/internal/lsp/source/format.go
+++ b/internal/lsp/source/format.go
@@ -21,7 +21,7 @@
 )
 
 // 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 @@
 }
 
 // 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 @@
 
 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 @@
 	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 @@
 		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)
 }
diff --git a/internal/lsp/source/imports.go b/internal/lsp/source/imports.go
index 1e80d10..22121d6 100644
--- a/internal/lsp/source/imports.go
+++ b/internal/lsp/source/imports.go
@@ -13,6 +13,7 @@
 	"strconv"
 	"strings"
 
+	"golang.org/x/tools/internal/lsp/diff"
 	"golang.org/x/tools/internal/span"
 )
 
@@ -35,7 +36,7 @@
 //	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 @@
 		return nil, err
 	}
 
-	edits = append(edits, TextEdit{
+	edits = append(edits, diff.TextEdit{
 		Span:    spn,
 		NewText: newText,
 	})
diff --git a/internal/lsp/source/imports_test.go b/internal/lsp/source/imports_test.go
index 2d054db..e10c082 100644
--- a/internal/lsp/source/imports_test.go
+++ b/internal/lsp/source/imports_test.go
@@ -8,6 +8,8 @@
 	"go/parser"
 	"go/token"
 	"testing"
+
+	"golang.org/x/tools/internal/lsp/diff"
 )
 
 var fset = token.NewFileSet()
@@ -125,7 +127,7 @@
 		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 @@
 	{
 		name: "import c",
 		pkg:  "os",
-		in: `package main 
+		in: `package main
 
 import "C"
 `,
@@ -155,7 +157,7 @@
 	{
 		name: "existing imports",
 		pkg:  "os",
-		in: `package main 
+		in: `package main
 
 import "io"
 `,
@@ -173,7 +175,7 @@
 	{
 		name: "existing imports with comment",
 		pkg:  "os",
-		in: `package main 
+		in: `package main
 
 import "io" // A comment
 `,
@@ -191,7 +193,7 @@
 	{
 		name: "existing imports multiline comment",
 		pkg:  "os",
-		in: `package main 
+		in: `package main
 
 import "io" /* A comment
 that
@@ -212,7 +214,7 @@
 		name:       "renamed import",
 		renamedPkg: "o",
 		pkg:        "os",
-		in: `package main 
+		in: `package main
 `,
 		want: []importInfo{
 			importInfo{
@@ -314,7 +316,7 @@
 	}
 }
 
-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
diff --git a/internal/lsp/source/rename.go b/internal/lsp/source/rename.go
index 7164f81..56e6dfa 100644
--- a/internal/lsp/source/rename.go
+++ b/internal/lsp/source/rename.go
@@ -14,6 +14,7 @@
 	"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 @@
 }
 
 // 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 @@
 
 	// 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 @@
 		}
 
 		// Replace the identifier with r.to.
-		edit := TextEdit{
+		edit := diff.TextEdit{
 			Span:    refSpan,
 			NewText: r.to,
 		}
@@ -153,7 +154,7 @@
 				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 @@
 }
 
 // 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 @@
 	format.Node(&buf, r.fset, updated)
 	newText := buf.String()
 
-	return &TextEdit{
+	return &diff.TextEdit{
 		Span:    spn,
 		NewText: newText,
 	}, nil
diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go
index 16d92f2..854edaa 100644
--- a/internal/lsp/source/source_test.go
+++ b/internal/lsp/source/source_test.go
@@ -289,13 +289,12 @@
 			}
 			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 @@
 			}
 			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 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
diff --git a/internal/lsp/source/suggested_fix.go b/internal/lsp/source/suggested_fix.go
index a433723..0e4d847 100644
--- a/internal/lsp/source/suggested_fix.go
+++ b/internal/lsp/source/suggested_fix.go
@@ -2,7 +2,9 @@
 
 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 @@
 			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)
 	}
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index 3a0fb8f..4a0c073 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -10,13 +10,10 @@
 	"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 @@
 	// 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
-	})
-}