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: #16716

Signed-off-by: glepnir <glephunter@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/buffer.c b/src/buffer.c
index 0624f9d..697efa3 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -2494,6 +2494,7 @@
     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);
 #ifdef FEAT_COMPL_FUNC
     clear_string_option(&buf->b_p_cfu);
     free_callback(&buf->b_cfu_cb);
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 39f6aa9..3b13cf0 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -2104,6 +2104,8 @@
 			ret_number_bool,    f_complete_check},
     {"complete_info",	0, 1, FEARG_1,	    arg1_list_string,
 			ret_dict_any,	    f_complete_info},
+    {"complete_match",	0, 2, 0,	    NULL,
+			ret_list_any,	    f_complete_match},
     {"confirm",		1, 4, FEARG_1,	    arg4_string_string_number_string,
 			ret_number,	    f_confirm},
     {"copy",		1, 1, FEARG_1,	    NULL,
diff --git a/src/insexpand.c b/src/insexpand.c
index ace4f55..77c9831 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -3551,6 +3551,147 @@
 }
 
 /*
+ * 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_u    *str,
+    int        len,
+    int        pos)
+{
+    list_T    *match;
+    int        ret;
+
+    match = list_alloc();
+    if (match == NULL)
+        return FAIL;
+
+    if ((ret = list_append_number(match, pos + 1)) == FAIL
+	    || (ret = list_append_string(match, str, len)) == FAIL
+	    || (ret = list_append_list(rettv->vval.v_list, match)) == FAIL)
+    {
+        vim_free(match);
+        return FAIL;
+    }
+
+    return OK;
+}
+
+/*
+ * "complete_match()" function
+ */
+    void
+f_complete_match(typval_T *argvars, typval_T *rettv)
+{
+    linenr_T    lnum;
+    colnr_T     col;
+    char_u      *line = NULL;
+    char_u      *ise = NULL;
+    regmatch_T  regmatch;
+    char_u      *before_cursor = NULL;
+    char_u      *cur_end = NULL;
+    char_u      *trig = NULL;
+    int          bytepos = 0;
+    char_u	part[MAXPATHL];
+    int		ret;
+
+    if (rettv_list_alloc(rettv) == FAIL)
+	return;
+
+    ise = curbuf->b_p_ise[0] != NUL ? curbuf->b_p_ise : p_ise;
+
+    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_invalid_argument));
+	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;
+	}
+    }
+
+    line = ml_get_buf(curbuf, lnum, FALSE);
+    if (line == NULL)
+	return;
+
+    before_cursor = vim_strnsave(line, col);
+    if (before_cursor == NULL)
+	return;
+
+    if (ise == NULL || *ise == NUL)
+    {
+	regmatch.regprog = vim_regcomp((char_u *)"\\k\\+$", RE_MAGIC);
+	if (regmatch.regprog != NULL)
+	{
+	    if (vim_regexec_nl(&regmatch, before_cursor, (colnr_T)0))
+	    {
+		bytepos = (int)(regmatch.startp[0] - before_cursor);
+		trig = vim_strnsave(regmatch.startp[0],
+			regmatch.endp[0] - regmatch.startp[0]);
+		if (trig == NULL)
+		{
+		    vim_free(before_cursor);
+		    return;
+		}
+
+		ret = add_match_to_list(rettv, trig, -1, bytepos);
+		vim_free(trig);
+		if (ret == FAIL)
+		{
+		    vim_free(trig);
+		    vim_regfree(regmatch.regprog);
+		    return;
+		}
+	    }
+	    vim_regfree(regmatch.regprog);
+	}
+    }
+    else
+    {
+	char_u	*p = ise;
+	cur_end = before_cursor + (int)STRLEN(before_cursor);
+
+	while (*p != NUL)
+	{
+	    int len = copy_option_part(&p, part, MAXPATHL, ",");
+
+	    if (len > 0 && len <= col)
+	    {
+		if (STRNCMP(cur_end - len, part, len) == 0)
+		{
+		    bytepos = col - len;
+		    if (add_match_to_list(rettv, part, len, bytepos) == FAIL)
+		    {
+			vim_free(before_cursor);
+			return;
+		    }
+		}
+	    }
+	}
+    }
+
+    vim_free(before_cursor);
+}
+
+/*
  * Return Insert completion mode name string
  */
     static char_u *
