From b4802af7755f26193a47a116f3bd5e0ae9ff7f10 Mon Sep 17 00:00:00 2001 From: Randy Palamar Date: Fri, 8 May 2026 11:43:29 -0600 Subject: [PATCH 1/2] core: unify lua and :set option handling The only thing that needs to be different about these paths is how the value that will be assigned to option is obtained. When the value comes from lua we can ask lua directly for the expected type but when it comes from :set we need to parse it ourselves. Besides that lookup of the option, and setting of option should not be using different codepaths. By combining them we no longer need worry about modifying multiple places in the code to add an option or update its handling. The addition of a vis_option_get() helper should also facilitate easy building of a full table of option values to pass to lua but that is not implemented here. Furthermore the C-API was updated to add a VisOptionGetFunction pointer to the option registration which would allow custom options to be included in the table but since addition to lua would require breaking the existing API it needs to be further discussed. --- sam.c | 147 +--------------- ui.h | 1 + util.c | 11 +- util.h | 10 ++ vis-cmds.c | 255 +++------------------------- vis-lua.c | 452 ++++++++++++++++++++++++-------------------------- vis-options.c | 437 ++++++++++++++++++++++++++++++++++++++++++++++++ vis.c | 1 + vis.h | 50 ++++-- 9 files changed, 736 insertions(+), 628 deletions(-) create mode 100644 vis-options.c diff --git a/sam.c b/sam.c index 37e56d83a..f61f2a660 100644 --- a/sam.c +++ b/sam.c @@ -258,147 +258,6 @@ static const CommandDef cmddef_select = { NULL, VIS_HELP(NULL) CMD_NONE, NULL, cmd_select }; -/* :set command options */ -typedef struct { - const char *names[3]; /* name and optional alias */ - enum VisOption flags; /* option type, etc. */ - VIS_HELP_DECL(const char *help;) /* short, one line help text */ - VisOptionFunction *func; /* option handler, NULL for builtins */ - void *context; /* context passed to option handler function */ -} OptionDef; - -enum { - OPTION_SHELL, - OPTION_ESCDELAY, - OPTION_AUTOINDENT, - OPTION_EXPANDTAB, - OPTION_TABWIDTH, - OPTION_SHOW_SPACES, - OPTION_SHOW_TABS, - OPTION_SHOW_NEWLINES, - OPTION_SHOW_EOF, - OPTION_STATUSBAR, - OPTION_NUMBER, - OPTION_NUMBER_RELATIVE, - OPTION_CURSOR_LINE, - OPTION_COLOR_COLUMN, - OPTION_SAVE_METHOD, - OPTION_LOAD_METHOD, - OPTION_CHANGE_256COLORS, - OPTION_LAYOUT, - OPTION_IGNORECASE, - OPTION_BREAKAT, - OPTION_WRAP_COLUMN, -}; - -static const OptionDef options[] = { - [OPTION_SHELL] = { - { "shell" }, - VIS_OPTION_TYPE_STRING, - VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)") - }, - [OPTION_ESCDELAY] = { - { "escdelay" }, - VIS_OPTION_TYPE_NUMBER, - VIS_HELP("Milliseconds to wait to distinguish from terminal escape sequences") - }, - [OPTION_AUTOINDENT] = { - { "autoindent", "ai" }, - VIS_OPTION_TYPE_BOOL, - VIS_HELP("Copy leading white space from previous line") - }, - [OPTION_EXPANDTAB] = { - { "expandtab", "et" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Replace entered with `tabwidth` spaces") - }, - [OPTION_TABWIDTH] = { - { "tabwidth", "tw" }, - VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab") - }, - [OPTION_SHOW_SPACES] = { - { "showspaces" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display replacement symbol instead of a space") - }, - [OPTION_SHOW_TABS] = { - { "showtabs" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display replacement symbol for tabs") - }, - [OPTION_SHOW_NEWLINES] = { - { "shownewlines" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display replacement symbol for newlines") - }, - [OPTION_SHOW_EOF] = { - { "showeof" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display replacement symbol for lines after the end of the file") - }, - [OPTION_STATUSBAR] = { - { "statusbar", "sb" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display status bar") - }, - [OPTION_NUMBER] = { - { "numbers", "nu" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display absolute line numbers") - }, - [OPTION_NUMBER_RELATIVE] = { - { "relativenumbers", "rnu" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Display relative line numbers") - }, - [OPTION_CURSOR_LINE] = { - { "cursorline", "cul" }, - VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Highlight current cursor line") - }, - [OPTION_COLOR_COLUMN] = { - { "colorcolumn", "cc" }, - VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Highlight a fixed column") - }, - [OPTION_SAVE_METHOD] = { - { "savemethod" }, - VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'") - }, - [OPTION_LOAD_METHOD] = { - { "loadmethod" }, - VIS_OPTION_TYPE_STRING, - VIS_HELP("How to load existing files 'auto', 'read' or 'mmap'") - }, - [OPTION_CHANGE_256COLORS] = { - { "change256colors" }, - VIS_OPTION_TYPE_BOOL, - VIS_HELP("Change 256 color palette to support 24bit colors") - }, - [OPTION_LAYOUT] = { - { "layout" }, - VIS_OPTION_TYPE_STRING, - VIS_HELP("Vertical or horizontal window layout") - }, - [OPTION_IGNORECASE] = { - { "ignorecase", "ic" }, - VIS_OPTION_TYPE_BOOL, - VIS_HELP("Ignore case when searching") - }, - [OPTION_BREAKAT] = { - { "breakat", "brk" }, - VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Characters which might cause a word wrap") - }, - [OPTION_WRAP_COLUMN] = { - { "wrapcolumn", "wc" }, - VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW, - VIS_HELP("Wrap lines at minimum of window width and wrapcolumn") - }, -}; - bool sam_init(Vis *vis) { if (!(vis->cmds = map_new())) return false; @@ -408,9 +267,9 @@ bool sam_init(Vis *vis) { if (!(vis->options = map_new())) return false; - for (int i = 0; i < LENGTH(options); i++) { - for (const char *const *name = options[i].names; *name; name++) - ret &= map_put(vis->options, *name, &options[i]); + for (int i = 0; i < countof(vis_options_table); i++) { + for (const char *const *name = vis_options_table[i].names; *name; name++) + ret &= map_put(vis->options, *name, vis_options_table + i); } return ret; diff --git a/ui.h b/ui.h index 1bb7c2066..6a4395d90 100644 --- a/ui.h +++ b/ui.h @@ -12,6 +12,7 @@ enum UiLayout { UI_LAYOUT_HORIZONTAL, UI_LAYOUT_VERTICAL, + UI_LAYOUT_COUNT, }; enum UiOption { diff --git a/util.c b/util.c index 99cdb97b7..2cdc5269d 100644 --- a/util.c +++ b/util.c @@ -86,7 +86,7 @@ memory_scan_reverse(const void *memory, uint8_t byte, ptrdiff_t n) } static str8 -str8_from_c_str(char *c_str) +str8_from_c_str(const char *c_str) { str8 result = {.data = (uint8_t *)c_str}; if (c_str) while (*c_str) c_str++; @@ -94,6 +94,15 @@ str8_from_c_str(char *c_str) return result; } +static bool +str8_equal(str8 a, str8 b) +{ + bool result = a.length == b.length; + for (ptrdiff_t i = 0; result && i < a.length; i++) + result = a.data[i] == b.data[i]; + return result; +} + static void str8_split_at(str8 s, str8 *left, str8 *right, ptrdiff_t n) { diff --git a/util.h b/util.h index d6906a32e..3ae1141b6 100644 --- a/util.h +++ b/util.h @@ -29,6 +29,7 @@ #undef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 +#include #include #include #include @@ -65,6 +66,8 @@ #include #endif +#define InvalidCodePath assert(0) + #if defined(__clang__) || defined(__GNUC__) #define likely(x) __builtin_expect(!!(x), 1) #define unlikely(x) __builtin_expect(!!(x), 0) @@ -73,10 +76,17 @@ #define unlikely(x) (x) #endif +#ifndef countof +#define countof(a) (sizeof(a) / sizeof(*a)) +#endif + #define LENGTH(x) ((int)(sizeof (x) / sizeof *(x))) #define MIN(a, b) ((a) > (b) ? (b) : (a)) #define MAX(a, b) ((a) < (b) ? (b) : (a)) +#define Between(x, a, b) ((x) >= (a) && (x) <= (b)) +#define Clamp(x, a, b) (((x) < (a)) ? (a) : ((x) > (b)) ? (b) : (x)) + /* is c the start of a utf8 sequence? */ #define ISUTF8(c) (((c)&0xC0)!=0x80) #define ISASCII(ch) ((unsigned char)ch < 0x80) diff --git a/vis-cmds.c b/vis-cmds.c index 6b3a6e0fc..4c0a64201 100644 --- a/vis-cmds.c +++ b/vis-cmds.c @@ -61,79 +61,11 @@ bool vis_cmd_unregister(Vis *vis, const char *name) { return true; } -static void option_free(OptionDef *opt) { - if (!opt) - return; - for (size_t i = 0; i < LENGTH(options); i++) { - if (opt == &options[i]) - return; - } - - for (const char **name = opt->names; *name; name++) - free((char*)*name); - free(VIS_HELP_USE((char*)opt->help)); - free(opt); -} - -bool vis_option_register(Vis *vis, const char *names[], enum VisOption flags, - VisOptionFunction *func, void *context, const char *help) { - - if (!names || !names[0]) - return false; - - for (const char **name = names; *name; name++) { - if (map_get(vis->options, *name)) - return false; - } - OptionDef *opt = calloc(1, sizeof *opt); - if (!opt) - return false; - for (size_t i = 0; i < LENGTH(opt->names)-1 && names[i]; i++) { - if (!(opt->names[i] = strdup(names[i]))) - goto err; - } - opt->flags = flags; - opt->func = func; - opt->context = context; -#if CONFIG_HELP - if (help && !(opt->help = strdup(help))) - goto err; -#endif - for (const char **name = names; *name; name++) - map_put(vis->options, *name, opt); - return true; -err: - option_free(opt); - return false; -} - -bool vis_option_unregister(Vis *vis, const char *name) { - OptionDef *opt = map_get(vis->options, name); - if (!opt) - return false; - for (const char **alias = opt->names; *alias; alias++) { - if (!map_delete(vis->options, *alias)) - return false; - } - option_free(opt); - return true; -} - static bool cmd_user(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) { CmdUser *user = map_get(vis->usercmds, argv[0]); return user && user->func(vis, win, user->data, cmd->flags == '!', argv, sel, range); } -void vis_shell_set(Vis *vis, const char *new_shell) { - char *shell = strdup(new_shell); - if (!shell) { - vis_info_show(vis, "Failed to change shell"); - } else { - free(vis->shell); - vis->shell = shell; - } -} - /* parse human-readable boolean value in s. If successful, store the result in * outval and return true. Else return false and leave outval alone. */ static bool parse_bool(const char *s, bool *outval) { @@ -154,29 +86,26 @@ static bool parse_bool(const char *s, bool *outval) { static bool cmd_set(Vis *vis, Win *win, Command *cmd, const char *argv[], Selection *sel, Filerange *range) { - if (!argv[1] || !argv[1][0] || argv[3]) { + str8 name = str8_from_c_str(argv[1]); + if (name.length == 0 || argv[3]) { vis_info_show(vis, "Expecting: set option [value]"); return false; } - char name[256]; - strncpy(name, argv[1], sizeof(name)-1); - char *lastchar = &name[strlen(name)-1]; - bool toggle = (*lastchar == '!'); - if (toggle) - *lastchar = '\0'; + bool toggle = name.data[name.length - 1] == '!'; + name.length -= (int)toggle; - OptionDef *opt = map_closest(vis->options, name); + VisOption *opt = vis_option_from_string(vis, name); if (!opt) { - vis_info_show(vis, "Unknown option: `%s'", name); + vis_info_show(vis, "Unknown option: `%.*s'", (int)name.length, name.data); return false; } - if (opt->flags & VIS_OPTION_DEPRECATED && strcmp(opt->context, name) == 0) - vis_info_show(vis, "%s is deprecated and will be removed in the next release", name); + if (opt->flags & VIS_OPTION_DEPRECATED && str8_equal(name, str8_from_c_str(opt->set_context))) + vis_info_show(vis, "%.*s is deprecated and will be removed in the next release", (int)name.length, name.data); if (!win && (opt->flags & VIS_OPTION_NEED_WINDOW)) { - vis_info_show(vis, "Need active window for `:set %s'", name); + vis_info_show(vis, "Need active window for `:set %.*s'", (int)name.length, name.data); return false; } @@ -191,20 +120,22 @@ static bool cmd_set(Vis *vis, Win *win, Command *cmd, const char *argv[], Select } } - Arg arg; + VisValue value = {0}; if (opt->flags & VIS_OPTION_TYPE_STRING) { - if (!(opt->flags & VIS_OPTION_VALUE_OPTIONAL) && !argv[2]) { + if (!argv[2]) { vis_info_show(vis, "Expecting string option value"); return false; } - arg.s = argv[2]; + value.kind = VisValueKind_String; + value.u.string = argv[2]; } else if (opt->flags & VIS_OPTION_TYPE_BOOL) { - if (!argv[2]) { - arg.b = !toggle; - } else if (!parse_bool(argv[2], &arg.b)) { + bool boolean = !toggle; + if (argv[2] && !parse_bool(argv[2], &boolean)) { vis_info_show(vis, "Expecting boolean option value not: `%s'", argv[2]); return false; } + value.kind = VisValueKind_Boolean; + value.u.boolean = boolean; } else if (opt->flags & VIS_OPTION_TYPE_NUMBER) { if (!argv[2]) { vis_info_show(vis, "Expecting number"); @@ -224,157 +155,13 @@ static bool cmd_set(Vis *vis, Win *win, Command *cmd, const char *argv[], Select return false; } - if (lval < 0) { - vis_info_show(vis, "Expecting positive number"); - return false; - } - arg.i = lval; + value.kind = VisValueKind_Integer; + value.u.integer = lval; } else { return false; } - size_t opt_index = 0; - for (; opt_index < LENGTH(options); opt_index++) { - if (opt == &options[opt_index]) - break; - } - - switch (opt_index) { - case OPTION_SHELL: - vis_shell_set(vis, arg.s); - break; - case OPTION_ESCDELAY: - { - termkey_set_waittime(vis->ui.termkey, arg.i); - break; - } - case OPTION_EXPANDTAB: - vis->win->expandtab = toggle ? !vis->win->expandtab : arg.b; - break; - case OPTION_AUTOINDENT: - vis->autoindent = toggle ? !vis->autoindent : arg.b; - break; - case OPTION_TABWIDTH: - view_tabwidth_set(&vis->win->view, arg.i); - break; - case OPTION_SHOW_SPACES: - case OPTION_SHOW_TABS: - case OPTION_SHOW_NEWLINES: - case OPTION_SHOW_EOF: - case OPTION_STATUSBAR: - { - const int values[] = { - [OPTION_SHOW_SPACES] = UI_OPTION_SYMBOL_SPACE, - [OPTION_SHOW_TABS] = UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL, - [OPTION_SHOW_NEWLINES] = UI_OPTION_SYMBOL_EOL, - [OPTION_SHOW_EOF] = UI_OPTION_SYMBOL_EOF, - [OPTION_STATUSBAR] = UI_OPTION_STATUSBAR, - }; - int flags = win->options; - if (arg.b || (toggle && !(flags & values[opt_index]))) - flags |= values[opt_index]; - else - flags &= ~values[opt_index]; - win_options_set(win, flags); - break; - } - case OPTION_NUMBER: { - enum UiOption opt = win->options; - if (arg.b || (toggle && !(opt & UI_OPTION_LINE_NUMBERS_ABSOLUTE))) { - opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE; - opt |= UI_OPTION_LINE_NUMBERS_ABSOLUTE; - } else { - opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE; - } - win_options_set(win, opt); - break; - } - case OPTION_NUMBER_RELATIVE: { - enum UiOption opt = win->options; - if (arg.b || (toggle && !(opt & UI_OPTION_LINE_NUMBERS_RELATIVE))) { - opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE; - opt |= UI_OPTION_LINE_NUMBERS_RELATIVE; - } else { - opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE; - } - win_options_set(win, opt); - break; - } - case OPTION_CURSOR_LINE: { - enum UiOption opt = win->options; - if (arg.b || (toggle && !(opt & UI_OPTION_CURSOR_LINE))) - opt |= UI_OPTION_CURSOR_LINE; - else - opt &= ~UI_OPTION_CURSOR_LINE; - win_options_set(win, opt); - break; - } - case OPTION_COLOR_COLUMN: - if (arg.i >= 0) - win->view.colorcolumn = arg.i; - break; - case OPTION_SAVE_METHOD: - if (strcmp("auto", arg.s) == 0) { - win->file->save_method = TEXT_SAVE_AUTO; - } else if (strcmp("atomic", arg.s) == 0) { - win->file->save_method = TEXT_SAVE_ATOMIC; - } else if (strcmp("inplace", arg.s) == 0) { - win->file->save_method = TEXT_SAVE_INPLACE; - } else { - vis_info_show(vis, "Invalid save method `%s', expected " - "'auto', 'atomic' or 'inplace'", arg.s); - return false; - } - break; - case OPTION_LOAD_METHOD: - if (strcmp("auto", arg.s) == 0) { - vis->load_method = TEXT_LOAD_AUTO; - } else if (strcmp("read", arg.s) == 0) { - vis->load_method = TEXT_LOAD_READ; - } else if (strcmp("mmap", arg.s) == 0) { - vis->load_method = TEXT_LOAD_MMAP; - } else { - vis_info_show(vis, "Invalid load method `%s', expected " - "'auto', 'read' or 'mmap'", arg.s); - return false; - } - break; - case OPTION_CHANGE_256COLORS: - vis->change_colors = toggle ? !vis->change_colors : arg.b; - break; - case OPTION_LAYOUT: { - enum UiLayout layout; - if (strcmp("h", arg.s) == 0) { - layout = UI_LAYOUT_HORIZONTAL; - } else if (strcmp("v", arg.s) == 0) { - layout = UI_LAYOUT_VERTICAL; - } else { - vis_info_show(vis, "Invalid layout `%s', expected 'h' or 'v'", arg.s); - return false; - } - ui_arrange(&vis->ui, layout); - break; - } - case OPTION_IGNORECASE: - vis->ignorecase = toggle ? !vis->ignorecase : arg.b; - break; - case OPTION_BREAKAT: - if (!view_breakat_set(&win->view, arg.s)) { - vis_info_show(vis, "Failed to set breakat"); - return false; - } - break; - case OPTION_WRAP_COLUMN: - if (arg.i >= 0) - win->view.wrapcolumn = arg.i; - break; - default: - if (!opt->func) - return false; - return opt->func(vis, win, opt->context, toggle, opt->flags, name, &arg); - } - - return true; + return vis_option_set(vis, win, opt, value, toggle); } static bool is_file_pattern(const char *pattern) { @@ -719,7 +506,7 @@ void vis_print_cmds(Vis *vis, Buffer *buf, const char *prefix) { static bool print_option(const char *key, void *value, void *data) { char desc[256]; - const OptionDef *opt = value; + const VisOption *opt = value; const char *help = VIS_HELP_USE(opt->help); if (strcmp(key, opt->names[0])) return true; diff --git a/vis-lua.c b/vis-lua.c index 6b94a0e9c..9edac0c33 100644 --- a/vis-lua.c +++ b/vis-lua.c @@ -107,6 +107,26 @@ static void stack_dump(lua_State *L, const char *format, ...) { #endif +VIS_INTERNAL str8 +vis_lua_to_str8(lua_State *L, int index) +{ + str8 result; + size_t length; + result.data = (uint8_t *)lua_tolstring(L, index, &length); + result.length = (ptrdiff_t)length; + return result; +} + +VIS_INTERNAL str8 +vis_lua_check_str8(lua_State *L, int index) +{ + str8 result; + size_t length; + result.data = (uint8_t *)luaL_checklstring(L, index, &length); + result.length = (ptrdiff_t)length; + return result; +} + static int panic_handler(lua_State *L) { void *ud = NULL; lua_getallocf(L, &ud); @@ -1021,21 +1041,92 @@ static int textobject_register(lua_State *L) { return 1; } -static bool option_lua(Vis *vis, Win *win, void *context, bool toggle, - enum VisOption flags, const char *name, Arg *value) { +VIS_INTERNAL bool +vis_lua_is_vis_option(VisOption *option) +{ + bool result = option && !(option->flags & VIS_OPTION_NEED_WINDOW) && (option != vis_options_table + OPTION_LAYOUT); + return result; +} + +VIS_INTERNAL bool +vis_lua_is_window_option(VisOption *option) +{ + bool result = option && (option->flags & VIS_OPTION_NEED_WINDOW); + return result; +} + +VIS_INTERNAL int +vis_lua_push_value(lua_State *L, VisValue value) +{ + int result = 1; + switch (value.kind) { + case VisValueKind_Integer:{ lua_pushinteger(L, value.u.integer); }break; + case VisValueKind_Boolean:{ lua_pushboolean(L, value.u.boolean); }break; + case VisValueKind_String:{ lua_pushstring(L, value.u.string); }break; + default:{result = 0;}break; + } + return result; +} + +VIS_INTERNAL int +vis_lua_push_option(Vis *vis, Win *win, lua_State *L, VisOption *option) +{ + int result = vis_lua_push_value(L, vis_option_get(vis, win, option)); + return result; +} + +VIS_INTERNAL VisValue +vis_lua_option_get(lua_State *L, VisOptionFlags flags, int next) +{ + VisValue result = {0}; + if (!lua_isnoneornil(L, next)) { + if (flags & VIS_OPTION_TYPE_NUMBER) { + result.kind = VisValueKind_Integer; + result.u.integer = luaL_checkinteger(L, next); + } else if (flags & VIS_OPTION_TYPE_STRING) { + result.kind = VisValueKind_String; + result.u.string = lua_tostring(L, next); + } else if (flags & VIS_OPTION_TYPE_BOOL) { + result.kind = VisValueKind_Boolean; + result.u.boolean = lua_toboolean(L, next); + } else { + InvalidCodePath; + } + } + return result; +} + +VIS_INTERNAL int +vis_lua_option_set(Vis *vis, Win *win, VisOption *option, lua_State *L, int next) +{ + assert(!(option->flags & VIS_OPTION_NEED_WINDOW) || win); + VisValue value = vis_lua_option_get(L, option->flags, next); + vis_option_set(vis, win, option, value, 0); + return 0; +} + +VIS_INTERNAL VIS_OPTION_SET_FUNCTION(vis_lua_option_set_handler) +{ lua_State *L = vis->lua; - if (!func_ref_get(L, context)) - return false; - if (flags & VIS_OPTION_TYPE_BOOL) - lua_pushboolean(L, value->b); - else if (flags & VIS_OPTION_TYPE_STRING) - lua_pushstring(L, value->s); - else if (flags & VIS_OPTION_TYPE_NUMBER) - lua_pushnumber(L, value->i); - else - return false; - lua_pushboolean(L, toggle); - return pcall(vis, L, 2, 2) == 0 && (!lua_isboolean(L, -1) || lua_toboolean(L, -1)); + bool result = false; + if (func_ref_get(L, context) && vis_lua_push_value(L, value)) { + lua_pushboolean(L, toggle); + lua_pushstring(L, name); + result = pcall(vis, L, 3, 2) == LUA_OK && (!lua_isboolean(L, -1) || lua_toboolean(L, -1)); + } + return result; +} + +VIS_INTERNAL VIS_OPTION_GET_FUNCTION(vis_lua_option_get_handler) +{ + VisValue result = {0}; + lua_State *L = vis->lua; + if (func_ref_get(L, context)) { + lua_pushstring(L, name); + if (pcall(vis, L, 1, 1) == LUA_OK) + result = vis_lua_option_get(L, flags, -1); + } + return result; } /*** @@ -1044,32 +1135,46 @@ static bool option_lua(Vis *vis, Win *win, void *context, bool toggle, * @function option_register * @tparam string name the option name * @tparam string type the option type (`bool`, `string` or `number`) - * @tparam function handler the Lua function being called when the option is changed + * @tparam function set Lua function called when the option is set + * @tparam[opt] function get Lua function which returns the current value of the option * @tparam[opt] string help the single line help text as displayed in `:help` * @treturn bool whether the option was successfully registered * @usage - * vis:option_register("foo", "bool", function(value, toggle) - * if not vis.win then return false end - * vis.win.foo = toggle and not vis.win.foo or value - * vis:info("Option foo = " .. tostring(vis.win.foo)) + * local set_foo = function(value, toggle, name) + * -- NOTE: name == "foo" here, you could define this as a generic handler + * vis.foo = toggle and not vis.foo or value + * vis:info("Option foo = " .. tostring(vis.foo)) * return true - * end, "Foo enables superpowers") + * end + * local get_foo = function(name) + * -- NOTE: name == "foo" here, you could define this as a generic handler + * return vis.foo + * end + * vis:option_register("foo", "bool", set_foo, get_foo, "Foo enables superpowers") */ -static int option_register(lua_State *L) { +VIS_INTERNAL int +vis_lua_option_register(lua_State *L) +{ Vis *vis = obj_ref_check(L, 1, "vis"); const char *name = luaL_checkstring(L, 2); - const char *type = luaL_checkstring(L, 3); - const void *func = func_ref_new(L, 4); - const char *help = luaL_optstring(L, 5, NULL); - const char *names[] = { name, NULL }; - enum VisOption flags = 0; - if (strcmp(type, "string") == 0) + str8 type = vis_lua_check_str8(L, 3); + const void *set = func_ref_new(L, 4); + const void *get = 0; + + int read_index = 5; + if (lua_isfunction(L, read_index)) + get = func_ref_new(L, read_index++); + const char *help = luaL_optstring(L, read_index, 0); + + VisOptionFlags flags = 0; + if (str8_equal(type, str8("string"))) flags |= VIS_OPTION_TYPE_STRING; - else if (strcmp(type, "number") == 0) + else if (str8_equal(type, str8("number"))) flags |= VIS_OPTION_TYPE_NUMBER; else flags |= VIS_OPTION_TYPE_BOOL; - bool ret = vis_option_register(vis, names, flags, option_lua, (void*)func, help); + bool ret = vis_option_register(vis, (const char *[]){name, 0}, flags, vis_lua_option_set_handler, + vis_lua_option_get_handler, (void *)set, (void *)get, help); lua_pushboolean(L, ret); return 1; } @@ -1081,7 +1186,9 @@ static int option_register(lua_State *L) { * @tparam string name the option name * @treturn bool whether the option was successfully unregistered */ -static int option_unregister(lua_State *L) { +VIS_INTERNAL int +vis_lua_option_unregister(lua_State *L) +{ Vis *vis = obj_ref_check(L, 1, "vis"); const char *name = luaL_checkstring(L, 2); bool ret = vis_option_unregister(vis, name); @@ -1481,33 +1588,6 @@ static int vis_index(lua_State *L) { return index_common(L); } -static int vis_options_assign(Vis *vis, lua_State *L, const char *key, int next) { - if (strcmp(key, "autoindent") == 0 || strcmp(key, "ai") == 0) { - vis->autoindent = lua_toboolean(L, next); - } else if (strcmp(key, "changecolors") == 0) { - vis->change_colors = lua_toboolean(L, next); - } else if (strcmp(key, "escdelay") == 0) { - termkey_set_waittime(vis->ui.termkey, luaL_checkinteger(L, next)); - } else if (strcmp(key, "ignorecase") == 0 || strcmp(key, "ic") == 0) { - vis->ignorecase = lua_toboolean(L, next); - } else if (strcmp(key, "loadmethod") == 0) { - if (!lua_isstring(L, next)) - return newindex_common(L); - const char *lm = lua_tostring(L, next); - if (strcmp(lm, "auto") == 0) - vis->load_method = TEXT_LOAD_AUTO; - else if (strcmp(lm, "read") == 0) - vis->load_method = TEXT_LOAD_READ; - else if (strcmp(lm, "mmap") == 0) - vis->load_method = TEXT_LOAD_MMAP; - } else if (strcmp(key, "shell") == 0) { - if (!lua_isstring(L, next)) - return newindex_common(L); - vis_shell_set(vis, lua_tostring(L, next)); - } - return 0; -} - static int vis_newindex(lua_State *L) { Vis *vis = obj_ref_check(L, 1, "vis"); if (lua_isstring(L, 2)) { @@ -1556,10 +1636,17 @@ static int vis_newindex(lua_State *L) { */ lua_pushnil(L); while (lua_next(L, 3)) { - if (lua_isstring(L, 4)) - ret += vis_options_assign(vis, L, lua_tostring(L, 4), 5); - else + if (lua_isstring(L, 4)) { + VisOption *option = vis_option_from_string(vis, vis_lua_to_str8(L, 4)); + if (vis_lua_is_vis_option(option)) { + ret += vis_lua_option_set(vis, 0, option, L, 5); + } else if (!option) { + // NOTE(rnp): string is not a valid option name, a new field can be added + ret += newindex_common(L); + } + } else { ret += newindex_common(L); + } lua_pop(L, 1); } lua_pop(L, 1); @@ -1586,8 +1673,8 @@ static const struct luaL_Reg vis_lua[] = { { "motion_register", motion_register }, { "textobject", textobject }, { "textobject_register", textobject_register }, - { "option_register", option_register }, - { "option_unregister", option_unregister }, + { "option_register", vis_lua_option_register }, + { "option_unregister", vis_lua_option_unregister }, { "command_register", command_register }, { "complete_command", complete_command }, { "feedkeys", feedkeys }, @@ -1615,58 +1702,40 @@ static const struct luaL_Reg vis_lua[] = { * @see Window.options */ -static int vis_options_index(lua_State *L) { +VIS_INTERNAL int +vis_lua_options_index(lua_State *L) +{ Vis *vis = obj_ref_check_containerof(L, 1, VIS_LUA_TYPE_VIS_OPTS, offsetof(Vis, options)); - if (!vis) - return -1; - if (lua_isstring(L, 2)) { - const char *key = lua_tostring(L, 2); - if (strcmp(key, "autoindent") == 0 || strcmp(key, "ai") == 0) { - lua_pushboolean(L, vis->autoindent); - return 1; - } else if (strcmp(key, "changecolors") == 0) { - lua_pushboolean(L, vis->change_colors); - return 1; - } else if (strcmp(key, "escdelay") == 0) { - lua_pushinteger(L, termkey_get_waittime(vis->ui.termkey)); - return 1; - } else if (strcmp(key, "ignorecase") == 0 || strcmp(key, "ic") == 0) { - lua_pushboolean(L, vis->ignorecase); - return 1; - } else if (strcmp(key, "loadmethod") == 0) { - switch (vis->load_method) { - case TEXT_LOAD_AUTO: - lua_pushliteral(L, "auto"); - break; - case TEXT_LOAD_READ: - lua_pushliteral(L, "read"); - break; - case TEXT_LOAD_MMAP: - lua_pushliteral(L, "mmap"); - break; - } - return 1; - } else if (strcmp(key, "shell") == 0) { - lua_pushstring(L, vis->shell); - return 1; + if (vis) { + if (lua_isstring(L, 2)) { + VisOption *option = vis_option_from_string(vis, vis_lua_to_str8(L, 2)); + if (vis_lua_is_vis_option(option)) + return vis_lua_push_option(vis, 0, L, option); } + return index_common(L); } - return index_common(L); + return -1; } -static int vis_options_newindex(lua_State *L) { +VIS_INTERNAL int +vis_lua_options_newindex(lua_State *L) +{ Vis *vis = obj_ref_check_containerof(L, 1, VIS_LUA_TYPE_VIS_OPTS, offsetof(Vis, options)); - if (!vis) - return 0; - if (lua_isstring(L, 2)) - return vis_options_assign(vis, L, lua_tostring(L, 2), 3); - return newindex_common(L); + if (vis) { + if (lua_isstring(L, 2)) { + VisOption *option = vis_option_from_string(vis, vis_lua_to_str8(L, 2)); + if (vis_lua_is_vis_option(option)) + return vis_lua_option_set(vis, 0, option, L, 3); + } + return newindex_common(L); + } + return 0; } static const struct luaL_Reg vis_option_funcs[] = { - { "__index", vis_options_index }, - { "__newindex", vis_options_newindex}, - { NULL, NULL }, + {"__index", vis_lua_options_index }, + {"__newindex", vis_lua_options_newindex}, + {0}, }; /*** @@ -1687,12 +1756,10 @@ static int ui_index(lua_State *L) { Ui *ui = obj_ref_check(L, 1, VIS_LUA_TYPE_UI); if (lua_isstring(L, 2)) { - const char *key = lua_tostring(L, 2); - - if (strcmp(key, "layout") == 0) { - lua_pushinteger(L, ui->layout); - return 1; - } + VisOption *option = vis_option_from_string(ui->vis, vis_lua_to_str8(L, 2)); + // TODO(rnp): better filtering + if (option == vis_options_table + OPTION_LAYOUT) + return vis_lua_push_option(ui->vis, 0, L, option); } return index_common(L); @@ -1886,78 +1953,13 @@ static int window_index(lua_State *L) { return index_common(L); } -static int window_options_assign(Win *win, lua_State *L, const char *key, int next) { - enum UiOption flags = win->options; - if (strcmp(key, "breakat") == 0 || strcmp(key, "brk") == 0) { - if (lua_isstring(L, next)) - view_breakat_set(&win->view, lua_tostring(L, next)); - } else if (strcmp(key, "colorcolumn") == 0 || strcmp(key, "cc") == 0) { - win->view.colorcolumn = luaL_checkinteger(L, next); - } else if (strcmp(key, "cursorline") == 0 || strcmp(key, "cul") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_CURSOR_LINE; - else - flags &= ~UI_OPTION_CURSOR_LINE; - win_options_set(win, flags); - } else if (strcmp(key, "numbers") == 0 || strcmp(key, "nu") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_LINE_NUMBERS_ABSOLUTE; - else - flags &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE; - win_options_set(win, flags); - } else if (strcmp(key, "relativenumbers") == 0 || strcmp(key, "rnu") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_LINE_NUMBERS_RELATIVE; - else - flags &= ~UI_OPTION_LINE_NUMBERS_RELATIVE; - win_options_set(win, flags); - } else if (strcmp(key, "showeof") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_SYMBOL_EOF; - else - flags &= ~UI_OPTION_SYMBOL_EOF; - win_options_set(win, flags); - } else if (strcmp(key, "shownewlines") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_SYMBOL_EOL; - else - flags &= ~UI_OPTION_SYMBOL_EOL; - win_options_set(win, flags); - } else if (strcmp(key, "showspaces") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_SYMBOL_SPACE; - else - flags &= ~UI_OPTION_SYMBOL_SPACE; - win_options_set(win, flags); - } else if (strcmp(key, "showtabs") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_SYMBOL_TAB; - else - flags &= ~UI_OPTION_SYMBOL_TAB; - win_options_set(win, flags); - } else if (strcmp(key, "statusbar") == 0) { - if (lua_toboolean(L, next)) - flags |= UI_OPTION_STATUSBAR; - else - flags &= ~UI_OPTION_STATUSBAR; - win_options_set(win, flags); - } else if (strcmp(key, "wrapcolumn") == 0 || strcmp(key, "wc") == 0) { - win->view.wrapcolumn = luaL_checkinteger(L, next); - } else if (strcmp(key, "tabwidth") == 0 || strcmp(key, "tw") == 0) { - view_tabwidth_set(&win->view, luaL_checkinteger(L, next)); - } else if (strcmp(key, "expandtab") == 0 || strcmp(key, "et") == 0) { - win->expandtab = lua_toboolean(L, next); - } - return 0; -} - static int window_newindex(lua_State *L) { Win *win = obj_ref_check(L, 1, VIS_LUA_TYPE_WINDOW); if (lua_isstring(L, 2)) { const char *key = lua_tostring(L, 2); if (strcmp(key, "options") == 0 && lua_istable(L, 3)) { - int ret = 0; + int result = 0; /* since we don't know which keys are in the table we push * a nil then use lua_next() to remove it and push the * table's key-value pairs to the stack. these can then be @@ -1965,14 +1967,21 @@ static int window_newindex(lua_State *L) { */ lua_pushnil(L); while (lua_next(L, 3)) { - if (lua_isstring(L, 4)) - ret += window_options_assign(win, L, lua_tostring(L, 4), 5); - else - ret += newindex_common(L); + if (lua_isstring(L, 4)) { + VisOption *option = vis_option_from_string(win->vis, vis_lua_to_str8(L, 4)); + if (vis_lua_is_window_option(option)) { + result += vis_lua_option_set(win->vis, win, option, L, 5); + } else if (!option) { + // NOTE(rnp): string is not a valid option name, a new field can be added + result += newindex_common(L); + } + } else { + result += newindex_common(L); + } lua_pop(L, 1); } lua_pop(L, 1); - return ret; + return result; } else if (strcmp(key, "file") == 0 && lua_isstring(L, 3)) { const char* filename = lua_tostring(L, 3); if (!vis_window_change_file(win, filename)) { @@ -2199,69 +2208,40 @@ static const struct luaL_Reg window_funcs[] = { * @see Vis.options */ -static int window_options_index(lua_State *L) { +VIS_INTERNAL int +vis_lua_window_options_index(lua_State *L) +{ Win *win = obj_ref_check_containerof(L, 1, VIS_LUA_TYPE_WIN_OPTS, offsetof(Win, view)); - if (!win) - return -1; - if (lua_isstring(L, 2)) { - const char *key = lua_tostring(L, 2); - if (strcmp(key, "breakat") == 0 || strcmp(key, "brk") == 0) { - lua_pushstring(L, win->view.breakat); - return 1; - } else if (strcmp(key, "colorcolumn") == 0 || strcmp(key, "cc") == 0) { - lua_pushinteger(L, win->view.colorcolumn); - return 1; - } else if (strcmp(key, "cursorline") == 0 || strcmp(key, "cul") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_CURSOR_LINE); - return 1; - } else if (strcmp(key, "expandtab") == 0 || strcmp(key, "et") == 0) { - lua_pushboolean(L, win->expandtab); - return 1; - } else if (strcmp(key, "numbers") == 0 || strcmp(key, "nu") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_LINE_NUMBERS_ABSOLUTE); - return 1; - } else if (strcmp(key, "relativenumbers") == 0 || strcmp(key, "rnu") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_LINE_NUMBERS_RELATIVE); - return 1; - } else if (strcmp(key, "showeof") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_SYMBOL_EOF); - return 1; - } else if (strcmp(key, "shownewlines") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_SYMBOL_EOL); - return 1; - } else if (strcmp(key, "showspaces") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_SYMBOL_SPACE); - return 1; - } else if (strcmp(key, "showtabs") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_SYMBOL_TAB); - return 1; - } else if (strcmp(key, "statusbar") == 0) { - lua_pushboolean(L, win->options & UI_OPTION_STATUSBAR); - return 1; - } else if (strcmp(key, "tabwidth") == 0 || strcmp(key, "tw") == 0) { - lua_pushinteger(L, win->view.tabwidth); - return 1; - } else if (strcmp(key, "wrapcolumn") == 0 || strcmp(key, "wc") == 0) { - lua_pushinteger(L, win->view.wrapcolumn); - return 1; + if (win) { + if (lua_isstring(L, 2)) { + VisOption *option = vis_option_from_string(win->vis, vis_lua_to_str8(L, 2)); + if (vis_lua_is_window_option(option)) + return vis_lua_push_option(win->vis, win, L, option); } + return index_common(L); } - return index_common(L); + return -1; } -static int window_options_newindex(lua_State *L) { +VIS_INTERNAL int +vis_lua_window_options_newindex(lua_State *L) +{ Win *win = obj_ref_check_containerof(L, 1, VIS_LUA_TYPE_WIN_OPTS, offsetof(Win, view)); - if (!win) - return 0; - if (lua_isstring(L, 2)) - return window_options_assign(win, L, lua_tostring(L, 2), 3); - return newindex_common(L); + if (win) { + if (lua_isstring(L, 2)) { + VisOption *option = vis_option_from_string(win->vis, vis_lua_to_str8(L, 2)); + if (vis_lua_is_window_option(option)) + return vis_lua_option_set(win->vis, win, option, L, 3); + } + return newindex_common(L); + } + return 0; } static const struct luaL_Reg window_option_funcs[] = { - { "__index", window_options_index }, - { "__newindex", window_options_newindex}, - { NULL, NULL }, + {"__index", vis_lua_window_options_index }, + {"__newindex", vis_lua_window_options_newindex}, + {0}, }; static int window_selections_index(lua_State *L) { diff --git a/vis-options.c b/vis-options.c new file mode 100644 index 000000000..b0794d613 --- /dev/null +++ b/vis-options.c @@ -0,0 +1,437 @@ +/* :set command options */ +typedef struct { + const char *names[3]; /* name and optional alias */ + VisOptionFlags flags; /* option type, etc. */ + VIS_HELP_DECL(const char *help;) /* short, one line help text */ + VisOptionSetFunction *set; /* option handler, NULL for builtins */ + VisOptionGetFunction *get; /* option handler, NULL for builtins */ + void *set_context; /* context passed to option handler function */ + void *get_context; /* context passed to option handler function */ +} VisOption; + +enum { + OPTION_SHELL, + OPTION_ESCDELAY, + OPTION_AUTOINDENT, + OPTION_EXPANDTAB, + OPTION_TABWIDTH, + OPTION_SHOW_SPACES, + OPTION_SHOW_TABS, + OPTION_SHOW_NEWLINES, + OPTION_SHOW_EOF, + OPTION_STATUSBAR, + OPTION_NUMBER, + OPTION_NUMBER_RELATIVE, + OPTION_CURSOR_LINE, + OPTION_COLOR_COLUMN, + OPTION_SAVE_METHOD, + OPTION_LOAD_METHOD, + OPTION_CHANGE_256COLORS, + OPTION_LAYOUT, + OPTION_IGNORECASE, + OPTION_BREAKAT, + OPTION_WRAP_COLUMN, +}; + +static const VisOption vis_options_table[] = { + [OPTION_SHELL] = { + { "shell" }, + VIS_OPTION_TYPE_STRING, + VIS_HELP("Shell to use for external commands (default: $SHELL, /etc/passwd, /bin/sh)") + }, + [OPTION_ESCDELAY] = { + { "escdelay" }, + VIS_OPTION_TYPE_NUMBER, + VIS_HELP("Milliseconds to wait to distinguish from terminal escape sequences") + }, + [OPTION_AUTOINDENT] = { + { "autoindent", "ai" }, + VIS_OPTION_TYPE_BOOL, + VIS_HELP("Copy leading white space from previous line") + }, + [OPTION_EXPANDTAB] = { + { "expandtab", "et" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Replace entered with `tabwidth` spaces") + }, + [OPTION_TABWIDTH] = { + { "tabwidth", "tw" }, + VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Number of spaces to display (and insert if `expandtab` is enabled) for a tab") + }, + [OPTION_SHOW_SPACES] = { + { "showspaces" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display replacement symbol instead of a space") + }, + [OPTION_SHOW_TABS] = { + { "showtabs" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display replacement symbol for tabs") + }, + [OPTION_SHOW_NEWLINES] = { + { "shownewlines" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display replacement symbol for newlines") + }, + [OPTION_SHOW_EOF] = { + { "showeof" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display replacement symbol for lines after the end of the file") + }, + [OPTION_STATUSBAR] = { + { "statusbar", "sb" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display status bar") + }, + [OPTION_NUMBER] = { + { "numbers", "nu" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display absolute line numbers") + }, + [OPTION_NUMBER_RELATIVE] = { + { "relativenumbers", "rnu" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Display relative line numbers") + }, + [OPTION_CURSOR_LINE] = { + { "cursorline", "cul" }, + VIS_OPTION_TYPE_BOOL|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Highlight current cursor line") + }, + [OPTION_COLOR_COLUMN] = { + { "colorcolumn", "cc" }, + VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Highlight a fixed column") + }, + [OPTION_SAVE_METHOD] = { + { "savemethod" }, + VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Save method to use for current file 'auto', 'atomic' or 'inplace'") + }, + [OPTION_LOAD_METHOD] = { + { "loadmethod" }, + VIS_OPTION_TYPE_STRING, + VIS_HELP("How to load existing files 'auto', 'read' or 'mmap'") + }, + [OPTION_CHANGE_256COLORS] = { + { "change256colors" }, + VIS_OPTION_TYPE_BOOL, + VIS_HELP("Change 256 color palette to support 24bit colors") + }, + [OPTION_LAYOUT] = { + { "layout" }, + // NOTE(rnp): layout technically wants an int but also has semantic names. + // lua can pass the int directly but :set will use the string name + VIS_OPTION_TYPE_STRING|VIS_OPTION_TYPE_NUMBER, + VIS_HELP("Vertical or horizontal window layout") + }, + [OPTION_IGNORECASE] = { + { "ignorecase", "ic" }, + VIS_OPTION_TYPE_BOOL, + VIS_HELP("Ignore case when searching") + }, + [OPTION_BREAKAT] = { + { "breakat", "brk" }, + VIS_OPTION_TYPE_STRING|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Characters which might cause a word wrap") + }, + [OPTION_WRAP_COLUMN] = { + { "wrapcolumn", "wc" }, + VIS_OPTION_TYPE_NUMBER|VIS_OPTION_NEED_WINDOW, + VIS_HELP("Wrap lines at minimum of window width and wrapcolumn") + }, +}; + +VIS_INTERNAL void +vis_option_free(VisOption *opt) +{ + if (!opt || Between(opt, vis_options_table, (vis_options_table + countof(vis_options_table) - 1))) + return; + + for (const char **name = opt->names; *name; name++) + free((char*)*name); + free(VIS_HELP_USE((char*)opt->help)); + free(opt); +} + +VIS_EXPORT bool +vis_option_register(Vis *vis, const char *names[], VisOptionFlags flags, + VisOptionSetFunction *set, VisOptionGetFunction *get, + void *set_context, void *get_context, const char *help) +{ + if (!names || !names[0]) + return false; + + for (const char **name = names; *name; name++) { + if (map_get(vis->options, *name)) + return false; + } + VisOption *opt = calloc(1, sizeof *opt); + if (!opt) + return false; + for (size_t i = 0; i < countof(opt->names)-1 && names[i]; i++) { + if (!(opt->names[i] = strdup(names[i]))) + goto err; + } + opt->flags = flags; + opt->set = set; + opt->get = get; + opt->set_context = set_context; + opt->get_context = get_context; +#if CONFIG_HELP + if (help && !(opt->help = strdup(help))) + goto err; +#endif + for (const char **name = names; *name; name++) + map_put(vis->options, *name, opt); + return true; +err: + vis_option_free(opt); + return false; +} + +VIS_EXPORT bool +vis_option_unregister(Vis *vis, const char *name) +{ + VisOption *opt = map_get(vis->options, name); + if (!opt) + return false; + for (const char **alias = opt->names; *alias; alias++) { + if (!map_delete(vis->options, *alias)) + return false; + } + vis_option_free(opt); + return true; +} + +VIS_INTERNAL VisOption * +vis_option_from_string(Vis *vis, str8 string) +{ + // TODO(rnp): this is pure c brain damage. make map system use strings with length + char name[256]; + + ptrdiff_t length = MIN(countof(name) - 1, string.length); + memcpy(name, string.data, length); + name[length] = 0; + + VisOption *result = map_closest(vis->options, name); + return result; +} + +VIS_EXPORT void +vis_shell_set(Vis *vis, const char *new_shell) +{ + char *shell = strdup(new_shell); + if (!shell) { + vis_info_show(vis, "Failed to change shell"); + } else { + free(vis->shell); + vis->shell = shell; + } +} + +VIS_INTERNAL bool +vis_option_set(Vis *vis, Win *win, VisOption *option, VisValue value, bool toggle) +{ + // NOTE(rnp): the switch statement below forces the compiler to clamp this result + ptrdiff_t option_index = option - vis_options_table; + + bool result = true; + switch (option_index) { + case OPTION_AUTOINDENT:{ vis->autoindent = toggle ? !vis->autoindent : value.u.boolean; }break; + case OPTION_CHANGE_256COLORS:{ vis->change_colors = toggle ? !vis->change_colors : value.u.boolean; }break; + case OPTION_COLOR_COLUMN:{ win->view.colorcolumn = MAX(value.u.integer, 0); }break; + case OPTION_ESCDELAY:{ termkey_set_waittime(vis->ui.termkey, value.u.integer); }break; + case OPTION_EXPANDTAB:{ win->expandtab = toggle ? !win->expandtab : value.u.boolean; }break; + case OPTION_IGNORECASE:{ vis->ignorecase = toggle ? !vis->ignorecase : value.u.boolean; }break; + case OPTION_SHELL:{ vis_shell_set(vis, value.u.string); }break; + case OPTION_TABWIDTH:{ view_tabwidth_set(&win->view, value.u.integer); }break; + case OPTION_WRAP_COLUMN:{ win->view.wrapcolumn = MAX(0, value.u.integer); }break; + + case OPTION_CURSOR_LINE: + case OPTION_SHOW_EOF: + case OPTION_SHOW_NEWLINES: + case OPTION_SHOW_SPACES: + case OPTION_SHOW_TABS: + case OPTION_STATUSBAR: + { + const int values[] = { + [OPTION_CURSOR_LINE] = UI_OPTION_CURSOR_LINE, + [OPTION_SHOW_EOF] = UI_OPTION_SYMBOL_EOF, + [OPTION_SHOW_NEWLINES] = UI_OPTION_SYMBOL_EOL, + [OPTION_SHOW_SPACES] = UI_OPTION_SYMBOL_SPACE, + [OPTION_SHOW_TABS] = UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL, + [OPTION_STATUSBAR] = UI_OPTION_STATUSBAR, + }; + int flags = win->options; + if (toggle) { + flags ^= values[option_index]; + } else if (value.u.boolean) { + flags |= values[option_index]; + } else { + flags &= ~values[option_index]; + } + win_options_set(win, flags); + }break; + + case OPTION_NUMBER:{ + enum UiOption opt = win->options; + if (value.u.boolean || (toggle && !(opt & UI_OPTION_LINE_NUMBERS_ABSOLUTE))) { + opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE; + opt |= UI_OPTION_LINE_NUMBERS_ABSOLUTE; + } else { + opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE; + } + win_options_set(win, opt); + }break; + + case OPTION_NUMBER_RELATIVE:{ + enum UiOption opt = win->options; + if (value.u.boolean || (toggle && !(opt & UI_OPTION_LINE_NUMBERS_RELATIVE))) { + opt &= ~UI_OPTION_LINE_NUMBERS_ABSOLUTE; + opt |= UI_OPTION_LINE_NUMBERS_RELATIVE; + } else { + opt &= ~UI_OPTION_LINE_NUMBERS_RELATIVE; + } + win_options_set(win, opt); + }break; + + case OPTION_SAVE_METHOD:{ + if (strcmp("auto", value.u.string) == 0) { + win->file->save_method = TEXT_SAVE_AUTO; + } else if (strcmp("atomic", value.u.string) == 0) { + win->file->save_method = TEXT_SAVE_ATOMIC; + } else if (strcmp("inplace", value.u.string) == 0) { + win->file->save_method = TEXT_SAVE_INPLACE; + } else { + vis_info_show(vis, "Invalid save method `%s', expected " + "'auto', 'atomic' or 'inplace'", value.u.string); + return false; + } + }break; + + case OPTION_LOAD_METHOD:{ + if (strcmp("auto", value.u.string) == 0) { + vis->load_method = TEXT_LOAD_AUTO; + } else if (strcmp("read", value.u.string) == 0) { + vis->load_method = TEXT_LOAD_READ; + } else if (strcmp("mmap", value.u.string) == 0) { + vis->load_method = TEXT_LOAD_MMAP; + } else { + vis_info_show(vis, "Invalid load method `%s', expected " + "'auto', 'read' or 'mmap'", value.u.string); + result = false; + } + }break; + + case OPTION_LAYOUT:{ + enum UiLayout layout; + if (value.kind == VisValueKind_String) { + if (strcmp("h", value.u.string) == 0) { + layout = UI_LAYOUT_HORIZONTAL; + } else if (strcmp("v", value.u.string) == 0) { + layout = UI_LAYOUT_VERTICAL; + } else { + vis_info_show(vis, "Invalid layout `%s', expected 'h' or 'v'", value.u.string); + result = false; + } + } else { + assert(value.kind == VisValueKind_Integer); + layout = Clamp(value.u.integer, 0, UI_LAYOUT_COUNT - 1); + } + if (result) ui_arrange(&vis->ui, layout); + }break; + + case OPTION_BREAKAT:{ + if (!view_breakat_set(&win->view, value.u.string)) { + vis_info_show(vis, "Failed to set breakat"); + result = false; + } + }break; + + default:{ + result = option->set ? option->set(vis, win, option->names[0], option->set_context, option->flags, value, toggle) + : false; + }break; + } + + return result; +} + + +VIS_INTERNAL VisValue +vis_option_get(Vis *vis, Win *win, VisOption *option) +{ + VisValue result = {0}; + if (option && (!(option->flags & VIS_OPTION_NEED_WINDOW) || win)) { + // NOTE(rnp): the switch statement below forces the compiler to clamp this result + ptrdiff_t option_index = option - vis_options_table; + + if (option->flags & VIS_OPTION_TYPE_NUMBER) { + result.kind = VisValueKind_Integer; + } else if (option->flags & VIS_OPTION_TYPE_STRING) { + result.kind = VisValueKind_String; + } else if (option->flags & VIS_OPTION_TYPE_BOOL) { + result.kind = VisValueKind_Boolean; + } + + switch (option_index) { + case OPTION_AUTOINDENT:{ result.u.boolean = vis->autoindent; }break; + case OPTION_BREAKAT:{ result.u.string = win->view.breakat; }break; + case OPTION_CHANGE_256COLORS:{ result.u.boolean = vis->change_colors; }break; + case OPTION_COLOR_COLUMN:{ result.u.integer = win->view.colorcolumn; }break; + case OPTION_ESCDELAY:{ result.u.integer = termkey_get_waittime(vis->ui.termkey); }break; + case OPTION_EXPANDTAB:{ result.u.boolean = win->expandtab; }break; + case OPTION_IGNORECASE:{ result.u.boolean = vis->ignorecase; }break; + case OPTION_LAYOUT:{ result.u.integer = vis->ui.layout; }break; + case OPTION_SHELL:{ result.u.string = vis->shell; }break; + case OPTION_TABWIDTH:{ result.u.integer = win->view.tabwidth; }break; + case OPTION_WRAP_COLUMN:{ result.u.integer = win->view.wrapcolumn; }break; + + case OPTION_CURSOR_LINE: + case OPTION_NUMBER: + case OPTION_NUMBER_RELATIVE: + case OPTION_SHOW_EOF: + case OPTION_SHOW_NEWLINES: + case OPTION_SHOW_SPACES: + case OPTION_SHOW_TABS: + case OPTION_STATUSBAR: + { + const int values[] = { + [OPTION_NUMBER] = UI_OPTION_LINE_NUMBERS_ABSOLUTE, + [OPTION_NUMBER_RELATIVE] = UI_OPTION_LINE_NUMBERS_RELATIVE, + [OPTION_CURSOR_LINE] = UI_OPTION_CURSOR_LINE, + [OPTION_SHOW_EOF] = UI_OPTION_SYMBOL_EOF, + [OPTION_SHOW_NEWLINES] = UI_OPTION_SYMBOL_EOL, + [OPTION_SHOW_SPACES] = UI_OPTION_SYMBOL_SPACE, + [OPTION_SHOW_TABS] = UI_OPTION_SYMBOL_TAB|UI_OPTION_SYMBOL_TAB_FILL, + [OPTION_STATUSBAR] = UI_OPTION_STATUSBAR, + }; + result.u.boolean = (win->options & values[option_index]) != 0; + }break; + + case OPTION_SAVE_METHOD:{ + switch (win->file->save_method) { + case TEXT_SAVE_AUTO:{ result.u.string = "auto"; }break; + case TEXT_SAVE_ATOMIC:{ result.u.string = "atomic"; }break; + case TEXT_SAVE_INPLACE:{result.u.string = "inplace";}break; + } + }break; + + case OPTION_LOAD_METHOD:{ + switch (vis->load_method) { + case TEXT_LOAD_AUTO:{result.u.string = "auto";}break; + case TEXT_LOAD_READ:{result.u.string = "read";}break; + case TEXT_LOAD_MMAP:{result.u.string = "mmap";}break; + } + }break; + + default:{ + result = option->get ? option->get(vis, win, option->names[0], option->get_context, option->flags) + : (VisValue){0}; + }break; + } + } + return result; +} diff --git a/vis.c b/vis.c index 559988883..f1e1ddcde 100644 --- a/vis.c +++ b/vis.c @@ -10,6 +10,7 @@ #include "buffer.c" #include "event-basic.c" #include "map.c" +#include "vis-options.c" #include "sam.c" #include "text.c" #include "ui-terminal.c" diff --git a/vis.h b/vis.h index cd68d5edc..ec3cf39cd 100644 --- a/vis.h +++ b/vis.h @@ -52,6 +52,23 @@ typedef union { void (*f)(Vis*); } Arg; +typedef enum { + VisValueKind_Nil, + VisValueKind_String, + VisValueKind_Integer, + VisValueKind_Boolean, + VisValueKind_Count, +} VisValueKind; + +typedef struct { + VisValueKind kind; + union { + int64_t integer; + bool boolean; + const char *string; + } u; +} VisValue; + /** * Key action handling function. * @param vis The editor instance. @@ -1152,42 +1169,49 @@ VIS_EXPORT bool vis_cmd_unregister(Vis *vis, const char *name); * @{ */ /** Option properties. */ -enum VisOption { - VIS_OPTION_TYPE_BOOL = 1 << 0, +typedef enum { + VIS_OPTION_TYPE_BOOL = 1 << 0, VIS_OPTION_TYPE_STRING = 1 << 1, VIS_OPTION_TYPE_NUMBER = 1 << 2, - VIS_OPTION_VALUE_OPTIONAL = 1 << 3, - VIS_OPTION_NEED_WINDOW = 1 << 4, - VIS_OPTION_DEPRECATED = 1 << 5, -}; + VIS_OPTION_NEED_WINDOW = 1 << 3, + VIS_OPTION_DEPRECATED = 1 << 4, +} VisOptionFlags; /** * Option handler function. * @param vis The editor instance. * @param win The window to which option should apply, might be ``NULL``. * @param context User provided context pointer as given to `vis_option_register`. - * @param force Whether the option was specified with a bang ``!``. + * @param toggle Whether the option was specified with a bang ``!``. * @param option_flags The applicable option flags. * @param name Name of option which was set. * @param value The new option value. */ -typedef bool (VisOptionFunction)(Vis *vis, Win *win, void *context, bool force, - enum VisOption option_flags, const char *name, Arg *value); +#define VIS_OPTION_SET_FUNCTION(fname) bool fname(Vis *vis, Win *win, const char *name, \ + void *context, VisOptionFlags flags, \ + VisValue value, bool toggle) +typedef VIS_OPTION_SET_FUNCTION(VisOptionSetFunction); +#define VIS_OPTION_GET_FUNCTION(fname) VisValue fname(Vis *vis, Win *win, const char *name, \ + void *context, VisOptionFlags flags) +typedef VIS_OPTION_GET_FUNCTION(VisOptionGetFunction); /** * Register a new ``:set`` option. * @param vis The editor instance. * @param names A ``NULL`` terminated array of option names. * @param option_flags The applicable option flags. - * @param func The function handling the option. - * @param context User supplied context pointer passed to the handler function. + * @param set The function which handles a set operation for the option. + * @param get The function which handles a get operation for the option. + * @param set_context User supplied context pointer passed to the set function. + * @param get_context User supplied context pointer passed to the get function. * @param help Optional single line help text. * @rst * .. note:: Fails if any of the given option names is already registered. * @endrst */ -VIS_EXPORT bool vis_option_register(Vis *vis, const char *names[], enum VisOption option_flags, - VisOptionFunction *func, void *context, const char *help); +VIS_EXPORT bool vis_option_register(Vis *vis, const char *names[], VisOptionFlags flags, + VisOptionSetFunction *set, VisOptionGetFunction *get, + void *set_context, void *get_context, const char *help); /** * Unregister an existing ``:set`` option. * @param vis The editor instance. From fc6697d99ec6616f1c2b5a3a2dcc51e279213427 Mon Sep 17 00:00:00 2001 From: Randy Palamar Date: Wed, 13 May 2026 14:46:10 -0600 Subject: [PATCH 2/2] tests: lua: add option-register test --- test/lua/option-register.lua | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 test/lua/option-register.lua diff --git a/test/lua/option-register.lua b/test/lua/option-register.lua new file mode 100644 index 000000000..8848c3823 --- /dev/null +++ b/test/lua/option-register.lua @@ -0,0 +1,58 @@ +local local_options = {} +local option_set_handler = function(value, toggle, name) + if name == "test1" then + local_options[name] = toggle and not local_options[name] or value + else + local_options[name] = value + end +end +local option_get_handler = function(name) + return local_options[name] +end + +vis:option_register("test1", "bool", option_set_handler, option_get_handler) +vis:option_register("test2", "string", option_set_handler, option_get_handler) +vis:option_register("test3", "number", option_set_handler, option_get_handler) +vis:option_register("test4", "number", option_set_handler) + +describe("option_register", function() + it("set/get: bool", function() + local_options["test1"] = false + vis:command(":set test1 true") + assert.are.same(local_options["test1"], true) + assert.are.same(local_options["test1"], vis.options.test1) + end) + + it("set/get: string", function() + local_options["test2"] = "foo" + vis:command(":set test2 bar") + assert.are.same(local_options["test2"], "bar") + assert.are.same(local_options["test2"], vis.options.test2) + end) + + it("set/get: number", function() + local_options["test3"] = 1 + vis:command(":set test3 2") + assert.are.same(local_options["test3"], 2) + assert.are.same(local_options["test3"], vis.options.test3) + end) + + it("set/get: no get function", function() + local_options["test4"] = 1 + vis:command(":set test4 2") + assert.are.same(local_options["test4"], 2) + assert.are.same(vis.options.test4, nil) + end) + + it("set: bool toggle", function() + local_options["test1"] = true + vis:command(":set test1!") + assert.are.same(local_options["test1"], false) + end) + + it("set: through vis.options", function() + local_options["test2"] = "foo" + vis.options.test2 = "bar" + assert.are.same(local_options["test2"], "bar") + end) +end)