dte test coverage


Directory: ./
File: src/window.c
Date: 2025-06-04 06:50:24
Exec Total Coverage
Lines: 248 266 93.2%
Functions: 32 32 100.0%
Branches: 99 122 81.1%

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 11 Window *new_window(EditorState *e)
17 {
18 11 Window *window = xcalloc(1, sizeof(Window));
19 11 window->editor = e;
20 11 return window;
21 }
22
23 82 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 82 static_assert(SELECT_NONE == 0);
28
29 82 View *view = xmalloc(sizeof(*view));
30 82 *view = (View) {
31 .buffer = buffer,
32 .window = window,
33 82 .cursor = block_iter(buffer),
34 };
35
36 82 ptr_array_append(&buffer->views, view);
37 82 ptr_array_append(&window->views, view);
38 82 window->update_tabbar = true;
39 82 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 View *window_find_unclosable_view(Window *window)
134 {
135 // Check active view first
136
3/4
✓ Branch 0 (2→3) taken 4 times.
✗ Branch 1 (2→6) not taken.
✓ Branch 2 (4→5) taken 2 times.
✓ Branch 3 (4→6) taken 2 times.
4 if (window->view && !view_can_close(window->view)) {
137 2 return window->view;
138 }
139
140 2 void **ptrs = window->views.ptrs;
141
2/2
✓ Branch 0 (10→7) taken 2 times.
✓ Branch 1 (10→11) taken 2 times.
4 for (size_t i = 0, n = window->views.count; i < n; i++) {
142 2 View *view = ptrs[i];
143
1/2
✓ Branch 0 (8→9) taken 2 times.
✗ Branch 1 (8→11) not taken.
2 if (!view_can_close(view)) {
144 return view;
145 }
146 }
147
148 return NULL;
149 }
150
151 12 static void window_remove_views(Window *window)
152 {
153
2/2
✓ Branch 0 (4→3) taken 56 times.
✓ Branch 1 (4→5) taken 12 times.
68 while (window->views.count > 0) {
154 56 View *view = window->views.ptrs[window->views.count - 1];
155 56 remove_view(view);
156 }
157 12 }
158
159 // NOTE: window->frame isn't removed
160 11 void window_free(Window *window)
161 {
162 11 window_remove_views(window);
163 11 ptr_array_free_array(&window->views);
164 11 window->frame = NULL;
165 11 free(window);
166 11 }
167
168 // Remove view from view->window and view->buffer->views and free it
169 82 size_t remove_view(View *view)
170 {
171 82 Window *window = view->window;
172 82 window->update_tabbar = true;
173
2/2
✓ Branch 0 (2→3) taken 2 times.
✓ Branch 1 (2→4) taken 80 times.
82 if (view == window->prev_view) {
174 2 window->prev_view = NULL;
175 }
176
177 82 EditorState *e = window->editor;
178
2/2
✓ Branch 0 (4→5) taken 35 times.
✓ Branch 1 (4→6) taken 47 times.
82 if (view == e->view) {
179 35 e->view = NULL;
180 35 e->buffer = NULL;
181 }
182
183 82 size_t idx = ptr_array_remove(&window->views, view);
184 82 Buffer *buffer = view->buffer;
185 82 ptr_array_remove(&buffer->views, view);
186
187
2/2
✓ Branch 0 (8→9) taken 81 times.
✓ Branch 1 (8→13) taken 1 times.
82 if (buffer->views.count == 0) {
188
4/4
✓ Branch 0 (9→10) taken 80 times.
✓ Branch 1 (9→12) taken 1 times.
✓ Branch 2 (10→11) taken 44 times.
✓ Branch 3 (10→12) taken 36 times.
81 if (buffer->options.file_history && buffer->abs_filename) {
189 44 FileHistory *hist = &e->file_history;
190 44 file_history_append(hist, view->cy + 1, view->cx_char + 1, buffer->abs_filename);
191 }
192 81 buffer_remove_unlock_and_free(&e->buffers, buffer, &e->err, &e->locks_ctx);
193 }
194
195 82 free(view);
196 82 return idx;
197 }
198
199 26 void window_close_current_view(Window *window)
200 {
201 26 size_t idx = remove_view(window->view);
202
2/2
✓ Branch 0 (3→4) taken 21 times.
✓ Branch 1 (3→5) taken 5 times.
26 if (window->prev_view) {
203 21 window->view = window->prev_view;
204 21 window->prev_view = NULL;
205 21 return;
206 }
207
208 5 const PointerArray *views = &window->views;
209
2/2
✓ Branch 0 (5→6) taken 1 times.
✓ Branch 1 (5→7) taken 4 times.
5 if (views->count == 0) {
210 1 window_open_empty_buffer(window);
211 }
212
213 5 idx -= (views->count == idx);
214 5 window->view = views->ptrs[idx];
215 }
216
217 4 static void restore_cursor_from_history(const FileHistory *hist, View *view)
218 {
219 4 unsigned long row, col;
220
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)) {
221 1 move_to_filepos(view, row, col);
222 }
223 4 }
224
225 // Make `view` the current View in the editor, so that it takes input
226 // focus and becomes the active tab in its Window
227 93 void set_view(View *view)
228 {
229 93 EditorState *e = view->window->editor;
230
2/2
✓ Branch 0 (2→3) taken 83 times.
✓ Branch 1 (2→19) taken 10 times.
93 if (e->view == view) {
231 return;
232 }
233
234 // Forget `prev_view` when switching views. cmd_open(), cmd_reopen(),
235 // cmd_show() and cmd_exec() subsequently set it as appropriate, via
236 // window_open_new_file() or maybe_set_view().
237
2/2
✓ Branch 0 (3→4) taken 81 times.
✓ Branch 1 (3→5) taken 2 times.
83 if (e->window) {
238 81 e->window->prev_view = NULL;
239 }
240
241 83 e->view = view;
242 83 e->buffer = view->buffer;
243 83 e->window = view->window; // Parent Window becomes current Window
244 83 e->window->view = view;
245
246
2/2
✓ Branch 0 (5→6) taken 57 times.
✓ Branch 1 (5→10) taken 26 times.
83 if (!view->buffer->setup) {
247 // Run deferred initialization for view->buffer. This is done when
248 // buffers are first viewed instead of when they're opened, so as
249 // to allow opening many files without a sudden flood of extra work
250 // (e.g. loading multiple dte-syntax(5) files).
251 57 buffer_setup(e, view->buffer);
252
3/4
✓ Branch 0 (7→8) taken 57 times.
✗ Branch 1 (7→10) not taken.
✓ Branch 2 (8→9) taken 4 times.
✓ Branch 3 (8→10) taken 53 times.
57 if (view->buffer->options.file_history && view->buffer->abs_filename) {
253 4 restore_cursor_from_history(&e->file_history, view);
254 }
255 }
256
257 // view.cursor can be invalid if same buffer was modified from another view
258
2/2
✓ Branch 0 (10→11) taken 1 times.
✓ Branch 1 (10→13) taken 82 times.
83 if (view->restore_cursor) {
259 1 view->cursor.blk = BLOCK(view->buffer->blocks.next);
260 1 block_iter_goto_offset(&view->cursor, view->saved_cursor_offset);
261 1 view->restore_cursor = false;
262 1 view->saved_cursor_offset = 0;
263 }
264
265 // Save cursor states of views sharing same buffer
266 83 void **ptrs = view->buffer->views.ptrs;
267
2/2
✓ Branch 0 (18→14) taken 84 times.
✓ Branch 1 (18→19) taken 83 times.
167 for (size_t i = 0, n = view->buffer->views.count; i < n; i++) {
268 84 View *other = ptrs[i];
269
2/2
✓ Branch 0 (14→15) taken 1 times.
✓ Branch 1 (14→17) taken 83 times.
84 if (other != view) {
270 1 other->saved_cursor_offset = block_iter_get_offset(&other->cursor);
271 1 other->restore_cursor = true;
272 }
273 }
274 }
275
276 43 View *window_open_new_file(Window *window)
277 {
278 43 View *prev = window->view;
279 43 View *view = window_open_empty_buffer(window);
280 43 set_view(view);
281 43 window->prev_view = prev;
282 43 return view;
283 }
284
285 3 static bool buffer_is_untouched(const Buffer *b)
286 {
287
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;
288 }
289
290 6 static View *maybe_set_view(Window *window, View *view, View *prev, bool set_prev)
291 {
292
2/2
✓ Branch 0 (2→3) taken 3 times.
✓ Branch 1 (2→12) taken 3 times.
6 if (view && view != prev) {
293
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);
294 3 set_view(view);
295
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) {
296 // If window contains only one untouched buffer it'll be closed
297 // after opening another view. This is done because closing the
298 // last view (with window_close_current_view()) causes an empty
299 // view to be opened (windows must contain at least one buffer).
300 remove_view(prev);
301
2/2
✓ Branch 0 (10→11) taken 2 times.
✓ Branch 1 (10→12) taken 1 times.
3 } else if (set_prev) {
302 2 window->prev_view = prev;
303 }
304 }
305 6 return view;
306 }
307
308 1 View *window_open_file(Window *window, const char *filename, const char *encoding)
309 {
310 1 View *prev = window->view;
311 1 View *view = window_open_buffer(window, filename, false, encoding);
312 1 return maybe_set_view(window, view, prev, true);
313 }
314
315 // Open multiple files in window and return the first opened View
316 5 View *window_open_files(Window *window, char **filenames, const char *encoding)
317 {
318 5 View *prev = window->view;
319 5 View *first = NULL;
320 5 size_t prev_nr_views = window->views.count;
321
322
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++) {
323 27 View *view = window_open_buffer(window, filenames[i], false, encoding);
324
2/2
✓ Branch 0 (4→5) taken 2 times.
✓ Branch 1 (4→6) taken 25 times.
27 if (view && !first) {
325 2 first = view;
326 }
327 }
328
329 // `Window::prev_view` is set only if a single new view was opened
330 5 size_t nr_new_views = window->views.count - prev_nr_views;
331 5 return maybe_set_view(window, first, prev, nr_new_views == 1);
332 }
333
334 21 void buffer_mark_tabbars_changed(Buffer *buffer)
335 {
336
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++) {
337 22 View *view = buffer->views.ptrs[i];
338 22 view->window->update_tabbar = true;
339 }
340 21 }
341
342 62 static int line_numbers_width(const GlobalOptions *options, const View *view)
343 {
344
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) {
345 return 0;
346 }
347 6 size_t width = size_str_width(view->buffer->nl) + 1;
348 6 return MAX(width, LINE_NUMBERS_MIN_WIDTH);
349 }
350
351 41 static int edit_x_offset(const GlobalOptions *options, const View *view)
352 {
353 41 return line_numbers_width(options, view);
354 }
355
356 41 static int edit_y_offset(const GlobalOptions *options)
357 {
358 41 return options->tab_bar ? 1 : 0;
359 }
360
361 21 static void set_edit_size(Window *window, const GlobalOptions *options)
362 {
363 21 int xo = edit_x_offset(options, window->view);
364 21 int yo = edit_y_offset(options);
365 21 window->edit_w = window->w - xo;
366 21 window->edit_h = window->h - yo - 1; // statusline
367 21 window->edit_x = window->x + xo;
368 21 }
369
370 21 void window_calculate_line_numbers(Window *window)
371 {
372 21 const GlobalOptions *options = &window->editor->options;
373 21 int w = line_numbers_width(options, window->view);
374
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 20 times.
21 if (w != window->line_numbers.width) {
375 1 window->line_numbers.width = w;
376 1 window->line_numbers.first = 0;
377 1 window->line_numbers.last = 0;
378 1 mark_all_lines_changed(window->view->buffer);
379 }
380 21 set_edit_size(window, options);
381 21 }
382
383 20 void window_set_coordinates(Window *window, int x, int y)
384 {
385 20 const GlobalOptions *options = &window->editor->options;
386 20 window->x = x;
387 20 window->y = y;
388 20 window->edit_x = x + edit_x_offset(options, window->view);
389 20 window->edit_y = y + edit_y_offset(options);
390 20 }
391
392 21 void window_set_size(Window *window, int w, int h)
393 {
394 21 window->w = w;
395 21 window->h = h;
396 21 window_calculate_line_numbers(window);
397 21 }
398
399 7 unsigned int window_get_scroll_margin(const Window *window)
400 {
401 7 int edit_h = window->edit_h;
402
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→4) taken 7 times.
7 if (unlikely(edit_h <= 2)) {
403 return 0;
404 }
405
406 unsigned int max = (edit_h - 1) / 2;
407 unsigned int scroll_margin = window->editor->options.scroll_margin;
408 return MIN(max, scroll_margin);
409 }
410
411 // Recursion is bounded by the number of descendant frames, which is
412 // typically not more than 5 or so
413 // NOLINTNEXTLINE(misc-no-recursion)
414 12 void frame_for_each_window(const Frame *frame, void (*func)(Window*, void*), void *data)
415 {
416
2/2
✓ Branch 0 (2→3) taken 10 times.
✓ Branch 1 (2→5) taken 2 times.
12 if (frame->window) {
417 10 func(frame->window, data);
418 10 return;
419 }
420
421 2 void **ptrs = frame->frames.ptrs;
422
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++) {
423 4 frame_for_each_window(ptrs[i], func, data);
424 }
425 }
426
427 typedef struct {
428 const Window *const target; // Window to search for (set at init.)
429 Window *first; // Window passed in first callback invocation
430 Window *last; // Window passed in last callback invocation
431 Window *prev; // Window immediately before target (if any)
432 Window *next; // Window immediately after target (if any)
433 bool found; // Set to true when target is found
434 } WindowCallbackData;
435
436 6 static void find_prev_and_next(Window *window, void *ud)
437 {
438 6 WindowCallbackData *data = ud;
439 6 data->last = window;
440
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→6) taken 5 times.
6 if (data->found) {
441
1/2
✓ Branch 0 (3→4) taken 1 times.
✗ Branch 1 (3→5) not taken.
1 if (!data->next) {
442 1 data->next = window;
443 }
444 1 return;
445 }
446
2/2
✓ Branch 0 (6→7) taken 4 times.
✓ Branch 1 (6→8) taken 1 times.
5 if (!data->first) {
447 4 data->first = window;
448 }
449
2/2
✓ Branch 0 (8→9) taken 4 times.
✓ Branch 1 (8→10) taken 1 times.
5 if (window == data->target) {
450 4 data->found = true;
451 4 return;
452 }
453 1 data->prev = window;
454 }
455
456 1 Window *window_prev(Window *window)
457 {
458 1 WindowCallbackData data = {.target = window};
459 1 frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data);
460 1 BUG_ON(!data.found);
461
1/2
✓ Branch 0 (5→6) taken 1 times.
✗ Branch 1 (5→7) not taken.
1 return data.prev ? data.prev : data.last;
462 }
463
464 1 Window *window_next(Window *window)
465 {
466 1 WindowCallbackData data = {.target = window};
467 1 frame_for_each_window(window->editor->root_frame, find_prev_and_next, &data);
468 1 BUG_ON(!data.found);
469
1/2
✓ Branch 0 (5→6) taken 1 times.
✗ Branch 1 (5→7) not taken.
1 return data.next ? data.next : data.first;
470 }
471
472 3 void window_close(Window *window)
473 {
474 3 EditorState *e = window->editor;
475
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→7) taken 2 times.
3 if (!window->frame->parent) {
476 // Don't close last window
477 1 window_remove_views(window);
478 1 set_view(window_open_empty_buffer(window));
479 1 return;
480 }
481
482 2 WindowCallbackData data = {.target = window};
483 2 frame_for_each_window(e->root_frame, find_prev_and_next, &data);
484 2 BUG_ON(!data.found);
485
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;
486 2 BUG_ON(!next_or_prev);
487
488 2 frame_remove(e, window->frame);
489 2 e->window = NULL;
490 2 set_view(next_or_prev->view);
491
492 2 e->screen_update |= UPDATE_ALL;
493 2 frame_debug(e->root_frame);
494 }
495