Commit: patch 9.1.0009: Cannot easily get the list of matches

65 views
Skip to first unread message

Christian Brabandt

unread,
Jan 4, 2024, 4:45:09 PM1/4/24
to vim...@googlegroups.com
patch 9.1.0009: Cannot easily get the list of matches

Commit: https://github.com/vim/vim/commit/f93b1c881a99fa847a1bafa71877d7e16f18e6ef
Author: Yegappan Lakshmanan <yega...@yahoo.com>
Date: Thu Jan 4 22:28:46 2024 +0100

patch 9.1.0009: Cannot easily get the list of matches

Problem: Cannot easily get the list of matches
Solution: Add the matchstrlist() and matchbufline() Vim script
functions (Yegappan Lakshmanan)

closes: #13766

Signed-off-by: Yegappan Lakshmanan <yega...@yahoo.com>
Signed-off-by: Christian Brabandt <c...@256bit.org>

diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt
index 084c76edf..8f79d2001 100644
--- a/runtime/doc/builtin.txt
+++ b/runtime/doc/builtin.txt
@@ -370,6 +370,8 @@ matchadd({group}, {pattern} [, {priority} [, {id} [, {dict}]]])
matchaddpos({group}, {pos} [, {priority} [, {id} [, {dict}]]])
Number highlight positions with {group}
matcharg({nr}) List arguments of |:match|
+matchbufline({buf}, {pat}, {lnum}, {end}, [, {dict})
+ List all the {pat} matches in buffer {buf}
matchdelete({id} [, {win}]) Number delete match identified by {id}
matchend({expr}, {pat} [, {start} [, {count}]])
Number position where {pat} ends in {expr}
@@ -381,6 +383,8 @@ matchlist({expr}, {pat} [, {start} [, {count}]])
List match and submatches of {pat} in {expr}
matchstr({expr}, {pat} [, {start} [, {count}]])
String {count}'th match of {pat} in {expr}
+matchstrlist({list}, {pat} [, {dict})
+ List all the {pat} matches in {list}
matchstrpos({expr}, {pat} [, {start} [, {count}]])
List {count}'th match of {pat} in {expr}
max({expr}) Number maximum value of items in {expr}
@@ -6054,6 +6058,51 @@ matcharg({nr}) *matcharg()*

Can also be used as a |method|: >
GetMatch()->matcharg()
+<
+ *matchbufline()*
+matchbufline({buf}, {pat}, {lnum}, {end}, [, {dict}])
+ Returns the |List| of matches in lines from {lnum} to {end} in
+ buffer {buf} where {pat} matches.
+
+ {lnum} and {end} can either be a line number or the string "$"
+ to refer to the last line in {buf}.
+
+ The {dict} argument supports following items:
+ submatches include submatch information (|/\(|)
+
+ For each match, a |Dict| with the following items is returned:
+ byteidx starting byte index of the match
+    lnum line number where there is a match
+    text matched string
+ Note that there can be multiple matches in a single line.
+
+ This function works only for loaded buffers. First call
+ |bufload()| if needed.
+
+ When {buf} is not a valid buffer, the buffer is not loaded or
+ {lnum} or {end} is not valid then an error is given and an
+ empty |List| is returned.
+
+ Examples: >
+    " Assuming line 3 in buffer 5 contains "a"
+    :echo matchbufline(5, '\<\k\+\>', 3, 3)
+    [{'lnum': 3, 'byteidx': 0, 'text': 'a'}]
+    " Assuming line 4 in buffer 10 contains "tik tok"
+    :echo matchbufline(10, '\<\k\+\>', 1, 4)
+    [{'lnum': 4, 'byteidx': 0, 'text': 'tik'}, {'lnum': 4, 'byteidx': 4, 'text': 'tok'}]
+<
+ If {submatch} is present and is v:true, then submatches like
+ " ", " ", etc. are also returned.  Example: >
+    " Assuming line 2 in buffer 2 contains "acd"
+    :echo matchbufline(2, '\(a\)\?\(b\)\?\(c\)\?\(.*\)', 2, 2
+ \ {'submatches': v:true})
+    [{'lnum': 2, 'byteidx': 0, 'text': 'acd', 'submatches': ['a', '', 'c', 'd', '', '', '', '', '']}]
+< The "submatches" List always contains 9 items. If a submatch
+ is not found, then an empty string is returned for that
+ submatch.
+
+ Can also be used as a |method|: >
+ GetBuffer()->matchbufline('mypat', 1, '$')

matchdelete({id} [, {win}) *matchdelete()* *E802* *E803*
Deletes a match with ID {id} previously defined by |matchadd()|
@@ -6187,6 +6236,40 @@ matchlist({expr}, {pat} [, {start} [, {count}]]) *matchlist()*

Can also be used as a |method|: >
GetText()->matchlist('word')
+<
+ *matchstrlist()*
+matchstrlist({list}, {pat} [, {dict}])
+ Returns the |List| of matches in {list} where {pat} matches.
+ {list} is a |List| of strings. {pat} is matched against each
+ string in {list}.
+
+ The {dict} argument supports following items:
+ submatches include submatch information (|/\(|)
+
+ For each match, a |Dict| with the following items is returned:
+ byteidx starting byte index of the match.
+ idx index in {list} of the match.
+ text matched string
+ submatches a List of submatches. Present only if
+ "submatches" is set to v:true in {dict}.
+
+ Example: >
+    :echo matchstrlist(['tik tok'], '\<\k\+\>')
+    [{'idx': 0, 'byteidx': 0, 'text': 'tik'}, {'idx': 0, 'byteidx': 4, 'text': 'tok'}]
+    :echo matchstrlist(['a', 'b'], '\<\k\+\>')
+    [{'idx': 0, 'byteidx': 0, 'text': 'a'}, {'idx': 1, 'byteidx': 0, 'text': 'b'}]
+<
+ If "submatches" is present and is v:true, then submatches like
+ " ", " ", etc. are also returned. Example: >
+ :echo matchstrlist(['acd'], '\(a\)\?\(b\)\?\(c\)\?\(.*\)',
+ \ #{submatches: v:true})
+ [{'idx': 0, 'byteidx': 0, 'text': 'acd', 'submatches': ['a', '', 'c', 'd', '', '', '', '', '']}]
+< The "submatches" List always contains 9 items. If a submatch
+ is not found, then an empty string is returned for that
+ submatch.
+
+ Can also be used as a |method|: >
+ GetListOfStrings()->matchstrlist('mypat')

matchstr({expr}, {pat} [, {start} [, {count}]]) *matchstr()*
Same as |match()|, but return the matched string. Example: >
diff --git a/runtime/doc/tags b/runtime/doc/tags
index e8ac2bc19..9af1ac0f6 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -8594,6 +8594,7 @@ match-parens tips.txt /*match-parens*
matchadd() builtin.txt /*matchadd()*
matchaddpos() builtin.txt /*matchaddpos()*
matcharg() builtin.txt /*matcharg()*
+matchbufline() builtin.txt /*matchbufline()*
matchdelete() builtin.txt /*matchdelete()*
matchend() builtin.txt /*matchend()*
matchfuzzy() builtin.txt /*matchfuzzy()*
@@ -8602,6 +8603,7 @@ matchit-install usr_05.txt /*matchit-install*
matchlist() builtin.txt /*matchlist()*
matchparen pi_paren.txt /*matchparen*
matchstr() builtin.txt /*matchstr()*
+matchstrlist() builtin.txt /*matchstrlist()*
matchstrpos() builtin.txt /*matchstrpos()*
matlab-indent indent.txt /*matlab-indent*
matlab-indenting indent.txt /*matlab-indenting*
diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt
index c4f2a8c4b..2286d4851 100644
--- a/runtime/doc/usr_41.txt
+++ b/runtime/doc/usr_41.txt
@@ -743,10 +743,13 @@ String manipulation: *string-functions*
toupper() turn a string to uppercase
charclass() class of a character
match() position where a pattern matches in a string
+ matchbufline() all the matches of a pattern in a buffer
matchend() position where a pattern match ends in a string
matchfuzzy() fuzzy matches a string in a list of strings
matchfuzzypos() fuzzy matches a string in a list of strings
matchstr() match of a pattern in a string
+ matchstrlist() all the matches of a pattern in a List of
+ strings
matchstrpos() match and positions of a pattern in a string
matchlist() like matchstr() and also return submatches
stridx() first index of a short string in a long string
diff --git a/src/errors.h b/src/errors.h
index 828e8de24..dfb6ce644 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -1749,9 +1749,11 @@ EXTERN char e_recursive_loop_loading_syncolor_vim[]
#endif
EXTERN char e_buffer_nr_invalid_buffer_number[]
INIT(= N_("E680: <buffer=%d>: invalid buffer number"));
-#ifdef FEAT_QUICKFIX
+#if defined(FEAT_QUICKFIX) || defined(FEAT_EVAL)
EXTERN char e_buffer_is_not_loaded[]
INIT(= N_("E681: Buffer is not loaded"));
+#endif
+#ifdef FEAT_QUICKFIX
EXTERN char e_invalid_search_pattern_or_delimiter[]
INIT(= N_("E682: Invalid search pattern or delimiter"));
EXTERN char e_file_name_missing_or_invalid_pattern[]
diff --git a/src/evalfunc.c b/src/evalfunc.c
index 513ddccdb..e37b3a412 100644
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -100,9 +100,11 @@ static void f_line2byte(typval_T *argvars, typval_T *rettv);
static void f_luaeval(typval_T *argvars, typval_T *rettv);
#endif
static void f_match(typval_T *argvars, typval_T *rettv);
+static void f_matchbufline(typval_T *argvars, typval_T *rettv);
static void f_matchend(typval_T *argvars, typval_T *rettv);
static void f_matchlist(typval_T *argvars, typval_T *rettv);
static void f_matchstr(typval_T *argvars, typval_T *rettv);
+static void f_matchstrlist(typval_T *argvars, typval_T *rettv);
static void f_matchstrpos(typval_T *argvars, typval_T *rettv);
static void f_max(typval_T *argvars, typval_T *rettv);
static void f_min(typval_T *argvars, typval_T *rettv);
@@ -1176,6 +1178,8 @@ static argcheck_T arg2_map[] = {arg_list_or_dict_or_blob_or_string_mod, arg_map_
static argcheck_T arg2_mapnew[] = {arg_list_or_dict_or_blob_or_string, arg_any};
static argcheck_T arg25_matchadd[] = {arg_string, arg_string, arg_number, arg_number, arg_dict_any};
static argcheck_T arg25_matchaddpos[] = {arg_string, arg_list_any, arg_number, arg_number, arg_dict_any};
+static argcheck_T arg23_matchstrlist[] = {arg_list_string, arg_string, arg_dict_any};
+static argcheck_T arg45_matchbufline[] = {arg_buffer, arg_string, arg_lnum, arg_lnum, arg_dict_any};
static argcheck_T arg119_printf[] = {arg_string_or_nr, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any, arg_any};
static argcheck_T arg23_reduce[] = {arg_string_list_or_blob, arg_any, arg_any};
static argcheck_T arg24_remote_expr[] = {arg_string, arg_string, arg_string, arg_number};
@@ -2285,6 +2289,8 @@ static funcentry_T global_functions[] =
ret_number, f_matchaddpos},
{"matcharg", 1, 1, FEARG_1, arg1_number,
ret_list_string, f_matcharg},
+ {"matchbufline", 4, 5, FEARG_1, arg45_matchbufline,
+ ret_list_any, f_matchbufline},
{"matchdelete", 1, 2, FEARG_1, arg2_number,
ret_number_bool, f_matchdelete},
{"matchend", 2, 4, FEARG_1, arg24_match_func,
@@ -2297,6 +2303,8 @@ static funcentry_T global_functions[] =
ret_list_string, f_matchlist},
{"matchstr", 2, 4, FEARG_1, arg24_match_func,
ret_string, f_matchstr},
+ {"matchstrlist", 2, 3, FEARG_1, arg23_matchstrlist,
+ ret_list_any, f_matchstrlist},
{"matchstrpos", 2, 4, FEARG_1, arg24_match_func,
ret_list_any, f_matchstrpos},
{"max", 1, 1, FEARG_1, arg1_list_or_dict,
@@ -8023,6 +8031,183 @@ theend:
p_cpo = save_cpo;
}

+/*
+ * Return all the matches in string "str" for pattern "rmp".
+ * The matches are returned in the List "mlist".
+ * If "submatches" is TRUE, then submatch information is also returned.
+ * "matchbuf" is TRUE when called for matchbufline().
+ */
+ static int
+get_matches_in_str(
+ char_u *str,
+ regmatch_T *rmp,
+ list_T *mlist,
+ int idx,
+ int submatches,
+ int matchbuf)
+{
+ long len = (long)STRLEN(str);
+ int match = 0;
+ colnr_T startidx = 0;
+
+ for (;;)
+ {
+ match = vim_regexec_nl(rmp, str, startidx);
+ if (!match)
+ break;
+
+ dict_T *d = dict_alloc();
+ if (d == NULL)
+ return FAIL;
+ if (list_append_dict(mlist, d) == FAIL)
+ return FAIL;;
+
+ if (dict_add_number(d, matchbuf ? "lnum" : "idx", idx) == FAIL)
+ return FAIL;
+
+ if (dict_add_number(d, "byteidx",
+ (colnr_T)(rmp->startp[0] - str)) == FAIL)
+ return FAIL;
+
+ if (dict_add_string_len(d, "text", rmp->startp[0],
+ (int)(rmp->endp[0] - rmp->startp[0])) == FAIL)
+ return FAIL;
+
+ if (submatches)
+ {
+ list_T *sml = list_alloc();
+ if (sml == NULL)
+ return FAIL;
+
+ if (dict_add_list(d, "submatches", sml) == FAIL)
+ return FAIL;
+
+ // return a list with the submatches
+ for (int i = 1; i < NSUBEXP; ++i)
+ {
+ if (rmp->endp[i] == NULL)
+ {
+ if (list_append_string(sml, (char_u *)"", 0) == FAIL)
+ return FAIL;
+ }
+ else if (list_append_string(sml, rmp->startp[i],
+ (int)(rmp->endp[i] - rmp->startp[i])) == FAIL)
+ return FAIL;
+ }
+ }
+ startidx = (colnr_T)(rmp->endp[0] - str);
+ if (startidx >= (colnr_T)len || str + startidx <= rmp->startp[0])
+ break;
+ }
+
+ return OK;
+}
+
+/*
+ * "matchbufline()" function
+ */
+ static void
+f_matchbufline(typval_T *argvars, typval_T *rettv)
+{
+ list_T *retlist = NULL;
+ char_u *save_cpo;
+ char_u patbuf[NUMBUFLEN];
+ regmatch_T regmatch;
+
+ rettv->vval.v_number = -1;
+ if (rettv_list_alloc(rettv) != OK)
+ return;
+ retlist = rettv->vval.v_list;
+
+ if (check_for_buffer_arg(argvars, 0) == FAIL
+ || check_for_string_arg(argvars, 1) == FAIL
+ || check_for_lnum_arg(argvars, 2) == FAIL
+ || check_for_lnum_arg(argvars, 3) == FAIL
+ || check_for_opt_dict_arg(argvars, 4) == FAIL)
+ return;
+
+ int prev_did_emsg = did_emsg;
+ buf_T *buf = tv_get_buf(&argvars[0], FALSE);
+ if (buf == NULL)
+ {
+ if (did_emsg == prev_did_emsg)
+ semsg(_(e_invalid_buffer_name_str), tv_get_string(&argvars[0]));
+ return;
+ }
+ if (buf->b_ml.ml_mfp == NULL)
+ {
+ emsg(_(e_buffer_is_not_loaded));
+ return;
+ }
+
+ char_u *pat = tv_get_string_buf(&argvars[1], patbuf);
+
+ int did_emsg_before = did_emsg;
+ linenr_T slnum = tv_get_lnum_buf(&argvars[2], buf);
+ if (did_emsg > did_emsg_before)
+ return;
+ if (slnum < 1)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "lnum");
+ return;
+ }
+
+ linenr_T elnum = tv_get_lnum_buf(&argvars[3], buf);
+ if (did_emsg > did_emsg_before)
+ return;
+ if (elnum < 1 || elnum < slnum)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "end_lnum");
+ return;
+ }
+
+ if (elnum > buf->b_ml.ml_line_count)
+ elnum = buf->b_ml.ml_line_count;
+
+ int submatches = FALSE;
+ if (argvars[4].v_type != VAR_UNKNOWN)
+ {
+ dict_T *d = argvars[4].vval.v_dict;
+ if (d != NULL)
+ {
+ dictitem_T *di = dict_find(d, (char_u *)"submatches", -1);
+ if (di != NULL)
+ {
+ if (di->di_tv.v_type != VAR_BOOL)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "submatches");
+ return;
+ }
+ submatches = tv_get_bool(&di->di_tv);
+ }
+ }
+ }
+
+ // Make 'cpoptions' empty, the 'l' flag should not be used here.
+ save_cpo = p_cpo;
+ p_cpo = empty_option;
+
+ regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+ if (regmatch.regprog == NULL)
+ goto theend;
+ regmatch.rm_ic = p_ic;
+
+ while (slnum <= elnum)
+ {
+ char_u *str = ml_get_buf(buf, slnum, FALSE);
+ if (get_matches_in_str(str, &regmatch, retlist, slnum, submatches,
+ TRUE) == FAIL)
+ goto cleanup;
+ slnum++;
+ }
+
+cleanup:
+ vim_regfree(regmatch.regprog);
+
+theend:
+ p_cpo = save_cpo;
+}
+
/*
* "match()" function
*/
@@ -8059,6 +8244,85 @@ f_matchstr(typval_T *argvars, typval_T *rettv)
find_some_match(argvars, rettv, MATCH_STR);
}