diff --git a/src/option.c b/src/option.c
index cfdeb44..ab9ee66 100644
--- a/src/option.c
+++ b/src/option.c
@@ -6400,6 +6400,9 @@
 	    clear_string_option(&buf->b_p_cot);
 	    buf->b_cot_flags = 0;
 	    break;
+	case PV_ISE:
+	    clear_string_option(&buf->b_p_ise);
+	    break;
 	case PV_DICT:
 	    clear_string_option(&buf->b_p_dict);
 	    break;
@@ -6518,6 +6521,7 @@
 	    case PV_INC:  return (char_u *)&(curbuf->b_p_inc);
 #endif
 	    case PV_COT:  return (char_u *)&(curbuf->b_p_cot);
+	    case PV_ISE:  return (char_u *)&(curbuf->b_p_ise);
 	    case PV_DICT: return (char_u *)&(curbuf->b_p_dict);
 	    case PV_TSR:  return (char_u *)&(curbuf->b_p_tsr);
 #ifdef FEAT_COMPL_FUNC
@@ -6600,6 +6604,8 @@
 #endif
 	case PV_COT:	return *curbuf->b_p_cot != NUL
 				    ? (char_u *)&(curbuf->b_p_cot) : p->var;
+	case PV_ISE:	return *curbuf->b_p_ise != NUL
+				    ? (char_u *)&(curbuf->b_p_ise) : p->var;
 	case PV_DICT:	return *curbuf->b_p_dict != NUL
 				    ? (char_u *)&(curbuf->b_p_dict) : p->var;
 	case PV_TSR:	return *curbuf->b_p_tsr != NUL
@@ -7431,6 +7437,7 @@
 	    buf->b_cot_flags = 0;
 	    buf->b_p_dict = empty_option;
 	    buf->b_p_tsr = empty_option;
+	    buf->b_p_ise = empty_option;
 #ifdef FEAT_COMPL_FUNC
 	    buf->b_p_tsrfu = empty_option;
 #endif
diff --git a/src/option.h b/src/option.h
index db1030d..bb1226e 100644
--- a/src/option.h
+++ b/src/option.h
@@ -731,6 +731,7 @@
 EXTERN char_u	*p_indk;	// 'indentkeys'
 #endif
 EXTERN int	p_im;		// 'insertmode'
+EXTERN char_u	*p_ise;		// 'isexpand'
 EXTERN char_u	*p_isf;		// 'isfname'
 EXTERN char_u	*p_isi;		// 'isident'
 EXTERN char_u	*p_isk;		// 'iskeyword'
@@ -1205,6 +1206,7 @@
     , BV_INEX
 #endif
     , BV_INF
+    , BV_ISE
     , BV_ISK
 #ifdef FEAT_CRYPT
     , BV_KEY
diff --git a/src/optiondefs.h b/src/optiondefs.h
index 8ed4ebe..f035104 100644
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -90,6 +90,7 @@
 # define PV_INEX	OPT_BUF(BV_INEX)
 #endif
 #define PV_INF		OPT_BUF(BV_INF)
+#define PV_ISE		OPT_BOTH(OPT_BUF(BV_ISE))
 #define PV_ISK		OPT_BUF(BV_ISK)
 #ifdef FEAT_CRYPT
 # define PV_KEY		OPT_BUF(BV_KEY)
@@ -1458,6 +1459,10 @@
     {"insertmode",  "im",   P_BOOL|P_VI_DEF|P_VIM,
 			    (char_u *)&p_im, PV_NONE, did_set_insertmode, NULL,
 			    {(char_u *)FALSE, (char_u *)0L} SCTX_INIT},
+    {"isexpand",    "ise",  P_STRING|P_VI_DEF|P_ONECOMMA|P_NODUP,
+			    (char_u *)&p_ise, PV_ISE, did_set_isexpand, NULL,
+			    {(char_u *)"", (char_u *)0L}
+			    SCTX_INIT},
     {"isfname",	    "isf",  P_STRING|P_VI_DEF|P_COMMA|P_NODUP,
 			    (char_u *)&p_isf, PV_NONE, did_set_isopt, NULL,
 			    {
diff --git a/src/optionstr.c b/src/optionstr.c
index 90b8e52..f60957e 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -310,6 +310,7 @@
     check_string_option(&buf->b_p_cinw);
     check_string_option(&buf->b_p_cot);
     check_string_option(&buf->b_p_cpt);
+    check_string_option(&buf->b_p_ise);
 #ifdef FEAT_COMPL_FUNC
     check_string_option(&buf->b_p_cfu);
     check_string_option(&buf->b_p_ofu);
@@ -2865,6 +2866,48 @@
 #endif
 
 /*
+ * The 'isexpand' option is changed.
+ */
+    char *
+did_set_isexpand(optset_T *args)
+{
+    char_u  *ise = p_ise;
+    char_u  *p;
+    int     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_invalid_argument;
+	    last_was_comma = TRUE;
+	    p++;
+	    continue;
+	}
+
+	last_was_comma = FALSE;
+	MB_PTR_ADV(p);
+    }
+
+    if (last_was_comma)
+	return e_invalid_argument;
+
+    return NULL;
+}
+
+
+/*
  * The 'iskeyword' option is changed.
  */
     char *
diff --git a/src/proto/insexpand.pro b/src/proto/insexpand.pro
index 8529b7b..e9ff626 100644
--- a/src/proto/insexpand.pro
+++ b/src/proto/insexpand.pro
@@ -67,4 +67,5 @@
 int ins_complete(int c, int enable_pum);
 void free_insexpand_stuff(void);
 int ins_compl_cancel(void);
+void f_complete_match(typval_T *argvars, typval_T *rettv);
 /* vim: set ft=c : */
diff --git a/src/proto/optionstr.pro b/src/proto/optionstr.pro
index dc1fb0a..4c4ec3e 100644
--- a/src/proto/optionstr.pro
+++ b/src/proto/optionstr.pro
@@ -101,6 +101,7 @@
 int expand_set_highlight(optexpand_T *args, int *numMatches, char_u ***matches);
 char *did_set_iconstring(optset_T *args);
 char *did_set_imactivatekey(optset_T *args);
+char *did_set_isexpand(optset_T *args);
 char *did_set_iskeyword(optset_T *args);
 char *did_set_isopt(optset_T *args);
 char *did_set_jumpoptions(optset_T *args);
diff --git a/src/structs.h b/src/structs.h
index b5c898d..45e1b18 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -3302,6 +3302,7 @@
     char_u	*b_p_fo;	// 'formatoptions'
     char_u	*b_p_flp;	// 'formatlistpat'
     int		b_p_inf;	// 'infercase'
+    char_u	*b_p_ise;	// 'isexpand' local value
     char_u	*b_p_isk;	// 'iskeyword'
 #ifdef FEAT_FIND_ID
     char_u	*b_p_def;	// 'define' local value
diff --git a/src/testdir/gen_opt_test.vim b/src/testdir/gen_opt_test.vim
index a087629..3d798e9 100644
--- a/src/testdir/gen_opt_test.vim
+++ b/src/testdir/gen_opt_test.vim
@@ -229,6 +229,7 @@
       \ '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/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index b03132f..73565a5 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -4328,4 +4328,86 @@
   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)
+
+  bw!
+  unlet g:result
+  set isexpand&
+  delfunc TestComplete
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab nofoldenable
diff --git a/src/version.c b/src/version.c
index 417088b..8d88124 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1341,
+/**/
     1340,
 /**/
     1339,