src/editor.c
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include <langinfo.h> | ||
| 2 | #include <locale.h> | ||
| 3 | #include <stdio.h> | ||
| 4 | #include <stdlib.h> | ||
| 5 | #include <sys/stat.h> | ||
| 6 | #include <unistd.h> | ||
| 7 | #include "editor.h" | ||
| 8 | #include "bind.h" | ||
| 9 | #include "bookmark.h" | ||
| 10 | #include "compiler.h" | ||
| 11 | #include "encoding.h" | ||
| 12 | #include "file-option.h" | ||
| 13 | #include "filetype.h" | ||
| 14 | #include "lock.h" | ||
| 15 | #include "signals.h" | ||
| 16 | #include "syntax/syntax.h" | ||
| 17 | #include "terminal/color.h" | ||
| 18 | #include "terminal/input.h" | ||
| 19 | #include "terminal/key.h" | ||
| 20 | #include "terminal/output.h" | ||
| 21 | #include "terminal/paste.h" | ||
| 22 | #include "ui.h" | ||
| 23 | #include "util/exitcode.h" | ||
| 24 | #include "util/intern.h" | ||
| 25 | #include "util/log.h" | ||
| 26 | #include "util/str-util.h" | ||
| 27 | #include "util/time-util.h" | ||
| 28 | #include "util/xmalloc.h" | ||
| 29 | #include "util/xsnprintf.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 3 → 4 taken 11 times.
✗ Branch 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 7 → 9 not taken.
✓ Branch 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 | static const char *get_user_config_dir(const char *home, const char *dte_home) | |
| 70 | { | ||
| 71 |
2/2✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 10 times.
|
11 | if (dte_home) { |
| 72 | 1 | return str_intern(dte_home); | |
| 73 | } | ||
| 74 | |||
| 75 | // TODO: Use "${XDG_CONFIG_HOME:-$HOME/.config}/dte", either if it | ||
| 76 | // exists or (as a default) if "$HOME/.dte" *doesn't* exist. The | ||
| 77 | // latter case may also require one or more calls to mkdir(3). | ||
| 78 | |||
| 79 | 10 | char buf[8192]; | |
| 80 | 10 | return mem_intern(buf, xsnprintf(buf, sizeof buf, "%s/.dte", home)); | |
| 81 | } | ||
| 82 | |||
| 83 | 11 | EditorState *init_editor_state(const char *home, const char *dte_home) | |
| 84 | { | ||
| 85 | 11 | set_and_check_locale(); | |
| 86 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 11 times.
|
11 | home = home ? home : ""; |
| 87 | 11 | EditorState *e = xmalloc(sizeof(*e)); | |
| 88 | |||
| 89 | 22 | *e = (EditorState) { | |
| 90 | .status = EDITOR_INITIALIZING, | ||
| 91 | 11 | .home_dir = strview_intern(home), | |
| 92 | 11 | .user_config_dir = get_user_config_dir(home, dte_home), | |
| 93 | .flags = EFLAG_HEADLESS, | ||
| 94 | 11 | .regexp_word_tokens = regexp_get_word_boundary_tokens(), | |
| 95 | 11 | .modes = hashmap_new(3, HMAP_BORROWED_KEYS), | |
| 96 | 11 | .aliases = hashmap_new(32, HMAP_NO_FLAGS), | |
| 97 | 11 | .required_syntax_files = hashset_new(0, false), | |
| 98 | 11 | .required_syntax_builtins = hashset_new(0, false), | |
| 99 | .command_history = { | ||
| 100 | .max_entries = 512, | ||
| 101 | }, | ||
| 102 | .search_history = { | ||
| 103 | .max_entries = 128, | ||
| 104 | }, | ||
| 105 | .terminal = { | ||
| 106 | 11 | .obuf = term_outbuf(), | |
| 107 | }, | ||
| 108 | .cursor_styles = { | ||
| 109 | [CURSOR_MODE_DEFAULT] = {.type = CURSOR_DEFAULT, .color = COLOR_DEFAULT}, | ||
| 110 | [CURSOR_MODE_INSERT] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 111 | [CURSOR_MODE_OVERWRITE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 112 | [CURSOR_MODE_CMDLINE] = {.type = CURSOR_KEEP, .color = COLOR_KEEP}, | ||
| 113 | }, | ||
| 114 | .options = { | ||
| 115 | .auto_indent = true, | ||
| 116 | .detect_indent = 0, | ||
| 117 | .editorconfig = false, | ||
| 118 | .emulate_tab = false, | ||
| 119 | .expand_tab = false, | ||
| 120 | .file_history = true, | ||
| 121 | .indent_width = 8, | ||
| 122 | .overwrite = false, | ||
| 123 | .save_unmodified = SAVE_FULL, | ||
| 124 | .syntax = true, | ||
| 125 | .tab_width = 8, | ||
| 126 | .text_width = 72, | ||
| 127 | .ws_error = WSE_SPECIAL, | ||
| 128 | |||
| 129 | // Global-only options | ||
| 130 | .case_sensitive_search = CSS_TRUE, | ||
| 131 | .crlf_newlines = false, | ||
| 132 | .display_special = false, | ||
| 133 | .esc_timeout = 100, | ||
| 134 | .filesize_limit = 250ULL << 20, // 250MiB | ||
| 135 | .lock_files = true, | ||
| 136 | .msg_compile = 0, | ||
| 137 | .msg_tag = 0, | ||
| 138 | .optimize_true_color = false, | ||
| 139 | .scroll_margin = 0, | ||
| 140 | .select_cursor_char = true, | ||
| 141 | .set_window_title = false, | ||
| 142 | .show_line_numbers = false, | ||
| 143 | 11 | .statusline_left = str_intern(" %f%s%m%s%r%s%M"), | |
| 144 | 11 | .statusline_right = str_intern(" %y,%X %u %o %E%s%b%s%n %t %p "), | |
| 145 | .syntax_line_limit = 512ULL << 10, // 512KiB | ||
| 146 | .syntax_size_limit = 100ULL << 20, // 100MiB | ||
| 147 | .tab_bar = true, | ||
| 148 | .utf8_bom = false, | ||
| 149 | .window_separator = WINSEP_BAR, | ||
| 150 | } | ||
| 151 | }; | ||
| 152 | |||
| 153 | 11 | sanity_check_global_options(&e->options); | |
| 154 | |||
| 155 | 11 | pid_t pid = getpid(); | |
| 156 | 11 | bool leader = pid == getsid(0); | |
| 157 | 11 | e->session_leader = leader; | |
| 158 |
1/2✓ Branch 18 → 19 taken 11 times.
✗ Branch 18 → 20 not taken.
|
22 | LOG_INFO("pid: %jd%s", (intmax_t)pid, leader ? " (session leader)" : ""); |
| 159 | |||
| 160 | 11 | pid_t pgid = getpgrp(); | |
| 161 |
1/2✓ Branch 22 → 23 taken 11 times.
✗ Branch 22 → 24 not taken.
|
11 | if (pgid != pid) { |
| 162 | 11 | LOG_INFO("pgid: %jd", (intmax_t)pgid); | |
| 163 | } | ||
| 164 | |||
| 165 | 11 | mode_t mask = get_umask(); | |
| 166 | 11 | e->new_file_mode = 0666 & ~mask; | |
| 167 | 11 | LOG_INFO("umask: %04o", 0777u & (unsigned int)mask); | |
| 168 | |||
| 169 | 11 | init_file_locks_context(&e->locks_ctx, e->user_config_dir, pid); | |
| 170 | |||
| 171 | // Allow child processes to detect that they're running under dte | ||
| 172 | 11 | xsetenv("DTE_VERSION", VERSION); | |
| 173 | |||
| 174 | // Initial capacities here should accommodate at least the number | ||
| 175 | // of bindings created by exec_rc_files() | ||
| 176 | 11 | e->normal_mode = new_mode(&e->modes, "normal", &normal_commands, 150); | |
| 177 | 11 | e->command_mode = new_mode(&e->modes, "command", &cmd_mode_commands, 40); | |
| 178 | 11 | e->search_mode = new_mode(&e->modes, "search", &search_mode_commands, 40); | |
| 179 | 11 | e->mode = e->normal_mode; | |
| 180 | |||
| 181 | 11 | return e; | |
| 182 | } | ||
| 183 | |||
| 184 | 34 | static void free_mode_handler(ModeHandler *handler) | |
| 185 | { | ||
| 186 | 34 | ptr_array_free_array(&handler->fallthrough_modes); | |
| 187 | 34 | free_bindings(&handler->key_bindings); | |
| 188 | 34 | free(handler); | |
| 189 | 34 | } | |
| 190 | |||
| 191 | 12 | void clear_all_messages(EditorState *e) | |
| 192 | { | ||
| 193 |
2/2✓ Branch 5 → 3 taken 36 times.
✓ Branch 5 → 6 taken 12 times.
|
48 | for (size_t i = 0; i < ARRAYLEN(e->messages); i++) { |
| 194 | 36 | clear_messages(&e->messages[i]); | |
| 195 | } | ||
| 196 | 12 | } | |
| 197 | |||
| 198 | 11 | void free_editor_state(EditorState *e) | |
| 199 | { | ||
| 200 | 11 | size_t n = e->terminal.obuf.count; | |
| 201 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 11 times.
|
11 | if (n) { |
| 202 | ✗ | LOG_DEBUG("%zu unflushed bytes in terminal output buffer", n); | |
| 203 | } | ||
| 204 | 11 | n = e->terminal.ibuf.len; | |
| 205 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 11 times.
|
11 | if (n) { |
| 206 | ✗ | LOG_DEBUG("%zu unprocessed bytes in terminal input buffer", n); | |
| 207 | } | ||
| 208 | |||
| 209 | 11 | free(e->clipboard.buf); | |
| 210 | 11 | free_file_options(&e->file_options); | |
| 211 | 11 | free_filetypes(&e->filetypes); | |
| 212 | 11 | free_syntaxes(&e->syntaxes); | |
| 213 | 11 | file_history_free(&e->file_history); | |
| 214 | 11 | history_free(&e->command_history); | |
| 215 | 11 | history_free(&e->search_history); | |
| 216 | 11 | search_free_regexp(&e->search); | |
| 217 | 11 | clear_all_messages(e); | |
| 218 | 11 | cmdline_free(&e->cmdline); | |
| 219 | 11 | free_macro(&e->macro); | |
| 220 | 11 | tag_file_free(&e->tagfile); | |
| 221 | 11 | free_buffers(&e->buffers, &e->err, &e->locks_ctx); | |
| 222 | 11 | free_file_locks_context(&e->locks_ctx); | |
| 223 | |||
| 224 | 11 | ptr_array_free_cb(&e->bookmarks, FREE_FUNC(file_location_free)); | |
| 225 | 11 | hashmap_free(&e->compilers, FREE_FUNC(free_compiler)); | |
| 226 | 11 | hashmap_free(&e->modes, FREE_FUNC(free_mode_handler)); | |
| 227 | 11 | hashmap_free(&e->styles.other, free); | |
| 228 | 11 | hashmap_free(&e->aliases, free); | |
| 229 | 11 | hashset_free(&e->required_syntax_files); | |
| 230 | 11 | hashset_free(&e->required_syntax_builtins); | |
| 231 | |||
| 232 | 11 | free_interned_strings(); | |
| 233 | 11 | free_interned_regexps(); | |
| 234 | 11 | free(e); | |
| 235 | 11 | } | |
| 236 | |||
| 237 | ✗ | static bool buffer_contains_block(const Buffer *buffer, const Block *ref) | |
| 238 | { | ||
| 239 | ✗ | const Block *blk; | |
| 240 | ✗ | block_for_each(blk, &buffer->blocks) { | |
| 241 | ✗ | if (blk == ref) { | |
| 242 | return true; | ||
| 243 | } | ||
| 244 | } | ||
| 245 | return false; | ||
| 246 | } | ||
| 247 | |||
| 248 | ✗ | static void sanity_check(const View *view) | |
| 249 | { | ||
| 250 | ✗ | if (!DEBUG_ASSERTIONS_ENABLED) { | |
| 251 | return; | ||
| 252 | } | ||
| 253 | ✗ | const BlockIter *cursor = &view->cursor; | |
| 254 | ✗ | BUG_ON(!buffer_contains_block(view->buffer, cursor->blk)); | |
| 255 | ✗ | BUG_ON(cursor->offset > cursor->blk->size); | |
| 256 | } | ||
| 257 | |||
| 258 | ✗ | void any_key(Terminal *term, unsigned int esc_timeout) | |
| 259 | { | ||
| 260 | ✗ | KeyCode key; | |
| 261 | ✗ | xfputs("Press any key to continue\r\n", stderr); | |
| 262 | |||
| 263 | ✗ | do { | |
| 264 | ✗ | key = term_read_input(term, esc_timeout); | |
| 265 | ✗ | } while (key == KEY_NONE || key == KEYCODE_REDRAW); | |
| 266 | |||
| 267 | ✗ | bool bpaste = key == KEYCODE_BRACKETED_PASTE; | |
| 268 | ✗ | if (bpaste || key == KEYCODE_DETECTED_PASTE) { | |
| 269 | ✗ | term_discard_paste(&term->ibuf, bpaste); | |
| 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 | ✗ | static 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(Terminal *term, bool final) | |
| 319 | { | ||
| 320 | ✗ | if (final) { | |
| 321 | ✗ | term_restore_title(term); | |
| 322 | } else { | ||
| 323 | ✗ | term_restore_and_save_title(term); | |
| 324 | } | ||
| 325 | |||
| 326 | ✗ | TermOutputBuffer *obuf = &term->obuf; | |
| 327 | ✗ | term_clear_screen(obuf); | |
| 328 | ✗ | term_move_cursor(obuf, 0, term->height - 1); | |
| 329 | ✗ | term_restore_cursor_style(term); | |
| 330 | ✗ | term_show_cursor(term); | |
| 331 | ✗ | term_restore_private_modes(term); | |
| 332 | ✗ | term_use_normal_screen_buffer(term); | |
| 333 | ✗ | term_end_sync_update(term); | |
| 334 | ✗ | term_output_flush(obuf); | |
| 335 | ✗ | } | |
| 336 | |||
| 337 | ✗ | static ScreenState get_screen_state(const EditorState *e) | |
| 338 | { | ||
| 339 | ✗ | const View *view = e->view; | |
| 340 | ✗ | return (ScreenState) { | |
| 341 | ✗ | .is_modified = buffer_modified(view->buffer), | |
| 342 | ✗ | .set_window_title = e->options.set_window_title, | |
| 343 | ✗ | .id = view->buffer->id, | |
| 344 | ✗ | .cy = view->cy, | |
| 345 | ✗ | .vx = view->vx, | |
| 346 | ✗ | .vy = view->vy | |
| 347 | }; | ||
| 348 | } | ||
| 349 | |||
| 350 | ✗ | static void log_timing_info(const struct timespec *start, bool enabled) | |
| 351 | { | ||
| 352 | ✗ | struct timespec end; | |
| 353 | ✗ | if (likely(!enabled) || !xgettime(&end)) { | |
| 354 | ✗ | return; | |
| 355 | } | ||
| 356 | |||
| 357 | ✗ | double ms = timespec_to_fp_milliseconds(timespec_subtract(&end, start)); | |
| 358 | ✗ | LOG_INFO("main loop time: %.3f ms", ms); | |
| 359 | } | ||
| 360 | |||
| 361 | ✗ | void main_loop(EditorState *e, unsigned int terminal_query_level, bool timing) | |
| 362 | { | ||
| 363 | ✗ | BUG_ON(e->flags & EFLAG_HEADLESS); | |
| 364 | ✗ | ui_first_start(e, terminal_query_level); | |
| 365 | |||
| 366 | ✗ | while (e->status == EDITOR_RUNNING) { | |
| 367 | ✗ | if (unlikely(resized)) { | |
| 368 | ✗ | LOG_INFO("SIGWINCH received"); | |
| 369 | ✗ | ui_resize(e); | |
| 370 | } | ||
| 371 | |||
| 372 | ✗ | KeyCode key = term_read_input(&e->terminal, e->options.esc_timeout); | |
| 373 | ✗ | if (unlikely(key == KEY_NONE)) { | |
| 374 | ✗ | continue; | |
| 375 | } | ||
| 376 | |||
| 377 | ✗ | struct timespec start; | |
| 378 | ✗ | timing = unlikely(timing) && xgettime(&start); | |
| 379 | ✗ | const ScreenState s = get_screen_state(e); | |
| 380 | |||
| 381 | ✗ | if (unlikely(key == KEYCODE_REDRAW)) { | |
| 382 | ✗ | e->screen_update |= UPDATE_ALL_WINDOWS; | |
| 383 | } else { | ||
| 384 | ✗ | clear_error(&e->err); | |
| 385 | ✗ | handle_input(e, key); | |
| 386 | ✗ | sanity_check(e->view); | |
| 387 | } | ||
| 388 | |||
| 389 | ✗ | update_screen(e, &s); | |
| 390 | ✗ | log_timing_info(&start, timing); | |
| 391 | } | ||
| 392 | |||
| 393 | ✗ | BUG_ON(e->status < 0 || e->status > EDITOR_EXIT_MAX); | |
| 394 | ✗ | } | |
| 395 |