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
This commit is contained in:
neeshy
2025-02-09 16:23:30 -05:00
committed by GitHub
parent ac207c3ac2
commit 59a171fd99
2 changed files with 45 additions and 15 deletions

View File

@@ -32,27 +32,53 @@ do
--- ---
--- See |v_star-default| and |v_#-default| --- See |v_star-default| and |v_#-default|
do do
local function _visual_search(cmd) local function _visual_search(forward)
assert(cmd == '/' or cmd == '?') assert(forward == 0 or forward == 1)
local chunks = local pos = vim.fn.getpos('.')
vim.fn.getregion(vim.fn.getpos('.'), vim.fn.getpos('v'), { type = vim.fn.mode() }) local vpos = vim.fn.getpos('v')
local mode = vim.fn.mode()
local chunks = vim.fn.getregion(pos, vpos, { type = mode })
local esc_chunks = vim local esc_chunks = vim
.iter(chunks) .iter(chunks)
:map(function(v) :map(function(v)
return vim.fn.escape(v, cmd == '/' and [[/\]] or [[?\]]) return vim.fn.escape(v, [[\]])
end) end)
:totable() :totable()
local esc_pat = table.concat(esc_chunks, [[\n]]) local esc_pat = table.concat(esc_chunks, [[\n]])
local search_cmd = ([[%s\V%s%s]]):format(cmd, esc_pat, '\n') if #esc_pat == 0 then
return '\27' .. search_cmd vim.api.nvim_echo({ { 'E348: No string under cursor' } }, true, { err = true })
return '<Esc>'
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 '<Esc>' .. count .. 'n'
end end
vim.keymap.set('x', '*', function() vim.keymap.set('x', '*', function()
return _visual_search('/') return _visual_search(1)
end, { desc = ':help v_star-default', expr = true, replace_keycodes = false }) end, { desc = ':help v_star-default', expr = true })
vim.keymap.set('x', '#', function() vim.keymap.set('x', '#', function()
return _visual_search('?') return _visual_search(0)
end, { desc = ':help v_#-default', expr = true, replace_keycodes = false }) end, { desc = ':help v_#-default', expr = true })
end end
--- Map Y to y$. This mimics the behavior of D and C. See |Y-default| --- Map Y to y$. This mimics the behavior of D and C. See |Y-default|

View File

@@ -119,7 +119,9 @@ describe('default', function()
[[testing <CR> /?\!3]], [[testing <CR> /?\!3]],
[[testing <CR> /?\!4]], [[testing <CR> /?\!4]],
}) })
n.feed('gg0vf!o*') n.feed('gg0vf!')
n.poke_eventloop()
n.feed('*')
screen:expect([[ screen:expect([[
{3:testing <CR> /?\!}1 | {3:testing <CR> /?\!}1 |
{4:^testing <CR> /?\!}2 | {4:^testing <CR> /?\!}2 |
@@ -127,7 +129,7 @@ describe('default', function()
{3:testing <CR> /?\!}4 | {3:testing <CR> /?\!}4 |
{1:~ }|*2 {1:~ }|*2
{2:[No Name] [+] 2,1 All}| {2:[No Name] [+] 2,1 All}|
/\Vtesting <CR> \/?\\! [2/4] | /\Vtesting <CR> /?\\! [2/4] |
]]) ]])
n.feed('n') n.feed('n')
screen:expect([[ screen:expect([[
@@ -137,9 +139,11 @@ describe('default', function()
{3:testing <CR> /?\!}4 | {3:testing <CR> /?\!}4 |
{1:~ }|*2 {1:~ }|*2
{2:[No Name] [+] 3,1 All}| {2:[No Name] [+] 3,1 All}|
/\Vtesting <CR> \/?\\! [3/4] | /\Vtesting <CR> /?\\! [3/4] |
]]) ]])
n.feed('G0vf!o#') n.feed('G0vf!')
n.poke_eventloop()
n.feed('#')
screen:expect([[ screen:expect([[
{3:testing <CR> /?\!}1 | {3:testing <CR> /?\!}1 |
{3:testing <CR> /?\!}2 | {3:testing <CR> /?\!}2 |