neovim

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

commit 9bfd0162dc11478febbdfd0fd9c9c880a0f454ae
parent bec1449cc51081e064d254f88601358094f3abe8
Author: zeertzjq <zeertzjq@outlook.com>
Date:   Mon,  2 Jun 2025 14:16:17 +0800

vim-patch:9.1.1422: scheduling of complete function can be improved

Problem:  scheduling of complete function can be improved
Solution: call user completion functions earlier when just determining
          the insertion column (Girish Palya)

This change improves the scheduling behavior of async user-defined
completion functions (such as `F{func}`, `F`, or `'o'` values in the
`'complete'` option), particularly benefiting LSP clients.

Currently, these user functions are invoked twice:

1. First with `findstart = 1` to determine the completion start
   position.
2. Then with `findstart = 0` to retrieve the actual matches.

Previously, both calls were executed back-to-back. With this change, the
first call (`findstart = 1`) is performed earlier—before any matches are
gathered from other sources.

This adjustment gives event-driven completion sources (e.g., LSP
clients) more time to send their requests while Vim concurrently
collects matches from other sources like the current buffer.

Not sure about the real-world performance gains, but this approach
should, in theory, improve responsiveness and reduce latency for
asynchronous completions.

To test, try using yegappan LSP client:

```vim
set cpt+=o^10
autocmd VimEnter * g:LspOptionsSet({ autoComplete: false, omniComplete: true })
```

If you prefer to use 'native' auto-completion (without plugins), try the
following configuration:

```vim
set cot=menuone,popup,noselect,nearest
autocmd TextChangedI * InsComplete()
def InsComplete()
  if getcharstr(1) == '' && getline('.')->strpart(0, col('.') - 1) =~ '\k$'
    SkipTextChangedI()
    feedkeys("\<c-n>", "n")
  endif
enddef
inoremap <silent> <c-e> <c-r>=<SID>SkipTextChangedI()<cr><c-e>
inoremap <silent> <c-y> <c-r>=<SID>SkipTextChangedI()<cr><c-y>
def SkipTextChangedI(): string
  set eventignore+=TextChangedI
  timer_start(1, (_) => {
    set eventignore-=TextChangedI
  })
  return ''
enddef
inoremap <silent><expr> <tab> pumvisible() ? "\<c-n>" : "\<tab>"
inoremap <silent><expr> <s-tab> pumvisible() ? "\<c-p>" : "\<s-tab>"
```

closes: vim/vim#17396

https://github.com/vim/vim/commit/98c29dbfd1c0765cbe5a2fce71072a33ad629f34

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

Diffstat:
Msrc/nvim/insexpand.c | 217++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
1 file changed, 144 insertions(+), 73 deletions(-)

diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c @@ -308,8 +308,9 @@ static int *compl_fuzzy_scores; /// Define the structure for completion source (in 'cpt' option) information typedef struct cpt_source_T { - bool refresh_always; ///< Flag to indicate function has 'refresh:always' set - int max_matches; ///< Maximum number of items to display in menu from the source + bool cs_refresh_always; ///< Whether 'refresh:always' is set for func + int cs_startcol; ///< Start column returned by func + int cs_max_matches; ///< Max items to display from this source } cpt_source_T; /// Pointer to the array of completion sources @@ -1382,7 +1383,7 @@ static void trim_compl_match_array(void) // Calculate size of trimmed array, respecting max_matches per source. int new_size = 0; for (int i = 0; i < cpt_sources_count; i++) { - int limit = cpt_sources_array[i].max_matches; + int limit = cpt_sources_array[i].cs_max_matches; new_size += (limit > 0 && match_counts[i] > limit) ? limit : match_counts[i]; } @@ -1397,7 +1398,7 @@ static void trim_compl_match_array(void) for (int i = 0; i < compl_match_arraysize; i++) { int src_idx = compl_match_array[i].pum_cpt_source_idx; if (src_idx != -1) { - int limit = cpt_sources_array[src_idx].max_matches; + int limit = cpt_sources_array[src_idx].cs_max_matches; if (limit <= 0 || match_counts[src_idx] < limit) { trimmed[trimmed_idx++] = compl_match_array[i]; match_counts[src_idx]++; @@ -1491,7 +1492,7 @@ static int ins_compl_build_pum(void) match_count = 1; max_matches_found = false; } else if (cpt_sources_array && !max_matches_found) { - int max_matches = cpt_sources_array[cur_source].max_matches; + int max_matches = cpt_sources_array[cur_source].cs_max_matches; if (max_matches > 0 && match_count > max_matches) { max_matches_found = true; } @@ -3510,6 +3511,9 @@ static bool may_advance_cpt_index(const char *cpt) { const char *p = cpt; + if (cpt_sources_index == -1) { + return false; + } while (*p == ',' || *p == ' ') { // Skip delimiters p++; } @@ -3619,12 +3623,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar } } else if (*st->e_cpt == 'F' || *st->e_cpt == 'o') { compl_type = CTRL_X_FUNCTION; - if (*st->e_cpt == 'o') { - st->func_cb = &curbuf->b_ofu_cb; - } else { - st->func_cb = (*++st->e_cpt != ',' && *st->e_cpt != NUL) - ? get_cpt_func_callback(st->e_cpt) : &curbuf->b_cfu_cb; - } + st->func_cb = get_callback_if_cpt_func(st->e_cpt); if (!st->func_cb) { compl_type = -1; } @@ -4239,15 +4238,27 @@ static void get_register_completion(void) } } -/// Return the callback function associated with "funcname". -static Callback *get_cpt_func_callback(char *funcname) +/// Return the callback function associated with "p" if it points to a +/// userfunc. +static Callback *get_callback_if_cpt_func(char *p) { static Callback cb; char buf[LSIZE]; - size_t slen = copy_option_part(&funcname, buf, LSIZE, ","); - if (slen > 0 && option_set_callback_func(buf, &cb)) { - return &cb; + 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; + } else { + return &curbuf->b_cfu_cb; + } } return NULL; } @@ -4375,6 +4386,44 @@ static void strip_caret_numbers_in_place(char *str) *write = '\0'; } +/// Call functions specified in the 'cpt' option with findstart=1, +/// and retrieve the startcol. +static void prepare_cpt_compl_funcs(void) +{ + // Make a copy of 'cpt' in case the buffer gets wiped out + char *cpt = xstrdup(curbuf->b_p_cpt); + strip_caret_numbers_in_place(cpt); + + // Re-insert the text removed by ins_compl_delete(). + ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1); + + int idx = 0; + for (char *p = cpt; *p;) { + while (*p == ',' || *p == ' ') { // Skip delimiters + p++; + } + Callback *cb = get_callback_if_cpt_func(p); + if (cb) { + int startcol; + if (get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol) == FAIL) { + if (startcol == -3) { + cpt_sources_array[idx].cs_refresh_always = false; + } else { + startcol = -2; + } + } + cpt_sources_array[idx].cs_startcol = startcol; + } + (void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p + idx++; + } + + // Undo insertion + ins_compl_delete(false); + + xfree(cpt); +} + /// Safely advance the cpt_sources_index by one. static int advance_cpt_sources_index_safe(void) { @@ -4419,10 +4468,6 @@ static int ins_compl_get_exp(pos_T *ini) strip_caret_numbers_in_place(st.e_cpt_copy); st.e_cpt = st.e_cpt_copy; st.last_match_pos = st.first_match_pos = *ini; - - if (ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval()) { - cpt_sources_init(); - } } else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf)) { st.ins_buf = curbuf; // In case the buffer was wiped out. } @@ -4431,8 +4476,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 (ctrl_x_mode_normal() && !ctrl_x_mode_line_or_eval() + && !(compl_cont_status & CONT_LOCAL)) { + // ^N completion, not ^X^L or complete() or ^X^N + if (!compl_started) { // Before showing menu the first time + setup_cpt_sources(); + } + prepare_cpt_compl_funcs(); + cpt_sources_index = 0; + } + // For ^N/^P loop over all the flags/windows/buffers in 'complete' - cpt_sources_index = 0; while (true) { found_new_match = FAIL; st.set_match_pos = false; @@ -5271,9 +5325,29 @@ static int get_cmdline_compl_info(char *line, colnr_T curs_col) return OK; } +/// Set global variables related to completion: +/// compl_col, compl_length, compl_pattern, and cpt_compl_pattern. +static void set_compl_globals(int startcol, colnr_T curs_col, bool is_cpt_compl) +{ + if (startcol < 0 || startcol > curs_col) { + startcol = curs_col; + } + int len = curs_col - startcol; + + // Re-obtain line in case it has changed + char *line = ml_get(curwin->w_cursor.lnum); + + String *pattern = is_cpt_compl ? &cpt_compl_pattern : &compl_pattern; + pattern->data = xstrnsave(line + startcol, (size_t)len); + pattern->size = (size_t)len; + if (!is_cpt_compl) { + compl_col = startcol; + compl_length = len; + } +} + /// Get the pattern, column and length for user defined completion ('omnifunc', /// 'completefunc' and 'thesaurusfunc') -/// Sets the global variables: compl_col, compl_length and compl_pattern. /// Uses the global variable: spell_bad_len /// /// @param cb set if triggered by a function in the 'cpt' option, otherwise NULL @@ -5345,21 +5419,8 @@ static int get_userdefined_compl_info(colnr_T curs_col, Callback *cb, int *start // completion. compl_opt_refresh_always = false; - if (col < 0 || col > curs_col) { - col = curs_col; - } - - // Setup variables for completion. Need to obtain "line" again, - // it may have become invalid. - char *line = ml_get(curwin->w_cursor.lnum); - int len = curs_col - col; - String *compl_pat = is_cpt_function ? &cpt_compl_pattern : &compl_pattern; - compl_pat->data = xstrnsave(line + col, (size_t)len); - compl_pat->size = (size_t)compl_length; - if (!is_cpt_function) { - compl_col = col; - compl_length = len; + set_compl_globals(col, curs_col, false); } return OK; } @@ -5851,12 +5912,12 @@ static void cpt_sources_clear(void) cpt_sources_count = 0; } -/// Initialize the info associated with completion sources. -static void cpt_sources_init(void) +/// Setup completion sources. +static void setup_cpt_sources(void) { char buf[LSIZE]; - int count = 0; + int count = 0; for (char *p = curbuf->b_p_cpt; *p;) { while (*p == ',' || *p == ' ') { // Skip delimiters p++; @@ -5866,24 +5927,27 @@ static void cpt_sources_init(void) count++; } } + if (count == 0) { + return; + } + cpt_sources_clear(); cpt_sources_count = count; - if (count > 0) { - cpt_sources_array = xcalloc((size_t)count, sizeof(cpt_source_T)); - count = 0; - 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[count].max_matches = atoi(t + 1); - } - count++; + cpt_sources_array = xcalloc((size_t)count, sizeof(cpt_source_T)); + + int idx = 0; + 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); } + idx++; } } } @@ -5892,7 +5956,7 @@ static void cpt_sources_init(void) static bool is_cpt_func_refresh_always(void) { for (int i = 0; i < cpt_sources_count; i++) { - if (cpt_sources_array[i].refresh_always) { + if (cpt_sources_array[i].cs_refresh_always) { return true; } } @@ -5985,24 +6049,24 @@ static compl_T *remove_old_matches(void) /// 'refresh:always' flag. static void get_cpt_func_completion_matches(Callback *cb) { + int startcol = cpt_sources_array[cpt_sources_index].cs_startcol; + API_CLEAR_STRING(cpt_compl_pattern); - int startcol; - int ret = get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol); - if (ret == FAIL && startcol == -3) { - cpt_sources_array[cpt_sources_index].refresh_always = false; - } else if (ret == OK) { - expand_by_function(0, cpt_compl_pattern.data, cb); - cpt_sources_array[cpt_sources_index].refresh_always = compl_opt_refresh_always; - compl_opt_refresh_always = false; + + if (startcol == -2 || startcol == -3) { + return; } + + set_compl_globals(startcol, curwin->w_cursor.col, true); + expand_by_function(0, cpt_compl_pattern.data, cb); + cpt_sources_array[cpt_sources_index].cs_refresh_always = compl_opt_refresh_always; + compl_opt_refresh_always = false; } /// Retrieve completion matches from functions in the 'cpt' option where the /// 'refresh:always' flag is set. static void cpt_compl_refresh(void) { - Callback *cb = NULL; - // Make the completion list linear (non-cyclic) ins_compl_make_linear(); // Make a copy of 'cpt' in case the buffer gets wiped out @@ -6015,16 +6079,23 @@ static void cpt_compl_refresh(void) p++; } - if (cpt_sources_array[cpt_sources_index].refresh_always) { - if (*p == 'o') { - cb = &curbuf->b_ofu_cb; - } else if (*p == 'F') { - cb = (*(p + 1) != ',' && *(p + 1) != NUL) - ? get_cpt_func_callback(p + 1) : &curbuf->b_cfu_cb; - } + if (cpt_sources_array[cpt_sources_index].cs_refresh_always) { + Callback *cb = get_callback_if_cpt_func(p); if (cb) { compl_curr_match = remove_old_matches(); - get_cpt_func_completion_matches(cb); + int startcol; + int ret = get_userdefined_compl_info(curwin->w_cursor.col, cb, &startcol); + if (ret == FAIL) { + if (startcol == -3) { + cpt_sources_array[cpt_sources_index].cs_refresh_always = false; + } else { + startcol = -2; + } + } + cpt_sources_array[cpt_sources_index].cs_startcol = startcol; + if (ret == OK) { + get_cpt_func_completion_matches(cb); + } } }