diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt index e78ffe703b..13fa624876 100644 --- a/runtime/doc/index.txt +++ b/runtime/doc/index.txt @@ -485,6 +485,7 @@ Tag Command Op-pending and Visual-mode action ~ |v_a]| a] same as a[ |v_a`| a` string in backticks |v_ab| ab "a block" from "[(" to "])" (with braces) +|v_al| al all lines (entire buffer) |v_ap| ap "a paragraph" (with white space) |v_as| as "a sentence" (with white space) |v_at| at "a tag block" (with white space) @@ -503,6 +504,7 @@ Tag Command Op-pending and Visual-mode action ~ |v_i]| i] same as i[ |v_i`| i` string in backticks without the backticks |v_ib| ib "inner block" from "[(" to "])" +|v_il| il inner line (without surrounding whitespace) |v_ip| ip "inner paragraph" |v_is| is "inner sentence" |v_it| it "inner tag block" @@ -952,6 +954,7 @@ Tag Command Note Visual-mode action ~ |v_a`| a` extend highlighted area with a backtick quoted string |v_ab| ab extend highlighted area with a () block +|v_al| al extend highlighted area to all lines |v_ap| ap extend highlighted area with a paragraph |v_as| as extend highlighted area with a sentence |v_at| at extend highlighted area with a tag block @@ -982,6 +985,7 @@ Tag Command Note Visual-mode action ~ |v_i`| i` extend highlighted area with a backtick quoted string (without the backticks) |v_ib| ib extend highlighted area with inner () block +|v_il| il select inner line under cursor |v_ip| ip extend highlighted area with inner paragraph |v_is| is extend highlighted area with inner sentence |v_it| it extend highlighted area with inner tag block diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt index a9ed65743b..489bcb420a 100644 --- a/runtime/doc/motion.txt +++ b/runtime/doc/motion.txt @@ -582,6 +582,17 @@ ip "inner paragraph", select [count] paragraphs (see is also a paragraph boundary. When used in Visual mode it is made linewise. + *v_al* *al* +al "all lines", select the whole buffer. + When used in Visual mode it is made linewise. + + *v_il* *il* +il "inner line", select the current line without leading + or trailing white space. Fails on a blank or + white-space-only line. + When used in Visual mode it selects the line under the + cursor and switches to charwise Visual mode. + a] *v_a]* *v_a[* *a]* *a[* a[ "a [] block", select [count] '[' ']' blocks. This goes backwards to the [count] unclosed '[', and finds diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index adf2a09583..e370ced411 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -149,6 +149,8 @@ EDITOR • |:packupdate| and |:packdel| for managing |vim.pack|. • 'scrollback' is now also valid in |prompt-buffer| buffers to limit the number of history lines kept above the prompt. +• |v_al| and |v_il| text objects select the whole buffer and the current line + without leading or trailing white space. EVENTS diff --git a/runtime/doc/quickref.txt b/runtime/doc/quickref.txt index f446333b8e..92ce9c2a93 100644 --- a/runtime/doc/quickref.txt +++ b/runtime/doc/quickref.txt @@ -487,6 +487,8 @@ In Insert or Command-line mode: |v_is| N is Select "inner sentence" |v_ap| N ap Select "a paragraph" |v_ip| N ip Select "inner paragraph" +|v_al| al Select all lines +|v_il| il Select "inner line" |v_ab| N ab Select "a block" (from "[(" to "])") |v_ib| N ib Select "inner block" (from "[(" to "])") |v_aB| N aB Select "a Block" (from `[{` to `]}`) diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 9efd5656b1..59a6252c55 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -396,6 +396,8 @@ Normal commands: - |gO| shows a filetype-defined "outline" of the current buffer. - |Q| replays the last recorded macro instead of switching to Ex mode (|gQ|). - |ZR| performs |:restart| +- |v_al| selects the whole buffer; |v_il| selects the current line without + leading or trailing white space. Options: diff --git a/runtime/doc/visual.txt b/runtime/doc/visual.txt index c6294cec38..e44e8f634e 100644 --- a/runtime/doc/visual.txt +++ b/runtime/doc/visual.txt @@ -228,6 +228,8 @@ The objects that can be used are: is inner sentence |v_is| ap a paragraph (with white space) |v_ap| ip inner paragraph |v_ip| + al all lines |v_al| + il inner line |v_il| ab a () block (with parentheses) |v_ab| ib inner () block |v_ib| aB a {} block (with braces) |v_aB| diff --git a/src/nvim/normal.c b/src/nvim/normal.c index a8ae229462..f9434c1c1e 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -1819,6 +1819,7 @@ void clearop(oparg_T *oap) oap->regname = 0; oap->motion_force = NUL; oap->use_reg_one = false; + oap->restore_cursor = false; motion_force = NUL; } @@ -6356,6 +6357,9 @@ static void nv_object(cmdarg_T *cap) case 'p': // "ap" = a paragraph flag = current_par(cap->oap, cap->count1, include, 'p'); break; + case 'l': // "il" = inner line, "al" = all lines + flag = current_line(cap->oap, include); + break; case 's': // "as" = a sentence flag = current_sent(cap->oap, cap->count1, include); break; diff --git a/src/nvim/normal_defs.h b/src/nvim/normal_defs.h index 7b49b28a0f..1b04ef2f25 100644 --- a/src/nvim/normal_defs.h +++ b/src/nvim/normal_defs.h @@ -31,6 +31,7 @@ typedef struct { pos_T start; ///< start of the operator pos_T end; ///< end of the operator pos_T cursor_start; ///< cursor position before motion for "gw" + bool restore_cursor; ///< restore cursor after yank linenr_T line_count; ///< number of lines from op_start to op_end (inclusive) bool empty; ///< op_start and op_end the same (only used by op_change()) diff --git a/src/nvim/ops.c b/src/nvim/ops.c index dff66afdac..62b375b440 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -3695,6 +3695,9 @@ void do_pending_operator(cmdarg_T *cap, int old_col, bool gui_yank) } else { restore_lbr(lbr_saved); oap->excl_tr_ws = cap->cmdchar == 'z'; + if (oap->restore_cursor) { + curwin->w_cursor = oap->cursor_start; + } op_yank(oap, !gui_yank); } check_cursor_col(curwin); diff --git a/src/nvim/textobject.c b/src/nvim/textobject.c index 0c33b319bb..ae7ccab9ba 100644 --- a/src/nvim/textobject.c +++ b/src/nvim/textobject.c @@ -3,9 +3,11 @@ #include #include #include +#include #include "nvim/ascii_defs.h" #include "nvim/buffer_defs.h" +#include "nvim/charset.h" #include "nvim/cursor.h" #include "nvim/drawscreen.h" #include "nvim/edit.h" @@ -718,6 +720,75 @@ int current_word(oparg_T *oap, int count, bool include, bool bigword) return OK; } +/// Find the current line ("il") or all lines ("al"). +/// +/// @param include true: all lines, false: current line without surrounding +/// white space +int current_line(oparg_T *oap, bool include) +{ + if (include) { + if (VIsual_active) { + VIsual.lnum = 1; + VIsual.col = 0; + VIsual.coladd = 0; + VIsual_mode = 'V'; + redraw_curbuf_later(UPD_INVERTED); + showmode(); + } else { + oap->cursor_start = curwin->w_cursor; + oap->restore_cursor = true; + oap->start.lnum = 1; + oap->start.col = 0; + oap->start.coladd = 0; + oap->motion_type = kMTLineWise; + } + curwin->w_cursor.lnum = curbuf->b_ml.ml_line_count; + curwin->w_cursor.col = 0; + curwin->w_cursor.coladd = 0; + return OK; + } + + char *line = ml_get(curwin->w_cursor.lnum); + char *start = skipwhite(line); + char *end = line + strlen(line); + while (end > start) { + char *prev = mb_prevptr(line, end); + if (!ascii_iswhite((uint8_t)(*prev))) { + break; + } + end = prev; + } + if (start == end) { + return FAIL; + } + + pos_T start_pos = curwin->w_cursor; + start_pos.col = (colnr_T)(start - line); + start_pos.coladd = 0; + + pos_T end_pos = curwin->w_cursor; + end_pos.col = (colnr_T)(mb_prevptr(line, end) - line); + end_pos.coladd = 0; + + if (VIsual_active) { + VIsual = start_pos; + curwin->w_cursor = end_pos; + if (*p_sel == 'e' && ltoreq(VIsual, curwin->w_cursor)) { + inc_cursor(); + } + VIsual_mode = 'v'; + redraw_curbuf_later(UPD_INVERTED); + showmode(); + } else { + oap->start = start_pos; + oap->motion_type = kMTCharWise; + oap->inclusive = true; + curwin->w_cursor = end_pos; + } + + return OK; +} + /// Find sentence(s) under the cursor, cursor at end. /// When Visual active, extend it by one or more sentences. int current_sent(oparg_T *oap, int count, bool include) diff --git a/test/functional/editor/textobject_spec.lua b/test/functional/editor/textobject_spec.lua new file mode 100644 index 0000000000..c6c30a80a8 --- /dev/null +++ b/test/functional/editor/textobject_spec.lua @@ -0,0 +1,102 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() + +local api = n.api +local clear = n.clear +local command = n.command +local eq = t.eq +local eval = n.eval +local feed = n.feed +local fn = n.fn + +local function set_lines(lines) + api.nvim_buf_set_lines(0, 0, -1, true, lines) +end + +local function get_lines() + return api.nvim_buf_get_lines(0, 0, -1, true) +end + +local function set_cursor(row, col) + api.nvim_win_set_cursor(0, { row, col }) +end + +describe('line textobject', function() + before_each(clear) + + it('selects the current line without surrounding whitespace', function() + set_lines({ 'one', ' two words ', 'three' }) + set_cursor(2, 4) + feed('yil') + eq('two words', fn.getreg('"')) + + set_cursor(2, 4) + feed('vily') + eq('two words', fn.getreg('"')) + eq('v', fn.visualmode()) + + set_cursor(2, 4) + feed('Vily') + eq('two words', fn.getreg('"')) + eq('v', fn.visualmode()) + + set_cursor(2, 0) + feed('0v$ily') + eq('two words', fn.getreg('"')) + + set_cursor(1, 0) + feed('Vjily') + eq('two words', fn.getreg('"')) + + command('set selection=exclusive') + set_cursor(2, 4) + feed('vily') + eq('two words', fn.getreg('"')) + command('set selection&') + + set_cursor(2, 4) + feed('dil') + eq({ 'one', ' ', 'three' }, get_lines()) + + set_lines({ ' αβ ', ' ', 'last' }) + set_cursor(1, 2) + feed('yil') + eq('αβ', fn.getreg('"')) + + command('set selection=exclusive') + set_cursor(1, 2) + feed('vily') + eq('αβ', fn.getreg('"')) + command('set selection&') + + fn.setreg('"', 'unchanged') + set_cursor(2, 0) + eq(0, fn.assert_beeps('normal yil')) + eq('unchanged', fn.getreg('"')) + + set_cursor(2, 0) + eq(0, fn.assert_beeps('normal vily')) + eq('unchanged', fn.getreg('"')) + end) + + it('selects all lines without moving the cursor for yank', function() + set_lines({ ' αβ ', ' ', 'last' }) + command('let g:textobj_line_yank_pos = []') + command([[autocmd TextYankPost ++once let g:textobj_line_yank_pos = getpos('.')]]) + + set_cursor(3, 0) + feed('yal') + eq(' αβ \n \nlast\n', fn.getreg('"')) + eq({ 0, 3, 1, 0 }, fn.getpos('.')) + eq({ 0, 3, 1, 0 }, eval('g:textobj_line_yank_pos')) + + set_cursor(3, 0) + feed('valy') + eq(' αβ \n \nlast\n', fn.getreg('"')) + eq('V', fn.visualmode()) + + set_cursor(2, 0) + feed('dal') + eq({ '' }, get_lines()) + end) +end)