Skip to content

Commit 85edb9e

Browse files
Ian Cottrellianthehat
authored andcommitted
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Rebecca Stambler <[email protected]>
1 parent d9ab56a commit 85edb9e

18 files changed

+192
-143
lines changed

internal/lsp/cmd/format.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,10 @@ import (
99
"flag"
1010
"fmt"
1111
"io/ioutil"
12-
"strings"
1312

1413
"golang.org/x/tools/internal/lsp"
1514
"golang.org/x/tools/internal/lsp/diff"
1615
"golang.org/x/tools/internal/lsp/protocol"
17-
"golang.org/x/tools/internal/lsp/source"
1816
"golang.org/x/tools/internal/span"
1917
errors "golang.org/x/xerrors"
2018
)
@@ -82,9 +80,7 @@ func (f *format) Run(ctx context.Context, args ...string) error {
8280
if err != nil {
8381
return errors.Errorf("%v: %v", spn, err)
8482
}
85-
ops := source.EditsToDiff(sedits)
86-
lines := diff.SplitLines(string(file.mapper.Content))
87-
formatted := strings.Join(diff.ApplyEdits(lines, ops), "")
83+
formatted := diff.ApplyEdits(string(file.mapper.Content), sedits)
8884
printIt := true
8985
if f.List {
9086
printIt = false
@@ -100,7 +96,7 @@ func (f *format) Run(ctx context.Context, args ...string) error {
10096
}
10197
if f.Diff {
10298
printIt = false
103-
u := diff.ToUnified(filename+".orig", filename, lines, ops)
99+
u := diff.ToUnified(filename+".orig", filename, string(file.mapper.Content), sedits)
104100
fmt.Print(u)
105101
}
106102
if printIt {

internal/lsp/diff/hooks.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 diff supports a pluggable diff algorithm.
6+
package diff
7+
8+
import (
9+
"sort"
10+
11+
"golang.org/x/tools/internal/span"
12+
)
13+
14+
// TextEdit represents a change to a section of a document.
15+
// The text within the specified span should be replaced by the supplied new text.
16+
type TextEdit struct {
17+
Span span.Span
18+
NewText string
19+
}
20+
21+
var (
22+
ComputeEdits func(uri span.URI, before, after string) []TextEdit
23+
ApplyEdits func(before string, edits []TextEdit) string
24+
ToUnified func(from, to string, before string, edits []TextEdit) string
25+
)
26+
27+
func SortTextEdits(d []TextEdit) {
28+
// Use a stable sort to maintain the order of edits inserted at the same position.
29+
sort.SliceStable(d, func(i int, j int) bool {
30+
return span.Compare(d[i].Span, d[j].Span) < 0
31+
})
32+
}

internal/lsp/diff/myers.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 diff
6+
7+
import (
8+
"fmt"
9+
"strings"
10+
11+
"golang.org/x/tools/internal/lsp/diff/myers"
12+
"golang.org/x/tools/internal/span"
13+
)
14+
15+
func init() {
16+
ComputeEdits = myersComputeEdits
17+
ApplyEdits = myersApplyEdits
18+
ToUnified = myersToUnified
19+
}
20+
21+
func myersComputeEdits(uri span.URI, before, after string) []TextEdit {
22+
u := myers.SplitLines(before)
23+
f := myers.SplitLines(after)
24+
return myersDiffToEdits(uri, myers.Operations(u, f))
25+
}
26+
27+
func myersApplyEdits(before string, edits []TextEdit) string {
28+
ops := myersEditsToDiff(edits)
29+
return strings.Join(myers.ApplyEdits(myers.SplitLines(before), ops), "")
30+
}
31+
32+
func myersToUnified(from, to string, before string, edits []TextEdit) string {
33+
u := myers.SplitLines(before)
34+
ops := myersEditsToDiff(edits)
35+
return fmt.Sprint(myers.ToUnified(from, to, u, ops))
36+
}
37+
38+
func myersDiffToEdits(uri span.URI, ops []*myers.Op) []TextEdit {
39+
edits := make([]TextEdit, 0, len(ops))
40+
for _, op := range ops {
41+
s := span.New(uri, span.NewPoint(op.I1+1, 1, 0), span.NewPoint(op.I2+1, 1, 0))
42+
switch op.Kind {
43+
case myers.Delete:
44+
// Delete: unformatted[i1:i2] is deleted.
45+
edits = append(edits, TextEdit{Span: s})
46+
case myers.Insert:
47+
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
48+
if content := strings.Join(op.Content, ""); content != "" {
49+
edits = append(edits, TextEdit{Span: s, NewText: content})
50+
}
51+
}
52+
}
53+
return edits
54+
}
55+
56+
func myersEditsToDiff(edits []TextEdit) []*myers.Op {
57+
iToJ := 0
58+
ops := make([]*myers.Op, len(edits))
59+
for i, edit := range edits {
60+
i1 := edit.Span.Start().Line() - 1
61+
i2 := edit.Span.End().Line() - 1
62+
kind := myers.Insert
63+
if edit.NewText == "" {
64+
kind = myers.Delete
65+
}
66+
ops[i] = &myers.Op{
67+
Kind: kind,
68+
Content: myers.SplitLines(edit.NewText),
69+
I1: i1,
70+
I2: i2,
71+
J1: i1 + iToJ,
72+
}
73+
if kind == myers.Insert {
74+
iToJ += len(ops[i].Content)
75+
} else {
76+
iToJ -= i2 - i1
77+
}
78+
}
79+
return ops
80+
}

internal/lsp/diff/diff.go renamed to internal/lsp/diff/myers/diff.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
// Package diff implements the Myers diff algorithm.
6-
package diff
5+
// Package myers implements the Myers diff algorithm.
6+
package myers
77

88
import "strings"
99

internal/lsp/diff/diff_test.go renamed to internal/lsp/diff/myers/diff_test.go

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package diff_test
5+
package myers_test
66

77
import (
88
"flag"
@@ -14,7 +14,7 @@ import (
1414
"strings"
1515
"testing"
1616

17-
"golang.org/x/tools/internal/lsp/diff"
17+
"golang.org/x/tools/internal/lsp/diff/myers"
1818
)
1919

2020
const (
@@ -28,22 +28,22 @@ var verifyDiff = flag.Bool("verify-diff", false, "Check that the unified diff ou
2828
func TestDiff(t *testing.T) {
2929
for _, test := range []struct {
3030
a, b string
31-
lines []*diff.Op
32-
operations []*diff.Op
31+
lines []*myers.Op
32+
operations []*myers.Op
3333
unified string
3434
nodiff bool
3535
}{
3636
{
3737
a: "A\nB\nC\n",
3838
b: "A\nB\nC\n",
39-
operations: []*diff.Op{},
39+
operations: []*myers.Op{},
4040
unified: `
4141
`[1:]}, {
4242
a: "A\n",
4343
b: "B\n",
44-
operations: []*diff.Op{
45-
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0},
46-
&diff.Op{Kind: diff.Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0},
44+
operations: []*myers.Op{
45+
&myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
46+
&myers.Op{Kind: myers.Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0},
4747
},
4848
unified: `
4949
@@ -1 +1 @@
@@ -52,9 +52,9 @@ func TestDiff(t *testing.T) {
5252
`[1:]}, {
5353
a: "A",
5454
b: "B",
55-
operations: []*diff.Op{
56-
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0},
57-
&diff.Op{Kind: diff.Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0},
55+
operations: []*myers.Op{
56+
&myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
57+
&myers.Op{Kind: myers.Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0},
5858
},
5959
unified: `
6060
@@ -1 +1 @@
@@ -65,12 +65,12 @@ func TestDiff(t *testing.T) {
6565
`[1:]}, {
6666
a: "A\nB\nC\nA\nB\nB\nA\n",
6767
b: "C\nB\nA\nB\nA\nC\n",
68-
operations: []*diff.Op{
69-
&diff.Op{Kind: diff.Delete, I1: 0, I2: 1, J1: 0},
70-
&diff.Op{Kind: diff.Delete, I1: 1, I2: 2, J1: 0},
71-
&diff.Op{Kind: diff.Insert, Content: []string{"B\n"}, I1: 3, I2: 3, J1: 1},
72-
&diff.Op{Kind: diff.Delete, I1: 5, I2: 6, J1: 4},
73-
&diff.Op{Kind: diff.Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
68+
operations: []*myers.Op{
69+
&myers.Op{Kind: myers.Delete, I1: 0, I2: 1, J1: 0},
70+
&myers.Op{Kind: myers.Delete, I1: 1, I2: 2, J1: 0},
71+
&myers.Op{Kind: myers.Insert, Content: []string{"B\n"}, I1: 3, I2: 3, J1: 1},
72+
&myers.Op{Kind: myers.Delete, I1: 5, I2: 6, J1: 4},
73+
&myers.Op{Kind: myers.Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
7474
},
7575
unified: `
7676
@@ -1,7 +1,6 @@
@@ -89,10 +89,10 @@ func TestDiff(t *testing.T) {
8989
{
9090
a: "A\nB\n",
9191
b: "A\nC\n\n",
92-
operations: []*diff.Op{
93-
&diff.Op{Kind: diff.Delete, I1: 1, I2: 2, J1: 1},
94-
&diff.Op{Kind: diff.Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
95-
&diff.Op{Kind: diff.Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
92+
operations: []*myers.Op{
93+
&myers.Op{Kind: myers.Delete, I1: 1, I2: 2, J1: 1},
94+
&myers.Op{Kind: myers.Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
95+
&myers.Op{Kind: myers.Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
9696
},
9797
unified: `
9898
@@ -1,2 +1,3 @@
@@ -120,9 +120,9 @@ func TestDiff(t *testing.T) {
120120
+K
121121
`[1:]},
122122
} {
123-
a := diff.SplitLines(test.a)
124-
b := diff.SplitLines(test.b)
125-
ops := diff.Operations(a, b)
123+
a := myers.SplitLines(test.a)
124+
b := myers.SplitLines(test.b)
125+
ops := myers.Operations(a, b)
126126
if test.operations != nil {
127127
if len(ops) != len(test.operations) {
128128
t.Fatalf("expected %v operations, got %v", len(test.operations), len(ops))
@@ -134,15 +134,15 @@ func TestDiff(t *testing.T) {
134134
}
135135
}
136136
}
137-
applied := diff.ApplyEdits(a, ops)
137+
applied := myers.ApplyEdits(a, ops)
138138
for i, want := range applied {
139139
got := b[i]
140140
if got != want {
141141
t.Errorf("expected %v got %v", want, got)
142142
}
143143
}
144144
if test.unified != "" {
145-
diff := diff.ToUnified(fileA, fileB, a, ops)
145+
diff := myers.ToUnified(fileA, fileB, a, ops)
146146
got := fmt.Sprint(diff)
147147
if !strings.HasPrefix(got, unifiedPrefix) {
148148
t.Errorf("expected prefix:\n%s\ngot:\n%s", unifiedPrefix, got)
@@ -166,7 +166,7 @@ func TestDiff(t *testing.T) {
166166
}
167167

168168
func getDiffOutput(a, b string) (string, error) {
169-
fileA, err := ioutil.TempFile("", "diff.in")
169+
fileA, err := ioutil.TempFile("", "myers.in")
170170
if err != nil {
171171
return "", err
172172
}
@@ -177,7 +177,7 @@ func getDiffOutput(a, b string) (string, error) {
177177
if err := fileA.Close(); err != nil {
178178
return "", err
179179
}
180-
fileB, err := ioutil.TempFile("", "diff.in")
180+
fileB, err := ioutil.TempFile("", "myers.in")
181181
if err != nil {
182182
return "", err
183183
}

internal/lsp/diff/unified.go renamed to internal/lsp/diff/myers/unified.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5-
package diff
5+
package myers
66

77
import (
88
"fmt"

internal/lsp/format.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package lsp
77
import (
88
"context"
99

10+
"golang.org/x/tools/internal/lsp/diff"
1011
"golang.org/x/tools/internal/lsp/protocol"
1112
"golang.org/x/tools/internal/lsp/source"
1213
"golang.org/x/tools/internal/span"
@@ -51,7 +52,7 @@ func spanToRange(ctx context.Context, view source.View, spn span.Span) (source.G
5152
return f, m, rng, nil
5253
}
5354

54-
func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]protocol.TextEdit, error) {
55+
func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) {
5556
if edits == nil {
5657
return nil, nil
5758
}
@@ -69,17 +70,17 @@ func ToProtocolEdits(m *protocol.ColumnMapper, edits []source.TextEdit) ([]proto
6970
return result, nil
7071
}
7172

72-
func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]source.TextEdit, error) {
73+
func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) {
7374
if edits == nil {
7475
return nil, nil
7576
}
76-
result := make([]source.TextEdit, len(edits))
77+
result := make([]diff.TextEdit, len(edits))
7778
for i, edit := range edits {
7879
spn, err := m.RangeSpan(edit.Range)
7980
if err != nil {
8081
return nil, err
8182
}
82-
result[i] = source.TextEdit{
83+
result[i] = diff.TextEdit{
8384
Span: spn,
8485
NewText: edit.NewText,
8586
}

internal/lsp/lsp_test.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,7 @@ func (r *runner) Format(t *testing.T, data tests.Formats) {
287287
if err != nil {
288288
t.Error(err)
289289
}
290-
ops := source.EditsToDiff(sedits)
291-
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
290+
got := diff.ApplyEdits(string(m.Content), sedits)
292291
if gofmted != got {
293292
t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got)
294293
}
@@ -334,8 +333,7 @@ func (r *runner) Import(t *testing.T, data tests.Imports) {
334333
if err != nil {
335334
t.Error(err)
336335
}
337-
ops := source.EditsToDiff(sedits)
338-
got := strings.Join(diff.ApplyEdits(diff.SplitLines(string(m.Content)), ops), "")
336+
got := diff.ApplyEdits(string(m.Content), sedits)
339337
if goimported != got {
340338
t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got)
341339
}
@@ -549,7 +547,7 @@ func (r *runner) Rename(t *testing.T, data tests.Renames) {
549547
}
550548
}
551549

552-
func applyEdits(contents string, edits []source.TextEdit) string {
550+
func applyEdits(contents string, edits []diff.TextEdit) string {
553551
res := contents
554552

555553
// Apply the edits from the end of the file forward

internal/lsp/source/completion.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"golang.org/x/tools/go/ast/astutil"
1515
"golang.org/x/tools/internal/imports"
16+
"golang.org/x/tools/internal/lsp/diff"
1617
"golang.org/x/tools/internal/lsp/fuzzy"
1718
"golang.org/x/tools/internal/lsp/snippet"
1819
"golang.org/x/tools/internal/span"
@@ -41,7 +42,7 @@ type CompletionItem struct {
4142
// Additional text edits should be used to change text unrelated to the current cursor position
4243
// (for example adding an import statement at the top of the file if the completion item will
4344
// insert an unqualified type).
44-
AdditionalTextEdits []TextEdit
45+
AdditionalTextEdits []diff.TextEdit
4546

4647
// Depth is how many levels were searched to find this completion.
4748
// For example when completing "foo<>", "fooBar" is depth 0, and

0 commit comments

Comments
 (0)