65
lua/u.lua
65
lua/u.lua
@@ -6,8 +6,8 @@ local M = {}
|
||||
|
||||
local MAX_COL = vim.v.maxcol
|
||||
|
||||
--- @param bufnr number
|
||||
--- @param lnum number 1-based
|
||||
--- @param bufnr integer
|
||||
--- @param lnum integer 1-based
|
||||
local function line_text(bufnr, lnum)
|
||||
return vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)[1]
|
||||
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.__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.__eq(a, b)
|
||||
return getmetatable(a) == Pos
|
||||
@@ -240,8 +243,14 @@ function Pos:find_next(dir, predicate)
|
||||
end
|
||||
|
||||
--- 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
|
||||
function Pos:find_match(max_chars, invocations)
|
||||
if invocations == nil then invocations = {} end
|
||||
@@ -317,6 +326,10 @@ end
|
||||
-- Range class (forward declared for Extmark)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
--- @class u.Range
|
||||
--- @field start u.Pos
|
||||
--- @field stop u.Pos|nil
|
||||
--- @field mode 'v'|'V'
|
||||
local Range = {}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@@ -406,14 +419,14 @@ function Range.__tostring(self)
|
||||
end
|
||||
end
|
||||
|
||||
local _1 = posstr(self.start)
|
||||
local _2 = posstr(self.stop)
|
||||
local start_str = posstr(self.start)
|
||||
local stop_str = posstr(self.stop)
|
||||
return string.format(
|
||||
'Range{bufnr=%d, mode=%s, start=%s, stop=%s}',
|
||||
self.start.bufnr,
|
||||
self.mode,
|
||||
_1,
|
||||
_2
|
||||
start_str,
|
||||
stop_str
|
||||
)
|
||||
end
|
||||
|
||||
@@ -499,25 +512,25 @@ end
|
||||
--- @param opts? { bufnr?: number, contains_cursor?: boolean, pos?: u.Pos, user_defined?: boolean }
|
||||
--- @return u.Range|nil
|
||||
function Range.from_motion(motion, opts)
|
||||
-- Options handling:
|
||||
-- SECTION: Normalize options
|
||||
opts = opts or {}
|
||||
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.user_defined == nil then opts.user_defined = false end
|
||||
|
||||
-- Extract some information from the motion:
|
||||
-- SECTION: Parse motion string
|
||||
--- @type 'a'|'i', string
|
||||
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)
|
||||
|
||||
-- Capture the original state of the buffer for restoration later.
|
||||
-- SECTION: Capture original state for restoration
|
||||
local original_state = {
|
||||
winview = vim.fn.winsaveview(),
|
||||
regquote = vim.fn.getreg '"',
|
||||
unnamed_register = vim.fn.getreg '"',
|
||||
cursor = vim.fn.getpos '.',
|
||||
pos_lbrack = vim.fn.getpos "'[",
|
||||
pos_rbrack = vim.fn.getpos "']",
|
||||
mark_lbracket = vim.fn.getpos "'[",
|
||||
mark_rbracket = vim.fn.getpos "']",
|
||||
opfunc = vim.go.operatorfunc,
|
||||
prev_captured_range = _G.Range__from_motion_opfunc_captured_range,
|
||||
prev_mode = vim.fn.mode(),
|
||||
@@ -526,6 +539,7 @@ function Range.from_motion(motion, opts)
|
||||
--- @type u.Range|nil
|
||||
_G.Range__from_motion_opfunc_captured_range = nil
|
||||
|
||||
-- SECTION: Execute motion and capture result
|
||||
vim.api.nvim_buf_call(opts.bufnr, function()
|
||||
if opts.pos ~= nil then opts.pos:save_to_pos '.' end
|
||||
|
||||
@@ -545,19 +559,19 @@ function Range.from_motion(motion, opts)
|
||||
end)
|
||||
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
||||
|
||||
-- Restore original state:
|
||||
-- SECTION: Restore state
|
||||
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.pos_lbrack)
|
||||
vim.fn.setpos("']", original_state.pos_rbrack)
|
||||
vim.fn.setpos("'[", original_state.mark_lbracket)
|
||||
vim.fn.setpos("']", original_state.mark_rbracket)
|
||||
if original_state.prev_mode ~= 'n' then original_state.vinf:set_visual_selection() end
|
||||
vim.go.operatorfunc = original_state.opfunc
|
||||
_G.Range__from_motion_opfunc_captured_range = original_state.prev_captured_range
|
||||
|
||||
if not captured_range then return nil end
|
||||
|
||||
-- Fixup the bounds:
|
||||
-- SECTION: Fixup edge cases (quote text objects, 'it' tag)
|
||||
if
|
||||
-- I have no idea why, but when yanking `i"`, the stop-mark is
|
||||
-- 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
|
||||
end
|
||||
|
||||
-- SECTION: Validate cursor containment
|
||||
if
|
||||
opts.contains_cursor and not captured_range:contains(Pos.new(unpack(original_state.cursor)))
|
||||
then
|
||||
@@ -873,14 +888,14 @@ function Range:sub(i, j)
|
||||
|
||||
local start = get_pos(i)
|
||||
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)
|
||||
end
|
||||
|
||||
local stop
|
||||
if j then stop = get_pos(j) end
|
||||
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)
|
||||
end
|
||||
return Range.new(start, stop, 'v')
|
||||
@@ -896,8 +911,6 @@ end
|
||||
function Range:text() return vim.fn.join(self:lines(), '\n') end
|
||||
|
||||
--- @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)
|
||||
if l < 0 then l = self:line_count() + l + 1 end
|
||||
if l > self:line_count() then return end
|
||||
@@ -1043,6 +1056,9 @@ end
|
||||
-- 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)
|
||||
local __U__OpKeymapOpFunc_rhs = nil
|
||||
|
||||
@@ -1238,7 +1254,6 @@ local utils = {}
|
||||
--- -- Perform search and replace...
|
||||
--- end, { nargs = 2, range = true })
|
||||
--- ```
|
||||
-- luacheck: ignore
|
||||
function utils.ucmd(name, cmd, opts)
|
||||
opts = opts or {}
|
||||
local cmd2 = cmd
|
||||
|
||||
Reference in New Issue
Block a user