blob: ebe2d101d1ff43d67cb5e268d343cf6a363ee2c5 [file] [log] [blame]
// 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 = ""
}