65
lua/u.lua
65
lua/u.lua
@@ -6,8 +6,8 @@ local M = {}
|
|||||||
|
|
||||||
local MAX_COL = vim.v.maxcol
|
local MAX_COL = vim.v.maxcol
|
||||||
|
|
||||||
--- @param bufnr number
|
--- @param bufnr integer
|
||||||
--- @param lnum number 1-based
|
--- @param lnum integer 1-based
|
||||||
local function line_text(bufnr, lnum)
|
local function line_text(bufnr, lnum)
|
||||||
return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
|
return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
|
||||||
end
|
end
|
||||||
@@ -73,7 +73,10 @@ function Pos.from10(bufnr, lnum1, col0, off) return Pos.new(bufnr, lnum1, col0 +
|
|||||||
|
|
||||||
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
function Pos.invalid() return Pos.new(0, 0, 0, 0) end
|
||||||
|
|
||||||
function Pos.__lt(a, b) return a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col) end
|
function Pos.__lt(a, b)
|
||||||
|
return a.bufnr == b.bufnr
|
||||||
|
and (a.lnum < b.lnum or (a.lnum == b.lnum and a.col < b.col))
|
||||||
|
end
|
||||||
function Pos.__le(a, b) return a < b or a == b end
|
function Pos.__le(a, b) return a < b or a == b end
|
||||||
function Pos.__eq(a, b)
|
function Pos.__eq(a, b)
|
||||||
return getmetatable(a) == Pos
|
return getmetatable(a) == Pos
|
||||||
@@ -240,8 +243,14 @@ function Pos:find_next(dir, predicate)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Finds the matching bracket/paren for the current position.
|
--- Finds the matching bracket/paren for the current position.
|
||||||
--- @param max_chars? number|nil
|
---
|
||||||
--- @param invocations? u.Pos[]
|
--- Algorithm: Scans forward (for openers) or backward (for closers) until we find
|
||||||
|
--- a matching character. When encountering a nested bracket of the same type, we
|
||||||
|
--- recursively find its match first, then continue scanning. This handles deeply
|
||||||
|
--- nested structures correctly.
|
||||||
|
---
|
||||||
|
--- @param max_chars? number|nil Safety limit to prevent infinite loops
|
||||||
|
--- @param invocations? u.Pos[] Tracks positions to prevent infinite recursion
|
||||||
--- @return u.Pos|nil
|
--- @return u.Pos|nil
|
||||||
function Pos:find_match(max_chars, invocations)
|
function Pos:find_match(max_chars, invocations)
|
||||||
if invocations == nil then invocations = {} end
|
if invocations == nil then invocations = {} end
|
||||||
@@ -317,6 +326,10 @@ end
|
|||||||
-- Range class (forward declared for Extmark)
|
-- Range class (forward declared for Extmark)
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
--- @class u.Range
|
||||||
|
--- @field start u.Pos
|
||||||
|
--- @field stop u.Pos|nil
|
||||||
|
--- @field mode 'v'|'V'
|
||||||
local Range = {}
|
local Range = {}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@@ -406,14 +419,14 @@ function Range.__tostring(self)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local _1 = posstr(self.start)
|
local start_str = posstr(self.start)
|
||||||
local _2 = posstr(self.stop)
|
local stop_str = posstr(self.stop)
|
||||||
return string.format(
|
return string.format(
|
||||||
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
|
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
|
||||||
self.start.bufnr,
|
self.start.bufnr,
|
||||||
self.mode,
|
self.mode,
|
||||||
_1,
|
start_str,
|
||||||
_2
|
stop_str
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -499,25 +512,25 @@ end
|
|||||||
--- @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 }
|
||||||
--- @return u.Range|nil
|
--- @return u.Range|nil
|
||||||
function Range.from_motion(motion, opts)
|
function Range.from_motion(motion, opts)
|
||||||
-- Options handling:
|
-- SECTION: Normalize options
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
if opts.bufnr == nil or opts.bufnr == 0 then opts.bufnr = vim.api.nvim_get_current_buf() end
|
if opts.bufnr == nil or opts.bufnr == 0 then opts.bufnr = vim.api.nvim_get_current_buf() end
|
||||||
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
if opts.contains_cursor == nil then opts.contains_cursor = false end
|
||||||
if opts.user_defined == nil then opts.user_defined = false end
|
if opts.user_defined == nil then opts.user_defined = false end
|
||||||
|
|
||||||
-- Extract some information from the motion:
|
-- SECTION: Parse motion string
|
||||||
--- @type 'a'|'i', string
|
--- @type 'a'|'i', string
|
||||||
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)
|
||||||
|
|
||||||
-- Capture the original state of the buffer for restoration later.
|
-- SECTION: Capture original state for restoration
|
||||||
local original_state = {
|
local original_state = {
|
||||||
winview = vim.fn.winsaveview(),
|
winview = vim.fn.winsaveview(),
|
||||||
regquote = vim.fn.getreg '"',
|
unnamed_register = vim.fn.getreg '"',
|
||||||
cursor = vim.fn.getpos '.',
|
cursor = vim.fn.getpos '.',
|
||||||
pos_lbrack = vim.fn.getpos "'[",
|
mark_lbracket = vim.fn.getpos "'[",
|
||||||
pos_rbrack = vim.fn.getpos "']",
|
mark_rbracket = vim.fn.getpos "']",
|
||||||
opfunc = vim.go.operatorfunc,
|
opfunc = vim.go.operatorfunc,
|
||||||
prev_captured_range = _G.Range__from_motion_opfunc_captured_range,
|
prev_captured_range = _G.Range__from_motion_opfunc_captured_range,
|
||||||
prev_mode = vim.fn.mode(),
|
prev_mode = vim.fn.mode(),
|
||||||
@@ -526,6 +539,7 @@ function Range.from_motion(motion, opts)
|
|||||||
--- @type u.Range|nil
|
--- @type u.Range|nil
|
||||||
_G.Range__from_motion_opfunc_captured_range = nil
|
_G.Range__from_motion_opfunc_captured_range = nil
|
||||||
|
|
||||||
|
-- SECTION: Execute motion and capture result
|
||||||
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
|
||||||
|
|
||||||
@@ -545,19 +559,19 @@ function Range.from_motion(motion, opts)
|
|||||||
end)
|
end)
|
||||||
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
||||||
|
|
||||||
-- Restore original state:
|
-- SECTION: Restore state
|
||||||
vim.fn.winrestview(original_state.winview)
|
vim.fn.winrestview(original_state.winview)
|
||||||
vim.fn.setreg('"', original_state.regquote)
|
vim.fn.setreg('"', original_state.unnamed_register)
|
||||||
vim.fn.setpos('.', original_state.cursor)
|
vim.fn.setpos('.', original_state.cursor)
|
||||||
vim.fn.setpos("'[", original_state.pos_lbrack)
|
vim.fn.setpos("'[", original_state.mark_lbracket)
|
||||||
vim.fn.setpos("']", original_state.pos_rbrack)
|
vim.fn.setpos("']", original_state.mark_rbracket)
|
||||||
if original_state.prev_mode ~= 'n' then original_state.vinf:set_visual_selection() end
|
if original_state.prev_mode ~= 'n' then original_state.vinf:set_visual_selection() end
|
||||||
vim.go.operatorfunc = original_state.opfunc
|
vim.go.operatorfunc = original_state.opfunc
|
||||||
_G.Range__from_motion_opfunc_captured_range = original_state.prev_captured_range
|
_G.Range__from_motion_opfunc_captured_range = original_state.prev_captured_range
|
||||||
|
|
||||||
if not captured_range then return nil end
|
if not captured_range then return nil end
|
||||||
|
|
||||||
-- Fixup the bounds:
|
-- SECTION: Fixup edge cases (quote text objects, 'it' tag)
|
||||||
if
|
if
|
||||||
-- I have no idea why, but when yanking `i"`, the stop-mark is
|
-- I have no idea why, but when yanking `i"`, the stop-mark is
|
||||||
-- placed on the ending quote. For other text-objects, the stop-
|
-- placed on the ending quote. For other text-objects, the stop-
|
||||||
@@ -573,6 +587,7 @@ function Range.from_motion(motion, opts)
|
|||||||
captured_range.stop = captured_range.stop:find_next(-1, motion_rest) or captured_range.stop
|
captured_range.stop = captured_range.stop:find_next(-1, motion_rest) or captured_range.stop
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- SECTION: Validate cursor containment
|
||||||
if
|
if
|
||||||
opts.contains_cursor and not captured_range:contains(Pos.new(unpack(original_state.cursor)))
|
opts.contains_cursor and not captured_range:contains(Pos.new(unpack(original_state.cursor)))
|
||||||
then
|
then
|
||||||
@@ -873,14 +888,14 @@ function Range:sub(i, j)
|
|||||||
|
|
||||||
local start = get_pos(i)
|
local start = get_pos(i)
|
||||||
if not start then
|
if not start then
|
||||||
-- start is inalid, so return an empty range:
|
-- start is invalid, so return an empty range:
|
||||||
return Range.new(self.start, nil, self.mode)
|
return Range.new(self.start, nil, self.mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
local stop
|
local stop
|
||||||
if j then stop = get_pos(j) end
|
if j then stop = get_pos(j) end
|
||||||
if not stop then
|
if not stop then
|
||||||
-- stop is inalid, so return an empty range:
|
-- stop is invalid, so return an empty range:
|
||||||
return Range.new(start, nil, self.mode)
|
return Range.new(start, nil, self.mode)
|
||||||
end
|
end
|
||||||
return Range.new(start, stop, 'v')
|
return Range.new(start, stop, 'v')
|
||||||
@@ -896,8 +911,6 @@ end
|
|||||||
function Range:text() return vim.fn.join(self:lines(), '\n') end
|
function Range:text() return vim.fn.join(self:lines(), '\n') end
|
||||||
|
|
||||||
--- @param l number
|
--- @param l number
|
||||||
-- luacheck: ignore
|
|
||||||
--- @return { line: string, idx0: { start: number, stop: number }, lnum: number, range: fun():u.Range, text: fun():string }|nil
|
|
||||||
function Range:line(l)
|
function Range:line(l)
|
||||||
if l < 0 then l = self:line_count() + l + 1 end
|
if l < 0 then l = self:line_count() + l + 1 end
|
||||||
if l > self:line_count() then return end
|
if l > self:line_count() then return end
|
||||||
@@ -1043,6 +1056,9 @@ end
|
|||||||
-- opkeymap
|
-- opkeymap
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- NOTE: These must be global because Neovim's 'operatorfunc' option only
|
||||||
|
-- accepts VimScript expressions (v:lua.FunctionName), not direct Lua
|
||||||
|
-- references. This is a Neovim API limitation.
|
||||||
--- @type fun(range: u.Range): nil|(fun():any)
|
--- @type fun(range: u.Range): nil|(fun():any)
|
||||||
local __U__OpKeymapOpFunc_rhs = nil
|
local __U__OpKeymapOpFunc_rhs = nil
|
||||||
|
|
||||||
@@ -1238,7 +1254,6 @@ local utils = {}
|
|||||||
--- -- Perform search and replace...
|
--- -- Perform search and replace...
|
||||||
--- end, { nargs = 2, range = true })
|
--- end, { nargs = 2, range = true })
|
||||||
--- ```
|
--- ```
|
||||||
-- luacheck: ignore
|
|
||||||
function utils.ucmd(name, cmd, opts)
|
function utils.ucmd(name, cmd, opts)
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local cmd2 = cmd
|
local cmd2 = cmd
|
||||||
|
|||||||
Reference in New Issue
Block a user