0
0
mirror of https://github.com/vim/vim.git synced 2025-07-04 23:07:33 -04:00

feat: allow functions in 'complete' to trigger from non-keyword characters

Previously, functions specified in the `'complete'` option were restricted to
starting completion only from keyword characters (as introduced in
PR 17065). This change removes that restriction.

With this change, user-defined functions (e.g., `omnifunc`, `userfunc`) used
in `'complete'` can now initiate completion even when triggered from
non-keyword characters. This makes it easier to reuse existing functions
alongside other sources without having to consider whether the cursor is on a
keyword or non-keyword character, or worry about where the replacement should
begin (i.e., the `findstart=1` return value).

The logic for both the “collection” and “filtering” phases now fully respects
each source’s specified start column. This also extends to fuzzy matching,
making completions more predictable.

Internally, this builds on previously merged infrastructure that tracks
per-source metadata. This PR focuses on applying that metadata to compute the
leader string and insertion text appropriately for each match.

Also, a memory corruption has been fixed in prepare_cpt_compl_funcs().
This commit is contained in:
Girish Palya 2025-07-03 06:41:44 +02:00
parent 20eb68a8f2
commit 00cde6144e
4 changed files with 316 additions and 70 deletions

View File

@ -648,7 +648,7 @@ Completion can be done for:
10. User defined completion |i_CTRL-X_CTRL-U| 10. User defined completion |i_CTRL-X_CTRL-U|
11. omni completion |i_CTRL-X_CTRL-O| 11. omni completion |i_CTRL-X_CTRL-O|
12. Spelling suggestions |i_CTRL-X_s| 12. Spelling suggestions |i_CTRL-X_s|
13. keywords in 'complete' |i_CTRL-N| |i_CTRL-P| 13. completions from 'complete' |i_CTRL-N| |i_CTRL-P|
14. contents from registers |i_CTRL-X_CTRL-R| 14. contents from registers |i_CTRL-X_CTRL-R|
Additionally, |i_CTRL-X_CTRL-Z| stops completion without changing the text. Additionally, |i_CTRL-X_CTRL-Z| stops completion without changing the text.
@ -1103,25 +1103,23 @@ CTRL-X s Locate the word in front of the cursor and find the
previous one. previous one.
Completing keywords from different sources *compl-generic* Completing from different sources *compl-generic*
*i_CTRL-N* *i_CTRL-N*
CTRL-N Find next match for words that start with the CTRL-N Find the next match for a word ending at the cursor,
keyword in front of the cursor, looking in places using the sources specified in the 'complete' option.
specified with the 'complete' option. The found All sources complete from keywords, except functions,
keyword is inserted in front of the cursor. which may complete from non-keyword. The matched
text is inserted before the cursor.
*i_CTRL-P* *i_CTRL-P*
CTRL-P Find previous match for words that start with the CTRL-P Same as CTRL-N, but find the previous match.
keyword in front of the cursor, looking in places
specified with the 'complete' option. The found
keyword is inserted in front of the cursor.
CTRL-N Search forward for next matching keyword. This CTRL-N Search forward through the matches and insert the
keyword replaces the previous matching keyword. next one.
CTRL-P Search backwards for next matching keyword. This CTRL-P Search backward through the matches and insert the
keyword replaces the previous matching keyword. previous one.
CTRL-X CTRL-N or CTRL-X CTRL-N or
CTRL-X CTRL-P Further use of CTRL-X CTRL-N or CTRL-X CTRL-P will CTRL-X CTRL-P Further use of CTRL-X CTRL-N or CTRL-X CTRL-P will

View File

