diff options
-rw-r--r-- | runtime/doc/builtin.txt | 35 | ||||
-rw-r--r-- | runtime/doc/usr_41.txt | 2 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/vimfn.lua | 39 | ||||
-rw-r--r-- | src/nvim/eval.c | 79 | ||||
-rw-r--r-- | src/nvim/eval.lua | 42 | ||||
-rw-r--r-- | test/old/testdir/test_filter_map.vim | 142 | ||||
-rw-r--r-- | test/old/testdir/vim9.vim | 4 |
7 files changed, 317 insertions, 26 deletions
diff --git a/runtime/doc/builtin.txt b/runtime/doc/builtin.txt index 416a10460a..cf82c478b6 100644 --- a/runtime/doc/builtin.txt +++ b/runtime/doc/builtin.txt @@ -1866,6 +1866,41 @@ foldtextresult({lnum}) *foldtextresult()* line, "'m" mark m, etc. Useful when exporting folded text, e.g., to HTML. +foreach({expr1}, {expr2}) *foreach()* + {expr1} must be a |List|, |String|, |Blob| or |Dictionary|. + For each item in {expr1} execute {expr2}. {expr1} is not + modified; its values may be, as with |:lockvar| 1. |E741| + See |map()| and |filter()| to modify {expr1}. + + {expr2} must be a |string| or |Funcref|. + + If {expr2} is a |string|, inside {expr2} |v:val| has the value + of the current item. For a |Dictionary| |v:key| has the key + of the current item and for a |List| |v:key| has the index of + the current item. For a |Blob| |v:key| has the index of the + current byte. For a |String| |v:key| has the index of the + current character. + Examples: > + call foreach(mylist, 'let used[v:val] = v:true') +< This records the items that are in the {expr1} list. + + Note that {expr2} is the result of expression and is then used + as a command. Often it is good to use a |literal-string| to + avoid having to double backslashes. + + If {expr2} is a |Funcref| it must take two arguments: + 1. the key or the index of the current item. + 2. the value of the current item. + With a lambda you don't get an error if it only accepts one + argument. + If the function returns a value, it is ignored. + + Returns {expr1} in all cases. + When an error is encountered while executing {expr2} no + further items in {expr1} are processed. + When {expr2} is a Funcref errors inside a function are ignored, + unless it was defined with the "abort" flag. + fullcommand({name}) *fullcommand()* Get the full command name from a short abbreviated command diff --git a/runtime/doc/usr_41.txt b/runtime/doc/usr_41.txt index acb957d0c1..2dae9333b6 100644 --- a/runtime/doc/usr_41.txt +++ b/runtime/doc/usr_41.txt @@ -665,6 +665,7 @@ List manipulation: *list-functions* filter() remove selected items from a List map() change each List item mapnew() make a new List with changed items + foreach() apply function to List items reduce() reduce a List to a value slice() take a slice of a List sort() sort a List @@ -696,6 +697,7 @@ Dictionary manipulation: *dict-functions* filter() remove selected entries from a Dictionary map() change each Dictionary entry mapnew() make a new Dictionary with changed items + foreach() apply function to Dictionary items keys() get List of Dictionary keys values() get List of Dictionary values items() get List of Dictionary key-value pairs diff --git a/runtime/lua/vim/_meta/vimfn.lua b/runtime/lua/vim/_meta/vimfn.lua index 01b4ef920b..59497f96e9 100644 --- a/runtime/lua/vim/_meta/vimfn.lua +++ b/runtime/lua/vim/_meta/vimfn.lua @@ -2308,6 +2308,45 @@ function vim.fn.foldtext() end --- @return string function vim.fn.foldtextresult(lnum) end +--- {expr1} must be a |List|, |String|, |Blob| or |Dictionary|. +--- For each item in {expr1} execute {expr2}. {expr1} is not +--- modified; its values may be, as with |:lockvar| 1. |E741| +--- See |map()| and |filter()| to modify {expr1}. +--- +--- {expr2} must be a |string| or |Funcref|. +--- +--- If {expr2} is a |string|, inside {expr2} |v:val| has the value +--- of the current item. For a |Dictionary| |v:key| has the key +--- of the current item and for a |List| |v:key| has the index of +--- the current item. For a |Blob| |v:key| has the index of the +--- current byte. For a |String| |v:key| has the index of the +--- current character. +--- Examples: > +--- call foreach(mylist, 'let used[v:val] = v:true') +--- <This records the items that are in the {expr1} list. +--- +--- Note that {expr2} is the result of expression and is then used +--- as a command. Often it is good to use a |literal-string| to +--- avoid having to double backslashes. +--- +--- If {expr2} is a |Funcref| it must take two arguments: +--- 1. the key or the index of the current item. +--- 2. the value of the current item. +--- With a lambda you don't get an error if it only accepts one +--- argument. +--- If the function returns a value, it is ignored. +--- +--- Returns {expr1} in all cases. +--- When an error is encountered while executing {expr2} no +--- further items in {expr1} are processed. +--- When {expr2} is a Funcref errors inside a function are ignored, +--- unless it was defined with the "abort" flag. +--- +--- @param expr1 any +--- @param expr2 any +--- @return any +function vim.fn.foreach(expr1, expr2) end + --- Get the full command name from a short abbreviated command --- name; see |20.2| for details on command abbreviations. --- diff --git a/src/nvim/eval.c b/src/nvim/eval.c index ad2ff89f7e..faa652f80a 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -308,11 +308,12 @@ static partial_T *vvlua_partial; /// v: hashtab #define vimvarht vimvardict.dv_hashtab -/// Enum used by filter(), map() and mapnew() +/// Enum used by filter(), map(), mapnew() and foreach() typedef enum { FILTERMAP_FILTER, FILTERMAP_MAP, FILTERMAP_MAPNEW, + FILTERMAP_FOREACH, } filtermap_T; #ifdef INCLUDE_GENERATED_DECLARATIONS @@ -5098,7 +5099,8 @@ void assert_error(garray_T *gap) tv_list_append_string(vimvars[VV_ERRORS].vv_list, gap->ga_data, (ptrdiff_t)gap->ga_len); } -/// Implementation of map() and filter() for a Dict. +/// Implementation of map(), filter(), foreach() for a Dict. Apply "expr" to +/// every item in Dict "d" and return the result in "rettv". static void filter_map_dict(dict_T *d, filtermap_T filtermap, const char *func_name, const char *arg_errmsg, typval_T *expr, typval_T *rettv) { @@ -5166,7 +5168,7 @@ static void filter_map_dict(dict_T *d, filtermap_T filtermap, const char *func_n d->dv_lock = prev_lock; } -/// Implementation of map() and filter() for a Blob. +/// Implementation of map(), filter(), foreach() for a Blob. static void filter_map_blob(blob_T *blob_arg, filtermap_T filtermap, typval_T *expr, const char *arg_errmsg, typval_T *rettv) { @@ -5209,20 +5211,22 @@ static void filter_map_blob(blob_T *blob_arg, filtermap_T filtermap, typval_T *e || did_emsg) { break; } - if (newtv.v_type != VAR_NUMBER && newtv.v_type != VAR_BOOL) { - tv_clear(&newtv); - emsg(_(e_invalblob)); - break; - } - if (filtermap != FILTERMAP_FILTER) { - if (newtv.vval.v_number != val) { - tv_blob_set(b_ret, i, (uint8_t)newtv.vval.v_number); + if (filtermap != FILTERMAP_FOREACH) { + if (newtv.v_type != VAR_NUMBER && newtv.v_type != VAR_BOOL) { + tv_clear(&newtv); + emsg(_(e_invalblob)); + break; + } + if (filtermap != FILTERMAP_FILTER) { + if (newtv.vval.v_number != val) { + tv_blob_set(b_ret, i, (uint8_t)newtv.vval.v_number); + } + } else if (rem) { + char *const p = (char *)blob_arg->bv_ga.ga_data; + memmove(p + i, p + i + 1, (size_t)(b->bv_ga.ga_len - i - 1)); + b->bv_ga.ga_len--; + i--; } - } else if (rem) { - char *const p = (char *)blob_arg->bv_ga.ga_data; - memmove(p + i, p + i + 1, (size_t)(b->bv_ga.ga_len - i - 1)); - b->bv_ga.ga_len--; - i--; } idx++; } @@ -5230,7 +5234,7 @@ static void filter_map_blob(blob_T *blob_arg, filtermap_T filtermap, typval_T *e b->bv_lock = prev_lock; } -/// Implementation of map() and filter() for a String. +/// Implementation of map(), filter(), foreach() for a String. static void filter_map_string(const char *str, filtermap_T filtermap, typval_T *expr, typval_T *rettv) { @@ -5259,7 +5263,8 @@ static void filter_map_string(const char *str, filtermap_T filtermap, typval_T * tv_clear(&newtv); tv_clear(&tv); break; - } else if (filtermap != FILTERMAP_FILTER) { + } + if (filtermap == FILTERMAP_MAP || filtermap == FILTERMAP_MAPNEW) { if (newtv.v_type != VAR_STRING) { tv_clear(&newtv); tv_clear(&tv); @@ -5268,7 +5273,7 @@ static void filter_map_string(const char *str, filtermap_T filtermap, typval_T * } else { ga_concat(&ga, newtv.vval.v_string); } - } else if (!rem) { + } else if (filtermap == FILTERMAP_FOREACH || !rem) { ga_concat(&ga, tv.vval.v_string); } @@ -5281,7 +5286,8 @@ static void filter_map_string(const char *str, filtermap_T filtermap, typval_T * rettv->vval.v_string = ga.ga_data; } -/// Implementation of map() and filter() for a List. +/// Implementation of map(), filter(), foreach() for a List. Apply "expr" to +/// every item in List "l" and return the result in "rettv". static void filter_map_list(list_T *l, filtermap_T filtermap, const char *func_name, const char *arg_errmsg, typval_T *expr, typval_T *rettv) { @@ -5345,21 +5351,25 @@ static void filter_map_list(list_T *l, filtermap_T filtermap, const char *func_n tv_list_set_lock(l, prev_lock); } -/// Implementation of map() and filter(). +/// Implementation of map(), filter() and foreach(). static void filter_map(typval_T *argvars, typval_T *rettv, filtermap_T filtermap) { const char *const func_name = (filtermap == FILTERMAP_MAP ? "map()" : (filtermap == FILTERMAP_MAPNEW ? "mapnew()" - : "filter()")); + : (filtermap == FILTERMAP_FILTER + ? "filter()" + : "foreach()"))); const char *const arg_errmsg = (filtermap == FILTERMAP_MAP ? N_("map() argument") : (filtermap == FILTERMAP_MAPNEW ? N_("mapnew() argument") - : N_("filter() argument"))); + : (filtermap == FILTERMAP_FILTER + ? N_("filter() argument") + : N_("foreach() argument")))); - // map() and filter() return the first argument, also on failure. + // map(), filter(), foreach() return the first argument, also on failure. if (filtermap != FILTERMAP_MAPNEW && argvars[0].v_type != VAR_STRING) { tv_copy(&argvars[0], rettv); } @@ -5407,7 +5417,7 @@ static void filter_map(typval_T *argvars, typval_T *rettv, filtermap_T filtermap } } -/// Handle one item for map() and filter(). +/// Handle one item for map(), filter(), foreach(). /// Sets v:val to "tv". Caller must set v:key. /// /// @param tv original value @@ -5422,6 +5432,17 @@ static int filter_map_one(typval_T *tv, typval_T *expr, const filtermap_T filter int retval = FAIL; tv_copy(tv, &vimvars[VV_VAL].vv_tv); + + newtv->v_type = VAR_UNKNOWN; + if (filtermap == FILTERMAP_FOREACH && expr->v_type == VAR_STRING) { + // foreach() is not limited to an expression + do_cmdline_cmd(expr->vval.v_string); + if (!did_emsg) { + retval = OK; + } + goto theend; + } + argv[0] = vimvars[VV_KEY].vv_tv; argv[1] = vimvars[VV_VAL].vv_tv; if (eval_expr_typval(expr, false, argv, 2, newtv) == FAIL) { @@ -5438,6 +5459,8 @@ static int filter_map_one(typval_T *tv, typval_T *expr, const filtermap_T filter if (error) { goto theend; } + } else if (filtermap == FILTERMAP_FOREACH) { + tv_clear(newtv); } retval = OK; theend: @@ -5463,6 +5486,12 @@ void f_mapnew(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) filter_map(argvars, rettv, FILTERMAP_MAPNEW); } +/// "foreach()" function +void f_foreach(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) +{ + filter_map(argvars, rettv, FILTERMAP_FOREACH); +} + /// "function()" function /// "funcref()" function void common_function(typval_T *argvars, typval_T *rettv, bool is_funcref) diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua index 9545cb9823..9a66d20dbb 100644 --- a/src/nvim/eval.lua +++ b/src/nvim/eval.lua @@ -2923,6 +2923,48 @@ M.funcs = { returns = 'string', signature = 'foldtextresult({lnum})', }, + foreach = { + args = 2, + base = 1, + desc = [=[ + {expr1} must be a |List|, |String|, |Blob| or |Dictionary|. + For each item in {expr1} execute {expr2}. {expr1} is not + modified; its values may be, as with |:lockvar| 1. |E741| + See |map()| and |filter()| to modify {expr1}. + + {expr2} must be a |string| or |Funcref|. + + If {expr2} is a |string|, inside {expr2} |v:val| has the value + of the current item. For a |Dictionary| |v:key| has the key + of the current item and for a |List| |v:key| has the index of + the current item. For a |Blob| |v:key| has the index of the + current byte. For a |String| |v:key| has the index of the + current character. + Examples: > + call foreach(mylist, 'let used[v:val] = v:true') + <This records the items that are in the {expr1} list. + + Note that {expr2} is the result of expression and is then used + as a command. Often it is good to use a |literal-string| to + avoid having to double backslashes. + + If {expr2} is a |Funcref| it must take two arguments: + 1. the key or the index of the current item. + 2. the value of the current item. + With a lambda you don't get an error if it only accepts one + argument. + If the function returns a value, it is ignored. + + Returns {expr1} in all cases. + When an error is encountered while executing {expr2} no + further items in {expr1} are processed. + When {expr2} is a Funcref errors inside a function are ignored, + unless it was defined with the "abort" flag. + ]=], + name = 'foreach', + params = { { 'expr1', 'any' }, { 'expr2', 'any' } }, + signature = 'foreach({expr1}, {expr2})', + }, foreground = { args = 0, params = {}, diff --git a/test/old/testdir/test_filter_map.vim b/test/old/testdir/test_filter_map.vim index fb435f4291..5ef56c5623 100644 --- a/test/old/testdir/test_filter_map.vim +++ b/test/old/testdir/test_filter_map.vim @@ -14,6 +14,18 @@ func Test_filter_map_list_expr_string() call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], 'v:key * 2')) call assert_equal([9, 9, 9, 9], map([1, 2, 3, 4], 9)) call assert_equal([7, 7, 7], map([1, 2, 3], ' 7 ')) + + " foreach() + let list01 = [1, 2, 3, 4] + let list02 = [] + call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(list02, v:val * 2)')) + call assert_equal([2, 4, 6, 8], list02) + let list02 = [] + call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(list02, v:key * 2)')) + call assert_equal([0, 2, 4, 6], list02) + let list02 = [] + call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(list02, 9)')) + call assert_equal([9, 9, 9, 9], list02) endfunc " dict with expression string @@ -29,6 +41,14 @@ func Test_filter_map_dict_expr_string() call assert_equal({"foo": 2, "bar": 4, "baz": 6}, map(copy(dict), 'v:val * 2')) call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), 'v:key[0]')) call assert_equal({"foo": 9, "bar": 9, "baz": 9}, map(copy(dict), 9)) + + " foreach() + let dict01 = {} + call assert_equal(dict, foreach(copy(dict), 'let dict01[v:key] = v:val * 2')) + call assert_equal({"foo": 2, "bar": 4, "baz": 6}, dict01) + let dict01 = {} + call assert_equal(dict, foreach(copy(dict), 'let dict01[v:key] = v:key[0]')) + call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, dict01) endfunc " list with funcref @@ -54,6 +74,16 @@ func Test_filter_map_list_expr_funcref() return a:index * 2 endfunc call assert_equal([0, 2, 4, 6], map([1, 2, 3, 4], function('s:filter4'))) + + " foreach() + func! s:foreach1(index, val) abort + call add(g:test_variable, a:val + 1) + return [ 11, 12, 13, 14 ] + endfunc + let g:test_variable = [] + call assert_equal([0, 1, 2, 3, 4], foreach(range(5), function('s:foreach1'))) + call assert_equal([1, 2, 3, 4, 5], g:test_variable) + call remove(g:, 'test_variable') endfunc func Test_filter_map_nested() @@ -90,11 +120,46 @@ func Test_filter_map_dict_expr_funcref() return a:key[0] endfunc call assert_equal({"foo": "f", "bar": "b", "baz": "b"}, map(copy(dict), function('s:filter4'))) + + " foreach() + func! s:foreach1(key, val) abort + call extend(g:test_variable, {a:key: a:val * 2}) + return [ 11, 12, 13, 14 ] + endfunc + let g:test_variable = {} + call assert_equal(dict, foreach(copy(dict), function('s:foreach1'))) + call assert_equal({"foo": 2, "bar": 4, "baz": 6}, g:test_variable) + call remove(g:, 'test_variable') +endfunc + +func Test_map_filter_locked() + let list01 = [1, 2, 3, 4] + lockvar 1 list01 + call assert_fails('call filter(list01, "v:val > 1")', 'E741:') + call assert_equal([2, 4, 6, 8], map(list01, 'v:val * 2')) + call assert_equal([1, 2, 3, 4], map(list01, 'v:val / 2')) + call assert_equal([2, 4, 6, 8], mapnew(list01, 'v:val * 2')) + let g:test_variable = [] + call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(g:test_variable, v:val * 2)')) + call remove(g:, 'test_variable') + call assert_fails('call filter(list01, "v:val > 1")', 'E741:') + unlockvar 1 list01 + lockvar! list01 + call assert_fails('call filter(list01, "v:val > 1")', 'E741:') + call assert_fails('call map(list01, "v:val * 2")', 'E741:') + call assert_equal([2, 4, 6, 8], mapnew(list01, 'v:val * 2')) + let g:test_variable = [] + call assert_equal([1, 2, 3, 4], foreach(list01, 'call add(g:test_variable, v:val * 2)')) + call assert_fails('call foreach(list01, "let list01[0] = -1")', 'E741:') + call assert_fails('call filter(list01, "v:val > 1")', 'E741:') + call remove(g:, 'test_variable') + unlockvar! list01 endfunc func Test_map_filter_fails() call assert_fails('call map([1], "42 +")', 'E15:') call assert_fails('call filter([1], "42 +")', 'E15:') + call assert_fails('call foreach([1], "let a = }")', 'E15:') call assert_fails("let l = filter([1, 2, 3], '{}')", 'E728:') call assert_fails("let l = filter({'k' : 10}, '{}')", 'E728:') call assert_fails("let l = filter([1, 2], {})", 'E731:') @@ -108,6 +173,8 @@ func Test_map_filter_fails() " Nvim doesn't have null partials " call assert_equal([1, 2, 3], filter([1, 2, 3], test_null_partial())) call assert_fails("let l = filter([1, 2], {a, b, c -> 1})", 'E119:') + call assert_fails('call foreach([1], "xyzzy")', 'E492:') + call assert_fails('call foreach([1], "let a = foo")', 'E121:') endfunc func Test_map_and_modify() @@ -125,7 +192,7 @@ endfunc func Test_filter_and_modify() let l = [0] - " cannot change the list halfway a map() + " cannot change the list halfway thru filter() call assert_fails('call filter(l, "remove(l, 0)")', 'E741:') let d = #{a: 0, b: 0, c: 0} @@ -135,6 +202,18 @@ func Test_filter_and_modify() call assert_fails('call filter(b, "remove(b, 0)")', 'E741:') endfunc +func Test_foreach_and_modify() + let l = [0] + " cannot change the list halfway thru foreach() + call assert_fails('call foreach(l, "let a = remove(l, 0)")', 'E741:') + + let d = #{a: 0, b: 0, c: 0} + call assert_fails('call foreach(d, "let a = remove(d, v:key)")', 'E741:') + + let b = 0z1234 + call assert_fails('call foreach(b, "let a = remove(b, 0)")', 'E741:') +endfunc + func Test_mapnew_dict() let din = #{one: 1, two: 2} let dout = mapnew(din, {k, v -> string(v)}) @@ -162,6 +241,36 @@ func Test_mapnew_blob() call assert_equal(0z129956, bout) endfunc +func Test_foreach_blob() + let lines =<< trim END + LET g:test_variable = [] + call assert_equal(0z0001020304, foreach(0z0001020304, 'call add(g:test_variable, v:val)')) + call assert_equal([0, 1, 2, 3, 4], g:test_variable) + END + call CheckLegacyAndVim9Success(lines) + + func! s:foreach1(index, val) abort + call add(g:test_variable, a:val) + return [ 11, 12, 13, 14 ] + endfunc + let g:test_variable = [] + call assert_equal(0z0001020304, foreach(0z0001020304, function('s:foreach1'))) + call assert_equal([0, 1, 2, 3, 4], g:test_variable) + + let lines =<< trim END + def Foreach1(_, val: any): list<number> + add(g:test_variable, val) + return [ 11, 12, 13, 14 ] + enddef + g:test_variable = [] + assert_equal(0z0001020304, foreach(0z0001020304, Foreach1)) + assert_equal([0, 1, 2, 3, 4], g:test_variable) + END + call CheckDefSuccess(lines) + + call remove(g:, 'test_variable') +endfunc + " Test for using map(), filter() and mapnew() with a string func Test_filter_map_string() " filter() @@ -221,6 +330,37 @@ func Test_filter_map_string() END call CheckLegacyAndVim9Success(lines) + " foreach() + let lines =<< trim END + VAR s = "abc" + LET g:test_variable = [] + call assert_equal(s, foreach(s, 'call add(g:test_variable, v:val)')) + call assert_equal(['a', 'b', 'c'], g:test_variable) + LET g:test_variable = [] + LET s = 'あiうえお' + call assert_equal(s, foreach(s, 'call add(g:test_variable, v:val)')) + call assert_equal(['あ', 'i', 'う', 'え', 'お'], g:test_variable) + END + call CheckLegacyAndVim9Success(lines) + func! s:foreach1(index, val) abort + call add(g:test_variable, a:val) + return [ 11, 12, 13, 14 ] + endfunc + let g:test_variable = [] + call assert_equal('abcd', foreach('abcd', function('s:foreach1'))) + call assert_equal(['a', 'b', 'c', 'd'], g:test_variable) + let lines =<< trim END + def Foreach1(_, val: string): list<number> + add(g:test_variable, val) + return [ 11, 12, 13, 14 ] + enddef + g:test_variable = [] + assert_equal('abcd', foreach('abcd', Foreach1)) + assert_equal(['a', 'b', 'c', 'd'], g:test_variable) + END + call CheckDefSuccess(lines) + call remove(g:, 'test_variable') + let lines =<< trim END #" map() and filter() call assert_equal('[あ][⁈][a][😊][⁉][💕][💕][b][💕]', map(filter('あx⁈ax😊x⁉💕💕b💕x', '"x" != v:val'), '"[" .. v:val .. "]"')) diff --git a/test/old/testdir/vim9.vim b/test/old/testdir/vim9.vim index f8a90c1e97..218fab6c5e 100644 --- a/test/old/testdir/vim9.vim +++ b/test/old/testdir/vim9.vim @@ -2,6 +2,10 @@ " Use a different file name for each run. let s:sequence = 1 +func CheckDefSuccess(lines) + return +endfunc + func CheckDefFailure(lines, error, lnum = -3) return endfunc |