internal/diff: first stab add diffing functionality

for supporting various commands:
- cue test
- cue diff/snapshot

Change-Id: I9ddccb0622530db9759cd25768656b712268d3c5
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/3981
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/internal/diff/print.go b/internal/diff/print.go
new file mode 100644
index 0000000..ebe2d10
--- /dev/null
+++ b/internal/diff/print.go
@@ -0,0 +1,271 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package diff
+
+import (
+	"fmt"
+	"io"
+
+	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/format"
+)
+
+// Print the differences between two structs represented by an edit script.
+func Print(w io.Writer, es *EditScript) error {
+	p := printer{
+		w:       w,
+		margin:  2,
+		context: 2,
+	}
+	p.script(es)
+	return p.errs
+}
+
+type printer struct {
+	w         io.Writer
+	context   int
+	margin    int
+	indent    int
+	prefix    string
+	hasPrefix bool
+	hasPrint  bool
+	errs      errors.Error
+}
+
+func (p *printer) writeRaw(b []byte) {
+	if len(b) == 0 {
+		return
+	}
+	if !p.hasPrefix {
+		io.WriteString(p.w, p.prefix)
+		p.hasPrefix = true
+	}
+	if !p.hasPrint {
+		fmt.Fprintf(p.w, "% [1]*s", p.indent+p.margin-len(p.prefix), "")
+		p.hasPrint = true
+	}
+	p.w.Write(b)
+}
+
+func (p *printer) Write(b []byte) (n int, err error) {
+	i, last := 0, 0
+	for ; i < len(b); i++ {
+		if b[i] != '\n' {
+			continue
+		}
+		p.writeRaw(b[last:i])
+		last = i + 1
+		io.WriteString(p.w, "\n")
+		p.hasPrefix = false
+		p.hasPrint = false
+	}
+	p.writeRaw(b[last:])
+	return len(b), nil
+}
+
+func (p *printer) write(b []byte) {
+	_, _ = p.Write(b)
+}
+
+func (p *printer) printLen(align int, str string) {
+	fmt.Fprintf(p, "% -[1]*s", align, str)
+}
+
+func (p *printer) println(s string) {
+	fmt.Fprintln(p, s)
+}
+
+func (p *printer) printf(format string, args ...interface{}) {
+	fmt.Fprintf(p, format, args...)
+}
+
+func (p *printer) script(e *EditScript) {
+	switch e.x.Kind() {
+	case cue.StructKind:
+		p.printStruct(e)
+	case cue.ListKind:
+		p.printList(e)
+	default:
+		p.printf("BadExpr")
+	}
+}
+
+func (p *printer) findRun(es *EditScript, i int) (start, end int) {
+	lastEnd := i
+
+	for ; i < es.Len() && es.edits[i].kind == Identity; i++ {
+	}
+	start = i
+
+	// Find end of run
+	include := p.context
+	for ; i < es.Len(); i++ {
+		e := es.edits[i]
+		if e.kind != Identity {
+			include = p.context + 1
+			continue
+		}
+		if include--; include == 0 {
+			break
+		}
+	}
+
+	if i-start > 0 {
+		// Adjust start of run
+		if s := start - p.context; s > lastEnd {
+			start = s
+		} else {
+			start = lastEnd
+		}
+	}
+	return start, i
+}
+
+func (p *printer) printStruct(es *EditScript) {
+	// TODO: consider not printing outer curlies, or make it an option.
+	// if p.indent > 0 {
+	p.println("{")
+	defer p.println("}")
+	// }
+	p.indent += 4
+	defer func() {
+		p.indent -= 4
+	}()
+
+	var start, i int
+	for i < es.Len() {
+		lastEnd := i
+		// Find provisional start of run.
+		start, i = p.findRun(es, i)
+
+		p.printSkipped(start - lastEnd)
+		p.printFieldRun(es, start, i)
+	}
+	p.printSkipped(es.Len() - i)
+}
+
+func (p *printer) printList(es *EditScript) {
+	p.println("[")
+	p.indent += 4
+	defer func() {
+		p.indent -= 4
+		p.println("]")
+	}()
+
+	x := getElems(es.x)
+	y := getElems(es.y)
+
+	var start, i int
+	for i < es.Len() {
+		lastEnd := i
+		// Find provisional start of run.
+		start, i = p.findRun(es, i)
+
+		p.printSkipped(start - lastEnd)
+		p.printElemRun(es, x, y, start, i)
+	}
+	p.printSkipped(es.Len() - i)
+}
+
+func getElems(x cue.Value) (a []cue.Value) {
+	for i, _ := x.List(); i.Next(); {
+		a = append(a, i.Value())
+	}
+	return a
+}
+
+func (p *printer) printSkipped(n int) {
+	if n > 0 {
+		p.printf("... // %d identical elements\n", n)
+	}
+}
+
+func (p *printer) printValue(v cue.Value) {
+	// TODO: have indent option.
+	b, _ := format.Node(v.Syntax())
+	p.write(b)
+}
+
+func (p *printer) printFieldRun(es *EditScript, start, end int) {
+	// Determine max field len.
+	for i := start; i < end; i++ {
+		e := es.edits[i]
+
+		switch e.kind {
+		case UniqueX:
+			p.printField("-", es, es.LabelX(i), es.ValueX(i))
+
+		case UniqueY:
+			p.printField("+", es, es.LabelY(i), es.ValueY(i))
+
+		case Modified:
+			if e.sub != nil {
+				io.WriteString(p, es.LabelX(i))
+				io.WriteString(p, " ")
+				p.script(e.sub)
+				break
+			}
+			// TODO: show per-line differences for multiline strings.
+			p.printField("-", es, es.LabelX(i), es.ValueX(i))
+			p.printField("+", es, es.LabelY(i), es.ValueY(i))
+
+		case Identity:
+			// TODO: write on one line
+			p.printField("", es, es.LabelX(i), es.ValueX(i))
+		}
+	}
+}
+
+func (p *printer) printField(prefix string, es *EditScript, label string, v cue.Value) {
+	p.prefix = prefix
+	io.WriteString(p, label)
+	io.WriteString(p, " ")
+	p.printValue(v)
+	io.WriteString(p, "\n")
+	p.prefix = ""
+}
+
+func (p *printer) printElemRun(es *EditScript, x, y []cue.Value, start, end int) {
+	for _, e := range es.edits[start:end] {
+		switch e.kind {
+		case UniqueX:
+			p.printElem("-", x[e.XPos()])
+
+		case UniqueY:
+			p.printElem("+", y[e.YPos()])
+
+		case Modified:
+			if e.sub != nil {
+				p.script(e.sub)
+				break
+			}
+			// TODO: show per-line differences for multiline strings.
+			p.printElem("-", x[e.XPos()])
+			p.printElem("+", y[e.YPos()])
+
+		case Identity:
+			// TODO: write on one line
+			p.printElem("", x[e.XPos()])
+		}
+	}
+}
+
+func (p *printer) printElem(prefix string, v cue.Value) {
+	p.prefix = prefix
+	p.printValue(v)
+	io.WriteString(p, ",\n")
+	p.prefix = ""
+}