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