Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions lua/nvim-treesitter-textobjects/repeatable_move.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,41 @@ local function repeat_last_move_fFtT(opts)
motion = opts.forward and ',' or ';'
end

local mode = vim.api.nvim_get_mode().mode
local operator_pending = mode:sub(1, 2) == 'no'
-- *forced-motion* character typed by the user before the `;` or `,` motion: `v`, `V`, `CTRL-V`
-- or an empty string when the motion wasn't forced.
local forced = mode:sub(3)

-- This changes operator-pending (no) mode to operator-pending-visual (nov) mode to include last
-- character in the region when going forward. In other words, going forward will include current
-- cursor and found character.
local inclusive = (opts.forward and vim.api.nvim_get_mode().mode == 'no') and 'v' or ''
local inclusive = (opts.forward and operator_pending and forced == '') and 'v' or ''

local cursor_before = vim.api.nvim_win_get_cursor(0)
vim.cmd([[normal! ]] .. inclusive .. vim.v.count1 .. motion)
local cursor_after = vim.api.nvim_win_get_cursor(0)

-- Handle a use case when a motion in an operator-pending doesn't visually selects any text
-- region. Without "turning off" the `v` a single character at the cursor's position is selected.
if vim.deep_equal(cursor_before, cursor_after) then
-- Handle a use case when a motion in an operator-pending doesn't visually selects any text
-- region. Without "turning off" the `v` a single character at the cursor's position is selected.
--
-- For example: `yfn` and `y2;` at the end of the line.
if inclusive == 'v' then
vim.cmd([[normal! ]] .. inclusive)
end

return
end

-- Handle a *forced-motion* (`:h forced-motion`) typed by the user, like in `yfn` and `yv;`.
--
-- For example: `yfn` and `y2;` at the end of the line.
if inclusive == 'v' and vim.deep_equal(cursor_before, cursor_after) then
vim.cmd([[normal! ]] .. inclusive)
-- The `;` and `,` motions above are performed by `normal!`, so Neovim applies the force to a
-- plain cursor movement (exclusive) instead of an `fFtT` repeat (inclusive when going forward).
-- The force can't be undone with another `v` so shrink the region by a single character instead
-- to make the forward motion exclusive.
if opts.forward and operator_pending and forced == 'v' then
vim.api.nvim_win_set_cursor(0, { cursor_after[1], cursor_after[2] - 1 })
end
end

Expand Down
68 changes: 68 additions & 0 deletions tests/repeatable_move/common.lua
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,66 @@ function M.run_builtin_find_test(file, spec)
vim.cmd('edit!')
end

-- Test the *forced-motion* (`:h forced-motion`) `v` typed before a `;` or `,` repeat in
-- operator-pending mode, like in `yfn` and `yv;`.
--
-- The `v` inverts the inclusive/exclusive behavior of the repeated fFtT motion.
function M.run_forced_find_test(file, spec)
assert.are.same(1, vim.fn.filereadable(file), string.format('File "%s" not readable', file))

-- load reference file
vim.cmd(string.format('edit %s', file))

vim.api.nvim_win_set_cursor(0, { spec.row, 0 })
local line = vim.api.nvim_get_current_line()
local num_cols = #line

for col = 0, num_cols - 1 do
for _, cmd in pairs({ 'f', 'F', 't', 'T' }) do
for _, repeat_cmd in pairs({ ';', ',' }) do
for _, count in pairs({ '', '2' }) do
-- Check whether the repeat finds anything at all, using vim's built-in search and repeat
vim.api.nvim_win_set_cursor(0, { spec.row, col })
vim.cmd([[normal! ]] .. cmd .. spec.char)
local found_from = vim.fn.col('.')
vim.cmd([[normal! ]] .. count .. repeat_cmd)
local found_to = vim.fn.col('.')

if found_from ~= found_to then
-- Get ground truth using vim's built-in search and repeat (operator-pending mode)
vim.api.nvim_win_set_cursor(0, { spec.row, col })
vim.cmd([[normal! ]] .. cmd .. spec.char)
vim.fn.setreg('0', '')
vim.cmd([[normal! yv]] .. count .. repeat_cmd)
local gt_reg = vim.fn.getreg('0')

-- test using tstextobj repeatable_move.lua (operator-pending mode)
vim.api.nvim_win_set_cursor(0, { spec.row, col })
vim.cmd([[normal ]] .. cmd .. spec.char)
vim.fn.setreg('0', '')
vim.cmd([[normal yv]] .. count .. repeat_cmd)
local ts_reg = vim.fn.getreg('0')

assert.are.same(
gt_reg,
ts_reg,
string.format(
"Command %s with forced repeat v%s%s works differently than vim's built-in find, col: %d",
cmd,
count,
repeat_cmd,
col
)
)
end
end
end
end
end
-- clear any changes to avoid 'No write since last change (add ! to override)'
vim.cmd('edit!')
end

local Runner = {}
Runner.__index = Runner

Expand All @@ -167,6 +227,14 @@ function Runner:builtin_find(file, spec, title)
end)
end

function Runner:forced_find(file, spec, title)
title = title and title or tostring(spec.row)
self.it(string.format('%s[%s]', file, title), function()
local path = vim.fs.joinpath(self.base_dir, file)
M.run_forced_find_test(path, spec)
end)
end

M.Runner = Runner

return M
4 changes: 4 additions & 0 deletions tests/repeatable_move/python_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ describe('builtin find Python:', function()
describe('repeat:', function()
run:builtin_find('aligned_indent.py', { row = 1, char = 'n' })
end)

describe('forced repeat:', function()
run:forced_find('aligned_indent.py', { row = 1, char = 'n' })
end)
end)
Loading