Skip to content

Commit a0e182a

Browse files
fix(repeatable_move): repeat_last_move always uses count 1 when used in operator-pending (no) mode. (#861)
I've noticed that after switching to `main` branch from `master` branch the `;` and `,` motions are not using `[count]` in operator pending mode. For example: `df/` and then `2d;` remove 2 slashes instead of expected 3 slashes (count 2 in `d;` is ignored). My nvim version: ```text NVIM v0.12.0-dev-2063+g8bdfd286e5 Build type: RelWithDebInfo LuaJIT 2.1.1767980792 Run "nvim -V1 -v" for more info ``` My bindings `nvim-treesitter-textobjects`: ```lua -- ;, and fFtT keymaps local ts_repeat_move = require "nvim-treesitter-textobjects.repeatable_move" vim.keymap.set({ "n", "v", "o" }, ";", ts_repeat_move.repeat_last_move) vim.keymap.set( { "n", "v", "o" }, ",", ts_repeat_move.repeat_last_move_opposite ) vim.keymap.set( { "n", "v", "o" }, "f", ts_repeat_move.builtin_f_expr, { expr = true } ) vim.keymap.set( { "n", "v", "o" }, "F", ts_repeat_move.builtin_F_expr, { expr = true } ) vim.keymap.set( { "n", "v", "o" }, "t", ts_repeat_move.builtin_t_expr, { expr = true } ) vim.keymap.set( { "n", "v", "o" }, "T", ts_repeat_move.builtin_T_expr, { expr = true } ) ```
1 parent 52bda74 commit a0e182a

File tree

2 files changed

+122
-17
lines changed

2 files changed

+122
-17
lines changed

lua/nvim-treesitter-textobjects/repeatable_move.lua

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
local M = {}
22

3+
---@class TSTextObjects.MovefFtTOpts
4+
---@field forward boolean If true, move forward, and false is for backward.
5+
36
---@class TSTextObjects.MoveOpts
47
---@field forward boolean If true, move forward, and false is for backward.
58
---@field start? boolean If true, choose the start of the node, and false is for the end.
@@ -26,14 +29,45 @@ M.make_repeatable_move = function(move_fn)
2629
end
2730
end
2831

29-
--- Enter visual mode (nov) if operator-pending (no) mode (fixes #699)
30-
--- Why? According to https://learnvimscriptthehardway.stevelosh.com/chapters/15.html
31-
--- If your operator-pending mapping ends with some text visually selected, Vim will operate on that text.
32-
--- Otherwise, Vim will operate on the text between the original cursor position and the new position.
33-
local function force_operator_pending_visual_mode()
34-
local mode = vim.api.nvim_get_mode()
35-
if mode.mode == 'no' then
36-
vim.cmd.normal({ 'v', bang = true })
32+
--- Handle inclusive/exclusive behavior of the `;` and `,` motions used after fFtT motions.
33+
---
34+
--- Meaning, that the following operator-pending calls (with `y` operator in this case) behave
35+
--- exactly like in plain NeoVim:
36+
---
37+
--- - `yfn` and `y;` - inclusive.
38+
--- - `yfn` and `y,` - exclusive.
39+
--- - `yFn` and `y;` - exclusive.
40+
--- - `yFn` and `y,` - inclusive.
41+
--- - `ytn` and `y;` - inclusive.
42+
--- - `ytn` and `y,` - exclusive.
43+
--- - `yTn` and `y;` - exclusive.
44+
--- - `yTn` and `y,` - inclusive.
45+
---@param opts TSTextObjects.MovefFtTOpts
46+
---@return nil
47+
local function repeat_last_move_fFtT(opts)
48+
local motion = ''
49+
50+
if M.last_move.func == 'f' or M.last_move.func == 't' then
51+
motion = opts.forward and ';' or ','
52+
else
53+
motion = opts.forward and ',' or ';'
54+
end
55+
56+
-- This changes operator-pending (no) mode to operator-pending-visual (nov) mode to include last
57+
-- character in the region when going forward. In other words, going forward will include current
58+
-- cursor and found character.
59+
local inclusive = (opts.forward and vim.api.nvim_get_mode().mode == 'no') and 'v' or ''
60+
61+
local cursor_before = vim.api.nvim_win_get_cursor(0)
62+
vim.cmd([[normal! ]] .. inclusive .. vim.v.count1 .. motion)
63+
local cursor_after = vim.api.nvim_win_get_cursor(0)
64+
65+
-- Handle a use case when a motion in an operator-pending doesn't visually selects any text
66+
-- region. Without "turning off" the `v` a single character at the cursor's position is selected.
67+
--
68+
-- For example: `yfn` and `y2;` at the end of the line.
69+
if inclusive == 'v' and vim.deep_equal(cursor_before, cursor_after) then
70+
vim.cmd([[normal! ]] .. inclusive)
3771
end
3872
end
3973

@@ -43,12 +77,8 @@ M.repeat_last_move = function(opts_extend)
4377
return
4478
end
4579
local opts = vim.tbl_deep_extend('force', M.last_move.opts, opts_extend or {})
46-
if M.last_move.func == 'f' or M.last_move.func == 't' then
47-
force_operator_pending_visual_mode()
48-
vim.cmd([[normal! ]] .. vim.v.count1 .. (opts.forward and ';' or ','))
49-
elseif M.last_move.func == 'F' or M.last_move.func == 'T' then
50-
force_operator_pending_visual_mode()
51-
vim.cmd([[normal! ]] .. vim.v.count1 .. (opts.forward and ',' or ';'))
80+
if vim.list_contains({ 'f', 'F', 't', 'T' }, M.last_move.func) then
81+
repeat_last_move_fFtT({ forward = opts.forward })
5282
else
5383
-- we assume other textobjects (move) already handle operator-pending mode correctly
5484
M.last_move.func(opts, unpack(M.last_move.additional_args))

tests/repeatable_move/common.lua

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ function M.run_builtin_find_test(file, spec)
2020
for col = 0, num_cols - 1 do
2121
for _, cmd in pairs({ 'f', 'F', 't', 'T' }) do
2222
for _, repeat_cmd in pairs({ ';', ',' }) do
23-
-- Get ground truth using vim's built-in search and repeat
23+
-- Get ground truth using vim's built-in search and repeat (normal mode)
2424
vim.api.nvim_win_set_cursor(0, { spec.row, col })
2525
local gt_cols = {}
2626
vim.cmd([[normal! ]] .. cmd .. spec.char)
@@ -36,7 +36,7 @@ function M.run_builtin_find_test(file, spec)
3636
vim.cmd([[normal! 2]] .. cmd .. spec.char)
3737
gt_cols[#gt_cols + 1] = vim.fn.col('.')
3838

39-
-- test using tstextobj repeatable_move.lua
39+
-- test using tstextobj repeatable_move.lua (normal mode)
4040
vim.api.nvim_win_set_cursor(0, { spec.row, col })
4141
local ts_cols = {}
4242
vim.cmd([[normal ]] .. cmd .. spec.char)
@@ -61,7 +61,82 @@ function M.run_builtin_find_test(file, spec)
6161
assert.are.same(
6262
gt_cols,
6363
ts_cols,
64-
string.format("Command %s works differently than vim's built-in find, col: %d", cmd, col)
64+
string.format(
65+
"Command %s with repeat %s works differently than vim's built-in find, col: %d",
66+
cmd,
67+
repeat_cmd,
68+
col
69+
)
70+
)
71+
72+
-- Get ground truth using vim's built-in search and repeat (operator-pending mode)
73+
vim.api.nvim_win_set_cursor(0, { spec.row, col })
74+
local gt_regs = {}
75+
vim.fn.setreg('0', '')
76+
vim.cmd([[normal! y]] .. cmd .. spec.char)
77+
gt_regs[#gt_regs + 1] = vim.fn.getreg('0')
78+
vim.fn.setreg('0', '')
79+
vim.cmd([[normal! y]] .. repeat_cmd)
80+
gt_regs[#gt_regs + 1] = vim.fn.getreg('0')
81+
vim.fn.setreg('0', '')
82+
vim.cmd([[normal! y2]] .. repeat_cmd)
83+
gt_regs[#gt_regs + 1] = vim.fn.getreg('0')
84+
vim.fn.setreg('0', '')
85+
vim.cmd([[normal! l]] .. repeat_cmd)
86+
vim.fn.setreg('0', '')
87+
vim.cmd([[normal! y2]] .. repeat_cmd)
88+
gt_regs[#gt_regs + 1] = vim.fn.getreg('0')
89+
vim.fn.setreg('0', '')
90+
vim.cmd([[normal! h]] .. repeat_cmd)
91+
vim.fn.setreg('0', '')
92+
vim.cmd([[normal! y2]] .. repeat_cmd)
93+
gt_regs[#gt_regs + 1] = vim.fn.getreg('0')
94+
vim.fn.setreg('0', '')
95+
vim.cmd([[normal! 2y]] .. cmd .. spec.char)
96+
gt_regs[#gt_regs + 1] = vim.fn.getreg('0')
97+
98+
-- test using tstextobj repeatable_move.lua (operator-pending mode)
99+
vim.api.nvim_win_set_cursor(0, { spec.row, col })
100+
local ts_regs = {}
101+
vim.fn.setreg('0', '')
102+
vim.cmd([[normal y]] .. cmd .. spec.char)
103+
ts_regs[#ts_regs + 1] = vim.fn.getreg('0')
104+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
105+
vim.fn.setreg('0', '')
106+
vim.cmd([[normal y]] .. repeat_cmd)
107+
ts_regs[#ts_regs + 1] = vim.fn.getreg('0')
108+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
109+
vim.fn.setreg('0', '')
110+
vim.cmd([[normal y2]] .. repeat_cmd)
111+
ts_regs[#ts_regs + 1] = vim.fn.getreg('0')
112+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
113+
vim.fn.setreg('0', '')
114+
vim.cmd([[normal l]] .. repeat_cmd)
115+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
116+
vim.fn.setreg('0', '')
117+
vim.cmd([[normal y2]] .. repeat_cmd)
118+
ts_regs[#ts_regs + 1] = vim.fn.getreg('0')
119+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
120+
vim.fn.setreg('0', '')
121+
vim.cmd([[normal h]] .. repeat_cmd)
122+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
123+
vim.fn.setreg('0', '')
124+
vim.cmd([[normal y2]] .. repeat_cmd)
125+
ts_regs[#ts_regs + 1] = vim.fn.getreg('0')
126+
assert.are.same(spec.row, vim.fn.line('.'), "Command shouldn't move cursor over rows")
127+
vim.fn.setreg('0', '')
128+
vim.cmd([[normal 2y]] .. cmd .. spec.char)
129+
ts_regs[#ts_regs + 1] = vim.fn.getreg('0')
130+
131+
assert.are.same(
132+
gt_regs,
133+
ts_regs,
134+
string.format(
135+
"Command %s with repeat %s works differently than vim's built-in find, col: %d",
136+
cmd,
137+
repeat_cmd,
138+
col
139+
)
65140
)
66141
end
67142
end

0 commit comments

Comments
 (0)