neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit b1e35cbd7b32d4163131bdd1d828db9ecb19fd62
parent 196802900323f872ff68facc61bcfec01e476613
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Sat,  9 Aug 2025 09:48:30 +0800

vim-patch:9.1.1603: completion: cannot use autoloaded funcs in 'complete' F{func}

Problem:  completion: cannot use autoloaded funcs in 'complete' F{func}
          (Maxim Kim)
Solution: Make it work (Girish Palya)

fixes: vim/vim#17869
closes: vim/vim#17885

https://github.com/vim/vim/commit/1bfe86a7d37ea849078255f16f99ed69fbf205dc

Cherry-pick Test_omni_autoload() from patch 8.2.3223.

Co-authored-by: Girish Palya <girishji@gmail.com>

Diffstat:
Msrc/nvim/buffer.c | 3+++
Msrc/nvim/buffer_defs.h | 2++
Msrc/nvim/eval.c | 3+++
Msrc/nvim/insexpand.c | 198+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/nvim/option.c | 1+
Msrc/nvim/optionstr.c | 18+++++++++++++++---
Mtest/old/testdir/test_ins_complete.vim | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/old/testdir/test_options.vim | 12+++++++++---
8 files changed, 279 insertions(+), 45 deletions(-)

diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c @@ -68,6 +68,7 @@ #include "nvim/help.h" #include "nvim/indent.h" #include "nvim/indent_c.h" +#include "nvim/insexpand.h" #include "nvim/main.h" #include "nvim/map_defs.h" #include "nvim/mapping.h" @@ -2104,6 +2105,8 @@ void free_buf_options(buf_T *buf, bool free_p_ff) callback_free(&buf->b_ofu_cb); clear_string_option(&buf->b_p_tsrfu); callback_free(&buf->b_tsrfu_cb); + clear_cpt_callbacks(&buf->b_p_cpt_cb, buf->b_p_cpt_count); + buf->b_p_cpt_count = 0; clear_string_option(&buf->b_p_gefm); clear_string_option(&buf->b_p_gp); clear_string_option(&buf->b_p_mp); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h @@ -540,6 +540,8 @@ struct file_buffer { #ifdef BACKSLASH_IN_FILENAME char *b_p_csl; ///< 'completeslash' #endif + Callback *b_p_cpt_cb; ///< F{func} in 'complete' callback + int b_p_cpt_count; ///< Count of values in 'complete' char *b_p_cfu; ///< 'completefunc' Callback b_cfu_cb; ///< 'completefunc' callback char *b_p_ofu; ///< 'omnifunc' diff --git a/src/nvim/eval.c b/src/nvim/eval.c @@ -4795,6 +4795,9 @@ bool garbage_collect(bool testing) ABORTING(set_ref_in_callback)(&buf->b_tsrfu_cb, copyID, NULL, NULL); ABORTING(set_ref_in_callback)(&buf->b_tfu_cb, copyID, NULL, NULL); ABORTING(set_ref_in_callback)(&buf->b_ffu_cb, copyID, NULL, NULL); + if (!abort && buf->b_p_cpt_cb != NULL) { + ABORTING(set_ref_in_cpt_callbacks)(buf->b_p_cpt_cb, buf->b_p_cpt_count, copyID); + } } // 'completefunc', 'omnifunc' and 'thesaurusfunc' callbacks diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c @@ -2821,9 +2821,31 @@ static buf_T *ins_compl_next_buf(buf_T *buf, int flag) return buf; } +/// Count the number of entries in the 'complete' option (curbuf->b_p_cpt). +/// Each non-empty, comma-separated segment is counted as one entry. +static int get_cpt_sources_count(void) +{ + char dummy[LSIZE]; + int count = 0; + + for (char *p = curbuf->b_p_cpt; *p != NUL;) { + while (*p == ',' || *p == ' ') { + p++; // Skip delimiters + } + if (*p != NUL) { + (void)copy_option_part(&p, dummy, LSIZE, ","); // Advance p + count++; + } + } + + return count; +} + static Callback cfu_cb; ///< 'completefunc' callback function static Callback ofu_cb; ///< 'omnifunc' callback function static Callback tsrfu_cb; ///< 'thesaurusfunc' callback function +static Callback *cpt_cb; ///< Callback functions associated with F{func} +static int cpt_cb_count; ///< Number of cpt callbacks /// Copy a global callback function to a buffer local callback. static void copy_global_to_buflocal_cb(Callback *globcb, Callback *bufcb) @@ -2876,6 +2898,101 @@ void set_buflocal_ofu_callback(buf_T *buf) copy_global_to_buflocal_cb(&ofu_cb, &buf->b_ofu_cb); } +/// Free an array of 'complete' F{func} callbacks and set the pointer to NULL. +void clear_cpt_callbacks(Callback **callbacks, int count) +{ + if (callbacks == NULL || *callbacks == NULL) { + return; + } + + for (int i = 0; i < count; i++) { + callback_free(&(*callbacks)[i]); + } + + XFREE_CLEAR(*callbacks); +} + +/// Copies a list of Callback structs from src to *dest, clearing any existing +/// entries and allocating memory for the destination. +static void copy_cpt_callbacks(Callback **dest, int *dest_cnt, Callback *src, int cnt) +{ + if (cnt == 0) { + return; + } + + clear_cpt_callbacks(dest, *dest_cnt); + *dest = xcalloc((size_t)cnt, sizeof(Callback)); + *dest_cnt = cnt; + + for (int i = 0; i < cnt; i++) { + if (src[i].type != kCallbackNone) { + callback_copy(&(*dest)[i], &src[i]); + } + } +} + +/// Copy global 'complete' F{func} callbacks into the given buffer's local +/// callback array. Clears any existing buffer-local callbacks first. +void set_buflocal_cpt_callbacks(buf_T *buf) +{ + if (buf == NULL || cpt_cb_count == 0) { + return; + } + copy_cpt_callbacks(&buf->b_p_cpt_cb, &buf->b_p_cpt_count, cpt_cb, cpt_cb_count); +} + +/// Parse 'complete' option and initialize F{func} callbacks. +/// Frees any existing callbacks and allocates new ones. +/// Only F{func} entries are processed; others are ignored. +int set_cpt_callbacks(optset_T *args) +{ + bool local = (args->os_flags & OPT_LOCAL) != 0; + + if (curbuf == NULL) { + return FAIL; + } + + clear_cpt_callbacks(&curbuf->b_p_cpt_cb, curbuf->b_p_cpt_count); + curbuf->b_p_cpt_count = 0; + + int count = get_cpt_sources_count(); + if (count == 0) { + return OK; + } + + curbuf->b_p_cpt_cb = xcalloc((size_t)count, sizeof(Callback)); + curbuf->b_p_cpt_count = count; + + char buf[LSIZE]; + int idx = 0; + for (char *p = curbuf->b_p_cpt; *p != NUL;) { + while (*p == ',' || *p == ' ') { + p++; // Skip delimiters + } + if (*p != NUL) { + size_t slen = copy_option_part(&p, buf, LSIZE, ","); // Advance p + if (slen > 0 && buf[0] == 'F' && buf[1] != NUL) { + char *caret = vim_strchr(buf, '^'); + if (caret != NULL) { + *caret = NUL; + } + if (option_set_callback_func(buf + 1, &curbuf->b_p_cpt_cb[idx]) != OK) { + curbuf->b_p_cpt_cb[idx].type = kCallbackNone; + } + } + idx++; + } + } + + if (!local) { // ':set' used insted of ':setlocal' + // Cache the callback array + copy_cpt_callbacks(&cpt_cb, &cpt_cb_count, curbuf->b_p_cpt_cb, + curbuf->b_p_cpt_count); + } + + return OK; +} + /// 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(<name>) or funcref(<name>) or a @@ -2900,6 +3017,22 @@ const char *did_set_thesaurusfunc(optset_T *args FUNC_ATTR_UNUSED) return retval == FAIL ? e_invarg : NULL; } +/// Mark "copyID" references in an array of F{func} callbacks so that they are +/// not garbage collected. +bool set_ref_in_cpt_callbacks(Callback *callbacks, int count, int copyID) +{ + bool abort = false; + + if (callbacks == NULL) { + return false; + } + + for (int i = 0; i < count; i++) { + abort = abort || set_ref_in_callback(&callbacks[i], copyID, NULL, NULL); + } + return abort; +} + /// Mark the global 'completefunc' 'omnifunc' and 'thesaurusfunc' callbacks with /// "copyID" so that they are not garbage collected. bool set_ref_in_insexpand_funcs(int copyID) @@ -2907,6 +3040,7 @@ bool set_ref_in_insexpand_funcs(int copyID) bool abort = set_ref_in_callback(&cfu_cb, copyID, NULL, NULL); abort = abort || set_ref_in_callback(&ofu_cb, copyID, NULL, NULL); abort = abort || set_ref_in_callback(&tsrfu_cb, copyID, NULL, NULL); + abort = abort || set_ref_in_cpt_callbacks(cpt_cb, cpt_cb_count, copyID); return abort; } @@ -3663,7 +3797,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar // compl_type = -1; } else if (*st->e_cpt == 'F' || *st->e_cpt == 'o') { compl_type = CTRL_X_FUNCTION; - st->func_cb = get_callback_if_cpt_func(st->e_cpt); + st->func_cb = get_callback_if_cpt_func(st->e_cpt, cpt_sources_index); if (!st->func_cb) { compl_type = -1; } @@ -4326,28 +4460,25 @@ static void get_register_completion(void) } } -/// Return the callback function associated with "p" if it points to a -/// userfunc. -static Callback *get_callback_if_cpt_func(char *p) +/// Return the callback function associated with "p" if it refers to a +/// user-defined function in the 'complete' option. +/// The "idx" parameter is used for indexing callback entries. +static Callback *get_callback_if_cpt_func(char *p, int idx) { - static Callback cb; - char buf[LSIZE]; - if (*p == 'o') { return &curbuf->b_ofu_cb; } + if (*p == 'F') { if (*++p != ',' && *p != NUL) { - callback_free(&cb); - size_t slen = copy_option_part(&p, buf, LSIZE, ","); - if (slen > 0 && option_set_callback_func(buf, &cb)) { - return &cb; - } - return NULL; + // 'F{func}' case + return curbuf->b_p_cpt_cb[idx].type != kCallbackNone + ? &curbuf->b_p_cpt_cb[idx] : NULL; } else { - return &curbuf->b_cfu_cb; + return &curbuf->b_cfu_cb; // 'cfu' } } + return NULL; } @@ -4480,7 +4611,7 @@ static void prepare_cpt_compl_funcs(void) break; } - Callback *cb = get_callback_if_cpt_func(p); + Callback *cb = get_callback_if_cpt_func(p, idx); if (cb) { int startcol; if (get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol) == FAIL) { @@ -6050,6 +6181,7 @@ void free_insexpand_stuff(void) callback_free(&cfu_cb); callback_free(&ofu_cb); callback_free(&tsrfu_cb); + clear_cpt_callbacks(&cpt_cb, cpt_cb_count); } #endif @@ -6075,47 +6207,35 @@ static void cpt_sources_clear(void) /// Setup completion sources. static void setup_cpt_sources(void) { - char buf[LSIZE]; - - // Make a copy of 'cpt' in case the buffer gets wiped out - char *cpt = xstrdup(curbuf->b_p_cpt); + cpt_sources_clear(); - int count = 0; - for (char *p = cpt; *p;) { - while (*p == ',' || *p == ' ') { // Skip delimiters - p++; - } - if (*p) { // If not end of string, count this segment - (void)copy_option_part(&p, buf, LSIZE, ","); // Advance p - count++; - } - } + int count = get_cpt_sources_count(); if (count == 0) { - goto theend; + return; } - cpt_sources_clear(); - cpt_sources_count = count; cpt_sources_array = xcalloc((size_t)count, sizeof(cpt_source_T)); + char buf[LSIZE]; int idx = 0; - for (char *p = cpt; *p;) { + for (char *p = curbuf->b_p_cpt; *p;) { while (*p == ',' || *p == ' ') { // Skip delimiters p++; } if (*p) { // If not end of string, count this segment memset(buf, 0, LSIZE); size_t slen = copy_option_part(&p, buf, LSIZE, ","); // Advance p - char *t; - if (slen > 0 && (t = vim_strchr(buf, '^')) != NULL) { - cpt_sources_array[idx].cs_max_matches = atoi(t + 1); + if (slen > 0) { + char *caret = vim_strchr(buf, '^'); + if (caret != NULL) { + cpt_sources_array[idx].cs_max_matches = atoi(caret + 1); + } } idx++; } } -theend: - xfree(cpt); + cpt_sources_count = count; } /// Return true if any of the completion sources have 'refresh' set to 'always'. @@ -6248,7 +6368,7 @@ static void cpt_compl_refresh(void) } if (cpt_sources_array[cpt_sources_index].cs_refresh_always) { - Callback *cb = get_callback_if_cpt_func(p); + Callback *cb = get_callback_if_cpt_func(p, cpt_sources_index); if (cb) { compl_curr_match = remove_old_matches(); int startcol; diff --git a/src/nvim/option.c b/src/nvim/option.c @@ -5109,6 +5109,7 @@ void buf_copy_options(buf_T *buf, int flags) } buf->b_p_cpt = xstrdup(p_cpt); COPY_OPT_SCTX(buf, kBufOptComplete); + set_buflocal_cpt_callbacks(buf); #ifdef BACKSLASH_IN_FILENAME buf->b_p_csl = xstrdup(p_csl); COPY_OPT_SCTX(buf, kBufOptCompleteslash); diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c @@ -118,6 +118,15 @@ char *illegal_char(char *errbuf, size_t errbuflen, int c) return errbuf; } +static char *illegal_char_after_chr(char *errbuf, size_t errbuflen, int c) +{ + if (errbuf == NULL) { + return ""; + } + vim_snprintf(errbuf, errbuflen, _(e_illegal_character_after_chr), c); + return errbuf; +} + /// Check string options in a buffer for NULL value. void check_buf_options(buf_T *buf) { @@ -902,9 +911,8 @@ const char *did_set_complete(optset_T *args) } if (char_before != NUL) { if (args->os_errbuf != NULL) { - vim_snprintf(args->os_errbuf, args->os_errbuflen, - _(e_illegal_character_after_chr), char_before); - return args->os_errbuf; + return illegal_char_after_chr(args->os_errbuf, args->os_errbuflen, + char_before); } return NULL; } @@ -913,6 +921,10 @@ const char *did_set_complete(optset_T *args) p++; } } + + if (set_cpt_callbacks(args) != OK) { + return illegal_char_after_chr(args->os_errbuf, args->os_errbuflen, 'F'); + } return NULL; } diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim @@ -178,6 +178,38 @@ func Test_omni_throw() set omnifunc= complete& endfunc +func Test_omni_autoload() + let save_rtp = &rtp + set rtp=Xruntime/some + let dir = 'Xruntime/some/autoload' + call mkdir(dir, 'pR') + + let lines =<< trim END + func omni#Func(findstart, base) + if a:findstart + return 1 + else + return ['match'] + endif + endfunc + + eval 1 + 2 + END + call writefile(lines, dir .. '/omni.vim') + + new + setlocal omnifunc=omni#Func + call feedkeys("i\<C-X>\<C-O>\<Esc>", 'xt') + + setlocal complete=.,Fomni#Func + call feedkeys("S\<C-N>\<Esc>", 'xt') + setlocal complete& + + bwipe! + set omnifunc= + let &rtp = save_rtp +endfunc + func Test_completefunc_args() let s:args = [] func! CompleteFunc(findstart, base) @@ -2773,6 +2805,15 @@ func Test_omnifunc_callback() call assert_equal([[1, ''], [0, 'script1']], g:OmniFunc3Args) bw! + set complete=Fs:OmniFunc3 + new + call setline(1, 'script1') + let g:OmniFunc3Args = [] + call feedkeys("A\<C-N>\<Esc>", 'x') + call assert_equal([[1, ''], [0, 'script1']], g:OmniFunc3Args) + bw! + set complete& + let &omnifunc = 's:OmniFunc3' new call setline(1, 'script2') @@ -5437,4 +5478,50 @@ func Test_autocomplete_timer() unlet g:CallCount endfunc +func s:TestCompleteScriptLocal(findstart, base) + if a:findstart + return 1 + else + return ['foo', 'foobar'] + endif +endfunc + +" Issue 17869 +func Test_scriplocal_autoload_func() + let save_rtp = &rtp + set rtp=Xruntime/some + let dir = 'Xruntime/some/autoload' + call mkdir(dir, 'pR') + + let lines =<< trim END + func compl#Func(findstart, base) + if a:findstart + return 1 + else + return ['match', 'matchfoo'] + endif + endfunc + END + call writefile(lines, dir .. '/compl.vim') + + call Ntest_override("char_avail", 1) + new + inoremap <buffer> <F2> <Cmd>let b:matches = complete_info(["matches"]).matches<CR> + set autocomplete + + setlocal complete=.,Fcompl#Func + call feedkeys("im\<F2>\<Esc>0", 'xt!') + call assert_equal(['match', 'matchfoo'], b:matches->mapnew('v:val.word')) + + setlocal complete=.,F<SID>TestCompleteScriptLocal + call feedkeys("Sf\<F2>\<Esc>0", 'xt!') + call assert_equal(['foo', 'foobar'], b:matches->mapnew('v:val.word')) + + setlocal complete& + set autocomplete& + bwipe! + call Ntest_override("char_avail", 0) + let &rtp = save_rtp +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim @@ -290,11 +290,17 @@ func Test_complete() set complete=.,w,b,u,k,s,i,d,],t,U,F,o set complete=. set complete=.^10,t^0 - set complete+=Ffuncref('foo'\\,\ [10]) - set complete=Ffuncref('foo'\\,\ [10])^10 + + func Foo(a, b) + return '' + endfunc + + set complete+=Ffuncref('Foo'\\,\ [10]) + set complete=Ffuncref('Foo'\\,\ [10])^10 set complete& - set complete+=Ffunction('g:foo'\\,\ [10\\,\ 20]) + set complete+=Ffunction('g:Foo'\\,\ [10\\,\ 20]) set complete& + delfunc Foo endfun func Test_set_completion()