# HG changeset patch # User Christian Brabandt # Date 1708470902 -3600 # Node ID e6defaa1e46aeab8cf2ecd0aebeb05dc60fd88d7 # Parent d9a589d77d3e3ea336e5f183611863924c5d9936 patch 9.1.0120: hard to get visual region using Vim script Commit: https://github.com/vim/vim/commit/3f905ab3c4f66562f4a224bf00f49d98a0b0da91 Author: Shougo Matsushita Date: Wed Feb 21 00:02:45 2024 +0100 patch 9.1.0120: hard to get visual region using Vim script Problem: hard to get visual region using Vim script Solution: Add getregion() Vim script function (Shougo Matsushita, Jakub ?uczy?ski) closes: #13998 closes: #11579 Co-authored-by: =?UTF-8?q?Jakub=20=C5=81uczy=C5=84ski?= Co-authored-by: Shougo Matsushita Signed-off-by: Shougo Matsushita 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 @@ -263,6 +263,8 @@ getqflist({what}) Dict get specific qui getreg([{regname} [, 1 [, {list}]]]) String or List contents of a register getreginfo([{regname}]) Dict information about a register +getregion({pos1}, {pos2}, {type}) + List get the text from {pos1} to {pos2} getregtype([{regname}]) String type of a register getscriptinfo([{opts}]) List list of sourced scripts gettabinfo([{expr}]) List list of tab pages @@ -4266,6 +4268,43 @@ getreginfo([{regname}]) *getreginfo( Can also be used as a |method|: > GetRegname()->getreginfo() +getregion({pos1}, {pos2}, {type}) *getregion()* + Returns the list of strings from {pos1} to {pos2} as if it's + selected in visual mode of {type}. + For possible values of {pos1} and {pos2} see |line()|. + {type} is the selection type: + "v" for |characterwise| mode + "V" for |linewise| mode + "" for |blockwise-visual| mode + You can get the last selection type by |visualmode()|. + If Visual mode is active, use |mode()| to get the Visual mode + (e.g., in a |:vmap|). + This function uses the line and column number from the + specified position. + It is useful to get text starting and ending in different + columns, such as |characterwise-visual| selection. + + Note that: + - Order of {pos1} and {pos2} doesn't matter, it will always + return content from the upper left position to the lower + right position. + - If 'virtualedit' is enabled and selection is past the end of + line, resulting lines are filled with blanks. + - If the selection starts or ends in the middle of a multibyte + character, it is not included but its selected part is + substituted with spaces. + - If {pos1} or {pos2} equals "v" (see |line()|) and it is not in + |visual-mode|, an empty list is returned. + - If {pos1}, {pos2} or {type} is an invalid string, an empty + list is returned. + + Examples: > + :xnoremap + \ echo getregion('v', '.', mode()) +< + Can also be used as a |method|: > + '.'->getregion("'a', 'v') +< getregtype([{regname}]) *getregtype()* The result is a String, which is type of register {regname}. The value will be one of: diff --git a/runtime/doc/tags b/runtime/doc/tags --- a/runtime/doc/tags +++ b/runtime/doc/tags @@ -7760,6 +7760,7 @@ getqflist() builtin.txt /*getqflist()* getqflist-examples quickfix.txt /*getqflist-examples* getreg() builtin.txt /*getreg()* getreginfo() builtin.txt /*getreginfo()* +getregion() builtin.txt /*getregion()* getregtype() builtin.txt /*getregtype()* getscript pi_getscript.txt /*getscript* getscript-autoinstall pi_getscript.txt /*getscript-autoinstall* 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 @@ -1,4 +1,4 @@ -*usr_41.txt* For Vim version 9.1. Last change: 2024 Feb 01 +*usr_41.txt* For Vim version 9.1. Last change: 2024 Feb 20 VIM USER MANUAL - by Bram Moolenaar @@ -929,6 +929,7 @@ Cursor and mark position: *cursor-funct Working with text in the current buffer: *text-functions* getline() get a line or list of lines from the buffer + getregion() get a region of text from the buffer setline() replace a line in the buffer append() append line or list of lines in the buffer indent() indent of a specific line diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -41560,6 +41560,7 @@ Functions: ~ |foreach()| apply function to List items |matchbufline()| all the matches of a pattern in a buffer |matchstrlist()| all the matches of a pattern in a List of strings +|getregion()| get a region of text from a buffer Autocommands: ~ diff --git a/src/evalfunc.c b/src/evalfunc.c --- a/src/evalfunc.c +++ b/src/evalfunc.c @@ -63,15 +63,16 @@ static void f_get(typval_T *argvars, typ static void f_getchangelist(typval_T *argvars, typval_T *rettv); static void f_getcharpos(typval_T *argvars, typval_T *rettv); static void f_getcharsearch(typval_T *argvars, typval_T *rettv); +static void f_getcurpos(typval_T *argvars, typval_T *rettv); +static void f_getcursorcharpos(typval_T *argvars, typval_T *rettv); static void f_getenv(typval_T *argvars, typval_T *rettv); static void f_getfontname(typval_T *argvars, typval_T *rettv); static void f_getjumplist(typval_T *argvars, typval_T *rettv); static void f_getpid(typval_T *argvars, typval_T *rettv); -static void f_getcurpos(typval_T *argvars, typval_T *rettv); -static void f_getcursorcharpos(typval_T *argvars, typval_T *rettv); static void f_getpos(typval_T *argvars, typval_T *rettv); static void f_getreg(typval_T *argvars, typval_T *rettv); static void f_getreginfo(typval_T *argvars, typval_T *rettv); +static void f_getregion(typval_T *argvars, typval_T *rettv); static void f_getregtype(typval_T *argvars, typval_T *rettv); static void f_gettagstack(typval_T *argvars, typval_T *rettv); static void f_gettext(typval_T *argvars, typval_T *rettv); @@ -2131,6 +2132,8 @@ static funcentry_T global_functions[] = ret_getreg, f_getreg}, {"getreginfo", 0, 1, FEARG_1, arg1_string, ret_dict_any, f_getreginfo}, + {"getregion", 3, 3, FEARG_1, arg3_string, + ret_list_string, f_getregion}, {"getregtype", 0, 1, FEARG_1, arg1_string, ret_string, f_getregtype}, {"getscriptinfo", 0, 1, 0, arg1_dict_any, @@ -5453,6 +5456,186 @@ f_getpos(typval_T *argvars, typval_T *re } /* + * Convert from block_def to string + */ + static char_u * +block_def2str(struct block_def *bd) +{ + char_u *p, *ret; + size_t size = bd->startspaces + bd->endspaces + bd->textlen; + + ret = alloc(size + 1); + if (ret != NULL) + { + p = ret; + vim_memset(p, ' ', bd->startspaces); + p += bd->startspaces; + mch_memmove(p, bd->textstart, bd->textlen); + p += bd->textlen; + vim_memset(p, ' ', bd->endspaces); + *(p + bd->endspaces) = NUL; + } + return ret; +} + +/* + * "getregion()" function + */ + static void +f_getregion(typval_T *argvars, typval_T *rettv) +{ + linenr_T lnum; + oparg_T oap; + struct block_def bd; + char_u *akt = NULL; + int inclusive = TRUE; + int fnum = -1; + pos_T p1, p2; + pos_T *fp = NULL; + char_u *pos1, *pos2, *type; + int save_virtual = -1; + int l; + int region_type = -1; + int is_visual; + + if (rettv_list_alloc(rettv) == FAIL) + return; + + if (check_for_string_arg(argvars, 0) == FAIL + || check_for_string_arg(argvars, 1) == FAIL + || check_for_string_arg(argvars, 2) == FAIL) + return; + + // NOTE: var2fpos() returns static pointer. + fp = var2fpos(&argvars[0], TRUE, &fnum, FALSE); + if (fp == NULL) + return; + p1 = *fp; + + fp = var2fpos(&argvars[1], TRUE, &fnum, FALSE); + if (fp == NULL) + return; + p2 = *fp; + + pos1 = tv_get_string(&argvars[0]); + pos2 = tv_get_string(&argvars[1]); + type = tv_get_string(&argvars[2]); + + is_visual = (pos1[0] == 'v' && pos1[1] == NUL) + || (pos2[0] == 'v' && pos2[1] == NUL); + + if (is_visual && !VIsual_active) + return; + + if (type[0] == 'v' && type[1] == NUL) + region_type = MCHAR; + else if (type[0] == 'V' && type[1] == NUL) + region_type = MLINE; + else if (type[0] == Ctrl_V && type[1] == NUL) + region_type = MBLOCK; + else + return; + + save_virtual = virtual_op; + virtual_op = virtual_active(); + + if (!LT_POS(p1, p2)) + { + // swap position + pos_T p; + + p = p1; + p1 = p2; + p2 = p; + } + + if (region_type == MCHAR) + { + // handle 'selection' == "exclusive" + if (*p_sel == 'e' && !EQUAL_POS(p1, p2)) + { + if (p2.coladd > 0) + p2.coladd--; + else if (p2.col > 0) + { + p2.col--; + + mb_adjustpos(curbuf, &p2); + } + else if (p2.lnum > 1) + { + p2.lnum--; + p2.col = (colnr_T)STRLEN(ml_get(p2.lnum)); + if (p2.col > 0) + { + p2.col--; + + mb_adjustpos(curbuf, &p2); + } + } + } + // if fp2 is on NUL (empty line) inclusive becomes false + if (*ml_get_pos(&p2) == NUL && !virtual_op) + inclusive = FALSE; + } + else if (region_type == MBLOCK) + { + colnr_T sc1, ec1, sc2, ec2; + + getvvcol(curwin, &p1, &sc1, NULL, &ec1); + getvvcol(curwin, &p2, &sc2, NULL, &ec2); + oap.motion_type = OP_NOP; + oap.inclusive = TRUE; + oap.start = p1; + oap.end = p2; + oap.start_vcol = MIN(sc1, sc2); + if (*p_sel == 'e' && ec1 < sc2 && 0 < sc2 && ec2 > ec1) + oap.end_vcol = sc2 - 1; + else + oap.end_vcol = MAX(ec1, ec2); + } + + // Include the trailing byte of a multi-byte char. + l = utfc_ptr2len((char_u *)ml_get_pos(&p2)); + if (l > 1) + p2.col += l - 1; + + for (lnum = p1.lnum; lnum <= p2.lnum; lnum++) + { + int ret = 0; + + if (region_type == MLINE) + akt = vim_strsave(ml_get(lnum)); + else if (region_type == MBLOCK) + { + block_prep(&oap, &bd, lnum, FALSE); + akt = block_def2str(&bd); + } + else if (p1.lnum < lnum && lnum < p2.lnum) + akt = vim_strsave(ml_get(lnum)); + else + { + charwise_block_prep(p1, p2, &bd, lnum, inclusive); + akt = block_def2str(&bd); + } + + if (akt) + { + ret = list_append_string(rettv->vval.v_list, akt, -1); + vim_free(akt); + } + + if (akt == NULL || ret == FAIL) + { + list_free(rettv->vval.v_list); + break; + } + } + + virtual_op = save_virtual; +} + +/* * Common between getreg(), getreginfo() and getregtype(): get the register * name from the first argument. * Returns zero on error. diff --git a/src/ops.c b/src/ops.c --- a/src/ops.c +++ b/src/ops.c @@ -2415,6 +2415,84 @@ block_prep( } /* + * Get block text from "start" to "end" + */ + void +charwise_block_prep( + pos_T start, + pos_T end, + struct block_def *bdp, + linenr_T lnum, + int inclusive) +{ + colnr_T startcol = 0, endcol = MAXCOL; + int is_oneChar = FALSE; + colnr_T cs, ce; + char_u *p; + + p = ml_get(lnum); + bdp->startspaces = 0; + bdp->endspaces = 0; + + if (lnum == start.lnum) + { + startcol = start.col; + if (virtual_op) + { + getvcol(curwin, &start, &cs, NULL, &ce); + if (ce != cs && start.coladd > 0) + { + // Part of a tab selected -- but don't + // double-count it. + bdp->startspaces = (ce - cs + 1) + - start.coladd; + if (bdp->startspaces < 0) + bdp->startspaces = 0; + startcol++; + } + } + } + + if (lnum == end.lnum) + { + endcol = end.col; + if (virtual_op) + { + getvcol(curwin, &end, &cs, NULL, &ce); + if (p[endcol] == NUL || (cs + end.coladd < ce + // Don't add space for double-wide + // char; endcol will be on last byte + // of multi-byte char. + && (*mb_head_off)(p, p + endcol) == 0)) + { + if (start.lnum == end.lnum + && start.col == end.col) + { + // Special case: inside a single char + is_oneChar = TRUE; + bdp->startspaces = end.coladd + - start.coladd + inclusive; + endcol = startcol; + } + else + { + bdp->endspaces = end.coladd + + inclusive; + endcol -= inclusive; + } + } + } + } + if (endcol == MAXCOL) + endcol = (colnr_T)STRLEN(p); + if (startcol > endcol || is_oneChar) + bdp->textlen = 0; + else + bdp->textlen = endcol - startcol + inclusive; + bdp->textstart = p + startcol; +} + +/* * Handle the add/subtract operator. */ void diff --git a/src/proto/ops.pro b/src/proto/ops.pro --- a/src/proto/ops.pro +++ b/src/proto/ops.pro @@ -14,6 +14,7 @@ void adjust_cursor_eol(void); char_u *skip_comment(char_u *line, int process, int include_space, int *is_comment); int do_join(long count, int insert_space, int save_undo, int use_formatoptions, int setmark); void block_prep(oparg_T *oap, struct block_def *bdp, linenr_T lnum, int is_del); +void charwise_block_prep(pos_T start, pos_T end, struct block_def *bdp, linenr_T lnum, int inclusive); void op_addsub(oparg_T *oap, linenr_T Prenum1, int g_cmd); void clear_oparg(oparg_T *oap); void cursor_pos_info(dict_T *dict); diff --git a/src/register.c b/src/register.c --- a/src/register.c +++ b/src/register.c @@ -1148,7 +1148,6 @@ op_yank(oparg_T *oap, int deleting, int int yanktype = oap->motion_type; long yanklines = oap->line_count; linenr_T yankendlnum = oap->end.lnum; - char_u *p; char_u *pnew; struct block_def bd; #if defined(FEAT_CLIPBOARD) && defined(FEAT_X11) @@ -1240,70 +1239,7 @@ op_yank(oparg_T *oap, int deleting, int case MCHAR: { - colnr_T startcol = 0, endcol = MAXCOL; - int is_oneChar = FALSE; - colnr_T cs, ce; - - p = ml_get(lnum); - bd.startspaces = 0; - bd.endspaces = 0; - - if (lnum == oap->start.lnum) - { - startcol = oap->start.col; - if (virtual_op) - { - getvcol(curwin, &oap->start, &cs, NULL, &ce); - if (ce != cs && oap->start.coladd > 0) - { - // Part of a tab selected -- but don't - // double-count it. - bd.startspaces = (ce - cs + 1) - - oap->start.coladd; - if (bd.startspaces < 0) - bd.startspaces = 0; - startcol++; - } - } - } - - if (lnum == oap->end.lnum) - { - endcol = oap->end.col; - if (virtual_op) - { - getvcol(curwin, &oap->end, &cs, NULL, &ce); - if (p[endcol] == NUL || (cs + oap->end.coladd < ce - // Don't add space for double-wide - // char; endcol will be on last byte - // of multi-byte char. - && (*mb_head_off)(p, p + endcol) == 0)) - { - if (oap->start.lnum == oap->end.lnum - && oap->start.col == oap->end.col) - { - // Special case: inside a single char - is_oneChar = TRUE; - bd.startspaces = oap->end.coladd - - oap->start.coladd + oap->inclusive; - endcol = startcol; - } - else - { - bd.endspaces = oap->end.coladd - + oap->inclusive; - endcol -= oap->inclusive; - } - } - } - } - if (endcol == MAXCOL) - endcol = (colnr_T)STRLEN(p); - if (startcol > endcol || is_oneChar) - bd.textlen = 0; - else - bd.textlen = endcol - startcol + oap->inclusive; - bd.textstart = p + startcol; + charwise_block_prep(oap->start, oap->end, &bd, lnum, oap->inclusive); if (yank_copy_line(&bd, y_idx, FALSE) == FAIL) goto fail; break; diff --git a/src/testdir/test_vim9_builtin.vim b/src/testdir/test_vim9_builtin.vim --- a/src/testdir/test_vim9_builtin.vim +++ b/src/testdir/test_vim9_builtin.vim @@ -5197,4 +5197,13 @@ def Test_passing_type_to_builtin() v9.CheckScriptFailure(lines, 'E1405: Class "C" cannot be used as a value') enddef +def Test_getregion() + assert_equal(['x'], getregion('.', '.', 'v')->map((_, _) => 'x')) + + v9.CheckDefAndScriptFailure(['getregion(10, ".", "v")'], ['E1013: Argument 1: type mismatch, expected string but got number', 'E1174: String required for argument 1']) + assert_equal([''], getregion('.', '.', 'v')) + v9.CheckDefExecFailure(['getregion("a", ".", "v")'], 'E1209:') + v9.CheckDefExecAndScriptFailure(['getregion("", ".", "v")'], 'E1209: Invalid value for a line number') +enddef + " vim: ts=8 sw=2 sts=2 expandtab tw=80 fdm=marker diff --git a/src/testdir/test_visual.vim b/src/testdir/test_visual.vim --- a/src/testdir/test_visual.vim +++ b/src/testdir/test_visual.vim @@ -1630,4 +1630,159 @@ func Test_visual_substitute_visual() bwipe! endfunc +func Test_visual_getregion() + new + + call setline(1, ['one', 'two', 'three']) + + " Visual mode + call cursor(1, 1) + call feedkeys("\vjl", 'tx') + call assert_equal(['one', 'tw'], 'v'->getregion('.', 'v')) + call assert_equal(['one', 'tw'], '.'->getregion('v', 'v')) + call assert_equal(['o'], 'v'->getregion('v', 'v')) + call assert_equal(['w'], '.'->getregion('.', 'v')) + call assert_equal(['one', 'two'], '.'->getregion('v', 'V')) + call assert_equal(['on', 'tw'], '.'->getregion('v', "\")) + + " Line visual mode + call cursor(1, 1) + call feedkeys("\Vl", 'tx') + call assert_equal(['one'], getregion('v', '.', 'V')) + call assert_equal(['one'], getregion('.', 'v', 'V')) + call assert_equal(['one'], getregion('v', 'v', 'V')) + call assert_equal(['one'], getregion('.', '.', 'V')) + call assert_equal(['on'], '.'->getregion('v', 'v')) + call assert_equal(['on'], '.'->getregion('v', "\")) + + " Block visual mode + call cursor(1, 1) + call feedkeys("\\ll", 'tx') + call assert_equal(['one'], getregion('v', '.', "\")) + call assert_equal(['one'], getregion('.', 'v', "\")) + call assert_equal(['o'], getregion('v', 'v', "\")) + call assert_equal(['e'], getregion('.', '.', "\")) + call assert_equal(['one'], '.'->getregion('v', 'V')) + call assert_equal(['one'], '.'->getregion('v', 'v')) + + " Using Marks + call setpos("'a", [0, 2, 3, 0]) + call cursor(1, 1) + call assert_equal(['one', 'two'], "'a"->getregion('.', 'v')) + call assert_equal(['one', 'two'], "."->getregion("'a", 'v')) + call assert_equal(['one', 'two'], "."->getregion("'a", 'V')) + call assert_equal(['two'], "'a"->getregion("'a", 'V')) + call assert_equal(['one', 'two'], "."->getregion("'a", "\")) + + " Multiline with line visual mode + call cursor(1, 1) + call feedkeys("\Vjj", 'tx') + call assert_equal(['one', 'two', 'three'], getregion('v', '.', 'V')) + + " Multiline with block visual mode + call cursor(1, 1) + call feedkeys("\\jj", 'tx') + call assert_equal(['o', 't', 't'], getregion('v', '.', "\")) + + call cursor(1, 1) + call feedkeys("\\jj$", 'tx') + call assert_equal(['one', 'two', 'three'], getregion('v', '.', "\")) + + " 'virtualedit' + set virtualedit=all + call cursor(1, 1) + call feedkeys("\\10ljj$", 'tx') + call assert_equal(['one ', 'two ', 'three '], + \ getregion('v', '.', "\")) + set virtualedit& + + " Invalid position + call cursor(1, 1) + call feedkeys("\vjj$", 'tx') + call assert_fails("call getregion(1, 2, 'v')", 'E1174:') + call assert_fails("call getregion('.', {}, 'v')", 'E1174:') + call assert_equal([], getregion('', '.', 'v')) + call assert_equal([], getregion('.', '.', '')) + call feedkeys("\", 'tx') + call assert_equal([], getregion('v', '.', 'v')) + + " using an unset mark + call assert_equal([], "'z"->getregion(".", 'V')) + " using the wrong type + call assert_fails(':echo "."->getregion([],"V")', 'E1174:') + call assert_fails(':echo "."->getregion("$", {})', 'E1174:') + call assert_fails(':echo [0, 1, 1, 0]->getregion("$", "v")', 'E1174:') + + + bwipe! + " Selection in starts or ends in the middle of a multibyte character + new + call setline(1, [ + \ "abcdefghijk\u00ab", + \ "\U0001f1e6\u00ab\U0001f1e7\u00ab\U0001f1e8\u00ab\U0001f1e9", + \ "1234567890" + \ ]) + call cursor(1, 3) + call feedkeys("\\ljj", 'xt') + call assert_equal(['cd', "\u00ab ", '34'], + \ getregion('v', '.', "\")) + call cursor(1, 4) + call feedkeys("\\ljj", 'xt') + call assert_equal(['de', "\U0001f1e7", '45'], + \ getregion('v', '.', "\")) + call cursor(1, 5) + call feedkeys("\\jj", 'xt') + call assert_equal(['e', ' ', '5'], getregion('v', '.', "\")) + call cursor(1, 1) + call feedkeys("\vj", 'xt') + call assert_equal(['abcdefghijk«', "\U0001f1e6"], getregion('v', '.', "v")) + " marks on multibyte chars + set selection=exclusive + call setpos("'a", [0, 1, 11, 0]) + call setpos("'b", [0, 2, 16, 0]) + call setpos("'c", [0, 2, 0, 0]) + call cursor(1, 1) + call assert_equal(['ghijk', '🇨«🇩'], getregion("'a", "'b", "\")) + call assert_equal(['k«', '🇦«🇧«🇨'], getregion("'a", "'b", "v")) + call assert_equal(['k«'], getregion("'a", "'c", "v")) + + bwipe! + + " Exclusive selection + new + set selection=exclusive + call setline(1, ["a\tc", "x\tz", '', '']) + call cursor(1, 1) + call feedkeys("\v2l", 'xt') + call assert_equal(["a\t"], getregion('v', '.', 'v')) + call cursor(1, 1) + call feedkeys("\v$G", 'xt') + call assert_equal(["a\tc", "x\tz", ''], getregion('v', '.', 'v')) + call cursor(1, 1) + call feedkeys("\v$j", 'xt') + call assert_equal(["a\tc", "x\tz"], getregion('v', '.', 'v')) + call cursor(1, 1) + call feedkeys("\\$j", 'xt') + call assert_equal(["a\tc", "x\tz"], getregion('v', '.', "\")) + call cursor(1, 1) + call feedkeys("\\$G", 'xt') + call assert_equal(["a", "x", '', ''], getregion('v', '.', "\")) + call cursor(1, 1) + call feedkeys("\wv2j", 'xt') + call assert_equal(["c", "x\tz"], getregion('v', '.', 'v')) + + " virtualedit + set virtualedit=all + call cursor(1, 1) + call feedkeys("\2lv2lj", 'xt') + call assert_equal([' c', 'x '], getregion('v', '.', 'v')) + call cursor(1, 1) + call feedkeys("\2l\2l2j", 'xt') + call assert_equal([' ', ' ', ' '], getregion('v', '.', "\")) + set virtualedit& + set selection& + + 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 @@ -705,6 +705,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 120, +/**/ 119, /**/ 118,