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:
parent
2b5498619e
commit
fa491999fb
@ -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
|
||||
}
|
||||
|
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