+/*
+ * "matchstrlist()" function
+ */
+ static void
+f_matchstrlist(typval_T *argvars, typval_T *rettv)
+{
+ list_T *retlist = NULL;
+ char_u *save_cpo;
+ list_T *l = NULL;
+ listitem_T *li = NULL;
+ char_u patbuf[NUMBUFLEN];
+ regmatch_T regmatch;
+
+ rettv->vval.v_number = -1;
+ if (rettv_list_alloc(rettv) != OK)
+ return;
+ retlist = rettv->vval.v_list;
+
+ if (check_for_list_arg(argvars, 0) == FAIL
+ || check_for_string_arg(argvars, 1) == FAIL
+ || check_for_opt_dict_arg(argvars, 2) == FAIL)
+ return;
+
+ if ((l = argvars[0].vval.v_list) == NULL)
+ return;
+
+ char_u *pat = tv_get_string_buf_chk(&argvars[1], patbuf);
+ if (pat == NULL)
+ return;
+
+ // Make 'cpoptions' empty, the 'l' flag should not be used here.
+ save_cpo = p_cpo;
+ p_cpo = empty_option;
+
+ regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+ if (regmatch.regprog == NULL)
+ goto theend;
+ regmatch.rm_ic = p_ic;
+
+ int submatches = FALSE;
+ if (argvars[2].v_type != VAR_UNKNOWN)
+ {
+ dict_T *d = argvars[2].vval.v_dict;
+ if (d != NULL)
+ {
+ dictitem_T *di = dict_find(d, (char_u *)"submatches", -1);
+ if (di != NULL)
+ {
+ if (di->di_tv.v_type != VAR_BOOL)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "submatches");
+ goto cleanup;
+ }
+ submatches = tv_get_bool(&di->di_tv);
+ }
+ }
+ }
+
+ int idx = 0;
+ CHECK_LIST_MATERIALIZE(l);
+ FOR_ALL_LIST_ITEMS(l, li)
+ {
+ if (li->li_tv.v_type == VAR_STRING && li->li_tv.vval.v_string != NULL)
+ {
+ char_u *str = li->li_tv.vval.v_string;
+ if (get_matches_in_str(str, &regmatch, retlist, idx, submatches,
+ FALSE) == FAIL)
+ goto cleanup;
+ }
+ idx++;
+ }
+
+cleanup:
+ vim_regfree(regmatch.regprog);
+
+theend:
+ p_cpo = save_cpo;
+}
+
/*
* "matchstrpos()" function
*/
diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim
index 7bfe7fa75..85ccc78b8 100644
--- a/src/testdir/test_functions.vim
+++ b/src/testdir/test_functions.vim
@@ -1172,6 +1172,192 @@ func Test_matchstrpos()
call assert_equal(['', -1, -1], matchstrpos(test_null_list(), ' '))
endfunc

