diff options
Diffstat (limited to 'examples/dte/completion.c')
| -rw-r--r-- | examples/dte/completion.c | 879 |
1 files changed, 879 insertions, 0 deletions
diff --git a/examples/dte/completion.c b/examples/dte/completion.c new file mode 100644 index 0000000..89e8c8c --- /dev/null +++ b/examples/dte/completion.c @@ -0,0 +1,879 @@ +#include <fcntl.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> +#include "completion.h" +#include "bind.h" +#include "command/alias.h" +#include "command/args.h" +#include "command/parse.h" +#include "command/run.h" +#include "command/serialize.h" +#include "commands.h" +#include "compiler.h" +#include "config.h" +#include "filetype.h" +#include "options.h" +#include "show.h" +#include "syntax/color.h" +#include "tag.h" +#include "terminal/cursor.h" +#include "terminal/key.h" +#include "terminal/style.h" +#include "util/arith.h" +#include "util/array.h" +#include "util/ascii.h" +#include "util/bsearch.h" +#include "util/debug.h" +#include "util/intmap.h" +#include "util/log.h" +#include "util/numtostr.h" +#include "util/path.h" +#include "util/str-util.h" +#include "util/string-view.h" +#include "util/string.h" +#include "util/xdirent.h" +#include "util/xmalloc.h" +#include "vars.h" + +extern char **environ; + +typedef enum { + COLLECT_ALL, // (directories and files) + COLLECT_EXECUTABLES, // (directories and executable files) + COLLECT_DIRS_ONLY, +} FileCollectionType; + +static bool is_executable(int dir_fd, const char *filename) +{ + return faccessat(dir_fd, filename, X_OK, 0) == 0; +} + +static bool do_collect_files ( + PointerArray *array, + const char *dirname, + const char *dirprefix, + const char *fileprefix, + FileCollectionType type +) { + DIR *const dir = xopendir(dirname); + if (!dir) { + return false; + } + + const int dir_fd = dirfd(dir); + if (unlikely(dir_fd < 0)) { + LOG_ERRNO("dirfd"); + xclosedir(dir); + return false; + } + + size_t dlen = strlen(dirprefix); + size_t flen = strlen(fileprefix); + const struct dirent *de; + + while ((de = xreaddir(dir))) { + const char *name = de->d_name; + if (streq(name, ".") || streq(name, "..") || unlikely(streq(name, ""))) { + continue; + } + + // TODO: add a global option to allow dotfiles to be included + // even when there's no prefix + if (flen ? strncmp(name, fileprefix, flen) : name[0] == '.') { + continue; + } + + struct stat st; + if (fstatat(dir_fd, name, &st, AT_SYMLINK_NOFOLLOW)) { + continue; + } + + bool is_dir = S_ISDIR(st.st_mode); + if (S_ISLNK(st.st_mode)) { + if (!fstatat(dir_fd, name, &st, 0)) { + is_dir = S_ISDIR(st.st_mode); + } + } + + if (!is_dir) { + switch (type) { + case COLLECT_DIRS_ONLY: + continue; + case COLLECT_ALL: + break; + case COLLECT_EXECUTABLES: + if (!is_executable(dir_fd, name)) { + continue; + } + if (!dlen) { + dirprefix = "./"; + dlen = 2; + } + break; + default: + BUG("unhandled FileCollectionType value"); + } + } + + ptr_array_append(array, path_joinx(dirprefix, name, is_dir)); + } + + xclosedir(dir); + return true; +} + +static void collect_files(EditorState *e, CompletionState *cs, FileCollectionType type) +{ + StringView esc = cs->escaped; + if (strview_has_prefix(&esc, "~/")) { + CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); + char *str = parse_command_arg(&runner, esc.data, esc.length, false); + const char *slash = strrchr(str, '/'); + BUG_ON(!slash); + cs->tilde_expanded = true; + char *dir = path_dirname(cs->parsed); + char *dirprefix = path_dirname(str); + do_collect_files(&cs->completions, dir, dirprefix, slash + 1, type); + free(dirprefix); + free(dir); + free(str); + } else { + const char *slash = strrchr(cs->parsed, '/'); + if (!slash) { + do_collect_files(&cs->completions, ".", "", cs->parsed, type); + } else { + char *dir = path_dirname(cs->parsed); + do_collect_files(&cs->completions, dir, dir, slash + 1, type); + free(dir); + } + } + + if (cs->completions.count == 1) { + // Add space if completed string is not a directory + const char *s = cs->completions.ptrs[0]; + size_t len = strlen(s); + if (len > 0) { + cs->add_space_after_single_match = s[len - 1] != '/'; + } + } +} + +void collect_normal_aliases(EditorState *e, PointerArray *a, const char *prefix) +{ + collect_hashmap_keys(&e->aliases, a, prefix); +} + +static void collect_bound_keys(const IntMap *bindings, PointerArray *a, const char *prefix) +{ + char keystr[KEYCODE_STR_MAX]; + for (IntMapIter it = intmap_iter(bindings); intmap_next(&it); ) { + size_t keylen = keycode_to_string(it.entry->key, keystr); + if (str_has_prefix(keystr, prefix)) { + ptr_array_append(a, xmemdup(keystr, keylen + 1)); + } + } +} + +void collect_bound_normal_keys(EditorState *e, PointerArray *a, const char *prefix) +{ + collect_bound_keys(&e->modes[INPUT_NORMAL].key_bindings, a, prefix); +} + +void collect_hl_colors(EditorState *e, PointerArray *a, const char *prefix) +{ + collect_builtin_colors(a, prefix); + collect_hashmap_keys(&e->colors.other, a, prefix); +} + +void collect_compilers(EditorState *e, PointerArray *a, const char *prefix) +{ + collect_hashmap_keys(&e->compilers, a, prefix); +} + +void collect_env(EditorState* UNUSED_ARG(e), PointerArray *a, const char *prefix) +{ + if (strchr(prefix, '=')) { + return; + } + + for (size_t i = 0; environ[i]; i++) { + const char *var = environ[i]; + if (str_has_prefix(var, prefix)) { + const char *delim = strchr(var, '='); + if (likely(delim && delim != var)) { + ptr_array_append(a, xstrcut(var, delim - var)); + } + } + } +} + +static void complete_alias(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_normal_aliases(e, &cs->completions, cs->parsed); + } else if (a->nr_args == 1 && cs->parsed[0] == '\0') { + const char *cmd = find_alias(&e->aliases, a->args[0]); + if (cmd) { + ptr_array_append(&cs->completions, xstrdup(cmd)); + } + } +} + +static void complete_bind(EditorState *e, const CommandArgs *a) +{ + static const char flags[] = { + [INPUT_NORMAL] = 'n', + [INPUT_COMMAND] = 'c', + [INPUT_SEARCH] = 's', + }; + + static_assert(ARRAYLEN(flags) == ARRAYLEN(e->modes)); + InputMode mode = INPUT_NORMAL; + for (size_t i = 0, count = 0; i < ARRAYLEN(flags); i++) { + if (cmdargs_has_flag(a, flags[i])) { + if (++count >= 2) { + return; // Don't complete bindings for multiple modes + } + mode = i; + } + } + + const IntMap *key_bindings = &e->modes[mode].key_bindings; + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_bound_keys(key_bindings, &cs->completions, cs->parsed); + return; + } + + if (a->nr_args != 1 || cs->parsed[0] != '\0') { + return; + } + + KeyCode key; + if (!parse_key_string(&key, a->args[0])) { + return; + } + const CachedCommand *cmd = lookup_binding(key_bindings, key); + if (!cmd) { + return; + } + + ptr_array_append(&cs->completions, xstrdup(cmd->cmd_str)); +} + +static void complete_cd(EditorState *e, const CommandArgs* UNUSED_ARG(a)) +{ + CompletionState *cs = &e->cmdline.completion; + collect_files(e, cs, COLLECT_DIRS_ONLY); + if (str_has_prefix("-", cs->parsed)) { + if (likely(xgetenv("OLDPWD"))) { + ptr_array_append(&cs->completions, xstrdup("-")); + } + } +} + +static void complete_exec(EditorState *e, const CommandArgs *a) +{ + // TODO: add completion for [-ioe] option arguments + CompletionState *cs = &e->cmdline.completion; + collect_files(e, cs, a->nr_args == 0 ? COLLECT_EXECUTABLES : COLLECT_ALL); +} + +static void complete_compile(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + size_t n = a->nr_args; + if (n == 0) { + collect_compilers(e, &cs->completions, cs->parsed); + } else { + collect_files(e, cs, n == 1 ? COLLECT_EXECUTABLES : COLLECT_ALL); + } +} + +static void complete_cursor(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + size_t n = a->nr_args; + if (n == 0) { + collect_cursor_modes(&cs->completions, cs->parsed); + } else if (n == 1) { + collect_cursor_types(&cs->completions, cs->parsed); + } else if (n == 2) { + collect_cursor_colors(&cs->completions, cs->parsed); + // Add an example #rrggbb color, to make things more discoverable + static const char rgb_example[] = "#22AABB"; + if (str_has_prefix(rgb_example, cs->parsed)) { + ptr_array_append(&cs->completions, xstrdup(rgb_example)); + } + } +} + +static void complete_errorfmt(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_compilers(e, &cs->completions, cs->parsed); + } else if (a->nr_args >= 2 && !cmdargs_has_flag(a, 'i')) { + collect_errorfmt_capture_names(&cs->completions, cs->parsed); + } +} + +static void complete_ft(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_ft(&e->filetypes, &cs->completions, cs->parsed); + } +} + +static void complete_hi(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_hl_colors(e, &cs->completions, cs->parsed); + } else { + collect_colors_and_attributes(&cs->completions, cs->parsed); + } +} + +static void complete_include(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + if (cmdargs_has_flag(a, 'b')) { + collect_builtin_includes(&cs->completions, cs->parsed); + } else { + collect_files(e, cs, COLLECT_ALL); + } + } +} + +static void complete_macro(EditorState *e, const CommandArgs *a) +{ + static const char verbs[][8] = { + "cancel", + "play", + "record", + "stop", + "toggle", + }; + + if (a->nr_args != 0) { + return; + } + + CompletionState *cs = &e->cmdline.completion; + COLLECT_STRINGS(verbs, &cs->completions, cs->parsed); +} + +static void complete_move_tab(EditorState *e, const CommandArgs *a) +{ + if (a->nr_args != 0) { + return; + } + + static const char words[][8] = {"left", "right"}; + CompletionState *cs = &e->cmdline.completion; + COLLECT_STRINGS(words, &cs->completions, cs->parsed); +} + +static void complete_open(EditorState *e, const CommandArgs *a) +{ + if (!cmdargs_has_flag(a, 't')) { + collect_files(e, &e->cmdline.completion, COLLECT_ALL); + } +} + +static void complete_option(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + if (!cmdargs_has_flag(a, 'r')) { + collect_ft(&e->filetypes, &cs->completions, cs->parsed); + } + } else if (a->nr_args & 1) { + collect_auto_options(&cs->completions, cs->parsed); + } else { + collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed); + } +} + +static void complete_save(EditorState *e, const CommandArgs* UNUSED_ARG(a)) +{ + collect_files(e, &e->cmdline.completion, COLLECT_ALL); +} + +static void complete_quit(EditorState *e, const CommandArgs* UNUSED_ARG(a)) +{ + CompletionState *cs = &e->cmdline.completion; + if (str_has_prefix("0", cs->parsed)) { + ptr_array_append(&cs->completions, xstrdup("0")); + } + if (str_has_prefix("1", cs->parsed)) { + ptr_array_append(&cs->completions, xstrdup("1")); + } +} + +static void complete_redo(EditorState *e, const CommandArgs* UNUSED_ARG(a)) +{ + const Change *change = e->buffer->cur_change; + CompletionState *cs = &e->cmdline.completion; + for (unsigned long i = 1, n = change->nr_prev; i <= n; i++) { + ptr_array_append(&cs->completions, xstrdup(ulong_to_str(i))); + } +} + +static void complete_set(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if ((a->nr_args + 1) & 1) { + bool local = cmdargs_has_flag(a, 'l'); + bool global = cmdargs_has_flag(a, 'g'); + collect_options(&cs->completions, cs->parsed, local, global); + } else { + collect_option_values(e, &cs->completions, a->args[a->nr_args - 1], cs->parsed); + } +} + +static void complete_setenv(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_env(e, &cs->completions, cs->parsed); + } else if (a->nr_args == 1 && cs->parsed[0] == '\0') { + BUG_ON(!a->args[0]); + const char *value = getenv(a->args[0]); + if (value) { + ptr_array_append(&cs->completions, xstrdup(value)); + } + } +} + +static void complete_show(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + collect_show_subcommands(&cs->completions, cs->parsed); + } else if (a->nr_args == 1) { + BUG_ON(!a->args[0]); + collect_show_subcommand_args(e, &cs->completions, a->args[0], cs->parsed); + } +} + +static void complete_tag(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0 && !cmdargs_has_flag(a, 'r')) { + BUG_ON(!cs->parsed); + StringView prefix = strview_from_cstring(cs->parsed); + collect_tags(&e->tagfile, &cs->completions, &prefix); + } +} + +static void complete_toggle(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (a->nr_args == 0) { + bool global = cmdargs_has_flag(a, 'g'); + collect_toggleable_options(&cs->completions, cs->parsed, global); + } +} + +static void complete_wsplit(EditorState *e, const CommandArgs *a) +{ + CompletionState *cs = &e->cmdline.completion; + if (!cmdargs_has_flag(a, 't') && !cmdargs_has_flag(a, 'n')) { + collect_files(e, cs, COLLECT_ALL); + } +} + +typedef struct { + char cmd_name[12]; + void (*complete)(EditorState *e, const CommandArgs *a); +} CompletionHandler; + +static const CompletionHandler completion_handlers[] = { + {"alias", complete_alias}, + {"bind", complete_bind}, + {"cd", complete_cd}, + {"compile", complete_compile}, + {"cursor", complete_cursor}, + {"errorfmt", complete_errorfmt}, + {"exec", complete_exec}, + {"ft", complete_ft}, + {"hi", complete_hi}, + {"include", complete_include}, + {"macro", complete_macro}, + {"move-tab", complete_move_tab}, + {"open", complete_open}, + {"option", complete_option}, + {"quit", complete_quit}, + {"redo", complete_redo}, + {"save", complete_save}, + {"set", complete_set}, + {"setenv", complete_setenv}, + {"show", complete_show}, + {"tag", complete_tag}, + {"toggle", complete_toggle}, + {"wsplit", complete_wsplit}, +}; + +UNITTEST { + CHECK_BSEARCH_ARRAY(completion_handlers, cmd_name, strcmp); + // Ensure handlers are kept in sync with renamed/removed commands + for (size_t i = 0; i < ARRAYLEN(completion_handlers); i++) { + const char *name = completion_handlers[i].cmd_name; + if (!find_normal_command(name)) { + BUG("completion handler for non-existent command: \"%s\"", name); + } + } +} + +static bool can_collect_flags ( + char **args, + size_t argc, + size_t nr_flag_args, + bool allow_flags_after_nonflags +) { + if (allow_flags_after_nonflags) { + for (size_t i = 0; i < argc; i++) { + if (streq(args[i], "--")) { + return false; + } + } + return true; + } + + for (size_t i = 0, nonflag = 0; i < argc; i++) { + if (args[i][0] != '-') { + if (++nonflag > nr_flag_args) { + return false; + } + continue; + } + if (streq(args[i], "--")) { + return false; + } + } + + return true; +} + +static bool collect_command_flags ( + PointerArray *array, + char **args, + size_t argc, + const Command *cmd, + const CommandArgs *a, + const char *prefix +) { + BUG_ON(prefix[0] != '-'); + const char *flags = cmd->flags; + bool flags_after_nonflags = (flags[0] != '-'); + + if (!can_collect_flags(args, argc, a->nr_flag_args, flags_after_nonflags)) { + return false; + } + + flags += flags_after_nonflags ? 0 : 1; + if (ascii_isalnum(prefix[1]) && prefix[2] == '\0') { + if (strchr(flags, prefix[1])) { + ptr_array_append(array, xmemdup(prefix, 3)); + } + return true; + } + + if (prefix[1] != '\0') { + return true; + } + + char buf[3] = "-"; + for (size_t i = 0; flags[i]; i++) { + if (!ascii_isalnum(flags[i]) || cmdargs_has_flag(a, flags[i])) { + continue; + } + buf[1] = flags[i]; + ptr_array_append(array, xmemdup(buf, 3)); + } + + return true; +} + +static void collect_completions(EditorState *e, char **args, size_t argc) +{ + CompletionState *cs = &e->cmdline.completion; + PointerArray *arr = &cs->completions; + const char *prefix = cs->parsed; + if (!argc) { + collect_normal_commands(arr, prefix); + collect_normal_aliases(e, arr, prefix); + return; + } + + for (size_t i = 0; i < argc; i++) { + if (!args[i]) { + // Embedded NULLs indicate there are multiple commands. + // Just return early here and avoid handling this case. + return; + } + } + + const Command *cmd = find_normal_command(args[0]); + if (!cmd) { + return; + } + + char **args_copy = copy_string_array(args + 1, argc - 1); + CommandArgs a = cmdargs_new(args_copy); + ArgParseError err = do_parse_args(cmd, &a); + bool dash = (prefix[0] == '-'); + if ( + (err != ARGERR_NONE && err != ARGERR_TOO_FEW_ARGUMENTS) + || (a.nr_args >= cmd->max_args && cmd->max_args != 0xFF && !dash) + ) { + goto out; + } + + if (dash && collect_command_flags(arr, args + 1, argc - 1, cmd, &a, prefix)) { + goto out; + } + + if (cmd->max_args == 0) { + goto out; + } + + const CompletionHandler *h = BSEARCH(args[0], completion_handlers, vstrcmp); + if (h) { + h->complete(e, &a); + } else if (streq(args[0], "repeat")) { + if (a.nr_args == 1) { + collect_normal_commands(arr, prefix); + } else if (a.nr_args >= 2) { + collect_completions(e, args + 2, argc - 2); + } + } + +out: + free_string_array(args_copy); +} + +static bool is_var(const char *str, size_t len) +{ + if (len == 0 || str[0] != '$') { + return false; + } + if (len == 1) { + return true; + } + if (!is_alpha_or_underscore(str[1])) { + return false; + } + for (size_t i = 2; i < len; i++) { + if (!is_alnum_or_underscore(str[i])) { + return false; + } + } + return true; +} + +UNITTEST { + BUG_ON(!is_var(STRN("$VAR"))); + BUG_ON(!is_var(STRN("$xy_190"))); + BUG_ON(!is_var(STRN("$__x_y_z"))); + BUG_ON(!is_var(STRN("$x"))); + BUG_ON(!is_var(STRN("$A"))); + BUG_ON(!is_var(STRN("$_0"))); + BUG_ON(!is_var(STRN("$"))); + BUG_ON(is_var(STRN(""))); + BUG_ON(is_var(STRN("A"))); + BUG_ON(is_var(STRN("$.a"))); + BUG_ON(is_var(STRN("$xyz!"))); + BUG_ON(is_var(STRN("$1"))); + BUG_ON(is_var(STRN("$09"))); + BUG_ON(is_var(STRN("$1a"))); +} + +static int strptrcmp(const void *v1, const void *v2) +{ + const char *const *s1 = v1; + const char *const *s2 = v2; + return strcmp(*s1, *s2); +} + +static void init_completion(EditorState *e, const CommandLine *cmdline) +{ + CompletionState *cs = &e->cmdline.completion; + const CommandRunner runner = cmdrunner_for_mode(e, INPUT_NORMAL, false); + BUG_ON(cs->orig); + BUG_ON(runner.userdata != e); + BUG_ON(!runner.lookup_alias); + + const size_t cmdline_pos = cmdline->pos; + char *const cmd = string_clone_cstring(&cmdline->buf); + PointerArray array = PTR_ARRAY_INIT; + ssize_t semicolon = -1; + ssize_t completion_pos = -1; + + for (size_t pos = 0; true; ) { + while (ascii_isspace(cmd[pos])) { + pos++; + } + + if (pos >= cmdline_pos) { + completion_pos = cmdline_pos; + break; + } + + if (!cmd[pos]) { + break; + } + + if (cmd[pos] == ';') { + semicolon = array.count; + ptr_array_append(&array, NULL); + pos++; + continue; + } + + CommandParseError err; + size_t end = find_end(cmd, pos, &err); + if (err != CMDERR_NONE || end >= cmdline_pos) { + completion_pos = pos; + break; + } + + if (semicolon + 1 == array.count) { + char *name = xstrslice(cmd, pos, end); + const char *value = runner.lookup_alias(name, runner.userdata); + if (value) { + size_t save = array.count; + if (parse_commands(&runner, &array, value) != CMDERR_NONE) { + for (size_t i = save, n = array.count; i < n; i++) { + free(array.ptrs[i]); + array.ptrs[i] = NULL; + } + array.count = save; + ptr_array_append(&array, parse_command_arg(&runner, name, end - pos, true)); + } else { + // Remove NULL + array.count--; + } + } else { + ptr_array_append(&array, parse_command_arg(&runner, name, end - pos, true)); + } + free(name); + } else { + ptr_array_append(&array, parse_command_arg(&runner, cmd + pos, end - pos, true)); + } + pos = end; + } + + const char *str = cmd + completion_pos; + size_t len = cmdline_pos - completion_pos; + if (is_var(str, len)) { + char *name = xstrslice(str, 1, len); + completion_pos++; + collect_env(e, &cs->completions, name); + collect_normal_vars(&cs->completions, name); + free(name); + } else { + cs->escaped = string_view(str, len); + cs->parsed = parse_command_arg(&runner, str, len, true); + cs->add_space_after_single_match = true; + size_t count = array.count; + char **args = count ? (char**)array.ptrs + 1 + semicolon : NULL; + size_t argc = count ? array.count - semicolon - 1 : 0; + collect_completions(e, args, argc); + } + + ptr_array_free(&array); + ptr_array_sort(&cs->completions, strptrcmp); + cs->orig = cmd; // (takes ownership) + cs->tail = strview_from_cstring(cmd + cmdline_pos); + cs->head_len = completion_pos; +} + +static void do_complete_command(CommandLine *cmdline) +{ + const CompletionState *cs = &cmdline->completion; + const PointerArray *arr = &cs->completions; + const StringView middle = strview_from_cstring(arr->ptrs[cs->idx]); + const StringView tail = cs->tail; + const size_t head_length = cs->head_len; + + String buf = string_new(head_length + tail.length + middle.length + 16); + string_append_buf(&buf, cs->orig, head_length); + string_append_escaped_arg_sv(&buf, middle, !cs->tilde_expanded); + + bool single_completion = (arr->count == 1); + if (single_completion && cs->add_space_after_single_match) { + string_append_byte(&buf, ' '); + } + + size_t pos = buf.len; + string_append_strview(&buf, &tail); + cmdline_set_text(cmdline, string_borrow_cstring(&buf)); + cmdline->pos = pos; + string_free(&buf); + + if (single_completion) { + reset_completion(cmdline); + } +} + +void complete_command_next(EditorState *e) +{ + CompletionState *cs = &e->cmdline.completion; + const bool init = !cs->orig; + if (init) { + init_completion(e, &e->cmdline); + } + size_t count = cs->completions.count; + if (!count) { + return; + } + if (!init) { + cs->idx = size_increment_wrapped(cs->idx, count); + } + do_complete_command(&e->cmdline); +} + +void complete_command_prev(EditorState *e) +{ + CompletionState *cs = &e->cmdline.completion; + const bool init = !cs->orig; + if (init) { + init_completion(e, &e->cmdline); + } + size_t count = cs->completions.count; + if (!count) { + return; + } + if (!init) { + cs->idx = size_decrement_wrapped(cs->idx, count); + } + do_complete_command(&e->cmdline); +} + +void reset_completion(CommandLine *cmdline) +{ + CompletionState *cs = &cmdline->completion; + free(cs->parsed); + free(cs->orig); + ptr_array_free(&cs->completions); + *cs = (CompletionState){.orig = NULL}; +} + +void collect_hashmap_keys(const HashMap *map, PointerArray *a, const char *prefix) +{ + for (HashMapIter it = hashmap_iter(map); hashmap_next(&it); ) { + const char *name = it.entry->key; + if (str_has_prefix(name, prefix)) { + ptr_array_append(a, xstrdup(name)); + } + } +} |
