-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/lsp: implement diff for computing text edits
Rather than replacing the whole file on gofmt or goimports, use the Myers diff algorithm to compute diffs for a file. We send those back as text edits. Change-Id: I4f8cce5b27d51eae1911049ea002558a84cdf1bf Reviewed-on: https://go-review.googlesource.com/c/158579 Reviewed-by: Ian Cottrell <[email protected]>
- Loading branch information
1 parent
0e05534
commit 51e363b
Showing
9 changed files
with
411 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,263 @@ | ||
// 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 implements the Myers diff algorithm. | ||
package diff | ||
|
||
import "strings" | ||
|
||
// Sources: | ||
// https://blog.jcoglan.com/2017/02/15/the-myers-diff-algorithm-part-3/ | ||
// https://www.codeproject.com/Articles/42279/%2FArticles%2F42279%2FInvestigating-Myers-diff-algorithm-Part-1-of-2 | ||
|
||
type Op struct { | ||
Kind OpKind | ||
Content string | ||
I1, I2 int // indices of the line in a | ||
J1, J2 int // indices of the line in b | ||
} | ||
|
||
type OpKind int | ||
|
||
const ( | ||
Delete OpKind = iota | ||
Insert | ||
Equal | ||
) | ||
|
||
func (k OpKind) String() string { | ||
switch k { | ||
case Delete: | ||
return "delete" | ||
case Insert: | ||
return "insert" | ||
case Equal: | ||
return "equal" | ||
default: | ||
panic("unknown operation kind") | ||
} | ||
} | ||
|
||
func ApplyEdits(a []string, operations []*Op) []string { | ||
var b []string | ||
var prevI2 int | ||
for _, op := range operations { | ||
// catch up to latest indices | ||
if op.I1-prevI2 > 0 { | ||
for _, c := range a[prevI2:op.I1] { | ||
b = append(b, c) | ||
} | ||
} | ||
switch op.Kind { | ||
case Equal, Insert: | ||
b = append(b, op.Content) | ||
} | ||
prevI2 = op.I2 | ||
} | ||
// final catch up | ||
if len(a)-prevI2 > 0 { | ||
for _, c := range a[prevI2:len(a)] { | ||
b = append(b, c) | ||
} | ||
} | ||
return b | ||
} | ||
|
||
// Operations returns the list of operations to convert a into b, consolidating | ||
// operations for multiple lines and not including equal lines. | ||
func Operations(a, b []string) []*Op { | ||
trace, offset := shortestEditSequence(a, b) | ||
snakes := backtrack(trace, len(a), len(b), offset) | ||
|
||
var i int | ||
solution := make([]*Op, len(a)+len(b)) | ||
|
||
add := func(op *Op, i2, j2 int) { | ||
if op == nil { | ||
return | ||
} | ||
op.I2 = i2 | ||
op.J2 = j2 | ||
if op.Kind == Insert { | ||
op.Content = strings.Join(b[op.J1:op.J2], "") | ||
} | ||
solution[i] = op | ||
i++ | ||
} | ||
x, y := 0, 0 | ||
for _, snake := range snakes { | ||
if len(snake) < 2 { | ||
continue | ||
} | ||
var op *Op | ||
// delete (horizontal) | ||
for snake[0]-snake[1] > x-y { | ||
if op == nil { | ||
op = &Op{ | ||
Kind: Delete, | ||
I1: x, | ||
J1: y, | ||
} | ||
} | ||
x++ | ||
if x == len(a) { | ||
break | ||
} | ||
} | ||
add(op, x, y) | ||
op = nil | ||
// insert (vertical) | ||
for snake[0]-snake[1] < x-y { | ||
if op == nil { | ||
op = &Op{ | ||
Kind: Insert, | ||
I1: x, | ||
J1: y, | ||
} | ||
} | ||
y++ | ||
} | ||
add(op, x, y) | ||
op = nil | ||
// equal (diagonal) | ||
for x < snake[0] { | ||
x++ | ||
y++ | ||
} | ||
if x >= len(a) && y >= len(b) { | ||
break | ||
} | ||
} | ||
return solution[:i] | ||
} | ||
|
||
// Lines returns a list of per-line operations to convert a into b. | ||
func Lines(a, b []string) []*Op { | ||
trace, offset := shortestEditSequence(a, b) | ||
snakes := backtrack(trace, len(a), len(b), offset) | ||
|
||
var i int | ||
solution := make([]*Op, len(a)+len(b)) | ||
|
||
x, y := 0, 0 | ||
for _, snake := range snakes { | ||
if len(snake) < 2 { | ||
continue | ||
} | ||
// horizontal | ||
for snake[0]-snake[1] > x-y { | ||
solution[i] = &Op{ | ||
Kind: Delete, | ||
Content: a[x], | ||
} | ||
i++ | ||
x++ | ||
if x == len(a) { | ||
break | ||
} | ||
} | ||
// vertical | ||
for snake[0]-snake[1] < x-y { | ||
solution[i] = &Op{ | ||
Kind: Insert, | ||
Content: b[y], | ||
} | ||
i++ | ||
y++ | ||
} | ||
// diagonal | ||
for x < snake[0] { | ||
solution[i] = &Op{ | ||
Kind: Equal, | ||
Content: a[x], | ||
} | ||
i++ | ||
x++ | ||
y++ | ||
} | ||
if x >= len(a) && y >= len(b) { | ||
break | ||
} | ||
} | ||
return solution[:i] | ||
} | ||
|
||
// backtrack uses the trace for the edit sequence computation and returns the | ||
// "snakes" that make up the solution. A "snake" is a single deletion or | ||
// insertion followed by zero or diagnonals. | ||
func backtrack(trace [][]int, x, y, offset int) [][]int { | ||
snakes := make([][]int, len(trace)) | ||
d := len(trace) - 1 | ||
for ; x > 0 && y > 0 && d > 0; d-- { | ||
V := trace[d] | ||
if len(V) == 0 { | ||
continue | ||
} | ||
snakes[d] = []int{x, y} | ||
|
||
k := x - y | ||
|
||
var kPrev int | ||
if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) { | ||
kPrev = k + 1 | ||
} else { | ||
kPrev = k - 1 | ||
} | ||
|
||
x = V[kPrev+offset] | ||
y = x - kPrev | ||
} | ||
// this feels questionable | ||
if x < 0 || y < 0 { | ||
return snakes | ||
} | ||
snakes[d] = []int{x, y} | ||
return snakes | ||
} | ||
|
||
// shortestEditSequence returns the shortest edit sequence that converts a into b. | ||
func shortestEditSequence(a, b []string) ([][]int, int) { | ||
M, N := len(a), len(b) | ||
V := make([]int, 2*(N+M)+1) | ||
offset := N + M | ||
trace := make([][]int, N+M+1) | ||
|
||
// Iterate through the maximum possible length of the SES (N+M). | ||
for d := 0; d <= N+M; d++ { | ||
// k lines are represented by the equation y = x - k. We move in | ||
// increments of 2 because end points for even d are on even k lines. | ||
for k := -d; k <= d; k += 2 { | ||
// At each point, we either go down or to the right. We go down if | ||
// k == -d, and we go to the right if k == d. We also prioritize | ||
// the maximum x value, because we prefer deletions to insertions. | ||
var x int | ||
if k == -d || (k != d && V[k-1+offset] < V[k+1+offset]) { | ||
x = V[k+1+offset] // down | ||
} else { | ||
x = V[k-1+offset] + 1 // right | ||
} | ||
|
||
y := x - k | ||
|
||
// Diagonal moves while we have equal contents. | ||
for x < M && y < N && a[x] == b[y] { | ||
x++ | ||
y++ | ||
} | ||
|
||
V[k+offset] = x | ||
|
||
// Save the state of the array. | ||
copyV := make([]int, len(V)) | ||
copy(copyV, V) | ||
trace[d] = copyV | ||
|
||
// Return if we've exceeded the maximum values. | ||
if x >= M-1 && y >= N-1 { | ||
return trace, offset | ||
} | ||
} | ||
} | ||
return nil, 0 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
// 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 ( | ||
"reflect" | ||
"testing" | ||
) | ||
|
||
func TestDiff(t *testing.T) { | ||
for _, tt := range []struct { | ||
a, b []string | ||
lines []*Op | ||
operations []*Op | ||
}{ | ||
{ | ||
a: []string{"A", "B", "C", "A", "B", "B", "A"}, | ||
b: []string{"C", "B", "A", "B", "A", "C"}, | ||
lines: []*Op{ | ||
&Op{Kind: Delete, Content: "A"}, | ||
&Op{Kind: Delete, Content: "B"}, | ||
&Op{Kind: Equal, Content: "C"}, | ||
&Op{Kind: Insert, Content: "B"}, | ||
&Op{Kind: Equal, Content: "A"}, | ||
&Op{Kind: Equal, Content: "B"}, | ||
&Op{Kind: Delete, Content: "B"}, | ||
&Op{Kind: Equal, Content: "A"}, | ||
&Op{Kind: Insert, Content: "C"}, | ||
}, | ||
operations: []*Op{ | ||
&Op{Kind: Delete, I1: 0, I2: 1, J1: 0, J2: 0}, | ||
&Op{Kind: Delete, I1: 1, I2: 2, J1: 0, J2: 0}, | ||
&Op{Kind: Insert, Content: "B", I1: 3, I2: 3, J1: 1, J2: 2}, | ||
&Op{Kind: Delete, I1: 5, I2: 6, J1: 4, J2: 4}, | ||
&Op{Kind: Insert, Content: "C", I1: 7, I2: 7, J1: 5, J2: 6}, | ||
}, | ||
}, | ||
} { | ||
for i, got := range Lines(tt.a, tt.b) { | ||
want := tt.lines[i] | ||
if !reflect.DeepEqual(want, got) { | ||
t.Errorf("expected %v, got %v", want, got) | ||
} | ||
} | ||
b := ApplyEdits(tt.a, tt.lines) | ||
for i, want := range tt.b { | ||
got := b[i] | ||
if got != want { | ||
t.Errorf("expected %v got %v", want, got) | ||
} | ||
} | ||
for i, got := range Operations(tt.a, tt.b) { | ||
want := tt.operations[i] | ||
if !reflect.DeepEqual(want, got) { | ||
t.Errorf("expected %v, got %v", want, got) | ||
} | ||
} | ||
b = ApplyEdits(tt.a, tt.operations) | ||
for i, want := range tt.b { | ||
got := b[i] | ||
if got != want { | ||
t.Errorf("expected %v got %v", want, got) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.