mirror of
https://github.com/golang/go
synced 2024-11-18 17:44:47 -07:00
internal/lsp: add unified diff
This adds unified diff handling to the diff package. It also adds a lot more testing and also verifies the unified diff tests against the diff program if you run the tests with the flag -verify-diff This functionality is needed for some of the command verbs. Change-Id: I817438fd25c0b16f3f31578f51a886944e74a948 Reviewed-on: https://go-review.googlesource.com/c/tools/+/171024 Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
parent
2b5498619e
commit
fa491999fb
@ -5,18 +5,62 @@
|
|||||||
package diff
|
package diff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileA = "a/a.go"
|
||||||
|
fileB = "b/b.go"
|
||||||
|
unifiedPrefix = "--- " + fileA + "\n+++ " + fileB + "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
var verifyDiff = flag.Bool("verify-diff", false, "Check that the unified diff output matches `diff -u`")
|
||||||
|
|
||||||
func TestDiff(t *testing.T) {
|
func TestDiff(t *testing.T) {
|
||||||
for _, test := range []struct {
|
for _, test := range []struct {
|
||||||
a, b string
|
a, b string
|
||||||
lines []*Op
|
lines []*Op
|
||||||
operations []*Op
|
operations []*Op
|
||||||
|
unified string
|
||||||
|
nodiff bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
|
a: "A\nB\nC\n",
|
||||||
|
b: "A\nB\nC\n",
|
||||||
|
operations: []*Op{},
|
||||||
|
unified: `
|
||||||
|
`[1:]}, {
|
||||||
|
a: "A\n",
|
||||||
|
b: "B\n",
|
||||||
|
operations: []*Op{
|
||||||
|
&Op{Kind: Delete, I1: 0, I2: 1, J1: 0},
|
||||||
|
&Op{Kind: Insert, Content: []string{"B\n"}, I1: 1, I2: 1, J1: 0},
|
||||||
|
},
|
||||||
|
unified: `
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-A
|
||||||
|
+B
|
||||||
|
`[1:]}, {
|
||||||
|
a: "A",
|
||||||
|
b: "B",
|
||||||
|
operations: []*Op{
|
||||||
|
&Op{Kind: Delete, I1: 0, I2: 1, J1: 0},
|
||||||
|
&Op{Kind: Insert, Content: []string{"B"}, I1: 1, I2: 1, J1: 0},
|
||||||
|
},
|
||||||
|
unified: `
|
||||||
|
@@ -1 +1 @@
|
||||||
|
-A
|
||||||
|
\ No newline at end of file
|
||||||
|
+B
|
||||||
|
\ No newline at end of file
|
||||||
|
`[1:]}, {
|
||||||
a: "A\nB\nC\nA\nB\nB\nA\n",
|
a: "A\nB\nC\nA\nB\nB\nA\n",
|
||||||
b: "C\nB\nA\nB\nA\nC\n",
|
b: "C\nB\nA\nB\nA\nC\n",
|
||||||
operations: []*Op{
|
operations: []*Op{
|
||||||
@ -26,6 +70,19 @@ func TestDiff(t *testing.T) {
|
|||||||
&Op{Kind: Delete, I1: 5, I2: 6, J1: 4},
|
&Op{Kind: Delete, I1: 5, I2: 6, J1: 4},
|
||||||
&Op{Kind: Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
|
&Op{Kind: Insert, Content: []string{"C\n"}, I1: 7, I2: 7, J1: 5},
|
||||||
},
|
},
|
||||||
|
unified: `
|
||||||
|
@@ -1,7 +1,6 @@
|
||||||
|
-A
|
||||||
|
-B
|
||||||
|
C
|
||||||
|
+B
|
||||||
|
A
|
||||||
|
B
|
||||||
|
-B
|
||||||
|
A
|
||||||
|
+C
|
||||||
|
`[1:],
|
||||||
|
nodiff: true, // diff algorithm produces different delete/insert pattern
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
a: "A\nB\n",
|
a: "A\nB\n",
|
||||||
@ -35,26 +92,111 @@ func TestDiff(t *testing.T) {
|
|||||||
&Op{Kind: Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
|
&Op{Kind: Insert, Content: []string{"C\n"}, I1: 2, I2: 2, J1: 1},
|
||||||
&Op{Kind: Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
|
&Op{Kind: Insert, Content: []string{"\n"}, I1: 2, I2: 2, J1: 2},
|
||||||
},
|
},
|
||||||
|
unified: `
|
||||||
|
@@ -1,2 +1,3 @@
|
||||||
|
A
|
||||||
|
-B
|
||||||
|
+C
|
||||||
|
+
|
||||||
|
`[1:],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
a: "A\nB\nC\nD\nE\nF\nG\n",
|
||||||
|
b: "A\nH\nI\nJ\nE\nF\nK\n",
|
||||||
|
unified: `
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
A
|
||||||
|
-B
|
||||||
|
-C
|
||||||
|
-D
|
||||||
|
+H
|
||||||
|
+I
|
||||||
|
+J
|
||||||
|
E
|
||||||
|
F
|
||||||
|
-G
|
||||||
|
+K
|
||||||
|
`[1:]},
|
||||||
} {
|
} {
|
||||||
a := strings.SplitAfter(test.a, "\n")
|
a := strings.SplitAfter(test.a, "\n")
|
||||||
b := strings.SplitAfter(test.b, "\n")
|
b := strings.SplitAfter(test.b, "\n")
|
||||||
ops := Operations(a, b)
|
ops := Operations(a, b)
|
||||||
if len(ops) != len(test.operations) {
|
if test.operations != nil {
|
||||||
t.Fatalf("expected %v operations, got %v", len(test.operations), len(ops))
|
if len(ops) != len(test.operations) {
|
||||||
}
|
t.Fatalf("expected %v operations, got %v", len(test.operations), len(ops))
|
||||||
for i, got := range ops {
|
}
|
||||||
want := test.operations[i]
|
for i, got := range ops {
|
||||||
if !reflect.DeepEqual(want, got) {
|
want := test.operations[i]
|
||||||
t.Errorf("expected %v, got %v", want, got)
|
if !reflect.DeepEqual(want, got) {
|
||||||
|
t.Errorf("expected %v, got %v", want, got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
applied := ApplyEdits(a, test.operations)
|
applied := ApplyEdits(a, ops)
|
||||||
for i, want := range applied {
|
for i, want := range applied {
|
||||||
got := b[i]
|
got := b[i]
|
||||||
if got != want {
|
if got != want {
|
||||||
t.Errorf("expected %v got %v", want, got)
|
t.Errorf("expected %v got %v", want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if test.unified != "" {
|
||||||
|
diff := 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)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
got = got[len(unifiedPrefix):]
|
||||||
|
if test.unified != got {
|
||||||
|
t.Errorf("expected:\n%q\ngot:\n%q", test.unified, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *verifyDiff && test.unified != "" && !test.nodiff {
|
||||||
|
diff, err := getDiffOutput(test.a, test.b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if diff != test.unified {
|
||||||
|
t.Errorf("unified:\n%q\ndiff -u:\n%q", test.unified, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDiffOutput(a, b string) (string, error) {
|
||||||
|
fileA, err := ioutil.TempFile("", "diff.in")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(fileA.Name())
|
||||||
|
if _, err := fileA.Write([]byte(a)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := fileA.Close(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
fileB, err := ioutil.TempFile("", "diff.in")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.Remove(fileB.Name())
|
||||||
|
if _, err := fileB.Write([]byte(b)); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := fileB.Close(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cmd := exec.Command("diff", "-u", fileA.Name(), fileB.Name())
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*exec.ExitError); !ok {
|
||||||
|
return "", fmt.Errorf("failed to run diff -u %v %v: %v\n%v", fileA.Name(), fileB.Name(), err, string(out))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diff := string(out)
|
||||||
|
bits := strings.SplitN(diff, "\n", 3)
|
||||||
|
if len(bits) != 3 {
|
||||||
|
return "", fmt.Errorf("diff output did not have file prefix:\n%s", diff)
|
||||||
|
}
|
||||||
|
return bits[2], nil
|
||||||
|
}
|
||||||
|
151
internal/lsp/diff/unified.go
Normal file
151
internal/lsp/diff/unified.go
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Unified struct {
|
||||||
|
From, To string
|
||||||
|
Hunks []*Hunk
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hunk struct {
|
||||||
|
FromLine int
|
||||||
|
ToLine int
|
||||||
|
Lines []Line
|
||||||
|
}
|
||||||
|
|
||||||
|
type Line struct {
|
||||||
|
Kind OpKind
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
edge = 3
|
||||||
|
gap = edge * 2
|
||||||
|
)
|
||||||
|
|
||||||
|
func ToUnified(from, to string, lines []string, ops []*Op) Unified {
|
||||||
|
u := Unified{
|
||||||
|
From: from,
|
||||||
|
To: to,
|
||||||
|
}
|
||||||
|
if len(ops) == 0 {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
if lines[len(lines)-1] == "" {
|
||||||
|
lines = lines[:len(lines)-1]
|
||||||
|
}
|
||||||
|
var h *Hunk
|
||||||
|
last := -(gap + 2)
|
||||||
|
for _, op := range ops {
|
||||||
|
switch {
|
||||||
|
case op.I1 < last:
|
||||||
|
panic("cannot convert unsorted operations to unified diff")
|
||||||
|
case op.I1 == last:
|
||||||
|
//direct extension
|
||||||
|
case op.I1 <= last+gap:
|
||||||
|
//within range of previous lines, add the joiners
|
||||||
|
addEqualLines(h, lines, last, op.I1)
|
||||||
|
default:
|
||||||
|
//need to start a new hunk
|
||||||
|
if h != nil {
|
||||||
|
// add the edge to the previous hunk
|
||||||
|
addEqualLines(h, lines, last, last+edge)
|
||||||
|
u.Hunks = append(u.Hunks, h)
|
||||||
|
}
|
||||||
|
h = &Hunk{
|
||||||
|
FromLine: op.I1 + 1,
|
||||||
|
ToLine: op.J1 + 1,
|
||||||
|
}
|
||||||
|
// add the edge to the new hunk
|
||||||
|
delta := addEqualLines(h, lines, op.I1-edge, op.I1)
|
||||||
|
h.FromLine -= delta
|
||||||
|
h.ToLine -= delta
|
||||||
|
}
|
||||||
|
last = op.I1
|
||||||
|
switch op.Kind {
|
||||||
|
case Delete:
|
||||||
|
for i := op.I1; i < op.I2; i++ {
|
||||||
|
h.Lines = append(h.Lines, Line{Kind: Delete, Content: lines[i]})
|
||||||
|
last++
|
||||||
|
}
|
||||||
|
case Insert:
|
||||||
|
for _, c := range op.Content {
|
||||||
|
h.Lines = append(h.Lines, Line{Kind: Insert, Content: c})
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// all other op types ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if h != nil {
|
||||||
|
// add the edge to the final hunk
|
||||||
|
addEqualLines(h, lines, last, last+edge)
|
||||||
|
u.Hunks = append(u.Hunks, h)
|
||||||
|
}
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
func addEqualLines(h *Hunk, lines []string, start, end int) int {
|
||||||
|
delta := 0
|
||||||
|
for i := start; i < end; i++ {
|
||||||
|
if i < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i >= len(lines) {
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
h.Lines = append(h.Lines, Line{Kind: Equal, Content: lines[i]})
|
||||||
|
delta++
|
||||||
|
}
|
||||||
|
return delta
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u Unified) Format(f fmt.State, r rune) {
|
||||||
|
fmt.Fprintf(f, "--- %s\n", u.From)
|
||||||
|
fmt.Fprintf(f, "+++ %s\n", u.To)
|
||||||
|
for _, hunk := range u.Hunks {
|
||||||
|
fromCount, toCount := 0, 0
|
||||||
|
for _, l := range hunk.Lines {
|
||||||
|
switch l.Kind {
|
||||||
|
case Delete:
|
||||||
|
fromCount++
|
||||||
|
case Insert:
|
||||||
|
toCount++
|
||||||
|
default:
|
||||||
|
fromCount++
|
||||||
|
toCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, "@@")
|
||||||
|
if fromCount > 1 {
|
||||||
|
fmt.Fprintf(f, " -%d,%d", hunk.FromLine, fromCount)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(f, " -%d", hunk.FromLine)
|
||||||
|
}
|
||||||
|
if toCount > 1 {
|
||||||
|
fmt.Fprintf(f, " +%d,%d", hunk.ToLine, toCount)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(f, " +%d", hunk.ToLine)
|
||||||
|
}
|
||||||
|
fmt.Fprint(f, " @@\n")
|
||||||
|
for _, l := range hunk.Lines {
|
||||||
|
switch l.Kind {
|
||||||
|
case Delete:
|
||||||
|
fmt.Fprintf(f, "-%s", l.Content)
|
||||||
|
case Insert:
|
||||||
|
fmt.Fprintf(f, "+%s", l.Content)
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(f, " %s", l.Content)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(l.Content, "\n") {
|
||||||
|
fmt.Fprintf(f, "\n\\ No newline at end of file\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user