coverage~bigbes/sr-ht-spec64cae3afprosediff/render.go

Coverage
87.7% 93/106 statements
Δ
+0.0
Blob
23e29f6
1 package prosediff
2
3 import (
4 "fmt"
5 "strings"
6 "unicode/utf8"
7 )
8
9 // RenderOptions controls the plain-text renderer. It exists for tests and for
10 // reading a diff in a terminal; the web layer walks Diff.Changes itself.
11 type RenderOptions struct {
12 // Width wraps rendered prose. 0 means do not wrap.
13 Width int
14 // ShowEqual prints unchanged blocks too, instead of collapsing them.
15 ShowEqual bool
16 // Context is how many unchanged blocks to keep around a change when
17 // ShowEqual is false. 0 keeps none.
18 Context int
19 // Markers are the inline delete/insert brackets. Empty uses the
20 // defaults "[-", "-]", "{+", "+}".
21 DelOpen, DelClose, InsOpen, InsClose string
22 }
23
24 // DefaultRenderOptions wraps at 80 columns and hides unchanged blocks.
25 26 func DefaultRenderOptions() RenderOptions {
26 26 return RenderOptions{Width: 80, Context: 0}
27 26 }
28
29 43 func (o RenderOptions) markers() (string, string, string, string) {
30 43 d0, d1, i0, i1 := o.DelOpen, o.DelClose, o.InsOpen, o.InsClose
31 43 if d0 == "" && d1 == "" && i0 == "" && i1 == "" {
32 43 return "[-", "-]", "{+", "+}"
33 43 }
34 0 return d0, d1, i0, i1
35 }
36
37 // RenderText renders a diff as plain text.
38 27 func RenderText(d *Diff, opts RenderOptions) string {
39 27 var sb strings.Builder
40 27 keep := visible(d.Changes, opts)
41 27
42 27 lastPath := "\x00"
43 27 skipped := 0
44 70 flushSkipped := func() {
45 70 if skipped > 0 {
46 27 fmt.Fprintf(&sb, " … %d unchanged block(s)\n", skipped)
47 27 skipped = 0
48 27 }
49 }
50
51 85 for i, c := range d.Changes {
52 85 if !keep[i] {
53 42 if c.Kind == ChangeEqual {
54 42 skipped++
55 42 }
56 42 continue
57 }
58 43 flushSkipped()
59 43 b := c.New
60 43 if b == nil {
61 4 b = c.Old
62 4 }
63 43 if p := strings.Join(b.HeadingPath, " › "); p != lastPath {
64 43 if p == "" {
65 21 p = "(document preamble)"
66 21 }
67 43 fmt.Fprintf(&sb, "\n@@ %s @@\n", p)
68 43 lastPath = p
69 }
70 43 sb.WriteString(renderChange(c, opts))
71 }
72 27 flushSkipped()
73 27 return sb.String()
74 }
75
76 27 func visible(changes []BlockChange, opts RenderOptions) []bool {
77 27 keep := make([]bool, len(changes))
78 85 for i, c := range changes {
79 85 if c.Kind != ChangeEqual || opts.ShowEqual {
80 43 keep[i] = true
81 43 }
82 }
83 27 if opts.Context > 0 && !opts.ShowEqual {
84 0 orig := append([]bool(nil), keep...)
85 0 for i := range changes {
86 0 if !orig[i] {
87 0 continue
88 }
89 0 for j := i - opts.Context; j <= i+opts.Context; j++ {
90 0 if j >= 0 && j < len(keep) {
91 0 keep[j] = true
92 0 }
93 }
94 }
95 }
96 27 return keep
97 }
98
99 43 func renderChange(c BlockChange, opts RenderOptions) string {
100 43 var sb strings.Builder
101 43 d0, d1, i0, i1 := opts.markers()
102 43
103 43 switch c.Kind {
104 1 case ChangeEqual:
105 1 fmt.Fprintf(&sb, " %s %s\n", loc(c.Old, c.New), c.New.Label())
106 1 writeBody(&sb, " ", c.New.Text, opts.Width, c.New.Kind.Prose())
107
108 9 case ChangeInsert:
109 9 fmt.Fprintf(&sb, "+ %s %s\n", loc(nil, c.New), c.New.Label())
110 9 writeBody(&sb, "+ ", c.New.Text, opts.Width, c.New.Kind.Prose())
111
112 4 case ChangeDelete:
113 4 fmt.Fprintf(&sb, "- %s %s\n", loc(c.Old, nil), c.Old.Label())
114 4 writeBody(&sb, "- ", c.Old.Text, opts.Width, c.Old.Kind.Prose())
115
116 8 case ChangeMoveOut:
117 8 fmt.Fprintf(&sb, "< %s %s moved away (now line %d)\n",
118 8 loc(c.Old, nil), c.Old.Label(), c.New.StartLine)
119
120 8 case ChangeMoveIn:
121 8 fmt.Fprintf(&sb, "> %s %s moved here (was line %d)\n",
122 8 loc(nil, c.New), c.New.Label(), c.Old.StartLine)
123
124 13 case ChangeModify:
125 13 note := ""
126 13 if c.StructureOnly {
127 2 note = fmt.Sprintf(" (structure only: %s → %s)", c.Old.Label(), c.New.Label())
128 2 }
129 13 if c.Moved {
130 0 note += fmt.Sprintf(" (moved from line %d)", c.Old.StartLine)
131 0 }
132 13 fmt.Fprintf(&sb, "~ %s %s modified%s\n", loc(c.Old, c.New), c.New.Label(), note)
133 13 if len(c.Lines) > 0 {
134 15 for _, s := range c.Lines {
135 15 switch s.Op {
136 7 case OpEqual:
137 7 fmt.Fprintf(&sb, " %s\n", s.Text)
138 4 case OpDelete:
139 4 fmt.Fprintf(&sb, " - %s\n", s.Text)
140 4 case OpInsert:
141 4 fmt.Fprintf(&sb, " + %s\n", s.Text)
142 }
143 }
144 4 break
145 }
146 9 var body strings.Builder
147 29 for _, s := range c.Words {
148 29 if s.Space && body.Len() > 0 {
149 11 body.WriteByte(' ')
150 11 }
151 29 switch s.Op {
152 16 case OpEqual:
153 16 body.WriteString(s.Text)
154 6 case OpDelete:
155 6 body.WriteString(d0 + s.Text + d1)
156 7 case OpInsert:
157 7 body.WriteString(i0 + s.Text + i1)
158 }
159 }
160 9 writeBody(&sb, " ", body.String(), opts.Width, true)
161 }
162 43 return sb.String()
163 }
164
165 43 func loc(old, nw *Block) string {
166 43 switch {
167 14 case old != nil && nw != nil:
168 14 if old.StartLine == nw.StartLine {
169 14 return fmt.Sprintf("L%d", old.StartLine)
170 14 }
171 0 return fmt.Sprintf("L%d→%d", old.StartLine, nw.StartLine)
172 12 case old != nil:
173 12 return fmt.Sprintf("L%d", old.StartLine)
174 17 case nw != nil:
175 17 return fmt.Sprintf("L%d", nw.StartLine)
176 }
177 0 return "L?"
178 }
179
180 23 func writeBody(sb *strings.Builder, prefix, body string, width int, reflow bool) {
181 23 var lines []string
182 23 if reflow {
183 22 lines = wrap(body, width-len(prefix))
184 22 } else {
185 1 lines = strings.Split(body, "\n")
186 1 }
187 25 for _, line := range lines {
188 25 sb.WriteString(prefix)
189 25 sb.WriteString(line)
190 25 sb.WriteByte('\n')
191 25 }
192 }
193
194 // wrap reflows prose to width columns. Prose is always rewrapped: a diff that
195 // preserved the source wrapping would put the reviewer back where a line
196 // differ left them.
197 26 func wrap(s string, width int) []string {
198 26 fields := strings.Fields(s)
199 26 if len(fields) == 0 {
200 0 return []string{""}
201 0 }
202 26 if width <= 0 {
203 0 return []string{strings.Join(fields, " ")}
204 0 }
205 26 var out []string
206 26 line := fields[0]
207 163 for _, f := range fields[1:] {
208 163 if utf8.RuneCountInString(line)+1+utf8.RuneCountInString(f) > width {
209 7 out = append(out, line)
210 7 line = f
211 7 continue
212 }
213 156 line += " " + f
214 }
215 26 return append(out, line)
216 }