+" Test for matchstrlist()
+func Test_matchstrlist()
+ let lines =<< trim END
+ #" Basic match
+ call assert_equal([{'idx': 0, 'byteidx': 1, 'text': 'bout'},
+ \ {'idx': 1, 'byteidx': 1, 'text': 'bove'}],
+ \ matchstrlist(['about', 'above'], 'bo.*'))
+ #" no match
+ call assert_equal([], matchstrlist(['about', 'above'], 'xy.*'))
+ #" empty string
+ call assert_equal([], matchstrlist([''], '.'))
+ #" empty pattern
+ call assert_equal([{'idx': 0, 'byteidx': 0, 'text': ''}], matchstrlist(['abc'], ''))
+ #" method call
+ call assert_equal([{'idx': 0, 'byteidx': 2, 'text': 'it'}], ['editor']->matchstrlist('ed\zsit\zeor'))
+ #" single character matches
+ call assert_equal([{'idx': 0, 'byteidx': 5, 'text': 'r'}],
+ \ ['editor']->matchstrlist('r'))
+ call assert_equal([{'idx': 0, 'byteidx': 0, 'text': 'a'}], ['a']->matchstrlist('a'))
+ call assert_equal([{'idx': 0, 'byteidx': 0, 'text': ''}],
+ \ matchstrlist(['foobar'], '\zs'))
+ #" string with tabs
+ call assert_equal([{'idx': 0, 'byteidx': 1, 'text': 'foo'}],
+ \ matchstrlist([" foobar"], 'foo'))
+ #" string with multibyte characters
+ call assert_equal([{'idx': 0, 'byteidx': 2, 'text': '😊😊'}],
+ \ matchstrlist([" 😊😊"], '\k\+'))
+
+ #" null string
+ call assert_equal([], matchstrlist(test_null_list(), 'abc'))
+ call assert_equal([], matchstrlist([test_null_string()], 'abc'))
+ call assert_equal([{'idx': 0, 'byteidx': 0, 'text': ''}],
+ \ matchstrlist(['abc'], test_null_string()))
+
+ #" sub matches
+ call assert_equal([{'idx': 0, 'byteidx': 0, 'text': 'acd', 'submatches': ['a', '', 'c', 'd', '', '', '', '', '']}], matchstrlist(['acd'], '\(a\)\?\(b\)\?\(c\)\?\(.*\)', {'submatches': v:true}))
+
+ #" null dict argument
+ call assert_equal([{'idx': 0, 'byteidx': 0, 'text': 'vim'}],
+ \ matchstrlist(['vim'], '\w\+', test_null_dict()))
+
+ #" Error cases
+ call assert_fails("echo matchstrlist('abc', 'a')", 'E1211: List required for argument 1')
+ call assert_fails("echo matchstrlist(['abc'], {})", 'E1174: String required for argument 2')
+ call assert_fails("echo matchstrlist(['abc'], '.', [])", 'E1206: Dictionary required for argument 3')
+ call assert_fails("echo matchstrlist(['abc'], 'a', {'submatches': []})", 'E475: Invalid value for argument submatches')
+ call assert_fails("echo matchstrlist(['abc'], '\@=')", 'E866: (NFA regexp) Misplaced @')
+ END
+ call v9.CheckLegacyAndVim9Success(lines)
+
+ let lines =<< trim END
+ vim9script
+ # non string items
+ matchstrlist([0z10, {'a': 'x'}], 'x')
+ END
+ call v9.CheckSourceSuccess(lines)
+
+ let lines =<< trim END
+ vim9script
+ def Foo()
+ # non string items
+ assert_equal([], matchstrlist([0z10, {'a': 'x'}], 'x'))
+ enddef
+ Foo()
+ END
+ call v9.CheckSourceFailure(lines, 'E1013: Argument 1: type mismatch, expected list<string> but got list<any>', 2)
+endfunc
+
+" Test for matchbufline()
+func Test_matchbufline()
+ let lines =<< trim END
+ #" Basic match
+ new
+ call setline(1, ['about', 'above', 'below'])
+ VAR bnr = bufnr()
+ wincmd w
+ call assert_equal([{'lnum': 1, 'byteidx': 1, 'text': 'bout'},
+ \ {'lnum': 2, 'byteidx': 1, 'text': 'bove'}],
+ \ matchbufline(bnr, 'bo.*', 1, '$'))
+ #" multiple matches in a line
+ call setbufline(bnr, 1, ['about about', 'above above', 'below'])
+ call assert_equal([{'lnum': 1, 'byteidx': 1, 'text': 'bout'},
+ \ {'lnum': 1, 'byteidx': 7, 'text': 'bout'},
+ \ {'lnum': 2, 'byteidx': 1, 'text': 'bove'},
+ \ {'lnum': 2, 'byteidx': 7, 'text': 'bove'}],
+ \ matchbufline(bnr, 'bo\k\+', 1, '$'))
+ #" no match
+ call assert_equal([], matchbufline(bnr, 'xy.*', 1, '$'))
+ #" match on a particular line
+ call assert_equal([{'lnum': 2, 'byteidx': 7, 'text': 'bove'}],
+ \ matchbufline(bnr, 'bo\k\+$', 2, 2))
+ #" match on a particular line
+ call assert_equal([], matchbufline(bnr, 'bo.*', 3, 3))
+ #" empty string
+ call deletebufline(bnr, 1, '$')
+ call assert_equal([], matchbufline(bnr, '.', 1, '$'))
+ #" empty pattern
+ call setbufline(bnr, 1, 'abc')
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': ''}],
+ \ matchbufline(bnr, '', 1, '$'))
+ #" method call
+ call setbufline(bnr, 1, 'editor')
+ call assert_equal([{'lnum': 1, 'byteidx': 2, 'text': 'it'}],
+ \ bnr->matchbufline('ed\zsit\zeor', 1, 1))
+ #" single character matches
+ call assert_equal([{'lnum': 1, 'byteidx': 5, 'text': 'r'}],
+ \ matchbufline(bnr, 'r', 1, '$'))
+ call setbufline(bnr, 1, 'a')
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': 'a'}],
+ \ matchbufline(bnr, 'a', 1, '$'))
+ #" zero-width match
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': ''}],
+ \ matchbufline(bnr, '\zs', 1, '$'))
+ #" string with tabs
+ call setbufline(bnr, 1, " foobar")
+ call assert_equal([{'lnum': 1, 'byteidx': 1, 'text': 'foo'}],
+ \ matchbufline(bnr, 'foo', 1, '$'))
+ #" string with multibyte characters
+ call setbufline(bnr, 1, " 😊😊")
+ call assert_equal([{'lnum': 1, 'byteidx': 2, 'text': '😊😊'}],
+ \ matchbufline(bnr, '\k\+', 1, '$'))
+ #" empty buffer
+ call deletebufline(bnr, 1, '$')
+ call assert_equal([], matchbufline(bnr, 'abc', 1, '$'))
+
+ #" Non existing buffer
+ call setbufline(bnr, 1, 'abc')
+ call assert_fails("echo matchbufline(5000, 'abc', 1, 1)", 'E158: Invalid buffer name: 5000')
+ #" null string
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': ''}],
+ \ matchbufline(bnr, test_null_string(), 1, 1))
+ #" invalid starting line number
+ call assert_equal([], matchbufline(bnr, 'abc', 100, 100))
+ #" ending line number greater than the last line
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': 'abc'}],
+ \ matchbufline(bnr, 'abc', 1, 100))
+ #" ending line number greater than the starting line number
+ call setbufline(bnr, 1, ['one', 'two'])
+ call assert_fails($"echo matchbufline({bnr}, 'abc', 2, 1)", 'E475: Invalid value for argument end_lnum')
+
+ #" sub matches
+ call deletebufline(bnr, 1, '$')
+ call setbufline(bnr, 1, 'acd')
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': 'acd', 'submatches': ['a', '', 'c', 'd', '', '', '', '', '']}],
+ \ matchbufline(bnr, '\(a\)\?\(b\)\?\(c\)\?\(.*\)', 1, '$', {'submatches': v:true}))
+
+ #" null dict argument
+ call assert_equal([{'lnum': 1, 'byteidx': 0, 'text': 'acd'}],
+ \ matchbufline(bnr, '\w\+', '$', '$', test_null_dict()))
+
+ #" Error cases
+ call assert_fails("echo matchbufline([1], 'abc', 1, 1)", 'E1220: String or Number required for argument 1')
+ call assert_fails("echo matchbufline(1, {}, 1, 1)", 'E1174: String required for argument 2')
+ call assert_fails("echo matchbufline(1, 'abc', {}, 1)", 'E1220: String or Number required for argument 3')
+ call assert_fails("echo matchbufline(1, 'abc', 1, {})", 'E1220: String or Number required for argument 4')
+ call assert_fails($"echo matchbufline({bnr}, 'abc', -1, '$')", 'E475: Invalid value for argument lnum')
+ call assert_fails($"echo matchbufline({bnr}, 'abc', 1, -1)", 'E475: Invalid value for argument end_lnum')
+ call assert_fails($"echo matchbufline({bnr}, '\@=', 1, 1)", 'E866: (NFA regexp) Misplaced @')
+ call assert_fails($"echo matchbufline({bnr}, 'abc', 1, 1, {{'submatches': []}})", 'E475: Invalid value for argument submatches')
+ :%bdelete!
+ call assert_fails($"echo matchbufline({bnr}, 'abc', 1, '$'))", 'E681: Buffer is not loaded')
+ END
+ call v9.CheckLegacyAndVim9Success(lines)
+
+ call assert_fails($"echo matchbufline('', 'abc', 'abc', 1)", 'E475: Invalid value for argument lnum')
+ call assert_fails($"echo matchbufline('', 'abc', 1, 'abc')", 'E475: Invalid value for argument end_lnum')
+
+ let lines =<< trim END
+ vim9script
+ def Foo()
+ echo matchbufline('', 'abc', 'abc', 1)
+ enddef
+ Foo()
+ END
+ call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "abc"', 1)
+
+ let lines =<< trim END
+ vim9script
+ def Foo()
+ echo matchbufline('', 'abc', 1, 'abc')
+ enddef
+ Foo()
+ END
+ call v9.CheckSourceFailure(lines, 'E1030: Using a String as a Number: "abc"', 1)
+endfunc
+
func Test_nextnonblank_prevnonblank()
new
insert
diff --git a/src/version.c b/src/version.c
index 3839d9c35..2e7f2871e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -704,6 +704,8 @@ static char *(features[]) =

