dte test coverage


Directory: ./
File: src/edit.c
Date: 2026-01-09 16:07:09
Coverage Exec Excl Total
Lines: 90.7% 205 0 226
Functions: 100.0% 9 0 9
Branches: 78.6% 77 0 98

Line Branch Exec Source
1 #include <string.h>
2 #include "edit.h"
3 #include "block.h"
4 #include "buffer.h"
5 #include "syntax/highlight.h"
6 #include "util/debug.h"
7 #include "util/list.h"
8 #include "util/xmalloc.h"
9
10 enum {
11 BLOCK_EDIT_SIZE = 512
12 };
13
14 468 static void sanity_check_blocks(const View *view, bool check_newlines)
15 {
16 468 if (!DEBUG_ASSERTIONS_ENABLED) {
17 return;
18 }
19
20 468 const Buffer *buffer = view->buffer;
21 468 const Block *blk = BLOCK(buffer->blocks.next);
22 468 const Block *cursor_blk = view->cursor.blk;
23 468 BUG_ON(list_empty(&buffer->blocks));
24 468 BUG_ON(view->cursor.offset > cursor_blk->size);
25
26
2/2
✓ Branch 6 → 7 taken 19 times.
✓ Branch 6 → 24 taken 449 times.
468 if (blk->size == 0) {
27 // The only time a zero-sized block is valid is when it's the
28 // first and only block
29 19 BUG_ON(buffer->blocks.next->next != &buffer->blocks);
30 19 BUG_ON(cursor_blk != blk);
31 19 block_sanity_check(blk);
32 19 return;
33 }
34
35 unsigned int cursor_seen = 0;
36
2/2
✓ Branch 24 → 12 taken 453 times.
✓ Branch 24 → 25 taken 449 times.
902 block_for_each(blk, &buffer->blocks) {
37 453 block_sanity_check(blk);
38 453 cursor_seen += (blk == cursor_blk);
39 453 BUG_ON(blk->size == 0);
40
41 // Non-empty blocks must ALWAYS end with a newline, since
42 // lines MAY NOT straddle multiple blocks
43 453 BUG_ON(check_newlines && blk->data[blk->size - 1] != '\n');
44 453 BUG_ON(blk->nl < 1);
45 453 BUG_ON(DEBUG > 2 && count_nl(blk->data, blk->size) != blk->nl);
46 }
47
48 449 BUG_ON(cursor_seen != 1);
49 }
50
51 515 static size_t copy_count_nl(char *dst, const char *src, size_t len)
52 {
53 515 size_t nl = 0;
54
2/2
✓ Branch 6 → 3 taken 28783 times.
✓ Branch 6 → 7 taken 515 times.
29298 for (size_t i = 0; i < len; i++) {
55 28783 dst[i] = src[i];
56
2/2
✓ Branch 3 → 4 taken 238 times.
✓ Branch 3 → 5 taken 28545 times.
28783 if (src[i] == '\n') {
57 238 nl++;
58 }
59 }
60 515 return nl;
61 }
62
63 335 static size_t insert_to_current(BlockIter *cursor, const char *buf, size_t len)
64 {
65 335 Block *blk = cursor->blk;
66 335 size_t offset = cursor->offset;
67 335 size_t size = blk->size + len;
68 335 block_grow(blk, size);
69 335 memmove(blk->data + offset + len, blk->data + offset, blk->size - offset);
70 335 size_t nl = copy_count_nl(blk->data + offset, buf, len);
71 335 blk->nl += nl;
72 335 blk->size = size;
73 335 return nl;
74 }
75
76 /*
77 * Combine current block and new data into smaller blocks:
78 *
79 * • Block _must_ contain whole lines
80 * • Block _must_ contain at least one line
81 * • Preferred maximum size of block is BLOCK_EDIT_SIZE
82 * • Size of any block can be larger than BLOCK_EDIT_SIZE only if there's
83 * a very long line
84 */
85 2 static size_t split_and_insert(BlockIter *cursor, const char *buf, size_t len)
86 {
87 2 Block *blk = cursor->blk;
88 2 ListHead *prev_node = blk->node.prev;
89 2 const char *buf1 = blk->data;
90 2 const char *buf2 = buf;
91 2 const char *buf3 = blk->data + cursor->offset;
92 2 size_t size1 = cursor->offset;
93 2 size_t size2 = len;
94 2 size_t size3 = blk->size - size1;
95 2 size_t total = size1 + size2 + size3;
96 2 size_t start = 0; // Beginning of new block
97 2 size_t size = 0; // Size of new block
98 2 size_t pos = 0; // Current position
99 2 size_t nl_added = 0;
100
101
2/2
✓ Branch 42 → 3 taken 28 times.
✓ Branch 42 → 43 taken 2 times.
30 while (start < total) {
102 // Size of new block if next line would be added
103 28 size_t new_size = 0;
104 28 size_t copied = 0;
105
106
2/2
✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 6 taken 23 times.
28 if (pos < size1) {
107 5 const char *nl = memchr(buf1 + pos, '\n', size1 - pos);
108
2/2
✓ Branch 4 → 5 taken 4 times.
✓ Branch 4 → 6 taken 1 time.
5 if (nl) {
109 4 new_size = nl - buf1 + 1 - start;
110 }
111 }
112
113
3/4
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 11 taken 4 times.
✓ Branch 6 → 7 taken 20 times.
✓ Branch 6 → 11 taken 4 times.
28 if (!new_size && pos < size1 + size2) {
114 20 size_t offset = 0;
115
2/2
✓ Branch 7 → 8 taken 18 times.
✓ Branch 7 → 9 taken 2 times.
20 if (pos > size1) {
116 18 offset = pos - size1;
117 }
118
119 20 const char *nl = memchr(buf2 + offset, '\n', size2 - offset);
120
1/2
✓ Branch 9 → 10 taken 20 times.
✗ Branch 9 → 11 not taken.
20 if (nl) {
121 20 new_size = size1 + nl - buf2 + 1 - start;
122 }
123 }
124
125
2/2
✓ Branch 11 → 12 taken 4 times.
✓ Branch 11 → 17 taken 24 times.
28 if (!new_size && pos < total) {
126 4 size_t offset = 0;
127
2/2
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 14 taken 2 times.
4 if (pos > size1 + size2) {
128 2 offset = pos - size1 - size2;
129 }
130
131 4 const char *nl = memchr(buf3 + offset, '\n', size3 - offset);
132
1/2
✓ Branch 14 → 15 taken 4 times.
✗ Branch 14 → 16 not taken.
4 if (nl) {
133 4 new_size = size1 + size2 + nl - buf3 + 1 - start;
134 } else {
135 new_size = total - start;
136 }
137 }
138
139
2/2
✓ Branch 17 → 18 taken 25 times.
✓ Branch 17 → 20 taken 3 times.
28 if (new_size <= BLOCK_EDIT_SIZE) {
140 // Fits
141 25 size = new_size;
142 25 pos = start + new_size;
143
2/2
✓ Branch 18 → 19 taken 23 times.
✓ Branch 18 → 22 taken 2 times.
25 if (pos < total) {
144 23 continue;
145 }
146 } else {
147 // Does not fit
148
1/2
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 24 taken 3 times.
3 if (!size) {
149 // One block containing one very long line
150 size = new_size;
151 pos = start + new_size;
152 }
153 }
154
155 2 BUG_ON(!size);
156 5 Block *new = block_new(size);
157
2/2
✓ Branch 25 → 26 taken 1 time.
✓ Branch 25 → 28 taken 4 times.
5 if (start < size1) {
158 1 size_t avail = size1 - start;
159 1 size_t count = MIN(size, avail);
160 1 new->nl += copy_count_nl(new->data, buf1 + start, count);
161 1 copied += count;
162 1 start += count;
163 }
164
3/4
✓ Branch 28 → 29 taken 5 times.
✗ Branch 28 → 32 not taken.
✓ Branch 29 → 30 taken 4 times.
✓ Branch 29 → 32 taken 1 time.
5 if (start >= size1 && start < size1 + size2) {
165 4 size_t offset = start - size1;
166 4 size_t avail = size2 - offset;
167 4 size_t count = MIN(size - copied, avail);
168 4 new->nl += copy_count_nl(new->data + copied, buf2 + offset, count);
169 4 copied += count;
170 4 start += count;
171 }
172
2/2
✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 37 taken 2 times.
5 if (start >= size1 + size2) {
173 3 size_t offset = start - size1 - size2;
174 3 size_t avail = size3 - offset;
175 3 size_t count = size - copied;
176 3 BUG_ON(count > avail);
177 3 new->nl += copy_count_nl(new->data + copied, buf3 + offset, count);
178 3 copied += count;
179 3 start += count;
180 }
181
182 5 new->size = size;
183 5 BUG_ON(copied != size);
184 5 list_insert_before(&new->node, &blk->node);
185
186 5 nl_added += new->nl;
187 5 size = 0;
188 }
189
190 2 cursor->blk = BLOCK(prev_node->next);
191
1/2
✗ Branch 45 → 44 not taken.
✓ Branch 45 → 46 taken 2 times.
2 while (cursor->offset > cursor->blk->size) {
192 cursor->offset -= cursor->blk->size;
193 cursor->blk = BLOCK(cursor->blk->node.next);
194 }
195
196 2 nl_added -= blk->nl;
197 2 block_free(blk);
198 2 return nl_added;
199 }
200
201 337 static size_t insert_bytes(BlockIter *cursor, const char *buf, size_t len)
202 {
203 // Blocks must contain whole lines.
204 // Last char of buf might not be newline.
205 337 block_iter_normalize(cursor);
206
207 337 Block *blk = cursor->blk;
208 337 size_t new_size = blk->size + len;
209
4/4
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 326 times.
✓ Branch 4 → 5 taken 7 times.
✓ Branch 4 → 6 taken 4 times.
337 if (new_size <= blk->alloc || new_size <= BLOCK_EDIT_SIZE) {
210 333 return insert_to_current(cursor, buf, len);
211 }
212
213
4/4
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 9 taken 1 time.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 1 time.
4 if (blk->nl <= 1 && !memchr(buf, '\n', len)) {
214 // Can't split this possibly very long line.
215 // insert_to_current() is much faster than split_and_insert().
216 2 return insert_to_current(cursor, buf, len);
217 }
218 2 return split_and_insert(cursor, buf, len);
219 }
220
221 337 void do_insert(View *view, const char *buf, size_t len)
222 {
223 337 Buffer *buffer = view->buffer;
224 337 size_t nl = insert_bytes(&view->cursor, buf, len);
225 337 buffer->nl += nl;
226 337 sanity_check_blocks(view, true);
227
228 337 view_update_cursor_y(view);
229
2/2
✓ Branch 5 → 6 taken 213 times.
✓ Branch 5 → 7 taken 124 times.
337 buffer_mark_lines_changed(buffer, view->cy, nl ? LONG_MAX : view->cy);
230
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 335 times.
337 if (buffer->syntax) {
231 2 hl_insert(&buffer->line_start_states, view->cy, nl);
232 }
233 337 }
234
235 19 static bool only_block(const Buffer *buffer, const Block *blk)
236 {
237
2/4
✓ Branch 2 → 3 taken 19 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 19 times.
19 return blk->node.prev == &buffer->blocks && blk->node.next == &buffer->blocks;
238 }
239
240 90 char *do_delete(View *view, size_t len, bool sanity_check_newlines)
241 {
242
1/2
✓ Branch 2 → 3 taken 90 times.
✗ Branch 2 → 34 not taken.
90 if (len == 0) {
243 return NULL;
244 }
245
246 90 ListHead *saved_prev_node = NULL;
247 90 Block *blk = view->cursor.blk;
248 90 size_t offset = view->cursor.offset;
249
2/2
✓ Branch 3 → 4 taken 39 times.
✓ Branch 3 → 5 taken 51 times.
90 if (!offset) {
250 // The block where the cursor is can become empty and thus
251 // may be deleted
252 39 saved_prev_node = blk->node.prev;
253 }
254
255 90 Buffer *buffer = view->buffer;
256 90 char *deleted = xmalloc(len);
257 90 size_t pos = 0;
258 90 size_t deleted_nl = 0;
259
260 90 while (pos < len) {
261 90 ListHead *next = blk->node.next;
262 90 size_t avail = blk->size - offset;
263 90 size_t count = MIN(len - pos, avail);
264 90 char *ptr = blk->data + offset;
265 90 size_t nl = copy_count_nl(deleted + pos, ptr, count);
266
2/2
✓ Branch 8 → 9 taken 63 times.
✓ Branch 8 → 10 taken 27 times.
90 if (count < avail) {
267 63 memmove(ptr, ptr + count, avail - count);
268 }
269
270 90 deleted_nl += nl;
271 90 buffer->nl -= nl;
272 90 blk->nl -= nl;
273 90 blk->size -= count;
274
3/4
✓ Branch 10 → 11 taken 19 times.
✓ Branch 10 → 13 taken 71 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 19 times.
90 if (!blk->size && !only_block(buffer, blk)) {
275 block_free(blk);
276 }
277
278 90 offset = 0;
279 90 pos += count;
280 90 blk = BLOCK(next);
281 270 BUG_ON(pos < len && next == &buffer->blocks);
282 }
283
284
2/2
✓ Branch 18 → 19 taken 39 times.
✓ Branch 18 → 22 taken 51 times.
90 if (saved_prev_node) {
285 // Cursor was at beginning of a block that was possibly deleted
286
1/2
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 39 times.
39 if (saved_prev_node->next == &buffer->blocks) {
287 view->cursor.blk = BLOCK(saved_prev_node);
288 view->cursor.offset = view->cursor.blk->size;
289 } else {
290 39 view->cursor.blk = BLOCK(saved_prev_node->next);
291 }
292 }
293
294 90 blk = view->cursor.blk;
295
296 90 if (
297
2/2
✓ Branch 22 → 23 taken 71 times.
✓ Branch 22 → 27 taken 19 times.
90 blk->size
298
1/2
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 27 taken 71 times.
71 && blk->data[blk->size - 1] != '\n'
299 && blk->node.next != &buffer->blocks
300 ) {
301 Block *next = BLOCK(blk->node.next);
302 size_t size = blk->size + next->size;
303 block_grow(blk, size);
304 memcpy(blk->data + blk->size, next->data, next->size);
305 blk->size = size;
306 blk->nl += next->nl;
307 block_free(next);
308 }
309
310 90 sanity_check_blocks(view, sanity_check_newlines);
311
312 90 view_update_cursor_y(view);
313
2/2
✓ Branch 29 → 30 taken 51 times.
✓ Branch 29 → 31 taken 39 times.
90 buffer_mark_lines_changed(buffer, view->cy, deleted_nl ? LONG_MAX : view->cy);
314
315
1/2
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 90 times.
90 if (buffer->syntax) {
316 hl_delete(&buffer->line_start_states, view->cy, deleted_nl);
317 }
318
319 return deleted;
320 }
321
322 46 char *do_replace(View *view, size_t del, const char *buf, size_t ins)
323 {
324 46 BUG_ON(del == 0);
325 46 BUG_ON(ins == 0);
326 46 block_iter_normalize(&view->cursor);
327
328 46 Block *blk = view->cursor.blk;
329 46 size_t offset = view->cursor.offset;
330 46 size_t avail = blk->size - offset;
331
2/2
✓ Branch 7 → 8 taken 5 times.
✓ Branch 7 → 9 taken 41 times.
46 if (del >= avail) {
332 5 goto slow;
333 }
334
335 41 size_t new_size = blk->size + ins - del;
336
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 13 taken 41 times.
41 if (new_size > BLOCK_EDIT_SIZE) {
337 // Should split
338 if (blk->nl > 1 || memchr(buf, '\n', ins)) {
339 // Most likely can be split
340 goto slow;
341 }
342 }
343
344 41 block_grow(blk, new_size);
345
346 // Modification is limited to one block
347 41 Buffer *buffer = view->buffer;
348 41 char *ptr = blk->data + offset;
349 41 char *deleted = xmalloc(del);
350 41 size_t del_nl = copy_count_nl(deleted, ptr, del);
351 41 blk->nl -= del_nl;
352 41 buffer->nl -= del_nl;
353
354
2/2
✓ Branch 16 → 17 taken 28 times.
✓ Branch 16 → 18 taken 13 times.
41 if (del != ins) {
355 28 memmove(ptr + ins, ptr + del, avail - del);
356 }
357
358 41 size_t ins_nl = copy_count_nl(ptr, buf, ins);
359 41 blk->nl += ins_nl;
360 41 buffer->nl += ins_nl;
361 41 blk->size = new_size;
362 41 sanity_check_blocks(view, true);
363 41 view_update_cursor_y(view);
364
365 // If the number of inserted and removed bytes are the same, some
366 // line(s) changed but the lines after them didn't move up or down
367
2/2
✓ Branch 21 → 22 taken 27 times.
✓ Branch 21 → 23 taken 14 times.
41 long max = (del_nl == ins_nl) ? view->cy + del_nl : LONG_MAX;
368 41 buffer_mark_lines_changed(buffer, view->cy, max);
369
370
1/2
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 32 taken 41 times.
41 if (buffer->syntax) {
371 hl_delete(&buffer->line_start_states, view->cy, del_nl);
372 hl_insert(&buffer->line_start_states, view->cy, ins_nl);
373 }
374
375 return deleted;
376
377 5 slow:
378 // The "sanity_check_newlines" argument of do_delete() is false here
379 // because it may be removing a terminating newline that do_insert()
380 // is going to insert again at a different position:
381 5 deleted = do_delete(view, del, false);
382 5 BUG_ON(!deleted); // do_delete() only returns NULL if len is 0
383 5 do_insert(view, buf, ins);
384 5 return deleted;
385 }
386