From 59a171fd99a7452c8840eee55783fb6746994637 Mon Sep 17 00:00:00 2001 From: neeshy <60193883+neeshy@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:23:30 -0500 Subject: fix(defaults): improve visual search mappings #32378 Problem: The behavior of the visual search mappings aren't consistent with their normal mode counterparts. - The count isn't considered - Searching with an empty selection will match every character in the buffer - Searching backwards only jumps back when the cursor is positioned at the start of the selection. Solution: - Issue `n` `v:count1` times - Error out and exit visual mode when the selection is empty - Detect when the cursor is not at the start of the selection, and adjust the count accordingly Also, use the search register instead of the more error-prone approach of feeding the entire search string as an expression --- runtime/lua/vim/_defaults.lua | 48 ++++++++++++++++++++++++-------- test/functional/editor/defaults_spec.lua | 12 +++++--- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index b1e5687d74..afcc2cfc73 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -32,27 +32,53 @@ do --- --- See |v_star-default| and |v_#-default| do - local function _visual_search(cmd) - assert(cmd == '/' or cmd == '?') - local chunks = - vim.fn.getregion(vim.fn.getpos('.'), vim.fn.getpos('v'), { type = vim.fn.mode() }) + local function _visual_search(forward) + assert(forward == 0 or forward == 1) + local pos = vim.fn.getpos('.') + local vpos = vim.fn.getpos('v') + local mode = vim.fn.mode() + local chunks = vim.fn.getregion(pos, vpos, { type = mode }) local esc_chunks = vim .iter(chunks) :map(function(v) - return vim.fn.escape(v, cmd == '/' and [[/\]] or [[?\]]) + return vim.fn.escape(v, [[\]]) end) :totable() local esc_pat = table.concat(esc_chunks, [[\n]]) - local search_cmd = ([[%s\V%s%s]]):format(cmd, esc_pat, '\n') - return '\27' .. search_cmd + if #esc_pat == 0 then + vim.api.nvim_echo({ { 'E348: No string under cursor' } }, true, { err = true }) + return '' + end + local search = [[\V]] .. esc_pat + + vim.fn.setreg('/', search) + vim.fn.histadd('/', search) + vim.v.searchforward = forward + + -- The count has to be adjusted when searching backwards and the cursor + -- isn't positioned at the beginning of the selection + local count = vim.v.count1 + if forward == 0 then + local _, line, col, _ = unpack(pos) + local _, vline, vcol, _ = unpack(vpos) + if + line > vline + or mode == 'v' and line == vline and col > vcol + or mode == 'V' and col ~= 1 + or mode == '\22' and col > vcol + then + count = count + 1 + end + end + return '' .. count .. 'n' end vim.keymap.set('x', '*', function() - return _visual_search('/') - end, { desc = ':help v_star-default', expr = true, replace_keycodes = false }) + return _visual_search(1) + end, { desc = ':help v_star-default', expr = true }) vim.keymap.set('x', '#', function() - return _visual_search('?') - end, { desc = ':help v_#-default', expr = true, replace_keycodes = false }) + return _visual_search(0) + end, { desc = ':help v_#-default', expr = true }) end --- Map Y to y$. This mimics the behavior of D and C. See |Y-default| diff --git a/test/functional/editor/defaults_spec.lua b/test/functional/editor/defaults_spec.lua index ee6bfa1be9..25332d5b1f 100644 --- a/test/functional/editor/defaults_spec.lua +++ b/test/functional/editor/defaults_spec.lua @@ -119,7 +119,9 @@ describe('default', function() [[testing /?\!3]], [[testing /?\!4]], }) - n.feed('gg0vf!o*') + n.feed('gg0vf!') + n.poke_eventloop() + n.feed('*') screen:expect([[ {3:testing /?\!}1 | {4:^testing /?\!}2 | @@ -127,7 +129,7 @@ describe('default', function() {3:testing /?\!}4 | {1:~ }|*2 {2:[No Name] [+] 2,1 All}| - /\Vtesting \/?\\! [2/4] | + /\Vtesting /?\\! [2/4] | ]]) n.feed('n') screen:expect([[ @@ -137,9 +139,11 @@ describe('default', function() {3:testing /?\!}4 | {1:~ }|*2 {2:[No Name] [+] 3,1 All}| - /\Vtesting \/?\\! [3/4] | + /\Vtesting /?\\! [3/4] | ]]) - n.feed('G0vf!o#') + n.feed('G0vf!') + n.poke_eventloop() + n.feed('#') screen:expect([[ {3:testing /?\!}1 | {3:testing /?\!}2 | -- cgit