patch 9.1.1422: scheduling of complete function can be improved

Commit: 
https://github.com/vim/vim/commit/98c29dbfd1c0765cbe5a2fce71072a33ad629f34
Author: Girish Palya <giris...@gmail.com>
Date:   Sun Jun 1 19:40:00 2025 +0200

    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: #17396
    
    Signed-off-by: Girish Palya <giris...@gmail.com>
    Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/src/insexpand.c b/src/insexpand.c
index 1de15c3ff..66db022c1 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -221,8 +221,9 @@ static int    *compl_fuzzy_scores;
 // Define the structure for completion source (in 'cpt' option) information
 typedef struct cpt_source_T
 {
-    int refresh_always;        // Flag array to indicate which 'cpt' functions 
have 'refresh:always' set
-    int max_matches;   // Maximum number of items to display in the menu from 
the source
+    int        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;
 
 static cpt_source_T *cpt_sources_array; // Pointer to the array of completion 
sources
@@ -250,9 +251,9 @@ static void ins_compl_add_list(list_T *list);
 static void ins_compl_add_dict(dict_T *dict);
 static int get_userdefined_compl_info(colnr_T curs_col, callback_T *cb, int 
*startcol);
 static void get_cpt_func_completion_matches(callback_T *cb);
-static callback_T *get_cpt_func_callback(char_u *funcname);
+static callback_T *get_callback_if_cpt_func(char_u *p);
 # endif
-static int cpt_sources_init(void);
+static int setup_cpt_sources(void);
 static int is_cpt_func_refresh_always(void);
 static void cpt_sources_clear(void);
 static void cpt_compl_refresh(void);
@@ -1467,7 +1468,7 @@ trim_compl_match_array(void)
     // Calculate size of trimmed array, respecting max_matches per source.
     for (i = 0; i < cpt_sources_count; i++)
     {
-       limit = cpt_sources_array[i].max_matches;
+       limit = cpt_sources_array[i].cs_max_matches;
        new_size += (limit > 0 && match_counts[i] > limit)
            ? limit : match_counts[i];
     }
@@ -1485,7 +1486,7 @@ trim_compl_match_array(void)
        src_idx = compl_match_array[i].pum_cpt_source_idx;
        if (src_idx != -1)
        {
-           limit = cpt_sources_array[src_idx].max_matches;
+           limit = cpt_sources_array[src_idx].cs_max_matches;
            if (limit <= 0 || match_counts[src_idx] < limit)
            {
                trimmed[trimmed_idx++] = compl_match_array[i];
@@ -1581,7 +1582,7 @@ ins_compl_build_pum(void)
            }
            else if (cpt_sources_array != NULL && !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;
            }
@@ -4026,6 +4027,8 @@ may_advance_cpt_index(char_u *cpt)
 {
     char_u  *p = cpt;
 
+    if (cpt_sources_index == -1)
+       return FALSE;
     while (*p == ',' || *p == ' ') // Skip delimiters
        p++;
     return (*p != NUL);
@@ -4178,11 +4181,7 @@ process_next_cpt_value(
        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;
        }
@@ -4846,20 +4845,33 @@ get_next_default_completion(ins_compl_next_state_T *st, 
pos_T *start_pos)
     return found_new_match;
 }
 
+#ifdef FEAT_COMPL_FUNC
 /*
- * Return the callback function associated with "funcname".
+ * Return the callback function associated with "p" if it points to a
+ * userfunc.
  */
-#ifdef FEAT_COMPL_FUNC
     static callback_T *
-get_cpt_func_callback(char_u *funcname)
+get_callback_if_cpt_func(char_u *p)
 {
     static callback_T  cb;
     char_u             buf[LSIZE];
     int                        slen;
 
-    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)
+       {
+           free_callback(&cb);
+           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;
 }
 
@@ -5061,6 +5073,55 @@ strip_caret_numbers_in_place(char_u *str)
     *write = '
 }
 
+/*
+ * Call functions specified in the 'cpt' option with findstart=1,
+ * and retrieve the startcol.
+ */
+    static void
+prepare_cpt_compl_funcs(void)
+{
+#ifdef FEAT_COMPL_FUNC
+    char_u     *cpt;
+    char_u     *p;
+    callback_T *cb = NULL;
+    int                idx = 0;
+    int                startcol;
+
+    // Make a copy of 'cpt' in case the buffer gets wiped out
+    cpt = vim_strsave(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.string + get_compl_len(), -1);
+
+    for (p = cpt; *p;)
+    {
+       while (*p == ',' || *p == ' ') // Skip delimiters
+           p++;
+       cb = get_callback_if_cpt_func(p);
+       if (cb)
+       {
+           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();
+
+    vim_free(cpt);
+#endif
+}
+
 /*
  * Safely advance the cpt_sources_index by one.
  */
@@ -5117,10 +5178,6 @@ ins_compl_get_exp(pos_T *ini)
        strip_caret_numbers_in_place(st.e_cpt_copy);
        st.e_cpt = st.e_cpt_copy == NULL ? (char_u *)"" : 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())
-           return FAIL;
     }
     else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf))
        st.ins_buf = curbuf;  // In case the buffer was wiped out.
