Range.from_motion: preserve jumplist
Some checks failed
ci / ci (push) Failing after 1m5s

This commit is contained in:
2026-04-14 18:37:13 -06:00
parent 88b7a11efa
commit f9ea5b0658
3 changed files with 117 additions and 6 deletions

34
AGENTS.md Normal file
View File

@@ -0,0 +1,34 @@
# AGENTS.md
## Project
Single-file Neovim Lua micro-library (`lua/u.lua`) for range-based text operations, positions, operator-pending mappings, and text objects. Not a plugin — meant to be vendored by other plugins.
## Commands
All tasks use `mise`:
- `mise run fmt` — format (stylua)
- `mise run fmt:check` — check formatting
- `mise run lint` — type-check with `emmylua_check` (ignores `.prefix/`)
- `mise run test` — run busted against Neovim 0.12.1 (includes `test:prepare`)
- `mise run test:all` — test against 0.11.5, 0.12.1, and nightly
- `mise run test:coverage` — test with luacov (only covers `lua/u$`)
- `mise run ci``fmt:check``lint``test:all`
Run a single spec file: `busted spec/u_spec.lua` (after `mise run test:prepare`).
## Architecture
- **`lua/u.lua`** — the entire library (~1300 lines): `Pos`, `Range`, `opkeymap`, `define_txtobj`, `ucmd`, `repeat_`
- **`spec/u_spec.lua`** — all tests in one file
- **`library/`** — type stubs for busted/luv (stylua-ignored, used by emmylua)
- **`.prefix/`** — neovim version installs managed by `nvimv` (git-ignored, emmylua-ignored)
- **`examples/`** — usage examples (surround, splitjoin, text-objects, matcher)
## Conventions
- **1-based indexing** everywhere (v2+). `Pos.from00()` / `Range.from00()` convert from 0-based Neovim API values.
- Stylua: LuaJIT syntax, single quotes, no call parens, 2-space indent, sort requires, 100 col width
- Tests run inside Neovim via busted's `lua = "nvim -u NONE -i NONE -l"` (set in `.busted`)
- `test:prepare` installs busted rocks via `luarocks test --prepare` and manages nightly Neovim via `nvimv`

View File

@@ -552,12 +552,10 @@ function Range.from_motion(motion, opts)
local old_eventignore = vim.o.eventignore
vim.o.eventignore = 'all'
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
vim.cmd {
cmd = 'normal',
bang = not opts.user_defined,
args = { ESC .. 'g@' .. motion },
mods = { silent = true },
}
vim.cmd(
'keepjumps normal' .. (not opts.user_defined and '!' or '') .. ' ' .. ESC .. 'g@' .. motion,
{ mods = { silent = true } }
)
vim.o.eventignore = old_eventignore
end)
local captured_range = _G.Range__from_motion_opfunc_captured_range

View File

@@ -372,6 +372,85 @@ describe('Range', function()
vim.api.nvim_buf_delete(buf2, { force = true })
end)
it('from_motion does not modify the jumplist', function()
withbuf({
'line1',
'(hello world)',
'line3',
'{foo bar}',
'line5',
}, function()
-- Build up a jumplist that includes line 2:
vim.cmd.normal { args = { '5G' }, bang = true }
vim.cmd.normal { args = { '2G' }, bang = true }
vim.cmd.normal { args = { '4G' }, bang = true }
vim.cmd.normal { args = { '5G' }, bang = true }
-- Position cursor on line 2 (which is in the jumplist), inside the parens
vim.fn.setpos('.', { 0, 2, 3, 0 })
local jl_before = vim.fn.getjumplist()
local n_before = #jl_before[1]
local curjump_before = jl_before[2]
-- g@a( corrupts the jumplist by deduplicating the entry for line 2.
-- This is a query-only operation and should NOT change the jumplist:
Range.from_motion('a(', { contains_cursor = true })
local jl_after = vim.fn.getjumplist()
assert.are.same(
n_before,
#jl_after[1],
'from_motion should not add or remove jumplist entries'
)
assert.are.same(
curjump_before,
jl_after[2],
'from_motion should not change the curjump pointer'
)
end)
end)
it('find_nearest_brackets does not modify the jumplist', function()
withbuf({
'line1',
'(hello world)',
'line3',
'{foo bar}',
'line5',
}, function()
-- Build up a jumplist:
vim.cmd.normal { args = { '5G' }, bang = true }
vim.cmd.normal { args = { '2G' }, bang = true }
vim.cmd.normal { args = { '4G' }, bang = true }
vim.cmd.normal { args = { '5G' }, bang = true }
-- Move to a line near brackets
vim.fn.setpos('.', { 0, 2, 3, 0 })
local jl_before = vim.fn.getjumplist()
local n_before = #jl_before[1]
local curjump_before = jl_before[2]
-- This calls from_motion internally and should NOT change the jumplist:
Range.find_nearest_brackets()
local jl_after = vim.fn.getjumplist()
assert.are.same(
n_before,
#jl_after[1],
'find_nearest_brackets should not add or remove jumplist entries'
)
assert.are.same(
curjump_before,
jl_after[2],
'find_nearest_brackets should not change the curjump pointer'
)
end)
end)
it('from_motion restores visual selection when started in visual mode', function()
withbuf({ 'the quick brown fox' }, function()
-- Enter visual mode first