static int included_patches[] =
{ /* Add new patch number below this line */
+/**/
+ 9,
/**/
8,
/**/

Yegappan Lakshmanan

unread,
Jan 7, 2024, 11:30:41 AM1/7/24
to vim...@googlegroups.com, vim_use
Hi all,

On Thu, Jan 4, 2024 at 1:45 PM Christian Brabandt <cbl...@256bit.org> wrote:
patch 9.1.0009: Cannot easily get the list of matches

Commit: https://github.com/vim/vim/commit/f93b1c881a99fa847a1bafa71877d7e16f18e6ef
Author: Yegappan Lakshmanan <yega...@yahoo.com>
Date:   Thu Jan 4 22:28:46 2024 +0100

    patch 9.1.0009: Cannot easily get the list of matches

    Problem:  Cannot easily get the list of matches
    Solution: Add the matchstrlist() and matchbufline() Vim script
              functions (Yegappan Lakshmanan)


To demonstrate the use of the new matchbufline() function, I have created
the following script.  This does search text completion from the list of words
in the current buffer.  After typing a few letters in the "/" prompt, if you press
Tab, it will complete the word from the current buffer.  If you press Tab again,
then it will go to the next match.  If you press Shift-Tab, it will go back to
the previous match.

Regards,
Yegappan

--------------------------------------------------------------------------------------------------------
vim9script

var searchMatches: list<string> = []
var prevMatchIndex: number = 0
var prevCmdline: string = ''
var prevCmdlinePrefix: string = ''
var prevPat: string = ''

def SearchComplete(forward: bool): string
  var cmdtype: string = getcmdtype()
  if cmdtype != '/' && cmdtype != '?'
    feedkeys(forward ? "\<Tab>" : "\<S-Tab>", 'nt')
    return ''
  endif

  var cmdline: string = getcmdline()
  if cmdline != '' && cmdline ==# prevCmdline
    # Jump to the next/previous match
    if (cmdtype == '/' && forward) || (cmdtype == '?' && !forward)
      prevMatchIndex += 1
    else
      prevMatchIndex -= 1
    endif
    if prevMatchIndex < 0
      prevMatchIndex = searchMatches->len() - 1
    elseif prevMatchIndex >= searchMatches->len()
      prevMatchIndex = 0
    endif
    var s = searchMatches[prevMatchIndex][strlen(prevPat) : ]

    prevCmdline = prevCmdlinePrefix .. s
    setcmdline(prevCmdlinePrefix)
    return s
  endif

  var cmdpos: number = getcmdpos()

  var start: number = cmdpos - 1
  if start >= strlen(cmdline)
    start -= 1
  endif
  while start > 0 && cmdline[start - 1] =~ '\k'
    start -= 1
  endwhile
  var pat: string = cmdline[start : cmdpos]

  # Get the List of matches
  var start_lnum = &wrapscan || cmdtype == '?' ? 1 : line('.')
  var end_lnum = &wrapscan || cmdtype == '/' ? '$' : line('.')
  var l = matchbufline('', $'\<{pat}\k\+\>', start_lnum, end_lnum)
  if l->empty()
    return ''
  endif

  var curline = line('.')
  var curcol = col('.')
  var patStartByte = curcol - 1 - strlen(pat)

  l->filter((_, v) => v.lnum == curline ? cmdtype == '/' ? v.byteidx >= patStartByte : v.byteidx <= patStartByte : true)
  if l->empty()
    return ''
  endif

  # Sort by matched string and line/byte index
  l->sort((a, b) => {
    if a.text > b.text
      return 1
    elseif a.text == b.text
      if a.lnum > b.lnum
        return 1
      elseif a.lnum == b.lnum
        return a.byteidx > b.byteidx ? 1 : 0
      endif
      return 0
    endif
    return 0
  })
  # Remove duplicates
  l->uniq((a, b) => a.text ==# b.text ? 0 : 1)
  # Sort by byte index and line number
  l->sort((a, b) => a.lnum == b.lnum ? a.byteidx - b.byteidx : a.lnum - b.lnum)

  var startIdx: number
  if cmdtype == '/'
    startIdx = l->indexof((_, v) => v.lnum == curline ? v.byteidx >= patStartByte : v.lnum > curline)
  else
    startIdx = l->copy()->reverse()->indexof((_, v) => v.lnum == curline ? v.byteidx <= patStartByte : v.lnum < curline)
    startIdx = l->len() - startIdx - 1
  endif
 
  if startIdx == -1
    startIdx = forward ? 0 : searchMatches->len() - 1
  endif

  # Save the matched strings
  searchMatches = l->map((_, v) => v.text)

  var s = searchMatches[startIdx][strlen(pat) : ]

  # Save the state for jumping to the next previous match
  prevMatchIndex = startIdx
  prevPat = pat
  prevCmdline = cmdline .. s
  prevCmdlinePrefix = cmdline

  return s
enddef

cnoremap <Tab> <C-R>=<SID>SearchComplete(v:true)<CR>
cnoremap <S-Tab> <C-R>=<SID>SearchComplete(v:false)<CR>

# vim: ts=8 sw=2 sts=2 expandtab tw=80
--------------------------------------------------------------------------------------------------------

Christian Brabandt

unread,
Jan 8, 2024, 8:29:50 AM1/8/24
to vim...@googlegroups.com, vim...@googlegroups.com

On So, 07 Jan 2024, Yegappan Lakshmanan wrote:

> To demonstrate the use of the new matchbufline() function, I have created
> the following script.  This does search text completion from the list of words
> in the current buffer.  After typing a few letters in the "/" prompt, if you
> press
> Tab, it will complete the word from the current buffer.  If you press Tab
> again,
> then it will go to the next match.  If you press Shift-Tab, it will go back to
> the previous match.

That is great, thanks for sharing!

Thanks,
Christian
--
Two is not equal to three, even for large values of two.

Gary Johnson

unread,
Jan 8, 2024, 3:14:20 PM1/8/24
to vim...@googlegroups.com, vim_use
On 2024-01-07, Yegappan Lakshmanan wrote:

> To demonstrate the use of the new matchbufline() function, I have created
> the following script.  This does search text completion from the list of words
> in the current buffer.  After typing a few letters in the "/" prompt, if you
> press
> Tab, it will complete the word from the current buffer.  If you press Tab
> again,
> then it will go to the next match.  If you press Shift-Tab, it will go back to
> the previous match.

I saved your script to a file and sourced it, but it gives me an
error.

Error detected while processing /home/gary/.vim/match_tab.vim:
line 9:
E1171: Missing } after inline function

Line 9 is:

def SearchComplete(forward: bool): string

I've updated to 9.1.16 and done a clean build without any
configuration options.

I haven't done anything with vim9script, so I don't recognize syntax
errors by sight, nor do I know what else I might be missing.

Regards,
Gary

--------------------------------------------------------------------
$ VIMRUNTIME=runtime src/vim --version
VIM - Vi IMproved 9.1 (2024 Jan 02, compiled Jan 8 2024 11:48:09)
Included patches: 1-16
Compiled by gary@epicurus
Huge version with GTK3 GUI. Features included (+) or not (-):
+acl +file_in_path +mouse_urxvt -tag_any_white
+arabic +find_in_path +mouse_xterm -tcl
+autocmd +float +multi_byte +termguicolors
+autochdir +folding +multi_lang +terminal
-autoservername -footer -mzscheme +terminfo
+balloon_eval +fork() +netbeans_intg +termresponse
+balloon_eval_term +gettext +num64 +textobjects
+browse -hangul_input +packages +textprop
++builtin_terms +iconv +path_extra +timers
+byte_offset +insert_expand -perl +title
+channel +ipv6 +persistent_undo +toolbar
+cindent +job +popupwin +user_commands
+clientserver +jumplist +postscript +vartabs
+clipboard +keymap +printer +vertsplit
+cmdline_compl +lambda +profile +vim9script
+cmdline_hist +langmap -python +viminfo
+cmdline_info +libcall -python3 +virtualedit
+comments +linebreak +quickfix +visual
+conceal +lispindent +reltime +visualextra
+cryptv +listcmds +rightleft +vreplace
+cscope +localmap -ruby +wildignore
+cursorbind -lua +scrollbind +wildmenu
+cursorshape +menu +signs +windows
+dialog_con_gui +mksession +smartindent +writebackup
+diff +modify_fname +sodium +X11
+digraphs +mouse +sound +xattr
+dnd +mouseshape +spell -xfontset
-ebcdic +mouse_dec +startuptime +xim
+emacs_tags +mouse_gpm +statusline +xpm
+eval -mouse_jsbterm -sun_workshop +xsmp_interact
+ex_extra +mouse_netterm +syntax +xterm_clipboard
+extra_search +mouse_sgr +tag_binary -xterm_save
-farsi -mouse_sysmouse -tag_old_static
system vimrc file: "$VIM/vimrc"
user vimrc file: "$HOME/.vimrc"
2nd user vimrc file: "~/.vim/vimrc"
user exrc file: "$HOME/.exrc"
system gvimrc file: "$VIM/gvimrc"
user gvimrc file: "$HOME/.gvimrc"
2nd user gvimrc file: "~/.vim/gvimrc"
defaults file: "$VIMRUNTIME/defaults.vim"
system menu file: "$VIMRUNTIME/menu.vim"
fall-back for $VIM: "/usr/local/share/vim"
Compilation: gcc -c -I. -Iproto -DHAVE_CONFIG_H -DFEAT_GUI_GTK -pthread -I/usr/include/gtk-3.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -I/usr/include/gtk-3.0 -I/usr/include/gio-unix-2.0 -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 -I/usr/include/fribidi -I/usr/include/harfbuzz -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/uuid -I/usr/include/freetype2 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng16 -I/usr/include/x86_64-linux-gnu -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include -O2 -fno-strength-reduce -Wall -Wno-deprecated-declarations -D_REENTRANT -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=1
Linking: gcc -L/usr/local/lib -Wl,--as-needed -o vim -lgtk-3 -lgdk-3 -lpangocairo-1.0 -lpango-1.0 -lharfbuzz -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lSM -lICE -lXpm -lXt -lX11 -lXdmcp -lSM -lICE -lm -ltinfo -lselinux -lcanberra -lsodium -lacl -lattr -lgpm

Yegappan Lakshmanan

unread,
Jan 8, 2024, 4:57:30 PM1/8/24
to vim...@googlegroups.com, vim_use
Hi Gary,

On Mon, Jan 8, 2024 at 12:14 PM Gary Johnson <gary...@spocom.com> wrote:
On 2024-01-07, Yegappan Lakshmanan wrote:

> To demonstrate the use of the new matchbufline() function, I have created
> the following script.  This does search text completion from the list of words
> in the current buffer.  After typing a few letters in the "/" prompt, if you
> press
> Tab, it will complete the word from the current buffer.  If you press Tab
> again,
> then it will go to the next match.  If you press Shift-Tab, it will go back to
> the previous match.

I saved your script to a file and sourced it, but it gives me an
error.

    Error detected while processing /home/gary/.vim/match_tab.vim:
    line    9:
    E1171: Missing } after inline function

