commit 9c9c7ae30f50685e62984084767905c7d456cdd9
parent 33b0a004eb20fd5a0013b832414596190c059828
Author: zeertzjq <zeertzjq@outlook.com>
Date: Tue, 3 Jun 2025 07:00:25 +0800
Merge pull request #34247 from zeertzjq/vim-9.1.1301
vim-patch: various 'complete' features
Diffstat:
10 files changed, 1702 insertions(+), 99 deletions(-)
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
@@ -1178,6 +1178,9 @@ For example, the function can contain this: >
let matches = ... list of words ...
return {'words': matches, 'refresh': 'always'}
<
+If looking for matches is time-consuming, |complete_check()| may be used to
+maintain responsiveness.
+
*complete-items*
Each list item can either be a string or a Dictionary. When it is a string it
is used as the completion. When it is a Dictionary it can contain these
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -178,6 +178,11 @@ OPTIONS
• 'chistory' and 'lhistory' set size of the |quickfix-stack|.
• 'completefuzzycollect' enables fuzzy collection of candidates for (some)
|ins-completion| modes.
+• 'complete' new flags:
+ • "F{func}" complete using given function
+ • "F" complete using 'completefunc'
+ • "o" complete using 'omnifunc'
+• 'complete' allows limiting matches for sources using "{flag}^<limit>".
• 'completeopt' flag "nearset" sorts completion results by distance to cursor.
• 'diffopt' `inline:` configures diff highlighting for changes within a line.
• 'grepformat' is now a |global-local| option.
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -1506,6 +1506,28 @@ A jump table for the options with a short description can be found at |Q_op|.
] tag completion
t same as "]"
f scan the buffer names (as opposed to buffer contents)
+ F{func} call the function {func}. Multiple "F" flags may be specified.
+ Refer to |complete-functions| for details on how the function
+ is invoked and what it should return. The value can be the
+ name of a function or a |Funcref|. For |Funcref| values,
+ spaces must be escaped with a backslash ('\'), and commas with
+ double backslashes ('\\') (see |option-backslash|).
+ If the Dict returned by the {func} includes {"refresh": "always"},
+ the function will be invoked again whenever the leading text
+ 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()|
+ should be used to avoid blocking and preserve editor
+ responsiveness.
+ F equivalent to using "F{func}", where the function is taken from
+ the 'completefunc' option.
+ o equivalent to using "F{func}", where the function is taken from
+ the 'omnifunc' option.
Unloaded buffers are not loaded, thus their autocmds |:autocmd| are
not executed, this may lead to unexpected completions from some files
@@ -1516,6 +1538,13 @@ A jump table for the options with a short description can be found at |Q_op|.
based expansion (e.g., dictionary |i_CTRL-X_CTRL-K|, included patterns
|i_CTRL-X_CTRL-I|, tags |i_CTRL-X_CTRL-]| and normal expansions).
+ An optional match limit can be specified for a completion source by
+ appending a caret ("^") followed by a {count} to the source flag.
+ For example: ".^9,w,u,t^5" limits matches from the current buffer
+ to 9 and from tags to 5. Other sources remain unlimited.
+ Note: The match limit takes effect only during forward completion
+ (CTRL-N) and is ignored during backward completion (CTRL-P).
+
*'completefunc'* *'cfu'*
'completefunc' 'cfu' string (default "")
local to buffer
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -1027,6 +1027,28 @@ vim.bo.cms = vim.bo.commentstring
--- ] tag completion
--- t same as "]"
--- f scan the buffer names (as opposed to buffer contents)
+--- F{func} call the function {func}. Multiple "F" flags may be specified.
+--- Refer to `complete-functions` for details on how the function
+--- is invoked and what it should return. The value can be the
+--- name of a function or a `Funcref`. For `Funcref` values,
+--- spaces must be escaped with a backslash ('\'), and commas with
+--- double backslashes ('\\') (see `option-backslash`).
+--- If the Dict returned by the {func} includes {"refresh": "always"},
+--- the function will be invoked again whenever the leading text
+--- 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()`
+--- should be used to avoid blocking and preserve editor
+--- responsiveness.
+--- F equivalent to using "F{func}", where the function is taken from
+--- the 'completefunc' option.
+--- o equivalent to using "F{func}", where the function is taken from
+--- the 'omnifunc' option.
---
--- Unloaded buffers are not loaded, thus their autocmds `:autocmd` are
--- not executed, this may lead to unexpected completions from some files
@@ -1037,6 +1059,13 @@ vim.bo.cms = vim.bo.commentstring
--- based expansion (e.g., dictionary `i_CTRL-X_CTRL-K`, included patterns
--- `i_CTRL-X_CTRL-I`, tags `i_CTRL-X_CTRL-]` and normal expansions).
---
+--- An optional match limit can be specified for a completion source by
+--- appending a caret ("^") followed by a {count} to the source flag.
+--- For example: ".^9,w,u,t^5" limits matches from the current buffer
+--- to 9 and from tags to 5. Other sources remain unlimited.
+--- Note: The match limit takes effect only during forward completion
+--- (CTRL-N) and is ignored during backward completion (CTRL-P).
+---
--- @type string
vim.o.complete = ".,w,b,u,t"
vim.o.cpt = vim.o.complete
diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c
@@ -25,6 +25,7 @@
#include "nvim/edit.h"
#include "nvim/errors.h"
#include "nvim/eval.h"
+#include "nvim/eval/executor.h"
#include "nvim/eval/typval.h"
#include "nvim/eval/typval_defs.h"
#include "nvim/eval/userfunc.h"
@@ -174,6 +175,7 @@ struct compl_S {
bool cp_in_match_array; ///< collected by compl_match_array
int cp_user_abbr_hlattr; ///< highlight attribute for abbr
int cp_user_kind_hlattr; ///< highlight attribute for kind
+ int cp_cpt_source_idx; ///< index of this match's source in 'cpt' option
};
/// state information used for getting the next set of insert completion
@@ -190,6 +192,7 @@ typedef struct {
bool found_all; ///< found all matches of a certain type.
char *dict; ///< dictionary file to search
int dict_f; ///< "dict" is an exact file name or not
+ Callback *func_cb; ///< callback of function in 'cpt' option
} ins_compl_next_state_T;
#ifdef INCLUDE_GENERATED_DECLARATIONS
@@ -213,7 +216,7 @@ static const char e_compldel[] = N_("E840: Completion function deleted text");
// "compl_first_match" points to the start of the list.
// "compl_curr_match" points to the currently selected entry.
// "compl_shown_match" is different from compl_curr_match during
-// ins_compl_get_exp().
+// ins_compl_get_exp(), when new matches are added to the list.
// "compl_old_match" points to previous "compl_curr_match".
static compl_T *compl_first_match = NULL;
@@ -259,7 +262,8 @@ static bool compl_started = false;
static int ctrl_x_mode = CTRL_X_NORMAL;
static int compl_matches = 0; ///< number of completion matches
-static String compl_pattern = STRING_INIT;
+static String compl_pattern = STRING_INIT; ///< search pattern for matching items
+static String cpt_compl_pattern = STRING_INIT; ///< pattern returned by func in 'cpt'
static Direction compl_direction = FORWARD;
static Direction compl_shows_dir = FORWARD;
static int compl_pending = 0; ///< > 1 for postponed CTRL-N
@@ -302,6 +306,20 @@ static int compl_selected_item = -1;
static int *compl_fuzzy_scores;
+/// Define the structure for completion source (in 'cpt' option) information
+typedef struct cpt_source_T {
+ 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
+static cpt_source_T *cpt_sources_array;
+/// Total number of completion sources specified in the 'cpt' option
+static int cpt_sources_count;
+/// Index of the current completion source being expanded
+static int cpt_sources_index = -1;
+
// "compl_match_array" points the currently displayed list of entries in the
// popup menu. It is NULL when there is no popup menu.
static pumitem_T *compl_match_array = NULL;
@@ -975,7 +993,6 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons
match = xcalloc(1, sizeof(compl_T));
match->cp_number = flags & CP_ORIGINAL_TEXT ? 0 : -1;
match->cp_str = cbuf_to_string(str, (size_t)len);
- match->cp_score = score;
// match-fname is:
// - compl_curr_match->cp_fname if it is a string equal to fname.
@@ -995,6 +1012,8 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons
match->cp_flags = flags;
match->cp_user_abbr_hlattr = user_hl ? user_hl[0] : -1;
match->cp_user_kind_hlattr = user_hl ? user_hl[1] : -1;
+ match->cp_score = score;
+ match->cp_cpt_source_idx = cpt_sources_index;
if (cptext != NULL) {
for (int i = 0; i < CPT_COUNT; i++) {
@@ -1344,6 +1363,65 @@ static void trigger_complete_changed_event(int cur)
restore_v_event(v_event, &save_v_event);
}
+/// Trim compl_match_array to enforce max_matches per completion source.
+///
+/// Note: This special-case trimming is a workaround because compl_match_array
+/// becomes inconsistent with compl_first_match (list) after former is sorted by
+/// fuzzy score. The two structures end up in different orders.
+/// Ideally, compl_first_match list should have been sorted instead.
+///
+/// Returns recalculated index of shown match.
+static int trim_compl_match_array(int shown_match_idx)
+{
+ int remove_count = 0;
+
+ // Count current matches per source.
+ int *match_counts = xcalloc((size_t)cpt_sources_count, sizeof(int));
+ for (int i = 0; i < compl_match_arraysize; i++) {
+ int src_idx = compl_match_array[i].pum_cpt_source_idx;
+ if (src_idx != -1) {
+ match_counts[src_idx]++;
+ }
+ }
+
+ // 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].cs_max_matches;
+ new_size += (limit > 0 && match_counts[i] > limit) ? limit : match_counts[i];
+ }
+
+ if (new_size == compl_match_arraysize) {
+ goto theend;
+ }
+
+ // Create trimmed array while enforcing per-source limits
+ pumitem_T *trimmed = xcalloc((size_t)new_size, sizeof(pumitem_T));
+ memset(match_counts, 0, sizeof(int) * (size_t)cpt_sources_count);
+ int trimmed_idx = 0;
+ 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].cs_max_matches;
+ if (limit <= 0 || match_counts[src_idx] < limit) {
+ trimmed[trimmed_idx++] = compl_match_array[i];
+ match_counts[src_idx]++;
+ } else if (i < shown_match_idx) {
+ remove_count++;
+ }
+ } else {
+ trimmed[trimmed_idx++] = compl_match_array[i];
+ }
+ }
+ xfree(compl_match_array);
+ compl_match_array = trimmed;
+ compl_match_arraysize = new_size;
+
+theend:
+ xfree(match_counts);
+ return shown_match_idx - remove_count;
+}
+
/// pumitem qsort compare func
static int ins_compl_fuzzy_cmp(const void *a, const void *b)
{
@@ -1377,6 +1455,10 @@ static int ins_compl_build_pum(void)
bool fuzzy_sort = fuzzy_filter && !(cur_cot_flags & kOptCotFlagNosort);
compl_T *match_head = NULL, *match_tail = NULL;
+ int match_count = 0;
+ int cur_source = -1;
+ bool max_matches_found = false;
+ bool is_forward = compl_shows_dir_forward();
// If the current match is the original text don't find the first
// match after it, don't highlight anything.
@@ -1411,7 +1493,21 @@ static int ins_compl_build_pum(void)
comp->cp_flags &= ~CP_ICASE;
}
+ if (is_forward && !fuzzy_sort && comp->cp_cpt_source_idx != -1) {
+ if (cur_source != comp->cp_cpt_source_idx) {
+ cur_source = comp->cp_cpt_source_idx;
+ match_count = 1;
+ max_matches_found = false;
+ } else if (cpt_sources_array && !max_matches_found) {
+ int max_matches = cpt_sources_array[cur_source].cs_max_matches;
+ if (max_matches > 0 && match_count > max_matches) {
+ max_matches_found = true;
+ }
+ }
+ }
+
if (!match_at_original_text(comp)
+ && !max_matches_found
&& (compl_leader.data == NULL
|| ins_compl_equal(comp, compl_leader.data, compl_leader.size)
|| (fuzzy_filter && comp->cp_score > 0))) {
@@ -1454,6 +1550,9 @@ static int ins_compl_build_pum(void)
shown_match_ok = true;
}
}
+ if (is_forward && !fuzzy_sort && comp->cp_cpt_source_idx != -1) {
+ match_count++;
+ }
i++;
}
@@ -1495,6 +1594,7 @@ static int ins_compl_build_pum(void)
compl_match_array[i].pum_kind = comp->cp_text[CPT_KIND];
compl_match_array[i].pum_info = comp->cp_text[CPT_INFO];
compl_match_array[i].pum_score = comp->cp_score;
+ compl_match_array[i].pum_cpt_source_idx = comp->cp_cpt_source_idx;
compl_match_array[i].pum_user_abbr_hlattr = comp->cp_user_abbr_hlattr;
compl_match_array[i].pum_user_kind_hlattr = comp->cp_user_kind_hlattr;
compl_match_array[i++].pum_extra = comp->cp_text[CPT_MENU] != NULL
@@ -1514,6 +1614,9 @@ static int ins_compl_build_pum(void)
shown_match_ok = true;
}
+ if (fuzzy_sort && cpt_sources_array != NULL) {
+ cur = trim_compl_match_array(cur); // Truncate by max_matches in 'cpt'
+ }
if (!shown_match_ok) { // no displayed match at all
cur = -1;
}
@@ -1887,6 +1990,19 @@ char *find_line_end(char *ptr)
return s;
}
+/// Free a completion item in the list
+static void ins_compl_item_free(compl_T *match)
+{
+ API_CLEAR_STRING(match->cp_str);
+ // several entries may use the same fname, free it just once.
+ if (match->cp_flags & CP_FREE_FNAME) {
+ xfree(match->cp_fname);
+ }
+ free_cptext(match->cp_text);
+ tv_clear(&match->cp_user_data);
+ xfree(match);
+}
+
/// Free the list of completions
static void ins_compl_free(void)
{
@@ -1904,14 +2020,7 @@ static void ins_compl_free(void)
do {
compl_T *match = compl_curr_match;
compl_curr_match = compl_curr_match->cp_next;
- API_CLEAR_STRING(match->cp_str);
- // several entries may use the same fname, free it just once.
- if (match->cp_flags & CP_FREE_FNAME) {
- xfree(match->cp_fname);
- }
- free_cptext(match->cp_text);
- tv_clear(&match->cp_user_data);
- xfree(match);
+ ins_compl_item_free(match);
} while (compl_curr_match != NULL && !is_first_match(compl_curr_match));
compl_first_match = compl_curr_match = NULL;
compl_shown_match = NULL;
@@ -1935,6 +2044,7 @@ void ins_compl_clear(void)
kv_destroy(compl_orig_extmarks);
API_CLEAR_STRING(compl_orig_text);
compl_enter_selects = false;
+ cpt_sources_clear();
// clear v:completed_item
set_vim_var_dict(VV_COMPLETED_ITEM, tv_dict_alloc_lock(VAR_FIXED));
}
@@ -1952,8 +2062,8 @@ bool ins_compl_win_active(win_T *wp)
return ins_compl_active() && wp == compl_curr_win && wp->w_buffer == compl_curr_buf;
}
-/// Selected one of the matches. When false the match was edited or using the
-/// longest common string.
+/// Selected one of the matches. When false, the match was edited or
+/// using the longest common string.
bool ins_compl_used_match(void)
{
return compl_used_match;
@@ -2087,6 +2197,9 @@ static void ins_compl_new_leader(void)
if (compl_started) {
ins_compl_set_original_text(compl_leader.data, compl_leader.size);
+ if (is_cpt_func_refresh_always()) {
+ cpt_compl_refresh();
+ }
} else {
spell_bad_len = 0; // need to redetect bad word
// Matches were cleared, need to search for them now.
@@ -2170,6 +2283,7 @@ static void ins_compl_restart(void)
compl_matches = 0;
compl_cont_status = 0;
compl_cont_mode = 0;
+ cpt_sources_clear();
}
/// Set the first match, the original text.
@@ -2791,8 +2905,9 @@ static Callback *get_insert_callback(int type)
/// Execute user defined complete function 'completefunc', 'omnifunc' or
/// 'thesaurusfunc', and get matches in "matches".
///
-/// @param type either CTRL_X_OMNI or CTRL_X_FUNCTION or CTRL_X_THESAURUS
-static void expand_by_function(int type, char *base)
+/// @param type one of CTRL_X_OMNI or CTRL_X_FUNCTION or CTRL_X_THESAURUS
+/// @param cb set if triggered by a function in 'cpt' option, otherwise NULL
+static void expand_by_function(int type, char *base, Callback *cb)
{
list_T *matchlist = NULL;
dict_T *matchdict = NULL;
@@ -2800,9 +2915,14 @@ static void expand_by_function(int type, char *base)
const int save_State = State;
assert(curbuf != NULL);
- char *funcname = get_complete_funcname(type);
- if (*funcname == NUL) {
- return;
+
+ const bool is_cpt_function = (cb != NULL);
+ if (!is_cpt_function) {
+ char *funcname = get_complete_funcname(type);
+ if (*funcname == NUL) {
+ return;
+ }
+ cb = get_insert_callback(type);
}
// Call 'completefunc' to obtain the list of matches.
@@ -2819,8 +2939,6 @@ static void expand_by_function(int type, char *base)
// Insert mode in another buffer.
textlock++;
- Callback *cb = get_insert_callback(type);
-
// Call a function, which returns a list or dict.
if (callback_call(cb, 2, args, &rettv)) {
switch (rettv.v_type) {
@@ -3279,6 +3397,7 @@ static void get_complete_info(list_T *what_list, dict_T *retdict)
#define CI_WHAT_MATCHES 0x20
#define CI_WHAT_ALL 0xff
int what_flag;
+ const bool compl_fuzzy_match = (get_cot_flags() & kOptCotFlagFuzzy) != 0;
if (what_list == NULL) {
what_flag = CI_WHAT_ALL & ~(CI_WHAT_MATCHES|CI_WHAT_COMPLETED);
@@ -3345,7 +3464,9 @@ static void get_complete_info(list_T *what_list, dict_T *retdict)
&& compl_curr_match->cp_number == match->cp_number) {
selected_idx = list_idx;
}
- list_idx += 1;
+ if (compl_fuzzy_match || match->cp_in_match_array) {
+ list_idx += 1;
+ }
}
match = match->cp_next;
} while (match != NULL && !is_first_match(match));
@@ -3392,6 +3513,20 @@ static bool thesaurus_func_complete(int type)
&& (*curbuf->b_p_tsrfu != NUL || *p_tsrfu != NUL);
}
+/// Check if 'cpt' list index can be advanced to the next completion source.
+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++;
+ }
+ return (*p != NUL);
+}
+
/// Return value of process_next_cpt_value()
enum {
INS_COMPL_CPT_OK = 1,
@@ -3418,12 +3553,13 @@ enum {
/// the "st->e_cpt" option value and process the next matching source.
/// INS_COMPL_CPT_END if all the values in "st->e_cpt" are processed.
static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_arg,
- pos_T *start_match_pos, bool fuzzy_collect)
+ pos_T *start_match_pos, bool fuzzy_collect, bool *advance_cpt_idx)
{
int compl_type = -1;
int status = INS_COMPL_CPT_OK;
st->found_all = false;
+ *advance_cpt_idx = false;
while (*st->e_cpt == ',' || *st->e_cpt == ' ') {
st->e_cpt++;
@@ -3492,6 +3628,12 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar
st->dict = st->e_cpt;
st->dict_f = DICT_FIRST;
}
+ } 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);
+ if (!st->func_cb) {
+ compl_type = -1;
+ }
} else if (*st->e_cpt == 'i') {
compl_type = CTRL_X_PATH_PATTERNS;
} else if (*st->e_cpt == 'd') {
@@ -3510,6 +3652,7 @@ static int process_next_cpt_value(ins_compl_next_state_T *st, int *compl_type_ar
// in any case e_cpt is advanced to the next entry
copy_option_part(&st->e_cpt, IObuff, IOSIZE, ",");
+ *advance_cpt_idx = may_advance_cpt_index(st->e_cpt);
st->found_all = true;
if (compl_type == -1) {
@@ -3539,7 +3682,7 @@ static void get_next_include_file_completion(int compl_type)
static void get_next_dict_tsr_completion(int compl_type, char *dict, int dict_f)
{
if (thesaurus_func_complete(compl_type)) {
- expand_by_function(compl_type, compl_pattern.data);
+ expand_by_function(compl_type, compl_pattern.data, NULL);
} else {
ins_compl_dictionaries(dict != NULL
? dict
@@ -4102,6 +4245,42 @@ 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)
+{
+ 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;
+ } else {
+ return &curbuf->b_cfu_cb;
+ }
+ }
+ return NULL;
+}
+
+/// Retrieve new completion matches by invoking callback "cb".
+static void expand_cpt_function(Callback *cb)
+{
+ // Re-insert the text removed by ins_compl_delete().
+ ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1);
+ // Get matches
+ get_cpt_func_completion_matches(cb);
+ // Undo insertion
+ ins_compl_delete(false);
+}
+
/// get the next set of completion matches for "type".
/// @return true if a new match is found, otherwise false.
static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_T *ini)
@@ -4136,8 +4315,14 @@ static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_
break;
case CTRL_X_FUNCTION:
+ if (ctrl_x_mode_normal()) { // Invoked by a func in 'cpt' option
+ expand_cpt_function(st->func_cb);
+ } else {
+ expand_by_function(type, compl_pattern.data, NULL);
+ }
+ break;
case CTRL_X_OMNI:
- expand_by_function(type, compl_pattern.data);
+ expand_by_function(type, compl_pattern.data, NULL);
break;
case CTRL_X_SPELL:
@@ -4179,6 +4364,84 @@ static void get_next_bufname_token(void)
}
}
+/// Strips carets followed by numbers. This suffix typically represents the
+/// max_matches setting.
+static void strip_caret_numbers_in_place(char *str)
+{
+ char *read = str, *write = str, *p;
+
+ if (str == NULL) {
+ return;
+ }
+
+ while (*read) {
+ if (*read == '^') {
+ p = read + 1;
+ while (ascii_isdigit(*p)) {
+ p++;
+ }
+ if ((*p == ',' || *p == '\0') && p != read + 1) {
+ read = p;
+ continue;
+ } else {
+ *write++ = *read++;
+ }
+ } else {
+ *write++ = *read++;
+ }
+ }
+ *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)
+{
+ if (cpt_sources_index < cpt_sources_count - 1) {
+ cpt_sources_index++;
+ return OK;
+ }
+ semsg(_(e_list_index_out_of_range_nr), cpt_sources_index + 1);
+ return FAIL;
+}
+
/// Get the next expansion(s), using "compl_pattern".
/// The search starts at position "ini" in curbuf and in the direction
/// compl_direction.
@@ -4192,6 +4455,7 @@ static int ins_compl_get_exp(pos_T *ini)
static bool st_cleared = false;
int found_new_match;
int type = ctrl_x_mode;
+ bool may_advance_cpt_idx = false;
assert(curbuf != NULL);
@@ -4208,6 +4472,7 @@ static int ins_compl_get_exp(pos_T *ini)
xfree(st.e_cpt_copy);
// Make a copy of 'complete', in case the buffer is wiped out.
st.e_cpt_copy = xstrdup((compl_cont_status & CONT_LOCAL) ? "." : curbuf->b_p_cpt);
+ 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;
} else if (st.ins_buf != curbuf && !buf_valid(st.ins_buf)) {
@@ -4218,6 +4483,16 @@ 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'
while (true) {
found_new_match = FAIL;
@@ -4228,11 +4503,15 @@ static int ins_compl_get_exp(pos_T *ini)
// entries from 'complete' that look in loaded buffers.
if ((ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval())
&& (!compl_started || st.found_all)) {
- int status = process_next_cpt_value(&st, &type, ini, cfc_has_mode());
+ int status = process_next_cpt_value(&st, &type, ini,
+ cfc_has_mode(), &may_advance_cpt_idx);
if (status == INS_COMPL_CPT_END) {
break;
}
if (status == INS_COMPL_CPT_CONT) {
+ if (may_advance_cpt_idx && !advance_cpt_sources_index_safe()) {
+ break;
+ }
continue;
}
}
@@ -4246,6 +4525,10 @@ static int ins_compl_get_exp(pos_T *ini)
// get the next set of completion matches
found_new_match = get_next_completion_match(type, &st, ini);
+ if (may_advance_cpt_idx && !advance_cpt_sources_index_safe()) {
+ break;
+ }
+
// break the loop for specialized modes (use 'complete' just for the
// generic ctrl_x_mode == CTRL_X_NORMAL) or when we've found a new match
if ((ctrl_x_mode_not_default() && !ctrl_x_mode_line_or_eval())
@@ -4273,6 +4556,7 @@ static int ins_compl_get_exp(pos_T *ini)
compl_started = false;
}
}
+ cpt_sources_index = -1;
compl_started = true;
if ((ctrl_x_mode_normal() || ctrl_x_mode_line_or_eval())
@@ -4531,6 +4815,22 @@ static compl_T *find_comp_when_fuzzy(void)
return NULL;
}
+/// Find the appropriate completion item when 'complete' ('cpt') includes
+/// a 'max_matches' postfix. In this case, we search for a match where
+/// 'cp_in_match_array' is set, indicating that the match is also present
+/// in 'compl_match_array'.
+static compl_T *find_comp_when_cpt_sources(void)
+{
+ bool is_forward = compl_shows_dir_forward();
+ compl_T *match = compl_shown_match;
+
+ do {
+ match = is_forward ? match->cp_next : match->cp_prev;
+ } while (match->cp_next && !match->cp_in_match_array
+ && !match_at_original_text(match));
+ return match;
+}
+
/// Find the next set of matches for completion. Repeat the completion "todo"
/// times. The number of matches found is returned in 'num_matches'.
///
@@ -4551,19 +4851,30 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a
unsigned cur_cot_flags = get_cot_flags();
bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0;
bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0;
+ bool cpt_sources_active = compl_match_array && cpt_sources_array;
while (--todo >= 0) {
if (compl_shows_dir_forward() && compl_shown_match->cp_next != NULL) {
- compl_shown_match = compl_fuzzy_match && compl_match_array != NULL
- ? find_comp_when_fuzzy() : compl_shown_match->cp_next;
+ if (compl_match_array != NULL && compl_fuzzy_match) {
+ compl_shown_match = find_comp_when_fuzzy();
+ } else if (cpt_sources_active) {
+ compl_shown_match = find_comp_when_cpt_sources();
+ } else {
+ compl_shown_match = compl_shown_match->cp_next;
+ }
found_end = (compl_first_match != NULL
&& (is_first_match(compl_shown_match->cp_next)
|| is_first_match(compl_shown_match)));
} else if (compl_shows_dir_backward()
&& compl_shown_match->cp_prev != NULL) {
found_end = is_first_match(compl_shown_match);
- compl_shown_match = compl_fuzzy_match && compl_match_array != NULL
- ? find_comp_when_fuzzy() : compl_shown_match->cp_prev;
+ if (compl_match_array != NULL && compl_fuzzy_match) {
+ compl_shown_match = find_comp_when_fuzzy();
+ } else if (cpt_sources_active) {
+ compl_shown_match = find_comp_when_cpt_sources();
+ } else {
+ compl_shown_match = compl_shown_match->cp_prev;
+ }
found_end |= is_first_match(compl_shown_match);
} else {
if (!allow_get_expansion) {
@@ -5021,21 +5332,49 @@ 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
-static int get_userdefined_compl_info(colnr_T curs_col)
+///
+/// @param cb set if triggered by a function in the 'cpt' option, otherwise NULL
+/// @param startcol when not NULL, contains the column returned by function.
+static int get_userdefined_compl_info(colnr_T curs_col, Callback *cb, int *startcol)
{
// Call user defined function 'completefunc' with "a:findstart"
// set to 1 to obtain the length of text to use for completion.
const int save_State = State;
- // Call 'completefunc' or 'omnifunc' and get pattern length as a string
- char *funcname = get_complete_funcname(ctrl_x_mode);
- if (*funcname == NUL) {
- semsg(_(e_notset), ctrl_x_mode_function() ? "completefunc" : "omnifunc");
- return FAIL;
+ const bool is_cpt_function = (cb != NULL);
+ if (!is_cpt_function) {
+ // Call 'completefunc' or 'omnifunc' or 'thesaurusfunc' and get pattern
+ // length as a string
+ char *funcname = get_complete_funcname(ctrl_x_mode);
+ if (*funcname == NUL) {
+ semsg(_(e_notset), ctrl_x_mode_function() ? "completefunc" : "omnifunc");
+ return FAIL;
+ }
+ cb = get_insert_callback(ctrl_x_mode);
}
typval_T args[3];
@@ -5047,7 +5386,6 @@ static int get_userdefined_compl_info(colnr_T curs_col)
pos_T pos = curwin->w_cursor;
textlock++;
- Callback *cb = get_insert_callback(ctrl_x_mode);
colnr_T col = (colnr_T)callback_call_retnr(cb, 2, args);
textlock--;
@@ -5060,6 +5398,10 @@ static int get_userdefined_compl_info(colnr_T curs_col)
return FAIL;
}
+ if (startcol != NULL) {
+ *startcol = col;
+ }
+
// Return value -2 means the user complete function wants to cancel the
// complete without an error, do the same if the function did not execute
// successfully.
@@ -5069,6 +5411,9 @@ static int get_userdefined_compl_info(colnr_T curs_col)
// Return value -3 does the same as -2 and leaves CTRL-X mode.
if (col == -3) {
+ if (is_cpt_function) {
+ return FAIL;
+ }
ctrl_x_mode = CTRL_X_NORMAL;
edit_submode = NULL;
if (!shortmess(SHM_COMPLETIONMENU)) {
@@ -5081,20 +5426,9 @@ static int get_userdefined_compl_info(colnr_T curs_col)
// completion.
compl_opt_refresh_always = false;
- if (col < 0) {
- col = curs_col;
+ if (!is_cpt_function) {
+ set_compl_globals(col, curs_col, false);
}
- compl_col = col;
- if (compl_col > curs_col) {
- compl_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);
- compl_length = curs_col - compl_col;
- compl_pattern = cbuf_to_string(line + compl_col, (size_t)compl_length);
-
return OK;
}
@@ -5146,7 +5480,7 @@ static int compl_get_info(char *line, int startcol, colnr_T curs_col, bool *line
return get_cmdline_compl_info(line, curs_col);
} else if (ctrl_x_mode_function() || ctrl_x_mode_omni()
|| thesaurus_func_complete(ctrl_x_mode)) {
- if (get_userdefined_compl_info(curs_col) == FAIL) {
+ if (get_userdefined_compl_info(curs_col, NULL, NULL) != OK) {
return FAIL;
}
*line_invalid = true; // "line" may have become invalid
@@ -5576,3 +5910,210 @@ static void spell_back_to_badword(void)
start_arrow(&tpos);
}
}
+
+/// Reset the info associated with completion sources.
+static void cpt_sources_clear(void)
+{
+ XFREE_CLEAR(cpt_sources_array);
+ cpt_sources_index = -1;
+ cpt_sources_count = 0;
+}
+
+/// Setup completion sources.
+static void setup_cpt_sources(void)
+{
+ char buf[LSIZE];
+
+ int 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
+ (void)copy_option_part(&p, buf, LSIZE, ","); // Advance p
+ count++;
+ }
+ }
+ if (count == 0) {
+ return;
+ }
+
+ cpt_sources_clear();
+ cpt_sources_count = 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++;
+ }
+ }
+}
+
+/// Return true if any of the completion sources have 'refresh' set to 'always'.
+static bool is_cpt_func_refresh_always(void)
+{
+ for (int i = 0; i < cpt_sources_count; i++) {
+ if (cpt_sources_array[i].cs_refresh_always) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Make the completion list non-cyclic.
+static void ins_compl_make_linear(void)
+{
+ if (compl_first_match == NULL || compl_first_match->cp_prev == NULL) {
+ return;
+ }
+ compl_T *m = compl_first_match->cp_prev;
+ m->cp_next = NULL;
+ compl_first_match->cp_prev = NULL;
+}
+
+/// Remove the matches linked to the current completion source (as indicated by
+/// cpt_sources_index) from the completion list.
+static compl_T *remove_old_matches(void)
+{
+ compl_T *sublist_start = NULL, *sublist_end = NULL, *insert_at = NULL;
+ compl_T *current, *next;
+ bool compl_shown_removed = false;
+ bool forward = (compl_first_match->cp_cpt_source_idx < 0);
+
+ compl_direction = forward ? FORWARD : BACKWARD;
+ compl_shows_dir = compl_direction;
+
+ // Identify the sublist of old matches that needs removal
+ for (current = compl_first_match; current != NULL; current = current->cp_next) {
+ if (current->cp_cpt_source_idx < cpt_sources_index
+ && (forward || (!forward && !insert_at))) {
+ insert_at = current;
+ }
+
+ if (current->cp_cpt_source_idx == cpt_sources_index) {
+ if (!sublist_start) {
+ sublist_start = current;
+ }
+ sublist_end = current;
+ if (!compl_shown_removed && compl_shown_match == current) {
+ compl_shown_removed = true;
+ }
+ }
+
+ if ((forward && current->cp_cpt_source_idx > cpt_sources_index)
+ || (!forward && insert_at)) {
+ break;
+ }
+ }
+
+ // Re-assign compl_shown_match if necessary
+ if (compl_shown_removed) {
+ if (forward) {
+ compl_shown_match = compl_first_match;
+ } else { // Last node will have the prefix that is being completed
+ for (current = compl_first_match; current->cp_next != NULL;
+ current = current->cp_next) {}
+ compl_shown_match = current;
+ }
+ }
+
+ if (!sublist_start) { // No nodes to remove
+ return insert_at;
+ }
+
+ // Update links to remove sublist
+ if (sublist_start->cp_prev) {
+ sublist_start->cp_prev->cp_next = sublist_end->cp_next;
+ } else {
+ compl_first_match = sublist_end->cp_next;
+ }
+
+ if (sublist_end->cp_next) {
+ sublist_end->cp_next->cp_prev = sublist_start->cp_prev;
+ }
+
+ // Free all nodes in the sublist
+ sublist_end->cp_next = NULL;
+ for (current = sublist_start; current != NULL; current = next) {
+ next = current->cp_next;
+ ins_compl_item_free(current);
+ }
+
+ return insert_at;
+}
+
+/// Retrieve completion matches using the callback function "cb" and store the
+/// '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);
+
+ 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)
+{
+ // Make the completion list linear (non-cyclic)
+ ins_compl_make_linear();
+ // 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);
+
+ cpt_sources_index = 0;
+ for (char *p = cpt; *p;) {
+ while (*p == ',' || *p == ' ') { // Skip delimiters
+ p++;
+ }
+
+ 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();
+ 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);
+ }
+ }
+ }
+
+ (void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p
+ if (may_advance_cpt_index(p)) {
+ (void)advance_cpt_sources_index_safe();
+ }
+ }
+ cpt_sources_index = -1;
+
+ xfree(cpt);
+ // Make the list cyclic
+ compl_matches = ins_compl_make_cyclic();
+}
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
@@ -1412,7 +1412,7 @@ local options = {
abbreviation = 'cpt',
cb = 'did_set_complete',
defaults = '.,w,b,u,t',
- values = { '.', 'w', 'b', 'u', 'k', 'kspell', 's', 'i', 'd', ']', 't', 'U', 'f' },
+ values = { '.', 'w', 'b', 'u', 'k', 'kspell', 's', 'i', 'd', ']', 't', 'U', 'f', 'F', 'o' },
deny_duplicates = true,
desc = [=[
This option specifies how keyword completion |ins-completion| works
@@ -1438,6 +1438,28 @@ local options = {
] tag completion
t same as "]"
f scan the buffer names (as opposed to buffer contents)
+ F{func} call the function {func}. Multiple "F" flags may be specified.
+ Refer to |complete-functions| for details on how the function
+ is invoked and what it should return. The value can be the
+ name of a function or a |Funcref|. For |Funcref| values,
+ spaces must be escaped with a backslash ('\'), and commas with
+ double backslashes ('\\') (see |option-backslash|).
+ If the Dict returned by the {func} includes {"refresh": "always"},
+ the function will be invoked again whenever the leading text
+ 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()|
+ should be used to avoid blocking and preserve editor
+ responsiveness.
+ F equivalent to using "F{func}", where the function is taken from
+ the 'completefunc' option.
+ o equivalent to using "F{func}", where the function is taken from
+ the 'omnifunc' option.
Unloaded buffers are not loaded, thus their autocmds |:autocmd| are
not executed, this may lead to unexpected completions from some files
@@ -1447,6 +1469,13 @@ local options = {
As you can see, CTRL-N and CTRL-P can be used to do any 'iskeyword'-
based expansion (e.g., dictionary |i_CTRL-X_CTRL-K|, included patterns
|i_CTRL-X_CTRL-I|, tags |i_CTRL-X_CTRL-]| and normal expansions).
+
+ An optional match limit can be specified for a completion source by
+ appending a caret ("^") followed by a {count} to the source flag.
+ For example: ".^9,w,u,t^5" limits matches from the current buffer
+ to 9 and from tags to 5. Other sources remain unlimited.
+ Note: The match limit takes effect only during forward completion
+ (CTRL-N) and is ignored during backward completion (CTRL-P).
]=],
full_name = 'complete',
list = 'onecomma',
diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c
@@ -45,6 +45,7 @@
#include "nvim/spellfile.h"
#include "nvim/spellsuggest.h"
#include "nvim/strings.h"
+#include "nvim/tag.h"
#include "nvim/terminal.h"
#include "nvim/types_defs.h"
#include "nvim/vim_defs.h"
@@ -54,10 +55,12 @@
# include "optionstr.c.generated.h"
#endif
-static const char e_unclosed_expression_sequence[]
- = N_("E540: Unclosed expression sequence");
+static const char e_illegal_character_after_chr[]
+ = N_("E535: Illegal character after <%c>");
static const char e_comma_required[]
= N_("E536: Comma required");
+static const char e_unclosed_expression_sequence[]
+ = N_("E540: Unclosed expression sequence");
static const char e_unbalanced_groups[]
= N_("E542: Unbalanced groups");
static const char e_backupext_and_patchmode_are_equal[]
@@ -836,41 +839,67 @@ const char *did_set_commentstring(optset_T *args)
return NULL;
}
-/// The 'complete' option is changed.
+/// Check if value for 'complete' is valid when 'complete' option is changed.
const char *did_set_complete(optset_T *args)
{
char **varp = (char **)args->os_varp;
-
- // check if it is a valid value for 'complete' -- Acevedo
- for (char *s = *varp; *s;) {
- while (*s == ',' || *s == ' ') {
- s++;
- }
- if (!*s) {
- break;
+ char buffer[LSIZE];
+ uint8_t char_before = NUL;
+
+ for (char *p = *varp; *p;) {
+ memset(buffer, 0, LSIZE);
+ char *buf_ptr = buffer;
+ int escape = 0;
+
+ // Extract substring while handling escaped commas
+ while (*p && (*p != ',' || escape) && buf_ptr < (buffer + LSIZE - 1)) {
+ if (*p == '\\' && *(p + 1) == ',') {
+ escape = 1; // Mark escape mode
+ p++; // Skip '\'
+ } else {
+ escape = 0;
+ *buf_ptr++ = *p;
+ }
+ p++;
}
- if (vim_strchr(".wbuksid]tUf", (uint8_t)(*s)) == NULL) {
- return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*s));
+ *buf_ptr = NUL;
+
+ if (vim_strchr(".wbuksid]tUfFo", (uint8_t)(*buffer)) == NULL) {
+ return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*buffer));
}
- if (*++s != NUL && *s != ',' && *s != ' ') {
- if (s[-1] == 'k' || s[-1] == 's') {
- // skip optional filename after 'k' and 's'
- while (*s && *s != ',' && *s != ' ') {
- if (*s == '\\' && s[1] != NUL) {
- s++;
+
+ if (vim_strchr("ksF", (uint8_t)(*buffer)) == NULL && *(buffer + 1) != NUL
+ && *(buffer + 1) != '^') {
+ char_before = (uint8_t)(*buffer);
+ } else {
+ char *t;
+ // Test for a number after '^'
+ if ((t = vim_strchr(buffer, '^')) != NULL) {
+ *t++ = NUL;
+ if (!*t) {
+ char_before = '^';
+ } else {
+ for (; *t; t++) {
+ if (!ascii_isdigit(*t)) {
+ char_before = '^';
+ break;
+ }
}
- s++;
- }
- } else {
- if (args->os_errbuf != NULL) {
- vim_snprintf(args->os_errbuf, args->os_errbuflen,
- _("E535: Illegal character after <%c>"),
- *--s);
- return args->os_errbuf;
}
- return "";
}
}
+ 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 NULL;
+ }
+ // Skip comma and spaces
+ while (*p == ',' || *p == ' ') {
+ p++;
+ }
}
return NULL;
}
diff --git a/src/nvim/popupmenu.h b/src/nvim/popupmenu.h
@@ -16,6 +16,7 @@ typedef struct {
char *pum_info; ///< extra info
int pum_score; ///< fuzzy match score
int pum_idx; ///< index of item before sorting by score
+ int pum_cpt_source_idx; ///< index of completion source in 'cpt'
int pum_user_abbr_hlattr; ///< highlight attribute for abbr
int pum_user_kind_hlattr; ///< highlight attribute for kind
} pumitem_T;
diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim
@@ -134,10 +134,15 @@ func Test_omni_dash()
new
exe "normal Gofind -\<C-x>\<C-o>"
call assert_equal("find -help", getline('$'))
+ %d
+ set complete=o
+ exe "normal Gofind -\<C-n>"
+ " 'complete' inserts at 'iskeyword' boundary (so you get --help)
+ call assert_equal("find --help", getline('$'))
bwipe!
delfunc Omni
- set omnifunc=
+ set omnifunc= complete&
endfunc
func Test_omni_throw()
@@ -157,11 +162,21 @@ func Test_omni_throw()
call assert_exception('he he he')
call assert_equal(1, g:CallCount)
endtry
+ %d
+ set complete=o
+ let g:CallCount = 0
+ try
+ exe "normal ifoo\<C-n>"
+ call assert_false(v:true, 'command should have failed')
+ catch
+ call assert_exception('he he he')
+ call assert_equal(1, g:CallCount)
+ endtry
bwipe!
delfunc Omni
unlet g:CallCount
- set omnifunc=
+ set omnifunc= complete&
endfunc
func Test_completefunc_args()
@@ -184,6 +199,16 @@ func Test_completefunc_args()
call assert_equal(0, s:args[1][0])
set omnifunc=
+ set complete=FCompleteFunc
+ call feedkeys("i\<C-N>\<Esc>", 'x')
+ call assert_equal([1, 1], s:args[0])
+ call assert_equal(0, s:args[1][0])
+ set complete=o
+ call feedkeys("i\<C-N>\<Esc>", 'x')
+ call assert_equal([1, 1], s:args[0])
+ call assert_equal(0, s:args[1][0])
+ set complete&
+
bwipe!
unlet s:args
delfunc CompleteFunc
@@ -230,7 +255,7 @@ func s:CompleteDone_CheckCompletedItemDict(pre)
call assert_equal( ['one', 'two'], v:completed_item[ 'user_data' ] )
if a:pre
- call assert_equal('function', complete_info().mode)
+ call assert_equal(a:pre == 1 ? 'function' : 'keyword', complete_info().mode)
endif
let s:called_completedone = 1
@@ -248,7 +273,15 @@ func Test_CompleteDoneNone()
call assert_true(s:called_completedone)
call assert_equal(oldline, newline)
+ let s:called_completedone = 0
+
+ set complete=F<SID>CompleteDone_CompleteFuncNone
+ execute "normal a\<C-N>\<C-Y>"
+ set complete&
+ let newline = join(map(range(&columns), 'nr2char(screenchar(&lines-1, v:val+1))'), '')
+ call assert_true(s:called_completedone)
+ call assert_equal(oldline, newline)
let s:called_completedone = 0
au! CompleteDone
endfunc
@@ -269,6 +302,7 @@ func Test_CompleteDone_vevent_keys()
endfunc
set omnifunc=CompleteFunc
set completefunc=CompleteFunc
+ set complete=.,FCompleteFunc
set completeopt+=menuone
new
@@ -292,7 +326,11 @@ func Test_CompleteDone_vevent_keys()
call assert_equal('vim', g:complete_word)
call assert_equal('keyword', g:complete_type)
- call feedkeys("Shello vim visual v\<C-X>\<C-N>\<C-Y>", 'tx')
+ call feedkeys("Shello vim visual v\<C-N>\<ESC>", 'tx')
+ call assert_equal('', g:complete_word)
+ call assert_equal('keyword', g:complete_type)
+
+ call feedkeys("Shello vim visual v\<C-N>\<C-Y>", 'tx')
call assert_equal('vim', g:complete_word)
call assert_equal('keyword', g:complete_type)
@@ -350,6 +388,21 @@ func Test_CompleteDoneDict()
call assert_true(s:called_completedone)
let s:called_completedone = 0
+ au! CompleteDonePre
+ au! CompleteDone
+
+ au CompleteDonePre * :call <SID>CompleteDone_CheckCompletedItemDict(2)
+ au CompleteDone * :call <SID>CompleteDone_CheckCompletedItemDict(0)
+
+ set complete=.,F<SID>CompleteDone_CompleteFuncDict
+ execute "normal a\<C-N>\<C-Y>"
+ set complete&
+
+ call assert_equal(['one', 'two'], v:completed_item[ 'user_data' ])
+ call assert_true(s:called_completedone)
+
+ let s:called_completedone = 0
+ au! CompleteDonePre
au! CompleteDone
endfunc
@@ -393,6 +446,15 @@ func Test_CompleteDoneDictNoUserData()
call assert_true(s:called_completedone)
let s:called_completedone = 0
+
+ set complete=.,F<SID>CompleteDone_CompleteFuncDictNoUserData
+ execute "normal a\<C-N>\<C-Y>"
+ set complete&
+
+ call assert_equal('', v:completed_item[ 'user_data' ])
+ call assert_true(s:called_completedone)
+
+ let s:called_completedone = 0
au! CompleteDone
endfunc
@@ -426,6 +488,24 @@ func Test_CompleteDoneList()
call assert_true(s:called_completedone)
let s:called_completedone = 0
+
+ set complete=.,F<SID>CompleteDone_CompleteFuncList
+ execute "normal a\<C-N>\<C-Y>"
+ set complete&
+
+ call assert_equal('', v:completed_item[ 'user_data' ])
+ call assert_true(s:called_completedone)
+
+ let s:called_completedone = 0
+
+ set complete=.,F
+ execute "normal a\<C-N>\<C-Y>"
+ set complete&
+
+ call assert_equal('', v:completed_item[ 'user_data' ])
+ call assert_true(s:called_completedone)
+
+ let s:called_completedone = 0
au! CompleteDone
endfunc
@@ -468,11 +548,98 @@ func Test_completefunc_info()
set completefunc=CompleteTest
call feedkeys("i\<C-X>\<C-U>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
call assert_equal("matched{'pum_visible': 1, 'mode': 'function', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
+ %d
+ set complete=.,FCompleteTest
+ call feedkeys("i\<C-N>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
+ call assert_equal("matched{'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
+ %d
+ set complete=.,F
+ call feedkeys("i\<C-N>\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
+ call assert_equal("matched{'pum_visible': 1, 'mode': 'keyword', 'selected': 0, 'items': [{'word': 'matched', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}]}", getline(1))
+ set completeopt&
+ set complete&
+ set completefunc&
+endfunc
+
+func Test_cpt_func_cursorcol()
+ func CptColTest(findstart, query)
+ if a:findstart
+ call assert_equal("foo bar", getline(1))
+ call assert_equal(8, col('.'))
+ return col('.')
+ endif
+ call assert_equal("foo bar", getline(1))
+ call assert_equal(8, col('.'))
+ " return v:none
+ endfunc
+
+ set complete=FCptColTest
+ new
+ call feedkeys("ifoo bar\<C-N>", "tx")
+ bwipe!
+ new
+ set completeopt=longest
+ call feedkeys("ifoo bar\<C-N>", "tx")
+ bwipe!
+ new
+ set completeopt=menuone
+ call feedkeys("ifoo bar\<C-N>", "tx")
+ bwipe!
+ new
+ set completeopt=menuone,preinsert
+ call feedkeys("ifoo bar\<C-N>", "tx")
+ bwipe!
+ set complete& completeopt&
+ delfunc CptColTest
+endfunc
+
+func ScrollInfoWindowUserDefinedFn(findstart, query)
+ " User defined function (i_CTRL-X_CTRL-U)
+ if a:findstart
+ return col('.')
+ endif
+ let infostr = range(20)->mapnew({_, v -> string(v)})->join("\n")
+ return [{'word': 'foo', 'info': infostr}, {'word': 'bar'}]
+endfunc
+
+func ScrollInfoWindowPageDown()
+ call win_execute(popup_findinfo(), "normal! \<PageDown>")
+ return ''
+endfunc
+
+func ScrollInfoWindowPageUp()
+ call win_execute(popup_findinfo(), "normal! \<PageUp>")
+ return ''
+endfunc
+
+func ScrollInfoWindowTest(mvmt, count, fline)
+ new
+ set completeopt=menuone,popup,noinsert,noselect
+ set completepopup=height:5
+ set completefunc=ScrollInfoWindowUserDefinedFn
+ let keyseq = "i\<C-X>\<C-U>\<C-N>"
+ for _ in range(a:count)
+ let keyseq .= (a:mvmt == "pageup" ? "\<C-R>\<C-R>=ScrollInfoWindowPageUp()\<CR>" :
+ \ "\<C-R>\<C-R>=ScrollInfoWindowPageDown()\<CR>")
+ endfor
+ let keyseq .= "\<C-R>\<C-R>=string(popup_getpos(popup_findinfo()))\<CR>\<ESC>"
+ call feedkeys(keyseq, "tx")
+ call assert_match('''firstline'': ' . a:fline, getline(1))
bwipe!
set completeopt&
+ set completepopup&
set completefunc&
endfunc
+func Test_scroll_info_window()
+ throw 'Skipped: popup_findinfo() is N/A'
+ call ScrollInfoWindowTest("", 0, 1)
+ call ScrollInfoWindowTest("pagedown", 1, 4)
+ call ScrollInfoWindowTest("pagedown", 2, 7)
+ call ScrollInfoWindowTest("pagedown", 3, 11)
+ call ScrollInfoWindowTest("pageup", 3, 1)
+endfunc
+
func CompleteInfoUserDefinedFn(findstart, query)
" User defined function (i_CTRL-X_CTRL-U)
if a:findstart
@@ -482,24 +649,34 @@ func CompleteInfoUserDefinedFn(findstart, query)
endfunc
func CompleteInfoTestUserDefinedFn(mvmt, idx, noselect)
- new
if a:noselect
set completeopt=menuone,popup,noinsert,noselect
else
set completeopt=menu,preview
endif
- set completefunc=CompleteInfoUserDefinedFn
- call feedkeys("i\<C-X>\<C-U>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
- let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : ''
- call assert_equal(completed. "{'pum_visible': 1, 'mode': 'function', 'selected': " . a:idx . ", 'items': [" .
+ let items = "[" .
\ "{'word': 'foo', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'bar', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'baz', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}, " .
\ "{'word': 'qux', 'menu': '', 'user_data': '', 'info': '', 'kind': '', 'abbr': ''}" .
- \ "]}", getline(1))
+ \ "]"
+ new
+ set completefunc=CompleteInfoUserDefinedFn
+ call feedkeys("i\<C-X>\<C-U>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
+ let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : ''
+ call assert_equal(completed. "{'pum_visible': 1, 'mode': 'function', 'selected': " . a:idx . ", 'items': " . items . "}", getline(1))
+ %d
+ set complete=.,FCompleteInfoUserDefinedFn
+ call feedkeys("i\<C-N>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
+ let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : ''
+ call assert_equal(completed. "{'pum_visible': 1, 'mode': 'keyword', 'selected': " . a:idx . ", 'items': " . items . "}", getline(1))
+ %d
+ set complete=.,F
+ call feedkeys("i\<C-N>" . a:mvmt . "\<C-R>\<C-R>=string(complete_info())\<CR>\<ESC>", "tx")
+ let completed = a:idx != -1 ? ['foo', 'bar', 'baz', 'qux']->get(a:idx) : ''
+ call assert_equal(completed. "{'pum_visible': 1, 'mode': 'keyword', 'selected': " . a:idx . ", 'items': " . items . "}", getline(1))
bwipe!
- set completeopt&
- set completefunc&
+ set completeopt& completefunc& complete&
endfunc
func Test_complete_info_user_defined_fn()
@@ -867,6 +1044,10 @@ func Test_completefunc_error()
set completefunc=CompleteFunc
call setline(1, ['', 'abcd', ''])
call assert_fails('exe "normal 2G$a\<C-X>\<C-U>"', 'E565:')
+ set complete=FCompleteFunc
+ call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:')
+ set complete=F
+ call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:')
" delete text when called for the second time
func CompleteFunc2(findstart, base)
@@ -879,6 +1060,10 @@ func Test_completefunc_error()
set completefunc=CompleteFunc2
call setline(1, ['', 'abcd', ''])
call assert_fails('exe "normal 2G$a\<C-X>\<C-U>"', 'E565:')
+ set complete=FCompleteFunc2
+ call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:')
+ set complete=F
+ call assert_fails('exe "normal 2G$a\<C-N>"', 'E565:')
" Jump to a different window from the complete function
func CompleteFunc3(findstart, base)
@@ -891,9 +1076,15 @@ func Test_completefunc_error()
set completefunc=CompleteFunc3
new
call assert_fails('exe "normal a\<C-X>\<C-U>"', 'E565:')
+ %d
+ set complete=FCompleteFunc3
+ call assert_fails('exe "normal a\<C-N>"', 'E565:')
+ %d
+ set complete=F
+ call assert_fails('exe "normal a\<C-N>"', 'E565:')
close!
- set completefunc&
+ set completefunc& complete&
delfunc CompleteFunc
delfunc CompleteFunc2
delfunc CompleteFunc3
@@ -912,8 +1103,16 @@ func Test_completefunc_invalid_data()
set completefunc=CompleteFunc
exe "normal i\<C-X>\<C-U>"
call assert_equal('moon', getline(1))
- set completefunc&
- close!
+ %d
+ set complete=FCompleteFunc
+ exe "normal i\<C-N>"
+ call assert_equal('moon', getline(1))
+ %d
+ set complete=F
+ exe "normal i\<C-N>"
+ call assert_equal('moon', getline(1))
+ set completefunc& complete&
+ bw!
endfunc
" Test for errors in using complete() function
@@ -1589,18 +1788,363 @@ func Test_complete_item_refresh_always()
return #{words: res, refresh: 'always'}
endif
endfunc
- new
set completeopt=menu,longest
set completefunc=Tcomplete
+ new
exe "normal! iup\<C-X>\<C-U>\<BS>\<BS>\<BS>\<BS>\<BS>"
call assert_equal('up', getline(1))
call assert_equal(6, g:CallCount)
+ %d
+ let g:CallCount = 0
+ set complete=FTcomplete
+ exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>"
+ call assert_equal('up', getline(1))
+ call assert_equal(6, g:CallCount)
+ %d
+ let g:CallCount = 0
+ set complete=F
+ exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>"
+ call assert_equal('up', getline(1))
+ call assert_equal(6, g:CallCount)
+ %d
+ let g:CallCount = 0
+ set omnifunc=Tcomplete
+ set complete=o
+ exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>"
+ call assert_equal('up', getline(1))
+ call assert_equal(6, g:CallCount)
+ bw!
set completeopt&
+ set complete&
set completefunc&
- bw!
delfunc Tcomplete
endfunc
+" Test for 'cpt' user func that fails (return -2/-3) when refresh:always
+func Test_cpt_func_refresh_always_fail()
+ func! CompleteFail(retval, findstart, base)
+ if a:findstart
+ return a:retval
+ endif
+ call assert_equal(-999, a:findstart) " Should not reach here
+ endfunc
+ new
+ set complete=Ffunction('CompleteFail'\\,\ [-2])
+ exe "normal! ia\<C-N>"
+ %d
+ set complete=Ffunction('CompleteFail'\\,\ [-3])
+ exe "normal! ia\<C-N>"
+ bw!
+
+ func! CompleteFailIntermittent(retval, findstart, base)
+ if a:findstart
+ if g:CallCount == 2
+ let g:CallCount += 1
+ return a:retval
+ endif
+ return col('.') - 1
+ endif
+ let g:CallCount += 1
+ let res = [[], ['foo', 'fbar'], ['foo1', 'foo2'], ['foofail'], ['fooo3']]
+ return #{words: res[g:CallCount], refresh: 'always'}
+ endfunc
+ new
+ set completeopt=menuone,noselect
+ set complete=Ffunction('CompleteFailIntermittent'\\,\ [-2])
+ let g:CallCount = 0
+ exe "normal! if\<C-N>\<c-r>=complete_info([\"items\"])\<cr>"
+ call assert_match('''word'': ''foo''.*''word'': ''fbar''', getline(1))
+ call assert_equal(1, g:CallCount)
+ %d
+ let g:CallCount = 0
+ exe "normal! if\<C-N>o\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
+ call assert_match('''selected'': -1.*''word'': ''foo1''.*''word'': ''foo2''', getline(1))
+ call assert_equal(2, g:CallCount)
+ %d
+ set complete=Ffunction('CompleteFailIntermittent'\\,\ [-3])
+ let g:CallCount = 0
+ exe "normal! if\<C-N>o\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
+ call assert_match('''selected'': -1.*''word'': ''foo1''.*''word'': ''foo2''', getline(1))
+ call assert_equal(2, g:CallCount)
+ %d
+ set complete=Ffunction('CompleteFailIntermittent'\\,\ [-2])
+ " completion mode is dismissed when there are no matches in list
+ let g:CallCount = 0
+ exe "normal! if\<C-N>oo\<c-r>=complete_info([\"items\"])\<cr>"
+ call assert_equal('foo{''items'': []}', getline(1))
+ call assert_equal(3, g:CallCount)
+ %d
+ let g:CallCount = 0
+ exe "normal! if\<C-N>oo\<bs>\<c-r>=complete_info([\"items\"])\<cr>"
+ call assert_equal('fo{''items'': []}', getline(1))
+ call assert_equal(3, g:CallCount)
+ %d
+ " completion mode continues when matches from other sources present
+ set complete=.,Ffunction('CompleteFailIntermittent'\\,\ [-2])
+ call setline(1, 'fooo1')
+ let g:CallCount = 0
+ exe "normal! Gof\<C-N>oo\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
+ call assert_equal('foo{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', '
+ \ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
+ \ getline(2))
+ call assert_equal(3, g:CallCount)
+ %d
+ call setline(1, 'fooo1')
+ let g:CallCount = 0
+ exe "normal! Gof\<C-N>oo\<bs>\<c-r>=complete_info([\"items\"])\<cr>"
+ call assert_match('''word'': ''fooo1''.*''word'': ''fooo3''', getline(2))
+ call assert_equal(4, g:CallCount)
+ %d
+ " refresh will stop when -3 is returned
+ set complete=.,,\ Ffunction('CompleteFailIntermittent'\\,\ [-3])
+ call setline(1, 'fooo1')
+ let g:CallCount = 0
+ exe "normal! Gof\<C-N>o\<bs>\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', '
+ \ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
+ \ getline(2))
+ call assert_equal(3, g:CallCount)
+ %d
+ call setline(1, 'fooo1')
+ let g:CallCount = 0
+ exe "normal! Gof\<C-N>oo\<bs>\<c-r>=complete_info([\"items\", \"selected\"])\<cr>"
+ call assert_equal('fo{''selected'': -1, ''items'': [{''word'': ''fooo1'', ''menu'': '''', '
+ \ . '''user_data'': '''', ''info'': '''', ''kind'': '''', ''abbr'': ''''}]}',
+ \ getline(2))
+ call assert_equal(3, g:CallCount)
+ bw!
+
+ set complete& completeopt&
+ delfunc CompleteFail
+ delfunc CompleteFailIntermittent
+endfunc
+
+" Select items before they are removed by refresh:always
+func Test_cpt_select_item_refresh_always()
+
+ func CompleteMenuWords()
+ let info = complete_info(["items", "selected"])
+ call map(info.items, {_, v -> v.word})
+ return info
+ endfunc
+
+ func! CompleteItemsSelect(compl, findstart, base)
+ if a:findstart
+ return col('.') - 1
+ endif
+ let g:CallCount += 1
+ if g:CallCount == 2
+ return #{words: a:compl, refresh: 'always'}
+ endif
+ let res = [[], ['fo', 'foobar'], [], ['foo1', 'foo2']]
+ return #{words: res[g:CallCount], refresh: 'always'}
+ endfunc
+
+ new
+ set complete=.,Ffunction('CompleteItemsSelect'\\,\ [[]])
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-n>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('fo{''selected'': 1, ''items'': [''foobarbar'', ''fo'', ''foobar'']}', getline(2))
+ call assert_equal(1, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-p>\<c-p>\<c-p>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('fo{''selected'': 0, ''items'': [''fo'', ''foobar'', ''foobarbar'']}', getline(2))
+ call assert_equal(1, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-n>o\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('foo{''selected'': -1, ''items'': []}' , getline(2))
+ call assert_equal(1, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-p>\<c-p>\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+
+ %d
+ set complete=.,Ffunction('CompleteItemsSelect'\\,\ [['foonext']])
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'', ''foonext'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-p>\<c-p>\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [''foonext'', ''foobarbar'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+
+ %d
+ call setline(1, "foob")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('foo{''selected'': 0, ''items'': [''foob'', ''foonext'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+ %d
+ call setline(1, "foob")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<bs>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('fo{''selected'': 0, ''items'': [''foob'', ''foo1'', ''foo2'']}', getline(2))
+ call assert_equal(3, g:CallCount)
+
+ %d
+ call setline(1, "foob")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('foo{''selected'': 1, ''items'': [''foonext'', ''foob'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+ %d
+ call setline(1, "foob")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-p>\<bs>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('fo{''selected'': 2, ''items'': [''foo1'', ''foo2'', ''foob'']}', getline(2))
+ call assert_equal(3, g:CallCount)
+
+ %d
+ set complete=.,Ffunction('CompleteItemsSelect'\\,\ [['fo'\\,\ 'foonext']])
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-n>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [''foobarbar'', ''fo'', ''foonext'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-p>\<c-p>\<c-p>\<bs>\<c-r>=CompleteMenuWords()\<cr>"
+ call assert_equal('f{''selected'': -1, ''items'': [''fo'', ''foonext'', ''foobarbar'']}', getline(2))
+ call assert_equal(2, g:CallCount)
+ bw!
+
+ set complete&
+ delfunc CompleteMenuWords
+ delfunc CompleteItemsSelect
+endfunc
+
+" Test two functions together, each returning refresh:always
+func Test_cpt_multi_func_refresh_always()
+
+ func CompleteMenuMatches()
+ let info = complete_info(["matches", "selected"])
+ call map(info.matches, {_, v -> v.word})
+ return info
+ endfunc
+
+ func! CompleteItems1(findstart, base)
+ if a:findstart
+ return col('.') - 1
+ endif
+ let g:CallCount1 += 1
+ let res = [[], [], ['foo1', 'foobar1'], [], ['foo11', 'foo12'], [], ['foo13', 'foo14']]
+ return #{words: res[g:CallCount1], refresh: 'always'}
+ endfunc
+
+ func! CompleteItems2(findstart, base)
+ if a:findstart
+ return col('.') - 1
+ endif
+ let g:CallCount2 += 1
+ let res = [[], [], [], ['foo2', 'foobar2'], ['foo21', 'foo22'], ['foo23'], []]
+ return #{words: res[g:CallCount2], refresh: 'always'}
+ endfunc
+
+ set complete=
+ exe "normal! if\<C-N>\<c-r>=CompleteMenuMatches()\<cr>"
+ " \x0e is <c-n>
+ call assert_equal("f\x0e" . '{''matches'': [], ''selected'': -1}', getline(1))
+
+ set completeopt=menuone,noselect
+ set complete=FCompleteItems1,FCompleteItems2
+
+ new
+ let g:CallCount1 = 0
+ let g:CallCount2 = 0
+ exe "normal! if\<c-n>o\<c-n>o\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('foo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1))
+ call assert_equal(3, g:CallCount1)
+ call assert_equal(3, g:CallCount2)
+ %d
+ let g:CallCount1 = 0
+ let g:CallCount2 = 0
+ exe "normal! if\<c-p>o\<c-p>o\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('foo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1))
+ call assert_equal(3, g:CallCount1)
+ call assert_equal(3, g:CallCount2)
+ %d
+ let g:CallCount1 = 0
+ let g:CallCount2 = 0
+ exe "normal! if\<c-p>\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('f{''matches'': [], ''selected'': -1}', getline(1))
+ call assert_equal(1, g:CallCount1)
+ call assert_equal(1, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-n>\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('f{''matches'': [''foo1'', ''foobar1''], ''selected'': -1}', getline(1))
+ call assert_equal(2, g:CallCount2)
+ call assert_equal(2, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-n>o\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('fo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1))
+ call assert_equal(3, g:CallCount2)
+ call assert_equal(3, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-p>o\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('fo{''matches'': [''foo2'', ''foobar2''], ''selected'': -1}', getline(1))
+ call assert_equal(3, g:CallCount2)
+ call assert_equal(3, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-n>oo\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('foo{''matches'': [''foo11'', ''foo12'', ''foo21'', ''foo22''], ''selected'': -1}', getline(1))
+ call assert_equal(4, g:CallCount2)
+ call assert_equal(4, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-n>oo\<bs>\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('fo{''matches'': [''foo23''], ''selected'': -1}', getline(1))
+ call assert_equal(5, g:CallCount2)
+ call assert_equal(5, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-p>oo\<bs>\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('fo{''matches'': [''foo23''], ''selected'': -1}', getline(1))
+ call assert_equal(5, g:CallCount2)
+ call assert_equal(5, g:CallCount2)
+ %d
+ let g:CallCount1 = 1
+ let g:CallCount2 = 1
+ exe "normal! if\<c-n>oo\<bs>o\<c-r>=CompleteMenuMatches()\<cr>"
+ call assert_equal('foo{''matches'': [''foo13'', ''foo14''], ''selected'': -1}', getline(1))
+ call assert_equal(6, g:CallCount2)
+ call assert_equal(6, g:CallCount2)
+ bw!
+
+ set complete& completeopt&
+ delfunc CompleteMenuMatches
+ delfunc CompleteItems1
+ delfunc CompleteItems2
+endfunc
+
" Test for completing from a thesaurus file without read permission
func Test_complete_unreadable_thesaurus_file()
CheckUnix
@@ -1640,6 +2184,143 @@ func Test_no_mapping_for_ctrl_x_key()
bwipe!
endfunc
+" Test for different ways of setting a function in 'complete' option
+func Test_cpt_func_callback()
+ func CompleteFunc1(callnr, findstart, base)
+ call add(g:CompleteFunc1Args, [a:callnr, a:findstart, a:base])
+ return a:findstart ? 0 : []
+ endfunc
+ func CompleteFunc2(findstart, base)
+ call add(g:CompleteFunc2Args, [a:findstart, a:base])
+ return a:findstart ? 0 : []
+ endfunc
+
+ let lines =<< trim END
+ #" Test for using a global function name
+ set complete=Fg:CompleteFunc2
+ new
+ call setline(1, 'global')
+ LET g:CompleteFunc2Args = []
+ call feedkeys("A\<C-N>\<Esc>", 'x')
+ call assert_equal([[1, ''], [0, 'global']], g:CompleteFunc2Args)
+ set complete&
+ bw!
+
+ #" Test for using a function()
+ set complete=Ffunction('g:CompleteFunc1'\\,\ [10])
+ new
+ call setline(1, 'one')
+ LET g:CompleteFunc1Args = []
+ call feedkeys("A\<C-N>\<Esc>", 'x')
+ call assert_equal([[10, 1, ''], [10, 0, 'one']], g:CompleteFunc1Args)
+ set complete&
+ bw!
+
+ #" Using a funcref variable
+ set complete=Ffuncref('g:CompleteFunc1'\\,\ [11])
+ new
+ call setline(1, 'two')
+ LET g:CompleteFunc1Args = []
+ call feedkeys("A\<C-N>\<Esc>", 'x')
+ call assert_equal([[11, 1, ''], [11, 0, 'two']], g:CompleteFunc1Args)
+ set complete&
+ bw!
+
+ END
+ call CheckLegacyAndVim9Success(lines)
+
+ " Test for using a script-local function name
+ func s:CompleteFunc3(findstart, base)
+ call add(g:CompleteFunc3Args, [a:findstart, a:base])
+ return a:findstart ? 0 : []
+ endfunc
+ set complete=Fs:CompleteFunc3
+ new
+ call setline(1, 'script1')
+ let g:CompleteFunc3Args = []
+ call feedkeys("A\<C-N>\<Esc>", 'x')
+ call assert_equal([[1, ''], [0, 'script1']], g:CompleteFunc3Args)
+ set complete&
+ bw!
+
+ let &complete = 'Fs:CompleteFunc3'
+ new
+ call setline(1, 'script2')
+ let g:CompleteFunc3Args = []
+ call feedkeys("A\<C-N>\<Esc>", 'x')
+ call assert_equal([[1, ''], [0, 'script2']], g:CompleteFunc3Args)
+ bw!
+ delfunc s:CompleteFunc3
+ set complete&
+
+ " In Vim9 script s: can be omitted
+ let lines =<< trim END
+ vim9script
+ var CompleteFunc4Args = []
+ def CompleteFunc4(findstart: bool, base: string): any
+ add(CompleteFunc4Args, [findstart, base])
+ return findstart ? 0 : []
+ enddef
+ set complete=FCompleteFunc4
+ new
+ setline(1, 'script1')
+ feedkeys("A\<C-N>\<Esc>", 'x')
+ assert_equal([[1, ''], [0, 'script1']], CompleteFunc4Args)
+ set complete&
+ bw!
+ END
+ call CheckScriptSuccess(lines)
+
+ " Vim9 tests
+ let lines =<< trim END
+ vim9script
+
+ def Vim9CompleteFunc(callnr: number, findstart: number, base: string): any
+ add(g:Vim9completeFuncArgs, [callnr, findstart, base])
+ return findstart ? 0 : []
+ enddef
+
+ # Test for using a def function with completefunc
+ set complete=Ffunction('Vim9CompleteFunc'\\,\ [60])
+ new | only
+ setline(1, 'one')
+ g:Vim9completeFuncArgs = []
+ feedkeys("A\<C-N>\<Esc>", 'x')
+ assert_equal([[60, 1, ''], [60, 0, 'one']], g:Vim9completeFuncArgs)
+ bw!
+
+ # Test for using a global function name
+ &complete = 'Fg:CompleteFunc2'
+ new | only
+ setline(1, 'two')
+ g:CompleteFunc2Args = []
+ feedkeys("A\<C-N>\<Esc>", 'x')
+ assert_equal([[1, ''], [0, 'two']], g:CompleteFunc2Args)
+ bw!
+
+ # Test for using a script-local function name
+ def LocalCompleteFunc(findstart: number, base: string): any
+ add(g:LocalCompleteFuncArgs, [findstart, base])
+ return findstart ? 0 : []
+ enddef
+ &complete = 'FLocalCompleteFunc'
+ new | only
+ setline(1, 'three')
+ g:LocalCompleteFuncArgs = []
+ feedkeys("A\<C-N>\<Esc>", 'x')
+ assert_equal([[1, ''], [0, 'three']], g:LocalCompleteFuncArgs)
+ bw!
+ END
+ call CheckScriptSuccess(lines)
+
+ " cleanup
+ set completefunc& complete&
+ delfunc CompleteFunc1
+ delfunc CompleteFunc2
+ unlet g:CompleteFunc1Args g:CompleteFunc2Args
+ %bw!
+endfunc
+
" Test for different ways of setting the 'completefunc' option
func Test_completefunc_callback()
func CompleteFunc1(callnr, findstart, base)
@@ -2517,10 +3198,19 @@ endfunc
func Test_complete_smartindent()
new
setlocal smartindent completefunc=FooBarComplete
-
exe "norm! o{\<cr>\<c-x>\<c-u>\<c-p>}\<cr>\<esc>"
let result = getline(1,'$')
call assert_equal(['', '{','}',''], result)
+ %d
+ setlocal complete=FFooBarComplete
+ exe "norm! o{\<cr>\<c-n>\<c-p>}\<cr>\<esc>"
+ let result = getline(1,'$')
+ call assert_equal(['', '{','}',''], result)
+ %d
+ setlocal complete=F
+ exe "norm! o{\<cr>\<c-n>\<c-p>}\<cr>\<esc>"
+ let result = getline(1,'$')
+ call assert_equal(['', '{','}',''], result)
bw!
delfunction! FooBarComplete
endfunc
@@ -3468,6 +4158,199 @@ func Test_complete_multiline_marks()
delfunc Omni_test
endfunc
+func Test_complete_match_count()
+ func! PrintMenuWords()
+ let info = complete_info(["selected", "matches"])
+ call map(info.matches, {_, v -> v.word})
+ return info
+ endfunc
+
+ new
+ set cpt=.^0,w
+ call setline(1, ["fo", "foo", "foobar", "fobarbaz"])
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo'', ''foo'', ''foobar'', ''fobarbaz''], ''selected'': 0}', getline(5))
+ 5d
+ set cpt=.^0,w
+ exe "normal! Gof\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fobarbaz{''matches'': [''fo'', ''foo'', ''foobar'', ''fobarbaz''], ''selected'': 3}', getline(5))
+ 5d
+ set cpt=.^1,w
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo''], ''selected'': 0}', getline(5))
+ 5d
+ " max_matches is ignored for backward search
+ exe "normal! Gof\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fobarbaz{''matches'': [''fo'', ''foo'', ''foobar'', ''fobarbaz''], ''selected'': 3}', getline(5))
+ 5d
+ set cpt=.^2,w
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo'', ''foo''], ''selected'': 0}', getline(5))
+ 5d
+ set cot=menuone,noselect
+ set cpt=.^1,w
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('f{''matches'': [''fo''], ''selected'': -1}', getline(5))
+ set cot&
+
+ func ComplFunc(findstart, base)
+ if a:findstart
+ return col(".")
+ endif
+ return ["foo1", "foo2", "foo3", "foo4"]
+ endfunc
+
+ %d
+ set completefunc=ComplFunc
+ set cpt=.^1,F^2
+ call setline(1, ["fo", "foo", "foobar", "fobarbaz"])
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5))
+ 5d
+ set cpt=.^1,,,F^2,,,
+ call setline(1, ["fo", "foo", "foobar", "fobarbaz"])
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('foo1{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 1}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('foo2{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 2}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-n>\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('f{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': -1}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5))
+
+ 5d
+ exe "normal! Gof\<c-n>\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('f{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': -1}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('foo2{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 2}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('foo1{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 1}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': 0}', getline(5))
+ 5d
+ exe "normal! Gof\<c-n>\<c-p>\<c-p>\<c-p>\<c-p>\<c-p>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('f{''matches'': [''fo'', ''foo1'', ''foo2''], ''selected'': -1}', getline(5))
+
+ %d
+ call setline(1, ["foo"])
+ set cpt=FComplFunc^2,.
+ exe "normal! Gof\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('foo1{''matches'': [''foo1'', ''foo2'', ''foo''], ''selected'': 0}', getline(2))
+ bw!
+
+ " Test refresh:always with max_items
+ let g:CallCount = 0
+ func! CompleteItemsSelect(findstart, base)
+ if a:findstart
+ return col('.') - 1
+ endif
+ let g:CallCount += 1
+ let res = [[], ['foobar'], ['foo1', 'foo2', 'foo3'], ['foo4', 'foo5', 'foo6']]
+ return #{words: res[g:CallCount], refresh: 'always'}
+ endfunc
+
+ new
+ set complete=.,Ffunction('CompleteItemsSelect')^2
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-n>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('foobar{''matches'': [''foobarbar'', ''foobar''], ''selected'': 1}', getline(2))
+ call assert_equal(1, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-p>o\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('fo{''matches'': [''foobarbar'', ''foo1'', ''foo2''], ''selected'': -1}', getline(2))
+ call assert_equal(2, g:CallCount)
+ %d
+ call setline(1, "foobarbar")
+ let g:CallCount = 0
+ exe "normal! Gof\<c-n>\<c-p>o\<bs>\<c-r>=PrintMenuWords()\<cr>"
+ call assert_equal('f{''matches'': [''foobarbar'', ''foo4'', ''foo5''], ''selected'': -1}', getline(2))
+ call assert_equal(3, g:CallCount)
+ bw!
+
+ " Test 'fuzzy' with max_items
+ " XXX: Cannot use complete_info() since it is broken for 'fuzzy'
+ new
+ set completeopt=menu,noselect,fuzzy
+ set complete=.
+ call setline(1, ["abcd", "abac", "abdc"])
+ execute "normal Goa\<c-n>c\<c-n>"
+ call assert_equal('abac', getline(4))
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+ set complete=.^1
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+ set complete=.^2
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+ set complete=.^3
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+ set complete=.^4
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+
+ func! ComplFunc(findstart, base)
+ if a:findstart
+ return col(".")
+ endif
+ return ["abcde", "abacr"]
+ endfunc
+
+ set complete=.,FComplFunc^1
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>"
+ call assert_equal('abacr', getline(4))
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+ set complete=.^1,FComplFunc^1
+ execute "normal Sa\<c-n>c\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('abac', getline(4))
+ bw!
+
+ " Items with '\n' that cause menu to shift, with no leader (issue #17394)
+ func! ComplFunc(findstart, base)
+ if a:findstart == 1
+ return col('.') - 1
+ endif
+ return ["one\ntwo\nthree", "four five six", "hello\nworld\nhere"]
+ endfunc
+ set completeopt=menuone,popup,noselect,fuzzy infercase
+ set complete=.^1,FComplFunc^5
+ new
+ call setline(1, ["foo", "bar", "baz"])
+ execute "normal Go\<c-n>\<c-n>\<c-n>"
+ call assert_equal(['one', 'two', 'three'], getline(4, 6))
+ %d
+ call setline(1, ["foo", "bar", "baz"])
+ execute "normal Go\<c-n>\<c-n>\<c-n>\<c-p>"
+ call assert_equal('foo', getline(4))
+ execute "normal S\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('foo', getline(4))
+ set complete=.^1,FComplFunc^2
+ execute "normal S\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>\<c-n>"
+ call assert_equal('foo', getline(4))
+ execute "normal S\<c-n>\<c-p>\<c-p>\<c-p>\<c-n>\<c-n>"
+ call assert_equal('four five six', getline(4))
+ bw!
+
+ set completeopt& complete& infercase&
+ delfunc PrintMenuWords
+ delfunc ComplFunc
+ delfunc CompleteItemsSelect
+endfunc
+
func Test_complete_append_selected_match_default()
" when typing a normal character during completion,
" completion is ended, see
@@ -3587,7 +4470,7 @@ endfunc
" Test 'nearest' flag of 'completeopt'
func Test_nearest_cpt_option()
- func PrintMenuWords()
+ func! PrintMenuWords()
let info = complete_info(["selected", "matches"])
call map(info.matches, {_, v -> v.word})
return info
@@ -3876,4 +4759,41 @@ func Test_register_completion()
set ignorecase&
endfunc
+" Test refresh:always with unloaded buffers (issue #17363)
+func Test_complete_unloaded_buf_refresh_always()
+ func TestComplete(findstart, base)
+ if a:findstart
+ let line = getline('.')
+ let start = col('.') - 1
+ while start > 0 && line[start - 1] =~ '\a'
+ let start -= 1
+ endwhile
+ return start
+ else
+ let g:CallCount += 1
+ let res = ["update1", "update12", "update123"]
+ return #{words: res, refresh: 'always'}
+ endif
+ endfunc
+
+ let g:CallCount = 0
+ set completeopt=menu,longest
+ set completefunc=TestComplete
+ set complete=b,u,t,i,F
+ badd foo1
+ badd foo2
+ new
+ exe "normal! iup\<C-N>\<BS>\<BS>\<BS>\<BS>\<BS>"
+ call assert_equal('up', getline(1))
+ call assert_equal(6, g:CallCount)
+
+ bd! foo1
+ bd! foo2
+ bw!
+ set completeopt&
+ set complete&
+ set completefunc&
+ delfunc TestComplete
+endfunc
+
" vim: shiftwidth=2 sts=2 expandtab nofoldenable
diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim
@@ -277,6 +277,23 @@ func Test_complete()
call feedkeys("i\<C-N>\<Esc>", 'xt')
bwipe!
call assert_fails('set complete=ix', 'E535:')
+ call assert_fails('set complete=x', 'E539:')
+ call assert_fails('set complete=..', 'E535:')
+ set complete=.,w,b,u,k,\ s,i,d,],t,U,F,o
+ call assert_fails('set complete=i^-10', 'E535:')
+ call assert_fails('set complete=i^x', 'E535:')
+ call assert_fails('set complete=k^2,t^-1,s^', 'E535:')
+ call assert_fails('set complete=t^-1', 'E535:')
+ call assert_fails('set complete=kfoo^foo2', 'E535:')
+ call assert_fails('set complete=kfoo^', 'E535:')
+ call assert_fails('set complete=.^', 'E535:')
+ 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
+ set complete&
+ set complete+=Ffunction('g:foo'\\,\ [10\\,\ 20])
set complete&
endfun