mirror of
https://github.com/neovim/neovim
synced 2025-07-16 09:11:51 +00:00
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:
@ -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);
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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(®match, 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)
|
||||
{
|
||||
|
@ -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:
|
||||
|
@ -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'
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
{
|
||||
|
Reference in New Issue
Block a user