# HG changeset patch # User Christian Brabandt # Date 1704404703 -3600 # Node ID 034445b3af100fc7c97e362ea62cb03f96eb0de8 # Parent 231c0e600dd6d7c11a8c1794872c698b7bb2ae7e patch 9.1.0009: Cannot easily get the list of matches Commit: https://github.com/vim/vim/commit/f93b1c881a99fa847a1bafa71877d7e16f18e6ef Author: Yegappan Lakshmanan 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 Signed-off-by: Christian Brabandt diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -370,6 +370,8 @@ matchadd({group}, {pattern} [, {priority 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} [, {c 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 + "\1", "\2", 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} [, {c 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 + "\1", "\2", 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 --- 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-ins 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 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -743,10 +743,13 @@ String manipulation: *string-functio 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 --- a/src/errors.h +++ b/src/errors.h @@ -1749,9 +1749,11 @@ EXTERN char e_recursive_loop_loading_syn #endif EXTERN char e_buffer_nr_invalid_buffer_number[] INIT(= N_("E680: : 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 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -100,9 +100,11 @@ static void f_line2byte(typval_T *argvar 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 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, @@ -8024,6 +8032,183 @@ theend: } /* + * 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, ®match, retlist, slnum, submatches, + TRUE) == FAIL) + goto cleanup; + slnum++; + } + +cleanup: + vim_regfree(regmatch.regprog); + +theend: + p_cpo = save_cpo; +} + +/* * "match()" function */ static void @@ -8060,6 +8245,85 @@ f_matchstr(typval_T *argvars, typval_T * } /* + * "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, ®match, retlist, idx, submatches, + FALSE) == FAIL) + goto cleanup; + } + idx++; + } + +cleanup: + vim_regfree(regmatch.regprog); + +theend: + p_cpo = save_cpo; +} + +/* * "matchstrpos()" function */ static void diff --git a/src/testdir/test_functions.vim b/src/testdir/test_functions.vim --- 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(), '\a')) 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(["\tfoobar"], 'foo')) + #" string with multibyte characters + call assert_equal([{'idx': 0, 'byteidx': 2, 'text': '😊😊'}], + \ matchstrlist(["\t\t😊😊"], '\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 but got list', 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, "\tfoobar") + call assert_equal([{'lnum': 1, 'byteidx': 1, 'text': 'foo'}], + \ matchbufline(bnr, 'foo', 1, '$')) + #" string with multibyte characters + call setbufline(bnr, 1, "\t\t😊😊") + 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 --- a/src/version.c +++ b/src/version.c @@ -705,6 +705,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 9, +/**/ 8, /**/ 7,