1
0
mirror of https://github.com/golang/go synced 2024-11-18 15:04:44 -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:
Ian Cottrell 2019-04-04 17:42:34 -04:00
parent 2b5498619e
commit fa491999fb
2 changed files with 301 additions and 8 deletions

View File

@ -5,18 +5,62 @@
package diff
import (
"flag"
"fmt"
"io/ioutil"
"os"
"os/exec"
"reflect"
"strings"
"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) {
for _, test := range []struct {
a, b string
lines []*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",
b: "C\nB\nA\nB\nA\nC\n",
operations: []*Op{
@ -26,6 +70,19 @@ func TestDiff(t *testing.T) {
&Op{Kind: Delete, I1: 5, I2: 6, J1: 4},
&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",
@ -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{"\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")
b := strings.SplitAfter(test.b, "\n")
ops := Operations(a, b)
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]
if !reflect.DeepEqual(want, got) {
t.Errorf("expected %v, got %v", want, got)
if test.operations != nil {
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]
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 {
got := b[i]
if got != want {
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
}

View 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")
}
}
}
}