vim-patch:9.1.1341: cannot define completion triggers

Problem:  Cannot define completion triggers and act upon it
Solution: add the new option 'isexpand' and add the complete_match()
          function to return the completion matches according to the
          'isexpand' setting (glepnir)

Currently, completion trigger position is determined solely by the
'iskeyword' pattern (\k\+$), which causes issues when users need
different completion behaviors - such as triggering after '/' for
comments or '.' for methods. Modifying 'iskeyword' to include these
characters has undesirable side effects on other Vim functionality that
relies on keyword definitions.

Introduce a new buffer-local option 'isexpand' that allows specifying
different completion triggers and add the complete_match() function that
finds the appropriate start column for completion based on these
triggers, scanning backwards from cursor position.

This separation of concerns allows customized completion behavior
without affecting iskeyword-dependent features. The option's
buffer-local nature enables per-filetype completion triggers.

closes: vim/vim#16716

bcd5995b40

Co-authored-by: glepnir <glephunter@gmail.com>
This commit is contained in:
glepnir
2025-04-26 13:06:43 +08:00
parent ac8ae1596c
commit fcabbc2283
13 changed files with 434 additions and 0 deletions

View File

@ -2083,6 +2083,7 @@ void free_buf_options(buf_T *buf, bool free_p_ff)
clear_string_option(&buf->b_p_cinw);
clear_string_option(&buf->b_p_cot);
clear_string_option(&buf->b_p_cpt);
clear_string_option(&buf->b_p_ise);
clear_string_option(&buf->b_p_cfu);
callback_free(&buf->b_cfu_cb);
clear_string_option(&buf->b_p_ofu);

View File

@ -558,6 +558,7 @@ struct file_buffer {
char *b_p_fo; ///< 'formatoptions'
char *b_p_flp; ///< 'formatlistpat'
int b_p_inf; ///< 'infercase'
char *b_p_ise; ///< 'isexpand'
char *b_p_isk; ///< 'iskeyword'
char *b_p_def; ///< 'define' local value
char *b_p_inc; ///< 'include'

View File

@ -1477,6 +1477,59 @@ M.funcs = {
returns = 'table',
signature = 'complete_info([{what}])',
},
complete_match = {
args = { 0, 2 },
base = 0,
desc = [=[
Returns a List of matches found according to the 'isexpand'
option. Each match is represented as a List containing
[startcol, trigger_text] where:
- startcol: column position where completion should start,
or -1 if no trigger position is found. For multi-character
triggers, returns the column of the first character.
- trigger_text: the matching trigger string from 'isexpand',
or empty string if no match was found or when using the
default 'iskeyword' pattern.
When 'isexpand' is empty, uses the 'iskeyword' pattern
"\k\+$" to find the start of the current keyword.
When no arguments are provided, uses the current cursor
position.
Examples: >
set isexpand=.,->,/,/*,abc
func CustomComplete()
let res = complete_match()
if res->len() == 0 | return | endif
let [col, trigger] = res[0]
let items = []
if trigger == '/*'
let items = ['/** */']
elseif trigger == '/'
let items = ['/*! */', '// TODO:', '// fixme:']
elseif trigger == '.'
let items = ['length()']
elseif trigger =~ '^\->'
let items = ['map()', 'reduce()']
elseif trigger =~ '^\abc'
let items = ['def', 'ghk']
endif
if items->len() > 0
let startcol = trigger =~ '^/' ? col : col + len(trigger)
call complete(startcol, items)
endif
endfunc
inoremap <Tab> <Cmd>call CustomComplete()<CR>
<
Return type: list<list<any>>
]=],
name = 'complete_match',
params = { { 'lnum', 'integer?' }, { 'col', 'integer?' } },
returns = 'table',
signature = 'complete_match([{lnum}, {col}])',
},
confirm = {
args = { 1, 4 },
base = 1,

View File

@ -3081,6 +3081,104 @@ void f_complete_check(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
RedrawingDisabled = saved;
}
/// Add match item to the return list.
/// Returns FAIL if out of memory, OK otherwise.
static int add_match_to_list(typval_T *rettv, char *str, int pos)
{
list_T *match = tv_list_alloc(kListLenMayKnow);
if (match == NULL) {
return FAIL;
}
tv_list_append_number(match, pos + 1);
tv_list_append_string(match, str, -1);
tv_list_append_list(rettv->vval.v_list, match);
return OK;
}
/// "complete_match()" function
void f_complete_match(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
tv_list_alloc_ret(rettv, kListLenUnknown);
char *ise = curbuf->b_p_ise[0] != NUL ? curbuf->b_p_ise : p_ise;
linenr_T lnum = 0;
colnr_T col = 0;
char part[MAXPATHL];
if (argvars[0].v_type == VAR_UNKNOWN) {
lnum = curwin->w_cursor.lnum;
col = curwin->w_cursor.col;
} else if (argvars[1].v_type == VAR_UNKNOWN) {
emsg(_(e_invarg));
return;
} else {
lnum = (linenr_T)tv_get_number(&argvars[0]);
col = (colnr_T)tv_get_number(&argvars[1]);
if (lnum < 1 || lnum > curbuf->b_ml.ml_line_count) {
semsg(_(e_invalid_line_number_nr), lnum);
return;
}
if (col < 1 || col > ml_get_buf_len(curbuf, lnum)) {
semsg(_(e_invalid_column_number_nr), col + 1);
return;
}
}
char *line = ml_get_buf(curbuf, lnum);
if (line == NULL) {
return;
}
char *before_cursor = xstrnsave(line, (size_t)col);
if (before_cursor == NULL) {
return;
}
if (ise == NULL || *ise == NUL) {
regmatch_T regmatch;
regmatch.regprog = vim_regcomp("\\k\\+$", RE_MAGIC);
if (regmatch.regprog != NULL) {
if (vim_regexec_nl(&regmatch, before_cursor, (colnr_T)0)) {
int bytepos = (int)(regmatch.startp[0] - before_cursor);
char *trig = xstrnsave(regmatch.startp[0], (size_t)(regmatch.endp[0] - regmatch.startp[0]));
if (trig == NULL) {
xfree(before_cursor);
return;
}
int ret = add_match_to_list(rettv, trig, bytepos);
xfree(trig);
if (ret == FAIL) {
xfree(trig);
vim_regfree(regmatch.regprog);
return;
}
}
vim_regfree(regmatch.regprog);
}
} else {
char *p = ise;
char *cur_end = before_cursor + (int)strlen(before_cursor);
while (*p != NUL) {
size_t len = copy_option_part(&p, part, MAXPATHL, ",");
if (len > 0 && (int)len <= col) {
if (strncmp(cur_end - len, part, len) == 0) {
int bytepos = col - (int)len;
if (add_match_to_list(rettv, part, bytepos) == FAIL) {
xfree(before_cursor);
return;
}
}
}
}
}
xfree(before_cursor);
}
/// Return Insert completion mode name string
static char *ins_compl_mode(void)
{

View File

@ -4460,6 +4460,8 @@ void *get_varp_scope_from(vimoption_T *p, int opt_flags, buf_T *buf, win_T *win)
return &(buf->b_p_def);
case kOptInclude:
return &(buf->b_p_inc);
case kOptIsexpand:
return &(buf->b_p_ise);
case kOptCompleteopt:
return &(buf->b_p_cot);
case kOptDictionary:
@ -4545,6 +4547,8 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win)
return *buf->b_p_def != NUL ? &(buf->b_p_def) : p->var;
case kOptInclude:
return *buf->b_p_inc != NUL ? &(buf->b_p_inc) : p->var;
case kOptIsexpand:
return *buf->b_p_ise != NUL ? &(buf->b_p_ise) : p->var;
case kOptCompleteopt:
return *buf->b_p_cot != NUL ? &(buf->b_p_cot) : p->var;
case kOptDictionary:

View File

@ -376,6 +376,7 @@ EXTERN char *p_indk; ///< 'indentkeys'
EXTERN char *p_icm; ///< 'inccommand'
EXTERN char *p_isf; ///< 'isfname'
EXTERN char *p_isi; ///< 'isident'
EXTERN char *p_ise; ///< 'isexpand'
EXTERN char *p_isk; ///< 'iskeyword'
EXTERN char *p_isp; ///< 'isprint'
EXTERN int p_js; ///< 'joinspaces'

View File

@ -4628,6 +4628,29 @@ local options = {
type = 'boolean',
immutable = true,
},
{
abbreviation = 'ise',
cb = 'did_set_isexpand',
defaults = '',
deny_duplicates = false,
desc = [=[
Defines characters and patterns for completion in insert mode. Used by
the |complete_match()| function to determine the starting position for
completion. This is a comma-separated list of triggers. Each trigger
can be:
- A single character like "." or "/"
- A sequence of characters like "->", "/*", or "/**"
Note: Use "\\," to add a literal comma as trigger character, see
|option-backslash|.
]=],
full_name = 'isexpand',
list = 'onecomma',
scope = { 'global', 'buf' },
short_desc = N_('Defines characters and patterns for completion in insert mode'),
type = 'string',
varname = 'p_ise',
},
{
abbreviation = 'isf',
cb = 'did_set_isopt',

View File

@ -85,6 +85,7 @@ void didset_string_options(void)
check_str_opt(kOptBackupcopy, NULL);
check_str_opt(kOptBelloff, NULL);
check_str_opt(kOptCompletefuzzycollect, NULL);
check_str_opt(kOptIsexpand, NULL);
check_str_opt(kOptCompleteopt, NULL);
check_str_opt(kOptSessionoptions, NULL);
check_str_opt(kOptViewoptions, NULL);
@ -1316,6 +1317,44 @@ const char *did_set_inccommand(optset_T *args FUNC_ATTR_UNUSED)
return did_set_str_generic(args);
}
/// The 'isexpand' option is changed.
const char *did_set_isexpand(optset_T *args)
{
char *ise = p_ise;
char *p;
bool last_was_comma = false;
if (args->os_flags & OPT_LOCAL) {
ise = curbuf->b_p_ise;
}
for (p = ise; *p != NUL;) {
if (*p == '\\' && p[1] == ',') {
p += 2;
last_was_comma = false;
continue;
}
if (*p == ',') {
if (last_was_comma) {
return e_invarg;
}
last_was_comma = true;
p++;
continue;
}
last_was_comma = false;
MB_PTR_ADV(p);
}
if (last_was_comma) {
return e_invarg;
}
return NULL;
}
/// The 'iskeyword' option is changed.
const char *did_set_iskeyword(optset_T *args)
{