commit 2c3929624ac9f28d0bdd0a0423facae7063456d1
parent 196802900323f872ff68facc61bcfec01e476613
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sat, 9 Aug 2025 11:20:35 +0800
Merge pull request #35260 from zeertzjq/vim-9.1.1603
vim-patch:9.1.{1603,1609}
Diffstat:
8 files changed, 312 insertions(+), 49 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
@@ -333,7 +333,6 @@ typedef struct cpt_source_T {
uint64_t compl_start_tv; ///< Timestamp when match collection starts
} cpt_source_T;
-#define STARTCOL_NONE -9
/// Pointer to the array of completion sources
static cpt_source_T *cpt_sources_array;
/// Total number of completion sources specified in the 'cpt' option
@@ -2821,9 +2820,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 +2897,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 +3016,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 +3039,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 +3796,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 +4459,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 +4610,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) {
@@ -4489,10 +4619,12 @@ static void prepare_cpt_compl_funcs(void)
} else {
startcol = -2;
}
+ } else if (startcol < 0 || startcol > curwin->w_cursor.col) {
+ startcol = curwin->w_cursor.col;
}
cpt_sources_array[idx].cs_startcol = startcol;
} else {
- cpt_sources_array[idx].cs_startcol = STARTCOL_NONE;
+ cpt_sources_array[idx].cs_startcol = -3;
}
(void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p
@@ -6050,6 +6182,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 +6208,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 +6369,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;
@@ -6259,14 +6380,14 @@ static void cpt_compl_refresh(void)
} else {
startcol = -2;
}
+ } else if (startcol < 0 || startcol > curwin->w_cursor.col) {
+ startcol = curwin->w_cursor.col;
}
cpt_sources_array[cpt_sources_index].cs_startcol = startcol;
if (ret == OK) {
compl_source_start_timer(cpt_sources_index);
get_cpt_func_completion_matches(cb);
}
- } else {
- cpt_sources_array[cpt_sources_index].cs_startcol = STARTCOL_NONE;
}
}
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,78 @@ 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
+
+" Issue #17907
+func Test_omni_start_invalid_col()
+ func OmniFunc(startcol, findstart, base)
+ if a:findstart
+ return a:startcol
+ else
+ return ['foo', 'foobar']
+ endif
+ endfunc
+
+ new
+ redraw " need this to prevent NULL dereference in Nvim
+ set complete=o
+ set omnifunc=funcref('OmniFunc',\ [-1])
+ call setline(1, ['baz '])
+ call feedkeys("A\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('baz foo', getline(1))
+
+ set omnifunc=funcref('OmniFunc',\ [1000])
+ call setline(1, ['bar '])
+ call feedkeys("A\<C-N>\<Esc>0", 'tx!')
+ call assert_equal('bar foo', getline(1))
+ bw!
+
+ delfunc OmniFunc
+ set omnifunc& complete&
+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()