]> git.giacomolli.uk - onechat/blob - view.go
Render Markdown with plain terminal styles
[onechat] / view.go
1 package main
2
3 import (
4         "fmt"
5         "math"
6         "math/bits"
7         "strconv"
8         "strings"
9 )
10
11 const (
12         // viewChatHigh and viewChatLow are the limits of the chat view in
13         // bytes.
14         viewChatHigh = 128000
15         viewChatLow  = 64000
16
17         // viewCompactionHigh and viewCompactionLow are the limits of the
18         // compaction view in bytes.
19         viewCompactionHigh = 32000
20         viewCompactionLow  = 16000
21
22         // viewPending is the text of a line that is not built yet.
23         viewPending = "(not summarized yet: zoom it)"
24 )
25
26 // newView returns an empty view with the limits high and low.
27 func newView(high, low int) *View {
28         return &View{
29                 high:  high,
30                 low:   low,
31                 nodes: make(map[NodeKey]Node),
32         }
33 }
34
35 // restoreView returns the view of state with the limits high and low. Each line
36 // must continue the one before it, and each line above level 0 must be built.
37 func restoreView(state ViewState, high, low int, tree ViewNodeReader) (*View, error) {
38         v := newView(high, low)
39         v.merging = state.Merging
40         for _, pair := range state.Lines {
41                 line := NodeKey{pair[0], pair[1]}
42                 if line.level < 0 || line.level >= bits.UintSize-2 || line.index < 0 || line.index<<line.level != v.end {
43                         return nil, fmt.Errorf("view line (%d, %d) does not start at message %d", line.level, line.index, v.end)
44                 }
45
46                 built, err := v.load(tree, line)
47                 if err != nil {
48                         return nil, err
49                 }
50
51                 if line.level > 0 && !built {
52                         return nil, fmt.Errorf("view line (%d, %d) is not built", line.level, line.index)
53                 }
54
55                 v.lines = append(v.lines, line)
56                 v.end = nodeEnd(line.level, line.index)
57         }
58
59         return v, nil
60 }
61
62 // View is a list of tree nodes that covers messages 0 to end-1 without a gap,
63 // oldest first. Each new message appends its leaf as a line. Once the size of
64 // the view goes above high, one batch merges the most due sibling pairs until
65 // the size is at most low. A batch merges only pairs whose parent is built. If
66 // the batch cannot get to low, it continues at each new message until it does.
67 //
68 // The view keeps the nodes of its built lines in memory, so its size and its
69 // text need no read of the tree. The size of a line is the size of its node
70 // text, or the size of the pending text when the node is not built.
71 //
72 // For the sibling pair (l, i) and (l, i+1), due is (T - last) / 2^l, with T the
73 // number of messages and last the ID of the last message of the pair. That is
74 // how long ago the pair ended, measured in its own line size. With a budget of
75 // the length of the rollback push list of the reference design, this order
76 // makes the same merges as the push. The oldest pair wins a tie.
77 type View struct {
78         high    int
79         low     int
80         lines   []NodeKey
81         nodes   map[NodeKey]Node
82         end     int
83         merging bool
84 }
85
86 // State returns the saved form of the view.
87 func (v *View) State() ViewState {
88         state := ViewState{
89                 Lines:   make([][2]int, len(v.lines)),
90                 Merging: v.merging,
91         }
92
93         for n, line := range v.lines {
94                 state.Lines[n] = [2]int{line.level, line.index}
95         }
96
97         return state
98 }
99
100 // Append adds the leaf of the next message as a line and merges when the view
101 // is too large. It tells if a merge changed the view.
102 func (v *View) Append(tree ViewNodeReader) (bool, error) {
103         line := NodeKey{0, v.end}
104         _, err := v.load(tree, line)
105         if err != nil {
106                 return false, err
107         }
108
109         v.lines = append(v.lines, line)
110         v.end++
111
112         return v.fit(tree)
113 }
114
115 // Coarsen takes each merge of other, a view of the same messages, and then
116 // merges in one batch until the size is at most low. Each line of the result is
117 // the coarser of the two lines that start at its first message. Dyadic ranges
118 // either nest or do not overlap, so the result covers the messages without a
119 // gap.
120 func (v *View) Coarsen(other *View, tree ViewNodeReader) error {
121         lines := make([]NodeKey, 0, len(v.lines))
122         nodes := make(map[NodeKey]Node, len(v.nodes))
123         mine, theirs := 0, 0
124         for start := 0; start < v.end; {
125                 line := v.lines[mine]
126                 node, built := v.nodes[line]
127                 if other.lines[theirs].level > line.level {
128                         line = other.lines[theirs]
129                         node, built = other.nodes[line]
130                 }
131
132                 lines = append(lines, line)
133                 if built {
134                         nodes[line] = node
135                 }
136
137                 start = nodeEnd(line.level, line.index)
138                 for mine < len(v.lines) && v.lines[mine].index<<v.lines[mine].level < start {
139                         mine++
140                 }
141
142                 for theirs < len(other.lines) && other.lines[theirs].index<<other.lines[theirs].level < start {
143                         theirs++
144                 }
145         }
146
147         v.lines = lines
148         v.nodes = nodes
149         v.merging = true
150         _, err := v.fit(tree)
151
152         return err
153 }
154
155 // Refresh keeps node when it is the node of a pending line.
156 func (v *View) Refresh(node Node) {
157         key := NodeKey{node.Level, node.Index}
158         for n := len(v.lines) - 1; n >= 0; n-- {
159                 line := v.lines[n]
160                 if line == key {
161                         v.nodes[key] = node
162                         return
163                 }
164
165                 if line.index<<line.level <= key.index<<key.level {
166                         return
167                 }
168         }
169 }
170
171 // Pending tells if a line of the view is not built yet.
172 func (v *View) Pending() bool {
173         for _, line := range v.lines {
174                 _, built := v.nodes[line]
175                 if !built {
176                         return true
177                 }
178         }
179
180         return false
181 }
182
183 // Render returns the view as the model reads it: one line "id+n|text" for each
184 // node inside <chat> tags, with the newlines of the text as spaces.
185 func (v *View) Render() string {
186         return "<chat>\n" + strings.Join(v.LinesWithout(nil), "") + "</chat>"
187 }
188
189 // LinesWithout returns the lines of the view as the model reads them, each with
190 // its newline, without the leaf lines of the messages in skip. A line that
191 // merges such a message with others stays.
192 func (v *View) LinesWithout(skip map[int]bool) []string {
193         lines := make([]string, 0, len(v.lines))
194         for _, line := range v.lines {
195                 if line.level == 0 && skip[line.index] {
196                         continue
197                 }
198
199                 var text strings.Builder
200
201                 v.renderLine(&text, line)
202
203                 lines = append(lines, text.String())
204         }
205
206         return lines
207 }
208
209 // Context returns the lines of the view before message end as the model reads
210 // them. It stops at the first line that is not built, so a compaction never
211 // sees a pending mark or a part of a message.
212 func (v *View) Context(end int) string {
213         var text strings.Builder
214
215         text.WriteString("<chat>\n")
216
217         for _, line := range v.lines {
218                 _, built := v.nodes[line]
219                 if !built || nodeEnd(line.level, line.index) > end {
220                         break
221                 }
222
223                 v.renderLine(&text, line)
224         }
225
226         text.WriteString("</chat>")
227
228         return text.String()
229 }
230
231 // load keeps the node of line when it is built, and tells if it is.
232 func (v *View) load(tree ViewNodeReader, line NodeKey) (bool, error) {
233         node, built, err := tree.Node(line.level, line.index)
234         if err != nil || !built {
235                 return false, err
236         }
237
238         v.nodes[line] = node
239
240         return true, nil
241 }
242
243 // fit merges the most due pairs while a batch runs, and tells if a merge
244 // changed the view.
245 func (v *View) fit(tree ViewNodeReader) (bool, error) {
246         size := v.Size()
247         if size > v.high {
248                 v.merging = true
249         }
250
251         merged := false
252         for v.merging && size > v.low {
253                 best, err := v.mostDue(tree)
254                 if err != nil || best < 0 {
255                         return merged, err
256                 }
257
258                 left, right := v.lines[best], v.lines[best+1]
259                 parent := NodeKey{left.level + 1, left.index / 2}
260                 _, err = v.load(tree, parent)
261                 if err != nil {
262                         return merged, err
263                 }
264
265                 size += v.lineSize(parent) - v.lineSize(left) - v.lineSize(right)
266                 delete(v.nodes, left)
267                 delete(v.nodes, right)
268
269                 v.lines[best] = parent
270                 v.lines = append(v.lines[:best+1], v.lines[best+2:]...)
271                 merged = true
272         }
273
274         v.merging = false
275
276         return merged, nil
277 }
278
279 // renderLine writes one view line, "id+n|text", and a newline.
280 func (v *View) renderLine(text *strings.Builder, line NodeKey) {
281         body := viewPending
282         node, built := v.nodes[line]
283         if built {
284                 body = strings.ReplaceAll(node.Text, "\n", " ")
285         }
286
287         text.WriteString(strconv.Itoa(line.index << line.level))
288         text.WriteByte('+')
289         text.WriteString(strconv.Itoa(1 << line.level))
290         text.WriteByte('|')
291         text.WriteString(body)
292         text.WriteByte('\n')
293 }
294
295 // Size returns the sum of the line sizes.
296 func (v *View) Size() int {
297         size := 0
298         for _, line := range v.lines {
299                 size += v.lineSize(line)
300         }
301
302         return size
303 }
304
305 // mostDue returns the position of the left line of the most due sibling pair
306 // whose parent is built, or -1 when there is no such pair.
307 func (v *View) mostDue(tree ViewNodeReader) (int, error) {
308         best := -1
309         bestDue := 0.0
310         for n := 0; n+1 < len(v.lines); n++ {
311                 left, right := v.lines[n], v.lines[n+1]
312                 if left.index%2 != 0 || right.level != left.level || right.index != left.index+1 {
313                         continue
314                 }
315
316                 last := nodeEnd(right.level, right.index) - 1
317                 due := math.Ldexp(float64(v.end-last), -left.level)
318                 if best >= 0 && due <= bestDue {
319                         continue
320                 }
321
322                 built, err := tree.Built(left.level+1, left.index/2)
323                 if err != nil {
324                         return -1, fmt.Errorf("check parent node: %w", err)
325                 }
326
327                 if built {
328                         best = n
329                         bestDue = due
330                 }
331         }
332
333         return best, nil
334 }
335
336 // lineSize returns the size of a view line.
337 func (v *View) lineSize(line NodeKey) int {
338         node, built := v.nodes[line]
339         if !built {
340                 return len(viewPending)
341         }
342
343         return node.Size
344 }
345
346 // ViewState is the saved form of a View. The limits are not part of it.
347 type ViewState struct {
348         Lines   [][2]int `json:"lines"`
349         Merging bool     `json:"merging"`
350 }
351
352 // ViewNodeReader gives the built nodes of a tree.
353 type ViewNodeReader interface {
354         Built(level, index int) (bool, error)
355         Node(level, index int) (Node, bool, error)
356 }