@@ -5129,8 +5186,20 @@ ins_compl_get_exp(pos_T *ini)
     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
+       {
+           if (setup_cpt_sources() == FAIL)
+               return FAIL;
+       }
+       prepare_cpt_compl_funcs();
+       cpt_sources_index = 0;
+    }
+
     // For ^N/^P loop over all the flags/windows/buffers in 'complete'.
-    cpt_sources_index = 0;
     for (;;)
     {
        found_new_match = FAIL;
@@ -6120,10 +6189,48 @@ get_cmdline_compl_info(char_u *line, colnr_T curs_col)
     return OK;
 }
 
+#ifdef FEAT_COMPL_FUNC
+/*
+ * Set global variables related to completion:
+ * compl_col, compl_length, compl_pattern, and cpt_compl_pattern.
+ */
+    static int
+set_compl_globals(
+    int                startcol UNUSED,
+    colnr_T    curs_col UNUSED,
+    int                is_cpt_compl UNUSED)
+{
+    char_u     *line = NULL;
+    string_T   *pattern = NULL;
+    int                len;
+
+    if (startcol < 0 || startcol > curs_col)
+       startcol = curs_col;
+    len = curs_col - startcol;
+
+    // Re-obtain line in case it has changed
+    line = ml_get(curwin->w_cursor.lnum);
+
+    pattern = is_cpt_compl ? &cpt_compl_pattern : &compl_pattern;
+    pattern->string = vim_strnsave(line + startcol, (size_t)len);
+    if (pattern->string == NULL)
+    {
+       pattern->length = 0;
+       return FAIL;
+    }
+    pattern->length = (size_t)len;
+    if (!is_cpt_compl)
+    {
+       compl_col = startcol;
+       compl_length = len;
+    }
+    return OK;
+}
+#endif
+
 /*
  * 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
  * Callback function "cb" is set if triggered by a function in the 'cpt'
  * option; otherwise, it is NULL.
@@ -6140,14 +6247,11 @@ get_userdefined_compl_info(
 #ifdef FEAT_COMPL_FUNC
     // Call user defined function 'completefunc' with "a:findstart"
     // set to 1 to obtain the length of text to use for completion.
-    char_u     *line = NULL;
     typval_T   args[3];
     int                col;
     char_u     *funcname = NULL;
     pos_T      pos;
     int                save_State = State;
-    int                len;
-    string_T   *compl_pat = NULL;
     int                is_cpt_function = (cb != NULL);
 
     if (!is_cpt_function)
@@ -6210,28 +6314,7 @@ get_userdefined_compl_info(
     compl_opt_refresh_always = FALSE;
     compl_opt_suppress_empty = FALSE;
 
-    if (col < 0 || col > curs_col)
-       col = curs_col;
-
-    // Setup variables for completion.  Need to obtain "line" again,
-    // it may have become invalid.
-    line = ml_get(curwin->w_cursor.lnum);
-    len = curs_col - col;
-    compl_pat = is_cpt_function ? &cpt_compl_pattern : &compl_pattern;
-    compl_pat->string = vim_strnsave(line + col, (size_t)len);
-    if (compl_pat->string == NULL)
-    {
-       compl_pat->length = 0;
-       return FAIL;
-    }
-    compl_pat->length = (size_t)compl_length;
-
-    if (!is_cpt_function)
-    {
-       compl_col = col;
-       compl_length = len;
-    }
-    ret = OK;
+    ret = !is_cpt_function ? set_compl_globals(col, curs_col, FALSE) : OK;
 #endif
 
     return ret;
@@ -6813,14 +6896,14 @@ cpt_sources_clear(void)
 }
 
 /*
- * Initialize the info associated with completion sources.
+ * Setup completion sources.
  */
     static int
