From 900dd2bdab85b25237cec638265d44c2174154ed Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 20 Aug 2022 08:27:10 +0800 Subject: vim-patch:8.2.3665: cannot use a lambda for 'tagfunc' Problem: Cannot use a lambda for 'tagfunc'. Solution: Use 'tagfunc' like 'opfunc'. (Yegappan Lakshmanan, closes vim/vim#9204) https://github.com/vim/vim/commit/19916a8c8920b6a1fd737ffa6d4e363fc7a96319 Co-authored-by: Yegappan Lakshmanan --- runtime/doc/options.txt | 10 ++++---- src/nvim/buffer.c | 1 + src/nvim/buffer_defs.h | 1 + src/nvim/option.c | 3 +++ src/nvim/optionstr.c | 5 ++++ src/nvim/tag.c | 41 +++++++++++++++++++++++++++++++- src/nvim/testdir/test_tagfunc.vim | 50 +++++++++++++++++++++++++++++++++++++++ 7 files changed, 106 insertions(+), 5 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 818c6d0115..883585116d 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -318,9 +318,9 @@ Some options ('completefunc', 'imactivatefunc', 'imstatusfunc', 'omnifunc', 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)} + set opfunc=function('MyOpFunc') + set opfunc=funcref('MyOpFunc') + let &opfunc = "{t -> MyOpFunc(t)}" < Setting the filetype @@ -6443,7 +6443,9 @@ A jump table for the options with a short description can be found at |Q_op|. This option specifies a function to be used to perform tag searches. The function gets the tag pattern and should return a List of matching tags. See |tag-function| for an explanation of how to write the - function and an example. + 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. *'taglength'* *'tl'* 'taglength' 'tl' number (default 0) diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index ade5c35450..4c8faccaa7 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -1981,6 +1981,7 @@ void free_buf_options(buf_T *buf, int free_p_ff) clear_string_option(&buf->b_p_tags); clear_string_option(&buf->b_p_tc); clear_string_option(&buf->b_p_tfu); + callback_free(&buf->b_tfu_cb); clear_string_option(&buf->b_p_dict); clear_string_option(&buf->b_p_tsr); clear_string_option(&buf->b_p_qe); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 2b42289858..d8e86a75cf 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -677,6 +677,7 @@ struct file_buffer { char *b_p_cfu; ///< 'completefunc' char *b_p_ofu; ///< 'omnifunc' char *b_p_tfu; ///< 'tagfunc' + Callback b_tfu_cb; ///< 'tagfunc' callback int b_p_eof; ///< 'endoffile' int b_p_eol; ///< 'endofline' int b_p_fixeol; ///< 'fixendofline' diff --git a/src/nvim/option.c b/src/nvim/option.c index 523ae13e52..1eabe8c540 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -78,6 +78,7 @@ #include "nvim/spellsuggest.h" #include "nvim/strings.h" #include "nvim/syntax.h" +#include "nvim/tag.h" #include "nvim/ui.h" #include "nvim/ui_compositor.h" #include "nvim/undo.h" @@ -577,6 +578,7 @@ void free_all_options(void) } } free_operatorfunc_option(); + free_tagfunc_option(); } #endif @@ -4383,6 +4385,7 @@ void buf_copy_options(buf_T *buf, int flags) COPY_OPT_SCTX(buf, BV_OFU); buf->b_p_tfu = xstrdup(p_tfu); COPY_OPT_SCTX(buf, BV_TFU); + buf_set_tfu_callback(buf); buf->b_p_sts = p_sts; COPY_OPT_SCTX(buf, BV_STS); buf->b_p_sts_nopaste = p_sts_nopaste; diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index 65bc9f60df..abae338daa 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -38,6 +38,7 @@ #include "nvim/spellfile.h" #include "nvim/spellsuggest.h" #include "nvim/strings.h" +#include "nvim/tag.h" #include "nvim/ui.h" #include "nvim/vim.h" #include "nvim/window.h" @@ -1480,6 +1481,10 @@ char *did_set_string_option(int opt_idx, char **varp, char *oldval, char *errbuf if (qf_process_qftf_option() == FAIL) { errmsg = e_invarg; } + } else if (gvarp == &p_tfu) { // 'tagfunc' + if (set_tagfunc_option() == FAIL) { + errmsg = e_invarg; + } } else { // Options that are a list of flags. p = NULL; diff --git a/src/nvim/tag.c b/src/nvim/tag.c index 264f961b43..e38fbb7da9 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -114,10 +114,49 @@ static char *tagmatchname = NULL; // name of last used tag static taggy_T ptag_entry = { NULL, INIT_FMARK, 0, 0, NULL }; static int tfu_in_use = false; // disallow recursive call of tagfunc +static Callback tfu_cb; // 'tagfunc' callback function // Used instead of NUL to separate tag fields in the growarrays. #define TAG_SEP 0x02 +/// Reads the 'tagfunc' option value and convert that to a callback value. +/// Invoked when the 'tagfunc' option is set. The option value can be a name of +/// a function (string), or function() or funcref() or a lambda. +int set_tagfunc_option(void) +{ + callback_free(&tfu_cb); + callback_free(&curbuf->b_tfu_cb); + + if (*curbuf->b_p_tfu == NUL) { + return OK; + } + + if (option_set_callback_func(curbuf->b_p_tfu, &tfu_cb) == FAIL) { + return FAIL; + } + + callback_copy(&curbuf->b_tfu_cb, &tfu_cb); + + return OK; +} + +#if defined(EXITFREE) +void free_tagfunc_option(void) +{ + callback_free(&tfu_cb); +} +#endif + +/// Copy the global 'tagfunc' callback function to the buffer-local 'tagfunc' +/// callback for 'buf'. +void buf_set_tfu_callback(buf_T *buf) +{ + callback_free(&buf->b_tfu_cb); + if (tfu_cb.data.funcref != NULL && *tfu_cb.data.funcref != NUL) { + callback_copy(&buf->b_tfu_cb, &tfu_cb); + } +} + /// Jump to tag; handling of tag commands and tag stack /// /// *tag != NUL: ":tag {tag}", jump to new tag, add to tag stack @@ -1129,7 +1168,7 @@ static int find_tagfunc_tags(char_u *pat, garray_T *ga, int *match_count, int fl flags & TAG_REGEXP ? "r": ""); save_pos = curwin->w_cursor; - result = call_vim_function(curbuf->b_p_tfu, 3, args, &rettv); + result = callback_call(&curbuf->b_tfu_cb, 3, args, &rettv); curwin->w_cursor = save_pos; // restore the cursor position d->dv_refcount--; diff --git a/src/nvim/testdir/test_tagfunc.vim b/src/nvim/testdir/test_tagfunc.vim index bdf5afa5b2..92c2f2d6a1 100644 --- a/src/nvim/testdir/test_tagfunc.vim +++ b/src/nvim/testdir/test_tagfunc.vim @@ -117,4 +117,54 @@ func Test_tagfunc_settagstack() delfunc Mytagfunc2 endfunc +" Test for different ways of setting the 'tagfunc' option +func Test_tagfunc_callback() + " Test for using a function() + func MytagFunc1(pat, flags, info) + let g:MytagFunc1_args = [a:pat, a:flags, a:info] + return v:null + endfunc + let g:MytagFunc1_args = [] + set tagfunc=function('MytagFunc1') + call assert_fails('tag abc', 'E433:') + call assert_equal(['abc', '', {}], g:MytagFunc1_args) + + " Test for using a funcref() + new + func MytagFunc2(pat, flags, info) + let g:MytagFunc2_args = [a:pat, a:flags, a:info] + return v:null + endfunc + let g:MytagFunc2_args = [] + set tagfunc=funcref('MytagFunc2') + call assert_fails('tag def', 'E433:') + call assert_equal(['def', '', {}], g:MytagFunc2_args) + + " Test for using a lambda function + new + func MytagFunc3(pat, flags, info) + let g:MytagFunc3_args = [a:pat, a:flags, a:info] + return v:null + endfunc + let g:MytagFunc3_args = [] + let &tagfunc= '{a, b, c -> MytagFunc3(a, b, c)}' + call assert_fails('tag ghi', 'E433:') + call assert_equal(['ghi', '', {}], g:MytagFunc3_args) + + " Test for clearing the 'tagfunc' option + set tagfunc='' + set tagfunc& + + call assert_fails("set tagfunc=function('abc')", "E700:") + call assert_fails("set tagfunc=funcref('abc')", "E700:") + let &tagfunc = "{a -> 'abc'}" + call assert_fails("echo taglist('a')", "E987:") + + " cleanup + delfunc MytagFunc1 + delfunc MytagFunc2 + delfunc MytagFunc3 + %bw! +endfunc + " vim: shiftwidth=2 sts=2 expandtab -- cgit From b3e9010f4783a51407ec5a5ad9fda1216d4db3fe Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 5 Nov 2022 15:30:54 +0800 Subject: vim-patch:8.2.3705: cannot pass a lambda name to function() or funcref() Problem: Cannot pass a lambda name to function() or funcref(). (Yegappan Lakshmanan) Solution: Handle a lambda name differently. https://github.com/vim/vim/commit/eba3b7f6645c8f856132b4c06a009a3b0a44e21c Co-authored-by: Bram Moolenaar --- src/nvim/eval.c | 5 ++--- src/nvim/eval/userfunc.c | 30 ++++++++++++++++++++++-------- src/nvim/testdir/test_expr.vim | 7 +++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/nvim/eval.c b/src/nvim/eval.c index fe4ae92834..c983388450 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -5029,9 +5029,8 @@ void common_function(typval_T *argvars, typval_T *rettv, bool is_funcref) if ((use_string && vim_strchr(s, AUTOLOAD_CHAR) == NULL) || is_funcref) { name = s; - trans_name = (char *)trans_function_name(&name, false, - TFN_INT | TFN_QUIET | TFN_NO_AUTOLOAD - | TFN_NO_DEREF, NULL, NULL); + trans_name = save_function_name(&name, false, + TFN_INT | TFN_QUIET | TFN_NO_AUTOLOAD | TFN_NO_DEREF, NULL); if (*name != NUL) { s = NULL; } diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index ef74ca58e3..e6b038a335 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -1886,6 +1886,27 @@ theend: return (char_u *)name; } +/// Call trans_function_name(), except that a lambda is returned as-is. +/// Returns the name in allocated memory. +char *save_function_name(char **name, bool skip, int flags, funcdict_T *fudi) +{ + char *p = *name; + char *saved; + + if (strncmp(p, "", 8) == 0) { + p += 8; + (void)getdigits(&p, false, 0); + saved = xstrndup(*name, (size_t)(p - *name)); + if (fudi != NULL) { + CLEAR_POINTER(fudi); + } + } else { + saved = (char *)trans_function_name(&p, skip, flags, fudi, NULL); + } + *name = p; + return saved; +} + #define MAX_FUNC_NESTING 50 /// List functions. @@ -2000,14 +2021,7 @@ void ex_function(exarg_T *eap) // s:func script-local function name // g:func global function name, same as "func" p = eap->arg; - if (strncmp(p, "", 8) == 0) { - p += 8; - (void)getdigits(&p, false, 0); - name = xstrndup(eap->arg, (size_t)(p - eap->arg)); - CLEAR_FIELD(fudi); - } else { - name = (char *)trans_function_name(&p, eap->skip, TFN_NO_AUTOLOAD, &fudi, NULL); - } + name = save_function_name(&p, eap->skip, TFN_NO_AUTOLOAD, &fudi); paren = (vim_strchr(p, '(') != NULL); if (name == NULL && (fudi.fd_dict == NULL || !paren) && !eap->skip) { // Return on an invalid expression in braces, unless the expression diff --git a/src/nvim/testdir/test_expr.vim b/src/nvim/testdir/test_expr.vim index ea874cc398..9dbc923b96 100644 --- a/src/nvim/testdir/test_expr.vim +++ b/src/nvim/testdir/test_expr.vim @@ -496,6 +496,13 @@ func Test_function_with_funcref() call assert_fails("call function('foo()')", 'E475:') call assert_fails("call function('foo()')", 'foo()') call assert_fails("function('')", 'E129:') + + let Len = {s -> strlen(s)} + call assert_equal(6, Len('foobar')) + let name = string(Len) + " can evaluate "function('99')" + call execute('let Ref = ' .. name) + call assert_equal(4, Ref('text')) endfunc func Test_funcref() -- cgit From 1508618d4c35dafee2b82726d2d27fae4e314386 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 20 Aug 2022 09:35:09 +0800 Subject: vim-patch:8.2.3712: cannot use Vim9 lambda for 'tagfunc' Problem: Cannot use Vim9 lambda for 'tagfunc'. Solution: Make it work, add more tests. (Yegappan Lakshmanan, closes vim/vim#9250) https://github.com/vim/vim/commit/05e59e3a9ffddbf93c7af02cd2ba1d0f822d4625 Omit Vim9 script in code and comment out in tests. Co-authored-by: Yegappan Lakshmanan --- runtime/doc/options.txt | 10 +++- src/nvim/insexpand.c | 6 +-- src/nvim/testdir/test_tagfunc.vim | 107 ++++++++++++++++++++++++++++++++++---- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 883585116d..a14dc1b99e 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -320,7 +320,15 @@ or a function reference or a lambda function. Examples: set opfunc=MyOpFunc set opfunc=function('MyOpFunc') set opfunc=funcref('MyOpFunc') - let &opfunc = "{t -> MyOpFunc(t)}" + set opfunc={a\ ->\ MyOpFunc(a)} + " set using a funcref variable + let Fn = function('MyTagFunc') + let &tagfunc = string(Fn) + " set using a lambda expression + let &tagfunc = "{t -> MyTagFunc(t)}" + " set using a variable with lambda expression + let L = {a, b, c -> MyTagFunc(a, b , c)} + let &tagfunc = string(L) < Setting the filetype diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 9f4c02da19..7ff4735480 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -2239,10 +2239,10 @@ static char_u *get_complete_funcname(int type) } } -/// Execute user defined complete function 'completefunc' or 'omnifunc', and -/// get matches in "matches". +/// Execute user defined complete function 'completefunc', 'omnifunc' or +/// 'thesaurusfunc', and get matches in "matches". /// -/// @param type CTRL_X_OMNI or CTRL_X_FUNCTION +/// @param type either CTRL_X_OMNI or CTRL_X_FUNCTION or CTRL_X_THESAURUS static void expand_by_function(int type, char_u *base) { list_T *matchlist = NULL; diff --git a/src/nvim/testdir/test_tagfunc.vim b/src/nvim/testdir/test_tagfunc.vim index 92c2f2d6a1..29ca69278d 100644 --- a/src/nvim/testdir/test_tagfunc.vim +++ b/src/nvim/testdir/test_tagfunc.vim @@ -124,32 +124,73 @@ func Test_tagfunc_callback() let g:MytagFunc1_args = [a:pat, a:flags, a:info] return v:null endfunc - let g:MytagFunc1_args = [] set tagfunc=function('MytagFunc1') - call assert_fails('tag abc', 'E433:') - call assert_equal(['abc', '', {}], g:MytagFunc1_args) + new | only + let g:MytagFunc1_args = [] + call assert_fails('tag a11', 'E433:') + call assert_equal(['a11', '', {}], g:MytagFunc1_args) + + " Using a funcref variable to set 'tagfunc' + let Fn = function('MytagFunc1') + let &tagfunc = string(Fn) + new | only + let g:MytagFunc1_args = [] + call assert_fails('tag a12', 'E433:') + call assert_equal(['a12', '', {}], g:MytagFunc1_args) + call assert_fails('let &tagfunc = Fn', 'E729:') " Test for using a funcref() - new func MytagFunc2(pat, flags, info) let g:MytagFunc2_args = [a:pat, a:flags, a:info] return v:null endfunc - let g:MytagFunc2_args = [] set tagfunc=funcref('MytagFunc2') - call assert_fails('tag def', 'E433:') - call assert_equal(['def', '', {}], g:MytagFunc2_args) + new | only + let g:MytagFunc2_args = [] + call assert_fails('tag a13', 'E433:') + call assert_equal(['a13', '', {}], g:MytagFunc2_args) + + " Using a funcref variable to set 'tagfunc' + let Fn = funcref('MytagFunc2') + let &tagfunc = string(Fn) + new | only + let g:MytagFunc2_args = [] + call assert_fails('tag a14', 'E433:') + call assert_equal(['a14', '', {}], g:MytagFunc2_args) + call assert_fails('let &tagfunc = Fn', 'E729:') " Test for using a lambda function - new func MytagFunc3(pat, flags, info) let g:MytagFunc3_args = [a:pat, a:flags, a:info] return v:null endfunc + set tagfunc={a,\ b,\ c\ ->\ MytagFunc3(a,\ b,\ c)} + new | only let g:MytagFunc3_args = [] - let &tagfunc= '{a, b, c -> MytagFunc3(a, b, c)}' - call assert_fails('tag ghi', 'E433:') - call assert_equal(['ghi', '', {}], g:MytagFunc3_args) + call assert_fails('tag a15', 'E433:') + call assert_equal(['a15', '', {}], g:MytagFunc3_args) + + " Set 'tagfunc' to a lambda expression + let &tagfunc = '{a, b, c -> MytagFunc3(a, b, c)}' + new | only + let g:MytagFunc3_args = [] + call assert_fails('tag a16', 'E433:') + call assert_equal(['a16', '', {}], g:MytagFunc3_args) + + " Set 'tagfunc' to a variable with a lambda expression + let Lambda = {a, b, c -> MytagFunc3(a, b, c)} + let &tagfunc = string(Lambda) + new | only + let g:MytagFunc3_args = [] + call assert_fails("tag a17", "E433:") + call assert_equal(['a17', '', {}], g:MytagFunc3_args) + call assert_fails('let &tagfunc = Lambda', 'E729:') + + " Test for using a lambda function with incorrect return value + let Lambda = {s -> strlen(s)} + let &tagfunc = string(Lambda) + new | only + call assert_fails("tag a17", "E987:") " Test for clearing the 'tagfunc' option set tagfunc='' @@ -160,10 +201,54 @@ func Test_tagfunc_callback() let &tagfunc = "{a -> 'abc'}" call assert_fails("echo taglist('a')", "E987:") + " Vim9 tests + let lines =<< trim END + vim9script + + # Test for using function() + def MytagFunc1(pat: string, flags: string, info: dict): any + g:MytagFunc1_args = [pat, flags, info] + return null + enddef + set tagfunc=function('MytagFunc1') + new | only + g:MytagFunc1_args = [] + assert_fails('tag a10', 'E433:') + assert_equal(['a10', '', {}], g:MytagFunc1_args) + + # Test for using a lambda + def MytagFunc2(pat: string, flags: string, info: dict): any + g:MytagFunc2_args = [pat, flags, info] + return null + enddef + &tagfunc = '(a, b, c) => MytagFunc2(a, b, c)' + new | only + g:MytagFunc2_args = [] + assert_fails('tag a20', 'E433:') + assert_equal(['a20', '', {}], g:MytagFunc2_args) + + # Test for using a variable with a lambda expression + var Fn: func = (a, b, c) => MytagFunc2(a, b, c) + &tagfunc = string(Fn) + new | only + g:MytagFunc2_args = [] + assert_fails('tag a30', 'E433:') + assert_equal(['a30', '', {}], g:MytagFunc2_args) + END + " call CheckScriptSuccess(lines) + + " Using Vim9 lambda expression in legacy context should fail + " set tagfunc=(a,\ b,\ c)\ =>\ g:MytagFunc2(a,\ b,\ c) + " new | only + " let g:MytagFunc3_args = [] + " call assert_fails("tag a17", "E117:") + " call assert_equal([], g:MytagFunc3_args) + " cleanup delfunc MytagFunc1 delfunc MytagFunc2 delfunc MytagFunc3 + set tagfunc& %bw! endfunc -- cgit From 1e4adf4b56cb7a360d16c4d04290c50ae7e80fbc Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 20 Aug 2022 08:45:15 +0800 Subject: vim-patch:8.2.3725: cannot use a lambda for 'completefunc' and 'omnifunc' Problem: Cannot use a lambda for 'completefunc' and 'omnifunc'. Solution: Implement lambda support. (Yegappan Lakshmanan, closes vim/vim#9257) https://github.com/vim/vim/commit/8658c759f05b317707d56e3b65a5ef63930c7498 Comment out Vim9 script in tests. Co-authored-by: Yegappan Lakshmanan --- runtime/doc/options.txt | 14 +- src/nvim/buffer.c | 3 + src/nvim/buffer_defs.h | 3 + src/nvim/eval.c | 19 +- src/nvim/eval/userfunc.c | 18 ++ src/nvim/insexpand.c | 100 ++++++- src/nvim/option.c | 5 +- src/nvim/optionstr.c | 12 + src/nvim/tag.c | 2 +- src/nvim/testdir/test_ins_complete.vim | 499 +++++++++++++++++++++++++++++++++ src/nvim/testdir/test_tagfunc.vim | 35 ++- 11 files changed, 677 insertions(+), 33 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index a14dc1b99e..37f8ea4b20 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -314,8 +314,8 @@ Note: In the future more global options can be made |global-local|. Using *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: +'operatorfunc', 'quickfixtextfunc', 'tagfunc' and 'thesaurusfunc') are set to +a function name or a function reference or a lambda function. Examples: > set opfunc=MyOpFunc set opfunc=function('MyOpFunc') @@ -1454,7 +1454,9 @@ A jump table for the options with a short description can be found at |Q_op|. This option specifies a function to be used for Insert mode completion with CTRL-X CTRL-U. |i_CTRL-X_CTRL-U| See |complete-functions| for an explanation of how the function is - invoked and what it should return. + invoked and what it should return. 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. @@ -4421,7 +4423,9 @@ A jump table for the options with a short description can be found at |Q_op|. This option specifies a function to be used for Insert mode omni completion with CTRL-X CTRL-O. |i_CTRL-X_CTRL-O| See |complete-functions| for an explanation of how the function is - invoked and what it should return. + invoked and what it should return. The value can be the name of a + function, a |lambda| or a |Funcref|. See |option-value-function| for + more information. This option is usually set by a filetype plugin: |:filetype-plugin-on| This option cannot be set from a |modeline| or in the |sandbox|, for @@ -6576,6 +6580,8 @@ A jump table for the options with a short description can be found at |Q_op|. global or local to buffer |global-local| This option specifies a function to be used for thesaurus completion with CTRL-X CTRL-T. |i_CTRL-X_CTRL-T| See |compl-thesaurusfunc|. + 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/buffer.c b/src/nvim/buffer.c index 4c8faccaa7..2c87677925 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -1971,8 +1971,11 @@ void free_buf_options(buf_T *buf, int free_p_ff) clear_string_option(&buf->b_p_cinw); clear_string_option(&buf->b_p_cpt); clear_string_option(&buf->b_p_cfu); + callback_free(&buf->b_cfu_cb); clear_string_option(&buf->b_p_ofu); + callback_free(&buf->b_ofu_cb); clear_string_option(&buf->b_p_tsrfu); + callback_free(&buf->b_tsrfu_cb); clear_string_option(&buf->b_p_gp); clear_string_option(&buf->b_p_mp); clear_string_option(&buf->b_p_efm); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index d8e86a75cf..6448c6b6f6 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -675,7 +675,9 @@ struct file_buffer { char *b_p_csl; ///< 'completeslash' #endif char *b_p_cfu; ///< 'completefunc' + Callback b_cfu_cb; ///< 'completefunc' callback char *b_p_ofu; ///< 'omnifunc' + Callback b_ofu_cb; ///< 'omnifunc' callback char *b_p_tfu; ///< 'tagfunc' Callback b_tfu_cb; ///< 'tagfunc' callback int b_p_eof; ///< 'endoffile' @@ -749,6 +751,7 @@ struct file_buffer { char *b_p_dict; ///< 'dictionary' local value char *b_p_tsr; ///< 'thesaurus' local value char *b_p_tsrfu; ///< 'thesaurusfunc' local value + Callback b_tsrfu_cb; ///< 'thesaurusfunc' callback long b_p_ul; ///< 'undolevels' local value int b_p_udf; ///< 'undofile' char *b_p_lw; ///< 'lispwords' local value diff --git a/src/nvim/eval.c b/src/nvim/eval.c index c983388450..c578d9fd39 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -1110,25 +1110,7 @@ fail: return ret; } -/// Call Vim script function and return the result as a number -/// -/// @param[in] func Function name. -/// @param[in] argc Number of arguments. -/// @param[in] argv Array with typval_T arguments. -/// -/// @return -1 when calling function fails, result of function otherwise. -varnumber_T call_func_retnr(const char *func, int argc, typval_T *argv) - FUNC_ATTR_NONNULL_ALL -{ - typval_T rettv; - if (call_vim_function((char *)func, argc, argv, &rettv) == FAIL) { - return -1; - } - varnumber_T retval = tv_get_number_chk(&rettv, NULL); - tv_clear(&rettv); - return retval; -} /// Call Vim script function and return the result as a string /// /// @param[in] func Function name. @@ -1151,6 +1133,7 @@ char *call_func_retstr(const char *const func, int argc, typval_T *argv) tv_clear(&rettv); return retval; } + /// Call Vim script function and return the result as a List /// /// @param[in] func Function name. diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index e6b038a335..d07cfe0bd9 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -1393,6 +1393,24 @@ func_call_skip_call: return r; } +/// call the 'callback' function and return the result as a number. +/// Returns -1 when calling the function fails. Uses argv[0] to argv[argc - 1] +/// for the function arguments. argv[argc] should have type VAR_UNKNOWN. +/// +/// @param argcount number of "argvars" +/// @param argvars vars for arguments, must have "argcount" PLUS ONE elements! +varnumber_T callback_call_retnr(Callback *callback, int argcount, typval_T *argvars) +{ + typval_T rettv; + if (!callback_call(callback, argcount, argvars, &rettv)) { + return -1; + } + + varnumber_T retval = tv_get_number_chk(&rettv, NULL); + tv_clear(&rettv); + return retval; +} + /// Give an error message for the result of a function. /// Nothing if "error" is FCERR_NONE. static void user_func_error(int error, const char_u *name) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 7ff4735480..1acdddedef 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -18,6 +18,7 @@ #include "nvim/edit.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" +#include "nvim/eval/userfunc.h" #include "nvim/ex_docmd.h" #include "nvim/ex_eval.h" #include "nvim/ex_getln.h" @@ -2224,6 +2225,82 @@ static buf_T *ins_compl_next_buf(buf_T *buf, int flag) return buf; } +static Callback cfu_cb; ///< 'completefunc' callback function +static Callback ofu_cb; ///< 'omnifunc' callback function +static Callback tsrfu_cb; ///< 'thesaurusfunc' callback function + +/// Copy a global callback function to a buffer local callback. +static void copy_global_to_buflocal_cb(Callback *globcb, Callback *bufcb) +{ + callback_free(bufcb); + if (globcb->data.funcref != NULL && *globcb->data.funcref != NUL) { + callback_copy(bufcb, globcb); + } +} + +/// Parse the 'completefunc' option value and set the callback function. +/// Invoked when the 'completefunc' option is set. The option value can be a +/// name of a function (string), or function() or funcref() or a +/// lambda expression. +int set_completefunc_option(void) +{ + int retval = option_set_callback_func(curbuf->b_p_cfu, &cfu_cb); + if (retval == OK) { + set_buflocal_cfu_callback(curbuf); + } + + return retval; +} + +/// Copy the global 'completefunc' callback function to the buffer-local +/// 'completefunc' callback for "buf". +void set_buflocal_cfu_callback(buf_T *buf) +{ + copy_global_to_buflocal_cb(&cfu_cb, &buf->b_cfu_cb); +} + +/// Parse the 'omnifunc' option value and set the callback function. +/// Invoked when the 'omnifunc' option is set. The option value can be a +/// name of a function (string), or function() or funcref() or a +/// lambda expression. +int set_omnifunc_option(void) +{ + int retval = option_set_callback_func(curbuf->b_p_ofu, &ofu_cb); + if (retval == OK) { + set_buflocal_ofu_callback(curbuf); + } + + return retval; +} + +/// Copy the global 'omnifunc' callback function to the buffer-local 'omnifunc' +/// callback for "buf". +void set_buflocal_ofu_callback(buf_T *buf) +{ + copy_global_to_buflocal_cb(&ofu_cb, &buf->b_ofu_cb); +} + +/// Parse the 'thesaurusfunc' option value and set the callback function. +/// Invoked when the 'thesaurusfunc' option is set. The option value can be a +/// name of a function (string), or function() or funcref() or a +/// lambda expression. +int set_thesaurusfunc_option(void) +{ + int retval; + + if (*curbuf->b_p_tsrfu != NUL) { + // buffer-local option set + callback_free(&curbuf->b_tsrfu_cb); + retval = option_set_callback_func(curbuf->b_p_tsrfu, &curbuf->b_tsrfu_cb); + } else { + // global option set + callback_free(&tsrfu_cb); + retval = option_set_callback_func(p_tsrfu, &tsrfu_cb); + } + + return retval; +} + /// Get the user-defined completion function name for completion "type" static char_u *get_complete_funcname(int type) { @@ -2239,6 +2316,19 @@ static char_u *get_complete_funcname(int type) } } +/// Get the callback to use for insert mode completion. +static Callback *get_insert_callback(int type) +{ + if (type == CTRL_X_FUNCTION) { + return &curbuf->b_cfu_cb; + } + if (type == CTRL_X_OMNI) { + return &curbuf->b_ofu_cb; + } + // CTRL_X_THESAURUS + return (*curbuf->b_p_tsrfu != NUL) ? &curbuf->b_tsrfu_cb : &tsrfu_cb; +} + /// Execute user defined complete function 'completefunc', 'omnifunc' or /// 'thesaurusfunc', and get matches in "matches". /// @@ -2272,8 +2362,10 @@ static void expand_by_function(int type, char_u *base) // Insert mode in another buffer. textlock++; + Callback *cb = get_insert_callback(type); + // Call a function, which returns a list or dict. - if (call_vim_function((char *)funcname, 2, args, &rettv) == OK) { + if (callback_call(cb, 2, args, &rettv)) { switch (rettv.v_type) { case VAR_LIST: matchlist = rettv.vval.v_list; @@ -3851,7 +3943,8 @@ static int get_userdefined_compl_info(colnr_T curs_col) pos_T pos = curwin->w_cursor; textlock++; - colnr_T col = (colnr_T)call_func_retnr((char *)funcname, 2, args); + Callback *cb = get_insert_callback(ctrl_x_mode); + colnr_T col = (colnr_T)callback_call_retnr(cb, 2, args); textlock--; State = save_State; @@ -4354,6 +4447,9 @@ static unsigned quote_meta(char_u *dest, char_u *src, int len) void free_insexpand_stuff(void) { XFREE_CLEAR(compl_orig_text); + callback_free(&cfu_cb); + callback_free(&ofu_cb); + callback_free(&tsrfu_cb); } #endif diff --git a/src/nvim/option.c b/src/nvim/option.c index 1eabe8c540..da3fad0d61 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -51,6 +51,7 @@ #include "nvim/highlight_group.h" #include "nvim/indent.h" #include "nvim/indent_c.h" +#include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/locale.h" #include "nvim/macros.h" @@ -4381,11 +4382,13 @@ void buf_copy_options(buf_T *buf, int flags) #endif buf->b_p_cfu = xstrdup(p_cfu); COPY_OPT_SCTX(buf, BV_CFU); + set_buflocal_cfu_callback(buf); buf->b_p_ofu = xstrdup(p_ofu); COPY_OPT_SCTX(buf, BV_OFU); + set_buflocal_ofu_callback(buf); buf->b_p_tfu = xstrdup(p_tfu); COPY_OPT_SCTX(buf, BV_TFU); - buf_set_tfu_callback(buf); + set_buflocal_tfu_callback(buf); buf->b_p_sts = p_sts; COPY_OPT_SCTX(buf, BV_STS); buf->b_p_sts_nopaste = p_sts_nopaste; diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index abae338daa..b088a4c8c7 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -1473,6 +1473,18 @@ char *did_set_string_option(int opt_idx, char **varp, char *oldval, char *errbuf } } } + } else if (gvarp == &p_cfu) { // 'completefunc' + if (set_completefunc_option() == FAIL) { + errmsg = e_invarg; + } + } else if (gvarp == &p_ofu) { // 'omnifunc' + if (set_omnifunc_option() == FAIL) { + errmsg = e_invarg; + } + } else if (gvarp == &p_tsrfu) { // 'thesaurusfunc' + if (set_thesaurusfunc_option() == FAIL) { + errmsg = e_invarg; + } } else if (varp == &p_opfunc) { // 'operatorfunc' if (set_operatorfunc_option() == FAIL) { errmsg = e_invarg; diff --git a/src/nvim/tag.c b/src/nvim/tag.c index e38fbb7da9..d6e62cff6d 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -149,7 +149,7 @@ void free_tagfunc_option(void) /// Copy the global 'tagfunc' callback function to the buffer-local 'tagfunc' /// callback for 'buf'. -void buf_set_tfu_callback(buf_T *buf) +void set_buflocal_tfu_callback(buf_T *buf) { callback_free(&buf->b_tfu_cb); if (tfu_cb.data.funcref != NULL && *tfu_cb.data.funcref != NUL) { diff --git a/src/nvim/testdir/test_ins_complete.vim b/src/nvim/testdir/test_ins_complete.vim index f706322a85..fc612dcbe2 100644 --- a/src/nvim/testdir/test_ins_complete.vim +++ b/src/nvim/testdir/test_ins_complete.vim @@ -1285,6 +1285,505 @@ func Test_no_mapping_for_ctrl_x_key() bwipe! endfunc +" Test for different ways of setting the 'completefunc' option +func Test_completefunc_callback() + " Test for using a function() + func MycompleteFunc1(findstart, base) + call add(g:MycompleteFunc1_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set completefunc=function('MycompleteFunc1') + new | only + call setline(1, 'one') + let g:MycompleteFunc1_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'one']], g:MycompleteFunc1_args) + bw! + + " Using a funcref variable to set 'completefunc' + let Fn = function('MycompleteFunc1') + let &completefunc = string(Fn) + new | only + call setline(1, 'two') + let g:MycompleteFunc1_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'two']], g:MycompleteFunc1_args) + call assert_fails('let &completefunc = Fn', 'E729:') + bw! + + " Test for using a funcref() + func MycompleteFunc2(findstart, base) + call add(g:MycompleteFunc2_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set completefunc=funcref('MycompleteFunc2') + new | only + call setline(1, 'three') + let g:MycompleteFunc2_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'three']], g:MycompleteFunc2_args) + bw! + + " Using a funcref variable to set 'completefunc' + let Fn = funcref('MycompleteFunc2') + let &completefunc = string(Fn) + new | only + call setline(1, 'four') + let g:MycompleteFunc2_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'four']], g:MycompleteFunc2_args) + call assert_fails('let &completefunc = Fn', 'E729:') + bw! + + " Test for using a lambda function + func MycompleteFunc3(findstart, base) + call add(g:MycompleteFunc3_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set completefunc={a,\ b,\ ->\ MycompleteFunc3(a,\ b,)} + new | only + call setline(1, 'five') + let g:MycompleteFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'five']], g:MycompleteFunc3_args) + bw! + + " Set 'completefunc' to a lambda expression + let &completefunc = '{a, b -> MycompleteFunc3(a, b)}' + new | only + call setline(1, 'six') + let g:MycompleteFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'six']], g:MycompleteFunc3_args) + bw! + + " Set 'completefunc' to a variable with a lambda expression + let Lambda = {a, b -> MycompleteFunc3(a, b)} + let &completefunc = string(Lambda) + new | only + call setline(1, 'seven') + let g:MycompleteFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'seven']], g:MycompleteFunc3_args) + call assert_fails('let &completefunc = Lambda', 'E729:') + bw! + + " Test for using a lambda function with incorrect return value + let Lambda = {s -> strlen(s)} + let &completefunc = string(Lambda) + new | only + call setline(1, 'eight') + call feedkeys("A\\\", 'x') + bw! + + " Test for clearing the 'completefunc' option + set completefunc='' + set completefunc& + + call assert_fails("set completefunc=function('abc')", "E700:") + call assert_fails("set completefunc=funcref('abc')", "E700:") + let &completefunc = "{a -> 'abc'}" + call feedkeys("A\\\", 'x') + + " Vim9 tests + let lines =<< trim END + vim9script + + # Test for using function() + def MycompleteFunc1(findstart: number, base: string): any + add(g:MycompleteFunc1_args, [findstart, base]) + return findstart ? 0 : [] + enddef + set completefunc=function('MycompleteFunc1') + new | only + setline(1, 'one') + g:MycompleteFunc1_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'one']], g:MycompleteFunc1_args) + bw! + + # Test for using a lambda + def MycompleteFunc2(findstart: number, base: string): any + add(g:MycompleteFunc2_args, [findstart, base]) + return findstart ? 0 : [] + enddef + &completefunc = '(a, b) => MycompleteFunc2(a, b)' + new | only + setline(1, 'two') + g:MycompleteFunc2_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'two']], g:MycompleteFunc2_args) + bw! + + # Test for using a variable with a lambda expression + var Fn: func = (a, b) => MycompleteFunc2(a, b) + &completefunc = string(Fn) + new | only + setline(1, 'three') + g:MycompleteFunc2_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'three']], g:MycompleteFunc2_args) + bw! + END + " call CheckScriptSuccess(lines) + + " Using Vim9 lambda expression in legacy context should fail + " set completefunc=(a,\ b)\ =>\ g:MycompleteFunc2(a,\ b) + " new | only + " let g:MycompleteFunc2_args = [] + " call assert_fails('call feedkeys("A\\\", "x")', 'E117:') + " call assert_equal([], g:MycompleteFunc2_args) + + " cleanup + delfunc MycompleteFunc1 + delfunc MycompleteFunc2 + delfunc MycompleteFunc3 + set completefunc& + %bw! +endfunc + +" Test for different ways of setting the 'omnifunc' option +func Test_omnifunc_callback() + " Test for using a function() + func MyomniFunc1(findstart, base) + call add(g:MyomniFunc1_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set omnifunc=function('MyomniFunc1') + new | only + call setline(1, 'one') + let g:MyomniFunc1_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'one']], g:MyomniFunc1_args) + bw! + + " Using a funcref variable to set 'omnifunc' + let Fn = function('MyomniFunc1') + let &omnifunc = string(Fn) + new | only + call setline(1, 'two') + let g:MyomniFunc1_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'two']], g:MyomniFunc1_args) + call assert_fails('let &omnifunc = Fn', 'E729:') + bw! + + " Test for using a funcref() + func MyomniFunc2(findstart, base) + call add(g:MyomniFunc2_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set omnifunc=funcref('MyomniFunc2') + new | only + call setline(1, 'three') + let g:MyomniFunc2_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'three']], g:MyomniFunc2_args) + bw! + + " Using a funcref variable to set 'omnifunc' + let Fn = funcref('MyomniFunc2') + let &omnifunc = string(Fn) + new | only + call setline(1, 'four') + let g:MyomniFunc2_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'four']], g:MyomniFunc2_args) + call assert_fails('let &omnifunc = Fn', 'E729:') + bw! + + " Test for using a lambda function + func MyomniFunc3(findstart, base) + call add(g:MyomniFunc3_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set omnifunc={a,\ b,\ ->\ MyomniFunc3(a,\ b,)} + new | only + call setline(1, 'five') + let g:MyomniFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'five']], g:MyomniFunc3_args) + bw! + + " Set 'omnifunc' to a lambda expression + let &omnifunc = '{a, b -> MyomniFunc3(a, b)}' + new | only + call setline(1, 'six') + let g:MyomniFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'six']], g:MyomniFunc3_args) + bw! + + " Set 'omnifunc' to a variable with a lambda expression + let Lambda = {a, b -> MyomniFunc3(a, b)} + let &omnifunc = string(Lambda) + new | only + call setline(1, 'seven') + let g:MyomniFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'seven']], g:MyomniFunc3_args) + call assert_fails('let &omnifunc = Lambda', 'E729:') + bw! + + " Test for using a lambda function with incorrect return value + let Lambda = {s -> strlen(s)} + let &omnifunc = string(Lambda) + new | only + call setline(1, 'eight') + call feedkeys("A\\\", 'x') + bw! + + " Test for clearing the 'omnifunc' option + set omnifunc='' + set omnifunc& + + call assert_fails("set omnifunc=function('abc')", "E700:") + call assert_fails("set omnifunc=funcref('abc')", "E700:") + let &omnifunc = "{a -> 'abc'}" + call feedkeys("A\\\", 'x') + + " Vim9 tests + let lines =<< trim END + vim9script + + # Test for using function() + def MyomniFunc1(findstart: number, base: string): any + add(g:MyomniFunc1_args, [findstart, base]) + return findstart ? 0 : [] + enddef + set omnifunc=function('MyomniFunc1') + new | only + setline(1, 'one') + g:MyomniFunc1_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'one']], g:MyomniFunc1_args) + bw! + + # Test for using a lambda + def MyomniFunc2(findstart: number, base: string): any + add(g:MyomniFunc2_args, [findstart, base]) + return findstart ? 0 : [] + enddef + &omnifunc = '(a, b) => MyomniFunc2(a, b)' + new | only + setline(1, 'two') + g:MyomniFunc2_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'two']], g:MyomniFunc2_args) + bw! + + # Test for using a variable with a lambda expression + var Fn: func = (a, b) => MyomniFunc2(a, b) + &omnifunc = string(Fn) + new | only + setline(1, 'three') + g:MyomniFunc2_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'three']], g:MyomniFunc2_args) + bw! + END + " call CheckScriptSuccess(lines) + + " Using Vim9 lambda expression in legacy context should fail + " set omnifunc=(a,\ b)\ =>\ g:MyomniFunc2(a,\ b) + " new | only + " let g:MyomniFunc2_args = [] + " call assert_fails('call feedkeys("A\\\", "x")', 'E117:') + " call assert_equal([], g:MyomniFunc2_args) + + " cleanup + delfunc MyomniFunc1 + delfunc MyomniFunc2 + delfunc MyomniFunc3 + set omnifunc& + %bw! +endfunc + +" Test for different ways of setting the 'thesaurusfunc' option +func Test_thesaurusfunc_callback() + " Test for using a function() + func MytsrFunc1(findstart, base) + call add(g:MytsrFunc1_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set thesaurusfunc=function('MytsrFunc1') + new | only + call setline(1, 'one') + let g:MytsrFunc1_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'one']], g:MytsrFunc1_args) + bw! + + " Using a funcref variable to set 'thesaurusfunc' + let Fn = function('MytsrFunc1') + let &thesaurusfunc = string(Fn) + new | only + call setline(1, 'two') + let g:MytsrFunc1_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'two']], g:MytsrFunc1_args) + call assert_fails('let &thesaurusfunc = Fn', 'E729:') + bw! + + " Test for using a funcref() + func MytsrFunc2(findstart, base) + call add(g:MytsrFunc2_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set thesaurusfunc=funcref('MytsrFunc2') + new | only + call setline(1, 'three') + let g:MytsrFunc2_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'three']], g:MytsrFunc2_args) + bw! + + " Using a funcref variable to set 'thesaurusfunc' + let Fn = funcref('MytsrFunc2') + let &thesaurusfunc = string(Fn) + new | only + call setline(1, 'four') + let g:MytsrFunc2_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'four']], g:MytsrFunc2_args) + call assert_fails('let &thesaurusfunc = Fn', 'E729:') + bw! + + " Test for using a lambda function + func MytsrFunc3(findstart, base) + call add(g:MytsrFunc3_args, [a:findstart, a:base]) + return a:findstart ? 0 : [] + endfunc + set thesaurusfunc={a,\ b,\ ->\ MytsrFunc3(a,\ b,)} + new | only + call setline(1, 'five') + let g:MytsrFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'five']], g:MytsrFunc3_args) + bw! + + " Set 'thesaurusfunc' to a lambda expression + let &thesaurusfunc = '{a, b -> MytsrFunc3(a, b)}' + new | only + call setline(1, 'six') + let g:MytsrFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'six']], g:MytsrFunc3_args) + bw! + + " Set 'thesaurusfunc' to a variable with a lambda expression + let Lambda = {a, b -> MytsrFunc3(a, b)} + let &thesaurusfunc = string(Lambda) + new | only + call setline(1, 'seven') + let g:MytsrFunc3_args = [] + call feedkeys("A\\\", 'x') + call assert_equal([[1, ''], [0, 'seven']], g:MytsrFunc3_args) + call assert_fails('let &thesaurusfunc = Lambda', 'E729:') + bw! + + " Test for using a lambda function with incorrect return value + let Lambda = {s -> strlen(s)} + let &thesaurusfunc = string(Lambda) + new | only + call setline(1, 'eight') + call feedkeys("A\\\", 'x') + bw! + + " Test for clearing the 'thesaurusfunc' option + set thesaurusfunc='' + set thesaurusfunc& + + call assert_fails("set thesaurusfunc=function('abc')", "E700:") + call assert_fails("set thesaurusfunc=funcref('abc')", "E700:") + let &thesaurusfunc = "{a -> 'abc'}" + call feedkeys("A\\\", 'x') + + " Vim9 tests + let lines =<< trim END + vim9script + + # Test for using function() + def MytsrFunc1(findstart: number, base: string): any + add(g:MytsrFunc1_args, [findstart, base]) + return findstart ? 0 : [] + enddef + set thesaurusfunc=function('MytsrFunc1') + new | only + setline(1, 'one') + g:MytsrFunc1_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'one']], g:MytsrFunc1_args) + bw! + + # Test for using a lambda + def MytsrFunc2(findstart: number, base: string): any + add(g:MytsrFunc2_args, [findstart, base]) + return findstart ? 0 : [] + enddef + &thesaurusfunc = '(a, b) => MytsrFunc2(a, b)' + new | only + setline(1, 'two') + g:MytsrFunc2_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'two']], g:MytsrFunc2_args) + bw! + + # Test for using a variable with a lambda expression + var Fn: func = (a, b) => MytsrFunc2(a, b) + &thesaurusfunc = string(Fn) + new | only + setline(1, 'three') + g:MytsrFunc2_args = [] + feedkeys("A\\\", 'x') + assert_equal([[1, ''], [0, 'three']], g:MytsrFunc2_args) + bw! + END + " call CheckScriptSuccess(lines) + + " Using Vim9 lambda expression in legacy context should fail + " set thesaurusfunc=(a,\ b)\ =>\ g:MytsrFunc2(a,\ b) + " new | only + " let g:MytsrFunc2_args = [] + " call assert_fails('call feedkeys("A\\\", "x")', 'E117:') + " call assert_equal([], g:MytsrFunc2_args) + " bw! + + " Use a buffer-local value and a global value + func MytsrFunc4(findstart, base) + call add(g:MytsrFunc4_args, [a:findstart, a:base]) + return a:findstart ? 0 : ['sunday'] + endfunc + set thesaurusfunc& + setlocal thesaurusfunc=function('MytsrFunc4') + call setline(1, 'sun') + let g:MytsrFunc4_args = [] + call feedkeys("A\\\", "x") + call assert_equal('sunday', getline(1)) + call assert_equal([[1, ''], [0, 'sun']], g:MytsrFunc4_args) + new + call setline(1, 'sun') + let g:MytsrFunc4_args = [] + call feedkeys("A\\\", "x") + call assert_equal('sun', getline(1)) + call assert_equal([], g:MytsrFunc4_args) + set thesaurusfunc=function('MytsrFunc1') + wincmd w + call setline(1, 'sun') + let g:MytsrFunc4_args = [] + call feedkeys("A\\\", "x") + call assert_equal('sunday', getline(1)) + call assert_equal([[1, ''], [0, 'sun']], g:MytsrFunc4_args) + + " cleanup + set thesaurusfunc& + delfunc MytsrFunc1 + delfunc MytsrFunc2 + delfunc MytsrFunc3 + delfunc MytsrFunc4 + %bw! +endfunc + func FooBarComplete(findstart, base) if a:findstart return col('.') - 1 diff --git a/src/nvim/testdir/test_tagfunc.vim b/src/nvim/testdir/test_tagfunc.vim index 29ca69278d..e3085b9395 100644 --- a/src/nvim/testdir/test_tagfunc.vim +++ b/src/nvim/testdir/test_tagfunc.vim @@ -117,6 +117,12 @@ func Test_tagfunc_settagstack() delfunc Mytagfunc2 endfunc +" Script local tagfunc callback function +func s:ScriptLocalTagFunc(pat, flags, info) + let g:ScriptLocalFuncArgs = [a:pat, a:flags, a:info] + return v:null +endfunc + " Test for different ways of setting the 'tagfunc' option func Test_tagfunc_callback() " Test for using a function() @@ -159,6 +165,21 @@ func Test_tagfunc_callback() call assert_equal(['a14', '', {}], g:MytagFunc2_args) call assert_fails('let &tagfunc = Fn', 'E729:') + " Test for using a script local function + set tagfunc=ScriptLocalTagFunc + new | only + let g:ScriptLocalFuncArgs = [] + call assert_fails('tag a15', 'E433:') + call assert_equal(['a15', '', {}], g:ScriptLocalFuncArgs) + + " Test for using a script local funcref variable + let Fn = function("s:ScriptLocalTagFunc") + let &tagfunc= string(Fn) + new | only + let g:ScriptLocalFuncArgs = [] + call assert_fails('tag a16', 'E433:') + call assert_equal(['a16', '', {}], g:ScriptLocalFuncArgs) + " Test for using a lambda function func MytagFunc3(pat, flags, info) let g:MytagFunc3_args = [a:pat, a:flags, a:info] @@ -167,30 +188,30 @@ func Test_tagfunc_callback() set tagfunc={a,\ b,\ c\ ->\ MytagFunc3(a,\ b,\ c)} new | only let g:MytagFunc3_args = [] - call assert_fails('tag a15', 'E433:') - call assert_equal(['a15', '', {}], g:MytagFunc3_args) + call assert_fails('tag a17', 'E433:') + call assert_equal(['a17', '', {}], g:MytagFunc3_args) " Set 'tagfunc' to a lambda expression let &tagfunc = '{a, b, c -> MytagFunc3(a, b, c)}' new | only let g:MytagFunc3_args = [] - call assert_fails('tag a16', 'E433:') - call assert_equal(['a16', '', {}], g:MytagFunc3_args) + call assert_fails('tag a18', 'E433:') + call assert_equal(['a18', '', {}], g:MytagFunc3_args) " Set 'tagfunc' to a variable with a lambda expression let Lambda = {a, b, c -> MytagFunc3(a, b, c)} let &tagfunc = string(Lambda) new | only let g:MytagFunc3_args = [] - call assert_fails("tag a17", "E433:") - call assert_equal(['a17', '', {}], g:MytagFunc3_args) + call assert_fails("tag a19", "E433:") + call assert_equal(['a19', '', {}], g:MytagFunc3_args) call assert_fails('let &tagfunc = Lambda', 'E729:') " Test for using a lambda function with incorrect return value let Lambda = {s -> strlen(s)} let &tagfunc = string(Lambda) new | only - call assert_fails("tag a17", "E987:") + call assert_fails("tag a20", "E987:") " Test for clearing the 'tagfunc' option set tagfunc='' -- cgit From 595f7f37a98f3af12fe94ba4332b8f33004b7e4b Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 5 Nov 2022 16:59:07 +0800 Subject: vim-patch:9.0.0246: using freed memory when 'tagfunc' deletes the buffer Problem: Using freed memory when 'tagfunc' deletes the buffer. Solution: Make a copy of the tag name. https://github.com/vim/vim/commit/adce965162dd89bf29ee0e5baf53652e7515762c Co-authored-by: Bram Moolenaar --- src/nvim/tag.c | 7 ++++++- src/nvim/testdir/test_tagfunc.vim | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/nvim/tag.c b/src/nvim/tag.c index d6e62cff6d..5216919d98 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -199,6 +199,7 @@ void do_tag(char *tag, int type, int count, int forceit, int verbose) int skip_msg = false; char_u *buf_ffname = (char_u *)curbuf->b_ffname; // name for priority computation int use_tfu = 1; + char *tofree = NULL; // remember the matches for the last used tag static int num_matches = 0; @@ -450,7 +451,10 @@ void do_tag(char *tag, int type, int count, int forceit, int verbose) // When desired match not found yet, try to find it (and others). if (use_tagstack) { - name = tagstack[tagstackidx].tagname; + // make a copy, the tagstack may change in 'tagfunc' + name = xstrdup(tagstack[tagstackidx].tagname); + xfree(tofree); + tofree = name; } else if (g_do_tagpreview != 0) { name = ptag_entry.tagname; } else { @@ -681,6 +685,7 @@ end_do_tag: } postponed_split = 0; // don't split next time g_do_tagpreview = 0; // don't do tag preview next time + xfree(tofree); } // List all the matching tags. diff --git a/src/nvim/testdir/test_tagfunc.vim b/src/nvim/testdir/test_tagfunc.vim index e3085b9395..ad761f1b23 100644 --- a/src/nvim/testdir/test_tagfunc.vim +++ b/src/nvim/testdir/test_tagfunc.vim @@ -273,4 +273,16 @@ func Test_tagfunc_callback() %bw! endfunc +func Test_tagfunc_wipes_buffer() + func g:Tag0unc0(t,f,o) + bwipe + endfunc + set tagfunc=g:Tag0unc0 + new + cal assert_fails('tag 0', 'E987:') + + delfunc g:Tag0unc0 + set tagfunc= +endfunc + " vim: shiftwidth=2 sts=2 expandtab -- cgit From 84881674fd702cad5b7572ac868f6d40965a2806 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Sat, 5 Nov 2022 17:01:39 +0800 Subject: vim-patch:9.0.0389: crash when 'tagfunc' closes the window Problem: Crash when 'tagfunc' closes the window. Solution: Bail out when the window was closed. https://github.com/vim/vim/commit/ccfde4d028e891a41e3548323c3d47b06fb0b83e Add docs for E1299 from Vim runtime. Co-authored-by: Bram Moolenaar --- runtime/doc/tagsrch.txt | 2 ++ src/nvim/tag.c | 11 +++++++++++ src/nvim/testdir/test_tagfunc.vim | 13 +++++++++++++ 3 files changed, 26 insertions(+) diff --git a/runtime/doc/tagsrch.txt b/runtime/doc/tagsrch.txt index d079db0717..aab6f78315 100644 --- a/runtime/doc/tagsrch.txt +++ b/runtime/doc/tagsrch.txt @@ -909,6 +909,8 @@ If the function returns |v:null| instead of a List, a standard tag lookup will be performed instead. It is not allowed to change the tagstack from inside 'tagfunc'. *E986* +It is not allowed to close a window or change window from inside 'tagfunc'. +*E1299* The following is a hypothetical example of a function used for 'tagfunc'. It uses the output of |taglist()| to generate the result: a list of tags in the diff --git a/src/nvim/tag.c b/src/nvim/tag.c index 5216919d98..16d6629c2e 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -106,6 +106,8 @@ static char_u *recurmsg = (char_u *)N_("E986: cannot modify the tag stack within tagfunc"); static char_u *tfu_inv_ret_msg = (char_u *)N_("E987: invalid return value from tagfunc"); +static char e_window_unexpectedly_close_while_searching_for_tags[] + = N_("E1299: Window unexpectedly closed while searching for tags"); static char *tagmatchname = NULL; // name of last used tag @@ -501,6 +503,15 @@ void do_tag(char *tag, int type, int count, int forceit, int verbose) // found: all matches found. } + // A tag function may do anything, which may cause various + // information to become invalid. At least check for the tagstack + // to still be the same. + if (tagstack != curwin->w_tagstack) { + emsg(_(e_window_unexpectedly_close_while_searching_for_tags)); + FreeWild(new_num_matches, new_matches); + break; + } + // If there already were some matches for the same name, move them // to the start. Avoids that the order changes when using // ":tnext" and jumping to another file. diff --git a/src/nvim/testdir/test_tagfunc.vim b/src/nvim/testdir/test_tagfunc.vim index ad761f1b23..8690e30e77 100644 --- a/src/nvim/testdir/test_tagfunc.vim +++ b/src/nvim/testdir/test_tagfunc.vim @@ -285,4 +285,17 @@ func Test_tagfunc_wipes_buffer() set tagfunc= endfunc +func Test_tagfunc_closes_window() + split any + func MytagfuncClose(pat, flags, info) + close + return [{'name' : 'mytag', 'filename' : 'Xtest', 'cmd' : '1'}] + endfunc + set tagfunc=MytagfuncClose + call assert_fails('tag xyz', 'E1299:') + + set tagfunc= +endfunc + + " vim: shiftwidth=2 sts=2 expandtab -- cgit