internal/lsp/diff: fix bug that adds extra line to files on format

Small changes to handle the last line in the diff library, LSP tests,
and diff to text edits conversion.

Fixes golang/go#30137

Change-Id: Iff0e53a04c2dabf6f54eb7c738b4c0837f16efba
Reviewed-on: https://go-review.googlesource.com/c/162217
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2019-02-12 13:51:16 -05:00
parent a21eef959b
commit 0e66cc6fab
5 changed files with 47 additions and 91 deletions

View File

@ -5,7 +5,9 @@
// Package diff implements the Myers diff algorithm. // Package diff implements the Myers diff algorithm.
package diff package diff
import "strings" import (
"strings"
)
// Sources: // Sources:
// https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/ // https://blog.jcoglan.com/2017/02/17/the-myers-diff-algorithm-part-3/
@ -70,6 +72,8 @@ func Operations(a, b []string) []*Op {
trace, offset := shortestEditSequence(a, b) trace, offset := shortestEditSequence(a, b)
snakes := backtrack(trace, len(a), len(b), offset) snakes := backtrack(trace, len(a), len(b), offset)
M, N := len(a), len(b)
var i int var i int
solution := make([]*Op, len(a)+len(b)) solution := make([]*Op, len(a)+len(b))
@ -101,7 +105,7 @@ func Operations(a, b []string) []*Op {
} }
} }
x++ x++
if x == len(a) { if x == M {
break break
} }
} }
@ -125,58 +129,7 @@ func Operations(a, b []string) []*Op {
x++ x++
y++ y++
} }
if x >= len(a) && y >= len(b) { if x >= M && y >= N {
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 break
} }
} }
@ -208,7 +161,6 @@ func backtrack(trace [][]int, x, y, offset int) [][]int {
x = V[kPrev+offset] x = V[kPrev+offset]
y = x - kPrev y = x - kPrev
} }
// this feels questionable
if x < 0 || y < 0 { if x < 0 || y < 0 {
return snakes return snakes
} }
@ -254,7 +206,7 @@ func shortestEditSequence(a, b []string) ([][]int, int) {
trace[d] = copyV trace[d] = copyV
// Return if we've exceeded the maximum values. // Return if we've exceeded the maximum values.
if x >= M-1 && y >= N-1 { if x == M && y == N {
return trace, offset return trace, offset
} }
} }

View File

