dte test coverage


Directory: ./
File: src/editor.c
Date: 2025-10-16 19:09:21
Exec Total Coverage
Lines: 91 194 46.9%
Functions: 7 17 41.2%
Branches: 13 50 26.0%

Line Branch Exec Source
1 #include <errno.h>
2 #include <langinfo.h>
3 #include <locale.h>
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <sys/stat.h>
7 #include <unistd.h>
8 #include "editor.h"
9 #include "bind.h"
10 #include "bookmark.h"
11 #include "compiler.h"
12 #include "encoding.h"
13 #include "file-option.h"
14 #include "filetype.h"
15 #include "lock.h"
16 #include "signals.h"
17 #include "syntax/syntax.h"
18 #include "terminal/color.h"
19 #include "terminal/input.h"
20 #include "terminal/key.h"
21 #include "terminal/output.h"
22 #include "terminal/paste.h"
23 #include "ui.h"
24 #include "util/exitcode.h"
25 #include "util/intern.h"
26 #include "util/intmap.h"
27 #include "util/log.h"
28 #include "util/time-util.h"
29 #include "util/xmalloc.h"
30 #include "util/xsnprintf.h"
31 #include "util/xstdio.h"
32 #include "version.h"
33
34 11 static void set_and_check_locale(void)
35 {
36 11 const char *default_locale = setlocale(LC_CTYPE, "");
37
1/2
✓ Branch 0 (3→4) taken 11 times.
✗ Branch 1 (3→8) not taken.
11 if (likely(default_locale)) {
38 11 const char *codeset = nl_langinfo(CODESET);
39 11 LOG_INFO("locale: %s (codeset: %s)", default_locale, codeset);
40
1/2
✗ Branch 0 (7→11) not taken.
✓ Branch 1 (7→19) taken 11 times.
11 if (likely(lookup_encoding(codeset) == UTF8)) {
41 return;
42 }
43 } else {
44 LOG_ERROR("failed to set default locale");
45 }
46
47 static const char fallbacks[][12] = {"C.UTF-8", "en_US.UTF-8"};
48 const char *fallback = NULL;
49 for (size_t i = 0; i < ARRAYLEN(fallbacks) && !fallback; i++) {
50 fallback = setlocale(LC_CTYPE, fallbacks[i]);
51 }
52 if (fallback) {
53 LOG_NOTICE("using fallback locale for LC_CTYPE: %s", fallback);
54 return;
55 }
56
57 LOG_ERROR("no UTF-8 fallback locales found");
58 fputs("setlocale() failed\n", stderr);
59 exit(EC_CONFIG_ERROR);
60 }
61
62 // Get permissions mask for new files
63 11 static mode_t get_umask(void)
64 {
65 11 mode_t old = umask(0); // Get by setting a dummy value
66 11 umask(old); // Restore previous value
67 11 return old;
68 }
69
70 11 static const char *get_user_config_dir(const char *home, const char *dte_home)
71 {
72
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 10 times.
11 if (dte_home) {
73 1 return str_intern(dte_home);
74 }
75
76 // TODO: Use "${XDG_CONFIG_HOME:-$HOME/.config}/dte", either if it
77 // exists or (as a default) if "$HOME/.dte" *doesn't* exist. The
78 // latter case may also require one or more calls to mkdir(3).
79
80 10 char buf[8192];
81 10 return mem_intern(buf, xsnprintf(buf, sizeof buf, "%s/.dte", home));
82 }
83
84 11 EditorState *init_editor_state(const char *home, const char *dte_home)
85 {
86 11 set_and_check_locale();
87
1/2
✗ Branch 0 (3→4) not taken.
✓ Branch 1 (3→5) taken 11 times.
11 home = home ? home : "";
88 11 EditorState *e = xmalloc(sizeof(*e));
89
90 22 *e = (EditorState) {
91 .status = EDITOR_INITIALIZING,
92 11 .home_dir = strview_intern(home),
93 11 .user_config_dir = get_user_config_dir(home, dte_home),
94 .flags = EFLAG_HEADLESS,
95 .command_history = {
96 .max_entries = 512,
97 },
98 .search_history = {
99 .max_entries = 128,
100 },
101 .terminal = {
102 .obuf = TERM_OUTPUT_INIT,
103 },
104 .cursor_styles = {
105 [CURSOR_MODE_DEFAULT] = {.type = CURSOR_DEFAULT, .color = COLOR_DEFAULT},
106 [CURSOR_MODE_INSERT] = {.type = CURSOR_KEEP, .color = COLOR_KEEP},
107 [CURSOR_MODE_OVERWRITE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP},
108 [CURSOR_MODE_CMDLINE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP},
109 },
110 .options = {
111 .auto_indent = true,
112 .detect_indent = 0,
113 .editorconfig = false,
114 .emulate_tab = false,
115 .expand_tab = false,
116 .file_history = true,
117 .indent_width = 8,
118 .overwrite = false,
119 .save_unmodified = SAVE_FULL,
120 .syntax = true,
121 .tab_width = 8,
122 .text_width = 72,
123 .ws_error = WSE_SPECIAL,
124
125 // Global-only options
126 .case_sensitive_search = CSS_TRUE,
127 .crlf_newlines = false,
128 .display_special = false,
129 .esc_timeout = 100,
130 .filesize_limit = 250,
131 .lock_files = true,
132 .optimize_true_color = true,
133 .scroll_margin = 0,
134 .select_cursor_char = true,
135 .set_window_title = false,
136 .show_line_numbers = false,
137 11 .statusline_left = str_intern(" %f%s%m%s%r%s%M"),
138 11 .statusline_right = str_intern(" %y,%X %u %o %E%s%b%s%n %t %p "),
139 .tab_bar = true,
140 .utf8_bom = false,
141 .window_separator = WINSEP_BAR,
142 .msg_compile = 0,
143 .msg_tag = 0,
144 }
145 };
146
147 11 sanity_check_global_options(&e->err, &e->options);
148
149 11 pid_t pid = getpid();
150 11 bool leader = pid == getsid(0);
151 11 e->session_leader = leader;
152
1/2
✓ Branch 0 (13→14) taken 11 times.
✗ Branch 1 (13→15) not taken.
22 LOG_INFO("pid: %jd%s", (intmax_t)pid, leader ? " (session leader)" : "");
153
154 11 pid_t pgid = getpgrp();
155
1/2
✓ Branch 0 (17→18) taken 11 times.
✗ Branch 1 (17→19) not taken.
11 if (pgid != pid) {
156 11 LOG_INFO("pgid: %jd", (intmax_t)pgid);
157 }
158
159 11 mode_t mask = get_umask();
160 11 e->new_file_mode = 0666 & ~mask;
161 11 LOG_INFO("umask: %04o", 0777u & (unsigned int)mask);
162
163 11 init_file_locks_context(&e->locks_ctx, e->user_config_dir, pid);
164
165 // Allow child processes to detect that they're running under dte
166
1/2
✗ Branch 0 (23→24) not taken.
✓ Branch 1 (23→25) taken 11 times.
11 if (unlikely(setenv("DTE_VERSION", VERSION, 1) != 0)) {
167 // errno is almost certainly ENOMEM, if setenv() failed here
168 fatal_error("setenv", errno);
169 }
170
171 11 RegexpWordBoundaryTokens *wb = &e->regexp_word_tokens;
172
1/2
✓ Branch 0 (26→27) taken 11 times.
✗ Branch 1 (26→28) not taken.
11 if (regexp_init_word_boundary_tokens(wb)) {
173 11 LOG_INFO("regex word boundary tokens detected: %s %s", wb->start, wb->end);
174 } else {
175 LOG_WARNING("no regex word boundary tokens detected");
176 }
177
178 11 HashMap *modes = &e->modes;
179 11 e->normal_mode = new_mode(modes, xstrdup("normal"), &normal_commands);
180 11 e->command_mode = new_mode(modes, xstrdup("command"), &cmd_mode_commands);
181 11 e->search_mode = new_mode(modes, xstrdup("search"), &search_mode_commands);
182 11 e->mode = e->normal_mode;
183
184 // Pre-allocate space for key bindings and aliases, so that no
185 // predictable (and thus unnecessary) reallocs are needed when
186 // loading built-in configs
187 11 hashmap_init(&e->aliases, 32, HMAP_NO_FLAGS);
188 11 intmap_init(&e->normal_mode->key_bindings, 150);
189 11 intmap_init(&e->command_mode->key_bindings, 40);
190 11 intmap_init(&e->search_mode->key_bindings, 40);
191 11 hashset_init(&e->required_syntax_files, 0, false);
192 11 hashset_init(&e->required_syntax_builtins, 0, false);
193
194 11 return e;
195 }
196
197 34 static void free_mode_handler(ModeHandler *handler)
198 {
199 34 ptr_array_free_array(&handler->fallthrough_modes);
200 34 free_bindings(&handler->key_bindings);
201 34 free(handler);
202 34 }
203
204 12 void clear_all_messages(EditorState *e)
205 {
206
2/2
✓ Branch 0 (5→3) taken 36 times.
✓ Branch 1 (5→6) taken 12 times.
48 for (size_t i = 0; i < ARRAYLEN(e->messages); i++) {
207 36 clear_messages(&e->messages[i]);
208 }
209 12 }
210
211 11 void free_editor_state(EditorState *e)
212 {
213 11 size_t n = e->terminal.obuf.count;
214
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→4) taken 11 times.
11 if (n) {
215 LOG_DEBUG("%zu unflushed bytes in terminal output buffer", n);
216 }
217 11 n = e->terminal.ibuf.len;
218
1/2
✗ Branch 0 (4→5) not taken.
✓ Branch 1 (4→6) taken 11 times.
11 if (n) {
219 LOG_DEBUG("%zu unprocessed bytes in terminal input buffer", n);
220 }
221
222 11 free(e->clipboard.buf);
223 11 free_file_options(&e->file_options);
224 11 free_filetypes(&e->filetypes);
225 11 free_syntaxes(&e->syntaxes);
226 11 file_history_free(&e->file_history);
227 11 history_free(&e->command_history);
228 11 history_free(&e->search_history);
229 11 search_free_regexp(&e->search);
230 11 clear_all_messages(e);
231 11 cmdline_free(&e->cmdline);
232 11 free_macro(&e->macro);
233 11 tag_file_free(&e->tagfile);
234 11 free_buffers(&e->buffers, &e->err, &e->locks_ctx);
235 11 free_file_locks_context(&e->locks_ctx);
236
237 11 ptr_array_free_cb(&e->bookmarks, FREE_FUNC(file_location_free));
238 11 hashmap_free(&e->compilers, FREE_FUNC(free_compiler));
239 11 hashmap_free(&e->modes, FREE_FUNC(free_mode_handler));
240 11 hashmap_free(&e->styles.other, free);
241 11 hashmap_free(&e->aliases, free);
242 11 hashset_free(&e->required_syntax_files);
243 11 hashset_free(&e->required_syntax_builtins);
244
245 11 free_interned_strings();
246 11 free_interned_regexps();
247 11 free(e);
248 11 }
249
250 static bool buffer_contains_block(const Buffer *buffer, const Block *ref)
251 {
252 const Block *blk;
253 block_for_each(blk, &buffer->blocks) {
254 if (blk == ref) {
255 return true;
256 }
257 }
258 return false;
259 }
260
261 static void sanity_check(const View *view)
262 {
263 if (!DEBUG_ASSERTIONS_ENABLED) {
264 return;
265 }
266 const BlockIter *cursor = &view->cursor;
267 BUG_ON(!buffer_contains_block(view->buffer, cursor->blk));
268 BUG_ON(cursor->offset > cursor->blk->size);
269 }
270
271 void any_key(Terminal *term, unsigned int esc_timeout)
272 {
273 KeyCode key;
274 xfputs("Press any key to continue\r\n", stderr);
275 while ((key = term_read_input(term, esc_timeout)) == KEY_NONE) {
276 ;
277 }
278 bool bracketed_paste = key == KEYCODE_BRACKETED_PASTE;
279 if (bracketed_paste || key == KEYCODE_DETECTED_PASTE) {
280 term_discard_paste(&term->ibuf, bracketed_paste);
281 }
282 }
283
284 NOINLINE
285 void ui_resize(EditorState *e)
286 {
287 BUG_ON(e->flags & EFLAG_HEADLESS);
288 resized = 0;
289 update_screen_size(&e->terminal, e->root_frame);
290
291 const ScreenState dummyval = {.id = 0};
292 e->screen_update |= UPDATE_ALL;
293 update_screen(e, &dummyval);
294 }
295
296 void ui_start(EditorState *e)
297 {
298 BUG_ON(e->flags & EFLAG_HEADLESS);
299 Terminal *term = &e->terminal;
300
301 // Note: the order of these calls is important - Kitty saves/restores
302 // some terminal state when switching buffers, so switching to the
303 // alternate screen buffer needs to happen before modes are enabled
304 term_use_alt_screen_buffer(term);
305 term_enable_private_modes(term);
306
307 term_restore_and_save_title(term);
308 ui_resize(e);
309 }
310
311 // Like ui_start(), but to be called only the first time the UI is started.
312 // Terminal queries are buffered before ui_resize() is called, so that only
313 // a single term_output_flush() is needed (i.e. as opposed to calling
314 // ui_start() + term_put_initial_queries() + term_output_flush()).
315 void ui_first_start(EditorState *e, unsigned int terminal_query_level)
316 {
317 BUG_ON(e->flags & EFLAG_HEADLESS);
318 Terminal *term = &e->terminal;
319
320 // The order of these calls is important; see ui_start()
321 term_use_alt_screen_buffer(term);
322 term_enable_private_modes(term);
323
324 term_save_title(term);
325 term_put_initial_queries(term, terminal_query_level);
326 ui_resize(e);
327 }
328
329 void ui_end(Terminal *term, bool final)
330 {
331 if (final) {
332 term_restore_title(term);
333 } else {
334 term_restore_and_save_title(term);
335 }
336
337 TermOutputBuffer *obuf = &term->obuf;
338 term_clear_screen(obuf);
339 term_move_cursor(obuf, 0, term->height - 1);
340 term_restore_cursor_style(term);
341 term_show_cursor(term);
342 term_restore_private_modes(term);
343 term_use_normal_screen_buffer(term);
344 term_end_sync_update(term);
345 term_output_flush(obuf);
346 }
347
348 static ScreenState get_screen_state(const EditorState *e)
349 {
350 const View *view = e->view;
351 return (ScreenState) {
352 .is_modified = buffer_modified(view->buffer),
353 .set_window_title = e->options.set_window_title,
354 .id = view->buffer->id,
355 .cy = view->cy,
356 .vx = view->vx,
357 .vy = view->vy
358 };
359 }
360
361 static void log_timing_info(const struct timespec *start, bool enabled)
362 {
363 struct timespec end;
364 if (likely(!enabled) || !xgettime(&end)) {
365 return;
366 }
367
368 double ms = timespec_to_fp_milliseconds(timespec_subtract(&end, start));
369 LOG_INFO("main loop time: %.3f ms", ms);
370 }
371
372 void main_loop(EditorState *e, bool timing)
373 {
374 BUG_ON(e->flags & EFLAG_HEADLESS);
375
376 while (e->status == EDITOR_RUNNING) {
377 if (unlikely(resized)) {
378 LOG_INFO("SIGWINCH received");
379 ui_resize(e);
380 }
381
382 KeyCode key = term_read_input(&e->terminal, e->options.esc_timeout);
383 if (unlikely(key == KEY_NONE)) {
384 continue;
385 }
386
387 struct timespec start;
388 timing = unlikely(timing) && xgettime(&start);
389
390 const ScreenState s = get_screen_state(e);
391 clear_error(&e->err);
392 handle_input(e, key);
393 sanity_check(e->view);
394 update_screen(e, &s);
395
396 log_timing_info(&start, timing);
397 }
398
399 BUG_ON(e->status < 0 || e->status > EDITOR_EXIT_MAX);
400 }
401