-cpt_sources_init(void)
+setup_cpt_sources(void)
 {
     char_u  buf[LSIZE];
     int            slen;
-    int            count = 0;
+    int            count = 0, idx = 0;
     char_u  *p;
 
     for (p = curbuf->b_p_cpt; *p;)
@@ -6833,31 +6916,31 @@ cpt_sources_init(void)
            count++;
        }
     }
+    if (count == 0)
+       return OK;
+
     cpt_sources_clear();
     cpt_sources_count = count;
-    if (count > 0)
+    cpt_sources_array = ALLOC_CLEAR_MULT(cpt_source_T, count);
+    if (cpt_sources_array == NULL)
     {
-       cpt_sources_array = ALLOC_CLEAR_MULT(cpt_source_T, count);
-       if (cpt_sources_array == NULL)
-       {
-           cpt_sources_count = 0;
-           return FAIL;
-       }
-       count = 0;
-       for (p = curbuf->b_p_cpt; *p;)
+       cpt_sources_count = 0;
+       return FAIL;
+    }
+
+    for (p = curbuf->b_p_cpt; *p;)
+    {
+       while (*p == ',' || *p == ' ') // Skip delimiters
+           p++;
+       if (*p) // If not end of string, count this segment
        {
-           while (*p == ',' || *p == ' ') // Skip delimiters
-               p++;
-           if (*p) // If not end of string, count this segment
-           {
-               char_u *t;
+           char_u *t;
 
-               vim_memset(buf, 0, LSIZE);
-               slen = copy_option_part(&p, buf, LSIZE, ","); // Advance p
-               if (slen > 0 && (t = vim_strchr(buf, '^')) != NULL)
-                   cpt_sources_array[count].max_matches = atoi((char *)t + 1);
-               count++;
-           }
+           vim_memset(buf, 0, LSIZE);
+           slen = copy_option_part(&p, buf, LSIZE, ","); // Advance p
+           if (slen > 0 && (t = vim_strchr(buf, '^')) != NULL)
+               cpt_sources_array[idx].cs_max_matches = atoi((char *)t + 1);
+           idx++;
        }
     }
     return OK;
@@ -6871,7 +6954,7 @@ is_cpt_func_refresh_always(void)
 {
 #ifdef FEAT_COMPL_FUNC
     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;
 #endif
     return FALSE;
@@ -6977,17 +7060,17 @@ remove_old_matches(void)
     static void
 get_cpt_func_completion_matches(callback_T *cb UNUSED)
 {
-    int            ret;
-    int            startcol;
+    int        startcol = cpt_sources_array[cpt_sources_index].cs_startcol;
 
     VIM_CLEAR_STRING(cpt_compl_pattern);
-    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)
+
+    if (startcol == -2 || startcol == -3)
+       return;
+
+    if (set_compl_globals(startcol, curwin->w_cursor.col, TRUE) == OK)
     {
        expand_by_function(0, cpt_compl_pattern.string, cb);
-       cpt_sources_array[cpt_sources_index].refresh_always =
+       cpt_sources_array[cpt_sources_index].cs_refresh_always =
            compl_opt_refresh_always;
        compl_opt_refresh_always = FALSE;
     }
@@ -7005,6 +7088,7 @@ cpt_compl_refresh(void)
     char_u     *cpt;
     char_u     *p;
     callback_T *cb = NULL;
+    int                startcol, ret;
 
     // Make the completion list linear (non-cyclic)
     ins_compl_make_linear();
@@ -7018,17 +7102,25 @@ cpt_compl_refresh(void)
        while (*p == ',' || *p == ' ') // Skip delimiters
            p++;
 
-       if (cpt_sources_array[cpt_sources_index].refresh_always)
+       if (cpt_sources_array[cpt_sources_index].cs_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;
+           cb = get_callback_if_cpt_func(p);
            if (cb)
            {
                compl_curr_match = remove_old_matches();
-               get_cpt_func_completion_matches(cb);
+               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);
            }
        }
 
diff --git a/src/version.c b/src/version.c
index 753012403..01c3050d1 100644
--- a/src/version.c
+++ b/src/version.c
@@ -709,6 +709,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    1422,
 /**/
     1421,
 /**/

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to vim_dev+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1uLmya-00BYKl-7I%40256bit.org.

Raspunde prin e-mail lui