@ -18,17 +18,6 @@ func TestDiff(t *testing.T) {
{ {
a: []string{"A", "B", "C", "A", "B", "B", "A"}, a: []string{"A", "B", "C", "A", "B", "B", "A"},
b: []string{"C", "B", "A", "B", "A", "C"}, 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{ operations: []*Op{
&Op{Kind: Delete, I1: 0, I2: 1, J1: 0, J2: 0}, &Op{Kind: Delete, I1: 0, I2: 1, J1: 0, J2: 0},
&Op{Kind: Delete, I1: 1, I2: 2, J1: 0, J2: 0}, &Op{Kind: Delete, I1: 1, I2: 2, J1: 0, J2: 0},
@ -37,27 +26,27 @@ func TestDiff(t *testing.T) {
&Op{Kind: Insert, Content: "C", I1: 7, I2: 7, J1: 5, J2: 6}, &Op{Kind: Insert, Content: "C", I1: 7, I2: 7, J1: 5, J2: 6},
}, },
}, },
{
a: []string{"A", "B"},
b: []string{"A", "C", ""},
operations: []*Op{
&Op{Kind: Delete, I1: 1, I2: 2, J1: 1, J2: 1},
&Op{Kind: Insert, Content: "C", I1: 2, I2: 2, J1: 1, J2: 2},
&Op{Kind: Insert, Content: "", I1: 2, I2: 2, J1: 2, J2: 3},
},
},
} { } {
for i, got := range Lines(tt.a, tt.b) { ops := Operations(tt.a, tt.b)
want := tt.lines[i] if len(ops) != len(tt.operations) {
if !reflect.DeepEqual(want, got) { t.Fatalf("expected %v operations, got %v", len(tt.operations), len(ops))
t.Errorf("expected %v, got %v", want, got)
}
} }
b := ApplyEdits(tt.a, tt.lines) for i, got := range ops {
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] want := tt.operations[i]
if !reflect.DeepEqual(want, got) { if !reflect.DeepEqual(want, got) {
t.Errorf("expected %v, got %v", want, got) t.Errorf("expected %v, got %v", want, got)
} }
} }
b = ApplyEdits(tt.a, tt.operations) b := ApplyEdits(tt.a, tt.operations)
for i, want := range tt.b { for i, want := range tt.b {
got := b[i] got := b[i]
if got != want { if got != want {

View File

@ -38,7 +38,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
// are being executed. If a test is added, this number must be changed. // are being executed. If a test is added, this number must be changed.
const expectedCompletionsCount = 63 const expectedCompletionsCount = 63
const expectedDiagnosticsCount = 17 const expectedDiagnosticsCount = 17
const expectedFormatCount = 3 const expectedFormatCount = 4
const expectedDefinitionsCount = 16 const expectedDefinitionsCount = 16
const expectedTypeDefinitionsCount = 2 const expectedTypeDefinitionsCount = 2
@ -380,25 +380,30 @@ func (f formats) test(t *testing.T, s *server) {
} }
var ops []*diff.Op var ops []*diff.Op
for _, edit := range edits { for _, edit := range edits {
start := int(edit.Range.Start.Line)
end := int(edit.Range.End.Line)
if start == end && edit.Range.End.Character > 1 {
end++
}
if edit.NewText == "" { // deletion if edit.NewText == "" { // deletion
ops = append(ops, &diff.Op{ ops = append(ops, &diff.Op{
Kind: diff.Delete, Kind: diff.Delete,
I1: int(edit.Range.Start.Line), I1: start,
I2: int(edit.Range.End.Line), I2: end,
}) })
} else if edit.Range.Start == edit.Range.End { // insertion } else if edit.Range.Start == edit.Range.End { // insertion
ops = append(ops, &diff.Op{ ops = append(ops, &diff.Op{
Kind: diff.Insert, Kind: diff.Insert,
Content: edit.NewText, Content: edit.NewText,
I1: int(edit.Range.Start.Line), I1: start,
I2: int(edit.Range.End.Line), I2: end,
}) })
} }
} }
split := strings.SplitAfter(string(original), "\n") split := strings.SplitAfter(string(original), "\n")
got := strings.Join(diff.ApplyEdits(split, ops), "") got := strings.Join(diff.ApplyEdits(split, ops), "")
if gofmted != got { if gofmted != got {
t.Errorf("format failed for %s: expected %v, got %v", filename, gofmted, got) t.Errorf("format failed for %s: expected '%v', got '%v'", filename, gofmted, got)
} }
} }
} }

View File

@ -82,21 +82,29 @@ func computeTextEdits(rng Range, tok *token.File, unformatted, formatted string)
u := strings.SplitAfter(unformatted, "\n") u := strings.SplitAfter(unformatted, "\n")
f := strings.SplitAfter(formatted, "\n") f := strings.SplitAfter(formatted, "\n")
for _, op := range diff.Operations(u, f) { for _, op := range diff.Operations(u, f) {
start := lineStart(tok, op.I1+1)
if start == token.NoPos && op.I1 == len(u) {
start = tok.Pos(tok.Size())
}
end := lineStart(tok, op.I2+1)
if end == token.NoPos && op.I2 == len(u) {
end = tok.Pos(tok.Size())
}
switch op.Kind { switch op.Kind {
case diff.Delete: case diff.Delete:
// Delete: unformatted[i1:i2] is deleted. // Delete: unformatted[i1:i2] is deleted.
edits = append(edits, TextEdit{ edits = append(edits, TextEdit{
Range: Range{ Range: Range{
Start: lineStart(tok, op.I1+1), Start: start,
End: lineStart(tok, op.I2+1), End: end,
}, },
}) })
case diff.Insert: case diff.Insert:
// Insert: formatted[j1:j2] is inserted at unformatted[i1:i1]. // Insert: formatted[j1:j2] is inserted at unformatted[i1:i1].
edits = append(edits, TextEdit{ edits = append(edits, TextEdit{
Range: Range{ Range: Range{
Start: lineStart(tok, op.I1+1), Start: start,
End: lineStart(tok, op.I1+1), End: start,
}, },
NewText: op.Content, NewText: op.Content,
}) })

View File

@ -0,0 +1,2 @@
package format //@format("package")
func _() {}