Merge pull request #34247 from zeertzjq/vim-9.1.1301

vim-patch: various 'complete' features
This commit is contained in:
zeertzjq
2025-06-03 07:00:25 +08:00
committed by GitHub
10 changed files with 1705 additions and 102 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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();
}

View File

@ -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',

View File

@ -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,40 +839,66 @@ 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;
char buffer[LSIZE];
uint8_t char_before = NUL;
// check if it is a valid value for 'complete' -- Acevedo
for (char *s = *varp; *s;) {
while (*s == ',' || *s == ' ') {
s++;
}
if (!*s) {
break;
}
if (vim_strchr(".wbuksid]tUf", (uint8_t)(*s)) == NULL) {
return illegal_char(args->os_errbuf, args->os_errbuflen, (uint8_t)(*s));
}
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++;
}
s++;
}
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 {
if (args->os_errbuf != NULL) {
vim_snprintf(args->os_errbuf, args->os_errbuflen,
_("E535: Illegal character after <%c>"),
*--s);
return args->os_errbuf;
}
return "";
escape = 0;
*buf_ptr++ = *p;
}
p++;
}
*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 (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;
}
}
}
}
}
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;

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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