dte test coverage


Directory: ./
File: src/window.c
Date: 2025-07-19 20:13:10
Exec Total Coverage
Lines: 254 272 93.4%
Functions: 32 32 100.0%
Branches: 98 120 81.7%

Line Branch Exec Source
1 #include <errno.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include "window.h"
5 #include "command/error.h"
6 #include "editor.h"
7 #include "file-history.h"
8 #include "load-save.h"
9 #include "lock.h"
10 #include "move.h"
11 #include "util/path.h"
12 #include "util/strtonum.h"
13 #include "util/xmalloc.h"
14 #include "util/xstring.h"
15
16 13 Window *new_window(EditorState *e)
17 {
18 13 Window *window = xcalloc1(sizeof(Window));
19 13 window->editor = e;
20 13 return window;
21 }
22
23 84 View *window_add_buffer(Window *window, Buffer *buffer)
24 {
25 // We rely on this being 0, for implicit initialization of
26 // View::selection and View::select_mode
27 84 static_assert(SELECT_NONE == 0);
28
29 84 View *view = xmalloc(sizeof(*view));
30 84 *view = (View) {
31 .buffer = buffer,
32 .window = window,
33 84 .cursor = block_iter(buffer),
34 };
35
36 84 ptr_array_append(&buffer->views, view);
37 84 ptr_array_append(&window->views, view);
38 84 window->update_tabbar = true;
39 84 return view;
40 }
41
42 49 View *window_open_empty_buffer(Window *window)
43 {
44 49 EditorState *e = window->editor;
45 49 return window_add_buffer(window, open_empty_buffer(&e->buffers, &e->options));
46 }
47
48 29 View *window_open_buffer (
49 Window *window,
50 const char *filename,
51 bool must_exist,
52 const char *encoding
53 ) {
54 29 ErrorBuffer *ebuf = &window->editor->err;
55
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→5) taken 28 times.
29 if (unlikely(filename[0] == '\0')) {
56 1 error_msg(ebuf, "Empty filename not allowed");
57 1 return NULL;
58 }
59
60 28 char *absolute = path_absolute(filename);
61
2/2
✓ Branch 0 (6→7) taken 1 times.
✓ Branch 1 (6→11) taken 27 times.
28 if (!absolute) {
62 1 bool nodir = (errno == ENOENT); // New file in non-existing dir (usually a mistake)
63
1/2
✗ Branch 0 (7→8) not taken.
✓ Branch 1 (7→9) taken 1 times.
1 const char *err = nodir ? "Directory does not exist" : strerror(errno);
64 1 error_msg(ebuf, "Error opening %s: %s", filename, err);
65 1 return NULL;
66 }
67
68 27 EditorState *e = window->editor;
69 27 Buffer *buffer = find_buffer(&e->buffers, absolute);
70
1/2
✗ Branch 0 (12→13) not taken.
✓ Branch 1 (12→19) taken 27 times.
27 if (buffer) {
71 // File already open in editor
72 if (!streq(absolute, buffer->abs_filename)) {
73 const char *bufname = buffer_filename(buffer);
74 char *s = short_filename(absolute, &e->home_dir);
75 info_msg(ebuf, "%s and %s are the same file", s, bufname); // Hard links
76 free(s);
77 }
78 free(absolute);
79 return window_find_or_create_view(window, buffer);
80 }
81
82 27 buffer = buffer_new(&e->buffers, &e->options, encoding);
83
2/2
✓ Branch 0 (21→22) taken 1 times.
✓ Branch 1 (21→24) taken 26 times.
27 if (!load_buffer(buffer, filename, &e->options, &e->err, must_exist)) {
84 1 buffer_remove_unlock_and_free(&e->buffers, buffer, &e->err, &e->locks_ctx);
85 1 free(absolute);
86 1 return NULL;
87 }
88
89 26 BUG_ON(!absolute);
90 26 BUG_ON(!path_is_absolute(absolute));
91 26 buffer->abs_filename = absolute;
92 26 buffer_update_short_filename(buffer, &e->home_dir);
93
94
1/2
✗ Branch 0 (27→28) not taken.
✓ Branch 1 (27→32) taken 26 times.
26 if (e->options.lock_files) {
95 if (!lock_file(&e->locks_ctx, ebuf, buffer->abs_filename)) {
96 buffer->readonly = true;
97 } else {
98 buffer->locked = true;
99 }
100 }
101
102
3/6
✓ Branch 0 (32→33) taken 26 times.
✗ Branch 1 (32→38) not taken.
✓ Branch 2 (33→34) taken 26 times.
✗ Branch 3 (33→38) not taken.
✗ Branch 4 (35→36) not taken.
✓ Branch 5 (35→38) taken 26 times.
26 if (buffer->file.mode != 0 && !buffer->readonly && access(filename, W_OK)) {
103 error_msg(ebuf, "No write permission to %s, marking read-only", filename);
104 buffer->readonly = true;
105 }
106
107 26 return window_add_buffer(window, buffer);
108 }
109
110 1 View *window_find_or_create_view(Window *window, Buffer *buffer)
111 {
112 1 View *view = window_find_view(window, buffer);
113
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→6) taken 1 times.
1 if (!view) {
114 // Buffer isn't open in this window; create a new view of it
115 view = window_add_buffer(window, buffer);
116 view->cursor = buffer_get_first_view(buffer)->cursor;
117 }
118 1 return view;
119 }
120
121 1 View *window_find_view(Window *window, Buffer *buffer)
122 {
123
1/2
✓ Branch 0 (5→3) taken 1 times.
✗ Branch 1 (5→6) not taken.
1 for (size_t i = 0, n = buffer->views.count; i < n; i++) {
124 1 View *view = buffer->views.ptrs[i];
125
1/2
✗ Branch 0 (3→4) not taken.
✓ Branch 1 (3→6) taken 1 times.
1 if (view->window == window) {
126 return view;
127 }
128 }
129 // Buffer isn't open in this window
130 return NULL;
131 }
132
133 4 size_t window_count_uncloseable_views(const Window *window, View **first_uncloseable)
134 {
135 4 const View *current_view = window->view;
136 4 View *first = NULL;
137 4 void **ptrs = window->views.ptrs;
138 4 size_t nr_uncloseable = 0;
139
140
2/2
✓ Branch 0 (9→3) taken 6 times.
✓ Branch 1 (9→10) taken 4 times.
10 for (size_t i = 0, n = window->views.count; i < n; i++) {
141 6 View *view = ptrs[i];
142
2/2
✓ Branch 0 (4→5) taken 4 times.
✓ Branch 1 (4→6) taken 2 times.
6 if (view_can_close(view)) {
143 4 continue;
144 }
145 2 nr_uncloseable++;
146 // Always use the current view as the out-param, if uncloseable
147
1/2
✓ Branch 0 (6→7) taken 2 times.
✗ Branch 1 (6→8) not taken.
2 if (!first || view == current_view) {
148 2 first = view;
149 }
150 }
151
152 4 *first_uncloseable = first;
153 4 return nr_uncloseable;
154 }
155
156 // Remove the View found at `window->views.ptrs[view_idx]`, then update
157 // history/locks/etc. (if applicable) and free the allocation
158 84 void window_remove_view_at_index(Window *window, size_t view_idx)
159 {
160 84 View *view = ptr_array_remove_index(&window->views, view_idx);
161
2/2
✓ Branch 0 (3→4) taken 2 times.
✓ Branch 1 (3→5) taken 82 times.
84 if (view == window->prev_view) {
162 2 window->prev_view = NULL;
163 }
164
165 84 EditorState *e = window->editor;
166
2/2
✓ Branch 0 (5→6) taken 37 times.
✓ Branch 1 (5→7) taken 47 times.
84 if (view == e->view) {
167 37 e->view = NULL;
168 37 e->buffer = NULL;
169 }
170
171 84 Buffer *buffer = view->buffer;
172 84 ptr_array_remove(&buffer->views, view);
173
174
2/2
✓ Branch 0 (8→9) taken 83 times.
✓ Branch 1 (8→13) taken 1 times.
84 if (buffer->views.count == 0) {
175
4/4
✓ Branch 0 (9→10) taken 82 times.
✓ Branch 1 (9→12) taken 1 times.
✓ Branch 2 (10→11) taken 44 times.
✓ Branch 3 (10→12) taken 38 times.
83 if (buffer->options.file_history && buffer->abs_filename) {
176 44 const char *path = buffer->abs_filename;
177 44 FileHistory *hist = &e->file_history;
178 44 file_history_append(hist, view->cy + 1, view->cx_char + 1, path);
179 }
180 83 buffer_remove_unlock_and_free(&e->buffers, buffer, &e->err, &e->locks_ctx);
181 }
182
183 84 window->update_tabbar = true;
184 84 free(view);
185 84 }
186
187 14 static void window_remove_views(Window *window)
188 {
189 // This loop terminates when `0--` wraps around to SIZE_MAX
190
2/2
✓ Branch 0 (5→3) taken 58 times.
✓ Branch 1 (5→6) taken 14 times.
72 for (size_t n = window->views.count, i = n - 1; i < n; i--) {
191 58 window_remove_view_at_index(window, i);
192 }
193 14 }
194
195 // NOTE: window->frame isn't removed
196 13 void window_free(Window *window)
197 {
198 13 window_remove_views(window);
199 13 ptr_array_free_array(&window->views);
200 13 window->frame = NULL;
201 13 free(window);
202 13 }
203
204 26 void window_close_current_view(Window *window)
205 {
206 26 size_t idx = view_remove(window->view);
207
2/2
✓ Branch 0 (3→4) taken 21 times.
✓ Branch 1 (3→5) taken 5 times.
26 if (window->prev_view) {
208 21 window->view = window->prev_view;
209 21 window->prev_view = NULL;
210 21 return;
211 }
212
213 5 const PointerArray *views = &window->views;
214
2/2
✓ Branch 0 (5→6) taken 1 times.
✓ Branch 1 (5→7) taken 4 times.
5 if (views->count == 0) {
215 1 window_open_empty_buffer(window);
216 }
217
218 5 idx -= (views->count == idx);
219 5 window->view = views->ptrs[idx];
220 }
221
222 4 static void restore_cursor_from_history(const FileHistory *hist, View *view)
223 {
224 4 unsigned long row, col;
225
2/2
✓ Branch 0 (3→4) taken 1 times.
✓ Branch 1 (3→5) taken 3 times.
4 if (file_history_find(hist, view->buffer->abs_filename, &row, &col)) {
226 1 move_to_filepos(view, row, col);
227 }
228 4 }
229
230 // Make `view` the current View in the editor, so that it takes input
231 // focus and becomes the active tab in its Window
232 95 void set_view(View *view)
233 {
234 95 EditorState *e = view->window->editor;
235
2/2
✓ Branch 0 (2→3) taken 85 times.
✓ Branch 1 (2→19) taken 10 times.
95 if (e->view == view) {
236 return;
237 }
238
239 // Forget `prev_view` when switching views. cmd_open(), cmd_reopen(),
240 // cmd_show() and cmd_exec() subsequently set it as appropriate, via
241 // window_open_new_file() or maybe_set_view().
242
2/2
✓ Branch 0 (3→4) taken 83 times.
✓ Branch 1 (3→5) taken 2 times.
85 if (e->window) {
243 83 e->window->prev_view = NULL;
244 }
245
246 85 e->view = view;
247 85 e->buffer = view->buffer;
248 85 e->window = view->window; // Parent Window becomes current Window
249 85 e->window->view = view;
250
251
2/2
✓ Branch 0 (5→6) taken 59 times.
✓ Branch 1 (5→10) taken 26 times.
85 if (!view->buffer->setup) {
252 // Run deferred initialization for view->buffer. This is done when
253 // buffers are first viewed instead of when they're opened, so as
254 // to allow opening many files without a sudden flood of extra work
255 // (e.g. loading multiple dte-syntax(5) files).
256 59 buffer_setup(e, view->buffer);
257
3/4
✓ Branch 0 (7→8) taken 59 times.
✗ Branch 1 (7→10) not taken.
✓ Branch 2 (8→9) taken 4 times.
✓ Branch 3 (8→10) taken 55 times.
59 if (view->buffer->options.file_history && view->buffer->abs_filename) {
258 4 restore_cursor_from_history(&e->file_history, view);
259 }
260 }
261
262 // view.cursor can be invalid if same buffer was modified from another view
263
2/2
✓ Branch 0 (10→11) taken 1 times.
✓ Branch 1 (10→13) taken 84 times.
85 if (view->restore_cursor) {
264 1 view->cursor.blk = BLOCK(view->buffer->blocks.next);
265 1 block_iter_goto_offset(&view->cursor, view->saved_cursor_offset);
266 1 view->restore_cursor = false;
267 1 view->saved_cursor_offset = 0;
268 }
269
270 // Save cursor states of views sharing same buffer
271 85 void **ptrs = view->buffer->views.ptrs;
272
2/2
✓ Branch 0 (18→14) taken 86 times.
✓ Branch 1 (18→19) taken 85 times.
171 for (size_t i = 0, n = view->buffer->views.count; i < n; i++) {
273 86 View *other = ptrs[i];
274
2/2
✓ Branch 0 (14→15) taken 1 times.
✓ Branch 1 (14→17) taken 85 times.
86 if (other != view) {
275 1 other->saved_cursor_offset = block_iter_get_offset(&other->cursor);
276 1 other->restore_cursor = true;
277 }
278 }
279 }
280
281 43 View *window_open_new_file(Window *window)
282 {
283 43 View *prev = window->view;
284 43 View *view = window_open_empty_buffer(window);
285 43 set_view(view);
286 43 window->prev_view = prev;
287 43 return view;
288 }
289
290 3 static bool buffer_is_untouched(const Buffer *b)
291 {
292
5/6
✓ Branch 0 (2→3) taken 2 times.
✓ Branch 1 (2→5) taken 1 times.
✓ Branch 2 (3→4) taken 1 times.
✓ Branch 3 (3→5) taken 1 times.
✗ Branch 4 (4→5) not taken.
✓ Branch 5 (4→6) taken 1 times.
3 return !b->abs_filename && b->change_head.nr_prev == 0 && !b->display_filename;
293 }
294
295 6 static View *maybe_set_view(Window *window, View *view, View *prev, bool set_prev)
296 {
297
2/2
✓ Branch 0 (2→3) taken 3 times.
✓ Branch 1 (2→12) taken 3 times.
6 if (view && view != prev) {
298
3/4
✓ Branch 0 (3→4) taken 3 times.
✗ Branch 1 (3→6) not taken.
✓ Branch 2 (4→5) taken 2 times.
✓ Branch 3 (4→6) taken 1 times.
3 bool useless_prev = prev && buffer_is_untouched(prev->buffer);
299 3 set_view(view);
300
3/4
✓ Branch 0 (7→8) taken 1 times.
✓ Branch 1 (7→10) taken 2 times.
✗ Branch 2 (8→9) not taken.
✓ Branch 3 (8→10) taken 1 times.
3 if (useless_prev && window->views.count == 2) {
301 // If window contains only one untouched buffer it'll be closed
302 // after opening another view. This is done because closing the
303 // last view (with window_close_current_view()) causes an empty
304 // view to be opened (windows must contain at least one buffer).
305 view_remove(prev);
306
2/2
✓ Branch 0 (10→11) taken 2 times.
✓ Branch 1 (10→12) taken 1 times.
3 } else if (set_prev) {
307 2 window->prev_view = prev;
308 }
309 }
310 6 return view;
311 }
312
313 1 View *window_open_file(Window *window, const char *filename, const char *encoding)
314 {
315 1 View *prev = window->view;
316 1 View *view = window_open_buffer(window, filename, false, encoding);
317 1 return maybe_set_view(window, view, prev, true);
318 }
319
320 // Open multiple files in window and return the first opened View
321 5 View *window_open_files(Window *window, char **filenames, const char *encoding)
322 {
323 5 View *prev = window->view;
324 5 View *first = NULL;
325 5 size_t prev_nr_views = window->views.count;
326
327
2/2
✓ Branch 0 (7→3) taken 27 times.
✓ Branch 1 (7→8) taken 5 times.
32 for (size_t i = 0; filenames[i]; i++) {
328 27 View *view = window_open_buffer(window, filenames[i], false, encoding);
329
2/2
✓ Branch 0 (4→5) taken 2 times.
✓ Branch 1 (4→6) taken 25 times.
27 if (view && !first) {
330 2 first = view;
331 }
332 }
333
334 // `Window::prev_view` is set only if a single new view was opened
335 5 size_t nr_new_views = window->views.count - prev_nr_views;
336 5 return maybe_set_view(window, first, prev, nr_new_views == 1);
337 }
338
339 21 void buffer_mark_tabbars_changed(Buffer *buffer)
340 {
341
2/2
✓ Branch 0 (4→3) taken 22 times.
✓ Branch 1 (4→5) taken 21 times.
43 for (size_t i = 0, n = buffer->views.count; i < n; i++) {
342 22 View *view = buffer->views.ptrs[i];
343 22 view->window->update_tabbar = true;
344 }
345 21 }
346
347 62 static int line_numbers_width(const GlobalOptions *options, const View *view)
348 {
349
4/4
✓ Branch 0 (2→3) taken 9 times.
✓ Branch 1 (2→5) taken 53 times.
✓ Branch 2 (3→4) taken 6 times.
✓ Branch 3 (3→5) taken 3 times.
62 if (!options->show_line_numbers || !view) {
350 return 0;
351 }
352 6 size_t width = size_str_width(view->buffer->nl) + 1;
353 6 return MAX(width, LINE_NUMBERS_MIN_WIDTH);
354 }
355
356 41 static int edit_x_offset(const GlobalOptions *options, const View *view)
357 {
358 41 return line_numbers_width(options, view);
359 }
360
361 41 static int edit_y_offset(const GlobalOptions *options)
362 {
363 41 return options->tab_bar ? 1 : 0;
364 }
365
366 21 static void set_edit_size(Window *window, const GlobalOptions *options)
367 {
368 21 int xo = edit_x_offset(options, window->view);
369 21 int yo = edit_y_offset(options);
370 21 window->edit_w = window->w - xo;
371 21 window->edit_h = window->h - yo - 1; // statusline
372 21 window->edit_x = window->x + xo;
373 21 }
374
375 21 void window_calculate_line_numbers(Window *window)
376 {
377 21 const GlobalOptions *options = &window->editor->options;
378 21 int w = line_numbers_width(options, window->view);
379
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 20 times.
21 if (w != window->line_numbers.width) {
380 1 window->line_numbers.width = w;
381 1 window->line_numbers.first = 0;
382 1 window->line_numbers.last = 0;
383 1 mark_all_lines_changed(window->view->buffer);
384 }
385 21 set_edit_size(window, options);
386 21 }
387
388 20 void window_set_coordinates(Window *window, int x, int y)
389 {
390 20 const GlobalOptions *options = &window->editor->options;
391 20 window->x = x;
392 20 window->y = y;
393 20 window->edit_x = x + edit_x_offset(options, window->view);
394 20 window->edit_y = y + edit_y_offset(options);
395 20 }
396
397 21 void window_set_size(Window *window, int w, int h)
398 {
399 21 window->w = w;
400 21 window->h = h;
401 21 window_calculate_line_numbers(window);
402 21 }
403
404 7 unsigned int window_get_scroll_margin(const Window *window)
405 {
406 7 int edit_h = window->edit_h;
407
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→4) taken 7 times.
7 if (unlikely(edit_h <= 2)) {
408 return 0;
409 }
410
411 unsigned int max = (edit_h - 1) / 2;
412 unsigned int scroll_margin = window->editor->options.scroll_margin;
413 return MIN(max, scroll_margin);
414 }
415
416 // Recursion is bounded by the number of descendant frames, which is
417 // typically not more than 5 or so
418 // NOLINTNEXTLINE(misc-no-recursion)
419 12 void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data)
420 {
421
2/2
✓ Branch 0 (2→3) taken 10 times.
✓ Branch 1 (2→5) taken 2 times.
12 if (frame->window) {
422 10 func(frame->window, data);
423 10 return;
424 }
425
426 2 void **ptrs = frame->frames.ptrs;
427
2/2
✓ Branch 0 (8→6) taken 4 times.
✓ Branch 1 (8→9) taken 2 times.
6 for (size_t i = 0, n = frame->frames.count; i < n; i++) {
428 4 frame_for_each_window(ptrs[i], func, data);
429 }
430 }
431
432 typedef struct {
433 const Window *const target; // Window to search for (set at init.)
434 Window *first; // Window passed in first callback invocation
435 Window *last; // Window passed in last callback invocation
436 Window *prev; // Window immediately before target (if any)
437 Window *next; // Window immediately after target (if any)
438 bool found; // Set to true when target is found
439 } WindowCallbackData;
440
441 6 static void find_prev_and_next(Window *window, void *ud)
442 {
443 6 WindowCallbackData *data = ud;
444 6 data->last = window;
445
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→6) taken 5 times.
6 if (data->found) {
446
1/2
✓ Branch 0 (3→4) taken 1 times.
✗ Branch 1 (3→5) not taken.
1 if (!data->next) {
447 1 data->next = window;
448 }
449 1 return;
450 }
451
2/2
✓ Branch 0 (6→7) taken 4 times.
✓ Branch 1 (6→8) taken 1 times.
5 if (!data->first) {
452 4 data->first = window;
453 }
454
2/2
✓ Branch 0 (8→9) taken 4 times.
✓ Branch 1 (8→10) taken 1 times.
5 if (window == data->target) {
455 4 data->found = true;
456 4 return;
457 }
458 1 data->prev = window;
459 }
460
461 1 Window *window_prev(Window *window)
462 {
463 1 WindowCallbackData data = {.target = window};
464 1 frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data);
465 1 BUG_ON(!data.found);
466
1/2
✓ Branch 0 (5→6) taken 1 times.
✗ Branch 1 (5→7) not taken.
1 return data.prev ? data.prev : data.last;
467 }
468
469 1 Window *window_next(Window *window)
470 {
471 1 WindowCallbackData data = {.target = window};
472 1 frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data);
473 1 BUG_ON(!data.found);
474
1/2
✓ Branch 0 (5→6) taken 1 times.
✗ Branch 1 (5→7) not taken.
1 return data.next ? data.next : data.first;
475 }
476
477 3 void window_close(Window *window)
478 {
479 3 EditorState *e = window->editor;
480
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→7) taken 2 times.
3 if (!window->frame->parent) {
481 // Don't close last window
482 1 window_remove_views(window);
483 1 set_view(window_open_empty_buffer(window));
484 1 return;
485 }
486
487 2 WindowCallbackData data = {.target = window};
488 2 frame_for_each_window(e->root_frame, find_prev_and_next, &data);
489 2 BUG_ON(!data.found);
490
2/2
✓ Branch 0 (10→11) taken 1 times.
✓ Branch 1 (10→12) taken 1 times.
2 Window *next_or_prev = data.next ? data.next : data.prev;
491 2 BUG_ON(!next_or_prev);
492
493 2 frame_remove(e, window->frame);
494 2 e->window = NULL;
495 2 set_view(next_or_prev->view);
496
497 2 e->screen_update |= UPDATE_ALL;
498 2 frame_debug(e->root_frame);
499 }
500