vim-patch:9.1.1933: completion: complete_match() is not useful (#36726)

Problem:  completion: complete_match() Vim script function and
          'isexpand' option are not that useful and confusing
          (after v9.1.1341)
Solution: Remove function and option and clean up code and documentation
          (Girish Palya).

complete_match() and 'isexpand' add no real functionality to Vim. They
duplicate what `strridx()` already does, yet pretend to be part of the
completion system. They have nothing to do with the completion mechanism.

* `f_complete_match()` in `insexpand.c` does not call any completion code.
   It’s just a `STRNCMP()` wrapper with fluff logic.
* `'isexpand'` exists only as a proxy argument to that function.
   It does nothing on its own and amounts to misuse of a new option.

The following Vim script function can be used to implement the same
functionality:

```vim
  func CompleteMatch(triggers, sep=',')
    let line = getline('.')->strpart(0, col('.') - 1)
    let result = []
    for trig in split(a:triggers, a:sep)
      let idx = strridx(line, trig)
      if l:idx >= 0
        call add(result, [idx + 1, trig])
      endif
    endfor
    return result
  endfunc
```

related: vim/vim#16716
fixes: vim/vim#18563
closes: vim/vim#18790

cbcbff8712

Co-authored-by: Girish Palya <girishji@gmail.com>
This commit is contained in:
zeertzjq
2025-11-28 10:10:31 +08:00
committed by GitHub
parent 19d5b28977
commit 2c6469aca4
18 changed files with 5 additions and 451 deletions

View File

@@ -93,7 +93,7 @@ Vim would never have become what it is now, without the help of these people!
improvements
Doug Kearns Runtime file maintainer
Foxe Chen Wayland support, new features
glepnir completion feature
glepnir work on improving completion feature, fixes
Girish Palya autocompletion (ins/cmdline), omnifunc
composing, search/subst completion, and more.
Hirohito Higashi lots of patches and fixes

View File

@@ -33,7 +33,7 @@ LSP
OPTIONS
• `'completefuzzycollect'` has been removed.
• `'completefuzzycollect'` and `'isexpand'` have been removed.
TREESITTER
@@ -43,6 +43,9 @@ UI
• `progress` attribute removed form |ui-messages| msg_show event
VIMSCRIPT
• `complete_match()` has been removed.
==============================================================================
BREAKING CHANGES *news-breaking*

View File

@@ -3739,23 +3739,6 @@ A jump table for the options with a short description can be found at |Q_op|.
and there is a letter before it, the completed part is made uppercase.
With 'noinfercase' the match is used as-is.
*'isexpand'* *'ise'*
'isexpand' 'ise' string (default "")
global or local to buffer |global-local|
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|.
Examples: >vim
set isexpand=.,->,/*,\\,
<
*'isfname'* *'isf'*
'isfname' 'isf' string (default for Windows:
"@,48-57,/,\,.,-,_,+,,,#,$,%,{,},[,],@-@,!,~,="

View File

@@ -943,8 +943,6 @@ Insert mode completion: *completion-functions*
complete_add() add to found matches
complete_check() check if completion should be aborted
complete_info() get current completion information
complete_match() get insert completion start match col and
trigger text
preinserted() check if text is inserted after cursor
pumvisible() check if the popup menu is displayed
pum_getpos() position and size of popup menu if visible

View File

@@ -1281,56 +1281,6 @@ complete_info([{what}]) *complete_info()*
Return: ~
(`table`)
complete_match([{lnum}, {col}]) *complete_match()*
Searches backward from the given position and returns a List
of matches according to the 'isexpand' option. When no
arguments are provided, uses the current cursor position.
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.
Examples: >vim
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>
<
Parameters: ~
• {lnum} (`integer?`)
• {col} (`integer?`)
Return: ~
(`table`)
confirm({msg} [, {choices} [, {default} [, {type}]]]) *confirm()*
confirm() offers the user a dialog, from which a choice can be
made. It returns the number of the choice. For the first

View File

@@ -3656,31 +3656,6 @@ vim.o.inf = vim.o.infercase
vim.bo.infercase = vim.o.infercase
vim.bo.inf = vim.bo.infercase
--- 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`.
---
--- Examples:
---
--- ```vim
--- set isexpand=.,->,/*,\\,
--- ```
---
---
--- @type string
vim.o.isexpand = ""
vim.o.ise = vim.o.isexpand
vim.bo.isexpand = vim.o.isexpand
vim.bo.ise = vim.bo.isexpand
vim.go.isexpand = vim.o.isexpand
vim.go.ise = vim.go.isexpand
--- The characters specified by this option are included in file names and
--- path names. Filenames are used for commands like "gf", "[i" and in
--- the tags file. It is also used for "\f" in a `pattern`.

View File

@@ -1127,53 +1127,6 @@ function vim.fn.complete_check() end
--- @return table
function vim.fn.complete_info(what) end
--- Searches backward from the given position and returns a List
--- of matches according to the 'isexpand' option. When no
--- arguments are provided, uses the current cursor position.
---
--- 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.
---
--- Examples: >vim
--- 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>
--- <
---
--- @param lnum? integer
--- @param col? integer
--- @return table
function vim.fn.complete_match(lnum, col) end
--- confirm() offers the user a dialog, from which a choice can be
--- made. It returns the number of the choice. For the first
--- choice this is 1.

View File

@@ -406,7 +406,6 @@ local options_list = {
header = N_ 'language specific',
{ 'isfname', N_ 'specifies the characters in a file name' },
{ 'isident', N_ 'specifies the characters in an identifier' },
{ 'isexpand', N_ 'defines trigger strings for complete_match()' },
{ 'iskeyword', N_ 'specifies the characters in a keyword' },
{ 'isprint', N_ 'specifies printable characters' },
{ 'quoteescape', N_ 'specifies escape characters in a string' },

View File

@@ -2105,7 +2105,6 @@ 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

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

View File

@@ -1494,57 +1494,6 @@ M.funcs = {
returns = 'table',
signature = 'complete_info([{what}])',
},
complete_match = {
args = { 0, 2 },
base = 0,
desc = [=[
Searches backward from the given position and returns a List
of matches according to the 'isexpand' option. When no
arguments are provided, uses the current cursor position.
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.
Examples: >vim
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>
<
]=],
name = 'complete_match',
params = { { 'lnum', 'integer' }, { 'col', 'integer' } },
returns = 'table',
signature = 'complete_match([{lnum}, {col}])',
},
confirm = {
args = { 1, 4 },
base = 1,

View File

@@ -3458,94 +3458,6 @@ void f_complete_check(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
RedrawingDisabled = saved;
}
/// Add match item to the return list.
static void add_match_to_list(typval_T *rettv, char *str, int pos)
{
list_T *match = tv_list_alloc(2);
tv_list_append_number(match, pos + 1);
tv_list_append_string(match, str, -1);
tv_list_append_list(rettv->vval.v_list, match);
}
/// "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 (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)) {
char *trig = xstrnsave(regmatch.startp[0], (size_t)(regmatch.endp[0] - regmatch.startp[0]));
int bytepos = (int)(regmatch.startp[0] - before_cursor);
add_match_to_list(rettv, trig, bytepos);
xfree(trig);
}
vim_regfree(regmatch.regprog);
}
} else {
char *p = ise;
char *p_space = NULL;
char *cur_end = before_cursor + (int)strlen(before_cursor);
while (*p != NUL) {
size_t len = 0;
if (p_space) {
len = (size_t)(p - p_space - 1);
memcpy(part, p_space + 1, len);
p_space = NULL;
} else {
char *next_comma = strchr((*p == ',') ? p + 1 : p, ',');
if (next_comma && *(next_comma + 1) == ' ') {
p_space = next_comma;
}
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;
add_match_to_list(rettv, part, bytepos);
}
}
}
}
xfree(before_cursor);
}
/// Return Insert completion mode name string
static char *ins_compl_mode(void)
{

View File

@@ -4467,8 +4467,6 @@ void *get_varp_scope_from(vimoption_T *p, int opt_flags, buf_T *buf, win_T *win)
return &(buf->b_p_inc);
case kOptCompleteopt:
return &(buf->b_p_cot);
case kOptIsexpand:
return &(buf->b_p_ise);
case kOptDictionary:
return &(buf->b_p_dict);
case kOptDiffanchors:
@@ -4558,8 +4556,6 @@ void *get_varp_from(vimoption_T *p, buf_T *buf, win_T *win)
return *buf->b_p_inc != NUL ? &(buf->b_p_inc) : p->var;
case kOptCompleteopt:
return *buf->b_p_cot != NUL ? &(buf->b_p_cot) : p->var;
case kOptIsexpand:
return *buf->b_p_ise != NUL ? &(buf->b_p_ise) : p->var;
case kOptDictionary:
return *buf->b_p_dict != NUL ? &(buf->b_p_dict) : p->var;
case kOptDiffanchors:
@@ -5267,7 +5263,6 @@ void buf_copy_options(buf_T *buf, int flags)
buf->b_p_dict = empty_string_option;
buf->b_p_dia = empty_string_option;
buf->b_p_tsr = empty_string_option;
buf->b_p_ise = empty_string_option;
buf->b_p_tsrfu = empty_string_option;
buf->b_p_qe = xstrdup(p_qe);
COPY_OPT_SCTX(buf, kBufOptQuoteescape);

View File

@@ -379,7 +379,6 @@ EXTERN int p_is; ///< 'incsearch'
EXTERN char *p_inde; ///< 'indentexpr'
EXTERN char *p_indk; ///< 'indentkeys'
EXTERN char *p_icm; ///< 'inccommand'
EXTERN char *p_ise; ///< 'isexpand'
EXTERN char *p_isf; ///< 'isfname'
EXTERN char *p_isi; ///< 'isident'
EXTERN char *p_isk; ///< 'iskeyword'

View File

@@ -4799,33 +4799,6 @@ local options = {
type = 'boolean',
immutable = true,
},
{
abbreviation = 'ise',
cb = 'did_set_isexpand',
defaults = '',
deny_duplicates = true,
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|.
Examples: >vim
set isexpand=.,->,/*,\\,
<
]=],
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

@@ -90,7 +90,6 @@ void didset_string_options(void)
check_str_opt(kOptCasemap, NULL);
check_str_opt(kOptBackupcopy, NULL);
check_str_opt(kOptBelloff, NULL);
check_str_opt(kOptIsexpand, NULL);
check_str_opt(kOptCompleteopt, NULL);
check_str_opt(kOptSessionoptions, NULL);
check_str_opt(kOptViewoptions, NULL);
@@ -1384,44 +1383,6 @@ 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)
{

View File

@@ -265,7 +265,6 @@ let test_values = {
\ 'helplang': [['', 'de', 'de,it'], ['xxx']],
"\ 'highlight': [['', 'e:Error'], ['xxx']],
"\ 'imactivatekey': [['', 'S-space'], ['xxx']],
\ 'isexpand': [['', '.,->', '/,/*,\\,'], [',,', '\\,,']],
\ 'isfname': [['', '@', '@,48-52'], ['xxx', '@48']],
\ 'isident': [['', '@', '@,48-52'], ['xxx', '@48']],
\ 'iskeyword': [['', '@', '@,48-52'], ['xxx', '@48']],

View File

@@ -4919,99 +4919,6 @@ func Test_nearest_cpt_option()
delfunc PrintMenuWords
endfunc
func Test_complete_match()
set isexpand=.,/,->,abc,/*,_
func TestComplete()
let res = complete_match()
if res->len() == 0
return
endif
let [startcol, expandchar] = res[0]
if startcol >= 0
let line = getline('.')
let items = []
if expandchar == '/*'
let items = ['/** */']
elseif expandchar =~ '^/'
let items = ['/*! */', '// TODO:', '// fixme:']
elseif expandchar =~ '^\.' && startcol < 4
let items = ['length()', 'push()', 'pop()', 'slice()']
elseif expandchar =~ '^\.' && startcol > 4
let items = ['map()', 'filter()', 'reduce()']
elseif expandchar =~ '^\abc'
let items = ['def', 'ghk']
elseif expandchar =~ '^\->'
let items = ['free()', 'xfree()']
else
let items = ['test1', 'test2', 'test3']
endif
call complete(expandchar =~ '^/' ? startcol : startcol + strlen(expandchar), items)
endif
endfunc
new
inoremap <buffer> <F5> <cmd>call TestComplete()<CR>
call feedkeys("S/*\<F5>\<C-Y>", 'tx')
call assert_equal('/** */', getline('.'))
call feedkeys("S/\<F5>\<C-N>\<C-Y>", 'tx')
call assert_equal('// TODO:', getline('.'))
call feedkeys("Swp.\<F5>\<C-N>\<C-Y>", 'tx')
call assert_equal('wp.push()', getline('.'))
call feedkeys("Swp.property.\<F5>\<C-N>\<C-Y>", 'tx')
call assert_equal('wp.property.filter()', getline('.'))
call feedkeys("Sp->\<F5>\<C-N>\<C-Y>", 'tx')
call assert_equal('p->xfree()', getline('.'))
call feedkeys("Swp->property.\<F5>\<C-Y>", 'tx')
call assert_equal('wp->property.map()', getline('.'))
call feedkeys("Sabc\<F5>\<C-Y>", 'tx')
call assert_equal('abcdef', getline('.'))
call feedkeys("S_\<F5>\<C-Y>", 'tx')
call assert_equal('_test1', getline('.'))
set ise&
call feedkeys("Sabc \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[1, 'abc']], g:result)
call assert_fails('call complete_match(99, 0)', 'E966:')
call assert_fails('call complete_match(1, 99)', 'E964:')
call assert_fails('call complete_match(1)', 'E474:')
set ise=你好,
call feedkeys("S你好 \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[1, '你好'], [4, '好']], g:result)
set ise=\\,,->
call feedkeys("Sabc, \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[4, ',']], g:result)
set ise=\ ,=
call feedkeys("Sif true \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[8, ' ']], g:result)
call feedkeys("Slet a = \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[7, '=']], g:result)
set ise={,\ ,=
call feedkeys("Sif true \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[8, ' ']], g:result)
call feedkeys("S{ \<ESC>:let g:result=complete_match()\<CR>", 'tx')
call assert_equal([[1, '{']], g:result)
bw!
unlet g:result
set isexpand&
delfunc TestComplete
endfunc
func Test_register_completion()
let @a = "completion test apple application"
let @b = "banana behavior better best"