diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index 2b2bfec6c7..ca1ddaabd4 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -905,6 +905,17 @@ or `unnamedplus`. The `mode()` function will return the state as it will be after applying the operator. +Here is an example for using a lambda function to create a normal-mode +operator to add quotes around text in the current line: > + + nnoremap let &opfunc='{t -> + \ getline(".") + \ ->split("\\zs") + \ ->insert("\"", col("'']")) + \ ->insert("\"", col("''[") - 1) + \ ->join("") + \ ->setline(".")}'g@ + ============================================================================== 2. Abbreviations *abbreviations* *Abbreviations* diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index a1f2eac5ed..27aa06e18b 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -312,6 +312,17 @@ Note: In the future more global options can be made |global-local|. Using ":setlocal" on a global option might work differently then. + *option-value-function* +Some options ('completefunc', 'imactivatefunc', 'imstatusfunc', 'omnifunc', +'operatorfunc', 'quickfixtextfunc' and 'tagfunc') are set to a function name +or a function reference or a lambda function. Examples: +> + set opfunc=MyOpFunc + set opfunc=function("MyOpFunc") + set opfunc=funcref("MyOpFunc") + set opfunc={t\ ->\ MyOpFunc(t)} +< + Setting the filetype :setf[iletype] [FALLBACK] {filetype} *:setf* *:setfiletype* @@ -4402,7 +4413,9 @@ A jump table for the options with a short description can be found at |Q_op|. 'operatorfunc' 'opfunc' string (default: empty) global This option specifies a function to be called by the |g@| operator. - See |:map-operator| for more info and an example. + See |:map-operator| for more info and an example. The value can be + the name of a function, a |lambda| or a |Funcref|. See + |option-value-function| for more information. This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. @@ -4696,8 +4709,9 @@ A jump table for the options with a short description can be found at |Q_op|. customize the information displayed in the quickfix or location window for each entry in the corresponding quickfix or location list. See |quickfix-window-function| for an explanation of how to write the - function and an example. The value can be the name of a function or a - lambda. + function and an example. The value can be the name of a function, a + |lambda| or a |Funcref|. See |option-value-function| for more + information. This option cannot be set from a |modeline| or in the |sandbox|, for security reasons. diff --git a/src/nvim/ops.c b/src/nvim/ops.c index c3edc5b315..b4fc7534bc 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -6136,6 +6136,23 @@ static void op_colon(oparg_T *oap) // do_cmdline() does the rest } +/// callback function for 'operatorfunc' +static Callback opfunc_cb; + +/// Process the 'operatorfunc' option value. +/// @return OK or FAIL +int set_operatorfunc_option(void) +{ + return option_set_callback_func(p_opfunc, &opfunc_cb); +} + +#if defined(EXITFREE) +void free_operatorfunc_option(void) +{ + callback_free(&opfunc_cb); +} +#endif + /// Handle the "g@" operator: call 'operatorfunc'. static void op_function(const oparg_T *oap) FUNC_ATTR_NONNULL_ALL @@ -6173,7 +6190,10 @@ static void op_function(const oparg_T *oap) // Reset finish_op so that mode() returns the right value. finish_op = false; - (void)call_func_retnr((char *)p_opfunc, 1, argv); + typval_T rettv; + if (callback_call(&opfunc_cb, 1, argv, &rettv) != FAIL) { + tv_clear(&rettv); + } virtual_op = save_virtual_op; finish_op = save_finish_op; diff --git a/src/nvim/option.c b/src/nvim/option.c index 09793cbdcf..e85a0d9ee4 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -66,6 +66,7 @@ #include "nvim/mouse.h" #include "nvim/move.h" #include "nvim/normal.h" +#include "nvim/ops.h" #include "nvim/option.h" #include "nvim/os/os.h" #include "nvim/os_unix.h" @@ -782,6 +783,7 @@ void free_all_options(void) clear_string_option((char_u **)options[i].var); } } + free_operatorfunc_option(); } #endif @@ -3264,8 +3266,12 @@ static char *did_set_string_option(int opt_idx, char_u **varp, char_u *oldval, c } } } - } else if (varp == &p_qftf) { - if (!qf_process_qftf_option()) { + } else if (varp == &p_opfunc) { // 'operatorfunc' + if (set_operatorfunc_option() == FAIL) { + errmsg = e_invarg; + } + } else if (varp == &p_qftf) { // 'quickfixtextfunc' + if (qf_process_qftf_option() == FAIL) { errmsg = e_invarg; } } else { @@ -6917,6 +6923,44 @@ static int fill_culopt_flags(char_u *val, win_T *wp) return OK; } +/// Set the callback function value for an option that accepts a function name, +/// lambda, et al. (e.g. 'operatorfunc', 'tagfunc', etc.) +/// @return OK if the option is successfully set to a function, otherwise FAIL +int option_set_callback_func(char_u *optval, Callback *optcb) +{ + if (optval == NULL || *optval == NUL) { + callback_free(optcb); + return OK; + } + + typval_T *tv; + if (*optval == '{' + || (STRNCMP(optval, "function(", 9) == 0) + || (STRNCMP(optval, "funcref(", 8) == 0)) { + // Lambda expression or a funcref + tv = eval_expr((char *)optval); + if (tv == NULL) { + return FAIL; + } + } else { + // treat everything else as a function name string + tv = xcalloc(1, sizeof(*tv)); + tv->v_type = VAR_STRING; + tv->vval.v_string = (char *)vim_strsave(optval); + } + + Callback cb; + if (!callback_from_typval(&cb, tv)) { + tv_free(tv); + return FAIL; + } + + callback_free(optcb); + *optcb = cb; + tv_free(tv); + return OK; +} + /// Check an option that can be a range of string values. /// /// @param list when true: accept a list of values diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 1c416a872b..d04c2bc470 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -3829,38 +3829,11 @@ static buf_T *qf_find_buf(qf_info_T *qi) return NULL; } -// Process the 'quickfixtextfunc' option value. -bool qf_process_qftf_option(void) +/// Process the 'quickfixtextfunc' option value. +/// @return OK or FAIL +int qf_process_qftf_option(void) { - if (p_qftf == NULL || *p_qftf == NUL) { - callback_free(&qftf_cb); - return true; - } - - typval_T *tv; - if (*p_qftf == '{') { - // Lambda expression - tv = eval_expr((char *)p_qftf); - if (tv == NULL) { - return false; - } - } else { - // treat everything else as a function name string - tv = xcalloc(1, sizeof(*tv)); - tv->v_type = VAR_STRING; - tv->vval.v_string = (char *)vim_strsave(p_qftf); - } - - Callback cb; - if (!callback_from_typval(&cb, tv)) { - tv_free(tv); - return false; - } - - callback_free(&qftf_cb); - qftf_cb = cb; - tv_free(tv); - return true; + return option_set_callback_func(p_qftf, &qftf_cb); } /// Update the w:quickfix_title variable in the quickfix/location list window in diff --git a/src/nvim/testdir/test_normal.vim b/src/nvim/testdir/test_normal.vim index 347404a579..2092b508ea 100644 --- a/src/nvim/testdir/test_normal.vim +++ b/src/nvim/testdir/test_normal.vim @@ -352,6 +352,70 @@ func Test_normal09a_operatorfunc() norm V10j,, call assert_equal(22, g:a) + " Use a lambda function for 'opfunc' + unmap ,, + call cursor(1, 1) + let g:a=0 + nmap ,, :set opfunc={type\ ->\ CountSpaces(type)}g@ + vmap ,, :call CountSpaces(visualmode(), 1) + 50 + norm V2j,, + call assert_equal(6, g:a) + norm V,, + call assert_equal(2, g:a) + norm ,,l + call assert_equal(0, g:a) + 50 + exe "norm 0\10j2l,," + call assert_equal(11, g:a) + 50 + norm V10j,, + call assert_equal(22, g:a) + + " use a partial function for 'opfunc' + let g:OpVal = 0 + func! Test_opfunc1(x, y, type) + let g:OpVal = a:x + a:y + endfunc + set opfunc=function('Test_opfunc1',\ [5,\ 7]) + normal! g@l + call assert_equal(12, g:OpVal) + " delete the function and try to use g@ + delfunc Test_opfunc1 + call test_garbagecollect_now() + call assert_fails('normal! g@l', 'E117:') + set opfunc= + + " use a funcref for 'opfunc' + let g:OpVal = 0 + func! Test_opfunc2(x, y, type) + let g:OpVal = a:x + a:y + endfunc + set opfunc=funcref('Test_opfunc2',\ [4,\ 3]) + normal! g@l + call assert_equal(7, g:OpVal) + " delete the function and try to use g@ + delfunc Test_opfunc2 + call test_garbagecollect_now() + call assert_fails('normal! g@l', 'E933:') + set opfunc= + + " Try to use a function with two arguments for 'operatorfunc' + let g:OpVal = 0 + func! Test_opfunc3(x, y) + let g:OpVal = 4 + endfunc + set opfunc=Test_opfunc3 + call assert_fails('normal! g@l', 'E119:') + call assert_equal(0, g:OpVal) + set opfunc= + delfunc Test_opfunc3 + unlet g:OpVal + + " Try to use a lambda function with two arguments for 'operatorfunc' + set opfunc={x,\ y\ ->\ 'done'} + call assert_fails('normal! g@l', 'E119:') + " clean up unmap ,, set opfunc=