diff options
Diffstat (limited to 'examples/dte/commands.c')
| -rw-r--r-- | examples/dte/commands.c | 2594 |
1 files changed, 2594 insertions, 0 deletions
diff --git a/examples/dte/commands.c b/examples/dte/commands.c new file mode 100644 index 0000000..8346309 --- /dev/null +++ b/examples/dte/commands.c @@ -0,0 +1,2594 @@ +#include <errno.h> +#include <fcntl.h> +#include <glob.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> +#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)); + } +} |
