commit 810a234978d3013e736c6efce0dd5875759cfc56
parent c1fa3c7c377b6e507782ef33d3d2b307931633d6
Author: zeertzjq <zeertzjq@outlook.com>
Date: Sun, 24 Aug 2025 13:16:55 +0800
vim-patch:9.1.1672: completion: cannot add timeouts for 'cpt' sources (#35447)
Problem: completion: cannot add timeouts for 'cpt' sources
(Evgeni Chasnovski)
Solution: Add the 'autocompletetimeout' and 'completetimeout' options
(Girish Palya)
fixes: vim/vim#17908
closes: vim/vim#17967
https://github.com/vim/vim/commit/69a337edc15a6c6eba6f16d2f3b7b223a149a938
Co-authored-by: Girish Palya <girishji@gmail.com>
Diffstat:
9 files changed, 190 insertions(+), 23 deletions(-)
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
@@ -1117,18 +1117,20 @@ CTRL-X CTRL-Z Stop completion without changing the text.
AUTOCOMPLETION *ins-autocompletion*
Vim can display a completion menu as you type, similar to using |i_CTRL-N|,
-but triggered automatically. See 'autocomplete' and 'autocompletedelay'.
-The menu items are collected from the sources listed in the 'complete' option.
+but triggered automatically. See 'autocomplete'. The menu items are collected
+from the sources listed in the 'complete' option, in order.
-Unlike manual |i_CTRL-N| completion, this mode uses a decaying timeout to keep
-Vim responsive. Sources earlier in the 'complete' list are given more time
-(higher priority), but every source is guaranteed a time slice, however small.
+A decaying timeout keeps Vim responsive. Sources earlier in the 'complete'
+list get more time (higher priority), but all sources receive at least a small
+time slice.
This mode is fully compatible with other completion modes. You can invoke
any of them at any time by typing |CTRL-X|, which temporarily suspends
autocompletion. To use |i_CTRL-N| specifically, press |CTRL-E| first to
dismiss the popup menu (see |complete_CTRL-E|).
+See also 'autocomplete', 'autocompletetimeout' and 'autocompletedelay'.
+
To get LSP-driven auto-completion, see |lsp-completion|.
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -754,6 +754,20 @@ A jump table for the options with a short description can be found at |Q_op|.
typing. If you prefer it not to open too quickly, set this value
slightly above your typing speed. See |ins-autocompletion|.
+ *'autocompletetimeout'* *'act'*
+'autocompletetimeout' 'act' number (default 80)
+ global
+ Initial timeout (in milliseconds) for the decaying time-sliced
+ completion algorithm. Starts at this value, halves for each slower
+ source until a minimum is reached. All sources run, but slower ones
+ are quickly de-prioritized. The default is tuned so the popup menu
+ opens within ~200ms even with multiple slow sources on a slow system.
+ Changing this value is rarely needed. Only 80 or higher is valid.
+ Special case: when 'complete' contains "F" or "o" (function sources),
+ a longer timeout is used, allowing up to ~1s for sources such as LSP
+ servers that may sometimes take longer (e.g., while loading modules).
+ See |ins-autocompletion|.
+
*'autoindent'* *'ai'* *'noautoindent'* *'noai'*
'autoindent' 'ai' boolean (default on)
local to buffer
@@ -1688,6 +1702,12 @@ A jump table for the options with a short description can be found at |Q_op|.
For Insert mode completion the buffer-local value is used. For
command line completion the global value is used.
+ *'completetimeout'* *'cto'*
+'completetimeout' 'cto' number (default 0)
+ global
+ Like 'autocompletetimeout', but applies to |i_CTRL-N| and |i_CTRL-P|
+ completion. Value of 0 disables the timeout; positive values allowed.
+
*'concealcursor'* *'cocu'*
'concealcursor' 'cocu' string (default "")
local to window
diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt
@@ -627,6 +627,7 @@ Short explanation of each option: *option-list*
'autochdir' 'acd' change directory to the file in the current window
'autocomplete' 'ac' enable automatic completion in insert mode
'autocompletedelay' 'acl' delay in msec before menu appears after typing
+'autocompletetimeout' 'act' initial decay timeout for autocompletion algorithm
'autoindent' 'ai' take indent for new line from previous line
'autoread' 'ar' autom. read file when changed outside of Vim
'autowrite' 'aw' automatically write file if changed
@@ -670,6 +671,7 @@ Short explanation of each option: *option-list*
'completefunc' 'cfu' function to be used for Insert mode completion
'completeopt' 'cot' options for Insert mode completion
'completeslash' 'csl' like 'shellslash' for completion
+'completetimeout' 'cto' initial decay timeout for CTRL-N and CTRL-P
'concealcursor' 'cocu' whether concealable text is hidden in cursor line
'conceallevel' 'cole' whether concealable text is shown or hidden
'confirm' 'cf' ask what to do about unsaved/read-only files
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -130,6 +130,23 @@ vim.o.acl = vim.o.autocompletedelay
vim.go.autocompletedelay = vim.o.autocompletedelay
vim.go.acl = vim.go.autocompletedelay
+--- Initial timeout (in milliseconds) for the decaying time-sliced
+--- completion algorithm. Starts at this value, halves for each slower
+--- source until a minimum is reached. All sources run, but slower ones
+--- are quickly de-prioritized. The default is tuned so the popup menu
+--- opens within ~200ms even with multiple slow sources on a slow system.
+--- Changing this value is rarely needed. Only 80 or higher is valid.
+--- Special case: when 'complete' contains "F" or "o" (function sources),
+--- a longer timeout is used, allowing up to ~1s for sources such as LSP
+--- servers that may sometimes take longer (e.g., while loading modules).
+--- See `ins-autocompletion`.
+---
+--- @type integer
+vim.o.autocompletetimeout = 80
+vim.o.act = vim.o.autocompletetimeout
+vim.go.autocompletetimeout = vim.o.autocompletetimeout
+vim.go.act = vim.go.autocompletetimeout
+
--- Copy indent from current line when starting a new line (typing <CR>
--- in Insert mode or when using the "o" or "O" command). If you do not
--- type anything on the new line except <BS> or CTRL-D and then type
@@ -1239,6 +1256,15 @@ vim.o.csl = vim.o.completeslash
vim.bo.completeslash = vim.o.completeslash
vim.bo.csl = vim.bo.completeslash
+--- Like 'autocompletetimeout', but applies to `i_CTRL-N` and `i_CTRL-P`
+--- completion. Value of 0 disables the timeout; positive values allowed.
+---
+--- @type integer
+vim.o.completetimeout = 0
+vim.o.cto = vim.o.completetimeout
+vim.go.completetimeout = vim.o.completetimeout
+vim.go.cto = vim.go.completetimeout
+
--- Sets the modes in which text in the cursor line can also be concealed.
--- When the current mode is listed then concealing happens just like in
--- other lines.
diff --git a/runtime/optwin.vim b/runtime/optwin.vim
@@ -1,7 +1,7 @@
" These commands create the option window.
"
" Maintainer: The Vim Project <https://github.com/vim/vim>
-" Last Change: 2025 Aug 16
+" Last Change: 2025 Aug 23
" Former Maintainer: Bram Moolenaar <Bram@vim.org>
" If there already is an option window, jump to that one.
@@ -737,6 +737,10 @@ if has("insert_expand")
call <SID>OptionL("cpt")
call <SID>AddOption("autocomplete", gettext("automatic completion in insert mode"))
call <SID>BinOptionG("ac", &ac)
+ call <SID>AddOption("autocompletetimeout", gettext("initial decay timeout for 'autocomplete' algorithm"))
+ call append("$", " \tset act=" . &act)
+ call <SID>AddOption("completetimeout", gettext("initial decay timeout for CTRL-N and CTRL-P completion"))
+ call append("$", " \tset cto=" . &cto)
call <SID>AddOption("autocompletedelay", gettext("delay in msec before menu appears after typing"))
call append("$", " \tset acl=" . &acl)
call <SID>AddOption("completeopt", gettext("whether to use a popup menu for Insert mode completion"))
diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c
@@ -289,6 +289,9 @@ static buf_T *compl_curr_buf = NULL; ///< buf where completion is active
// if the current source exceeds its timeout, it is interrupted and the next
// begins with half the time. A small minimum timeout ensures every source
// gets at least a brief chance.
+// Special case: when 'complete' contains "F" or "o" (function sources), a
+// longer fixed timeout is used (COMPL_FUNC_TIMEOUT_MS or
+// COMPL_FUNC_TIMEOUT_NON_KW_MS). - girish
static bool compl_autocomplete = false; ///< whether autocompletion is active
static uint64_t compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS;
static bool compl_time_slice_expired = false; ///< time budget exceeded for current source
@@ -303,6 +306,10 @@ static bool compl_from_nonkeyword = false; ///< completion started from non-
} \
} while (0)
+// Timeout values for F{func}, F and o values in 'complete'
+#define COMPL_FUNC_TIMEOUT_MS 300
+#define COMPL_FUNC_TIMEOUT_NON_KW_MS 1000
+
// List of flags for method of completion.
static int compl_cont_status = 0;
#define CONT_ADDING 1 ///< "normal" or "adding" expansion
@@ -4640,7 +4647,7 @@ static void prepare_cpt_compl_funcs(void)
/// Start the timer for the current completion source.
static void compl_source_start_timer(int source_idx)
{
- if (compl_autocomplete && cpt_sources_array != NULL) {
+ if (compl_autocomplete || p_cto > 0) {
cpt_sources_array[source_idx].compl_start_tv = os_hrtime();
compl_time_slice_expired = false;
}
@@ -4657,8 +4664,6 @@ static int advance_cpt_sources_index_safe(void)
return FAIL;
}
-#define COMPL_FUNC_TIMEOUT_MS 300
-#define COMPL_FUNC_TIMEOUT_NON_KW_MS 1000
/// Get the next expansion(s), using "compl_pattern".
/// The search starts at position "ini" in curbuf and in the direction
/// compl_direction.
@@ -4708,12 +4713,17 @@ static int ins_compl_get_exp(pos_T *ini)
compl_old_match = compl_curr_match; // remember the last current match
st.cur_match_pos = compl_dir_forward() ? &st.last_match_pos : &st.first_match_pos;
- if (cpt_sources_array != NULL && ctrl_x_mode_normal() && !ctrl_x_mode_line_or_eval()
- && !(compl_cont_status & CONT_LOCAL)) {
+ bool normal_mode_strict = ctrl_x_mode_normal() && !ctrl_x_mode_line_or_eval()
+ && !(compl_cont_status & CONT_LOCAL)
+ && cpt_sources_array != NULL;
+ if (normal_mode_strict) {
cpt_sources_index = 0;
- if (compl_autocomplete) {
+ if (compl_autocomplete || p_cto > 0) {
compl_source_start_timer(0);
- compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS;
+ compl_time_slice_expired = false;
+ compl_timeout_ms = compl_autocomplete
+ ? (uint64_t)MAX(COMPL_INITIAL_TIMEOUT_MS, p_act)
+ : (uint64_t)p_cto;
}
}
@@ -4743,12 +4753,15 @@ static int ins_compl_get_exp(pos_T *ini)
}
}
- if (compl_autocomplete && type == CTRL_X_FUNCTION) {
+ uint64_t compl_timeout_save = 0;
+ if (normal_mode_strict && type == CTRL_X_FUNCTION
+ && (compl_autocomplete || p_cto > 0)) {
// LSP servers may sporadically take >1s to respond (e.g., while
// loading modules), but other sources might already have matches.
// To show results quickly use a short timeout for keyword
// completion. Allow longer timeout for non-keyword completion
// where only function based sources (e.g. LSP) are active.
+ compl_timeout_save = compl_timeout_ms;
compl_timeout_ms = compl_from_nonkeyword
? COMPL_FUNC_TIMEOUT_NON_KW_MS : COMPL_FUNC_TIMEOUT_MS;
}
@@ -4796,9 +4809,10 @@ static int ins_compl_get_exp(pos_T *ini)
compl_started = false;
}
- // Reset the timeout after collecting matches from function source
- if (compl_autocomplete && type == CTRL_X_FUNCTION) {
- compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS;
+ // Restore the timeout after collecting matches from function source
+ if (normal_mode_strict && type == CTRL_X_FUNCTION
+ && (compl_autocomplete || p_cto > 0)) {
+ compl_timeout_ms = compl_timeout_save;
}
// For `^P` completion, reset `compl_curr_match` to the head to avoid
@@ -5295,10 +5309,6 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match
/// collecting, and halve the timeout.
static void check_elapsed_time(void)
{
- if (cpt_sources_array == NULL || cpt_sources_index < 0) {
- return;
- }
-
uint64_t start_tv = cpt_sources_array[cpt_sources_index].compl_start_tv;
uint64_t elapsed_ms = (os_hrtime() - start_tv) / 1000000;
@@ -5355,8 +5365,13 @@ void ins_compl_check_keys(int frequency, bool in_compl_func)
vungetc(c);
}
}
- } else if (compl_autocomplete) {
- check_elapsed_time();
+ } else {
+ bool normal_mode_strict = ctrl_x_mode_normal() && !ctrl_x_mode_line_or_eval()
+ && !(compl_cont_status & CONT_LOCAL)
+ && cpt_sources_array != NULL && cpt_sources_index >= 0;
+ if (normal_mode_strict && (compl_autocomplete || p_cto > 0)) {
+ check_elapsed_time();
+ }
}
if (compl_pending != 0 && !got_int && !(cot_flags & kOptCotFlagNoinsert)
diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h
@@ -292,6 +292,7 @@ EXTERN OptInt p_cwh; ///< 'cmdwinheight'
EXTERN OptInt p_ch; ///< 'cmdheight'
EXTERN char *p_cms; ///< 'commentstring'
EXTERN char *p_cpt; ///< 'complete'
+EXTERN OptInt p_cto; ///< 'completetimeout'
EXTERN OptInt p_columns; ///< 'columns'
EXTERN int p_confirm; ///< 'confirm'
EXTERN char *p_cfc; ///< 'completefuzzycollect'
@@ -301,6 +302,7 @@ EXTERN unsigned cia_flags; ///< order flags of 'completeitemalign'
EXTERN char *p_cot; ///< 'completeopt'
EXTERN unsigned cot_flags; ///< flags from 'completeopt'
EXTERN int p_ac; ///< 'autocomplete'
+EXTERN OptInt p_act; ///< 'autocompletetimeout'
EXTERN OptInt p_acl; ///< 'autocompletedelay'
#ifdef BACKSLASH_IN_FILENAME
EXTERN char *p_csl; ///< 'completeslash'
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
@@ -256,6 +256,27 @@ local options = {
varname = 'p_acl',
},
{
+ abbreviation = 'act',
+ defaults = 80,
+ desc = [=[
+ Initial timeout (in milliseconds) for the decaying time-sliced
+ completion algorithm. Starts at this value, halves for each slower
+ source until a minimum is reached. All sources run, but slower ones
+ are quickly de-prioritized. The default is tuned so the popup menu
+ opens within ~200ms even with multiple slow sources on a slow system.
+ Changing this value is rarely needed. Only 80 or higher is valid.
+ Special case: when 'complete' contains "F" or "o" (function sources),
+ a longer timeout is used, allowing up to ~1s for sources such as LSP
+ servers that may sometimes take longer (e.g., while loading modules).
+ See |ins-autocompletion|.
+ ]=],
+ full_name = 'autocompletetimeout',
+ scope = { 'global' },
+ short_desc = N_('initial decay timeout for autocompletion algorithm'),
+ type = 'number',
+ varname = 'p_act',
+ },
+ {
abbreviation = 'ai',
defaults = true,
desc = [=[
@@ -1723,6 +1744,19 @@ local options = {
varname = 'p_csl',
},
{
+ abbreviation = 'cto',
+ defaults = 0,
+ desc = [=[
+ Like 'autocompletetimeout', but applies to |i_CTRL-N| and |i_CTRL-P|
+ completion. Value of 0 disables the timeout; positive values allowed.
+ ]=],
+ full_name = 'completetimeout',
+ scope = { 'global' },
+ short_desc = N_('initial decay timeout for CTRL-N and CTRL-P'),
+ type = 'number',
+ varname = 'p_cto',
+ },
+ {
abbreviation = 'cocu',
cb = 'did_set_concealcursor',
defaults = '',
diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim
@@ -5573,6 +5573,68 @@ func Test_omni_start_invalid_col()
set omnifunc& complete&
endfunc
+func Test_completetimeout_autocompletetimeout()
+ func OmniFunc(findstart, base)
+ if a:findstart
+ return 1
+ else
+ return ['fooOmni']
+ endif
+ endfunc
+
+ set omnifunc=OmniFunc
+ call Ntest_override("char_avail", 1)
+ inoremap <F2> <Cmd>let b:matches = complete_info(["matches"]).matches<CR>
+
+ call setline(1, ['foobar', 'foobarbaz'])
+ new
+ call setline(1, ['foo', 'foobaz', ''])
+ set complete=.,o,w
+ call feedkeys("G", 'xt!')
+
+ set autocomplete
+ for tt in [1, 80, 1000, -1, 0]
+ exec $'set autocompletetimeout={tt}'
+ call feedkeys("\<Esc>Sf\<F2>\<Esc>0", 'xt!')
+ call assert_equal(['foobaz', 'foo', 'fooOmni', 'foobar', 'foobarbaz'], b:matches->mapnew('v:val.word'))
+ endfor
+ set autocomplete&
+
+ for tt in [80, 1000, -1, 0]
+ exec $'set completetimeout={tt}'
+ call feedkeys("\<Esc>Sf\<C-N>\<F2>\<Esc>0", 'xt!')
+ call assert_equal(['foo', 'foobaz', 'fooOmni', 'foobar', 'foobarbaz'], b:matches->mapnew('v:val.word'))
+ endfor
+
+ " Clock does not have fine granularity, so checking 'elapsed time' is only
+ " approximate. We can only test that some type of timeout is enforced.
+ call feedkeys("\<Esc>", 'xt!')
+ call setline(1, map(range(60000), '"foo" . v:val'))
+ set completetimeout=1
+ call feedkeys("Gof\<C-N>\<F2>\<Esc>0", 'xt!')
+ let match_count = len(b:matches->mapnew('v:val.word'))
+ call assert_true(match_count < 2000)
+
+ set completetimeout=1000
+ call feedkeys("\<Esc>Sf\<C-N>\<F2>\<Esc>0", 'xt!')
+ let match_count = len(b:matches->mapnew('v:val.word'))
+ call assert_true(match_count > 2000)
+
+ set autocomplete
+ set autocompletetimeout=81
+ call feedkeys("\<Esc>Sf\<F2>\<Esc>0", 'xt!')
+ let match_count = len(b:matches->mapnew('v:val.word'))
+ call assert_true(match_count < 50000)
+
+ call feedkeys("\<Esc>", 'xt!')
+ set complete& omnifunc& autocomplete& autocompletetimeout& completetimeout&
+ bwipe!
+ %d
+ call Ntest_override("char_avail", 0)
+ iunmap <F2>
+ delfunc OmniFunc
+endfunc
+
func Test_autocompletedelay()
CheckScreendump