range: extmarks/tsquery; renderer: text-change
All checks were successful
NeoVim tests / code-quality (push) Successful in 1m18s

This commit is contained in:
2025-06-11 20:04:46 -06:00
parent 12945a4cdf
commit 72b6886838
25 changed files with 1371 additions and 224 deletions

View File

@@ -1,6 +1,8 @@
local Extmark = require 'u.extmark'
local Pos = require 'u.pos'
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
local NS = vim.api.nvim_create_namespace 'u.range'
--- @class u.Range
--- @field start u.Pos
@@ -115,7 +117,7 @@ end
function Range.from_motion(motion, opts)
-- Options handling:
opts = opts or {}
if opts.bufnr == nil 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.user_defined == nil then opts.user_defined = false end
@@ -146,6 +148,8 @@ function Range.from_motion(motion, opts)
_G.Range__from_motion_opfunc = function(ty)
_G.Range__from_motion_opfunc_captured_range = Range.from_op_func(ty)
end
local old_eventignore = vim.o.eventignore
vim.o.eventignore = 'all'
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
vim.cmd {
cmd = 'normal',
@@ -153,6 +157,7 @@ function Range.from_motion(motion, opts)
args = { ESC .. 'g@' .. motion },
mods = { silent = true },
}
vim.o.eventignore = old_eventignore
end)
local captured_range = _G.Range__from_motion_opfunc_captured_range
@@ -193,6 +198,26 @@ function Range.from_motion(motion, opts)
return captured_range
end
--- @param opts? { contains_cursor?: boolean }
function Range.from_tsquery_caps(bufnr, query, opts)
opts = opts or { contains_cursor = true }
local ranges = Range.from_buf_text(bufnr):tsquery(query)
if not ranges then return end
if not opts.contains_cursor then return ranges end
local cursor = Pos.from_pos '.'
return vim.tbl_map(function(cap_ranges)
return vim
.iter(cap_ranges)
:filter(
--- @param r u.Range
function(r) return r:contains(cursor) end
)
:totable()
end, ranges)
end
--- Get range information from the currently selected visual text.
--- Note: from within a command mapping or an opfunc, use other specialized
--- utilities, such as:
@@ -220,19 +245,22 @@ end
--- @param args unknown
--- @return u.Range|nil
function Range.from_cmd_args(args)
--- @type 'v'|'V'
local mode
--- @type nil|u.Pos
local start
local stop
if args.range == 0 then
return nil
else
start = Pos.from_pos "'<"
stop = Pos.from_pos "'>"
mode = stop:is_col_max() and 'V' or 'v'
if args.range == 0 then return nil end
local bufnr = vim.api.nvim_get_current_buf()
if args.range == 1 then
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line1, Pos.MAX_COL), 'V')
end
local is_visual = vim.fn.histget('cmd', -1):sub(1, 5) == [['<,'>]]
--- @type 'v'|'V'
local mode = is_visual and vim.fn.visualmode() or 'V'
if is_visual then
return Range.new(Pos.from_pos "'<", Pos.from_pos "'>", mode)
else
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line2, Pos.MAX_COL), mode)
end
return Range.new(start, stop, mode)
end
function Range.find_nearest_brackets()
@@ -272,6 +300,13 @@ function Range:to_linewise()
return r
end
function Range:to_charwise()
local r = self:clone()
r.mode = 'v'
if r.stop:is_col_max() then r.stop = r.stop:as_real() end
return r
end
--- @param x u.Pos | u.Range
function Range:contains(x)
if getmetatable(x) == Pos then
@@ -313,27 +348,19 @@ end
--- @param left string
--- @param right string
function Range:save_to_pos(left, right)
if self:is_empty() then
self.start:save_to_pos(left)
self.start:save_to_pos(right)
else
self.start:save_to_pos(left)
self.stop:save_to_pos(right)
end
self.start:save_to_pos(left);
(self:is_empty() and self.start or self.stop):save_to_pos(right)
end
--- @param left string
--- @param right string
function Range:save_to_marks(left, right)
if self:is_empty() then
self.start:save_to_mark(left)
self.start:save_to_mark(right)
else
self.start:save_to_mark(left)
self.stop:save_to_mark(right)
end
self.start:save_to_mark(left);
(self:is_empty() and self.start or self.stop):save_to_mark(right)
end
function Range:save_to_extmark() return Extmark.from_range(self, NS) end
function Range:set_visual_selection()
if self:is_empty() then return end
if vim.api.nvim_get_current_buf() ~= self.start.bufnr then
@@ -348,14 +375,46 @@ function Range:set_visual_selection()
self.stop:save_to_pos '.'
end
--------------------------------------------------------------------------------
-- Range.from_* functions:
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
-- Text access/manipulation utilities:
--------------------------------------------------------------------------------
--- @param query string
function Range:tsquery(query)
local bufnr = self.start.bufnr
local lang = vim.treesitter.language.get_lang(vim.bo[bufnr].filetype)
if lang == nil then return end
local parser = vim.treesitter.get_parser(bufnr, lang)
if parser == nil then return end
local tree = parser:parse()[1]
if tree == nil then return end
local root = tree:root()
local q = vim.treesitter.query.parse(lang, query)
--- @type table<string, u.Range[]>
local ranges = {}
for id, match, _meta in
q:iter_captures(root, bufnr, self.start.lnum - 1, (self.stop or self.start).lnum)
do
local start_row0, start_col0, stop_row0, stop_col0 = match:range()
local range = Range.new(
Pos.new(bufnr, start_row0 + 1, start_col0 + 1),
Pos.new(bufnr, stop_row0 + 1, stop_col0),
'v'
)
if range.stop.lnum > vim.api.nvim_buf_line_count(bufnr) then
range.stop = range.stop:must_next(-1)
end
local capture_name = q.captures[id]
if not ranges[capture_name] then ranges[capture_name] = {} end
if self:contains(range) then table.insert(ranges[capture_name], range) end
end
return ranges
end
function Range:length()
if self:is_empty() then return 0 end