diff --git a/cmd/pkg-diff-example/main.go b/cmd/pkg-diff-example/main.go index a72e433..0cd450a 100644 --- a/cmd/pkg-diff-example/main.go +++ b/cmd/pkg-diff-example/main.go @@ -74,13 +74,13 @@ func main() { defer cancel() } e := diff.Myers(ctx, ab) - e = e.WithContextSize(*unified) // limit amount of output context + e = diff.EditScriptWithContextSize(e, *unified) // limit amount of output context opts := []diff.WriteOpt{ diff.Names(aName, bName), } if *color { opts = append(opts, diff.TerminalColor()) } - _, err = e.WriteUnified(os.Stdout, ab, opts...) + _, err = diff.WriteUnified(e, os.Stdout, ab, opts...) check(err) } diff --git a/context.go b/context.go index a8d9c2c..7c57583 100644 --- a/context.go +++ b/context.go @@ -1,89 +1,93 @@ package diff -import "fmt" +import ( + "fmt" + + "github.com/pkg/diff/edit" +) // WithContextSize returns an edit script preserving only n common elements of context for changes. // The returned edit script may alias the input. // If n is negative, WithContextSize panics. // To generate a "unified diff", use WithContextSize and then WriteUnified the resulting edit script. -func (e EditScript) WithContextSize(n int) EditScript { +func EditScriptWithContextSize(e edit.Script, n int) edit.Script { if n < 0 { panic(fmt.Sprintf("EditScript.WithContextSize called with negative n: %d", n)) } // Handle small scripts. - switch len(e.IndexRanges) { + switch len(e.Ranges) { case 0: - return EditScript{} + return edit.Script{} case 1: - if e.IndexRanges[0].IsEqual() { + if e.Ranges[0].IsEqual() { // Entirely identical contents. // Unclear what to do here. For now, just bail. // TODO: something else? what does command line diff do? - return EditScript{} + return edit.Script{} } - return scriptWithIndexRanges(e.IndexRanges[0]) + return edit.NewScript(e.Ranges[0]) } - out := make([]IndexRanges, 0, len(e.IndexRanges)) - for i, seg := range e.IndexRanges { + out := make([]edit.Range, 0, len(e.Ranges)) + for i, seg := range e.Ranges { if !seg.IsEqual() { out = append(out, seg) continue } if i == 0 { - // Leading IndexRanges. Keep only the final n entries. - if seg.len() > n { - seg = indexRangesLastN(seg, n) + // Leading Range. Keep only the final n entries. + if seg.Len() > n { + seg = rangeLastN(seg, n) } out = append(out, seg) continue } - if i == len(e.IndexRanges)-1 { - // Trailing IndexRanges. Keep only the first n entries. - if seg.len() > n { - seg = indexRangesFirstN(seg, n) + if i == len(e.Ranges)-1 { + // Trailing Range. Keep only the first n entries. + if seg.Len() > n { + seg = rangeFirstN(seg, n) } out = append(out, seg) continue } - if seg.len() <= n*2 { - // Small middle IndexRanges. Keep unchanged. + if seg.Len() <= n*2 { + // Small middle Range. Keep unchanged. out = append(out, seg) continue } - // Large middle IndexRanges. Break into two disjoint parts. - out = append(out, indexRangesFirstN(seg, n), indexRangesLastN(seg, n)) + // Large middle Range. Break into two disjoint parts. + out = append(out, rangeFirstN(seg, n), rangeLastN(seg, n)) } // TODO: Stock macOS diff also trims common blank lines // from the beginning/end of eq IndexRangess. // Perhaps we should do that here too. - // Or perhaps that should be a separate, composable EditScript method? - return EditScript{IndexRanges: out} + // Or perhaps that should be a separate, composable function? + return edit.Script{Ranges: out} } -func indexRangesFirstN(seg IndexRanges, n int) IndexRanges { +func rangeFirstN(seg edit.Range, n int) edit.Range { if !seg.IsEqual() { - panic("indexRangesFirstN bad op") + panic("rangeFirstN bad op") } - if seg.len() < n { - panic("indexRangesFirstN bad Len") + if seg.Len() < n { + panic("rangeFirstN bad Len") } - return IndexRanges{ + return edit.Range{ LowA: seg.LowA, HighA: seg.LowA + n, LowB: seg.LowB, HighB: seg.LowB + n, } } -func indexRangesLastN(seg IndexRanges, n int) IndexRanges { +func rangeLastN(seg edit.Range, n int) edit.Range { if !seg.IsEqual() { - panic("indexRangesLastN bad op") + panic("rangeLastN bad op") } - if seg.len() < n { - panic("indexRangesLastN bad Len") + if seg.Len() < n { + panic("rangeLastN bad Len") } - return IndexRanges{ + return edit.Range{ LowA: seg.HighA - n, HighA: seg.HighA, LowB: seg.HighB - n, HighB: seg.HighB, } diff --git a/diff.go b/diff.go index fe177ed..fe9cf5d 100644 --- a/diff.go +++ b/diff.go @@ -1,9 +1,10 @@ package diff import ( - "bytes" "fmt" "io" + + "github.com/pkg/diff/edit" ) // A Pair is two things that can be diffed using the Myers diff algorithm. @@ -48,112 +49,12 @@ type PairWriterTo interface { // If you have paid the O(n) cost to intern all strings involved in both A and B, // then string comparisons are reduced to cheap pointer comparisons. -// An op is a edit operation used to transform A into B. -type op int8 - -//go:generate stringer -type op - -const ( - del op = -1 - eq op = 0 - ins op = 1 -) - -// IndexRanges represents a pair of clopen index ranges. -// They represent elements A[LowA:HighA] and B[LowB:HighB]. -type IndexRanges struct { - LowA, HighA int - LowB, HighB int -} - -// IsInsert reports whether r represents an insertion in an EditScript. -// If so, the inserted elements are B[LowB:HighB]. -func (r *IndexRanges) IsInsert() bool { - return r.LowA == r.HighA -} - -// IsDelete reports whether r represents a deletion in an EditScript. -// If so, the deleted elements are A[LowA:HighA]. -func (r *IndexRanges) IsDelete() bool { - return r.LowB == r.HighB -} - -// IsEqual reports whether r represents a series of equal elements in an EditScript. -// If so, the elements A[LowA:HighA] are equal to the elements B[LowB:HighB]. -func (r *IndexRanges) IsEqual() bool { - return r.HighB-r.LowB == r.HighA-r.LowA -} - -func (r *IndexRanges) op() op { - if r.IsInsert() { - return ins - } - if r.IsDelete() { - return del - } - if r.IsEqual() { - return eq - } - panic("malformed IndexRanges") -} - -func (s IndexRanges) debugString() string { +func rangeString(r edit.Range) string { // This output is helpful when hacking on a Myers diff. // In other contexts it is usually more natural to group LowA, HighA and LowB, HighB. - return fmt.Sprintf("(%d, %d) -- %s %d --> (%d, %d)", s.LowA, s.LowB, s.op(), s.len(), s.HighA, s.HighB) -} - -func (s IndexRanges) len() int { - if s.LowA == s.HighA { - return s.HighB - s.LowB - } - return s.HighA - s.LowA -} - -// An EditScript is an edit script to alter A into B. -type EditScript struct { - IndexRanges []IndexRanges -} - -// IsIdentity reports whether e is the identity edit script, that is, whether A and B are identical. -// See the TestHelper example. -func (e EditScript) IsIdentity() bool { - for _, seg := range e.IndexRanges { - if !seg.IsEqual() { - return false - } - } - return true -} - -// Stat reports the number of insertions and deletions in e. -func (e EditScript) Stat() (ins, del int) { - for _, r := range e.IndexRanges { - switch { - case r.IsDelete(): - del += r.HighA - r.LowA - case r.IsInsert(): - ins += r.HighB - r.LowB - } - } - return ins, del + return fmt.Sprintf("(%d, %d) -- %s %d --> (%d, %d)", r.LowA, r.LowB, r.Op(), r.Len(), r.HighA, r.HighB) } // TODO: consider adding an "it just works" test helper that accepts two slices (via interface{}), // diffs them using Strings or Bytes or Slices (using reflect.DeepEqual) as appropriate, // and calls t.Errorf with a generated diff if they're not equal. - -// scriptWithIndexRanges returns an EditScript containing s. -// It is used to reduce line noise. -func scriptWithIndexRanges(s ...IndexRanges) EditScript { - return EditScript{IndexRanges: s} -} - -// dump formats s for debugging. -func (e EditScript) dump() string { - buf := new(bytes.Buffer) - for _, seg := range e.IndexRanges { - fmt.Fprintln(buf, seg) - } - return buf.String() -} diff --git a/edit/edit.go b/edit/edit.go new file mode 100644 index 0000000..f696257 --- /dev/null +++ b/edit/edit.go @@ -0,0 +1,118 @@ +// Package edit provides edit scripts. +// Edit scripts are a core notion for diffs. +// The represent a way to go from A to B by a sequence +// of insertions, deletions, and equal elements. +package edit + +import ( + "fmt" + "strings" +) + +// A Script is an edit script to alter A into B. +type Script struct { + Ranges []Range +} + +// NewScript returns a Script containing the ranges r. +// It is only a convenience wrapper used to reduce line noise. +func NewScript(r ...Range) Script { + return Script{Ranges: r} +} + +// IsIdentity reports whether s is the identity edit script, +// that is, whether A and B are identical. +func (s *Script) IsIdentity() bool { + for _, r := range s.Ranges { + if !r.IsEqual() { + return false + } + } + return true +} + +// Stat reports the number of insertions and deletions in s. +func (s *Script) Stat() (ins, del int) { + for _, r := range s.Ranges { + switch { + case r.IsDelete(): + del += r.HighA - r.LowA + case r.IsInsert(): + ins += r.HighB - r.LowB + } + } + return ins, del +} + +// dump formats s for debugging. +func (s *Script) dump() string { + buf := new(strings.Builder) + for _, r := range s.Ranges { + fmt.Fprintln(buf, r) + } + return buf.String() +} + +// A Range is a pair of clopen index ranges. +// It represents the elements A[LowA:HighA] and B[LowB:HighB]. +type Range struct { + LowA, HighA int + LowB, HighB int +} + +// IsInsert reports whether r represents an insertion in a Script. +// If so, the inserted elements are B[LowB:HighB]. +func (r *Range) IsInsert() bool { + return r.LowA == r.HighA +} + +// IsDelete reports whether r represents a deletion in a Script. +// If so, the deleted elements are A[LowA:HighA]. +func (r *Range) IsDelete() bool { + return r.LowB == r.HighB +} + +// IsEqual reports whether r represents a series of equal elements in a Script. +// If so, the elements A[LowA:HighA] are equal to the elements B[LowB:HighB]. +func (r *Range) IsEqual() bool { + return r.HighB-r.LowB == r.HighA-r.LowA +} + +// An Op is a edit operation used to transform A into B. +type Op int8 + +//go:generate stringer -type Op + +const ( + Del Op = -1 // delete + Eq Op = 0 // equal + Ins Op = 1 // insert +) + +// Op reports what kind of operation r represents. +// This can also be determined by calling r.IsInsert, +// r.IsDelete, and r.IsEqual, +// but this form is sometimes more convenient to use. +func (r *Range) Op() Op { + if r.IsInsert() { + return Ins + } + if r.IsDelete() { + return Del + } + if r.IsEqual() { + return Eq + } + panic("malformed Range") +} + +// Len reports the number of elements in r. +// In a deletion, it is the number of deleted elements. +// In an insertion, it is the number of inserted elements. +// For equal elements, it is the number of equal elements. +func (r *Range) Len() int { + if r.LowA == r.HighA { + return r.HighB - r.LowB + } + return r.HighA - r.LowA +} diff --git a/edit/op_string.go b/edit/op_string.go new file mode 100644 index 0000000..9560eaa --- /dev/null +++ b/edit/op_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type Op"; DO NOT EDIT. + +package edit + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Del - -1] + _ = x[Eq-0] + _ = x[Ins-1] +} + +const _Op_name = "DelEqIns" + +var _Op_index = [...]uint8{0, 3, 5, 8} + +func (i Op) String() string { + i -= -1 + if i < 0 || i >= Op(len(_Op_index)-1) { + return "Op(" + strconv.FormatInt(int64(i+-1), 10) + ")" + } + return _Op_name[_Op_index[i]:_Op_index[i+1]] +} diff --git a/example_test.go b/example_test.go index ecf7a7b..b750b51 100644 --- a/example_test.go +++ b/example_test.go @@ -17,8 +17,8 @@ func Example_testHelper() { if e.IsIdentity() { return } - e = e.WithContextSize(1) - e.WriteUnified(os.Stdout, ab) + e = diff.EditScriptWithContextSize(e, 1) + diff.WriteUnified(e, os.Stdout, ab) // Output: // --- a // +++ b @@ -33,7 +33,7 @@ func Example_strings() { b := []string{"a", "c", "d"} ab := diff.Strings(a, b) e := diff.Myers(context.Background(), ab) - e.WriteUnified(os.Stdout, ab) + diff.WriteUnified(e, os.Stdout, ab) // Output: // --- a // +++ b @@ -49,7 +49,7 @@ func Example_Names() { b := []string{"a", "c", "d"} ab := diff.Strings(a, b) e := diff.Myers(context.Background(), ab) - e.WriteUnified(os.Stdout, ab, diff.Names("before", "after")) + diff.WriteUnified(e, os.Stdout, ab, diff.Names("before", "after")) // Output: // --- before // +++ after diff --git a/myers.go b/myers.go index 3e5ca00..e4e16cc 100644 --- a/myers.go +++ b/myers.go @@ -3,18 +3,20 @@ package diff import ( "context" "fmt" + + "github.com/pkg/diff/edit" ) -// Myers calculates an EditScript (diff) for ab using the Myers diff algorithm. +// Myers calculates an edit.Script (diff) for ab using the Myers diff algorithm. // Because diff calculation can be expensive, Myers supports cancellation via ctx. -func Myers(ctx context.Context, ab Pair) EditScript { +func Myers(ctx context.Context, ab Pair) edit.Script { aLen := ab.LenA() bLen := ab.LenB() if aLen == 0 { - return scriptWithIndexRanges(IndexRanges{HighB: bLen}) + return edit.NewScript(edit.Range{HighB: bLen}) } if bLen == 0 { - return scriptWithIndexRanges(IndexRanges{HighA: aLen}) + return edit.NewScript(edit.Range{HighA: aLen}) } max := aLen + bLen @@ -28,7 +30,7 @@ search: for d := 0; d < max; d++ { // Only check context every 16th iteration to reduce overhead. if ctx != nil && uint(d)%16 == 0 && ctx.Err() != nil { - return EditScript{} + return edit.Script{} } // TODO: this seems like it will frequently be bigger than necessary. @@ -61,13 +63,13 @@ search: if len(trace) == max { // No commonality at all, delete everything and then insert everything. // This is handled as a special case to avoid complicating the logic below. - return scriptWithIndexRanges(IndexRanges{HighA: aLen}, IndexRanges{HighB: bLen}) + return edit.NewScript(edit.Range{HighA: aLen}, edit.Range{HighB: bLen}) } // Create reversed edit script. x := aLen y := bLen - var e EditScript + var e edit.Script for d := len(trace) - 1; d >= 0; d-- { v := trace[d] k := x - y @@ -80,24 +82,24 @@ search: prevx := v[max+prevk] prevy := prevx - prevk for x > prevx && y > prevy { - e.appendToReversed(IndexRanges{LowA: x - 1, LowB: y - 1, HighA: x, HighB: y}) + appendToReversed(&e, edit.Range{LowA: x - 1, LowB: y - 1, HighA: x, HighB: y}) x-- y-- } if d > 0 { - e.appendToReversed(IndexRanges{LowA: prevx, LowB: prevy, HighA: x, HighB: y}) + appendToReversed(&e, edit.Range{LowA: prevx, LowB: prevy, HighA: x, HighB: y}) } x, y = prevx, prevy } // Reverse reversed edit script, to return to natural order. - e.reverse() + reverse(e) // Sanity check - for i := 1; i < len(e.IndexRanges); i++ { - prevop := e.IndexRanges[i-1].op() - currop := e.IndexRanges[i].op() - if (prevop == currop) || (prevop == ins && currop != eq) || (currop == del && prevop != eq) { + for i := 1; i < len(e.Ranges); i++ { + prevop := e.Ranges[i-1].Op() + currop := e.Ranges[i].Op() + if (prevop == currop) || (prevop == edit.Ins && currop != edit.Eq) || (currop == edit.Del && prevop != edit.Eq) { panic(fmt.Errorf("bad script: %v -> %v", prevop, currop)) } } @@ -105,45 +107,45 @@ search: return e } -func (e EditScript) reverse() { - for i := 0; i < len(e.IndexRanges)/2; i++ { - j := len(e.IndexRanges) - i - 1 - e.IndexRanges[i], e.IndexRanges[j] = e.IndexRanges[j], e.IndexRanges[i] +func reverse(e edit.Script) { + for i := 0; i < len(e.Ranges)/2; i++ { + j := len(e.Ranges) - i - 1 + e.Ranges[i], e.Ranges[j] = e.Ranges[j], e.Ranges[i] } } -func (e *EditScript) appendToReversed(seg IndexRanges) { - if len(e.IndexRanges) == 0 { - e.IndexRanges = append(e.IndexRanges, seg) +func appendToReversed(e *edit.Script, seg edit.Range) { + if len(e.Ranges) == 0 { + e.Ranges = append(e.Ranges, seg) return } - u, ok := combineIndexRangess(seg, e.IndexRanges[len(e.IndexRanges)-1]) + u, ok := combineRanges(seg, e.Ranges[len(e.Ranges)-1]) if !ok { - e.IndexRanges = append(e.IndexRanges, seg) + e.Ranges = append(e.Ranges, seg) return } - e.IndexRanges[len(e.IndexRanges)-1] = u + e.Ranges[len(e.Ranges)-1] = u return } -// combineIndexRangess combines s and t into a single IndexRanges if possible +// combineRanges combines s and t into a single edit.Range if possible // and reports whether it succeeded. -func combineIndexRangess(s, t IndexRanges) (u IndexRanges, ok bool) { - if t.len() == 0 { +func combineRanges(s, t edit.Range) (u edit.Range, ok bool) { + if t.Len() == 0 { return s, true } - if s.len() == 0 { + if s.Len() == 0 { return t, true } - if s.op() != t.op() { - return IndexRanges{LowA: -1, HighA: -1, LowB: -1, HighB: -1}, false + if s.Op() != t.Op() { + return edit.Range{LowA: -1, HighA: -1, LowB: -1, HighB: -1}, false } - switch s.op() { - case ins: + switch s.Op() { + case edit.Ins: s.HighB = t.HighB - case del: + case edit.Del: s.HighA = t.HighA - case eq: + case edit.Eq: s.HighA = t.HighA s.HighB = t.HighB default: diff --git a/myers_test.go b/myers_test.go index 8a91e4e..b9e3da2 100644 --- a/myers_test.go +++ b/myers_test.go @@ -4,13 +4,15 @@ import ( "context" "reflect" "testing" + + "github.com/pkg/diff/edit" ) func TestMyers(t *testing.T) { tests := []struct { name string a, b string - want []IndexRanges + want []edit.Range wantStatIns int wantStatDel int }{ @@ -18,7 +20,7 @@ func TestMyers(t *testing.T) { name: "BasicExample", a: "ABCABBA", b: "CBABAC", - want: []IndexRanges{ + want: []edit.Range{ {LowA: 0, HighA: 2, LowB: 0, HighB: 0}, {LowA: 2, HighA: 3, LowB: 0, HighB: 1}, {LowA: 3, HighA: 3, LowB: 1, HighB: 2}, @@ -34,7 +36,7 @@ func TestMyers(t *testing.T) { name: "AllDifferent", a: "ABCDE", b: "xyz", - want: []IndexRanges{ + want: []edit.Range{ {LowA: 0, HighA: 5, LowB: 0, HighB: 0}, {LowA: 0, HighA: 0, LowB: 0, HighB: 3}, }, @@ -48,7 +50,7 @@ func TestMyers(t *testing.T) { t.Run(test.name, func(t *testing.T) { ab := &diffByByte{a: test.a, b: test.b} got := Myers(context.Background(), ab) - want := EditScript{IndexRanges: test.want} + want := edit.Script{Ranges: test.want} if !reflect.DeepEqual(got, want) { // Ironically, it'd be nice to provide a diff between got and want here... diff --git a/op_string.go b/op_string.go deleted file mode 100644 index 862732c..0000000 --- a/op_string.go +++ /dev/null @@ -1,17 +0,0 @@ -// Code generated by "stringer -type op"; DO NOT EDIT. - -package diff - -import "strconv" - -const _op_name = "deleqins" - -var _op_index = [...]uint8{0, 3, 5, 8} - -func (i op) String() string { - i -= -1 - if i < 0 || i >= op(len(_op_index)-1) { - return "op(" + strconv.FormatInt(int64(i+-1), 10) + ")" - } - return _op_name[_op_index[i]:_op_index[i+1]] -} diff --git a/print.go b/print.go index 0aec855..684b36d 100644 --- a/print.go +++ b/print.go @@ -3,6 +3,8 @@ package diff import ( "fmt" "io" + + "github.com/pkg/diff/edit" ) // TODO: add diff writing that uses < and > (don't know what that is called) @@ -51,7 +53,7 @@ const ( // WriteUnified writes e to w using unified diff format. // ab writes the individual elements. Opts are optional write arguments. // WriteUnified returns the number of bytes written and the first error (if any) encountered. -func (e EditScript) WriteUnified(w io.Writer, ab WriterTo, opts ...WriteOpt) (int, error) { +func WriteUnified(e edit.Script, w io.Writer, ab WriterTo, opts ...WriteOpt) (int, error) { // read opts nameA := "a" nameB := "b" @@ -83,14 +85,14 @@ func (e EditScript) WriteUnified(w io.Writer, ab WriterTo, opts ...WriteOpt) (in fmt.Fprintf(ew, "--- %s\n", nameA) fmt.Fprintf(ew, "+++ %s\n", nameB) - for i := 0; i < len(e.IndexRanges); { + for i := 0; i < len(e.Ranges); { // Peek into the future to learn the line ranges for this chunk of output. // A chunk of output ends when there's a discontiguity in the edit script. var ar, br lineRange var started [2]bool var j int - for j = i; j < len(e.IndexRanges); j++ { - curr := e.IndexRanges[j] + for j = i; j < len(e.Ranges); j++ { + curr := e.Ranges[j] if !curr.IsInsert() { if !started[0] { ar.first = curr.LowA @@ -105,11 +107,11 @@ func (e EditScript) WriteUnified(w io.Writer, ab WriterTo, opts ...WriteOpt) (in } br.last = curr.HighB } - if j+1 >= len(e.IndexRanges) { + if j+1 >= len(e.Ranges) { // end of script break } - if next := e.IndexRanges[j+1]; curr.HighA != next.LowA || curr.HighB != next.LowB { + if next := e.Ranges[j+1]; curr.HighA != next.LowA || curr.HighB != next.LowB { // discontiguous edit script break } @@ -131,9 +133,9 @@ func (e EditScript) WriteUnified(w io.Writer, ab WriterTo, opts ...WriteOpt) (in // Print prefixed lines. for k := i; k <= j; k++ { - seg := e.IndexRanges[k] - switch seg.op() { - case eq: + seg := e.Ranges[k] + switch seg.Op() { + case edit.Eq: if needsColorReset { ew.WriteString(ansiReset) } @@ -143,7 +145,7 @@ func (e EditScript) WriteUnified(w io.Writer, ab WriterTo, opts ...WriteOpt) (in ab.WriteATo(ew, m) ew.WriteByte('\n') } - case del: + case edit.Del: if color { ew.WriteString(ansiFgRed) needsColorReset = true @@ -154,7 +156,7 @@ func (e EditScript) WriteUnified(w io.Writer, ab WriterTo, opts ...WriteOpt) (in ab.WriteATo(ew, m) ew.WriteByte('\n') } - case ins: + case edit.Ins: if color { ew.WriteString(ansiFgGreen) needsColorReset = true diff --git a/unified_test.go b/unified_test.go index ddda903..7ac1019 100644 --- a/unified_test.go +++ b/unified_test.go @@ -77,13 +77,13 @@ func TestGolden(t *testing.T) { as := strings.Split(test.a, "\n") bs := strings.Split(test.b, "\n") ab := diff.Strings(as, bs) - // TODO: supply an EditScript to the tests instead doing a Myers diff here. + // TODO: supply an edit.Script to the tests instead doing a Myers diff here. // Doing it as I have done, the lazy way, mixes concerns: diff algorithm vs unification algorithm // vs unified diff formatting. e := diff.Myers(context.Background(), ab) - e = e.WithContextSize(3) + e = diff.EditScriptWithContextSize(e, 3) buf := new(bytes.Buffer) - e.WriteUnified(buf, ab, test.opts...) + diff.WriteUnified(e, buf, ab, test.opts...) got := buf.String() if test.want != got { t.Logf("%q\n", test.want)