diff options
author | zeertzjq <zeertzjq@outlook.com> | 2024-01-16 08:00:08 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-16 08:00:08 +0800 |
commit | 73e1942abe7a96d63ce3749af4187f2cdff87e69 (patch) | |
tree | bd508b354771b2c01bbcc41dc83fb82a3d0d38b7 | |
parent | ae48d965d70cc721a3165c40ba0c34d95408e229 (diff) | |
download | rneovim-73e1942abe7a96d63ce3749af4187f2cdff87e69.tar.gz rneovim-73e1942abe7a96d63ce3749af4187f2cdff87e69.tar.bz2 rneovim-73e1942abe7a96d63ce3749af4187f2cdff87e69.zip |
vim-patch:9.1.0009: Cannot easily get the list of matches (#27028)
Problem: Cannot easily get the list of matches
Solution: Add the matchstrlist() and matchbufline() Vim script
functions (Yegappan Lakshmanan)
closes: vim/vim#13766
Omit CHECK_LIST_MATERIALIZE(): it populates a List with numbers only,
and there is a check for strings below.
https://github.com/vim/vim/commit/f93b1c881a99fa847a1bafa71877d7e16f18e6ef
vim-patch:eb3475df0d92
runtime(doc): Replace non-breaking space with normal space (vim/vim#13868)
https://github.com/vim/vim/commit/eb3475df0d927a178789cf8e7fc4983932e1cdbe
Co-authored-by: Yegappan Lakshmanan <4298407+yegappan@users.noreply.github.com>
-rw-r--r-- | runtime/doc/builtin.txt | 73 | ||||
-rw-r--r-- | runtime/doc/usr_41.txt | 3 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vimfn.lua | 85 | ||||
-rw-r--r-- | src/nvim/eval.lua | 93 | ||||
-rw-r--r-- | src/nvim/eval/funcs.c | 212 | ||||
-rw-r--r-- | src/nvim/eval/typval.c | 16 | ||||
-rw-r--r-- | src/nvim/globals.h | 2 | ||||
-rw-r--r-- | src/nvim/quickfix.c | 2 | ||||
-rw-r--r-- | src/nvim/sign.c | 2 | ||||
-rw-r--r-- | test/old/testdir/test_functions.vim | 188 | ||||
-rw-r--r-- | test/old/testdir/vim9.vim | 43 |
11 files changed, 713 insertions, 6 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index d85735a008..416a10460a 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -4480,6 +4480,47 @@ matcharg({nr}) *matcharg()* Highlighting matches using the |:match| commands are limited to three matches. |matchadd()| does not have this limitation. +matchbufline({buf}, {pat}, {lnum}, {end}, [, {dict}]) *matchbufline()* + 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: >vim + " 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: >vim + " 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. + matchdelete({id} [, {win}]) *matchdelete()* *E802* *E803* Deletes a match with ID {id} previously defined by |matchadd()| or one of the |:match| commands. Returns 0 if successful, @@ -4617,6 +4658,36 @@ matchstr({expr}, {pat} [, {start} [, {count}]]) *matchstr()* When {expr} is a |List| then the matching item is returned. The type isn't changed, it's not necessarily a String. +matchstrlist({list}, {pat} [, {dict}]) *matchstrlist()* + 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: >vim + :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: >vim + :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. + matchstrpos({expr}, {pat} [, {start} [, {count}]]) *matchstrpos()* Same as |matchstr()|, but return the matched string, the start position and the end position of the match. Example: >vim @@ -4643,7 +4714,7 @@ max({expr}) *max()* it returns the maximum of all values in the Dictionary. If {expr} is neither a List nor a Dictionary, or one of the items in {expr} cannot be used as a Number this results in - an error. An empty |List| or |Dictionary| results in zero. + an error. An empty |List| or |Dictionary| results in zero. menu_get({path} [, {modes}]) *menu_get()* Returns a |List| of |Dictionaries| describing |menus| (defined diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index e206a804f4..acb957d0c1 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -610,10 +610,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/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index fd9b68a6f3..01b4ef920b 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -5412,6 +5412,54 @@ function vim.fn.matchaddpos(group, pos, priority, id, dict) end --- @return any function vim.fn.matcharg(nr) end +--- 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: >vim +--- " 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: >vim +--- " 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. +--- +--- @param buf string|integer +--- @param pat string +--- @param lnum string|integer +--- @param end_ string|integer +--- @param dict? table +--- @return any +function vim.fn.matchbufline(buf, pat, lnum, end_, dict) end + --- Deletes a match with ID {id} previously defined by |matchadd()| --- or one of the |:match| commands. Returns 0 if successful, --- otherwise -1. See example for |matchadd()|. All matches can @@ -5581,6 +5629,41 @@ function vim.fn.matchlist(expr, pat, start, count) end --- @return any function vim.fn.matchstr(expr, pat, start, count) end +--- 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: >vim +--- :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: >vim +--- :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. +--- +--- @param list string[] +--- @param pat string +--- @param dict? table +--- @return any +function vim.fn.matchstrlist(list, pat, dict) end + --- Same as |matchstr()|, but return the matched string, the start --- position and the end position of the match. Example: >vim --- echo matchstrpos("testing", "ing") @@ -5612,7 +5695,7 @@ function vim.fn.matchstrpos(expr, pat, start, count) end --- it returns the maximum of all values in the Dictionary. --- If {expr} is neither a List nor a Dictionary, or one of the --- items in {expr} cannot be used as a Number this results in ---- an error. An empty |List| or |Dictionary| results in zero. +--- an error. An empty |List| or |Dictionary| results in zero. --- --- @param expr any --- @return any diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 6bd6087a4e..9545cb9823 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -6615,6 +6615,60 @@ M.funcs = { params = { { 'nr', 'integer' } }, signature = 'matcharg({nr})', }, + matchbufline = { + args = { 4, 5 }, + base = 1, + desc = [=[ + 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: >vim + " 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: >vim + " 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. + ]=], + name = 'matchbufline', + params = { + { 'buf', 'string|integer' }, + { 'pat', 'string' }, + { 'lnum', 'string|integer' }, + { 'end', 'string|integer' }, + { 'dict', 'table' }, + }, + signature = 'matchbufline({buf}, {pat}, {lnum}, {end}, [, {dict}])', + }, matchdelete = { args = { 1, 2 }, base = 1, @@ -6799,6 +6853,43 @@ M.funcs = { params = { { 'expr', 'any' }, { 'pat', 'any' }, { 'start', 'any' }, { 'count', 'any' } }, signature = 'matchstr({expr}, {pat} [, {start} [, {count}]])', }, + matchstrlist = { + args = { 2, 3 }, + base = 1, + desc = [=[ + 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: >vim + :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: >vim + :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. + ]=], + name = 'matchstrlist', + params = { { 'list', 'string[]' }, { 'pat', 'string' }, { 'dict', 'table' } }, + signature = 'matchstrlist({list}, {pat} [, {dict}])', + }, matchstrpos = { args = { 2, 4 }, base = 1, @@ -6836,7 +6927,7 @@ M.funcs = { it returns the maximum of all values in the Dictionary. If {expr} is neither a List nor a Dictionary, or one of the items in {expr} cannot be used as a Number this results in - an error. An empty |List| or |Dictionary| results in zero. + an error. An empty |List| or |Dictionary| results in zero. ]=], name = 'max', diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 4d60cd3660..26e4e24124 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -4731,6 +4731,151 @@ 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 void get_matches_in_str(const char *str, regmatch_T *rmp, list_T *mlist, int idx, + bool submatches, bool matchbuf) +{ + size_t len = strlen(str); + int match = 0; + colnr_T startidx = 0; + + while (true) { + match = vim_regexec_nl(rmp, str, startidx); + if (!match) { + break; + } + + dict_T *d = tv_dict_alloc(); + tv_list_append_dict(mlist, d); + + if (matchbuf) { + tv_dict_add_nr(d, S_LEN("lnum"), idx); + } else { + tv_dict_add_nr(d, S_LEN("idx"), idx); + } + + tv_dict_add_nr(d, S_LEN("byteidx"), + (colnr_T)(rmp->startp[0] - str)); + + tv_dict_add_str_len(d, S_LEN("text"), rmp->startp[0], + (int)(rmp->endp[0] - rmp->startp[0])); + + if (submatches) { + list_T *sml = tv_list_alloc(NSUBEXP - 1); + + tv_dict_add_list(d, S_LEN("submatches"), sml); + + // return a list with the submatches + for (int i = 1; i < NSUBEXP; i++) { + if (rmp->endp[i] == NULL) { + tv_list_append_string(sml, "", 0); + } else { + tv_list_append_string(sml, rmp->startp[i], rmp->endp[i] - rmp->startp[i]); + } + } + } + startidx = (colnr_T)(rmp->endp[0] - str); + if (startidx >= (colnr_T)len || str + startidx <= rmp->startp[0]) { + break; + } + } +} + +/// "matchbufline()" function +static void f_matchbufline(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + rettv->vval.v_number = -1; + tv_list_alloc_ret(rettv, kListLenUnknown); + list_T *retlist = rettv->vval.v_list; + + if (tv_check_for_buffer_arg(argvars, 0) == FAIL + || tv_check_for_string_arg(argvars, 1) == FAIL + || tv_check_for_lnum_arg(argvars, 2) == FAIL + || tv_check_for_lnum_arg(argvars, 3) == FAIL + || tv_check_for_opt_dict_arg(argvars, 4) == FAIL) { + return; + } + + const 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 patbuf[NUMBUFLEN]; + const char *pat = tv_get_string_buf(&argvars[1], patbuf); + + const 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_invargval), "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_invargval), "end_lnum"); + return; + } + + if (elnum > buf->b_ml.ml_line_count) { + elnum = buf->b_ml.ml_line_count; + } + + bool submatches = false; + if (argvars[4].v_type != VAR_UNKNOWN) { + dict_T *d = argvars[4].vval.v_dict; + if (d != NULL) { + dictitem_T *di = tv_dict_find(d, S_LEN("submatches")); + if (di != NULL) { + if (di->di_tv.v_type != VAR_BOOL) { + semsg(_(e_invargval), "submatches"); + return; + } + submatches = tv_get_bool(&di->di_tv); + } + } + } + + // Make 'cpoptions' empty, the 'l' flag should not be used here. + char *const save_cpo = p_cpo; + p_cpo = empty_string_option; + + regmatch_T regmatch; + regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING); + if (regmatch.regprog == NULL) { + goto theend; + } + regmatch.rm_ic = p_ic; + + while (slnum <= elnum) { + const char *str = ml_get_buf(buf, slnum); + get_matches_in_str(str, ®match, retlist, slnum, submatches, true); + slnum++; + } + + vim_regfree(regmatch.regprog); + +theend: + p_cpo = save_cpo; +} + /// "match()" function static void f_match(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { @@ -4755,6 +4900,73 @@ static void f_matchstr(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) find_some_match(argvars, rettv, kSomeMatchStr); } +/// "matchstrlist()" function +static void f_matchstrlist(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + rettv->vval.v_number = -1; + tv_list_alloc_ret(rettv, kListLenUnknown); + list_T *retlist = rettv->vval.v_list; + + if (tv_check_for_list_arg(argvars, 0) == FAIL + || tv_check_for_string_arg(argvars, 1) == FAIL + || tv_check_for_opt_dict_arg(argvars, 2) == FAIL) { + return; + } + + list_T *l = NULL; + if ((l = argvars[0].vval.v_list) == NULL) { + return; + } + + char patbuf[NUMBUFLEN]; + const char *pat = tv_get_string_buf_chk(&argvars[1], patbuf); + if (pat == NULL) { + return; + } + + // Make 'cpoptions' empty, the 'l' flag should not be used here. + char *const save_cpo = p_cpo; + p_cpo = empty_string_option; + + regmatch_T regmatch; + regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING); + if (regmatch.regprog == NULL) { + goto theend; + } + regmatch.rm_ic = p_ic; + + bool submatches = false; + if (argvars[2].v_type != VAR_UNKNOWN) { + dict_T *d = argvars[2].vval.v_dict; + if (d != NULL) { + dictitem_T *di = tv_dict_find(d, S_LEN("submatches")); + if (di != NULL) { + if (di->di_tv.v_type != VAR_BOOL) { + semsg(_(e_invargval), "submatches"); + goto cleanup; + } + submatches = tv_get_bool(&di->di_tv); + } + } + } + + int idx = 0; + TV_LIST_ITER_CONST(l, li, { + const typval_T *const li_tv = TV_LIST_ITEM_TV(li); + if (li_tv->v_type == VAR_STRING && li_tv->vval.v_string != NULL) { + const char *str = li_tv->vval.v_string; + get_matches_in_str(str, ®match, retlist, idx, submatches, false); + } + idx++; + }); + +cleanup: + vim_regfree(regmatch.regprog); + +theend: + p_cpo = save_cpo; +} + /// "matchstrpos()" function static void f_matchstrpos(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) { diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 2892cb60f9..d510cf2a3d 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -4346,6 +4346,22 @@ int tv_check_for_string_or_number_arg(const typval_T *const args, const int idx) return OK; } +/// Give an error and return FAIL unless "args[idx]" is a buffer number. +/// Buffer number can be a number or a string. +int tv_check_for_buffer_arg(const typval_T *const args, const int idx) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + return tv_check_for_string_or_number_arg(args, idx); +} + +/// Give an error and return FAIL unless "args[idx]" is a line number. +/// Line number can be a number or a string. +int tv_check_for_lnum_arg(const typval_T *const args, const int idx) + FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE +{ + return tv_check_for_string_or_number_arg(args, idx); +} + /// Give an error and return FAIL unless "args[idx]" is a string or a list. int tv_check_for_string_or_list_arg(const typval_T *const args, const int idx) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT FUNC_ATTR_PURE diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 0ace7f8e0e..f0cc9dba74 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -804,7 +804,9 @@ EXTERN const char e_argreq[] INIT(= N_("E471: Argument required")); EXTERN const char e_backslash[] INIT(= N_("E10: \\ should be followed by /, ? or &")); EXTERN const char e_cmdwin[] INIT(= N_("E11: Invalid in command-line window; <CR> executes, CTRL-C quits")); EXTERN const char e_curdir[] INIT(= N_("E12: Command not allowed in secure mode in current dir or tag search")); +EXTERN const char e_invalid_buffer_name_str[] INIT(= N_("E158: Invalid buffer name: %s")); EXTERN const char e_command_too_recursive[] INIT(= N_("E169: Command too recursive")); +EXTERN const char e_buffer_is_not_loaded[] INIT(= N_("E681: Buffer is not loaded")); EXTERN const char e_endif[] INIT(= N_("E171: Missing :endif")); EXTERN const char e_endtry[] INIT(= N_("E600: Missing :endtry")); EXTERN const char e_endwhile[] INIT(= N_("E170: Missing :endwhile")); diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index 3ae3807937..801c00933a 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -6916,7 +6916,7 @@ static int cbuffer_process_args(exarg_T *eap, buf_T **bufp, linenr_T *line1, lin } if (buf->b_ml.ml_mfp == NULL) { - emsg(_("E681: Buffer is not loaded")); + emsg(_(e_buffer_is_not_loaded)); return FAIL; } diff --git a/src/nvim/sign.c b/src/nvim/sign.c index e8d4f4b163..0cf35c4921 100644 --- a/src/nvim/sign.c +++ b/src/nvim/sign.c @@ -807,7 +807,7 @@ static int parse_sign_cmd_args(int cmd, char *arg, char **name, int *id, char ** } if (filename != NULL && *buf == NULL) { - semsg(_("E158: Invalid buffer name: %s"), filename); + semsg(_(e_invalid_buffer_name_str), filename); return FAIL; } diff --git a/test/old/testdir/test_functions.vim b/test/old/testdir/test_functions.vim index d277ff72df..938928cd0b 100644 --- a/test/old/testdir/test_functions.vim +++ b/test/old/testdir/test_functions.vim @@ -1044,6 +1044,192 @@ func Test_matchstrpos() call assert_equal(['', -1, -1], matchstrpos(v:_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(v:_null_list, 'abc')) + call assert_equal([], matchstrlist([v:_null_string], 'abc')) + call assert_equal([{'idx': 0, 'byteidx': 0, 'text': ''}], + \ matchstrlist(['abc'], v:_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\+', v:_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 CheckLegacyAndVim9Success(lines) + + let lines =<< trim END + vim9script + # non string items + matchstrlist([0z10, {'a': 'x'}], 'x') + END + call CheckSourceSuccess(lines) + + let lines =<< trim END + vim9script + def Foo() + # non string items + assert_equal([], matchstrlist([0z10, {'a': 'x'}], 'x')) + enddef + Foo() + END + call 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, "\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, v:_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\+', '$', '$', v:_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 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 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 CheckSourceFailure(lines, 'E1030: Using a String as a Number: "abc"', 1) +endfunc + func Test_nextnonblank_prevnonblank() new insert @@ -2104,7 +2290,7 @@ func Test_trim() call assert_equal(" \tabcd\t xxxx tail", trim(" \tabcd\t xxxx tail", "abx")) call assert_equal("RESERVE", trim("ä½ RESERVE好", "ä½ å¥½")) call assert_equal("您R E SER V Eæ—©", trim("ä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ ", "ä½ å¥½")) - call assert_equal("ä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ ", trim(" \n\r\r ä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ \t \x0B")) + call assert_equal("ä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ ", trim(" \n\r\r ä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ \t \x0B", )) call assert_equal("您R E SER V Eæ—©å¥½ä½ ä½ \t \x0B", trim(" ä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ \t \x0B", " ä½ å¥½")) call assert_equal("您R E SER V Eæ—©å¥½ä½ ä½ \t \x0B", trim(" tteessttttä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ \t \x0B ttestt", " ä½ å¥½tes")) call assert_equal("您R E SER V Eæ—©å¥½ä½ ä½ \t \x0B", trim(" tteessttttä½ å¥½æ‚¨R E SER V Eæ—©å¥½ä½ ä½ \t \x0B ttestt", " ä½ ä½ ä½ å¥½å¥½å¥½tttsses")) diff --git a/test/old/testdir/vim9.vim b/test/old/testdir/vim9.vim index 825f01c9d6..f8a90c1e97 100644 --- a/test/old/testdir/vim9.vim +++ b/test/old/testdir/vim9.vim @@ -42,6 +42,49 @@ func CheckScriptSuccess(lines) endtry endfunc +" :source a list of "lines" and check whether it fails with "error" +func CheckSourceFailure(lines, error, lnum = -3) + if get(a:lines, 0, '') ==# 'vim9script' + return + endif + new + call setline(1, a:lines) + try + call assert_fails('source', a:error, a:lines, a:lnum) + finally + bw! + endtry +endfunc + +" :source a list of "lines" and check whether it fails with the list of +" "errors" +func CheckSourceFailureList(lines, errors, lnum = -3) + if get(a:lines, 0, '') ==# 'vim9script' + return + endif + new + call setline(1, a:lines) + try + call assert_fails('source', a:errors, a:lines, a:lnum) + finally + bw! + endtry +endfunc + +" :source a list of "lines" and check whether it succeeds +func CheckSourceSuccess(lines) + if get(a:lines, 0, '') ==# 'vim9script' + return + endif + new + call setline(1, a:lines) + try + :source + finally + bw! + endtry +endfunc + func CheckDefAndScriptSuccess(lines) return endfunc |