dte test coverage


Directory: ./
File: src/change.c
Date: 2025-11-12 12:04:10
Coverage Exec Excl Total
Lines: 90.9% 211 0 232
Functions: 100.0% 23 0 23
Branches: 80.2% 85 0 106

Line Branch Exec Source
1 #include <stdlib.h>
2 #include <string.h>
3 #include "change.h"
4 #include "buffer.h"
5 #include "command/error.h"
6 #include "edit.h"
7 #include "editor.h"
8 #include "util/debug.h"
9 #include "util/xmalloc.h"
10 #include "window.h"
11
12 static struct {
13 ChangeMergeEnum merge;
14 ChangeMergeEnum prev_merge;
15 // This doesn't need to be local to Buffer, because commands are atomic
16 Change *barrier;
17 } cs; // NOLINT(*-avoid-non-const-global-variables)
18
19 336 static Change *alloc_change(void)
20 {
21 336 return xcalloc1(sizeof(Change));
22 }
23
24 333 static void add_change(Buffer *buffer, Change *change)
25 {
26 333 Change *head = buffer->cur_change;
27 333 change->next = head;
28 333 head->prev = xrenew(head->prev, head->nr_prev + 1);
29 333 head->prev[head->nr_prev++] = change;
30 333 buffer->cur_change = change;
31 333 }
32
33 56 static bool is_change_chain_barrier(const Change *change)
34 {
35
4/4
✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 4 taken 35 times.
✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 5 taken 6 times.
56 return !change->ins_count && !change->del_count;
36 }
37
38 271 static Change *new_change(Buffer *buffer)
39 {
40
2/2
✓ Branch 2 → 3 taken 31 times.
✓ Branch 2 → 5 taken 240 times.
271 if (cs.barrier) {
41 /*
42 * We are recording series of changes (:replace for example)
43 * and now we have just made the first change so we have to
44 * mark beginning of the chain.
45 *
46 * We could have done this before when starting the change
47 * chain but then we may have ended up with an empty chain.
48 * We don't want to record empty changes ever.
49 */
50 31 add_change(buffer, cs.barrier);
51 31 cs.barrier = NULL;
52 }
53
54 271 Change *change = alloc_change();
55 271 add_change(buffer, change);
56 271 return change;
57 }
58
59 271 static size_t buffer_offset(const View *view)
60 {
61 271 return block_iter_get_offset(&view->cursor);
62 }
63
64 265 static void record_insert(View *view, size_t len)
65 {
66 265 BUG_ON(!len);
67
4/4
✓ Branch 4 → 5 taken 212 times.
✓ Branch 4 → 9 taken 53 times.
✓ Branch 5 → 6 taken 88 times.
✓ Branch 5 → 9 taken 124 times.
265 if (cs.merge == cs.prev_merge && cs.merge == CHANGE_MERGE_INSERT) {
68 88 Change *change = view->buffer->cur_change;
69 88 BUG_ON(change->del_count);
70 88 change->ins_count += len;
71 88 return;
72 }
73
74 177 Change *change = new_change(view->buffer);
75 177 change->offset = buffer_offset(view);
76 177 change->ins_count = len;
77 }
78
79 54 static void record_delete(View *view, char *buf, size_t len, bool move_after)
80 {
81 54 BUG_ON(!len);
82 54 BUG_ON(!buf);
83 54 bool del = (cs.merge == CHANGE_MERGE_DELETE);
84 54 bool erase = (cs.merge == CHANGE_MERGE_ERASE);
85
86 // Consecutive DELETE or ERASE operations of the same type can be merged
87 // into the same Change entry. For matching DELETE operations, reallocate
88 // `change->buf`, then append and free `buf`. For ERASE, do likewise but
89 // with the arguments reversed.
90
4/4
✓ Branch 6 → 7 taken 43 times.
✓ Branch 6 → 16 taken 11 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 16 taken 41 times.
54 if (cs.merge == cs.prev_merge && (del || erase)) {
91 2 Change *change = view->buffer->cur_change;
92
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 1 time.
2 char *left = del ? change->buf : buf; // Reallocated
93 2 char *right = del ? buf : change->buf; // Appended and freed
94
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
2 size_t left_len = del ? change->del_count : len;
95 2 size_t right_len = del ? len : change->del_count;
96
1/2
✓ Branch 11 → 13 taken 1 time.
✗ Branch 11 → 14 not taken.
2 change->offset -= del ? 0 : len;
97
98 2 change->del_count += len;
99 2 change->buf = xrealloc(left, change->del_count);
100 2 memcpy(change->buf + left_len, right, right_len);
101 2 free(right);
102 2 return;
103 }
104
105 52 Change *change = new_change(view->buffer);
106 52 change->offset = buffer_offset(view);
107 52 change->del_count = len;
108 52 change->move_after = move_after;
109 52 change->buf = buf;
110 }
111
112 42 static void record_replace(View *view, char *deleted, size_t del_count, size_t ins_count)
113 {
114 42 BUG_ON(del_count && !deleted);
115 42 BUG_ON(!del_count && deleted);
116 42 BUG_ON(!del_count && !ins_count);
117
118 42 Change *change = new_change(view->buffer);
119 42 change->offset = buffer_offset(view);
120 42 change->ins_count = ins_count;
121 42 change->del_count = del_count;
122 42 change->buf = deleted;
123 42 }
124
125 8011 void begin_change(ChangeMergeEnum m)
126 {
127 8011 cs.merge = m;
128 8011 }
129
130 7997 void end_change(void)
131 {
132 7997 cs.prev_merge = cs.merge;
133 7997 }
134
135 34 void begin_change_chain(void)
136 {
137 34 BUG_ON(cs.barrier);
138
139 // Allocate change chain barrier but add it to the change tree only if
140 // there will be any real changes
141 34 cs.barrier = alloc_change();
142 34 cs.merge = CHANGE_MERGE_NONE;
143 34 }
144
145 34 void end_change_chain(View *view)
146 {
147
2/2
✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 31 times.
34 if (cs.barrier) {
148 // There were no changes in this change chain
149 3 free(cs.barrier);
150 3 cs.barrier = NULL;
151 } else {
152 // There were some changes; add end of chain marker
153 31 add_change(view->buffer, alloc_change());
154 }
155 34 }
156
157 26 static void fix_cursors(const View *view, size_t offset, size_t del, size_t ins)
158 {
159 26 const Buffer *buffer = view->buffer;
160
2/2
✓ Branch 9 → 3 taken 52 times.
✓ Branch 9 → 10 taken 26 times.
78 for (size_t i = 0, n = buffer->views.count; i < n; i++) {
161 52 View *v = buffer->views.ptrs[i];
162
4/4
✓ Branch 3 → 4 taken 26 times.
✓ Branch 3 → 8 taken 26 times.
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 8 taken 19 times.
52 if (v != view && offset < v->saved_cursor_offset) {
163
1/2
✓ Branch 5 → 6 taken 7 times.
✗ Branch 5 → 7 not taken.
7 if (offset + del <= v->saved_cursor_offset) {
164 7 v->saved_cursor_offset -= del;
165 7 v->saved_cursor_offset += ins;
166 } else {
167 v->saved_cursor_offset = offset;
168 }
169 }
170 }
171 26 }
172
173 50 static void reverse_change(View *view, Change *change)
174 {
175 50 const size_t ins_count = change->ins_count;
176 50 const size_t del_count = change->del_count;
177 50 BUG_ON(!del_count && !ins_count);
178
179
2/2
✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 6 taken 24 times.
50 if (view->buffer->views.count > 1) {
180 // NOLINTNEXTLINE(readability-suspicious-call-argument)
181 26 fix_cursors(view, change->offset, ins_count, del_count);
182 }
183
184 50 block_iter_goto_offset(&view->cursor, change->offset);
185
186
2/2
✓ Branch 7 → 8 taken 15 times.
✓ Branch 7 → 12 taken 35 times.
50 if (ins_count == 0) {
187 // Convert delete to insert
188 15 do_insert(view, change->buf, del_count);
189
2/2
✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 11 times.
15 if (change->move_after) {
190 4 block_iter_skip_bytes(&view->cursor, del_count);
191 }
192 15 change->ins_count = del_count;
193 15 change->del_count = 0;
194 15 free(change->buf);
195 15 change->buf = NULL;
196 15 return;
197 }
198
199
2/2
✓ Branch 12 → 13 taken 31 times.
✓ Branch 12 → 15 taken 4 times.
35 if (del_count == 0) {
200 // Convert insert to delete
201 31 change->buf = do_delete(view, ins_count, true);
202 31 change->del_count = ins_count;
203 31 change->ins_count = 0;
204 31 return;
205 }
206
207 // Reverse replace
208 // NOLINTNEXTLINE(readability-suspicious-call-argument)
209 4 char *buf = do_replace(view, ins_count, change->buf, del_count);
210 4 free(change->buf);
211 4 change->buf = buf;
212 4 change->ins_count = del_count;
213 4 change->del_count = ins_count;
214 }
215
216 1024 bool undo(View *view)
217 {
218 1024 Change *change = view->buffer->cur_change;
219 1024 view_reset_preferred_x(view);
220
2/2
✓ Branch 3 → 4 taken 47 times.
✓ Branch 3 → 12 taken 977 times.
1024 if (!change->next) {
221 return false;
222 }
223
224
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 10 taken 44 times.
47 if (is_change_chain_barrier(change)) {
225 unsigned long count = 0;
226 9 while (1) {
227 6 change = change->next;
228
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 8 taken 3 times.
6 if (is_change_chain_barrier(change)) {
229 break;
230 }
231 3 reverse_change(view, change);
232 3 count++;
233 }
234
1/2
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 3 times.
3 if (count > 1) {
235 ErrorBuffer *ebuf = &view->window->editor->err;
236 info_msg(ebuf, "Undid %lu changes", count);
237 }
238 } else {
239 44 reverse_change(view, change);
240 }
241
242 47 view->buffer->cur_change = change->next;
243 47 return true;
244 }
245
246 6 bool redo(View *view, unsigned long change_id)
247 {
248 6 ErrorBuffer *ebuf = &view->window->editor->err;
249 6 Change *change = view->buffer->cur_change;
250 6 view_reset_preferred_x(view);
251
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 7 taken 5 times.
6 if (!change->prev) {
252 // Don't complain if change_id is 0
253
1/2
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 6 not taken.
1 if (change_id) {
254 1 error_msg(ebuf, "Nothing to redo");
255 }
256 1 return false;
257 }
258
259 5 const unsigned long nr_prev = change->nr_prev;
260 5 BUG_ON(nr_prev == 0);
261
2/2
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 12 taken 4 times.
5 if (change_id == 0) {
262 // Default to newest change
263 1 change_id = nr_prev - 1;
264
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 16 taken 1 time.
1 if (nr_prev > 1) {
265 unsigned long i = change_id + 1;
266 info_msg(ebuf, "Redoing newest (%lu) of %lu possible changes", i, nr_prev);
267 }
268 } else {
269
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 16 taken 2 times.
4 if (--change_id >= nr_prev) {
270
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 15 taken 1 time.
2 if (nr_prev == 1) {
271 1 return error_msg(ebuf, "There is only 1 possible change to redo");
272 }
273 1 return error_msg(ebuf, "There are only %lu possible changes to redo", nr_prev);
274 }
275 }
276
277 3 change = change->prev[change_id];
278
1/2
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 22 taken 3 times.
3 if (is_change_chain_barrier(change)) {
279 unsigned long count = 0;
280 while (1) {
281 change = change->prev[change->nr_prev - 1];
282 if (is_change_chain_barrier(change)) {
283 break;
284 }
285 reverse_change(view, change);
286 count++;
287 }
288 if (count > 1) {
289 info_msg(ebuf, "Redid %lu changes", count);
290 }
291 } else {
292 3 reverse_change(view, change);
293 }
294
295 3 view->buffer->cur_change = change;
296 3 return true;
297 }
298
299 84 void free_changes(Change *c)
300 {
301 97 top:
302
2/2
✓ Branch 5 → 4 taken 333 times.
✓ Branch 5 → 6 taken 97 times.
430 while (c->nr_prev) {
303 333 c = c->prev[c->nr_prev - 1];
304 }
305
306 // c is leaf now
307
2/2
✓ Branch 10 → 7 taken 333 times.
✓ Branch 10 → 11 taken 84 times.
417 while (c->next) {
308 333 Change *next = c->next;
309 333 free(c->buf);
310 333 free(c);
311
312 333 c = next;
313
2/2
✓ Branch 7 → 8 taken 13 times.
✓ Branch 7 → 9 taken 320 times.
333 if (--c->nr_prev) {
314 13 goto top;
315 }
316
317 // We have become leaf
318 320 free(c->prev);
319 }
320 84 }
321
322 265 void buffer_insert_bytes(View *view, const char *buf, const size_t len)
323 {
324 265 view_reset_preferred_x(view);
325
1/2
✓ Branch 3 → 4 taken 265 times.
✗ Branch 3 → 13 not taken.
265 if (len == 0) {
326 return;
327 }
328
329 265 size_t rec_len = len;
330
4/4
✓ Branch 4 → 5 taken 221 times.
✓ Branch 4 → 8 taken 44 times.
✓ Branch 5 → 6 taken 42 times.
✓ Branch 5 → 8 taken 179 times.
265 if (buf[len - 1] != '\n' && block_iter_is_eof(&view->cursor)) {
331 // Force newline at EOF
332 42 do_insert(view, "\n", 1);
333 42 rec_len++;
334 }
335
336 265 do_insert(view, buf, len);
337 265 record_insert(view, rec_len);
338
339
1/2
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 13 taken 265 times.
265 if (view->buffer->views.count > 1) {
340 fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0);
341 }
342 }
343
344 98 static bool would_delete_last_bytes(const View *view, size_t count)
345 {
346 98 const Block *blk = view->cursor.blk;
347 98 size_t offset = view->cursor.offset;
348 98 while (1) {
349 98 size_t avail = blk->size - offset;
350
2/2
✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 6 taken 91 times.
98 if (avail > count) {
351 return false;
352 }
353
354
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 7 times.
7 if (blk->node.next == view->cursor.head) {
355 return true;
356 }
357
358 count -= avail;
359 blk = BLOCK(blk->node.next);
360 offset = 0;
361 }
362 }
363
364 57 static void buffer_delete_bytes_internal(View *view, size_t len, bool move_after)
365 {
366 57 view_reset_preferred_x(view);
367
2/2
✓ Branch 3 → 4 taken 56 times.
✓ Branch 3 → 18 taken 1 time.
57 if (len == 0) {
368 return;
369 }
370
371 // Check if all newlines from EOF would be deleted
372
2/2
✓ Branch 5 → 6 taken 3 times.
✓ Branch 5 → 13 taken 53 times.
56 if (would_delete_last_bytes(view, len)) {
373 3 BlockIter bi = view->cursor;
374 3 CodePoint u;
375
3/4
✓ Branch 7 → 8 taken 3 times.
✗ Branch 7 → 12 not taken.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 12 taken 1 time.
3 if (block_iter_prev_char(&bi, &u) && u != '\n') {
376 // No newline before cursor
377
1/2
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 12 not taken.
2 if (--len == 0) {
378 2 begin_change(CHANGE_MERGE_NONE);
379 2 return;
380 }
381 }
382 }
383
384 54 record_delete(view, do_delete(view, len, true), len, move_after);
385
386
1/2
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 18 taken 54 times.
54 if (view->buffer->views.count > 1) {
387 fix_cursors(view, block_iter_get_offset(&view->cursor), len, 0);
388 }
389 }
390
391 48 void buffer_delete_bytes(View *view, size_t len)
392 {
393 48 buffer_delete_bytes_internal(view, len, false);
394 48 }
395
396 9 void buffer_erase_bytes(View *view, size_t len)
397 {
398 9 buffer_delete_bytes_internal(view, len, true);
399 9 }
400
401 281 void buffer_replace_bytes(View *view, size_t del_count, const char *ins, size_t ins_count)
402 {
403
2/2
✓ Branch 2 → 3 taken 231 times.
✓ Branch 2 → 5 taken 50 times.
281 if (del_count == 0) {
404 231 buffer_insert_bytes(view, ins, ins_count);
405 231 return;
406 }
407
2/2
✓ Branch 5 → 6 taken 8 times.
✓ Branch 5 → 8 taken 42 times.
50 if (ins_count == 0) {
408 8 buffer_delete_bytes(view, del_count);
409 8 return;
410 }
411
412 42 view_reset_preferred_x(view);
413
414 // Check if all newlines from EOF would be deleted
415
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 15 taken 38 times.
42 if (would_delete_last_bytes(view, del_count)) {
416
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 15 taken 4 times.
4 if (ins[ins_count - 1] != '\n') {
417 // Don't replace last newline
418 if (--del_count == 0) {
419 buffer_insert_bytes(view, ins, ins_count);
420 return;
421 }
422 }
423 }
424
425 42 char *deleted = do_replace(view, del_count, ins, ins_count);
426 42 record_replace(view, deleted, del_count, ins_count);
427
428
1/2
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 20 taken 42 times.
42 if (view->buffer->views.count > 1) {
429 fix_cursors(view, block_iter_get_offset(&view->cursor), del_count, ins_count);
430 }
431 }
432