dte test coverage


Directory: ./
File: src/editor.c
Date: 2025-06-04 06:50:24
Exec Total Coverage
Lines: 87 172 50.6%
Functions: 6 14 42.9%
Branches: 11 38 28.9%

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