#include #include #include #include #include #include #include #include #include "commands.h" #include "bind.h" #include "bookmark.h" #include "buffer.h" #include "change.h" #include "cmdline.h" #include "command/alias.h" #include "command/args.h" #include "command/macro.h" #include "compiler.h" #include "config.h" #include "convert.h" #include "copy.h" #include "editor.h" #include "encoding.h" #include "error.h" #include "exec.h" #include "file-option.h" #include "filetype.h" #include "frame.h" #include "history.h" #include "load-save.h" #include "lock.h" #include "misc.h" #include "move.h" #include "msg.h" #include "regexp.h" #include "replace.h" #include "screen.h" #include "search.h" #include "selection.h" #include "shift.h" #include "show.h" #include "spawn.h" #include "syntax/color.h" #include "syntax/state.h" #include "syntax/syntax.h" #include "tag.h" #include "terminal/cursor.h" #include "terminal/mode.h" #include "terminal/osc52.h" #include "terminal/style.h" #include "terminal/terminal.h" #include "util/arith.h" #include "util/array.h" #include "util/ascii.h" #include "util/bit.h" #include "util/bsearch.h" #include "util/debug.h" #include "util/log.h" #include "util/path.h" #include "util/str-util.h" #include "util/strtonum.h" #include "util/time-util.h" #include "util/xmalloc.h" #include "util/xsnprintf.h" #include "vars.h" #include "view.h" #include "window.h" NOINLINE static void do_selection_noinline(View *view, SelectionType sel) { // Should only be called from do_selection() BUG_ON(sel == view->selection); if (sel == SELECT_NONE) { unselect(view); return; } if (view->selection) { if (view->selection != sel) { view->selection = sel; // TODO: be less brute force about this; only the first/last // line of the selection can change in this case mark_all_lines_changed(view->buffer); } return; } view->sel_so = block_iter_get_offset(&view->cursor); view->sel_eo = SEL_EO_RECALC; view->selection = sel; // Need to mark current line changed because cursor might // move up or down before screen is updated view_update_cursor_y(view); buffer_mark_lines_changed(view->buffer, view->cy, view->cy); } static void do_selection(View *view, SelectionType sel) { if (likely(sel == view->selection)) { // If `sel` is SELECT_NONE here, it's always equal to select_mode BUG_ON(!sel && view->select_mode); return; } do_selection_noinline(view, sel); } static char last_flag_or_default(const CommandArgs *a, char def) { size_t n = a->nr_flags; return n ? a->flags[n - 1] : def; } static char last_flag(const CommandArgs *a) { return last_flag_or_default(a, 0); } static bool has_flag(const CommandArgs *a, unsigned char flag) { return cmdargs_has_flag(a, flag); } static void handle_select_chars_or_lines_flags(View *view, const CommandArgs *a) { SelectionType sel; if (has_flag(a, 'l')) { sel = SELECT_LINES; } else if (has_flag(a, 'c')) { static_assert(SELECT_CHARS < SELECT_LINES); sel = MAX(SELECT_CHARS, view->select_mode); } else { sel = view->select_mode; } do_selection(view, sel); } static void handle_select_chars_flag(View *view, const CommandArgs *a) { BUG_ON(has_flag(a, 'l')); handle_select_chars_or_lines_flags(view, a); } static bool cmd_alias(EditorState *e, const CommandArgs *a) { const char *const name = a->args[0]; const char *const cmd = a->args[1]; if (unlikely(name[0] == '\0')) { return error_msg("Empty alias name not allowed"); } if (unlikely(name[0] == '-')) { // Disallowing this simplifies auto-completion for "alias " return error_msg("Alias name cannot begin with '-'"); } for (size_t i = 0; name[i]; i++) { unsigned char c = name[i]; if (unlikely(!(is_word_byte(c) || c == '-' || c == '?' || c == '!'))) { return error_msg("Invalid byte in alias name: %c (0x%02hhX)", c, c); } } if (unlikely(find_normal_command(name))) { return error_msg("Can't replace existing command %s with an alias", name); } if (likely(cmd)) { add_alias(&e->aliases, name, cmd); } else { remove_alias(&e->aliases, name); } return true; } static bool cmd_bind(EditorState *e, const CommandArgs *a) { const char *keystr = a->args[0]; const char *cmd = a->args[1]; KeyCode key; if (unlikely(!parse_key_string(&key, keystr))) { return error_msg("invalid key string: %s", keystr); } const bool modes[] = { [INPUT_NORMAL] = a->nr_flags == 0 || has_flag(a, 'n'), [INPUT_COMMAND] = has_flag(a, 'c'), [INPUT_SEARCH] = has_flag(a, 's'), }; static_assert(ARRAYLEN(modes) == ARRAYLEN(e->modes)); for (InputMode i = 0; i < ARRAYLEN(modes); i++) { if (!modes[i]) { continue; } IntMap *bindings = &e->modes[i].key_bindings; if (likely(cmd)) { CommandRunner runner = cmdrunner_for_mode(e, i, false); add_binding(bindings, key, cached_command_new(&runner, cmd)); } else { remove_binding(bindings, key); } } return true; } static bool cmd_bof(EditorState *e, const CommandArgs *a) { handle_select_chars_or_lines_flags(e->view, a); move_bof(e->view); return true; } static bool cmd_bol(EditorState *e, const CommandArgs *a) { static const FlagMapping map[] = { {'s', BOL_SMART}, {'t', BOL_SMART | BOL_SMART_TOGGLE}, }; SmartBolFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)); handle_select_chars_flag(e->view, a); move_bol_smart(e->view, flags); return true; } static bool cmd_bolsf(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); View *view = e->view; handle_select_chars_or_lines_flags(view, a); if (!block_iter_bol(&view->cursor)) { unsigned int margin = e->options.scroll_margin; long top = view->vy + window_get_scroll_margin(e->window, margin); if (view->cy > top) { move_up(view, view->cy - top); } else { block_iter_bof(&view->cursor); } } view_reset_preferred_x(view); return true; } static bool cmd_bookmark(EditorState *e, const CommandArgs *a) { if (has_flag(a, 'r')) { bookmark_pop(e->window, &e->bookmarks); return true; } bookmark_push(&e->bookmarks, get_current_file_location(e->view)); return true; } static bool cmd_case(EditorState *e, const CommandArgs *a) { change_case(e->view, last_flag_or_default(a, 't')); return true; } static void mark_tabbar_changed(Window *window, void* UNUSED_ARG(data)) { window->update_tabbar = true; } static bool cmd_cd(EditorState *e, const CommandArgs *a) { const char *dir = a->args[0]; if (unlikely(dir[0] == '\0')) { return error_msg("directory argument cannot be empty"); } if (streq(dir, "-")) { dir = xgetenv("OLDPWD"); if (!dir) { return error_msg("OLDPWD not set"); } } char buf[8192]; const char *cwd = getcwd(buf, sizeof(buf)); if (chdir(dir) != 0) { return error_msg_errno("changing directory failed"); } if (likely(cwd)) { int r = setenv("OLDPWD", cwd, 1); if (unlikely(r != 0)) { LOG_WARNING("failed to set OLDPWD: %s", strerror(errno)); } } cwd = getcwd(buf, sizeof(buf)); if (likely(cwd)) { int r = setenv("PWD", cwd, 1); if (unlikely(r != 0)) { LOG_WARNING("failed to set PWD: %s", strerror(errno)); } } for (size_t i = 0, n = e->buffers.count; i < n; i++) { Buffer *buffer = e->buffers.ptrs[i]; update_short_filename_cwd(buffer, &e->home_dir, cwd); } frame_for_each_window(e->root_frame, mark_tabbar_changed, NULL); return true; } static bool cmd_center_view(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); e->view->force_center = true; return true; } static bool cmd_clear(EditorState *e, const CommandArgs *a) { bool auto_indent = e->buffer->options.auto_indent && !has_flag(a, 'i'); clear_lines(e->view, auto_indent); return true; } static bool cmd_close(EditorState *e, const CommandArgs *a) { bool force = has_flag(a, 'f'); if (!force && !view_can_close(e->view)) { bool prompt = has_flag(a, 'p'); if (!prompt) { return error_msg ( "The buffer is modified; " "save or run 'close -f' to close without saving" ); } static const char str[] = "Close without saving changes? [y/N]"; if (dialog_prompt(e, str, "ny") != 'y') { return false; } } bool allow_quit = has_flag(a, 'q'); if (allow_quit && e->buffers.count == 1 && e->root_frame->frames.count <= 1) { e->status = EDITOR_EXIT_OK; return true; } bool allow_wclose = has_flag(a, 'w'); if (allow_wclose && e->window->views.count <= 1) { window_close(e->window); return true; } window_close_current_view(e->window); set_view(e->window->view); return true; } static bool cmd_command(EditorState *e, const CommandArgs *a) { const char *text = a->args[0]; set_input_mode(e, INPUT_COMMAND); if (text) { cmdline_set_text(&e->cmdline, text); } return true; } static bool cmd_compile(EditorState *e, const CommandArgs *a) { static const FlagMapping map[] = { {'1', SPAWN_READ_STDOUT}, {'p', SPAWN_PROMPT}, {'s', SPAWN_QUIET}, }; Compiler *c = find_compiler(&e->compilers, a->args[0]); if (unlikely(!c)) { return error_msg("No such error parser %s", a->args[0]); } SpawnContext ctx = { .editor = e, .argv = (const char **)a->args + 1, .flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)), }; clear_messages(&e->messages); bool ok = spawn_compiler(&ctx, c, &e->messages); if (e->messages.array.count) { activate_current_message_save(e); } return ok; } static bool cmd_copy(EditorState *e, const CommandArgs *a) { View *view = e->view; const BlockIter save = view->cursor; size_t size; bool line_copy; if (view->selection) { size = prepare_selection(view); line_copy = (view->selection == SELECT_LINES); } else { block_iter_bol(&view->cursor); BlockIter tmp = view->cursor; size = block_iter_eat_line(&tmp); line_copy = true; } if (unlikely(size == 0)) { return true; } bool internal = has_flag(a, 'i'); bool clipboard = has_flag(a, 'b'); bool primary = has_flag(a, 'p'); if (!(internal || clipboard || primary)) { internal = true; } if (internal) { copy(&e->clipboard, view, size, line_copy); } Terminal *term = &e->terminal; if ((clipboard || primary) && term->features & TFLAG_OSC52_COPY) { if (internal) { view->cursor = save; if (view->selection) { size = prepare_selection(view); } } char *buf = block_iter_get_bytes(&view->cursor, size); if (!term_osc52_copy(&term->obuf, buf, size, clipboard, primary)) { error_msg_errno("OSC 52 copy failed"); } free(buf); } if (!has_flag(a, 'k')) { unselect(view); } view->cursor = save; // TODO: return false if term_osc52_copy() failed? return true; } static bool cmd_cursor(EditorState *e, const CommandArgs *a) { if (unlikely(a->nr_args == 0)) { // Reset all cursor styles for (CursorInputMode m = 0; m < ARRAYLEN(e->cursor_styles); m++) { e->cursor_styles[m] = get_default_cursor_style(m); } e->cursor_style_changed = true; return true; } CursorInputMode mode = cursor_mode_from_str(a->args[0]); if (unlikely(mode >= NR_CURSOR_MODES)) { return error_msg("invalid mode argument: %s", a->args[0]); } TermCursorStyle style = get_default_cursor_style(mode); if (a->nr_args >= 2) { style.type = cursor_type_from_str(a->args[1]); if (unlikely(style.type == CURSOR_INVALID)) { return error_msg("invalid cursor type: %s", a->args[1]); } } if (a->nr_args >= 3) { style.color = cursor_color_from_str(a->args[2]); if (unlikely(style.color == COLOR_INVALID)) { return error_msg("invalid cursor color: %s", a->args[2]); } } e->cursor_styles[mode] = style; e->cursor_style_changed = true; return true; } static bool cmd_cut(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); View *view = e->view; const long x = view_get_preferred_x(view); if (view->selection) { bool is_lines = view->selection == SELECT_LINES; cut(&e->clipboard, view, prepare_selection(view), is_lines); if (view->selection == SELECT_LINES) { move_to_preferred_x(view, x); } unselect(view); } else { BlockIter tmp; block_iter_bol(&view->cursor); tmp = view->cursor; cut(&e->clipboard, view, block_iter_eat_line(&tmp), true); move_to_preferred_x(view, x); } return true; } static bool cmd_delete(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); delete_ch(e->view); return true; } static bool cmd_delete_eol(EditorState *e, const CommandArgs *a) { View *view = e->view; if (view->selection) { // TODO: return false? return true; } bool delete_newline_if_at_eol = has_flag(a, 'n'); BlockIter bi = view->cursor; if (delete_newline_if_at_eol) { CodePoint ch; if (block_iter_get_char(&view->cursor, &ch) == 1 && ch == '\n') { delete_ch(view); return true; } } buffer_delete_bytes(view, block_iter_eol(&bi)); return true; } static bool cmd_delete_line(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); delete_lines(e->view); return true; } static bool cmd_delete_word(EditorState *e, const CommandArgs *a) { bool skip_non_word = has_flag(a, 's'); BlockIter bi = e->view->cursor; buffer_delete_bytes(e->view, word_fwd(&bi, skip_non_word)); return true; } static bool cmd_down(EditorState *e, const CommandArgs *a) { handle_select_chars_or_lines_flags(e->view, a); move_down(e->view, 1); return true; } static bool cmd_eof(EditorState *e, const CommandArgs *a) { handle_select_chars_or_lines_flags(e->view, a); move_eof(e->view); return true; } static bool cmd_eol(EditorState *e, const CommandArgs *a) { handle_select_chars_flag(e->view, a); move_eol(e->view); return true; } static bool cmd_eolsf(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); View *view = e->view; handle_select_chars_or_lines_flags(view, a); if (!block_iter_eol(&view->cursor)) { Window *window = e->window; long margin = window_get_scroll_margin(window, e->options.scroll_margin); long bottom = view->vy + window->edit_h - 1 - margin; if (view->cy < bottom) { move_down(view, bottom - view->cy); } else { block_iter_eof(&view->cursor); } } view_reset_preferred_x(view); return true; } static bool cmd_erase(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); erase(e->view); return true; } static bool cmd_erase_bol(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); buffer_erase_bytes(e->view, block_iter_bol(&e->view->cursor)); return true; } static bool cmd_erase_word(EditorState *e, const CommandArgs *a) { View *view = e->view; bool skip_non_word = has_flag(a, 's'); buffer_erase_bytes(view, word_bwd(&view->cursor, skip_non_word)); return true; } static bool cmd_errorfmt(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args == 0); const char *name = a->args[0]; if (a->nr_args == 1) { remove_compiler(&e->compilers, name); return true; } bool ignore = has_flag(a, 'i'); return add_error_fmt(&e->compilers, name, ignore, a->args[1], a->args + 2); } static bool cmd_exec(EditorState *e, const CommandArgs *a) { ExecAction actions[3] = {EXEC_TTY, EXEC_TTY, EXEC_TTY}; SpawnFlags spawn_flags = 0; bool lflag = false; bool move_after_insert = false; bool strip_nl = false; for (size_t i = 0, n = a->nr_flags, argidx = 0, fd; i < n; i++) { switch (a->flags[i]) { case 'e': fd = STDERR_FILENO; break; case 'i': fd = STDIN_FILENO; break; case 'o': fd = STDOUT_FILENO; break; case 'p': spawn_flags |= SPAWN_PROMPT; continue; case 's': spawn_flags |= SPAWN_QUIET; continue; case 't': spawn_flags &= ~SPAWN_QUIET; continue; case 'l': lflag = true; continue; case 'm': move_after_insert = true; continue; case 'n': strip_nl = true; continue; default: BUG("unexpected flag"); return false; } const char *action_name = a->args[argidx++]; ExecAction action = lookup_exec_action(action_name, fd); if (unlikely(action == EXEC_INVALID)) { return error_msg("invalid action for -%c: '%s'", a->flags[i], action_name); } actions[fd] = action; } if (lflag && actions[STDIN_FILENO] == EXEC_BUFFER) { // For compat. with old "filter" and "pipe-to" commands actions[STDIN_FILENO] = EXEC_LINE; } const char **argv = (const char **)a->args + a->nr_flag_args; ssize_t outlen = handle_exec(e, argv, actions, spawn_flags, strip_nl); if (outlen <= 0) { return outlen == 0; } if (move_after_insert && actions[STDOUT_FILENO] == EXEC_BUFFER) { block_iter_skip_bytes(&e->view->cursor, outlen); } return true; } static bool cmd_ft(EditorState *e, const CommandArgs *a) { char **args = a->args; const char *filetype = args[0]; if (unlikely(!is_valid_filetype_name(filetype))) { return error_msg("Invalid filetype name: '%s'", filetype); } FileDetectionType dt = FT_EXTENSION; switch (last_flag(a)) { case 'b': dt = FT_BASENAME; break; case 'c': dt = FT_CONTENT; break; case 'f': dt = FT_FILENAME; break; case 'i': dt = FT_INTERPRETER; break; } size_t nfailed = 0; for (size_t i = 1, n = a->nr_args; i < n; i++) { if (!add_filetype(&e->filetypes, filetype, args[i], dt)) { nfailed++; } } return nfailed == 0; } static bool cmd_hi(EditorState *e, const CommandArgs *a) { if (unlikely(a->nr_args == 0)) { exec_builtin_color_reset(e); goto update; } char **strs = a->args + 1; size_t strs_len = a->nr_args - 1; TermColor color; ssize_t n = parse_term_color(&color, strs, strs_len); if (unlikely(n != strs_len)) { if (n < 0) { return error_msg("too many colors"); } BUG_ON(n > strs_len); return error_msg("invalid color or attribute: '%s'", strs[n]); } TermColorCapabilityType color_type = e->terminal.color_type; bool optimize = e->options.optimize_true_color; int32_t fg = color_to_nearest(color.fg, color_type, optimize); int32_t bg = color_to_nearest(color.bg, color_type, optimize); if ( color_type != TERM_TRUE_COLOR && has_flag(a, 'c') && (fg != color.fg || bg != color.bg) ) { return true; } color.fg = fg; color.bg = bg; set_highlight_color(&e->colors, a->args[0], &color); update: // Don't call update_all_syntax_colors() needlessly; it's called // right after config has been loaded if (e->status != EDITOR_INITIALIZING) { update_all_syntax_colors(&e->syntaxes, &e->colors); mark_everything_changed(e); } return true; } static bool cmd_include(EditorState *e, const CommandArgs *a) { ConfigFlags flags = has_flag(a, 'q') ? CFG_NOFLAGS : CFG_MUST_EXIST; if (has_flag(a, 'b')) { flags |= CFG_BUILTIN; } int err = read_normal_config(e, a->args[0], flags); // TODO: Clean up read_normal_config() so this can be simplified to `err == 0` return err == 0 || (err == ENOENT && !(flags & CFG_MUST_EXIST)); } static bool cmd_insert(EditorState *e, const CommandArgs *a) { const char *str = a->args[0]; if (has_flag(a, 'k')) { for (size_t i = 0; str[i]; i++) { insert_ch(e->view, str[i]); } return true; } bool move_after = has_flag(a, 'm'); insert_text(e->view, str, strlen(str), move_after); return true; } static bool cmd_join(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); join_lines(e->view); return true; } static bool cmd_left(EditorState *e, const CommandArgs *a) { handle_select_chars_flag(e->view, a); move_cursor_left(e->view); return true; } static bool cmd_line(EditorState *e, const CommandArgs *a) { const char *str = a->args[0]; size_t line, column; if (unlikely(!str_to_xfilepos(str, &line, &column))) { return error_msg("Invalid line number: %s", str); } View *view = e->view; long x = view_get_preferred_x(view); unselect(view); if (column >= 1) { // Column was specified; move to exact position move_to_filepos(view, line, column); } else { // Column was omitted; move to line while preserving current column move_to_line(view, line); move_to_preferred_x(view, x); } return true; } static bool cmd_load_syntax(EditorState *e, const CommandArgs *a) { const char *arg = a->args[0]; const char *slash = strrchr(arg, '/'); if (!slash) { const char *filetype = arg; if (find_syntax(&e->syntaxes, filetype)) { return true; } return !!load_syntax_by_filetype(e, filetype); } const char *filetype = slash + 1; if (find_syntax(&e->syntaxes, filetype)) { return error_msg("Syntax for filetype %s already loaded", filetype); } int err; return !!load_syntax_file(e, arg, CFG_MUST_EXIST, &err); } static bool cmd_macro(EditorState *e, const CommandArgs *a) { CommandMacroState *m = &e->macro; const char *action = a->args[0]; if (streq(action, "play") || streq(action, "run")) { for (size_t i = 0, n = m->macro.count; i < n; i++) { const char *cmd_str = m->macro.ptrs[i]; if (!handle_normal_command(e, cmd_str, false)) { return false; } } return true; } const char *msg; if (streq(action, "toggle")) { if (m->recording) { goto stop; } goto record; } if (streq(action, "record")) { record: msg = macro_record(m) ? "Recording macro" : "Already recording"; goto message; } if (streq(action, "stop")) { stop: if (!macro_stop(m)) { msg = "Not recording"; goto message; } size_t count = m->macro.count; const char *plural = (count != 1) ? "s" : ""; info_msg("Macro recording stopped; %zu command%s saved", count, plural); return true; } if (streq(action, "cancel")) { msg = macro_cancel(m) ? "Macro recording cancelled" : "Not recording"; goto message; } return error_msg("Unknown action '%s'", action); message: info_msg("%s", msg); // TODO: make this conditional? return true; } static bool cmd_match_bracket(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); View *view = e->view; CodePoint cursor_char; if (!block_iter_get_char(&view->cursor, &cursor_char)) { return error_msg("No character under cursor"); } CodePoint target = cursor_char; BlockIter bi = view->cursor; size_t level = 0; CodePoint u = 0; switch (cursor_char) { case '<': case '[': case '{': target++; // Fallthrough case '(': target++; goto search_fwd; case '>': case ']': case '}': target--; // Fallthrough case ')': target--; goto search_bwd; default: return error_msg("Character under cursor not matchable"); } search_fwd: block_iter_next_char(&bi, &u); BUG_ON(u != cursor_char); while (block_iter_next_char(&bi, &u)) { if (u == target) { if (level == 0) { block_iter_prev_char(&bi, &u); view->cursor = bi; return true; // Found } level--; } else if (u == cursor_char) { level++; } } goto not_found; search_bwd: while (block_iter_prev_char(&bi, &u)) { if (u == target) { if (level == 0) { view->cursor = bi; return true; // Found } level--; } else if (u == cursor_char) { level++; } } not_found: return error_msg("No matching bracket found"); } static bool cmd_move_tab(EditorState *e, const CommandArgs *a) { Window *window = e->window; const size_t ntabs = window->views.count; const char *str = a->args[0]; size_t to, from = ptr_array_idx(&window->views, e->view); BUG_ON(from >= ntabs); if (streq(str, "left")) { to = size_decrement_wrapped(from, ntabs); } else if (streq(str, "right")) { to = size_increment_wrapped(from, ntabs); } else { if (!str_to_size(str, &to) || to == 0) { return error_msg("Invalid tab position %s", str); } to = MIN(to, ntabs) - 1; } ptr_array_move(&window->views, from, to); window->update_tabbar = true; return true; } static bool cmd_msg(EditorState *e, const CommandArgs *a) { const char *str = a->args[0]; uint_least64_t np = cmdargs_flagset_value('n') | cmdargs_flagset_value('p'); if (u64_popcount(a->flag_set & np) + !!str >= 2) { return error_msg("flags [-n|-p] and [number] argument are mutually exclusive"); } MessageArray *msgs = &e->messages; size_t count = msgs->array.count; if (count == 0) { return true; } size_t p = msgs->pos; BUG_ON(p >= count); if (has_flag(a, 'n')) { p = MIN(p + 1, count - 1); } else if (has_flag(a, 'p')) { p = p ? p - 1 : 0; } else if (str) { if (!str_to_size(str, &p) || p == 0) { return error_msg("invalid message index: %s", str); } p = MIN(p - 1, count - 1); } msgs->pos = p; return activate_current_message(e); } static bool cmd_new_line(EditorState *e, const CommandArgs *a) { new_line(e->view, has_flag(a, 'a')); return true; } static bool cmd_next(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); size_t i = ptr_array_idx(&e->window->views, e->view); size_t n = e->window->views.count; BUG_ON(i >= n); set_view(e->window->views.ptrs[size_increment_wrapped(i, n)]); return true; } static bool xglob(char **args, glob_t *globbuf) { BUG_ON(!args); BUG_ON(!args[0]); int err = glob(*args, GLOB_NOCHECK, NULL, globbuf); while (err == 0 && *++args) { err = glob(*args, GLOB_NOCHECK | GLOB_APPEND, NULL, globbuf); } if (likely(err == 0)) { BUG_ON(globbuf->gl_pathc == 0); BUG_ON(!globbuf->gl_pathv); BUG_ON(!globbuf->gl_pathv[0]); return true; } BUG_ON(err == GLOB_NOMATCH); globfree(globbuf); return error_msg("glob: %s", (err == GLOB_NOSPACE) ? strerror(ENOMEM) : "failed"); } static bool cmd_open(EditorState *e, const CommandArgs *a) { bool temporary = has_flag(a, 't'); if (unlikely(temporary && a->nr_args > 0)) { return error_msg("'open -t' can't be used with filename arguments"); } const char *requested_encoding = NULL; char **args = a->args; if (unlikely(a->nr_flag_args > 0)) { // The "-e" flag is the only one that takes an argument, so the // above condition implies it was used BUG_ON(!has_flag(a, 'e')); requested_encoding = args[a->nr_flag_args - 1]; args += a->nr_flag_args; } Encoding encoding = {.type = ENCODING_AUTODETECT}; if (requested_encoding) { EncodingType enctype = lookup_encoding(requested_encoding); if (enctype == UTF8) { encoding = encoding_from_type(enctype); } else if (conversion_supported_by_iconv(requested_encoding, "UTF-8")) { encoding = encoding_from_name(requested_encoding); } else { if (errno == EINVAL) { return error_msg("Unsupported encoding '%s'", requested_encoding); } return error_msg ( "iconv conversion from '%s' failed: %s", requested_encoding, strerror(errno) ); } } if (a->nr_args == 0) { View *view = window_open_new_file(e->window); view->buffer->temporary = temporary; if (requested_encoding) { buffer_set_encoding(view->buffer, encoding, e->options.utf8_bom); } return true; } char **paths = args; glob_t globbuf; bool use_glob = has_flag(a, 'g'); if (use_glob) { if (!xglob(args, &globbuf)) { return false; } paths = globbuf.gl_pathv; } View *first_opened; if (!paths[1]) { // Previous view is remembered when opening single file first_opened = window_open_file(e->window, paths[0], &encoding); } else { // It makes no sense to remember previous view when opening multiple files first_opened = window_open_files(e->window, paths, &encoding); } if (use_glob) { globfree(&globbuf); } return !!first_opened; } static bool cmd_option(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args < 3); size_t nstrs = a->nr_args - 1; if (unlikely(nstrs & 1)) { return error_msg("Missing option value"); } char **strs = a->args + 1; if (unlikely(!validate_local_options(strs))) { return false; } PointerArray *opts = &e->file_options; if (has_flag(a, 'r')) { const StringView pattern = strview_from_cstring(a->args[0]); return add_file_options(opts, FOPTS_FILENAME, pattern, strs, nstrs); } const char *ft_list = a->args[0]; size_t errors = 0; for (size_t pos = 0, len = strlen(ft_list); pos < len; ) { const StringView filetype = get_delim(ft_list, &pos, len, ','); if (!add_file_options(opts, FOPTS_FILETYPE, filetype, strs, nstrs)) { errors++; } } return !errors; } static bool cmd_blkdown(EditorState *e, const CommandArgs *a) { View *view = e->view; handle_select_chars_or_lines_flags(view, a); // If current line is blank, skip past consecutive blank lines StringView line; fetch_this_line(&view->cursor, &line); if (strview_isblank(&line)) { while (block_iter_next_line(&view->cursor)) { fill_line_ref(&view->cursor, &line); if (!strview_isblank(&line)) { break; } } } // Skip past non-blank lines while (block_iter_next_line(&view->cursor)) { fill_line_ref(&view->cursor, &line); if (strview_isblank(&line)) { break; } } // If we reach the last populated line in the buffer, move down one line BlockIter tmp = view->cursor; block_iter_eol(&tmp); block_iter_skip_bytes(&tmp, 1); if (block_iter_is_eof(&tmp)) { view->cursor = tmp; } return true; } static bool cmd_blkup(EditorState *e, const CommandArgs *a) { View *view = e->view; handle_select_chars_or_lines_flags(view, a); // If cursor is on the first line, just move to bol if (view->cy == 0) { block_iter_bol(&view->cursor); return true; } // If current line is blank, skip past consecutive blank lines StringView line; fetch_this_line(&view->cursor, &line); if (strview_isblank(&line)) { while (block_iter_prev_line(&view->cursor)) { fill_line_ref(&view->cursor, &line); if (!strview_isblank(&line)) { break; } } } // Skip past non-blank lines while (block_iter_prev_line(&view->cursor)) { fill_line_ref(&view->cursor, &line); if (strview_isblank(&line)) { break; } } return true; } static bool cmd_paste(EditorState *e, const CommandArgs *a) { bool move_after = has_flag(a, 'm'); bool above_cursor = has_flag(a, 'a'); bool at_cursor = has_flag(a, 'c'); PasteLinesType type = PASTE_LINES_BELOW_CURSOR; if (above_cursor && at_cursor) { return error_msg("flags -a and -c are mutually exclusive"); } else if (above_cursor) { type = PASTE_LINES_ABOVE_CURSOR; } else if (at_cursor) { type = PASTE_LINES_INLINE; } paste(&e->clipboard, e->view, type, move_after); return true; } static bool cmd_pgdown(EditorState *e, const CommandArgs *a) { View *view = e->view; handle_select_chars_or_lines_flags(view, a); Window *window = e->window; long margin = window_get_scroll_margin(window, e->options.scroll_margin); long bottom = view->vy + window->edit_h - 1 - margin; long count; if (view->cy < bottom) { count = bottom - view->cy; } else { count = window->edit_h - 1 - margin * 2; } move_down(view, count); return true; } static bool cmd_pgup(EditorState *e, const CommandArgs *a) { View *view = e->view; handle_select_chars_or_lines_flags(view, a); Window *window = e->window; long margin = window_get_scroll_margin(window, e->options.scroll_margin); long top = view->vy + margin; long count; if (view->cy > top) { count = view->cy - top; } else { count = window->edit_h - 1 - margin * 2; } move_up(view, count); return true; } static bool cmd_prev(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); size_t i = ptr_array_idx(&e->window->views, e->view); size_t n = e->window->views.count; BUG_ON(i >= n); set_view(e->window->views.ptrs[size_decrement_wrapped(i, n)]); return true; } static View *window_find_modified_view(Window *window) { if (buffer_modified(window->view->buffer)) { return window->view; } for (size_t i = 0, n = window->views.count; i < n; i++) { View *view = window->views.ptrs[i]; if (buffer_modified(view->buffer)) { return view; } } return NULL; } static size_t count_modified_buffers(const PointerArray *buffers, View **first) { View *modified = NULL; size_t nr_modified = 0; for (size_t i = 0, n = buffers->count; i < n; i++) { Buffer *buffer = buffers->ptrs[i]; if (!buffer_modified(buffer)) { continue; } nr_modified++; if (!modified) { modified = buffer->views.ptrs[0]; } } BUG_ON(nr_modified > 0 && !modified); *first = modified; return nr_modified; } static bool cmd_quit(EditorState *e, const CommandArgs *a) { int exit_code = EDITOR_EXIT_OK; if (a->nr_args) { if (!str_to_int(a->args[0], &exit_code)) { return error_msg("Not a valid integer argument: '%s'", a->args[0]); } int max = EDITOR_EXIT_MAX; if (exit_code < 0 || exit_code > max) { return error_msg("Exit code should be between 0 and %d", max); } } View *first_modified = NULL; size_t n = count_modified_buffers(&e->buffers, &first_modified); if (n == 0) { goto exit; } BUG_ON(!first_modified); const char *plural = (n > 1) ? "s" : ""; if (has_flag(a, 'f')) { LOG_INFO("force quitting with %zu modified buffer%s", n, plural); goto exit; } // Activate a modified view (giving preference to the current view or // a view in the current window) View *view = window_find_modified_view(e->window); set_view(view ? view : first_modified); if (!has_flag(a, 'p')) { return error_msg("Save modified files or run 'quit -f' to quit without saving"); } char question[128]; xsnprintf ( question, sizeof question, "Quit without saving %zu modified buffer%s? [y/N]", n, plural ); if (dialog_prompt(e, question, "ny") != 'y') { return false; } LOG_INFO("quit prompt accepted with %zu modified buffer%s", n, plural); exit: e->status = exit_code; return true; } static bool cmd_redo(EditorState *e, const CommandArgs *a) { char *arg = a->args[0]; unsigned long change_id = 0; if (arg) { if (!str_to_ulong(arg, &change_id) || change_id == 0) { return error_msg("Invalid change id: %s", arg); } } if (!redo(e->view, change_id)) { return false; } unselect(e->view); return true; } static bool cmd_refresh(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); mark_everything_changed(e); return true; } static bool repeat_insert(EditorState *e, const char *str, unsigned int count, bool move_after) { size_t str_len = strlen(str); size_t bufsize; if (unlikely(size_multiply_overflows(count, str_len, &bufsize))) { return error_msg("Repeated insert would overflow"); } if (unlikely(bufsize == 0)) { return true; } char *buf = malloc(bufsize); if (unlikely(!buf)) { return error_msg_errno("malloc"); } char tmp[4096]; if (str_len == 1) { memset(buf, str[0], bufsize); goto insert; } else if (bufsize < 2 * sizeof(tmp) || str_len > sizeof(tmp) / 8) { for (size_t i = 0; i < count; i++) { memcpy(buf + (i * str_len), str, str_len); } goto insert; } size_t strs_per_tmp = sizeof(tmp) / str_len; size_t tmp_len = strs_per_tmp * str_len; size_t tmps_per_buf = bufsize / tmp_len; size_t remainder = bufsize % tmp_len; // Create a block of text containing `strs_per_tmp` concatenated strs for (size_t i = 0; i < strs_per_tmp; i++) { memcpy(tmp + (i * str_len), str, str_len); } // Copy `tmps_per_buf` copies of `tmp` into `buf` for (size_t i = 0; i < tmps_per_buf; i++) { memcpy(buf + (i * tmp_len), tmp, tmp_len); } // Copy the remainder into `buf` (if any) if (remainder) { memcpy(buf + (tmps_per_buf * tmp_len), tmp, remainder); } LOG_DEBUG ( "Optimized %u inserts of %zu bytes into %zu inserts of %zu bytes", count, str_len, tmps_per_buf, tmp_len ); insert: insert_text(e->view, buf, bufsize, move_after); free(buf); return true; } static bool cmd_repeat(EditorState *e, const CommandArgs *a) { unsigned int count; if (unlikely(!str_to_uint(a->args[0], &count))) { return error_msg("Not a valid repeat count: %s", a->args[0]); } if (unlikely(count == 0)) { return true; } const Command *cmd = find_normal_command(a->args[1]); if (unlikely(!cmd)) { return error_msg("No such command: %s", a->args[1]); } CommandArgs a2 = cmdargs_new(a->args + 2); current_command = cmd; bool ok = parse_args(cmd, &a2); current_command = NULL; if (unlikely(!ok)) { return false; } CommandFunc fn = cmd->cmd; if (fn == (CommandFunc)cmd_insert && !has_flag(&a2, 'k')) { // Use optimized implementation for repeated "insert" return repeat_insert(e, a2.args[0], count, has_flag(&a2, 'm')); } while (count--) { fn(e, &a2); } // TODO: return false if fn() fails? return true; } static bool cmd_replace(EditorState *e, const CommandArgs *a) { static const FlagMapping map[] = { {'b', REPLACE_BASIC}, {'c', REPLACE_CONFIRM}, {'g', REPLACE_GLOBAL}, {'i', REPLACE_IGNORE_CASE}, }; ReplaceFlags flags = cmdargs_convert_flags(a, map, ARRAYLEN(map)); return reg_replace(e->view, a->args[0], a->args[1], flags); } static bool cmd_right(EditorState *e, const CommandArgs *a) { handle_select_chars_flag(e->view, a); move_cursor_right(e->view); return true; } static bool stat_changed(const FileInfo *file, const struct stat *st) { // Don't compare st_mode because we allow chmod 755 etc. return !timespecs_equal(get_stat_mtime(st), &file->mtime) || st->st_dev != file->dev || st->st_ino != file->ino || st->st_size != file->size; } static bool save_unmodified_buffer(Buffer *buffer, const char *filename) { SaveUnmodifiedType type = buffer->options.save_unmodified; if (type == SAVE_NONE) { LOG_INFO("buffer unchanged; leaving file untouched"); return true; } BUG_ON(type != SAVE_TOUCH); struct timespec times[2]; if (unlikely(clock_gettime(CLOCK_REALTIME, ×[0]) != 0)) { LOG_ERRNO("aborting partial save; clock_gettime() failed"); return false; } times[1] = times[0]; if (unlikely(utimensat(AT_FDCWD, filename, times, 0) != 0)) { LOG_ERRNO("aborting partial save; utimensat() failed"); return false; } buffer->file.mtime = times[0]; LOG_INFO("buffer unchanged; mtime/atime updated"); return true; } static bool cmd_save(EditorState *e, const CommandArgs *a) { Buffer *buffer = e->buffer; if (unlikely(buffer->stdout_buffer)) { const char *f = buffer_filename(buffer); info_msg("%s can't be saved; it will be piped to stdout on exit", f); return true; } bool dos_nl = has_flag(a, 'd'); bool unix_nl = has_flag(a, 'u'); bool crlf = buffer->crlf_newlines; if (unlikely(dos_nl && unix_nl)) { return error_msg("flags -d and -u can't be used together"); } else if (dos_nl) { crlf = true; } else if (unix_nl) { crlf = false; } const char *requested_encoding = NULL; char **args = a->args; if (unlikely(a->nr_flag_args > 0)) { BUG_ON(!has_flag(a, 'e')); requested_encoding = args[a->nr_flag_args - 1]; args += a->nr_flag_args; } Encoding encoding = buffer->encoding; bool bom = buffer->bom; if (requested_encoding) { EncodingType et = lookup_encoding(requested_encoding); if (et == UTF8) { if (encoding.type != UTF8) { // Encoding changed encoding = encoding_from_type(et); bom = e->options.utf8_bom; } } else if (conversion_supported_by_iconv("UTF-8", requested_encoding)) { encoding = encoding_from_name(requested_encoding); if (encoding.name != buffer->encoding.name) { // Encoding changed bom = !!get_bom_for_encoding(encoding.type); } } else { if (errno == EINVAL) { return error_msg("Unsupported encoding '%s'", requested_encoding); } return error_msg ( "iconv conversion to '%s' failed: %s", requested_encoding, strerror(errno) ); } } bool b = has_flag(a, 'b'); bool B = has_flag(a, 'B'); if (unlikely(b && B)) { return error_msg("flags -b and -B can't be used together"); } else if (b) { bom = true; } else if (B) { bom = false; } char *absolute = buffer->abs_filename; bool force = has_flag(a, 'f'); bool new_locked = false; if (a->nr_args > 0) { if (args[0][0] == '\0') { return error_msg("Empty filename not allowed"); } char *tmp = path_absolute(args[0]); if (!tmp) { return error_msg_errno("Failed to make absolute path"); } if (absolute && streq(tmp, absolute)) { free(tmp); } else { absolute = tmp; } } else { if (!absolute) { if (!has_flag(a, 'p')) { return error_msg("No filename"); } set_input_mode(e, INPUT_COMMAND); cmdline_set_text(&e->cmdline, "save "); return true; } if (buffer->readonly && !force) { return error_msg("Use -f to force saving read-only file"); } } mode_t old_mode = buffer->file.mode; bool hardlinks = false; struct stat st; bool stat_ok = !stat(absolute, &st); if (!stat_ok) { if (errno != ENOENT) { error_msg("stat failed for %s: %s", absolute, strerror(errno)); goto error; } } else { if ( absolute == buffer->abs_filename && !force && stat_changed(&buffer->file, &st) ) { error_msg ( "File has been modified by another process; " "use 'save -f' to force overwrite" ); goto error; } if (S_ISDIR(st.st_mode)) { error_msg("Will not overwrite directory %s", absolute); goto error; } hardlinks = (st.st_nlink >= 2); } if (e->options.lock_files) { if (absolute == buffer->abs_filename) { if (!buffer->locked) { if (!lock_file(absolute)) { if (!force) { error_msg("Can't lock file %s", absolute); goto error; } } else { buffer->locked = true; } } } else { if (!lock_file(absolute)) { if (!force) { error_msg("Can't lock file %s", absolute); goto error; } } else { new_locked = true; } } } if (stat_ok) { if (absolute != buffer->abs_filename && !force) { error_msg("Use -f to overwrite %s", absolute); goto error; } // Allow chmod 755 etc. buffer->file.mode = st.st_mode; } if ( stat_ok && buffer->options.save_unmodified != SAVE_FULL && !stat_changed(&buffer->file, &st) && st.st_uid == buffer->file.uid && st.st_gid == buffer->file.gid && !buffer_modified(buffer) && absolute == buffer->abs_filename && encoding.name == buffer->encoding.name && crlf == buffer->crlf_newlines && bom == buffer->bom && save_unmodified_buffer(buffer, absolute) ) { BUG_ON(new_locked); return true; } if (!save_buffer(buffer, absolute, &encoding, crlf, bom, hardlinks)) { goto error; } buffer->saved_change = buffer->cur_change; buffer->readonly = false; buffer->temporary = false; buffer->crlf_newlines = crlf; buffer->bom = bom; if (requested_encoding) { buffer->encoding = encoding; } if (absolute != buffer->abs_filename) { if (buffer->locked) { // Filename changes, release old file lock unlock_file(buffer->abs_filename); } buffer->locked = new_locked; free(buffer->abs_filename); buffer->abs_filename = absolute; update_short_filename(buffer, &e->home_dir); // Filename change is not detected (only buffer_modified() change) mark_buffer_tabbars_changed(buffer); } if (!old_mode && streq(buffer->options.filetype, "none")) { // New file and most likely user has not changed the filetype if (buffer_detect_filetype(buffer, &e->filetypes)) { set_file_options(e, buffer); set_editorconfig_options(buffer); buffer_update_syntax(e, buffer); } } return true; error: if (new_locked) { unlock_file(absolute); } if (absolute != buffer->abs_filename) { free(absolute); } return false; } static bool cmd_scroll_down(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); View *view = e->view; view->vy++; if (view->cy < view->vy) { move_down(view, 1); } return true; } static bool cmd_scroll_pgdown(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); Window *window = e->window; View *view = e->view; long max = view->buffer->nl - window->edit_h + 1; if (view->vy < max && max > 0) { long count = window->edit_h - 1; if (view->vy + count > max) { count = max - view->vy; } view->vy += count; move_down(view, count); } else if (view->cy < view->buffer->nl) { move_down(view, view->buffer->nl - view->cy); } return true; } static bool cmd_scroll_pgup(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); Window *window = e->window; View *view = e->view; if (view->vy > 0) { long count = MIN(window->edit_h - 1, view->vy); view->vy -= count; move_up(view, count); } else if (view->cy > 0) { move_up(view, view->cy); } return true; } static bool cmd_scroll_up(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); Window *window = e->window; View *view = e->view; if (view->vy) { view->vy--; } if (view->vy + window->edit_h <= view->cy) { move_up(view, 1); } return true; } static uint_least64_t get_flagset_npw(void) { uint_least64_t npw = 0; npw |= cmdargs_flagset_value('n'); npw |= cmdargs_flagset_value('p'); npw |= cmdargs_flagset_value('w'); return npw; } static bool cmd_search(EditorState *e, const CommandArgs *a) { const char *pattern = a->args[0]; if (u64_popcount(a->flag_set & get_flagset_npw()) + !!pattern >= 2) { return error_msg("flags [-n|-p|-w] and [pattern] argument are mutually exclusive"); } View *view = e->view; char pattbuf[4096]; bool use_word_under_cursor = has_flag(a, 'w'); if (use_word_under_cursor) { StringView word = view_get_word_under_cursor(view); if (word.length == 0) { // Error message would not be very useful here return false; } const RegexpWordBoundaryTokens *rwbt = &e->regexp_word_tokens; const size_t bmax = sizeof(rwbt->start); static_assert_compatible_types(rwbt->start, char[8]); if (unlikely(word.length >= sizeof(pattbuf) - (bmax * 2))) { return error_msg("word under cursor too long"); } char *ptr = stpncpy(pattbuf, rwbt->start, bmax); memcpy(ptr, word.data, word.length); memcpy(ptr + word.length, rwbt->end, bmax); pattern = pattbuf; } SearchState *search = &e->search; SearchCaseSensitivity cs = e->options.case_sensitive_search; unselect(view); if (has_flag(a, 'n')) { return search_next(view, search, cs); } if (has_flag(a, 'p')) { return search_prev(view, search, cs); } search->reverse = has_flag(a, 'r'); if (!pattern) { set_input_mode(e, INPUT_SEARCH); return true; } bool found; search_set_regexp(search, pattern); if (use_word_under_cursor) { found = search_next_word(view, search, cs); } else { found = search_next(view, search, cs); } if (!has_flag(a, 'H')) { history_add(&e->search_history, pattern); } return found; } static bool cmd_select_block(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); select_block(e->view); // TODO: return false if select_block() doesn't select anything? return true; } static bool cmd_select(EditorState *e, const CommandArgs *a) { View *view = e->view; SelectionType sel = has_flag(a, 'l') ? SELECT_LINES : SELECT_CHARS; bool keep = has_flag(a, 'k'); if (!keep && view->selection && view->selection == sel) { sel = SELECT_NONE; } view->select_mode = sel; do_selection(view, sel); return true; } static bool cmd_set(EditorState *e, const CommandArgs *a) { bool global = has_flag(a, 'g'); bool local = has_flag(a, 'l'); if (!e->buffer) { if (unlikely(local)) { return error_msg("Flag -l makes no sense in config file"); } global = true; } char **args = a->args; size_t count = a->nr_args; if (count == 1) { return set_bool_option(e, args[0], local, global); } if (count & 1) { return error_msg("One or even number of arguments expected"); } size_t errors = 0; for (size_t i = 0; i < count; i += 2) { if (!set_option(e, args[i], args[i + 1], local, global)) { errors++; } } return !errors; } static bool cmd_setenv(EditorState* UNUSED_ARG(e), const CommandArgs *a) { const char *name = a->args[0]; if (unlikely(streq(name, "DTE_VERSION"))) { return error_msg("$DTE_VERSION cannot be changed"); } const size_t nr_args = a->nr_args; int res; if (nr_args == 2) { res = setenv(name, a->args[1], true); } else { BUG_ON(nr_args != 1); res = unsetenv(name); } if (likely(res == 0)) { return true; } if (errno == EINVAL) { return error_msg("Invalid environment variable name '%s'", name); } return error_msg_errno(nr_args == 2 ? "setenv" : "unsetenv"); } static bool cmd_shift(EditorState *e, const CommandArgs *a) { const char *arg = a->args[0]; int count; if (!str_to_int(arg, &count)) { return error_msg("Invalid number: %s", arg); } if (count == 0) { return error_msg("Count must be non-zero"); } shift_lines(e->view, count); return true; } static bool cmd_show(EditorState *e, const CommandArgs *a) { bool write_to_cmdline = has_flag(a, 'c'); if (write_to_cmdline && a->nr_args < 2) { return error_msg("\"show -c\" requires 2 arguments"); } return show(e, a->args[0], a->args[1], write_to_cmdline); } static bool cmd_suspend(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); if (e->status == EDITOR_INITIALIZING) { LOG_WARNING("suspend request ignored"); return false; } if (e->session_leader) { return error_msg("Session leader can't suspend"); } ui_end(e); bool suspended = !kill(0, SIGSTOP); if (!suspended) { error_msg_errno("kill"); } term_raw(); ui_start(e); return suspended; } static bool cmd_tag(EditorState *e, const CommandArgs *a) { if (has_flag(a, 'r')) { bookmark_pop(e->window, &e->bookmarks); return true; } StringView name; if (a->args[0]) { name = strview_from_cstring(a->args[0]); } else { name = view_get_word_under_cursor(e->view); if (name.length == 0) { return false; } } const char *filename = e->buffer->abs_filename; size_t ntags = tag_lookup(&e->tagfile, &name, filename, &e->messages); activate_current_message_save(e); return (ntags > 0); } static bool cmd_title(EditorState *e, const CommandArgs *a) { Buffer *buffer = e->buffer; if (buffer->abs_filename) { return error_msg("saved buffers can't be retitled"); } set_display_filename(buffer, xstrdup(a->args[0])); mark_buffer_tabbars_changed(buffer); return true; } static bool cmd_toggle(EditorState *e, const CommandArgs *a) { bool global = has_flag(a, 'g'); bool verbose = has_flag(a, 'v'); const char *option_name = a->args[0]; size_t nr_values = a->nr_args - 1; if (nr_values == 0) { return toggle_option(e, option_name, global, verbose); } char **values = a->args + 1; return toggle_option_values(e, option_name, global, verbose, values, nr_values); } static bool cmd_undo(EditorState *e, const CommandArgs *a) { View *view = e->view; bool move_only = has_flag(a, 'm'); if (move_only) { const Change *change = view->buffer->cur_change; if (!change->next) { // If there's only 1 change, there's nothing meaningful to move to return false; } block_iter_goto_offset(&view->cursor, change->offset); view_reset_preferred_x(view); return true; } if (!undo(view)) { return false; } unselect(view); return true; } static bool cmd_unselect(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); unselect(e->view); return true; } static bool cmd_up(EditorState *e, const CommandArgs *a) { handle_select_chars_or_lines_flags(e->view, a); move_up(e->view, 1); return true; } static bool cmd_view(EditorState *e, const CommandArgs *a) { Window *window = e->window; BUG_ON(window->views.count == 0); const char *arg = a->args[0]; size_t idx; if (streq(arg, "last")) { idx = window->views.count - 1; } else { if (!str_to_size(arg, &idx) || idx == 0) { return error_msg("Invalid view index: %s", arg); } idx = MIN(idx, window->views.count) - 1; } set_view(window->views.ptrs[idx]); return true; } static bool cmd_wclose(EditorState *e, const CommandArgs *a) { View *view = window_find_unclosable_view(e->window); bool force = has_flag(a, 'f'); if (!view || force) { goto close; } bool prompt = has_flag(a, 'p'); set_view(view); if (!prompt) { return error_msg ( "Save modified files or run 'wclose -f' to close " "window without saving" ); } if (dialog_prompt(e, "Close window without saving? [y/N]", "ny") != 'y') { return false; } close: window_close(e->window); return true; } static bool cmd_wflip(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); Frame *frame = e->window->frame; if (!frame->parent) { return false; } frame->parent->vertical ^= 1; mark_everything_changed(e); return true; } static bool cmd_wnext(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); e->window = next_window(e->window); set_view(e->window->view); mark_everything_changed(e); debug_frame(e->root_frame); return true; } static bool cmd_word_bwd(EditorState *e, const CommandArgs *a) { handle_select_chars_flag(e->view, a); bool skip_non_word = has_flag(a, 's'); word_bwd(&e->view->cursor, skip_non_word); view_reset_preferred_x(e->view); return true; } static bool cmd_word_fwd(EditorState *e, const CommandArgs *a) { handle_select_chars_flag(e->view, a); bool skip_non_word = has_flag(a, 's'); word_fwd(&e->view->cursor, skip_non_word); view_reset_preferred_x(e->view); return true; } static bool cmd_wprev(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); e->window = prev_window(e->window); set_view(e->window->view); mark_everything_changed(e); debug_frame(e->root_frame); return true; } static bool cmd_wrap_paragraph(EditorState *e, const CommandArgs *a) { const char *arg = a->args[0]; unsigned int width = e->buffer->options.text_width; if (arg) { if (!str_to_uint(arg, &width)) { return error_msg("invalid paragraph width: %s", arg); } unsigned int max = TEXT_WIDTH_MAX; if (width < 1 || width > max) { return error_msg("width must be between 1 and %u", max); } } format_paragraph(e->view, width); return true; } static bool cmd_wresize(EditorState *e, const CommandArgs *a) { Window *window = e->window; if (!window->frame->parent) { // Only window return false; } ResizeDirection dir = RESIZE_DIRECTION_AUTO; switch (last_flag(a)) { case 'h': dir = RESIZE_DIRECTION_HORIZONTAL; break; case 'v': dir = RESIZE_DIRECTION_VERTICAL; break; } const char *arg = a->args[0]; if (arg) { int n; if (!str_to_int(arg, &n)) { return error_msg("Invalid resize value: %s", arg); } if (arg[0] == '+' || arg[0] == '-') { add_to_frame_size(window->frame, dir, n); } else { resize_frame(window->frame, dir, n); } } else { equalize_frame_sizes(window->frame->parent); } mark_everything_changed(e); debug_frame(e->root_frame); // TODO: return false if resize failed? return true; } static bool cmd_wsplit(EditorState *e, const CommandArgs *a) { bool before = has_flag(a, 'b'); bool use_glob = has_flag(a, 'g') && a->nr_args > 0; bool vertical = has_flag(a, 'h'); bool root = has_flag(a, 'r'); bool temporary = has_flag(a, 't'); bool empty = temporary || has_flag(a, 'n'); if (unlikely(empty && a->nr_args > 0)) { return error_msg("flags -n and -t can't be used with filename arguments"); } char **paths = a->args; glob_t globbuf; if (use_glob) { if (!xglob(a->args, &globbuf)) { return false; } paths = globbuf.gl_pathv; } Frame *frame; if (root) { frame = split_root_frame(e, vertical, before); } else { frame = split_frame(e->window, vertical, before); } View *save = e->view; e->window = frame->window; e->view = NULL; e->buffer = NULL; mark_everything_changed(e); View *view; if (empty) { view = window_open_new_file(e->window); view->buffer->temporary = temporary; } else if (paths[0]) { view = window_open_files(e->window, paths, NULL); } else { view = window_add_buffer(e->window, save->buffer); view->cursor = save->cursor; set_view(view); } if (use_glob) { globfree(&globbuf); } if (!view) { // Open failed, remove new window remove_frame(e, e->window->frame); e->view = save; e->buffer = save->buffer; e->window = save->window; } debug_frame(e->root_frame); return !!view; } static bool cmd_wswap(EditorState *e, const CommandArgs *a) { BUG_ON(a->nr_args); Frame *frame = e->window->frame; Frame *parent = frame->parent; if (!parent) { return false; } size_t count = parent->frames.count; size_t current = ptr_array_idx(&parent->frames, frame); BUG_ON(current >= count); size_t next = size_increment_wrapped(current, count); void **ptrs = parent->frames.ptrs; Frame *tmp = ptrs[current]; ptrs[current] = ptrs[next]; ptrs[next] = tmp; mark_everything_changed(e); return true; } IGNORE_WARNING("-Wincompatible-pointer-types") static const Command cmds[] = { {"alias", "-", true, 1, 2, cmd_alias}, {"bind", "-cns", true, 1, 2, cmd_bind}, {"blkdown", "cl", false, 0, 0, cmd_blkdown}, {"blkup", "cl", false, 0, 0, cmd_blkup}, {"bof", "cl", false, 0, 0, cmd_bof}, {"bol", "cst", false, 0, 0, cmd_bol}, {"bolsf", "cl", false, 0, 0, cmd_bolsf}, {"bookmark", "r", false, 0, 0, cmd_bookmark}, {"case", "lu", false, 0, 0, cmd_case}, {"cd", "", true, 1, 1, cmd_cd}, {"center-view", "", false, 0, 0, cmd_center_view}, {"clear", "i", false, 0, 0, cmd_clear}, {"close", "fpqw", false, 0, 0, cmd_close}, {"command", "-", false, 0, 1, cmd_command}, {"compile", "-1ps", false, 2, -1, cmd_compile}, {"copy", "bikp", false, 0, 0, cmd_copy}, {"cursor", "", true, 0, 3, cmd_cursor}, {"cut", "", false, 0, 0, cmd_cut}, {"delete", "", false, 0, 0, cmd_delete}, {"delete-eol", "n", false, 0, 0, cmd_delete_eol}, {"delete-line", "", false, 0, 0, cmd_delete_line}, {"delete-word", "s", false, 0, 0, cmd_delete_word}, {"down", "cl", false, 0, 0, cmd_down}, {"eof", "cl", false, 0, 0, cmd_eof}, {"eol", "c", false, 0, 0, cmd_eol}, {"eolsf", "cl", false, 0, 0, cmd_eolsf}, {"erase", "", false, 0, 0, cmd_erase}, {"erase-bol", "", false, 0, 0, cmd_erase_bol}, {"erase-word", "s", false, 0, 0, cmd_erase_word}, {"errorfmt", "i", true, 1, 2 + ERRORFMT_CAPTURE_MAX, cmd_errorfmt}, {"exec", "-e=i=o=lmnpst", false, 1, -1, cmd_exec}, {"ft", "-bcfi", true, 2, -1, cmd_ft}, {"hi", "-c", true, 0, -1, cmd_hi}, {"include", "bq", true, 1, 1, cmd_include}, {"insert", "km", false, 1, 1, cmd_insert}, {"join", "", false, 0, 0, cmd_join}, {"left", "c", false, 0, 0, cmd_left}, {"line", "", false, 1, 1, cmd_line}, {"load-syntax", "", true, 1, 1, cmd_load_syntax}, {"macro", "", false, 1, 1, cmd_macro}, {"match-bracket", "", false, 0, 0, cmd_match_bracket}, {"move-tab", "", false, 1, 1, cmd_move_tab}, {"msg", "np", false, 0, 1, cmd_msg}, {"new-line", "a", false, 0, 0, cmd_new_line}, {"next", "", false, 0, 0, cmd_next}, {"open", "e=gt", false, 0, -1, cmd_open}, {"option", "-r", true, 3, -1, cmd_option}, {"paste", "acm", false, 0, 0, cmd_paste}, {"pgdown", "cl", false, 0, 0, cmd_pgdown}, {"pgup", "cl", false, 0, 0, cmd_pgup}, {"prev", "", false, 0, 0, cmd_prev}, {"quit", "fp", false, 0, 1, cmd_quit}, {"redo", "", false, 0, 1, cmd_redo}, {"refresh", "", false, 0, 0, cmd_refresh}, {"repeat", "-", false, 2, -1, cmd_repeat}, {"replace", "bcgi", false, 2, 2, cmd_replace}, {"right", "c", false, 0, 0, cmd_right}, {"save", "Bbde=fpu", false, 0, 1, cmd_save}, {"scroll-down", "", false, 0, 0, cmd_scroll_down}, {"scroll-pgdown", "", false, 0, 0, cmd_scroll_pgdown}, {"scroll-pgup", "", false, 0, 0, cmd_scroll_pgup}, {"scroll-up", "", false, 0, 0, cmd_scroll_up}, {"search", "Hnprw", false, 0, 1, cmd_search}, {"select", "kl", false, 0, 0, cmd_select}, {"select-block", "", false, 0, 0, cmd_select_block}, {"set", "gl", true, 1, -1, cmd_set}, {"setenv", "", true, 1, 2, cmd_setenv}, {"shift", "", false, 1, 1, cmd_shift}, {"show", "c", false, 1, 2, cmd_show}, {"suspend", "", false, 0, 0, cmd_suspend}, {"tag", "r", false, 0, 1, cmd_tag}, {"title", "", false, 1, 1, cmd_title}, {"toggle", "gv", false, 1, -1, cmd_toggle}, {"undo", "m", false, 0, 0, cmd_undo}, {"unselect", "", false, 0, 0, cmd_unselect}, {"up", "cl", false, 0, 0, cmd_up}, {"view", "", false, 1, 1, cmd_view}, {"wclose", "fp", false, 0, 0, cmd_wclose}, {"wflip", "", false, 0, 0, cmd_wflip}, {"wnext", "", false, 0, 0, cmd_wnext}, {"word-bwd", "cs", false, 0, 0, cmd_word_bwd}, {"word-fwd", "cs", false, 0, 0, cmd_word_fwd}, {"wprev", "", false, 0, 0, cmd_wprev}, {"wrap-paragraph", "", false, 0, 1, cmd_wrap_paragraph}, {"wresize", "hv", false, 0, 1, cmd_wresize}, {"wsplit", "bghnrt", false, 0, -1, cmd_wsplit}, {"wswap", "", false, 0, 0, cmd_wswap}, }; UNIGNORE_WARNINGS static bool allow_macro_recording(const Command *cmd, char **args) { CommandFunc fn = cmd->cmd; if (fn == (CommandFunc)cmd_macro || fn == (CommandFunc)cmd_command) { return false; } if (fn == (CommandFunc)cmd_search) { char **args_copy = copy_string_array(args, string_array_length(args)); CommandArgs a = cmdargs_new(args_copy); bool ret = true; if (do_parse_args(cmd, &a) == ARGERR_NONE) { if (a.nr_args == 0 && !(a.flag_set & get_flagset_npw())) { // If command is "search" with no pattern argument and without // flags -n, -p or -w, the command would put the editor into // search mode, which shouldn't be recorded. ret = false; } } free_string_array(args_copy); return ret; } if (fn == (CommandFunc)cmd_exec) { // TODO: don't record -o with open/tag/eval/msg } return true; } UNITTEST { const char *args[4] = {NULL}; char **argp = (char**)args; const Command *cmd = find_normal_command("left"); BUG_ON(!cmd); BUG_ON(!allow_macro_recording(cmd, argp)); cmd = find_normal_command("exec"); BUG_ON(!cmd); BUG_ON(!allow_macro_recording(cmd, argp)); cmd = find_normal_command("command"); BUG_ON(!cmd); BUG_ON(allow_macro_recording(cmd, argp)); cmd = find_normal_command("macro"); BUG_ON(!cmd); BUG_ON(allow_macro_recording(cmd, argp)); cmd = find_normal_command("search"); BUG_ON(!cmd); BUG_ON(allow_macro_recording(cmd, argp)); args[0] = "xyz"; BUG_ON(!allow_macro_recording(cmd, argp)); args[0] = "-n"; BUG_ON(!allow_macro_recording(cmd, argp)); args[0] = "-p"; BUG_ON(!allow_macro_recording(cmd, argp)); args[0] = "-w"; BUG_ON(!allow_macro_recording(cmd, argp)); args[0] = "-Hr"; BUG_ON(allow_macro_recording(cmd, argp)); args[1] = "str"; BUG_ON(!allow_macro_recording(cmd, argp)); } static void record_command(const Command *cmd, char **args, void *userdata) { if (!allow_macro_recording(cmd, args)) { return; } EditorState *e = userdata; macro_command_hook(&e->macro, cmd->name, args); } const Command *find_normal_command(const char *name) { return BSEARCH(name, cmds, command_cmp); } const CommandSet normal_commands = { .lookup = find_normal_command, .macro_record = record_command, .expand_variable = expand_normal_var, .expand_env_vars = true, }; const char *find_normal_alias(const char *name, void *userdata) { EditorState *e = userdata; return find_alias(&e->aliases, name); } bool handle_normal_command(EditorState *e, const char *cmd, bool allow_recording) { CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, allow_recording); return handle_command(&runner, cmd); } void exec_normal_config(EditorState *e, StringView config) { CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); exec_config(&runner, config); } int read_normal_config(EditorState *e, const char *filename, ConfigFlags flags) { CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); return read_config(&runner, filename, flags); } void collect_normal_commands(PointerArray *a, const char *prefix) { COLLECT_STRING_FIELDS(cmds, name, a, prefix); } UNITTEST { CHECK_BSEARCH_ARRAY(cmds, name, strcmp); for (size_t i = 0, n = ARRAYLEN(cmds); i < n; i++) { // Check that flags arrays is null-terminated within bounds const char *const flags = cmds[i].flags; BUG_ON(flags[ARRAYLEN(cmds[0].flags) - 1] != '\0'); // Count number of real flags (i.e. not including '-' or '=') size_t nr_real_flags = 0; for (size_t j = (flags[0] == '-' ? 1 : 0); flags[j]; j++) { unsigned char flag = flags[j]; if (ascii_isalnum(flag)) { nr_real_flags++; } else if (flag != '=') { BUG("invalid command flag: 0x%02hhX", flag); } } // Check that max. number of real flags fits in CommandArgs::flags // array (and also leaves 1 byte for null-terminator) CommandArgs a; BUG_ON(nr_real_flags >= ARRAYLEN(a.flags)); } }