1// Copyright 2019 CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package diff
16
17import (
18 "fmt"
19 "io"
20
21 "cuelang.org/go/cue"
22 "cuelang.org/go/cue/errors"
23)
24
25// Print the differences between two structs represented by an edit script.
26func Print(w io.Writer, es *EditScript) error {
27 p := printer{
28 w: w,
29 margin: 2,
30 context: 2,
31 }
32 p.script(es)
33 return p.errs
34}
35
36type printer struct {
37 w io.Writer
38 context int
39 margin int
40 indent int
41 prefix string
42 hasPrefix bool
43 hasPrint bool
44 errs errors.Error
45}
46
47func (p *printer) writeRaw(b []byte) {
48 if len(b) == 0 {
49 return
50 }
51 if !p.hasPrefix {
52 io.WriteString(p.w, p.prefix)
53 p.hasPrefix = true
54 }
55 if !p.hasPrint {
56 fmt.Fprintf(p.w, "% [1]*s", p.indent+p.margin-len(p.prefix), "")
57 p.hasPrint = true
58 }
59 p.w.Write(b)
60}
61
62func (p *printer) Write(b []byte) (n int, err error) {
63 i, last := 0, 0
64 for ; i < len(b); i++ {
65 if b[i] != '\n' {
66 continue
67 }
68 p.writeRaw(b[last:i])
69 last = i + 1
70 io.WriteString(p.w, "\n")
71 p.hasPrefix = false
72 p.hasPrint = false
73 }
74 p.writeRaw(b[last:])
75 return len(b), nil
76}
77
78func (p *printer) println(s string) {
79 fmt.Fprintln(p, s)
80}
81
82func (p *printer) printf(format string, args ...interface{}) {
83 fmt.Fprintf(p, format, args...)
84}
85
86func (p *printer) script(e *EditScript) {
87 switch e.X.Kind() {
88 case cue.StructKind:
89 p.printStruct(e)
90 case cue.ListKind:
91 p.printList(e)
92 default:
93 p.printElem("-", e.X)
94 p.printElem("+", e.Y)
95 }
96}
97
98func (p *printer) findRun(es *EditScript, i int) (start, end int) {
99 lastEnd := i
100
101 for ; i < len(es.Edits) && es.Edits[i].Kind == Identity; i++ {
102 }
103 start = i
104
105 // Find end of run
106 include := p.context
107 for ; i < len(es.Edits); i++ {
108 e := es.Edits[i]
109 if e.Kind != Identity {
110 include = p.context + 1
111 continue
112 }
113 if include--; include == 0 {
114 break
115 }
116 }
117
118 if i-start > 0 {
119 // Adjust start of run
120 if s := start - p.context; s > lastEnd {
121 start = s
122 } else {
123 start = lastEnd
124 }
125 }
126 return start, i
127}
128
129func (p *printer) printStruct(es *EditScript) {
130 // TODO: consider not printing outer curlies, or make it an option.
131 // if p.indent > 0 {
132 p.println("{")
133 defer p.println("}")
134 // }
135 p.indent += 4
136 defer func() {
137 p.indent -= 4
138 }()
139
140 var start, i int
141 for i < len(es.Edits) {
142 lastEnd := i
143 // Find provisional start of run.
144 start, i = p.findRun(es, i)
145
146 p.printSkipped(start - lastEnd)
147 p.printFieldRun(es, start, i)
148 }
149 p.printSkipped(len(es.Edits) - i)
150}
151
152func (p *printer) printList(es *EditScript) {
153 p.println("[")
154 p.indent += 4
155 defer func() {
156 p.indent -= 4
157 p.println("]")
158 }()
159
160 x := getElems(es.X)
161 y := getElems(es.Y)
162
163 var start, i int
164 for i < len(es.Edits) {
165 lastEnd := i
166 // Find provisional start of run.
167 start, i = p.findRun(es, i)
168
169 p.printSkipped(start - lastEnd)
170 p.printElemRun(es, x, y, start, i)
171 }
172 p.printSkipped(len(es.Edits) - i)
173}
174
175func getElems(x cue.Value) (a []cue.Value) {
176 for i, _ := x.List(); i.Next(); {
177 a = append(a, i.Value())
178 }
179 return a
180}
181
182func (p *printer) printSkipped(n int) {
183 if n > 0 {
184 p.printf("... // %d identical elements\n", n)
185 }
186}
187
188func (p *printer) printValue(v cue.Value) {
189 // TODO: have indent option.
190 s := fmt.Sprintf("%+v", v)
191 io.WriteString(p, s)
192}
193
194func (p *printer) printFieldRun(es *EditScript, start, end int) {
195 // Determine max field len.
196 for i := start; i < end; i++ {
197 e := es.Edits[i]
198
199 switch e.Kind {
200 case UniqueX:
201 p.printField("-", e.XSel.String(), es.X.LookupPath(cue.MakePath(e.XSel)))
202
203 case UniqueY:
204 p.printField("+", e.YSel.String(), es.Y.LookupPath(cue.MakePath(e.YSel)))
205
206 case Modified:
207 if e.Sub != nil {
208 io.WriteString(p, e.XSel.String())
209 io.WriteString(p, ": ")
210 p.script(e.Sub)
211 break
212 }
213 // TODO: show per-line differences for multiline strings.
214 p.printField("-", e.XSel.String(), es.X.LookupPath(cue.MakePath(e.XSel)))
215 p.printField("+", e.YSel.String(), es.Y.LookupPath(cue.MakePath(e.YSel)))
216
217 case Identity:
218 // TODO: write on one line
219 p.printField("", e.XSel.String(), es.X.LookupPath(cue.MakePath(e.XSel)))
220 }
221 }
222}
223
224func (p *printer) printField(prefix string, label string, v cue.Value) {
225 p.prefix = prefix
226 io.WriteString(p, label)
227 io.WriteString(p, ": ")
228 p.printValue(v)
229 io.WriteString(p, "\n")
230 p.prefix = ""
231}
232
233func (p *printer) printElemRun(es *EditScript, x, y []cue.Value, start, end int) {
234 for _, e := range es.Edits[start:end] {
235 switch e.Kind {
236 case UniqueX:
237 p.printElem("-", x[e.XSel.Index()])
238
239 case UniqueY:
240 p.printElem("+", y[e.YSel.Index()])
241
242 case Modified:
243 if e.Sub != nil {
244 p.script(e.Sub)
245 break
246 }
247 // TODO: show per-line differences for multiline strings.
248 p.printElem("-", x[e.XSel.Index()])
249 p.printElem("+", y[e.YSel.Index()])
250
251 case Identity:
252 // TODO: write on one line
253 p.printElem("", x[e.XSel.Index()])
254 }
255 }
256}
257
258func (p *printer) printElem(prefix string, v cue.Value) {
259 p.prefix = prefix
260 p.printValue(v)
261 io.WriteString(p, ",\n")
262 p.prefix = ""
263}