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/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) 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.