diff --git a/internal/lsp/diff/diff_test.go b/internal/lsp/diff/diff_test.go index 52e52a45ab..1eb32f4dc8 100644 --- a/internal/lsp/diff/diff_test.go +++ b/internal/lsp/diff/diff_test.go @@ -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 +} diff --git a/internal/lsp/diff/unified.go b/internal/lsp/diff/unified.go new file mode 100644 index 0000000000..ab014aae66 --- /dev/null +++ b/internal/lsp/diff/unified.go @@ -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") + } + } + } +}