@ -2123,15 +2123,12 @@ A jump table for the options with a short description can be found at |Q_op|.
name of a function or a |Funcref|. For |Funcref| values, name of a function or a |Funcref|. For |Funcref| values,
spaces must be escaped with a backslash ('\'), and commas with spaces must be escaped with a backslash ('\'), and commas with
double backslashes ('\\') (see |option-backslash|). double backslashes ('\\') (see |option-backslash|).
Unlike other sources, functions can provide completions starting
from a non-keyword character before the cursor, and their
start position for replacing text may differ from other sources.
If the Dict returned by the {func} includes {"refresh": "always"}, If the Dict returned by the {func} includes {"refresh": "always"},
the function will be invoked again whenever the leading text the function will be invoked again whenever the leading text
changes. changes.
Completion matches are always inserted at the keyword
boundary, regardless of the column returned by {func} when
a:findstart is 1. This ensures compatibility with other
completion sources.
To make further modifications to the inserted text, {func}
can make use of |CompleteDonePre|.
If generating matches is potentially slow, |complete_check()| If generating matches is potentially slow, |complete_check()|
should be used to avoid blocking and preserve editor should be used to avoid blocking and preserve editor
responsiveness. responsiveness.

View File

@ -226,6 +226,7 @@ typedef struct cpt_source_T
int cs_max_matches; // Max items to display from this source int cs_max_matches; // Max items to display from this source
} cpt_source_T; } cpt_source_T;
#define STARTCOL_NONE -9
static cpt_source_T *cpt_sources_array; // Pointer to the array of completion sources static cpt_source_T *cpt_sources_array; // Pointer to the array of completion sources
static int cpt_sources_count; // Total number of completion sources specified in the 'cpt' option static int cpt_sources_count; // Total number of completion sources specified in the 'cpt' option
static int cpt_sources_index = -1; // Index of the current completion source being expanded static int cpt_sources_index = -1; // Index of the current completion source being expanded
@ -250,7 +251,7 @@ static void ins_compl_fixRedoBufForLeader(char_u *ptr_arg);
static void ins_compl_add_list(list_T *list); static void ins_compl_add_list(list_T *list);
static void ins_compl_add_dict(dict_T *dict); 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 int get_userdefined_compl_info(colnr_T curs_col, callback_T *cb, int *startcol);
static void get_cpt_func_completion_matches(callback_T *cb, int restore_leader); static void get_cpt_func_completion_matches(callback_T *cb);
static callback_T *get_callback_if_cpt_func(char_u *p); static callback_T *get_callback_if_cpt_func(char_u *p);
# endif # endif
static int setup_cpt_sources(void); static int setup_cpt_sources(void);
@ -1409,6 +1410,76 @@ cp_compare_nearest(const void* a, const void* b)
return (score_a > score_b) ? 1 : (score_a < score_b) ? -1 : 0; return (score_a > score_b) ? 1 : (score_a < score_b) ? -1 : 0;
} }
/*
* Constructs a new string by prepending text from the current line (from
* startcol to compl_col) to the given source string. Stores the result in
* dest. Returns OK or FAIL.
*/
static int
prepend_startcol_text(string_T *dest, string_T *src, int startcol)
{
int prepend_len = compl_col - startcol;
int new_length = prepend_len + (int)src->length;
dest->length = (size_t)new_length;
dest->string = alloc(new_length + 1); // +1 for NUL
if (dest->string == NULL)
{
dest->length = 0;
return FAIL;
}
char_u *line = ml_get(curwin->w_cursor.lnum);
mch_memmove(dest->string, line + startcol, prepend_len);
mch_memmove(dest->string + prepend_len, src->string, src->length);
dest->string[new_length] = NUL;
return OK;
}
/*
* Returns the completion leader string adjusted for a specific source's
* startcol. If the source's startcol is before compl_col, prepends text from
* the buffer line to the original compl_leader.
*/
static string_T *
get_leader_for_startcol(compl_T *match, int cached)
{
static string_T adjusted_leader = {NULL, 0};
if (match == NULL)
{
VIM_CLEAR_STRING(adjusted_leader);
return NULL;
}
if (cpt_sources_array == NULL || compl_leader.string == NULL)
goto theend;
int cpt_idx = match->cp_cpt_source_idx;
if (cpt_idx < 0 || compl_col <= 0)
goto theend;
int startcol = cpt_sources_array[cpt_idx].cs_startcol;
if (startcol >= 0 && startcol < compl_col)
{
int prepend_len = compl_col - startcol;
int new_length = prepend_len + (int)compl_leader.length;
if (cached && (size_t)new_length == adjusted_leader.length
&& adjusted_leader.string != NULL)
return &adjusted_leader;
VIM_CLEAR_STRING(adjusted_leader);
if (prepend_startcol_text(&adjusted_leader, &compl_leader,
startcol) != OK)
goto theend;
return &adjusted_leader;
}
theend:
return &compl_leader;
}
/* /*
* Set fuzzy score. * Set fuzzy score.
*/ */
@ -1421,11 +1492,13 @@ set_fuzzy_score(void)
|| compl_leader.string == NULL || compl_leader.length == 0) || compl_leader.string == NULL || compl_leader.length == 0)
return; return;
(void)get_leader_for_startcol(NULL, TRUE); // Clear the cache
compl = compl_first_match; compl = compl_first_match;
do do
{ {
compl->cp_score = fuzzy_match_str(compl->cp_str.string, compl->cp_score = fuzzy_match_str(compl->cp_str.string,
compl_leader.string); get_leader_for_startcol(compl, TRUE)->string);
compl = compl->cp_next; compl = compl->cp_next;
} while (compl != NULL && !is_first_match(compl)); } while (compl != NULL && !is_first_match(compl));
} }
@ -1487,6 +1560,7 @@ ins_compl_build_pum(void)
int *match_count = NULL; int *match_count = NULL;
int is_forward = compl_shows_dir_forward(); int is_forward = compl_shows_dir_forward();
int is_cpt_completion = (cpt_sources_array != NULL); int is_cpt_completion = (cpt_sources_array != NULL);
string_T *leader;
// Need to build the popup menu list. // Need to build the popup menu list.
compl_match_arraysize = 0; compl_match_arraysize = 0;
@ -1509,6 +1583,8 @@ ins_compl_build_pum(void)
return -1; return -1;
} }
(void)get_leader_for_startcol(NULL, TRUE); // Clear the cache
compl = compl_first_match; compl = compl_first_match;
do do
{ {
@ -1519,10 +1595,12 @@ ins_compl_build_pum(void)
&& !ignorecase(compl_leader.string) && !fuzzy_filter) && !ignorecase(compl_leader.string) && !fuzzy_filter)
compl->cp_flags &= ~CP_ICASE; compl->cp_flags &= ~CP_ICASE;
leader = get_leader_for_startcol(compl, TRUE);
if (!match_at_original_text(compl) if (!match_at_original_text(compl)
&& (compl_leader.string == NULL && (leader->string == NULL
|| ins_compl_equal(compl, compl_leader.string, || ins_compl_equal(compl, leader->string,
(int)compl_leader.length) (int)leader->length)
|| (fuzzy_filter && compl->cp_score > 0))) || (fuzzy_filter && compl->cp_score > 0)))
{ {
// Limit number of items from each source if max_items is set. // Limit number of items from each source if max_items is set.
@ -2317,6 +2395,7 @@ ins_compl_need_restart(void)
ins_compl_new_leader(void) ins_compl_new_leader(void)
{ {
int cur_cot_flags = get_cot_flags(); int cur_cot_flags = get_cot_flags();
ins_compl_del_pum(); ins_compl_del_pum();
ins_compl_delete(); ins_compl_delete();
ins_compl_insert_bytes(compl_leader.string + get_compl_len(), -1); ins_compl_insert_bytes(compl_leader.string + get_compl_len(), -1);
@ -4970,7 +5049,7 @@ get_next_completion_match(int type, ins_compl_next_state_T *st, pos_T *ini)
#ifdef FEAT_COMPL_FUNC #ifdef FEAT_COMPL_FUNC
case CTRL_X_FUNCTION: case CTRL_X_FUNCTION:
if (ctrl_x_mode_normal()) // Invoked by a func in 'cpt' option if (ctrl_x_mode_normal()) // Invoked by a func in 'cpt' option
get_cpt_func_completion_matches(st->func_cb, TRUE); get_cpt_func_completion_matches(st->func_cb);
else else
expand_by_function(type, compl_pattern.string, NULL); expand_by_function(type, compl_pattern.string, NULL);
break; break;
@ -5059,6 +5138,9 @@ prepare_cpt_compl_funcs(void)
{ {
while (*p == ',' || *p == ' ') // Skip delimiters while (*p == ',' || *p == ' ') // Skip delimiters
p++; p++;
if (*p == NUL)
break;
cb = get_callback_if_cpt_func(p); cb = get_callback_if_cpt_func(p);
if (cb) if (cb)
{ {
@ -5072,6 +5154,9 @@ prepare_cpt_compl_funcs(void)
} }
cpt_sources_array[idx].cs_startcol = startcol; cpt_sources_array[idx].cs_startcol = startcol;
} }
else
cpt_sources_array[idx].cs_startcol = STARTCOL_NONE;
(void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p (void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p
idx++; idx++;
} }
@ -5268,25 +5353,36 @@ ins_compl_get_exp(pos_T *ini)
static void static void
ins_compl_update_shown_match(void) ins_compl_update_shown_match(void)
{ {
string_T *leader;
(void)get_leader_for_startcol(NULL, TRUE); // Clear the cache
leader = get_leader_for_startcol(compl_shown_match, TRUE);
while (!ins_compl_equal(compl_shown_match, while (!ins_compl_equal(compl_shown_match,
compl_leader.string, (int)compl_leader.length) leader->string, (int)leader->length)
&& compl_shown_match->cp_next != NULL && compl_shown_match->cp_next != NULL
&& !is_first_match(compl_shown_match->cp_next)) && !is_first_match(compl_shown_match->cp_next))
{
compl_shown_match = compl_shown_match->cp_next; compl_shown_match = compl_shown_match->cp_next;
leader = get_leader_for_startcol(compl_shown_match, TRUE);
}
// If we didn't find it searching forward, and compl_shows_dir is // If we didn't find it searching forward, and compl_shows_dir is
// backward, find the last match. // backward, find the last match.
if (compl_shows_dir_backward() if (compl_shows_dir_backward()
&& !ins_compl_equal(compl_shown_match, && !ins_compl_equal(compl_shown_match,
compl_leader.string, (int)compl_leader.length) leader->string, (int)leader->length)
&& (compl_shown_match->cp_next == NULL && (compl_shown_match->cp_next == NULL
|| is_first_match(compl_shown_match->cp_next))) || is_first_match(compl_shown_match->cp_next)))
{ {
while (!ins_compl_equal(compl_shown_match, while (!ins_compl_equal(compl_shown_match,
compl_leader.string, (int)compl_leader.length) leader->string, (int)leader->length)
&& compl_shown_match->cp_prev != NULL && compl_shown_match->cp_prev != NULL
&& !is_first_match(compl_shown_match->cp_prev)) && !is_first_match(compl_shown_match->cp_prev))
{
compl_shown_match = compl_shown_match->cp_prev; compl_shown_match = compl_shown_match->cp_prev;
leader = get_leader_for_startcol(compl_shown_match, TRUE);
}
} }
} }
@ -5409,6 +5505,27 @@ ins_compl_insert(int move_cursor)
size_t leader_len = ins_compl_leader_len(); size_t leader_len = ins_compl_leader_len();
char_u *has_multiple = vim_strchr(cp_str, '\n'); char_u *has_multiple = vim_strchr(cp_str, '\n');
// Since completion sources may provide matches with varying start
// positions, insert only the portion of the match that corresponds to the
// intended replacement range.
if (cpt_sources_array != NULL)
{
int cpt_idx = compl_shown_match->cp_cpt_source_idx;
if (cpt_idx >= 0 && compl_col >= 0)
{
int startcol = cpt_sources_array[cpt_idx].cs_startcol;
if (startcol >= 0 && startcol < (int)compl_col)
{
int skip = (int)compl_col - startcol;
if ((size_t)skip <= cp_str_len)
{
cp_str_len -= skip;
cp_str += skip;
}
}
}
}
// Make sure we don't go over the end of the string, this can happen with // Make sure we don't go over the end of the string, this can happen with
// illegal bytes. // illegal bytes.
if (compl_len < (int)cp_str_len) if (compl_len < (int)cp_str_len)
@ -5509,11 +5626,13 @@ find_next_completion_match(
int advance, int advance,
int *num_matches) int *num_matches)
{ {
int found_end = FALSE; int found_end = FALSE;
compl_T *found_compl = NULL; compl_T *found_compl = NULL;
unsigned int cur_cot_flags = get_cot_flags(); unsigned int cur_cot_flags = get_cot_flags();
int compl_no_select = (cur_cot_flags & COT_NOSELECT) != 0; int compl_no_select = (cur_cot_flags & COT_NOSELECT) != 0;
int compl_fuzzy_match = (cur_cot_flags & COT_FUZZY) != 0; int compl_fuzzy_match = (cur_cot_flags & COT_FUZZY) != 0;
string_T *leader;
while (--todo >= 0) while (--todo >= 0)
{ {
@ -5581,10 +5700,13 @@ find_next_completion_match(
} }
found_end = FALSE; found_end = FALSE;
} }
leader = get_leader_for_startcol(compl_shown_match, FALSE);
if (!match_at_original_text(compl_shown_match) if (!match_at_original_text(compl_shown_match)
&& compl_leader.string != NULL && leader->string != NULL
&& !ins_compl_equal(compl_shown_match, && !ins_compl_equal(compl_shown_match,
compl_leader.string, (int)compl_leader.length) leader->string, (int)leader->length)
&& !(compl_fuzzy_match && compl_shown_match->cp_score > 0)) && !(compl_fuzzy_match && compl_shown_match->cp_score > 0))
++todo; ++todo;
else else
@ -5767,7 +5889,13 @@ ins_compl_check_keys(int frequency, int in_compl_func)
// Check for a typed key. Do use mappings, otherwise vim_is_ctrl_x_key() // Check for a typed key. Do use mappings, otherwise vim_is_ctrl_x_key()
// can't do its work correctly. // can't do its work correctly.
c = vpeekc_any(); c = vpeekc_any();
if (c != NUL) if (c != NUL
#ifdef FEAT_EVAL
// If test_override("char_avail", 1) was called, ignore characters
// waiting in the typeahead buffer.
&& !disable_char_avail_for_testing
#endif
)
{ {
if (vim_is_ctrl_x_key(c) && c != Ctrl_X && c != Ctrl_R) if (vim_is_ctrl_x_key(c) && c != Ctrl_X && c != Ctrl_R)
{ {
@ -6123,30 +6251,39 @@ set_compl_globals(
colnr_T curs_col UNUSED, colnr_T curs_col UNUSED,
int is_cpt_compl UNUSED) int is_cpt_compl UNUSED)
{ {
char_u *line = NULL; if (is_cpt_compl)
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; VIM_CLEAR_STRING(cpt_compl_pattern);
return FAIL; if (startcol < compl_col)
return prepend_startcol_text(&cpt_compl_pattern, &compl_orig_text,
startcol);
else
{
cpt_compl_pattern.string = vim_strnsave(compl_orig_text.string,
compl_orig_text.length);
cpt_compl_pattern.length = compl_orig_text.length;
}
} }
pattern->length = (size_t)len; else
if (!is_cpt_compl)
{ {
if (startcol < 0 || startcol > curs_col)
startcol = curs_col;
// Re-obtain line in case it has changed
char_u *line = ml_get(curwin->w_cursor.lnum);
int len = curs_col - startcol;
compl_pattern.string = vim_strnsave(line + startcol, (size_t)len);
if (compl_pattern.string == NULL)
{
compl_pattern.length = 0;
return FAIL;
}
compl_pattern.length = (size_t)len;
compl_col = startcol; compl_col = startcol;
compl_length = len; compl_length = len;
} }
return OK; return OK;
} }
#endif #endif
@ -6301,7 +6438,9 @@ compl_get_info(char_u *line, int startcol, colnr_T curs_col, int *line_invalid)
|| (ctrl_x_mode & CTRL_X_WANT_IDENT || (ctrl_x_mode & CTRL_X_WANT_IDENT
&& !thesaurus_func_complete(ctrl_x_mode))) && !thesaurus_func_complete(ctrl_x_mode)))
{ {
return get_normal_compl_info(line, startcol, curs_col); if (get_normal_compl_info(line, startcol, curs_col) != OK)
return FAIL;
*line_invalid = TRUE; // 'cpt' func may have invalidated "line"
} }
else if (ctrl_x_mode_line_or_eval()) else if (ctrl_x_mode_line_or_eval())
{ {
@ -6975,23 +7114,14 @@ remove_old_matches(void)
*/ */
#ifdef FEAT_COMPL_FUNC #ifdef FEAT_COMPL_FUNC
static void static void
get_cpt_func_completion_matches(callback_T *cb UNUSED, int restore_leader) get_cpt_func_completion_matches(callback_T *cb UNUSED)
{ {
int startcol = cpt_sources_array[cpt_sources_index].cs_startcol; int startcol = cpt_sources_array[cpt_sources_index].cs_startcol;
int result;
VIM_CLEAR_STRING(cpt_compl_pattern);
if (startcol == -2 || startcol == -3) if (startcol == -2 || startcol == -3)
return; return;
if (restore_leader) // Re-insert the text removed by ins_compl_delete() if (set_compl_globals(startcol, curwin->w_cursor.col, TRUE) == OK)
ins_compl_insert_bytes(compl_orig_text.string + get_compl_len(), -1);
result = set_compl_globals(startcol, curwin->w_cursor.col, TRUE);
if (restore_leader)
ins_compl_delete(); // Undo insertion
if (result == OK)
{ {
expand_by_function(0, cpt_compl_pattern.string, cb); expand_by_function(0, cpt_compl_pattern.string, cb);
cpt_sources_array[cpt_sources_index].cs_refresh_always = cpt_sources_array[cpt_sources_index].cs_refresh_always =
@ -7025,6 +7155,8 @@ cpt_compl_refresh(void)
{ {
while (*p == ',' || *p == ' ') // Skip delimiters while (*p == ',' || *p == ' ') // Skip delimiters
p++; p++;
if (*p == NUL)
break;
if (cpt_sources_array[cpt_sources_index].cs_refresh_always) if (cpt_sources_array[cpt_sources_index].cs_refresh_always)
{ {
@ -7044,8 +7176,11 @@ cpt_compl_refresh(void)
} }
cpt_sources_array[cpt_sources_index].cs_startcol = startcol; cpt_sources_array[cpt_sources_index].cs_startcol = startcol;
if (ret == OK) if (ret == OK)
get_cpt_func_completion_matches(cb, FALSE); get_cpt_func_completion_matches(cb);
} }
else
cpt_sources_array[cpt_sources_index].cs_startcol
= STARTCOL_NONE;
} }
(void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p (void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p

View File

@ -133,8 +133,7 @@ func Test_omni_dash()
%d %d
set complete=o set complete=o
exe "normal Gofind -\<C-n>" exe "normal Gofind -\<C-n>"
" 'complete' inserts at 'iskeyword' boundary (so you get --help) call assert_equal("find -help", getline('$'))
call assert_equal("find --help", getline('$'))
bwipe! bwipe!
delfunc Omni delfunc Omni
@ -392,7 +391,7 @@ func Test_CompleteDone_vevent_keys()
call assert_equal('spell', g:complete_type) call assert_equal('spell', g:complete_type)
bwipe! bwipe!
set completeopt& omnifunc& completefunc& spell& spelllang& dictionary& set completeopt& omnifunc& completefunc& spell& spelllang& dictionary& complete&
autocmd! CompleteDone autocmd! CompleteDone
delfunc OnDone delfunc OnDone
delfunc CompleteFunc delfunc CompleteFunc
@ -1037,6 +1036,7 @@ func Test_completefunc_invalid_data()
exe "normal i\<C-N>" exe "normal i\<C-N>"
call assert_equal('moon', getline(1)) call assert_equal('moon', getline(1))
set completefunc& complete& set completefunc& complete&
delfunc! CompleteFunc
bw! bw!
endfunc endfunc
@ -4861,4 +4861,120 @@ func Test_complete_fuzzy_omnifunc_backspace()
unlet g:do_complete unlet g:do_complete
endfunc endfunc
" Test 'complete' containing F{func} that complete from nonkeyword
func Test_nonkeyword_trigger()
" Trigger expansion even when another char is waiting in the typehead
call test_override("char_avail", 1)
let g:CallCount = 0
func! NonKeywordComplete(findstart, base)
let line = getline('.')->strpart(0, col('.') - 1)
let nonkeyword2 = len(line) > 1 && match(line[-2:-2], '\k') != 0
if a:findstart
return nonkeyword2 ? col('.') - 3 : (col('.') - 2)
else
let g:CallCount += 1
return [$"{a:base}foo", $"{a:base}bar"]
endif
endfunc
new
inoremap <buffer> <F2> <Cmd>let b:matches = complete_info(["matches"]).matches<CR>
inoremap <buffer> <F3> <Cmd>let b:selected = complete_info(["selected"]).selected<CR>
call setline(1, ['abc', 'abcd', 'fo', 'b', ''])
" Test 1a: Nonkeyword before cursor lists words with at least two letters
call feedkeys("GS=\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo'], b:matches->mapnew('v:val.word'))
call assert_equal('=abc', getline('.'))
" Test 1b: With F{func} nonkeyword collects matches
set complete=.,FNonKeywordComplete
for noselect in range(2)
if noselect
set completeopt+=noselect
endif
let g:CallCount = 0
call feedkeys("S=\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '=foo', '=bar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal(noselect ? '=' : '=abc', getline('.'))
let g:CallCount = 0
call feedkeys("S->\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '->foo', '->bar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal(noselect ? '->' : '->abc', getline('.'))
set completeopt&
endfor
" Test 1c: Keyword collects from {func}
let g:CallCount = 0
call feedkeys("Sa\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'afoo', 'abar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal('abc', getline('.'))
set completeopt+=noselect
let g:CallCount = 0
call feedkeys("Sa\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'afoo', 'abar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal('a', getline('.'))
" Test 1d: Nonkeyword after keyword collects items again
let g:CallCount = 0
call feedkeys("Sa\<C-N>#\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '#foo', '#bar'], b:matches->mapnew('v:val.word'))
call assert_equal(2, g:CallCount)
call assert_equal('a#', getline('.'))
set completeopt&
" Test 2: Filter nonkeyword and keyword matches with differet startpos
set completeopt+=menuone,noselect
call feedkeys("S#a\<C-N>b\<F2>\<F3>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', '#abar'], b:matches->mapnew('v:val.word'))
call assert_equal(-1, b:selected)
call assert_equal('#ab', getline('.'))
set completeopt+=fuzzy
call feedkeys("S#a\<C-N>b\<F2>\<F3>\<Esc>0", 'tx!')
call assert_equal(['#abar', 'abc', 'abcd'], b:matches->mapnew('v:val.word'))
call assert_equal(-1, b:selected)
call assert_equal('#ab', getline('.'))
set completeopt&
" Test 3: Navigate menu containing nonkeyword and keyword items
call feedkeys("S->\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '->foo', '->bar'], b:matches->mapnew('v:val.word'))
call assert_equal('->abc', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 3) . "\<Esc>0", 'tx!')
call assert_equal('->fo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 4) . "\<Esc>0", 'tx!')
call assert_equal('->foo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 4) . "\<C-P>\<Esc>0", 'tx!')
call assert_equal('->fo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 5) . "\<Esc>0", 'tx!')
call assert_equal('->bar', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 5) . "\<C-P>\<Esc>0", 'tx!')
call assert_equal('->foo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 6) . "\<Esc>0", 'tx!')
call assert_equal('->', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 7) . "\<Esc>0", 'tx!')
call assert_equal('->abc', getline('.'))
call feedkeys("S->" . repeat("\<C-P>", 7) . "\<Esc>0", 'tx!')
call assert_equal('->fo', getline('.'))
" Replace
call feedkeys("S# x y z\<Esc>0lR\<C-N>\<Esc>0", 'tx!')
call assert_equal('#abcy z', getline('.'))
call feedkeys("S# x y z\<Esc>0lR" . repeat("\<C-P>", 4) . "\<Esc>0", 'tx!')
call assert_equal('#bary z', getline('.'))
bw!
call test_override("char_avail", 0)
delfunc NonKeywordComplete
set complete&
unlet g:CallCount
endfunc
" vim: shiftwidth=2 sts=2 expandtab nofoldenable " vim: shiftwidth=2 sts=2 expandtab nofoldenable