Line 9 is:

    def SearchComplete(forward: bool): string


I am not sure what is wrong from the above error message.  In case there
is a cut/paste error, can you please try the attached file?

- Yegappan
searchcomplete.vim

Gary Johnson

unread,
Jan 8, 2024, 6:04:04 PM1/8/24
to vim...@googlegroups.com, vim_use
On 2024-01-08, Yegappan Lakshmanan wrote:
> Hi Gary,
>
> On Mon, Jan 8, 2024 at 12:14 PM Gary Johnson wrote:
>
> On 2024-01-07, Yegappan Lakshmanan wrote:
>
> > To demonstrate the use of the new matchbufline() function, I have created
> > the following script.  This does search text completion from the list of
> words
> > in the current buffer.  After typing a few letters in the "/" prompt, if
> you
> > press
> > Tab, it will complete the word from the current buffer.  If you press Tab
> > again,
> > then it will go to the next match.  If you press Shift-Tab, it will go
> back to
> > the previous match.
>
> I saved your script to a file and sourced it, but it gives me an
> error.
>
>     Error detected while processing /home/gary/.vim/match_tab.vim:
>     line    9:
>     E1171: Missing } after inline function
>
> Line 9 is:
>
>     def SearchComplete(forward: bool): string
>
>
>
> I am not sure what is wrong from the above error message.  In case there
> is a cut/paste error, can you please try the attached file?

