diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index c95a16cd25..d59618a075 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -1263,6 +1263,56 @@ 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 call CustomComplete() +< + + 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 diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index ec139c1484..d7dbc350cf 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -3543,6 +3543,23 @@ 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,/,\,.,-,_,+,,,#,$,%,{,},[,],@-@,!,~,=" diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index 916fd9e2e2..cc37197a79 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -934,6 +934,8 @@ 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 pumvisible() check if the popup menu is displayed pum_getpos() position and size of popup menu if visible diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index e103226a44..5115c925be 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -3438,6 +3438,31 @@ 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`. diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 5ba94663cd..30bd45c889 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -1109,6 +1109,53 @@ 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 call CustomComplete() +--- < +--- +--- @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. diff --git a/runtime/optwin.vim b/runtime/optwin.vim index 9af414d909..eed625a7c7 100644 --- a/runtime/optwin.vim +++ b/runtime/optwin.vim @@ -1,7 +1,7 @@ " These commands create the option window. " " Maintainer: The Vim Project -" Last Change: 2025 Apr 07 +" Last Change: 2025 Apr 24 " Former Maintainer: Bram Moolenaar " If there already is an option window, jump to that one. @@ -1102,6 +1102,8 @@ call AddOption("isfname", gettext("specifies the characters in a file name" call OptionG("isf", &isf) call AddOption("isident", gettext("specifies the characters in an identifier")) call OptionG("isi", &isi) +call AddOption("isexpand", gettext("defines trigger strings for complete_match()")) +call append("$", "\t" .. s:local_to_buffer) call AddOption("iskeyword", gettext("specifies the characters in a keyword")) call append("$", "\t" .. s:local_to_buffer) call OptionL("isk") diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 5fec2f1211..9d4365cdfc 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -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); diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 3866a0bea5..b3335aac44 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -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' local value char *b_p_isk; ///< 'iskeyword' char *b_p_def; ///< 'define' local value char *b_p_inc; ///< 'include' diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 0013134857..eae54ca59a 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -1477,6 +1477,57 @@ 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 call CustomComplete() + < + ]=], + name = 'complete_match', + params = { { 'lnum', 'integer' }, { 'col', 'integer' } }, + returns = 'table', + signature = 'complete_match([{lnum}, {col}])', + }, confirm = { args = { 1, 4 }, base = 1, diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 453d9535c5..248da8ae82 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -3081,6 +3081,81 @@ 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(®match, 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 *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; + add_match_to_list(rettv, part, bytepos); + } + } + } + } + + xfree(before_cursor); +} + /// Return Insert completion mode name string static char *ins_compl_mode(void) { diff --git a/src/nvim/option.c b/src/nvim/option.c index bdf418c1f8..ae4520d9e1 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -4462,6 +4462,8 @@ 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 kOptThesaurus: @@ -4547,6 +4549,8 @@ 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 kOptThesaurus: @@ -5238,6 +5242,7 @@ void buf_copy_options(buf_T *buf, int flags) buf->b_cot_flags = 0; buf->b_p_dict = 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); diff --git a/src/nvim/option_vars.h b/src/nvim/option_vars.h index d8d3e1e124..16b009fa73 100644 --- a/src/nvim/option_vars.h +++ b/src/nvim/option_vars.h @@ -374,6 +374,7 @@ 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' diff --git a/src/nvim/options.lua b/src/nvim/options.lua index ed1cf175be..4a73846ad2 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -4634,6 +4634,33 @@ 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', diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index e26099d71b..c66526318e 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -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) { diff --git a/test/old/testdir/gen_opt_test.vim b/test/old/testdir/gen_opt_test.vim index 168a38e665..16e165cc13 100644 --- a/test/old/testdir/gen_opt_test.vim +++ b/test/old/testdir/gen_opt_test.vim @@ -261,6 +261,7 @@ let test_values = { "\ 'imactivatekey': [['', 'S-space'], ['xxx']], \ 'isfname': [['', '@', '@,48-52'], ['xxx', '@48']], \ 'isident': [['', '@', '@,48-52'], ['xxx', '@48']], + \ 'isexpand': [['', '.,->', '/,/*,\\,'], [',,', '\\,,']], \ 'iskeyword': [['', '@', '@,48-52'], ['xxx', '@48']], \ 'isprint': [['', '@', '@,48-52'], ['xxx', '@48']], \ 'jumpoptions': [['', 'stack'], ['xxx']], diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index ded4f7392e..9bc004733b 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -3598,4 +3598,86 @@ 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 call TestComplete() + + call feedkeys("S/*\\", 'tx') + call assert_equal('/** */', getline('.')) + + call feedkeys("S/\\\", 'tx') + call assert_equal('// TODO:', getline('.')) + + call feedkeys("Swp.\\\", 'tx') + call assert_equal('wp.push()', getline('.')) + + call feedkeys("Swp.property.\\\", 'tx') + call assert_equal('wp.property.filter()', getline('.')) + + call feedkeys("Sp->\\\", 'tx') + call assert_equal('p->xfree()', getline('.')) + + call feedkeys("Swp->property.\\", 'tx') + call assert_equal('wp->property.map()', getline('.')) + + call feedkeys("Sabc\\", 'tx') + call assert_equal('abcdef', getline('.')) + + call feedkeys("S_\\", 'tx') + call assert_equal('_test1', getline('.')) + + set ise& + call feedkeys("Sabc \:let g:result=complete_match()\", '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你好 \:let g:result=complete_match()\", 'tx') + call assert_equal([[1, '你好'], [4, '好']], g:result) + + set ise=\\,,-> + call feedkeys("Sabc, \:let g:result=complete_match()\", 'tx') + call assert_equal([[4, ',']], g:result) + + bw! + unlet g:result + set isexpand& + delfunc TestComplete +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable