From e79e2077607e8f829ba823308c91104a795736ba Mon Sep 17 00:00:00 2001 From: Ernie Rael Date: Sat, 13 Jan 2024 11:47:33 +0100 Subject: patch 9.1.0027: Vim is missing a foreach() func Problem: Vim is missing a foreach() func Solution: Implement foreach({expr1}, {expr2}) function, which applies {expr2} for each item in {expr1} without changing it (Ernie Rael) closes: #12166 Signed-off-by: Ernie Rael Signed-off-by: Christian Brabandt --- src/blob.c | 37 ++++++----- src/dict.c | 5 +- src/evalfunc.c | 35 ++++++++-- src/list.c | 81 +++++++++++++++-------- src/proto/list.pro | 1 + src/strings.c | 5 +- src/structs.h | 5 +- src/testdir/test_filter_map.vim | 142 +++++++++++++++++++++++++++++++++++++++- src/version.c | 2 + 9 files changed, 255 insertions(+), 58 deletions(-) (limited to 'src') diff --git a/src/blob.c b/src/blob.c index c5d7eaed64..9cdd504b6e 100644 --- a/src/blob.c +++ b/src/blob.c @@ -641,25 +641,28 @@ blob_filter_map( if (filter_map_one(&tv, expr, filtermap, fc, &newtv, &rem) == FAIL || did_emsg) break; - if (newtv.v_type != VAR_NUMBER && newtv.v_type != VAR_BOOL) + if (filtermap != FILTERMAP_FOREACH) { - clear_tv(&newtv); - emsg(_(e_invalid_operation_for_blob)); - break; - } - if (filtermap != FILTERMAP_FILTER) - { - if (newtv.vval.v_number != val) - blob_set(b_ret, i, newtv.vval.v_number); - } - else if (rem) - { - char_u *p = (char_u *)blob_arg->bv_ga.ga_data; + if (newtv.v_type != VAR_NUMBER && newtv.v_type != VAR_BOOL) + { + clear_tv(&newtv); + emsg(_(e_invalid_operation_for_blob)); + break; + } + if (filtermap != FILTERMAP_FILTER) + { + if (newtv.vval.v_number != val) + blob_set(b_ret, i, newtv.vval.v_number); + } + else if (rem) + { + char_u *p = (char_u *)blob_arg->bv_ga.ga_data; - mch_memmove(p + i, p + i + 1, - (size_t)b->bv_ga.ga_len - i - 1); - --b->bv_ga.ga_len; - --i; + mch_memmove(p + i, p + i + 1, + (size_t)b->bv_ga.ga_len - i - 1); + --b->bv_ga.ga_len; + --i; + } } ++idx; } diff --git a/src/dict.c b/src/dict.c index cb22b42c91..508d00cdf7 100644 --- a/src/dict.c +++ b/src/dict.c @@ -1329,8 +1329,8 @@ dict_extend_func( } /* - * Implementation of map() and filter() for a Dict. Apply "expr" to every - * item in Dict "d" and return the result in "rettv". + * Implementation of map(), filter(), foreach() for a Dict. Apply "expr" to + * every item in Dict "d" and return the result in "rettv". */ void dict_filter_map( @@ -1392,7 +1392,6 @@ dict_filter_map( arg_errmsg, TRUE))) break; set_vim_var_string(VV_KEY, di->di_key, -1); - newtv.v_type = VAR_UNKNOWN; r = filter_map_one(&di->di_tv, expr, filtermap, fc, &newtv, &rem); clear_tv(get_vim_var_tv(VV_KEY)); if (r == FAIL || did_emsg) diff --git a/src/evalfunc.c b/src/evalfunc.c index e37b3a412a..9b3bdf756d 100644 --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -607,10 +607,11 @@ arg_list_or_dict_or_blob_or_string_mod( } /* - * Check second argument of map() or filter(). + * Check second argument of map(), filter(), foreach(). */ static int -check_map_filter_arg2(type_T *type, argcontext_T *context, int is_map) +check_map_filter_arg2(type_T *type, argcontext_T *context, + filtermap_T filtermap) { type_T *expected_member = NULL; type_T *(args[2]); @@ -663,12 +664,14 @@ check_map_filter_arg2(type_T *type, argcontext_T *context, int is_map) { where_T where = WHERE_INIT; - if (is_map) + if (filtermap == FILTERMAP_MAP) t_func_exp.tt_member = expected_member == NULL || type_any_or_unknown(type->tt_member) ? &t_any : expected_member; - else + else if (filtermap == FILTERMAP_FILTER) t_func_exp.tt_member = &t_bool; + else // filtermap == FILTERMAP_FOREACH + t_func_exp.tt_member = &t_unknown; if (args[0] == NULL) args[0] = &t_unknown; if (type->tt_argcount == -1) @@ -693,7 +696,7 @@ arg_filter_func(type_T *type, type_T *decl_type UNUSED, argcontext_T *context) return OK; if (type->tt_type == VAR_FUNC) - return check_map_filter_arg2(type, context, FALSE); + return check_map_filter_arg2(type, context, FILTERMAP_FILTER); semsg(_(e_string_or_function_required_for_argument_nr), 2); return FAIL; } @@ -710,7 +713,24 @@ arg_map_func(type_T *type, type_T *decl_type UNUSED, argcontext_T *context) return OK; if (type->tt_type == VAR_FUNC) - return check_map_filter_arg2(type, context, TRUE); + return check_map_filter_arg2(type, context, FILTERMAP_MAP); + semsg(_(e_string_or_function_required_for_argument_nr), 2); + return FAIL; +} + +/* + * Check second argument of foreach(), the function. + */ + static int +arg_foreach_func(type_T *type, type_T *decl_type UNUSED, argcontext_T *context) +{ + if (type->tt_type == VAR_STRING + || type->tt_type == VAR_PARTIAL + || type_any_or_unknown(type)) + return OK; + + if (type->tt_type == VAR_FUNC) + return check_map_filter_arg2(type, context, FILTERMAP_FOREACH); semsg(_(e_string_or_function_required_for_argument_nr), 2); return FAIL; } @@ -1173,6 +1193,7 @@ static argcheck_T arg1_len[] = {arg_len1}; static argcheck_T arg3_libcall[] = {arg_string, arg_string, arg_string_or_nr}; static argcheck_T arg14_maparg[] = {arg_string, arg_string, arg_bool, arg_bool}; static argcheck_T arg2_filter[] = {arg_list_or_dict_or_blob_or_string_mod, arg_filter_func}; +static argcheck_T arg2_foreach[] = {arg_list_or_dict_or_blob_or_string, arg_foreach_func}; static argcheck_T arg2_instanceof[] = {arg_object, varargs_class, NULL }; static argcheck_T arg2_map[] = {arg_list_or_dict_or_blob_or_string_mod, arg_map_func}; static argcheck_T arg2_mapnew[] = {arg_list_or_dict_or_blob_or_string, arg_any}; @@ -2013,6 +2034,8 @@ static funcentry_T global_functions[] = ret_string, f_foldtext}, {"foldtextresult", 1, 1, FEARG_1, arg1_lnum, ret_string, f_foldtextresult}, + {"foreach", 2, 2, FEARG_1, arg2_foreach, + ret_first_arg, f_foreach}, {"foreground", 0, 0, 0, NULL, ret_void, f_foreground}, {"fullcommand", 1, 2, FEARG_1, arg2_string_bool, diff --git a/src/list.c b/src/list.c index b50cb03c98..e9f1ae3206 100644 --- a/src/list.c +++ b/src/list.c @@ -2325,7 +2325,7 @@ f_uniq(typval_T *argvars, typval_T *rettv) } /* - * Handle one item for map() and filter(). + * Handle one item for map(), filter(), foreach(). * Sets v:val to "tv". Caller must set v:key. */ int @@ -2341,6 +2341,17 @@ filter_map_one( int retval = FAIL; copy_tv(tv, get_vim_var_tv(VV_VAL)); + + 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] = *get_vim_var_tv(VV_KEY); argv[1] = *get_vim_var_tv(VV_VAL); if (eval_expr_typval(expr, FALSE, argv, 2, fc, newtv) == FAIL) @@ -2360,6 +2371,8 @@ filter_map_one( if (error) goto theend; } + else if (filtermap == FILTERMAP_FOREACH) + clear_tv(newtv); retval = OK; theend: clear_tv(get_vim_var_tv(VV_VAL)); @@ -2367,8 +2380,8 @@ theend: } /* - * Implementation of map() and filter() for a List. Apply "expr" to every item - * in List "l" and return the result in "rettv". + * Implementation of map(), filter(), foreach() for a List. Apply "expr" to + * every item in List "l" and return the result in "rettv". */ static void list_filter_map( @@ -2421,7 +2434,8 @@ list_filter_map( int stride = l->lv_u.nonmat.lv_stride; // List from range(): loop over the numbers - if (filtermap != FILTERMAP_MAPNEW) + // NOTE: foreach() returns the range_list_item + if (filtermap != FILTERMAP_MAPNEW && filtermap != FILTERMAP_FOREACH) { l->lv_first = NULL; l->lv_u.mat.lv_last = NULL; @@ -2444,27 +2458,30 @@ list_filter_map( clear_tv(&newtv); break; } - if (filtermap != FILTERMAP_FILTER) + if (filtermap != FILTERMAP_FOREACH) { - if (filtermap == FILTERMAP_MAP && argtype != NULL + if (filtermap != FILTERMAP_FILTER) + { + if (filtermap == FILTERMAP_MAP && argtype != NULL && check_typval_arg_type( - argtype->tt_member, &newtv, - func_name, 0) == FAIL) + argtype->tt_member, &newtv, + func_name, 0) == FAIL) + { + clear_tv(&newtv); + break; + } + // map(), mapnew(): always append the new value to the + // list + if (list_append_tv_move(filtermap == FILTERMAP_MAP + ? l : l_ret, &newtv) == FAIL) + break; + } + else if (!rem) { - clear_tv(&newtv); - break; + // filter(): append the list item value when not rem + if (list_append_tv_move(l, &tv) == FAIL) + break; } - // map(), mapnew(): always append the new value to the - // list - if (list_append_tv_move(filtermap == FILTERMAP_MAP - ? l : l_ret, &newtv) == FAIL) - break; - } - else if (!rem) - { - // filter(): append the list item value when not rem - if (list_append_tv_move(l, &tv) == FAIL) - break; } val += stride; @@ -2508,7 +2525,7 @@ list_filter_map( break; } else if (filtermap == FILTERMAP_FILTER && rem) - listitem_remove(l, li); + listitem_remove(l, li); ++idx; } } @@ -2519,7 +2536,7 @@ list_filter_map( } /* - * Implementation of map() and filter(). + * Implementation of map(), filter() and foreach(). */ static void filter_map(typval_T *argvars, typval_T *rettv, filtermap_T filtermap) @@ -2527,16 +2544,19 @@ filter_map(typval_T *argvars, typval_T *rettv, filtermap_T filtermap) typval_T *expr; char *func_name = filtermap == FILTERMAP_MAP ? "map()" : filtermap == FILTERMAP_MAPNEW ? "mapnew()" - : "filter()"; + : filtermap == FILTERMAP_FILTER ? "filter()" + : "foreach()"; char_u *arg_errmsg = (char_u *)(filtermap == FILTERMAP_MAP ? N_("map() argument") : filtermap == FILTERMAP_MAPNEW ? N_("mapnew() argument") - : N_("filter() argument")); + : filtermap == FILTERMAP_FILTER + ? N_("filter() argument") + : N_("foreach() argument")); int save_did_emsg; type_T *type = NULL; - // 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) copy_tv(&argvars[0], rettv); @@ -2629,6 +2649,15 @@ f_mapnew(typval_T *argvars, typval_T *rettv) filter_map(argvars, rettv, FILTERMAP_MAPNEW); } +/* + * "foreach()" function + */ + void +f_foreach(typval_T *argvars, typval_T *rettv) +{ + filter_map(argvars, rettv, FILTERMAP_FOREACH); +} + /* * "add(list, item)" function */ diff --git a/src/proto/list.pro b/src/proto/list.pro index 5abe03c096..0b58c692a4 100644 --- a/src/proto/list.pro +++ b/src/proto/list.pro @@ -56,6 +56,7 @@ int filter_map_one(typval_T *tv, typval_T *expr, filtermap_T filtermap, funccall void f_filter(typval_T *argvars, typval_T *rettv); void f_map(typval_T *argvars, typval_T *rettv); void f_mapnew(typval_T *argvars, typval_T *rettv); +void f_foreach(typval_T *argvars, typval_T *rettv); void f_add(typval_T *argvars, typval_T *rettv); void f_count(typval_T *argvars, typval_T *rettv); void f_extend(typval_T *argvars, typval_T *rettv); diff --git a/src/strings.c b/src/strings.c index 0aeea4bce9..b38c3d0bd7 100644 --- a/src/strings.c +++ b/src/strings.c @@ -942,7 +942,6 @@ string_filter_map( break; len = (int)STRLEN(tv.vval.v_string); - newtv.v_type = VAR_UNKNOWN; set_vim_var_nr(VV_KEY, idx); if (filter_map_one(&tv, expr, filtermap, fc, &newtv, &rem) == FAIL || did_emsg) @@ -951,7 +950,7 @@ string_filter_map( clear_tv(&tv); break; } - else if (filtermap != FILTERMAP_FILTER) + if (filtermap == FILTERMAP_MAP || filtermap == FILTERMAP_MAPNEW) { if (newtv.v_type != VAR_STRING) { @@ -963,7 +962,7 @@ string_filter_map( else ga_concat(&ga, newtv.vval.v_string); } - else if (!rem) + else if (filtermap == FILTERMAP_FOREACH || !rem) ga_concat(&ga, tv.vval.v_string); clear_tv(&newtv); diff --git a/src/structs.h b/src/structs.h index 6d77deb454..85f440d35d 100644 --- a/src/structs.h +++ b/src/structs.h @@ -4879,11 +4879,12 @@ typedef struct { hashtab_T sve_hashtab; } save_v_event_T; -// Enum used by filter(), map() and mapnew() +// Enum used by filter(), map(), mapnew() and foreach() typedef enum { FILTERMAP_FILTER, FILTERMAP_MAP, - FILTERMAP_MAPNEW + FILTERMAP_MAPNEW, + FILTERMAP_FOREACH } filtermap_T; // Structure used by switch_win() to pass values to restore_win() diff --git a/src/testdir/test_filter_map.vim b/src/testdir/test_filter_map.vim index 6a07bec215..37ebe847b1 100644 --- a/src/testdir/test_filter_map.vim +++ b/src/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:') @@ -106,6 +171,8 @@ func Test_map_filter_fails() call assert_fails("let l = filter([1, 2], function('min'))", 'E118:') 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() @@ -123,7 +190,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} @@ -133,6 +200,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)}) @@ -160,6 +239,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 v9.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 + 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 v9.CheckDefSuccess(lines) + + call remove(g:, 'test_variable') +endfunc + " Test for using map(), filter() and mapnew() with a string func Test_filter_map_string() " filter() @@ -219,6 +328,37 @@ func Test_filter_map_string() END call v9.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 v9.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 + 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 v9.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/src/version.c b/src/version.c index 6216b2f033..a17ae156b2 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 */ +/**/ + 27, /**/ 26, /**/ -- cgit v1.2.3