Hi Yegappan,

Thank you. That fixed the problem. I had copied your script from
your original posting by decode-saving your message from mutt, then
deleting the text before and after the script. It turns out that
the leading spaces in that copy included non-breaking space
characters, 0xa0, which somehow confused Vim's parser.

I did have to convert your attachment from DOS to Unix line endings,
but that was immediately apparent and not a problem.

Regards,
Gary

Yegappan Lakshmanan

unread,
Jan 9, 2024, 9:51:41 AM1/9/24
to vim...@googlegroups.com, vim_use
On Sun, Jan 7, 2024 at 8:30 AM Yegappan Lakshmanan <yega...@gmail.com> wrote:
Hi all,

On Thu, Jan 4, 2024 at 1:45 PM Christian Brabandt <cbl...@256bit.org> wrote:
patch 9.1.0009: Cannot easily get the list of matches

Commit: https://github.com/vim/vim/commit/f93b1c881a99fa847a1bafa71877d7e16f18e6ef
Author: Yegappan Lakshmanan <yega...@yahoo.com>
Date:   Thu Jan 4 22:28:46 2024 +0100

    patch 9.1.0009: Cannot easily get the list of matches

    Problem:  Cannot easily get the list of matches
    Solution: Add the matchstrlist() and matchbufline() Vim script
              functions (Yegappan Lakshmanan)


To demonstrate the use of the new matchbufline() function, I have created
the following script.  This does search text completion from the list of words
in the current buffer.  After typing a few letters in the "/" prompt, if you press
Tab, it will complete the word from the current buffer.  If you press Tab again,
then it will go to the next match.  If you press Shift-Tab, it will go back to
the previous match.


I have uploaded this plugin to the https://github.com/yegappan/searchcomplete repository.

Regards,
Yegappan 

Reply all
Reply to author
Forward
0 new messages