# HG changeset patch # User Bram Moolenaar # Date 1591461903 -7200 # Node ID f4455c71a8aab23852ca1da299b4ebba42f192af # Parent b4f4597870455e792f92615fd93436c2d14b4636 patch 8.2.0915: search() cannot skip over matches like searchpair() can Commit: https://github.com/vim/vim/commit/adc17a5f9d207fd1623fd923457a46efc9214777 Author: Bram Moolenaar Date: Sat Jun 6 18:37:51 2020 +0200 patch 8.2.0915: search() cannot skip over matches like searchpair() can Problem: Search() cannot skip over matches like searchpair() can. Solution: Add an optional "skip" argument. (Christian Brabandt, closes https://github.com/vim/vim/issues/861) diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -2716,7 +2716,7 @@ screencol() Number current cursor colu screenpos({winid}, {lnum}, {col}) Dict screen row and col of a text character screenrow() Number current cursor row screenstring({row}, {col}) String characters at screen position -search({pattern} [, {flags} [, {stopline} [, {timeout}]]]) +search({pattern} [, {flags} [, {stopline} [, {timeout} [, {skip}]]]]) Number search for {pattern} searchcount([{options}]) Dict get or update search stats searchdecl({name} [, {global} [, {thisblock}]]) @@ -2725,7 +2725,7 @@ searchpair({start}, {middle}, {end} [, { Number search for other end of start/end pair searchpairpos({start}, {middle}, {end} [, {flags} [, {skip} [...]]]) List search for other end of start/end pair -searchpos({pattern} [, {flags} [, {stopline} [, {timeout}]]]) +searchpos({pattern} [, {flags} [, {stopline} [, {timeout} [, {skip}]]]]) List search for {pattern} server2client({clientid}, {string}) Number send reply string @@ -8364,8 +8364,9 @@ screenstring({row}, {col}) *screenst Can also be used as a |method|: > GetRow()->screenstring(col) - -search({pattern} [, {flags} [, {stopline} [, {timeout}]]]) *search()* +< + *search()* +search({pattern} [, {flags} [, {stopline} [, {timeout} [, {skip}]]]]) Search for regexp pattern {pattern}. The search starts at the cursor position (you can use |cursor()| to set it). @@ -8413,6 +8414,15 @@ search({pattern} [, {flags} [, {stopline giving the argument. {only available when compiled with the |+reltime| feature} + If the {skip} expression is given it is evaluated with the + cursor positioned on the start of a match. If it evaluates to + non-zero this match is skipped. This can be used, for + example, to skip a match in a comment or a string. + {skip} can be a string, which is evaluated as an expression, a + function reference or a lambda. + When {skip} is omitted or empty, every match is accepted. + When evaluating {skip} causes an error the search is aborted + and -1 returned. *search()-sub-match* With the 'p' flag the returned value is one more than the first sub-match in \(\). One if none of them matched but the @@ -8696,7 +8706,8 @@ searchpairpos({start}, {middle}, {end} [ < See |match-parens| for a bigger and more useful example. -searchpos({pattern} [, {flags} [, {stopline} [, {timeout}]]]) *searchpos()* + *searchpos()* +searchpos({pattern} [, {flags} [, {stopline} [, {timeout} [, {skip}]]]]) Same as |search()|, but returns a |List| with the line and column position of the match. The first element of the |List| is the line number and the second element is the byte index of diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -801,12 +801,12 @@ static funcentry_T global_functions[] = {"screenpos", 3, 3, FEARG_1, ret_dict_number, f_screenpos}, {"screenrow", 0, 0, 0, ret_number, f_screenrow}, {"screenstring", 2, 2, FEARG_1, ret_string, f_screenstring}, - {"search", 1, 4, FEARG_1, ret_number, f_search}, + {"search", 1, 5, FEARG_1, ret_number, f_search}, {"searchcount", 0, 1, FEARG_1, ret_dict_any, f_searchcount}, {"searchdecl", 1, 3, FEARG_1, ret_number, f_searchdecl}, {"searchpair", 3, 7, 0, ret_number, f_searchpair}, {"searchpairpos", 3, 7, 0, ret_list_number, f_searchpairpos}, - {"searchpos", 1, 4, FEARG_1, ret_list_number, f_searchpos}, + {"searchpos", 1, 5, FEARG_1, ret_list_number, f_searchpos}, {"server2client", 2, 2, FEARG_1, ret_number, f_server2client}, {"serverlist", 0, 0, 0, ret_string, f_serverlist}, {"setbufline", 3, 3, FEARG_3, ret_number, f_setbufline}, @@ -6399,6 +6399,10 @@ search_cmn(typval_T *argvars, pos_T *mat int options = SEARCH_KEEP; int subpatnum; searchit_arg_T sia; + evalarg_T skip; + pos_T firstpos; + + CLEAR_FIELD(skip); pat = tv_get_string(&argvars[0]); dir = get_search_arg(&argvars[1], flagsp); // may set p_ws @@ -6412,20 +6416,23 @@ search_cmn(typval_T *argvars, pos_T *mat if (flags & SP_COLUMN) options |= SEARCH_COL; - // Optional arguments: line number to stop searching and timeout. + // Optional arguments: line number to stop searching, timeout and skip. if (argvars[1].v_type != VAR_UNKNOWN && argvars[2].v_type != VAR_UNKNOWN) { lnum_stop = (long)tv_get_number_chk(&argvars[2], NULL); if (lnum_stop < 0) goto theend; -#ifdef FEAT_RELTIME if (argvars[3].v_type != VAR_UNKNOWN) { +#ifdef FEAT_RELTIME time_limit = (long)tv_get_number_chk(&argvars[3], NULL); if (time_limit < 0) goto theend; +#endif + if (argvars[4].v_type != VAR_UNKNOWN + && evalarg_get(&argvars[4], &skip) == FAIL) + goto theend; } -#endif } #ifdef FEAT_RELTIME @@ -6447,13 +6454,48 @@ search_cmn(typval_T *argvars, pos_T *mat } pos = save_cursor = curwin->w_cursor; + CLEAR_FIELD(firstpos); CLEAR_FIELD(sia); sia.sa_stop_lnum = (linenr_T)lnum_stop; #ifdef FEAT_RELTIME sia.sa_tm = &tm; #endif - subpatnum = searchit(curwin, curbuf, &pos, NULL, dir, pat, 1L, + + // Repeat until {skip} returns FALSE. + for (;;) + { + subpatnum = searchit(curwin, curbuf, &pos, NULL, dir, pat, 1L, options, RE_SEARCH, &sia); + // finding the first match again means there is no match where {skip} + // evaluates to zero. + if (firstpos.lnum != 0 && EQUAL_POS(pos, firstpos)) + subpatnum = FAIL; + + if (subpatnum == FAIL || !evalarg_valid(&skip)) + // didn't find it or no skip argument + break; + firstpos = pos; + + // If the skip pattern matches, ignore this match. + { + int do_skip; + int err; + pos_T save_pos = curwin->w_cursor; + + curwin->w_cursor = pos; + do_skip = evalarg_call_bool(&skip, &err); + curwin->w_cursor = save_pos; + if (err) + { + // Evaluating {skip} caused an error, break here. + subpatnum = FAIL; + break; + } + if (!do_skip) + break; + } + } + if (subpatnum != FAIL) { if (flags & SP_SUBPAT) @@ -6481,6 +6523,7 @@ search_cmn(typval_T *argvars, pos_T *mat curwin->w_set_curswant = TRUE; theend: p_ws = save_p_ws; + evalarg_clean(&skip); return retval; } diff --git a/src/evalvars.c b/src/evalvars.c --- a/src/evalvars.c +++ b/src/evalvars.c @@ -3811,4 +3811,79 @@ free_callback(callback_T *callback) callback->cb_name = NULL; } +/* + * Process a function argument that can be a string expression or a function + * reference. + * "tv" must remain valid until calling evalarg_clean()! + * Returns FAIL when the argument is invalid. + */ + int +evalarg_get(typval_T *tv, evalarg_T *eva) +{ + if (tv->v_type == VAR_STRING + || tv->v_type == VAR_NUMBER + || tv->v_type == VAR_BOOL + || tv->v_type == VAR_SPECIAL) + { + eva->eva_string = tv_get_string_buf(tv, eva->eva_buf); + return OK; + } + + eva->eva_callback = get_callback(tv); + return eva->eva_callback.cb_name == NULL ? FAIL : OK; +} + +/* + * Return whether "eva" has a valid expression or callback. + */ + int +evalarg_valid(evalarg_T *eva) +{ + return eva->eva_string != NULL || eva->eva_callback.cb_name != NULL; +} + +/* + * Invoke the expression or callback "eva" and return the result in "tv". + * Returns FAIL if something failed + */ + int +evalarg_call(evalarg_T *eva, typval_T *tv) +{ + typval_T argv[1]; + + if (eva->eva_string != NULL) + return eval0(eva->eva_string, tv, NULL, EVAL_EVALUATE); + + argv[0].v_type = VAR_UNKNOWN; + return call_callback(&eva->eva_callback, -1, tv, 0, argv); +} + +/* + * Like evalarg_call(), but just return TRUE of FALSE. + * Sets "error" to TRUE if evaluation failed. + */ + int +evalarg_call_bool(evalarg_T *eva, int *error) +{ + typval_T tv; + int r; + + if (evalarg_call(eva, &tv) == FAIL) + { + *error = TRUE; + return FALSE; + } + r = tv_get_number(&tv); + clear_tv(&tv); + *error = FALSE; + return r; +} + + void +evalarg_clean(evalarg_T *eva) +{ + if (eva->eva_string == NULL) + free_callback(&eva->eva_callback); +} + #endif // FEAT_EVAL diff --git a/src/proto/evalvars.pro b/src/proto/evalvars.pro --- a/src/proto/evalvars.pro +++ b/src/proto/evalvars.pro @@ -88,4 +88,9 @@ callback_T get_callback(typval_T *arg); void put_callback(callback_T *cb, typval_T *tv); void set_callback(callback_T *dest, callback_T *src); void free_callback(callback_T *callback); +int evalarg_get(typval_T *tv, evalarg_T *eva); +int evalarg_valid(evalarg_T *eva); +int evalarg_call(evalarg_T *eva, typval_T *tv); +int evalarg_call_bool(evalarg_T *eva, int *error); +void evalarg_clean(evalarg_T *eva); /* vim: set ft=c : */ diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -4130,6 +4130,21 @@ typedef struct int sa_wrapped; // search wrapped around } searchit_arg_T; +/* + * Function argument that can be a string, funcref or partial. + * - declare: evalarg_T name; + * - init: CLEAR_FIELD(name); + * - set: evalarg_get(&argvars[3], &name); + * - use: if (evalarg_valid(&name)) res = evalarg_call(&name); + * - cleanup: evalarg_clean(&name); + */ +typedef struct +{ + char_u eva_buf[NUMBUFLEN]; // buffer for get_tv_string_buf() + char_u *eva_string; + callback_T eva_callback; +} evalarg_T; + #define WRITEBUFSIZE 8192 // size of normal write buffer #define FIO_LATIN1 0x01 // convert Latin1 diff --git a/src/testdir/test_syntax.vim b/src/testdir/test_syntax.vim --- a/src/testdir/test_syntax.vim +++ b/src/testdir/test_syntax.vim @@ -772,4 +772,54 @@ func Test_syntax_foldlevel() quit! endfunc +func Test_search_syntax_skip() + new + let lines =<< trim END + + /* This is VIM */ + Another Text for VIM + let a = "VIM" + END + call setline(1, lines) + syntax on + syntax match Comment "^/\*.*\*/" + syntax match String '".*"' + + " Skip argument using string evaluation. + 1 + call search('VIM', 'w', '', 0, 'synIDattr(synID(line("."), col("."), 1), "name") =~? "comment"') + call assert_equal('Another Text for VIM', getline('.')) + 1 + call search('VIM', 'w', '', 0, 'synIDattr(synID(line("."), col("."), 1), "name") !~? "string"') + call assert_equal(' let a = "VIM"', getline('.')) + + " Skip argument using Lambda. + 1 + call search('VIM', 'w', '', 0, { -> synIDattr(synID(line("."), col("."), 1), "name") =~? "comment"}) + call assert_equal('Another Text for VIM', getline('.')) + + 1 + call search('VIM', 'w', '', 0, { -> synIDattr(synID(line("."), col("."), 1), "name") !~? "string"}) + call assert_equal(' let a = "VIM"', getline('.')) + + " Skip argument using funcref. + func InComment() + return synIDattr(synID(line("."), col("."), 1), "name") =~? "comment" + endfunc + func InString() + return synIDattr(synID(line("."), col("."), 1), "name") !~? "string" + endfunc + 1 + call search('VIM', 'w', '', 0, function('InComment')) + call assert_equal('Another Text for VIM', getline('.')) + + 1 + call search('VIM', 'w', '', 0, function('InString')) + call assert_equal(' let a = "VIM"', getline('.')) + + delfunc InComment + delfunc InString + bwipe! +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -755,6 +755,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 915, +/**/ 914, /**/ 913,