(Range.from_motion) add max_lines and max_cols
Some checks failed
ci / ci (push) Failing after 34s

This commit is contained in:
2026-04-28 23:45:32 -06:00
parent f9ea5b0658
commit b7605b7976
2 changed files with 167 additions and 10 deletions

View File

@@ -511,8 +511,21 @@ function Range.from_lines(bufnr, start_line, stop_line)
return Range.new(Pos.new(bufnr, start_line, 1), Pos.new(bufnr, stop_line, vim.v.maxcol), 'V')
end
local BRACKET_MAP = {
['('] = '(',
[')'] = '(',
['b'] = '(',
['{'] = '{',
['}'] = '{',
['B'] = '{',
['['] = '[',
[']'] = '[',
['<'] = '<',
['>'] = '<',
}
--- @param motion string
--- @param opts? { bufnr?: number, contains_cursor?: boolean, pos?: u.Pos, user_defined?: boolean }
--- @param opts? { bufnr?: number, contains_cursor?: boolean, pos?: u.Pos, user_defined?: boolean, max_lines?: number, max_col?: number }
--- @return u.Range|nil
function Range.from_motion(motion, opts)
-- SECTION: Normalize options
@@ -526,6 +539,7 @@ function Range.from_motion(motion, opts)
local scope, motion_rest = motion:sub(1, 1), motion:sub(2)
local is_txtobj = scope == 'a' or scope == 'i'
local is_quote_txtobj = is_txtobj and vim.tbl_contains({ "'", '"', '`' }, motion_rest)
local is_bracket_txtobj = is_txtobj and BRACKET_MAP[motion_rest] ~= nil
-- SECTION: Capture original state for restoration
local original_state = {
@@ -546,6 +560,29 @@ function Range.from_motion(motion, opts)
vim.api.nvim_buf_call(opts.bufnr, function()
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
-- Pre-check: skip expensive g@ when target char not found within bounds
if
(is_bracket_txtobj or is_quote_txtobj)
and type(opts.max_lines) == 'number'
and opts.max_lines > 0
then
local cur = vim.fn.line '.'
local line_start = math.max(1, cur - opts.max_lines)
local line_end = math.min(vim.api.nvim_buf_line_count(opts.bufnr), cur + opts.max_lines)
local chunk = vim.api.nvim_buf_get_lines(opts.bufnr, line_start - 1, line_end, false)
local target = is_bracket_txtobj and BRACKET_MAP[motion_rest] or motion_rest
local max_col = opts.max_col
local found = false
for _, line in ipairs(chunk) do
local pos = line:find(target, 1, true)
if pos and (max_col == nil or pos <= max_col) then
found = true
break
end
end
if not found then return end
end
_G.Range__from_motion_opfunc = function(ty)
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
end
@@ -663,20 +700,28 @@ function Range.from_cmd_args(args)
end
end
function Range.find_nearest_brackets()
--- @param opts? { max_lines?: number, max_col?: number }
--- @return u.Range|nil
function Range.find_nearest_brackets(opts)
opts = opts or {}
local copts = { contains_cursor = true, max_lines = opts.max_lines, max_col = opts.max_col }
return Range.smallest {
Range.from_motion('a<', { contains_cursor = true }),
Range.from_motion('a[', { contains_cursor = true }),
Range.from_motion('a(', { contains_cursor = true }),
Range.from_motion('a{', { contains_cursor = true }),
Range.from_motion('a<', copts),
Range.from_motion('a[', copts),
Range.from_motion('a(', copts),
Range.from_motion('a{', copts),
}
end
function Range.find_nearest_quotes()
--- @param opts? { max_lines?: number, max_col?: number }
--- @return u.Range|nil
function Range.find_nearest_quotes(opts)
opts = opts or {}
local copts = { contains_cursor = true, max_lines = opts.max_lines, max_col = opts.max_col }
return Range.smallest {
Range.from_motion([[a']], { contains_cursor = true }),
Range.from_motion([[a"]], { contains_cursor = true }),
Range.from_motion([[a`]], { contains_cursor = true }),
Range.from_motion([[a']], copts),
Range.from_motion([[a"]], copts),
Range.from_motion([[a`]], copts),
}
end

View File

@@ -468,6 +468,118 @@ describe('Range', function()
end)
end)
describe('from_motion with max_lines', function()
it('returns nil when bracket chars are beyond max_lines', function()
-- A large block where { and } are far from cursor
local lines = {}
lines[1] = '{'
for i = 2, 200 do
lines[i] = 'content'
end
lines[201] = '}'
withbuf(lines, function()
vim.fn.setpos('.', { 0, 100, 1, 0 }) -- cursor inside the block, 99 lines from {
-- max_lines=50: { is 99 lines above, } is 101 lines below → pre-check fails
local range = Range.from_motion('a{', { max_lines = 50 })
assert.is_nil(range)
end)
end)
it('finds brackets within max_lines range', function()
withbuf({ 'this is a {', 'block', '} here' }, function()
vim.fn.setpos('.', { 0, 2, 1, 0 })
local range = Range.from_motion('a{', { max_lines = 10 })
assert.is_not_nil(range)
assert.are.same('{\nblock\n}', range:text())
end)
end)
it('works for closing bracket chars ) ] } >', function()
withbuf({ 'this is a (', 'block', ') here' }, function()
vim.fn.setpos('.', { 0, 2, 1, 0 })
local range = Range.from_motion('a)', { max_lines = 10 })
assert.is_not_nil(range)
assert.are.same('(\nblock\n)', range:text())
end)
end)
it('works for b and B aliases', function()
withbuf({ 'this is a {', 'block', '} here' }, function()
vim.fn.setpos('.', { 0, 2, 1, 0 })
local range = Range.from_motion('aB', { max_lines = 10 })
assert.is_not_nil(range)
assert.are.same('{\nblock\n}', range:text())
end)
end)
it('returns nil when quote chars are beyond max_lines', function()
local lines = {}
for i = 1, 200 do
lines[i] = 'no quotes here'
end
lines[1] = '"quoted"'
withbuf(lines, function()
vim.fn.setpos('.', { 0, 100, 1, 0 }) -- cursor 99 lines from quote char
local range = Range.from_motion('a"', { max_lines = 50 })
assert.is_nil(range)
end)
end)
it('finds quotes within max_lines range', function()
withbuf({ [[the "quick" brown fox]] }, function()
vim.fn.setpos('.', { 0, 1, 5, 0 })
local range = Range.from_motion('a"', { max_lines = 10 })
assert.is_not_nil(range)
assert.are.same('"quick"', range:text())
end)
end)
it('max_lines nil (default) does not pre-check', function()
local lines = {}
lines[1] = '{'
for i = 2, 200 do
lines[i] = 'content'
end
lines[201] = '}'
withbuf(lines, function()
vim.fn.setpos('.', { 0, 100, 1, 0 })
-- Without max_lines, g@ runs and finds the block
local range = Range.from_motion 'a{'
assert.is_not_nil(range)
end)
end)
it('skips brackets in lines exceeding max_col', function()
local long_line = string.rep('x', 10240) .. '{block}'
withbuf({ long_line }, function()
vim.fn.setpos('.', { 0, 1, 10245, 0 }) -- cursor inside {block}
-- { is at col 10241, beyond max_col=10240
local range = Range.from_motion('a{', { max_lines = 10, max_col = 10240 })
assert.is_nil(range)
end)
end)
it('finds brackets within max_col on a long line', function()
local long_line = string.rep('x', 100) .. '{block}'
withbuf({ long_line }, function()
vim.fn.setpos('.', { 0, 1, 105, 0 }) -- cursor inside 'block'
local range = Range.from_motion('a{', { max_lines = 10, max_col = 10240 })
assert.is_not_nil(range)
assert.are.same('{block}', range:text())
end)
end)
it('max_col nil (default) does not limit column search', function()
local long_line = string.rep('x', 10240) .. '{block}'
withbuf({ long_line }, function()
vim.fn.setpos('.', { 0, 1, 10245, 0 }) -- cursor inside {block}
-- { at col 10241, but no max_col limit
local range = Range.from_motion('a{', { max_lines = 10 })
assert.is_not_nil(range)
end)
end)
end)
it('from_tsquery_caps', function()
withbuf({
'-- a comment',