diff options
author | Josh Rahm <joshuarahm@gmail.com> | 2025-04-09 23:46:29 +0000 |
---|---|---|
committer | Josh Rahm <joshuarahm@gmail.com> | 2025-04-16 17:41:19 +0000 |
commit | 2034a8419e1c5675592cdd0d0ffeaadfda58001a (patch) | |
tree | 4ba185d58c2ea2b8893aad66aa96f6e5efaec1ef | |
parent | f068386c9f709c586f44169f4566b4e31ce973de (diff) | |
download | rneovim-2034a8419e1c5675592cdd0d0ffeaadfda58001a.tar.gz rneovim-2034a8419e1c5675592cdd0d0ffeaadfda58001a.tar.bz2 rneovim-2034a8419e1c5675592cdd0d0ffeaadfda58001a.zip |
feat(userregfunc): programmable user-defined registers with multibyte support
This patch introduces a new global option `userregfunc`, allowing users
to define custom behavior for registers not handled by Neovim
internally. This enables programmable registers using any Unicode
character — including multibyte characters.
- A new register slot `USER_REGISTER` is introduced. Any register not
matching the standard set (`0-9a-zA-Z"+-*%#/:.=`, etc.) is routed
through this system.
- When such a register is accessed, the function defined in
`userregfunc` is called with three arguments:
1. `{action}` (string): either `"yank"` or `"put"`
2. `{register}` (string): UTF-8 character name of the register
3. `{content}`:
- If `action == "yank"`: a dictionary with these keys:
- `lines` (list of strings): the yanked text
- `type` (string): one of `"v"` (charwise), `"V"` (linewise), or `"b"` (blockwise)
- `width` (number, optional): present if `type == "b"`
- `additional_data` (dict, optional): user-extensible metadata
- If `action == "put"`: this is always `v:null`
- The function may return either:
- A **string** (used as a charwise register), or
- A **dictionary** matching the structure above
- Internally, `read_userregister()` and `write_userregister()` convert
between `yankreg_T` and typval dictionaries.
- Messages and internal logic fully support multibyte register names via
UTF-8.
- A new `USER_REGISTER` slot is used for logical separation in the
register table.
Included in this patch is an extensible Lua framework (`vim.userregs`)
for defining user register handlers in Lua. It provides per-register
handlers via `register_handler(registers, handler)`
The global function `_G.def_userreg_func` is registered as the default
implementation of `'userregfunc'`, enabling seamless integration with
the Lua framework.
- Register `λ` dynamically inserts the current date
- Register `&` reads and writes from a "global register" file under
`stdpath("run")`
- Register `?` returns the result of a shell command
- Registers that auto-adjust based on filetype, cursor context, or
Treesitter nodes
This change expands the register model into a programmable abstraction —
fully scriptable and extensible — without breaking compatibility.
-rw-r--r-- | runtime/lua/vim/userregs.lua | 64 | ||||
-rw-r--r-- | runtime/plugin/userregs.vim | 2 | ||||
-rw-r--r-- | src/nvim/api/command.c | 10 | ||||
-rw-r--r-- | src/nvim/drawscreen.c | 5 | ||||
-rw-r--r-- | src/nvim/eval.c | 13 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 2 | ||||
-rw-r--r-- | src/nvim/eval/vars.c | 13 | ||||
-rw-r--r-- | src/nvim/ops.c | 322 | ||||
-rw-r--r-- | src/nvim/ops.h | 10 | ||||
-rw-r--r-- | src/nvim/option_vars.h | 1 | ||||
-rw-r--r-- | src/nvim/options.lua | 61 | ||||
-rw-r--r-- | src/nvim/viml/parser/expressions.c | 4 |
12 files changed, 456 insertions, 51 deletions
diff --git a/runtime/lua/vim/userregs.lua b/runtime/lua/vim/userregs.lua new file mode 100644 index 0000000000..d87dcfefa9 --- /dev/null +++ b/runtime/lua/vim/userregs.lua @@ -0,0 +1,64 @@ +local M = {} + +-- Table mapping register names (strings) to handler objects +-- Each handler must implement `yank(regname, contents)` and/or `put(regname)` +M.handlers = {} + +-- Default handler: stores register content in memory +local default_handler = { + registers = {} +} + +--- Called when a register is yanked into (i.e., written) +---@param regname string +---@param contents table +function default_handler.yank(regname, contents) + default_handler.registers[regname] = contents +end + +--- Called when a register is put from (i.e., read) +---@param regname string +---@return string|table +function default_handler.put(regname) + return default_handler.registers[regname] or "" +end + +--- Register a handler function for one or more register names. +--- @param register_names string|string[] A register or list of registers +--- @param handler table A table with `yank()` and/or `put()` methods +function M.register_handler(register_names, handler) + if type(register_names) == "string" then + M.handlers[register_names] = handler + elseif type(register_names) == "table" then + for _, reg in ipairs(register_names) do + M.handlers[reg] = handler + end + else + error("register_names must be a string or table of strings") + end +end + +--- This is the function Neovim will call via 'userregfunc'. +--- It dispatches based on the action and register name. +---@param action string "yank" or "put" +---@param regname string +---@param contents any Only passed when action is "yank" +---@return any Only returned when action is "put" +function M.fn(action, regname, contents) + local handler = M.handlers[regname] or default_handler + if action == "yank" and handler.yank then + handler.yank(regname, contents) + elseif action == "put" and handler.put then + return handler.put(regname) + else + vim.notify( + ("[userregs] No valid handler for action %q on register %q"):format(action, regname), + vim.log.levels.WARN + ) + end +end + +-- Expose the function to Neovim via 'set userregfunc=v:lua.def_userreg_func' +_G.def_userreg_func = M.fn + +return M diff --git a/runtime/plugin/userregs.vim b/runtime/plugin/userregs.vim new file mode 100644 index 0000000000..c61923334d --- /dev/null +++ b/runtime/plugin/userregs.vim @@ -0,0 +1,2 @@ +lua require('vim.userregs') +set userregfunc=v:lua.def_userreg_func diff --git a/src/nvim/api/command.c b/src/nvim/api/command.c index 4b93f09c61..b8892c9baa 100644 --- a/src/nvim/api/command.c +++ b/src/nvim/api/command.c @@ -496,18 +496,14 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Arena if (HAS_KEY(cmd, cmd, reg)) { VALIDATE_MOD((ea.argt & EX_REGSTR), "register", cmd->cmd.data); - VALIDATE_EXP((cmd->reg.size == 1), - "reg", "single character", cmd->reg.data, { - goto end; - }); - char regname = cmd->reg.data[0]; + int regname = utf_ptr2char(cmd->reg.data); VALIDATE((regname != '='), "%s", "Cannot use register \"=", { goto end; }); VALIDATE(valid_yank_reg(regname, (!IS_USER_CMDIDX(ea.cmdidx) && ea.cmdidx != CMD_put && ea.cmdidx != CMD_iput)), - "Invalid register: \"%c", regname, { + "Invalid register: \"%s", cmd->reg.data, { goto end; }); ea.regname = (uint8_t)regname; @@ -794,7 +790,7 @@ static void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdin // Command register. if (eap->argt & EX_REGSTR && eap->regname) { - kv_printf(cmdline, " %c", eap->regname); + kv_printf(cmdline, " %s", reg_to_mb(eap->regname)); } eap->argc = argc; diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index bf2bc077e0..fcb11fd281 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -96,6 +96,7 @@ #include "nvim/move.h" #include "nvim/normal.h" #include "nvim/normal_defs.h" +#include "nvim/ops.h" #include "nvim/option.h" #include "nvim/option_vars.h" #include "nvim/os/os_defs.h" @@ -1184,8 +1185,8 @@ static void recording_mode(int hl_id) } msg_puts_hl(_("recording"), hl_id, false); - char s[4]; - snprintf(s, ARRAY_SIZE(s), " @%c", reg_recording); + char s[4 + MB_MAXBYTES]; + snprintf(s, ARRAY_SIZE(s), " @%s", reg_to_mb(reg_recording)); msg_puts_hl(s, hl_id, false); } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 3f73074333..370565c441 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -3446,12 +3446,10 @@ static int eval7(char **arg, typval_T *rettv, evalarg_T *const evalarg, bool wan // Register contents: @r. case '@': (*arg)++; + int regname = mb_cptr2char_adv((const char**) arg); if (evaluate) { rettv->v_type = VAR_STRING; - rettv->vval.v_string = get_reg_contents(**arg, kGRegExprSrc); - } - if (**arg != NUL) { - (*arg)++; + rettv->vval.v_string = get_reg_contents(regname, kGRegExprSrc); } break; @@ -7073,16 +7071,17 @@ void set_argv_var(char **argv, int argc) /// Set v:register if needed. void set_reg_var(int c) { - char regname; + int regname; if (c == 0 || c == ' ') { regname = '"'; } else { - regname = (char)c; + regname = c; } + // Avoid free/alloc when the value is already right. if (vimvars[VV_REG].vv_str == NULL || vimvars[VV_REG].vv_str[0] != c) { - set_vim_var_string(VV_REG, ®name, 1); + set_vim_var_string(VV_REG, reg_to_mb(regname), -1); } } diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 25252cdfde..8ed73947e7 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -2858,7 +2858,7 @@ static int getreg_get_regname(typval_T *argvars) strregname = get_vim_var_str(VV_REG); } - return *strregname == 0 ? '"' : (uint8_t)(*strregname); + return *strregname == 0 ? '"' : utf_ptr2char(strregname); } /// "getreg()" function diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 012d23b567..8d637fcc43 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -34,6 +34,7 @@ #include "nvim/globals.h" #include "nvim/hashtab.h" #include "nvim/macros_defs.h" +#include "nvim/mbyte.h" #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/ops.h" @@ -599,7 +600,7 @@ const char *skip_var_list(const char *arg, int *var_count, int *semicolon, bool static const char *skip_var_one(const char *arg) { if (*arg == '@' && arg[1] != NUL) { - return arg + 2; + return arg + 1 + utfc_ptr2len(arg + 1); } return find_name_end(*arg == '$' || *arg == '&' ? arg + 1 : arg, NULL, NULL, FNE_INCL_BR | FNE_CHECK_START); @@ -908,16 +909,18 @@ static char *ex_let_register(char *arg, typval_T *const tv, const bool is_const, char *arg_end = NULL; arg++; + int regname = utf_ptr2char(arg); + int mblen = utf_ptr2len(arg); if (op != NULL && vim_strchr("+-*/%", (uint8_t)(*op)) != NULL) { semsg(_(e_letwrong), op); } else if (endchars != NULL - && vim_strchr(endchars, (uint8_t)(*skipwhite(arg + 1))) == NULL) { + && vim_strchr(endchars, (uint8_t)(*skipwhite(arg + mblen))) == NULL) { emsg(_(e_letunexp)); } else { char *ptofree = NULL; const char *p = tv_get_string_chk(tv); if (p != NULL && op != NULL && *op == '.') { - char *s = get_reg_contents(*arg == '@' ? '"' : *arg, kGRegExprSrc); + char *s = get_reg_contents(regname == '@' ? '"' : regname, kGRegExprSrc); if (s != NULL) { ptofree = concat_str(s, p); p = ptofree; @@ -925,8 +928,8 @@ static char *ex_let_register(char *arg, typval_T *const tv, const bool is_const, } } if (p != NULL) { - write_reg_contents(*arg == '@' ? '"' : *arg, p, (ssize_t)strlen(p), false); - arg_end = arg + 1; + write_reg_contents(*arg == '@' ? '"' : regname, p, (ssize_t)strlen(p), false); + arg_end = arg + mblen; } xfree(ptofree); } diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 977d26890e..e89b38296c 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -29,6 +29,7 @@ #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/eval/typval_defs.h" +#include "nvim/eval/userfunc.h" #include "nvim/ex_cmds2.h" #include "nvim/ex_cmds_defs.h" #include "nvim/ex_getln.h" @@ -80,6 +81,7 @@ static yankreg_T y_regs[NUM_REGISTERS] = { 0 }; static yankreg_T *y_previous = NULL; // ptr to last written yankreg +static int last_userreg_name = -1; // for behavior between start_batch_changes() and end_batch_changes()) static int batch_change_count = 0; // inside a script @@ -134,6 +136,15 @@ static char opchars[][3] = { { Ctrl_X, NUL, OPF_CHANGE }, // OP_NR_SUB }; +static char regstr_mb[MB_MAXBYTES + 1]; +/// Converts a register to a multibyte-character "string". Returns a pointer to a static buffer, so +/// this should be used quickly. +const char* reg_to_mb(int reg) +{ + regstr_mb[utf_char2bytes(reg, regstr_mb)] = 0; + return regstr_mb; +} + yankreg_T *get_y_previous(void) { return y_previous; @@ -875,17 +886,11 @@ char *get_expr_line_src(void) /// @param writing allow only writable registers bool valid_yank_reg(int regname, bool writing) { - if ((regname > 0 && ASCII_ISALNUM(regname)) - || (!writing && vim_strchr("/.%:=", regname) != NULL) - || regname == '#' - || regname == '"' - || regname == '-' - || regname == '_' - || regname == '*' - || regname == '+') { - return true; + if (writing && vim_strchr("/.%:=", regname) != NULL) { + return false; } - return false; + + return true; } /// @return yankreg_T to use, according to the value of `regname`. @@ -909,6 +914,8 @@ bool valid_yank_reg(int regname, bool writing) yankreg_T *get_yank_register(int regname, int mode) { yankreg_T *reg; + int do_eval = !(mode & YREG_NOEVAL); + mode &= ~YREG_NOEVAL; if ((mode == YREG_PASTE || mode == YREG_PUT) && get_clipboard(regname, ®, false)) { @@ -923,16 +930,26 @@ yankreg_T *get_yank_register(int regname, int mode) && (regname == 0 || regname == '"' || regname == '*' || regname == '+') && y_previous != NULL) { // in case clipboard not available, paste from previous used register + if (do_eval && is_yankreg_user_register(y_previous)) { + read_userregister(last_userreg_name, y_previous); + } return y_previous; } int i = op_reg_index(regname); - // when not 0-9, a-z, A-Z or '-'/'+'/'*': use register 0 if (i == -1) { i = 0; } + reg = &y_regs[i]; + if (i == USER_REGISTER) { + last_userreg_name = regname; + if (do_eval && (mode == YREG_PUT || mode == YREG_PASTE)) { + read_userregister(regname, reg); + } + } + if (mode == YREG_YANK) { // remember the written register for unnamed paste y_previous = reg; @@ -945,6 +962,14 @@ static bool is_append_register(int regname) return ASCII_ISUPPER(regname); } +static bool is_user_register(int regname) { + return op_reg_index(regname) == USER_REGISTER; +} + +static bool is_yankreg_user_register(yankreg_T* yankreg) { + return yankreg == &y_regs[USER_REGISTER]; +} + /// @return a copy of contents in register `name` for use in do_put. Should be /// freed by caller. yankreg_T *copy_register(int name) @@ -988,8 +1013,7 @@ int do_record(int c) if (reg_recording == 0) { // start recording - // registers 0-9, a-z and " are allowed - if (c < 0 || (!ASCII_ISALNUM(c) && c != '"')) { + if (!valid_yank_reg(c, true)) { retval = FAIL; } else { reg_recording = c; @@ -1038,10 +1062,12 @@ int do_record(int c) // We don't want to change the default register here, so save and // restore the current register name. yankreg_T *old_y_previous = y_previous; + int old_last_userreg_name = last_userreg_name; retval = stuff_yank(regname, p); y_previous = old_y_previous; + last_userreg_name = old_last_userreg_name; } } return retval; @@ -1082,6 +1108,10 @@ static int stuff_yank(int regname, char *p) reg->y_array[0] = cbuf_as_string(p, plen); reg->y_size = 1; reg->y_type = kMTCharWise; + + if (is_user_register(regname)) { + write_userregister(regname, reg); + } } reg->timestamp = os_time(); return OK; @@ -2767,6 +2797,10 @@ static void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append) xfree(reg->y_array); } + if (is_user_register(oap->regname)) { + write_userregister(oap->regname, curr); + } + if (message) { // Display message about yank? if (yank_type == kMTCharWise && yanklines == 1) { yanklines = 0; @@ -2778,7 +2812,7 @@ static void op_yank_reg(oparg_T *oap, bool message, yankreg_T *reg, bool append) if (oap->regname == NUL) { *namebuf = NUL; } else { - vim_snprintf(namebuf, sizeof(namebuf), _(" into \"%c"), oap->regname); + vim_snprintf(namebuf, sizeof(namebuf), _(" into \"%s"), reg_to_mb(oap->regname)); } // redisplay now, so message is not deleted @@ -5033,7 +5067,8 @@ void *get_reg_contents(int regname, int flags) return retval; } -static yankreg_T *init_write_reg(int name, yankreg_T **old_y_previous, bool must_append) +static yankreg_T *init_write_reg(int name, yankreg_T **old_y_previous, int *old_last_userreg_name, + bool must_append) { if (!valid_yank_reg(name, true)) { // check for valid reg name emsg_invreg(name); @@ -5042,6 +5077,7 @@ static yankreg_T *init_write_reg(int name, yankreg_T **old_y_previous, bool must // Don't want to change the current (unnamed) register. *old_y_previous = y_previous; + *old_last_userreg_name = last_userreg_name; yankreg_T *reg = get_yank_register(name, YREG_YANK); if (!is_append_register(name) && !must_append) { @@ -5050,7 +5086,7 @@ static yankreg_T *init_write_reg(int name, yankreg_T **old_y_previous, bool must return reg; } -static void finish_write_reg(int name, yankreg_T *reg, yankreg_T *old_y_previous) +static void finish_write_reg(int name, yankreg_T *reg, yankreg_T *old_y_previous, int old_last_userreg_name) { // Send text of clipboard register to the clipboard. set_clipboard(name, reg); @@ -5058,6 +5094,11 @@ static void finish_write_reg(int name, yankreg_T *reg, yankreg_T *old_y_previous // ':let @" = "val"' should change the meaning of the "" register if (name != '"') { y_previous = old_y_previous; + last_userreg_name = old_last_userreg_name; + } + + if (is_user_register(name)) { + write_userregister(name, reg); } } @@ -5090,13 +5131,14 @@ void write_reg_contents_lst(int name, char **strings, bool must_append, MotionTy } yankreg_T *old_y_previous, *reg; - if (!(reg = init_write_reg(name, &old_y_previous, must_append))) { + int old_last_userreg_name; + if (!(reg = init_write_reg(name, &old_y_previous, &old_last_userreg_name, must_append))) { return; } str_to_reg(reg, yank_type, (char *)strings, strlen((char *)strings), block_len, true); - finish_write_reg(name, reg, old_y_previous); + finish_write_reg(name, reg, old_y_previous, old_last_userreg_name); } /// write_reg_contents_ex - store `str` in register `name` @@ -5178,11 +5220,12 @@ void write_reg_contents_ex(int name, const char *str, ssize_t len, bool must_app } yankreg_T *old_y_previous, *reg; - if (!(reg = init_write_reg(name, &old_y_previous, must_append))) { + int old_last_userreg_name; + if (!(reg = init_write_reg(name, &old_y_previous, &old_last_userreg_name, must_append))) { return; } str_to_reg(reg, yank_type, str, (size_t)len, block_len, false); - finish_write_reg(name, reg, old_y_previous); + finish_write_reg(name, reg, old_y_previous, old_last_userreg_name); } /// str_to_reg - Put a string into a register. @@ -5683,6 +5726,15 @@ const char *did_set_operatorfunc(optset_T *args FUNC_ATTR_UNUSED) return NULL; } +static Callback urf_cb; +const char *did_set_userregfunc(optset_T *args FUNC_ATTR_UNUSED) +{ + if (option_set_callback_func(p_urf, &urf_cb) == FAIL) { + return e_invarg; + } + return NULL; +} + #if defined(EXITFREE) void free_operatorfunc_option(void) { @@ -6779,6 +6831,221 @@ static void set_clipboard(int name, yankreg_T *reg) eval_call_provider("clipboard", "set", args, true); } +dict_T *yankreg_to_dict(const yankreg_T *reg) +{ + if (reg == NULL) { + return NULL; + } + + dict_T *d = tv_dict_alloc(); + + // Add "lines" field + list_T *l = tv_list_alloc(reg->y_size); + for (size_t i = 0; i < reg->y_size; ++i) { + String str = reg->y_array[i]; + tv_list_append_string(l, str.data, str.size); + } + tv_dict_add_list(d, S_LEN("lines"), l); + + // Add "type" field + const char *type_str = NULL; + switch (reg->y_type) { + case kMTCharWise: + type_str = "v"; + break; + case kMTLineWise: + type_str = "V"; + break; + case kMTBlockWise: + type_str = "b"; + break; + default: + type_str = "v"; + break; + } + tv_dict_add_str(d, S_LEN("type"), xstrdup(type_str)); + + // Add "width" field for blockwise (optional) + if (reg->y_type == kMTBlockWise) { + tv_dict_add_nr(d, S_LEN("width"), reg->y_width); + } + + // Add "additional_data" fvield (empty by default) + tv_dict_add_dict(d, S_LEN("additional_data"), tv_dict_alloc()); + + return d; +} + +int dict_to_yankreg(dict_T *dict, yankreg_T *out) +{ + if (dict == NULL || out == NULL) { + return 1; + } + + dictitem_T *di_lines = tv_dict_find(dict, "lines", -1); + if (di_lines == NULL || di_lines->di_tv.v_type != VAR_LIST) { + return 1; + } + + list_T *lines = di_lines->di_tv.vval.v_list; + if (lines == NULL) { + return 1; + } + + free_register(out); + out->y_size = tv_list_len(lines); + out->y_array = xcalloc(out->y_size, sizeof(String)); + + int i = 0; + TV_LIST_ITER_CONST(lines, li, { + const typval_T *tv = TV_LIST_ITEM_TV(li); + const char *s = tv_get_string(tv); + if (s == NULL) { + s = ""; + } + + size_t len = strlen(s); + char* p = xstrnsave(s, len); + out->y_array[i++] = cbuf_to_string(p, len); + }); + + // Get "type" field + dictitem_T *di_type = tv_dict_find(dict, "type", -1); + if (di_type == NULL || di_type->di_tv.v_type != VAR_STRING) { + return 1; + } + + char *type = di_type->di_tv.vval.v_string; + if (type == NULL || strcmp(type, "v") == 0) { + out->y_type = kMTCharWise; + } else if (strcmp(type, "V") == 0) { + out->y_type = kMTLineWise; + } else if (strcmp(type, "b") == 0) { + out->y_type = kMTBlockWise; + } else { + return 1; + } + + // Get optional "width" field + dictitem_T *di_width = tv_dict_find(dict, "width", -1); + if (di_width != NULL && di_width->di_tv.v_type == VAR_NUMBER) { + out->y_width = (int)di_width->di_tv.vval.v_number; + } else { + out->y_width = 0; + } + + // Additional data is optional, no-op for now + out->additional_data = NULL; + + return true; +} + +int read_userregister(int regname, yankreg_T *out) +{ + if (out == NULL || p_urf == NULL || *p_urf == NUL) { + return 1; + } + + // Clear output + memset(out, 0, sizeof(*out)); + + typval_T rettv; + typval_T argv[3]; + + // argv[0] = "put" + argv[0].v_type = VAR_STRING; + argv[0].vval.v_string = "put"; + + // argv[1] = register name (UTF-8 string) + char regstr[MB_MAXBYTES + 1]; + int len = utf_char2bytes(regname, (char *)regstr); + regstr[len] = '\0'; + + argv[1].v_type = VAR_STRING; + argv[1].vval.v_string = regstr; + + // argv[2] = v:null + argv[2].v_type = VAR_SPECIAL; + argv[2].vval.v_special = kSpecialVarNull; + + // Call user function + if (callback_call(&urf_cb, 3, argv, &rettv) == FAIL) { + tv_clear(&rettv); + return 1; + } + + // Return is a string. Make the register a "charwise" register. + if (rettv.v_type == VAR_STRING) { + out->y_type = kMTCharWise; + out->y_size = 1; + out->y_array = xmalloc(sizeof(String)); + const char* ret = rettv.vval.v_string != NULL ? rettv.vval.v_string : ""; + size_t slen = strlen(ret); + char* saved = xstrnsave(ret, slen); + out->y_array[0] = cbuf_to_string(saved, slen); + + tv_clear(&rettv); + return 0; + } + + // Return is a dictionary. It should look like a yankreg_T. + if (rettv.v_type == VAR_DICT && rettv.vval.v_dict != NULL) { + int ec = dict_to_yankreg(rettv.vval.v_dict, out); + tv_clear(&rettv); + return ec; + } + + // Unexpected type + tv_clear(&rettv); + return 1; +} + +int write_userregister(int regname, const yankreg_T *in) +{ + int ec = 0; + typval_T argv[3] = { 0 }; + typval_T rettv = { 0 }; + dict_T* dict = NULL; + + if (in == NULL || p_urf == NULL || *p_urf == NUL) { + ec = 1; + goto end; + } + + argv[0].v_type = VAR_STRING; + argv[0].vval.v_string = "yank"; + + // Register name + char regstr[MB_MAXBYTES + 1]; + int len = utf_char2bytes(regname, regstr); + regstr[len] = '\0'; + + argv[1].v_type = VAR_STRING; + argv[1].vval.v_string = regstr; + + // Convert yankreg_T -> dict + dict = yankreg_to_dict(in); + if (dict == NULL) { + ec = 1; + goto end; + } + + argv[2].v_type = VAR_DICT; + argv[2].vval.v_dict = tv_dict_copy(NULL, dict, false, 0); + + if (callback_call(&urf_cb, 3, argv, &rettv)) { + ec = 1; + tv_clear(&rettv); + tv_clear(&argv[2]); + goto end; + } + tv_clear(&argv[2]); + + // We're not expecting anything specific from the return +end: + return ec; +} + /// Avoid slow things (clipboard) during batch operations (while/for-loops). void start_batch_changes(void) { @@ -6903,7 +7170,7 @@ size_t op_reg_amount(void) /// @param[in] is_unnamed Whether to set the unnamed regiseter to reg /// /// @return true on success, false on failure. -bool op_reg_set(const char name, const yankreg_T reg, bool is_unnamed) +bool op_reg_set(int name, const yankreg_T reg, bool is_unnamed) { int i = op_reg_index(name); if (i == -1) { @@ -6912,6 +7179,10 @@ bool op_reg_set(const char name, const yankreg_T reg, bool is_unnamed) free_register(&y_regs[i]); y_regs[i] = reg; + if (i == USER_REGISTER) { + write_userregister(name, ®); + } + if (is_unnamed) { y_previous = &y_regs[i]; } @@ -6923,12 +7194,17 @@ bool op_reg_set(const char name, const yankreg_T reg, bool is_unnamed) /// @param[in] name Register name. /// /// @return Pointer to the register contents or NULL. -const yankreg_T *op_reg_get(const char name) +const yankreg_T *op_reg_get(int name) { int i = op_reg_index(name); if (i == -1) { return NULL; } + + if (i == USER_REGISTER) { + read_userregister(name, &y_regs[i]); + } + return &y_regs[i]; } @@ -6937,7 +7213,7 @@ const yankreg_T *op_reg_get(const char name) /// @param[in] name Register name. /// /// @return true on success, false on failure. -bool op_reg_set_previous(const char name) +bool op_reg_set_previous(int name) { int i = op_reg_index(name); if (i == -1) { diff --git a/src/nvim/ops.h b/src/nvim/ops.h index 99b9b6182d..5f6c3595fd 100644 --- a/src/nvim/ops.h +++ b/src/nvim/ops.h @@ -59,7 +59,8 @@ enum { // The following registers should not be saved in ShaDa file: STAR_REGISTER = 37, PLUS_REGISTER = 38, - NUM_REGISTERS = 39, + USER_REGISTER = 39, + NUM_REGISTERS = 40, }; /// Operator IDs; The order must correspond to opchars[] in ops.c! @@ -118,6 +119,7 @@ typedef enum { YREG_PASTE, YREG_YANK, YREG_PUT, + YREG_NOEVAL = 0x10, } yreg_mode_t; #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -133,6 +135,10 @@ typedef enum { static inline int op_reg_index(const int regname) FUNC_ATTR_CONST { + if (regname == 0 || regname == '"') { + return -1; + } + if (ascii_isdigit(regname)) { return regname - '0'; } else if (ASCII_ISLOWER(regname)) { @@ -146,7 +152,7 @@ static inline int op_reg_index(const int regname) } else if (regname == '+') { return PLUS_REGISTER; } else { - return -1; + return USER_REGISTER; } } diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index d8d3e1e124..d8a7c60c48 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -539,6 +539,7 @@ EXTERN char *p_udir; ///< 'undodir' EXTERN int p_udf; ///< 'undofile' EXTERN OptInt p_ul; ///< 'undolevels' EXTERN OptInt p_ur; ///< 'undoreload' +EXTERN char* p_urf; ///< 'userregfunc' EXTERN OptInt p_uc; ///< 'updatecount' EXTERN OptInt p_ut; ///< 'updatetime' EXTERN char *p_shada; ///< 'shada' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index e687490704..1cb0a65bea 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -9678,6 +9678,63 @@ local options = { varname = 'p_ur', }, { + abbreviation = 'urf', + defaults = '', + cb = 'did_set_userregfunc', + desc = [=[ + Specifies a function to handle any registers that Neovim does not natively + handle. This allows the user to use all characters, including multi-byte ones, + as custom register names. + + The function is called whenever a user-defined register is accessed — either + when yanking to it ("yank") or putting from it ("put"). + + The function must have the following signature: + + function({action}, {register}, {content}) + + Parameters: + {action} string "yank" or "put" + "yank" is called when text is yanked into the register. + "put" is called when the register is used for insertion. + + {register} string A single-character register name. Can be multibyte. + + {content} dict Only present when {action} is "yank". Contains: + {lines}: list of strings representing the yanked text + {type}: "char", "line", or "block" + {width}: (optional) present if type is "block" + {additional_data}: (optional) arbitrary user data + + Return value (for "put"): + - A string (for "char" mode), or + - A dictionary with the same structure as {content}. + + Example (VimL):>vim + + let s:contents = {} + + function! MyUserregFunction(action, register, content) abort + if a:register == '?' && a:action ==# 'put' then + return strftime("YYYY-MM-DD") + end + if a:action ==# 'put' + return get(s:contents, a:register, '') + elseif a:action ==# 'yank' + let s:contents[a:register] = a:content + endif + endfunction + + set userregfunc=MyUserregFunction + < + ]=], + full_name = 'userregfunc', + scope = { 'global' }, + short_desc = N_('Dynamically generate register content via VimL functions'), + type = 'string', + varname = 'p_urf', + }, + { abbreviation = 'uc', cb = 'did_set_updatecount', defaults = 200, @@ -10642,13 +10699,13 @@ local function preprocess(o) if type(o.alias) == 'string' then o.alias = { - o.alias --[[@as string]], + o.alias --[[@as string]] , } end if type(o.defaults) ~= 'table' then o.defaults = { - if_true = o.defaults --[[@as string|boolean|number ]], + if_true = o.defaults --[[@as string|boolean|number ]] , } end end diff --git a/src/nvim/viml/parser/expressions.c b/src/nvim/viml/parser/expressions.c index 7fb4c62b35..5907cdc610 100644 --- a/src/nvim/viml/parser/expressions.c +++ b/src/nvim/viml/parser/expressions.c @@ -557,8 +557,8 @@ LexExprToken viml_pexpr_next_token(ParserState *const pstate, const int flags) case '@': ret.type = kExprLexRegister; if (pline.size > 1) { - ret.len++; - ret.data.reg.name = (uint8_t)pline.data[1]; + ret.len += utfc_ptr2len(pline.data + 1); + ret.data.reg.name = utf_ptr2char(pline.data + 1); } else { ret.data.reg.name = -1; } |