This commit is contained in:
65
lua/u.lua
65
lua/u.lua
@@ -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')
|
return Range.new(Pos.new(bufnr, start_line, 1), Pos.new(bufnr, stop_line, vim.v.maxcol), 'V')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local BRACKET_MAP = {
|
||||||
|
['('] = '(',
|
||||||
|
[')'] = '(',
|
||||||
|
['b'] = '(',
|
||||||
|
['{'] = '{',
|
||||||
|
['}'] = '{',
|
||||||
|
['B'] = '{',
|
||||||
|
['['] = '[',
|
||||||
|
[']'] = '[',
|
||||||
|
['<'] = '<',
|
||||||
|
['>'] = '<',
|
||||||
|
}
|
||||||
|
|
||||||
--- @param motion string
|
--- @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
|
--- @return u.Range|nil
|
||||||
function Range.from_motion(motion, opts)
|
function Range.from_motion(motion, opts)
|
||||||
-- SECTION: Normalize options
|
-- 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 scope, motion_rest = motion:sub(1, 1), motion:sub(2)
|
||||||
local is_txtobj = scope == 'a' or scope == 'i'
|
local is_txtobj = scope == 'a' or scope == 'i'
|
||||||
local is_quote_txtobj = is_txtobj and vim.tbl_contains({ "'", '"', '`' }, motion_rest)
|
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
|
-- SECTION: Capture original state for restoration
|
||||||
local original_state = {
|
local original_state = {
|
||||||
@@ -546,6 +560,29 @@ function Range.from_motion(motion, opts)
|
|||||||
vim.api.nvim_buf_call(opts.bufnr, function()
|
vim.api.nvim_buf_call(opts.bufnr, function()
|
||||||
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
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 = function(ty)
|
||||||
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
|
||||||
end
|
end
|
||||||
@@ -663,20 +700,28 @@ function Range.from_cmd_args(args)
|
|||||||
end
|
end
|
||||||
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 {
|
return Range.smallest {
|
||||||
Range.from_motion('a<', { contains_cursor = true }),
|
Range.from_motion('a<', copts),
|
||||||
Range.from_motion('a[', { contains_cursor = true }),
|
Range.from_motion('a[', copts),
|
||||||
Range.from_motion('a(', { contains_cursor = true }),
|
Range.from_motion('a(', copts),
|
||||||
Range.from_motion('a{', { contains_cursor = true }),
|
Range.from_motion('a{', copts),
|
||||||
}
|
}
|
||||||
end
|
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 {
|
return Range.smallest {
|
||||||
Range.from_motion([[a']], { contains_cursor = true }),
|
Range.from_motion([[a']], copts),
|
||||||
Range.from_motion([[a"]], { contains_cursor = true }),
|
Range.from_motion([[a"]], copts),
|
||||||
Range.from_motion([[a`]], { contains_cursor = true }),
|
Range.from_motion([[a`]], copts),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
112
spec/u_spec.lua
112
spec/u_spec.lua
@@ -468,6 +468,118 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
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()
|
it('from_tsquery_caps', function()
|
||||||
withbuf({
|
withbuf({
|
||||||
'-- a comment',
|
'-- a comment',
|
||||||
|
|||||||
Reference in New Issue
Block a user