range: extmarks/tsquery; renderer: text-change
This commit is contained in:
parent
6f86bfaa42
commit
06e6b88391
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -14,16 +14,16 @@ jobs:
|
|||||||
|
|
||||||
- name: Populate Nix store
|
- name: Populate Nix store
|
||||||
run:
|
run:
|
||||||
nix-shell --run 'true'
|
nix-shell --pure --run 'true'
|
||||||
|
|
||||||
- name: Type-check with lua-language-server
|
- name: Type-check with lua-language-server
|
||||||
run:
|
run:
|
||||||
nix-shell --run 'make lint'
|
nix-shell --pure --run 'make lint'
|
||||||
|
|
||||||
- name: Check formatting with stylua
|
- name: Check formatting with stylua
|
||||||
run:
|
run:
|
||||||
nix-shell --run 'make fmt-check'
|
nix-shell --pure --run 'make fmt-check'
|
||||||
|
|
||||||
- name: Run busted tests
|
- name: Run busted tests
|
||||||
run:
|
run:
|
||||||
nix-shell --run 'make test'
|
nix-shell --pure --run 'make test'
|
||||||
|
10
.woodpecker/ci.yaml
Normal file
10
.woodpecker/ci.yaml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build
|
||||||
|
image: nixos/nix
|
||||||
|
commands:
|
||||||
|
- nix-shell --pure --run 'make lint'
|
||||||
|
- nix-shell --pure --run 'make fmt-check'
|
||||||
|
- nix-shell --pure --run 'make test'
|
3
Makefile
3
Makefile
@ -19,3 +19,6 @@ test:
|
|||||||
@echo "## Generating coverage report"
|
@echo "## Generating coverage report"
|
||||||
@luacov
|
@luacov
|
||||||
@awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out
|
@awk '/^Summary$$/{flag=1;next} flag{print}' luacov.report.out
|
||||||
|
|
||||||
|
watch:
|
||||||
|
@watchexec -c -e lua make
|
||||||
|
@ -10,10 +10,10 @@
|
|||||||
-- change on the underlying filesystem.
|
-- change on the underlying filesystem.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias FsDir { kind: 'dir'; path: string; expanded: boolean; children: FsNode[] }
|
--- @alias u.examples.FsDir { kind: 'dir'; path: string; expanded: boolean; children: u.examples.FsNode[] }
|
||||||
--- @alias FsFile { kind: 'file'; path: string }
|
--- @alias u.examples.FsFile { kind: 'file'; path: string }
|
||||||
--- @alias FsNode FsDir | FsFile
|
--- @alias u.examples.FsNode u.examples.FsDir | u.examples.FsFile
|
||||||
--- @alias ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
--- @alias u.examples.ShowOpts { root_path?: string, width?: number, focus_path?: string }
|
||||||
|
|
||||||
local Buffer = require 'u.buffer'
|
local Buffer = require 'u.buffer'
|
||||||
local Renderer = require('u.renderer').Renderer
|
local Renderer = require('u.renderer').Renderer
|
||||||
@ -58,13 +58,13 @@ function H.relative(path, base)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param root_path string
|
--- @param root_path string
|
||||||
--- @return { tree: FsDir; path_to_node: table<string, FsNode> }
|
--- @return { tree: u.examples.FsDir; path_to_node: table<string, u.examples.FsNode> }
|
||||||
function H.get_tree_inf(root_path)
|
function H.get_tree_inf(root_path)
|
||||||
logger:info { 'get_tree_inf', root_path }
|
logger:info { 'get_tree_inf', root_path }
|
||||||
--- @type table<string, FsNode>
|
--- @type table<string, u.examples.FsNode>
|
||||||
local path_to_node = {}
|
local path_to_node = {}
|
||||||
|
|
||||||
--- @type FsDir
|
--- @type u.examples.FsDir
|
||||||
local tree = {
|
local tree = {
|
||||||
kind = 'dir',
|
kind = 'dir',
|
||||||
path = H.normalize(root_path or '.'),
|
path = H.normalize(root_path or '.'),
|
||||||
@ -77,8 +77,8 @@ function H.get_tree_inf(root_path)
|
|||||||
return { tree = tree, path_to_node = path_to_node }
|
return { tree = tree, path_to_node = path_to_node }
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param tree FsDir
|
--- @param tree u.examples.FsDir
|
||||||
--- @param path_to_node table<string, FsNode>
|
--- @param path_to_node table<string, u.examples.FsNode>
|
||||||
function H.populate_dir_children(tree, path_to_node)
|
function H.populate_dir_children(tree, path_to_node)
|
||||||
tree.children = {}
|
tree.children = {}
|
||||||
|
|
||||||
@ -135,7 +135,7 @@ local function _render_in_buffer(opts)
|
|||||||
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
local parts = H.split_path(H.relative(focused_path, tree_inf.tree.path))
|
||||||
local path_to_node = tree_inf.path_to_node
|
local path_to_node = tree_inf.path_to_node
|
||||||
|
|
||||||
--- @param node FsDir
|
--- @param node u.examples.FsDir
|
||||||
--- @param child_names string[]
|
--- @param child_names string[]
|
||||||
local function expand_to(node, child_names)
|
local function expand_to(node, child_names)
|
||||||
if #child_names == 0 then return end
|
if #child_names == 0 then return end
|
||||||
@ -310,7 +310,7 @@ local function _render_in_buffer(opts)
|
|||||||
--
|
--
|
||||||
local renderer = Renderer.new(opts.bufnr)
|
local renderer = Renderer.new(opts.bufnr)
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type { tree: FsDir; path_to_node: table<string, FsNode> }
|
--- @type { tree: u.examples.FsDir; path_to_node: table<string, u.examples.FsNode> }
|
||||||
local tree_inf = s_tree_inf:get()
|
local tree_inf = s_tree_inf:get()
|
||||||
local tree = tree_inf.tree
|
local tree = tree_inf.tree
|
||||||
|
|
||||||
@ -329,7 +329,7 @@ local function _render_in_buffer(opts)
|
|||||||
|
|
||||||
--- Since the filesystem is a recursive tree of nodes, we need to
|
--- Since the filesystem is a recursive tree of nodes, we need to
|
||||||
--- recursively render each node. This function does just that:
|
--- recursively render each node. This function does just that:
|
||||||
--- @param node FsNode
|
--- @param node u.examples.FsNode
|
||||||
--- @param level number
|
--- @param level number
|
||||||
local function render_node(node, level)
|
local function render_node(node, level)
|
||||||
local name = vim.fs.basename(node.path)
|
local name = vim.fs.basename(node.path)
|
||||||
@ -414,7 +414,7 @@ end
|
|||||||
local current_inf = nil
|
local current_inf = nil
|
||||||
|
|
||||||
--- Show the filetree:
|
--- Show the filetree:
|
||||||
--- @param opts? ShowOpts
|
--- @param opts? u.examples.ShowOpts
|
||||||
function M.show(opts)
|
function M.show(opts)
|
||||||
if current_inf ~= nil then return current_inf.controller end
|
if current_inf ~= nil then return current_inf.controller end
|
||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
@ -456,7 +456,7 @@ function M.hide()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Toggle the filetree:
|
--- Toggle the filetree:
|
||||||
--- @param opts? ShowOpts
|
--- @param opts? u.examples.ShowOpts
|
||||||
function M.toggle(opts)
|
function M.toggle(opts)
|
||||||
if current_inf == nil then
|
if current_inf == nil then
|
||||||
M.show(opts)
|
M.show(opts)
|
||||||
|
117
examples/form.lua
Normal file
117
examples/form.lua
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
-- form.lua:
|
||||||
|
--
|
||||||
|
-- This is a runnable example of a form. Open this file in Neovim, and execute
|
||||||
|
-- `:luafile %` to run it. It will create a new buffer to the side, and render
|
||||||
|
-- an interactive form. Edit the "inputs" between the `[...]` brackets, and
|
||||||
|
-- watch the buffer react immediately to your changes.
|
||||||
|
--
|
||||||
|
|
||||||
|
local Renderer = require('u.renderer').Renderer
|
||||||
|
local h = require('u.renderer').h
|
||||||
|
local tracker = require 'u.tracker'
|
||||||
|
|
||||||
|
-- Create a new, temporary, buffer to the side:
|
||||||
|
vim.cmd.vnew()
|
||||||
|
vim.bo.buftype = 'nofile'
|
||||||
|
vim.bo.bufhidden = 'wipe'
|
||||||
|
vim.bo.buflisted = false
|
||||||
|
local renderer = Renderer.new()
|
||||||
|
|
||||||
|
-- Create two signals:
|
||||||
|
local s_name = tracker.create_signal 'whoever-you-are'
|
||||||
|
local s_age = tracker.create_signal 'ideally-a-number'
|
||||||
|
|
||||||
|
-- We can create derived information from the signals above. Say we want to do
|
||||||
|
-- some validation on the input for `age`: we can do that with a memo:
|
||||||
|
local s_age_info = tracker.create_memo(function()
|
||||||
|
local age_raw = s_age:get()
|
||||||
|
local age_digits = age_raw:match '^%s*(%d+)%s*$'
|
||||||
|
local age_n = age_digits and tonumber(age_digits) or nil
|
||||||
|
return {
|
||||||
|
type = age_n and 'number' or 'string',
|
||||||
|
raw = age_raw,
|
||||||
|
n = age_n,
|
||||||
|
n1 = age_n and age_n + 1 or nil,
|
||||||
|
}
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- This is the render effect that depends on the signals created above. This
|
||||||
|
-- will re-run every time one of the signals changes.
|
||||||
|
tracker.create_effect(function()
|
||||||
|
local name = s_name:get()
|
||||||
|
local age = s_age:get()
|
||||||
|
local age_info = s_age_info:get()
|
||||||
|
|
||||||
|
-- Each time the signals change, we re-render the buffer:
|
||||||
|
renderer:render {
|
||||||
|
h.Type({}, '# Form Example'),
|
||||||
|
'\n\n',
|
||||||
|
|
||||||
|
-- We can also listen for when specific locations in the buffer change, on
|
||||||
|
-- a tag-by-tag basis. This gives us two-way data-binding between the
|
||||||
|
-- buffer and the signals.
|
||||||
|
{
|
||||||
|
'Name: ',
|
||||||
|
h.Structure({
|
||||||
|
on_change = function(text) s_name:set(text) end,
|
||||||
|
}, name),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'\nAge: ',
|
||||||
|
h.Structure({
|
||||||
|
on_change = function(text) s_age:set(text) end,
|
||||||
|
}, age),
|
||||||
|
},
|
||||||
|
|
||||||
|
'\n\n',
|
||||||
|
|
||||||
|
-- Show the values of the signals here, too, so that we can see the
|
||||||
|
-- reactivity in action. If you change the values in the tags above, you
|
||||||
|
-- can see the changes reflected here immediately.
|
||||||
|
{ 'Hello, "', name, '"!' },
|
||||||
|
|
||||||
|
--
|
||||||
|
-- A more complex example: we can do much more complex rendering, based on
|
||||||
|
-- the state. For example, if you type different values into the `age`
|
||||||
|
-- field, you can see not only the displayed information change, but also
|
||||||
|
-- the color of the highlights in this section will adapt to the type of
|
||||||
|
-- information that has been detected.
|
||||||
|
--
|
||||||
|
-- If string input is detected, values below are shown in the
|
||||||
|
-- `String`/`ErrorMsg` highlight groups.
|
||||||
|
--
|
||||||
|
-- If number input is detected, values below are shown in the `Number`
|
||||||
|
-- highlight group.
|
||||||
|
--
|
||||||
|
-- If a valid number is entered, then this section also displays how old
|
||||||
|
-- you willl be next year (`n + 1`).
|
||||||
|
--
|
||||||
|
|
||||||
|
'\n\n',
|
||||||
|
h.Type({}, '## Computed Information (derived from `age`)'),
|
||||||
|
'\n\n',
|
||||||
|
{
|
||||||
|
'Type: ',
|
||||||
|
h('text', {
|
||||||
|
hl = age_info.type == 'number' and 'Number' or 'String',
|
||||||
|
}, age_info.type),
|
||||||
|
},
|
||||||
|
{ '\nRaw input: ', h.String({}, '"' .. age_info.raw .. '"') },
|
||||||
|
{
|
||||||
|
'\nCurrent age: ',
|
||||||
|
age_info.n
|
||||||
|
-- Show the age:
|
||||||
|
and h.Number({}, tostring(age_info.n))
|
||||||
|
-- Show an error-placeholder if the age is invalid:
|
||||||
|
or h.ErrorMsg({}, '(?)'),
|
||||||
|
},
|
||||||
|
|
||||||
|
-- This part is shown conditionally, i.e., only if the age next year can be
|
||||||
|
-- computed:
|
||||||
|
age_info.n1
|
||||||
|
and {
|
||||||
|
'\nAge next year: ',
|
||||||
|
h.Number({}, tostring(age_info.n1)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
end)
|
@ -14,7 +14,7 @@ local ICONS = {
|
|||||||
}
|
}
|
||||||
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
local DEFAULT_ICON = { text = '', group = 'DiagnosticSignOk' }
|
||||||
|
|
||||||
--- @alias Notification {
|
--- @alias u.examples.Notification {
|
||||||
--- kind: number;
|
--- kind: number;
|
||||||
--- id: number;
|
--- id: number;
|
||||||
--- text: string;
|
--- text: string;
|
||||||
@ -30,7 +30,7 @@ local s_notifications = s_notifications_raw:debounce(50)
|
|||||||
|
|
||||||
-- Render effect:
|
-- Render effect:
|
||||||
tracker.create_effect(function()
|
tracker.create_effect(function()
|
||||||
--- @type Notification[]
|
--- @type u.examples.Notification[]
|
||||||
local notifs = s_notifications:get()
|
local notifs = s_notifications:get()
|
||||||
|
|
||||||
if #notifs == 0 then
|
if #notifs == 0 then
|
||||||
@ -99,14 +99,14 @@ local function my_notify(msg, level, opts)
|
|||||||
|
|
||||||
local id = math.random(math.huge)
|
local id = math.random(math.huge)
|
||||||
|
|
||||||
--- @param notifs Notification[]
|
--- @param notifs u.examples.Notification[]
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
table.insert(notifs, { kind = level, id = id, text = msg })
|
table.insert(notifs, { kind = level, id = id, text = msg })
|
||||||
return notifs
|
return notifs
|
||||||
end)
|
end)
|
||||||
|
|
||||||
vim.defer_fn(function()
|
vim.defer_fn(function()
|
||||||
--- @param notifs Notification[]
|
--- @param notifs u.examples.Notification[]
|
||||||
s_notifications_raw:schedule_update(function(notifs)
|
s_notifications_raw:schedule_update(function(notifs)
|
||||||
for i, notif in ipairs(notifs) do
|
for i, notif in ipairs(notifs) do
|
||||||
if notif.id == id then
|
if notif.id == id then
|
||||||
|
@ -44,7 +44,7 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
|
|||||||
-- shortest portion of this function.
|
-- shortest portion of this function.
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
--- @alias SelectController {
|
--- @alias u.examples.SelectController {
|
||||||
--- get_items: fun(): T[];
|
--- get_items: fun(): T[];
|
||||||
--- set_items: fun(items: T[]);
|
--- set_items: fun(items: T[]);
|
||||||
--- set_filter_text: fun(filter_text: string);
|
--- set_filter_text: fun(filter_text: string);
|
||||||
@ -53,17 +53,17 @@ local function shallow_copy_arr(arr) return vim.iter(arr):totable() end
|
|||||||
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
|
--- set_selected_indices: fun(indicies: number[], ephemeral?: boolean);
|
||||||
--- close: fun();
|
--- close: fun();
|
||||||
--- }
|
--- }
|
||||||
--- @alias SelectOpts<T> {
|
--- @alias u.examples.SelectOpts<T> {
|
||||||
--- items: `T`[];
|
--- items: `T`[];
|
||||||
--- multi?: boolean;
|
--- multi?: boolean;
|
||||||
--- format_item?: fun(item: T): Tree;
|
--- format_item?: fun(item: T): u.renderer.Tree;
|
||||||
--- on_finish?: fun(items: T[], indicies: number[]);
|
--- on_finish?: fun(items: T[], indicies: number[]);
|
||||||
--- on_selection_changed?: fun(items: T[], indicies: number[]);
|
--- on_selection_changed?: fun(items: T[], indicies: number[]);
|
||||||
--- mappings?: table<string, fun(select: SelectController)>;
|
--- mappings?: table<string, fun(select: u.examples.SelectController)>;
|
||||||
--- }
|
--- }
|
||||||
|
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param opts SelectOpts<T>
|
--- @param opts u.examples.SelectOpts<T>
|
||||||
function M.create_picker(opts) -- {{{
|
function M.create_picker(opts) -- {{{
|
||||||
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
local is_in_insert_mode = vim.api.nvim_get_mode().mode:sub(1, 1) == 'i'
|
||||||
local stopinsert = not is_in_insert_mode
|
local stopinsert = not is_in_insert_mode
|
||||||
@ -557,7 +557,7 @@ function M.create_picker(opts) -- {{{
|
|||||||
return safe_run(function() H.finish(true) end)
|
return safe_run(function() H.finish(true) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return controller --[[@as SelectController]]
|
return controller --[[@as u.examples.SelectController]]
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
@ -5,7 +5,7 @@ local Renderer = require('u.renderer').Renderer
|
|||||||
--- @field bufnr number
|
--- @field bufnr number
|
||||||
--- @field b vim.var_accessor
|
--- @field b vim.var_accessor
|
||||||
--- @field bo vim.bo
|
--- @field bo vim.bo
|
||||||
--- @field private renderer u.Renderer
|
--- @field renderer u.renderer.Renderer
|
||||||
local Buffer = {}
|
local Buffer = {}
|
||||||
Buffer.__index = Buffer
|
Buffer.__index = Buffer
|
||||||
|
|
||||||
@ -75,6 +75,9 @@ function Buffer:autocmd(event, opts)
|
|||||||
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.bufnr }))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param fn function
|
||||||
|
function Buffer:call(fn) return vim.api.nvim_buf_call(self.bufnr, fn) end
|
||||||
|
|
||||||
--- @param tree u.renderer.Tree
|
--- @param tree u.renderer.Tree
|
||||||
function Buffer:render(tree) return self.renderer:render(tree) end
|
function Buffer:render(tree) return self.renderer:render(tree) end
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ end
|
|||||||
--- @param line string
|
--- @param line string
|
||||||
--- @param bufnr? number
|
--- @param bufnr? number
|
||||||
function CodeWriter.from_line(line, bufnr)
|
function CodeWriter.from_line(line, bufnr)
|
||||||
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
local ws = line:match '^%s*'
|
local ws = line:match '^%s*'
|
||||||
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
local expandtab = vim.api.nvim_get_option_value('expandtab', { buf = bufnr })
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
local LOG_ROOT = vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log')
|
||||||
|
|
||||||
--- @params name string
|
--- @params name string
|
||||||
function M.file_for_name(name)
|
function M.file_for_name(name) return vim.fs.joinpath(LOG_ROOT, name .. '.log.jsonl') end
|
||||||
return vim.fs.joinpath(vim.fn.stdpath 'cache', 'u.log', name .. '.log.jsonl')
|
|
||||||
end
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Logger class
|
-- Logger class
|
||||||
@ -11,7 +11,6 @@ end
|
|||||||
|
|
||||||
--- @class u.Logger
|
--- @class u.Logger
|
||||||
--- @field name string
|
--- @field name string
|
||||||
--- @field private fd number
|
|
||||||
local Logger = {}
|
local Logger = {}
|
||||||
Logger.__index = Logger
|
Logger.__index = Logger
|
||||||
M.Logger = Logger
|
M.Logger = Logger
|
||||||
@ -20,10 +19,7 @@ M.Logger = Logger
|
|||||||
function Logger.new(name)
|
function Logger.new(name)
|
||||||
local file_path = M.file_for_name(name)
|
local file_path = M.file_for_name(name)
|
||||||
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
vim.fn.mkdir(vim.fs.dirname(file_path), 'p')
|
||||||
local self = setmetatable({
|
local self = setmetatable({ name = name }, Logger)
|
||||||
name = name,
|
|
||||||
fd = (vim.uv or vim.loop).fs_open(file_path, 'a', tonumber('644', 8)),
|
|
||||||
}, Logger)
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -32,10 +28,12 @@ end
|
|||||||
function Logger:write(level, ...)
|
function Logger:write(level, ...)
|
||||||
local data = { ... }
|
local data = { ... }
|
||||||
if #data == 1 then data = data[1] end
|
if #data == 1 then data = data[1] end
|
||||||
(vim.uv or vim.loop).fs_write(
|
local f = assert(io.open(M.file_for_name(self.name), 'a'), 'could not open file')
|
||||||
self.fd,
|
assert(
|
||||||
vim.json.encode { ts = os.date(), level = level, data = data } .. '\n'
|
f:write(vim.json.encode { ts = os.date(), level = level, data = data } .. '\n'),
|
||||||
|
'could not write to file'
|
||||||
)
|
)
|
||||||
|
f:close()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Logger:trace(...) self:write('INFO', ...) end
|
function Logger:trace(...) self:write('INFO', ...) end
|
||||||
@ -64,6 +62,12 @@ function M.setup()
|
|||||||
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
vim.cmd.terminal('tail -f "' .. log_file_path .. '"')
|
||||||
vim.cmd.startinsert()
|
vim.cmd.startinsert()
|
||||||
end, { nargs = '*' })
|
end, { nargs = '*' })
|
||||||
|
|
||||||
|
vim.api.nvim_create_user_command(
|
||||||
|
'Logroot',
|
||||||
|
function() vim.api.nvim_echo({ { LOG_ROOT } }, false, {}) end,
|
||||||
|
{}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
@ -67,6 +67,15 @@ function Pos.__sub(x, y)
|
|||||||
return x:next(-y)
|
return x:next(-y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param bufnr number
|
||||||
|
--- @param lnum number
|
||||||
|
function Pos.from_eol(bufnr, lnum)
|
||||||
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
local pos = Pos.new(bufnr, lnum, 0)
|
||||||
|
pos.col = pos:line():len()
|
||||||
|
return pos
|
||||||
|
end
|
||||||
|
|
||||||
--- @param name string
|
--- @param name string
|
||||||
--- @return u.Pos
|
--- @return u.Pos
|
||||||
function Pos.from_pos(name)
|
function Pos.from_pos(name)
|
||||||
@ -96,6 +105,8 @@ end
|
|||||||
|
|
||||||
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
function Pos:as_vim() return { self.bufnr, self.lnum, self.col, self.off } end
|
||||||
|
|
||||||
|
function Pos:eol() return Pos.from_eol(self.bufnr, self.lnum) end
|
||||||
|
|
||||||
--- @param pos string
|
--- @param pos string
|
||||||
function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
|
function Pos:save_to_pos(pos) vim.fn.setpos(pos, { self.bufnr, self.lnum, self.col, self.off }) end
|
||||||
|
|
||||||
|
180
lua/u/range.lua
180
lua/u/range.lua
@ -1,6 +1,13 @@
|
|||||||
local Pos = require 'u.pos'
|
local Pos = require 'u.pos'
|
||||||
|
|
||||||
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
local ESC = vim.api.nvim_replace_termcodes('<Esc>', true, false, true)
|
||||||
|
local NS = vim.api.nvim_create_namespace 'u.range'
|
||||||
|
|
||||||
|
---@class u.ExtmarkRange
|
||||||
|
---@field bufnr number
|
||||||
|
---@field id number
|
||||||
|
local ExtmarkRange = {}
|
||||||
|
ExtmarkRange.__index = ExtmarkRange
|
||||||
|
|
||||||
--- @class u.Range
|
--- @class u.Range
|
||||||
--- @field start u.Pos
|
--- @field start u.Pos
|
||||||
@ -83,6 +90,34 @@ function Range.from_marks(lpos, rpos)
|
|||||||
return Range.new(start, stop, mode)
|
return Range.new(start, stop, mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @param bufnr number
|
||||||
|
--- @param extmark vim.api.keyset.get_extmark_item_by_id
|
||||||
|
function Range.from_extmark(bufnr, extmark)
|
||||||
|
---@type integer, integer, vim.api.keyset.extmark_details | nil
|
||||||
|
local start_row0, start_col0, details = unpack(extmark)
|
||||||
|
|
||||||
|
local start = Pos.new(bufnr, start_row0 + 1, start_col0 + 1)
|
||||||
|
local stop = details and Pos.new(bufnr, details.end_row + 1, details.end_col)
|
||||||
|
|
||||||
|
if stop ~= nil then
|
||||||
|
-- Check for invalid extmark range:
|
||||||
|
if stop < start then return Range.new(stop) end
|
||||||
|
|
||||||
|
-- Check for stop-mark past the end of the buffer:
|
||||||
|
local buf_max_lines = vim.api.nvim_buf_line_count(bufnr)
|
||||||
|
if stop.lnum > buf_max_lines then
|
||||||
|
stop.lnum = buf_max_lines
|
||||||
|
stop = stop:eol()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- A stop mark at position 0 means it is at the end of the last line.
|
||||||
|
-- Move it back.
|
||||||
|
if stop.col == 0 then stop = stop:must_next(-1) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Range.new(start, stop, 'v')
|
||||||
|
end
|
||||||
|
|
||||||
--- @param bufnr? number
|
--- @param bufnr? number
|
||||||
function Range.from_buf_text(bufnr)
|
function Range.from_buf_text(bufnr)
|
||||||
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
@ -115,7 +150,7 @@ end
|
|||||||
function Range.from_motion(motion, opts)
|
function Range.from_motion(motion, opts)
|
||||||
-- Options handling:
|
-- Options handling:
|
||||||
opts = opts or {}
|
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.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
|
||||||
|
|
||||||
@ -146,6 +181,8 @@ function Range.from_motion(motion, opts)
|
|||||||
_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
|
||||||
|
local old_eventignore = vim.o.eventignore
|
||||||
|
vim.o.eventignore = 'all'
|
||||||
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
vim.go.operatorfunc = 'v:lua.Range__from_motion_opfunc'
|
||||||
vim.cmd {
|
vim.cmd {
|
||||||
cmd = 'normal',
|
cmd = 'normal',
|
||||||
@ -153,6 +190,7 @@ function Range.from_motion(motion, opts)
|
|||||||
args = { ESC .. 'g@' .. motion },
|
args = { ESC .. 'g@' .. motion },
|
||||||
mods = { silent = true },
|
mods = { silent = true },
|
||||||
}
|
}
|
||||||
|
vim.o.eventignore = old_eventignore
|
||||||
end)
|
end)
|
||||||
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
local captured_range = _G.Range__from_motion_opfunc_captured_range
|
||||||
|
|
||||||
@ -193,6 +231,26 @@ function Range.from_motion(motion, opts)
|
|||||||
return captured_range
|
return captured_range
|
||||||
end
|
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.
|
--- Get range information from the currently selected visual text.
|
||||||
--- Note: from within a command mapping or an opfunc, use other specialized
|
--- Note: from within a command mapping or an opfunc, use other specialized
|
||||||
--- utilities, such as:
|
--- utilities, such as:
|
||||||
@ -220,19 +278,22 @@ end
|
|||||||
--- @param args unknown
|
--- @param args unknown
|
||||||
--- @return u.Range|nil
|
--- @return u.Range|nil
|
||||||
function Range.from_cmd_args(args)
|
function Range.from_cmd_args(args)
|
||||||
--- @type 'v'|'V'
|
if args.range == 0 then return nil end
|
||||||
local mode
|
|
||||||
--- @type nil|u.Pos
|
local bufnr = vim.api.nvim_get_current_buf()
|
||||||
local start
|
if args.range == 1 then
|
||||||
local stop
|
return Range.new(Pos.new(bufnr, args.line1, 1), Pos.new(bufnr, args.line1, Pos.MAX_COL), 'V')
|
||||||
if args.range == 0 then
|
end
|
||||||
return nil
|
|
||||||
else
|
local is_visual = vim.fn.histget('cmd', -1):sub(1, 5) == [['<,'>]]
|
||||||
start = Pos.from_pos "'<"
|
--- @type 'v'|'V'
|
||||||
stop = Pos.from_pos "'>"
|
local mode = is_visual and vim.fn.visualmode() or 'V'
|
||||||
mode = stop:is_col_max() and 'V' 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
|
end
|
||||||
return Range.new(start, stop, mode)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range.find_nearest_brackets()
|
function Range.find_nearest_brackets()
|
||||||
@ -272,6 +333,13 @@ function Range:to_linewise()
|
|||||||
return r
|
return r
|
||||||
end
|
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
|
--- @param x u.Pos | u.Range
|
||||||
function Range:contains(x)
|
function Range:contains(x)
|
||||||
if getmetatable(x) == Pos then
|
if getmetatable(x) == Pos then
|
||||||
@ -313,25 +381,32 @@ end
|
|||||||
--- @param left string
|
--- @param left string
|
||||||
--- @param right string
|
--- @param right string
|
||||||
function Range:save_to_pos(left, right)
|
function Range:save_to_pos(left, right)
|
||||||
if self:is_empty() then
|
self.start:save_to_pos(left);
|
||||||
self.start:save_to_pos(left)
|
(self:is_empty() and self.start or self.stop):save_to_pos(right)
|
||||||
self.start:save_to_pos(right)
|
|
||||||
else
|
|
||||||
self.start:save_to_pos(left)
|
|
||||||
self.stop:save_to_pos(right)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param left string
|
--- @param left string
|
||||||
--- @param right string
|
--- @param right string
|
||||||
function Range:save_to_marks(left, right)
|
function Range:save_to_marks(left, right)
|
||||||
if self:is_empty() then
|
self.start:save_to_mark(left);
|
||||||
self.start:save_to_mark(left)
|
(self:is_empty() and self.start or self.stop):save_to_mark(right)
|
||||||
self.start:save_to_mark(right)
|
|
||||||
else
|
|
||||||
self.start:save_to_mark(left)
|
|
||||||
self.stop:save_to_mark(right)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Range:save_to_extmark()
|
||||||
|
local r = self:to_charwise()
|
||||||
|
local end_row = r.stop.lnum - 1
|
||||||
|
local end_col = r.stop.col
|
||||||
|
if self.mode == 'V' then
|
||||||
|
end_row = end_row + 1
|
||||||
|
end_col = 0
|
||||||
|
end
|
||||||
|
local id = vim.api.nvim_buf_set_extmark(r.start.bufnr, NS, r.start.lnum - 1, r.start.col - 1, {
|
||||||
|
right_gravity = false,
|
||||||
|
end_right_gravity = true,
|
||||||
|
end_row = end_row,
|
||||||
|
end_col = end_col,
|
||||||
|
})
|
||||||
|
return ExtmarkRange.new(r.start.bufnr, id)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Range:set_visual_selection()
|
function Range:set_visual_selection()
|
||||||
@ -348,14 +423,46 @@ function Range:set_visual_selection()
|
|||||||
self.stop:save_to_pos '.'
|
self.stop:save_to_pos '.'
|
||||||
end
|
end
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
-- Range.from_* functions:
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Text access/manipulation utilities:
|
-- 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()
|
function Range:length()
|
||||||
if self:is_empty() then return 0 end
|
if self:is_empty() then return 0 end
|
||||||
|
|
||||||
@ -596,4 +703,17 @@ function Range:highlight(group, opts)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function ExtmarkRange.new(bufnr, id) return setmetatable({ bufnr = bufnr, id = id }, ExtmarkRange) end
|
||||||
|
|
||||||
|
function ExtmarkRange:range()
|
||||||
|
return Range.from_extmark(
|
||||||
|
self.bufnr,
|
||||||
|
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, NS, self.id, {
|
||||||
|
details = true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
function ExtmarkRange:delete() vim.api.nvim_buf_del_extmark(self.bufnr, NS, self.id) end
|
||||||
|
|
||||||
return Range
|
return Range
|
||||||
|
@ -1,12 +1,23 @@
|
|||||||
|
function _G.URendererOpFuncSwallow() end
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
local H = {}
|
local H = {}
|
||||||
|
|
||||||
--- @alias u.renderer.Tag { kind: 'tag'; name: string, attributes: table<string, unknown>, children: u.renderer.Tree }
|
--- @alias u.renderer.TagEventHandler fun(tag: u.renderer.Tag, mode: string, lhs: string): string
|
||||||
|
|
||||||
|
--- @alias u.renderer.TagAttributes { [string]?: unknown; imap?: table<string, u.renderer.TagEventHandler>; nmap?: table<string, u.renderer.TagEventHandler>; vmap?: table<string, u.renderer.TagEventHandler>; xmap?: table<string, u.renderer.TagEventHandler>; omap?: table<string, u.renderer.TagEventHandler>, on_change?: fun(text: string): unknown }
|
||||||
|
|
||||||
|
--- @class u.renderer.Tag
|
||||||
|
--- @field kind 'tag'
|
||||||
|
--- @field name string
|
||||||
|
--- @field attributes u.renderer.TagAttributes
|
||||||
|
--- @field children u.renderer.Tree
|
||||||
|
|
||||||
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
|
--- @alias u.renderer.Node nil | boolean | string | u.renderer.Tag
|
||||||
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
--- @alias u.renderer.Tree u.renderer.Node | u.renderer.Node[]
|
||||||
|
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
--- @type table<string, fun(attributes: table<string, any>, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: table<string, any>, children: u.renderer.Tree): u.renderer.Tag>
|
--- @type table<string, fun(attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag> & fun(name: string, attributes: u.renderer.TagAttributes, children: u.renderer.Tree): u.renderer.Tag>
|
||||||
M.h = setmetatable({}, {
|
M.h = setmetatable({}, {
|
||||||
__call = function(_, name, attributes, children)
|
__call = function(_, name, attributes, children)
|
||||||
return {
|
return {
|
||||||
@ -17,7 +28,6 @@ M.h = setmetatable({}, {
|
|||||||
}
|
}
|
||||||
end,
|
end,
|
||||||
__index = function(_, name)
|
__index = function(_, name)
|
||||||
-- vim.print('dynamic hl ' .. name)
|
|
||||||
return function(attributes, children)
|
return function(attributes, children)
|
||||||
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
return M.h('text', vim.tbl_deep_extend('force', { hl = name }, attributes), children)
|
||||||
end
|
end
|
||||||
@ -25,14 +35,19 @@ M.h = setmetatable({}, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
-- Renderer {{{
|
-- Renderer {{{
|
||||||
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any }
|
--- @class u.renderer.RendererExtmark
|
||||||
|
--- @field id? number
|
||||||
|
--- @field start [number, number]
|
||||||
|
--- @field stop [number, number]
|
||||||
|
--- @field opts vim.api.keyset.set_extmark
|
||||||
|
--- @field tag u.renderer.Tag
|
||||||
|
|
||||||
--- @class u.Renderer
|
--- @class u.renderer.Renderer
|
||||||
--- @field bufnr number
|
--- @field bufnr number
|
||||||
--- @field ns number
|
--- @field ns number
|
||||||
--- @field changedtick number
|
--- @field changedtick number
|
||||||
--- @field old { lines: string[]; extmarks: RendererExtmark[] }
|
--- @field old { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
|
||||||
--- @field curr { lines: string[]; extmarks: RendererExtmark[] }
|
--- @field curr { lines: string[]; extmarks: u.renderer.RendererExtmark[] }
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
M.Renderer = Renderer
|
M.Renderer = Renderer
|
||||||
@ -51,7 +66,7 @@ function Renderer.is_tag_arr(x)
|
|||||||
end
|
end
|
||||||
--- @param bufnr number|nil
|
--- @param bufnr number|nil
|
||||||
function Renderer.new(bufnr) -- {{{
|
function Renderer.new(bufnr) -- {{{
|
||||||
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
|
if bufnr == nil or bufnr == 0 then bufnr = vim.api.nvim_get_current_buf() end
|
||||||
|
|
||||||
if vim.b[bufnr]._renderer_ns == nil then
|
if vim.b[bufnr]._renderer_ns == nil then
|
||||||
vim.b[bufnr]._renderer_ns = vim.api.nvim_create_namespace('my.renderer:' .. tostring(bufnr))
|
vim.b[bufnr]._renderer_ns = vim.api.nvim_create_namespace('my.renderer:' .. tostring(bufnr))
|
||||||
@ -64,6 +79,12 @@ function Renderer.new(bufnr) -- {{{
|
|||||||
old = { lines = {}, extmarks = {} },
|
old = { lines = {}, extmarks = {} },
|
||||||
curr = { lines = {}, extmarks = {} },
|
curr = { lines = {}, extmarks = {} },
|
||||||
}, Renderer)
|
}, Renderer)
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI', 'TextChangedP' }, {
|
||||||
|
buffer = bufnr,
|
||||||
|
callback = function() self:_on_text_changed() end,
|
||||||
|
})
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
@ -128,7 +149,7 @@ function Renderer.markup_to_lines(opts) -- {{{
|
|||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @param opts {
|
--- @param opts {
|
||||||
--- tree: string;
|
--- tree: u.renderer.Tree;
|
||||||
--- format_tag?: fun(tag: u.renderer.Tag): string;
|
--- format_tag?: fun(tag: u.renderer.Tag): string;
|
||||||
--- }
|
--- }
|
||||||
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_lines(opts), '\n') end
|
||||||
@ -136,7 +157,7 @@ function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_
|
|||||||
--- @param bufnr number
|
--- @param bufnr number
|
||||||
--- @param old_lines string[] | nil
|
--- @param old_lines string[] | nil
|
||||||
--- @param new_lines string[]
|
--- @param new_lines string[]
|
||||||
function Renderer.patch_lines(bufnr, old_lines, new_lines)
|
function Renderer.patch_lines(bufnr, old_lines, new_lines) -- {{{
|
||||||
--
|
--
|
||||||
-- Helpers:
|
-- Helpers:
|
||||||
--
|
--
|
||||||
@ -189,7 +210,7 @@ function Renderer.patch_lines(bufnr, old_lines, new_lines)
|
|||||||
-- No change
|
-- No change
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end -- }}}
|
||||||
|
|
||||||
--- @param tree u.renderer.Tree
|
--- @param tree u.renderer.Tree
|
||||||
function Renderer:render(tree) -- {{{
|
function Renderer:render(tree) -- {{{
|
||||||
@ -199,7 +220,7 @@ function Renderer:render(tree) -- {{{
|
|||||||
self.changedtick = changedtick
|
self.changedtick = changedtick
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type RendererExtmark[]
|
--- @type u.renderer.RendererExtmark[]
|
||||||
local extmarks = {}
|
local extmarks = {}
|
||||||
|
|
||||||
--- @type string[]
|
--- @type string[]
|
||||||
@ -214,32 +235,37 @@ function Renderer:render(tree) -- {{{
|
|||||||
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
|
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
|
||||||
end
|
end
|
||||||
|
|
||||||
local extmark = tag.attributes.extmark
|
local extmark_opts = tag.attributes.extmark or {}
|
||||||
|
|
||||||
-- Set any necessary keymaps:
|
-- Set any necessary keymaps:
|
||||||
for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do
|
for _, mode in ipairs { 'i', 'n', 'v', 'x', 'o' } do
|
||||||
for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do
|
for lhs, _ in pairs(tag.attributes[mode .. 'map'] or {}) do
|
||||||
-- Force creating an extmark if there are key handlers. To accurately
|
-- Force creating an extmark if there are key handlers. To accurately
|
||||||
-- sense the bounds of the text, we need an extmark:
|
-- sense the bounds of the text, we need an extmark:
|
||||||
extmark = extmark or {}
|
vim.keymap.set(mode, lhs, function()
|
||||||
vim.keymap.set(
|
local result = self:_expr_map_callback(mode, lhs)
|
||||||
'n',
|
-- If the handler indicates that it wants to swallow the event,
|
||||||
lhs,
|
-- we have to convert that intention into something compatible
|
||||||
function() return self:_expr_map_callback('n', lhs) end,
|
-- with expr-mappings, which don't support '<Nop>' (they try to
|
||||||
{ buffer = self.bufnr, expr = true, replace_keycodes = true }
|
-- execute the literal characters). We'll use the 'g@' operator
|
||||||
)
|
-- to do that, forwarding the event to an operatorfunc that does
|
||||||
|
-- nothing:
|
||||||
|
if result == '' then
|
||||||
|
vim.go.operatorfunc = 'v:lua.URendererOpFuncSwallow'
|
||||||
|
return 'g@ '
|
||||||
|
end
|
||||||
|
return result
|
||||||
|
end, { buffer = self.bufnr, expr = true, replace_keycodes = true })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if extmark then
|
|
||||||
table.insert(extmarks, {
|
table.insert(extmarks, {
|
||||||
start = start0,
|
start = start0,
|
||||||
stop = stop0,
|
stop = stop0,
|
||||||
opts = extmark,
|
opts = extmark_opts,
|
||||||
tag = tag,
|
tag = tag,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end, -- }}}
|
end, -- }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,25 +274,6 @@ function Renderer:render(tree) -- {{{
|
|||||||
self:_reconcile()
|
self:_reconcile()
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
--- @private
|
|
||||||
--- @param start integer
|
|
||||||
--- @param end_ integer
|
|
||||||
--- @param strict_indexing boolean
|
|
||||||
--- @param replacement string[]
|
|
||||||
function Renderer:_set_lines(start, end_, strict_indexing, replacement)
|
|
||||||
vim.api.nvim_buf_set_lines(self.bufnr, start, end_, strict_indexing, replacement)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @private
|
|
||||||
--- @param start_row integer
|
|
||||||
--- @param start_col integer
|
|
||||||
--- @param end_row integer
|
|
||||||
--- @param end_col integer
|
|
||||||
--- @param replacement string[]
|
|
||||||
function Renderer:_set_text(start_row, start_col, end_row, end_col, replacement)
|
|
||||||
vim.api.nvim_buf_set_text(self.bufnr, start_row, start_col, end_row, end_col, replacement)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
function Renderer:_reconcile() -- {{{
|
function Renderer:_reconcile() -- {{{
|
||||||
--
|
--
|
||||||
@ -277,10 +284,15 @@ function Renderer:_reconcile() -- {{{
|
|||||||
|
|
||||||
--
|
--
|
||||||
-- Step 2: reconcile extmarks:
|
-- Step 2: reconcile extmarks:
|
||||||
|
-- You may be tempted to try to keep track of which extmarks are needed, and
|
||||||
|
-- only delete those that are not needed. However, each time a tree is
|
||||||
|
-- rendered, brand new extmarks are created. For simplicity, it is better to
|
||||||
|
-- just delete all extmarks, and recreate them.
|
||||||
--
|
--
|
||||||
|
|
||||||
-- Clear current extmarks:
|
-- Clear current extmarks:
|
||||||
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
||||||
|
|
||||||
-- Set current extmarks:
|
-- Set current extmarks:
|
||||||
for _, extmark in ipairs(self.curr.extmarks) do
|
for _, extmark in ipairs(self.curr.extmarks) do
|
||||||
extmark.id = vim.api.nvim_buf_set_extmark(
|
extmark.id = vim.api.nvim_buf_set_extmark(
|
||||||
@ -292,6 +304,13 @@ function Renderer:_reconcile() -- {{{
|
|||||||
id = extmark.id,
|
id = extmark.id,
|
||||||
end_row = extmark.stop[1],
|
end_row = extmark.stop[1],
|
||||||
end_col = extmark.stop[2],
|
end_col = extmark.stop[2],
|
||||||
|
-- If we change the text starting from the beginning (where the extmark
|
||||||
|
-- is), we don't want the extmark to move to the right.
|
||||||
|
right_gravity = false,
|
||||||
|
-- If we change the text starting from the end (where the end extmark
|
||||||
|
-- is), we don't want the extmark to stay stationary: we want it to
|
||||||
|
-- move to the right.
|
||||||
|
end_right_gravity = true,
|
||||||
}, extmark.opts)
|
}, extmark.opts)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@ -306,7 +325,7 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
|||||||
-- find the tag with the smallest intersection that contains the cursor:
|
-- find the tag with the smallest intersection that contains the cursor:
|
||||||
local pos0 = vim.api.nvim_win_get_cursor(0)
|
local pos0 = vim.api.nvim_win_get_cursor(0)
|
||||||
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
||||||
local pos_infos = self:get_pos_infos(pos0)
|
local pos_infos = self:get_tags_at(pos0)
|
||||||
|
|
||||||
if #pos_infos == 0 then return lhs end
|
if #pos_infos == 0 then return lhs end
|
||||||
|
|
||||||
@ -316,9 +335,10 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
|||||||
local tag = pos_info.tag
|
local tag = pos_info.tag
|
||||||
|
|
||||||
-- is the tag listening?
|
-- is the tag listening?
|
||||||
|
--- @type u.renderer.TagEventHandler?
|
||||||
local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs)
|
local f = vim.tbl_get(tag.attributes, mode .. 'map', lhs)
|
||||||
if type(f) == 'function' then
|
if type(f) == 'function' then
|
||||||
local result = f()
|
local result = f(tag, mode, lhs)
|
||||||
if result == '' then
|
if result == '' then
|
||||||
-- bubble-up to the next tag, but set cancel to true, in case there are
|
-- bubble-up to the next tag, but set cancel to true, in case there are
|
||||||
-- no more tags to bubble up to:
|
-- no more tags to bubble up to:
|
||||||
@ -333,21 +353,145 @@ function Renderer:_expr_map_callback(mode, lhs) -- {{{
|
|||||||
return cancel and '' or lhs
|
return cancel and '' or lhs
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
|
function Renderer:_on_text_changed() -- {{{
|
||||||
|
-- Reset changedtick, so that the reconciler knows to refresh its cached
|
||||||
|
-- buffer-content before computing the diff:
|
||||||
|
self.changedtick = 0
|
||||||
|
|
||||||
|
--- @type integer, integer
|
||||||
|
local l, c = unpack(vim.api.nvim_win_get_cursor(0))
|
||||||
|
l = l - 1 -- make it actually 0-based
|
||||||
|
local pos_infos = self:get_tags_at({ l, c }, 'i')
|
||||||
|
for _, pos_info in ipairs(pos_infos) do
|
||||||
|
local extmark_inf = pos_info.extmark
|
||||||
|
local tag = pos_info.tag
|
||||||
|
|
||||||
|
local on_change = tag.attributes.on_change
|
||||||
|
if on_change and type(on_change) == 'function' then
|
||||||
|
local extmark =
|
||||||
|
vim.api.nvim_buf_get_extmark_by_id(self.bufnr, self.ns, extmark_inf.id, { details = true })
|
||||||
|
|
||||||
|
--- @type integer, integer, vim.api.keyset.extmark_details
|
||||||
|
local start_row0, start_col0, details = unpack(extmark)
|
||||||
|
local end_row0, end_col0 = details.end_row, details.end_col
|
||||||
|
|
||||||
|
local buf_max_line0 = math.max(1, vim.api.nvim_buf_line_count(self.bufnr) - 1)
|
||||||
|
if end_row0 > buf_max_line0 then
|
||||||
|
end_row0 = buf_max_line0
|
||||||
|
local last_line = vim.api.nvim_buf_get_lines(self.bufnr, end_row0, end_row0 + 1, false)[1]
|
||||||
|
or ''
|
||||||
|
end_col0 = last_line:len()
|
||||||
|
end
|
||||||
|
if end_col0 == 0 then
|
||||||
|
end_row0 = end_row0 - 1
|
||||||
|
local last_line = vim.api.nvim_buf_get_lines(self.bufnr, end_row0, end_row0 + 1, false)[1]
|
||||||
|
or ''
|
||||||
|
end_col0 = last_line:len()
|
||||||
|
end
|
||||||
|
|
||||||
|
if start_row0 == end_row0 and start_col0 == end_col0 then
|
||||||
|
on_change ''
|
||||||
|
else
|
||||||
|
local pos1 = { self.bufnr, start_row0 + 1, start_col0 + 1 }
|
||||||
|
local pos2 = { self.bufnr, end_row0 + 1, end_col0 }
|
||||||
|
local ok, lines = pcall(vim.fn.getregion, pos1, pos2, { type = 'v' })
|
||||||
|
if not ok then
|
||||||
|
vim.api.nvim_echo({
|
||||||
|
{ '(u.nvim:getregion:invalid-pos) ', 'ErrorMsg' },
|
||||||
|
{
|
||||||
|
'{ start, end } = ' .. vim.inspect({ pos1, pos2 }, { newline = ' ', indent = '' }),
|
||||||
|
},
|
||||||
|
}, true, {})
|
||||||
|
error(lines)
|
||||||
|
end
|
||||||
|
local text = table.concat(lines, '\n')
|
||||||
|
on_change(text)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end -- }}}
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
function Renderer:_debug() -- {{{
|
||||||
|
local prev_w = vim.api.nvim_get_current_win()
|
||||||
|
vim.cmd.vnew()
|
||||||
|
local info_bufnr = vim.api.nvim_get_current_buf()
|
||||||
|
vim.bo.bufhidden = 'delete'
|
||||||
|
vim.bo.buflisted = false
|
||||||
|
vim.bo.buftype = 'nowrite'
|
||||||
|
|
||||||
|
local ids = {}
|
||||||
|
local function cleanup()
|
||||||
|
for _, id in ipairs(ids) do
|
||||||
|
vim.api.nvim_del_autocmd(id)
|
||||||
|
end
|
||||||
|
vim.api.nvim_buf_delete(info_bufnr, { force = true })
|
||||||
|
end
|
||||||
|
|
||||||
|
table.insert(
|
||||||
|
ids,
|
||||||
|
vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI' }, {
|
||||||
|
callback = function()
|
||||||
|
if vim.api.nvim_get_current_win() ~= prev_w then return end
|
||||||
|
|
||||||
|
local l, c = unpack(vim.api.nvim_win_get_cursor(0))
|
||||||
|
l = l - 1 -- make it actually 0-based
|
||||||
|
|
||||||
|
local info = {
|
||||||
|
cursor = {
|
||||||
|
pos = { l, c },
|
||||||
|
tags = self:get_tags_at { l, c },
|
||||||
|
extmarks = vim.api.nvim_buf_get_extmarks(
|
||||||
|
self.bufnr,
|
||||||
|
self.ns,
|
||||||
|
{ l, c },
|
||||||
|
{ l, c },
|
||||||
|
{ details = true, overlap = true }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
computed = {
|
||||||
|
extmarks = self.curr.extmarks,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
vim.api.nvim_buf_set_lines(info_bufnr, 0, -1, true, vim.split(vim.inspect(info), '\n'))
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
table.insert(
|
||||||
|
ids,
|
||||||
|
vim.api.nvim_create_autocmd('WinClosed', {
|
||||||
|
pattern = tostring(vim.api.nvim_get_current_win()),
|
||||||
|
callback = cleanup,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
table.insert(
|
||||||
|
ids,
|
||||||
|
vim.api.nvim_create_autocmd('WinClosed', {
|
||||||
|
pattern = tostring(prev_w),
|
||||||
|
callback = cleanup,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_win(prev_w)
|
||||||
|
end -- }}}
|
||||||
|
|
||||||
--- Returns pairs of extmarks and tags associate with said extmarks. The
|
--- Returns pairs of extmarks and tags associate with said extmarks. The
|
||||||
--- returned tags/extmarks are sorted smallest (innermost) to largest
|
--- returned tags/extmarks are sorted smallest (innermost) to largest
|
||||||
--- (outermost).
|
--- (outermost).
|
||||||
---
|
---
|
||||||
--- @private (private for now)
|
--- @private (private for now)
|
||||||
--- @param pos0 [number; number]
|
--- @param pos0 [number; number]
|
||||||
--- @return { extmark: RendererExtmark; tag: u.renderer.Tag; }[]
|
--- @param mode string?
|
||||||
function Renderer:get_pos_infos(pos0) -- {{{
|
--- @return { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag; }[]
|
||||||
|
function Renderer:get_tags_at(pos0, mode) -- {{{
|
||||||
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
||||||
|
if not mode then mode = vim.api.nvim_get_mode().mode end
|
||||||
|
|
||||||
-- The cursor (block) occupies **two** extmark spaces: one for it's left
|
-- The cursor (block) occupies **two** extmark spaces: one for it's left
|
||||||
-- edge, and one for it's right. We need to do our own intersection test,
|
-- edge, and one for it's right. We need to do our own intersection test,
|
||||||
-- because the NeoVim API is over-inclusive in what it returns:
|
-- because the NeoVim API is over-inclusive in what it returns:
|
||||||
--- @type RendererExtmark[]
|
--- @type u.renderer.RendererExtmark[]
|
||||||
local intersecting_extmarks = vim
|
local mapped_extmarks = vim
|
||||||
.iter(
|
.iter(
|
||||||
vim.api.nvim_buf_get_extmarks(
|
vim.api.nvim_buf_get_extmarks(
|
||||||
self.bufnr,
|
self.bufnr,
|
||||||
@ -357,7 +501,7 @@ function Renderer:get_pos_infos(pos0) -- {{{
|
|||||||
{ details = true, overlap = true }
|
{ details = true, overlap = true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
--- @return RendererExtmark
|
--- @return u.renderer.RendererExtmark
|
||||||
:map(function(ext)
|
:map(function(ext)
|
||||||
--- @type number, number, number, { end_row?: number; end_col?: number }|nil
|
--- @type number, number, number, { end_row?: number; end_col?: number }|nil
|
||||||
local id, line0, col0, details = unpack(ext)
|
local id, line0, col0, details = unpack(ext)
|
||||||
@ -368,13 +512,43 @@ function Renderer:get_pos_infos(pos0) -- {{{
|
|||||||
end
|
end
|
||||||
return { id = id, start = start, stop = stop, opts = details }
|
return { id = id, start = start, stop = stop, opts = details }
|
||||||
end)
|
end)
|
||||||
--- @param ext RendererExtmark
|
:totable()
|
||||||
|
|
||||||
|
local intersecting_extmarks = vim
|
||||||
|
.iter(mapped_extmarks)
|
||||||
|
--- @param ext u.renderer.RendererExtmark
|
||||||
:filter(function(ext)
|
:filter(function(ext)
|
||||||
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
if ext.stop[1] ~= nil and ext.stop[2] ~= nil then
|
||||||
return cursor_line0 >= ext.start[1]
|
-- If we've "ciw" and "collapsed" an extmark onto the cursor,
|
||||||
and cursor_col0 >= ext.start[2]
|
-- the cursor pos will equal the exmark's start AND end. In this
|
||||||
|
-- case, we want to include the extmark.
|
||||||
|
if
|
||||||
|
cursor_line0 == ext.start[1]
|
||||||
|
and cursor_col0 == ext.start[2]
|
||||||
|
and cursor_line0 == ext.stop[1]
|
||||||
|
and cursor_col0 == ext.stop[2]
|
||||||
|
then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return
|
||||||
|
-- START: line check
|
||||||
|
cursor_line0 >= ext.start[1]
|
||||||
|
-- START: column check
|
||||||
|
and (cursor_line0 ~= ext.start[1] or cursor_col0 >= ext.start[2])
|
||||||
|
-- STOP: line check
|
||||||
and cursor_line0 <= ext.stop[1]
|
and cursor_line0 <= ext.stop[1]
|
||||||
and cursor_col0 < ext.stop[2]
|
-- STOP: column check
|
||||||
|
and (
|
||||||
|
cursor_line0 ~= ext.stop[1]
|
||||||
|
or (
|
||||||
|
mode == 'i'
|
||||||
|
-- In insert mode, the cursor is "thin", so <= to compensate:
|
||||||
|
and cursor_col0 <= ext.stop[2]
|
||||||
|
-- In normal mode, the cursor is "wide", so < to compensate:
|
||||||
|
or cursor_col0 < ext.stop[2]
|
||||||
|
)
|
||||||
|
)
|
||||||
else
|
else
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@ -384,8 +558,8 @@ function Renderer:get_pos_infos(pos0) -- {{{
|
|||||||
-- Sort the tags into smallest (inner) to largest (outer):
|
-- Sort the tags into smallest (inner) to largest (outer):
|
||||||
table.sort(
|
table.sort(
|
||||||
intersecting_extmarks,
|
intersecting_extmarks,
|
||||||
--- @param x1 RendererExtmark
|
--- @param x1 u.renderer.RendererExtmark
|
||||||
--- @param x2 RendererExtmark
|
--- @param x2 u.renderer.RendererExtmark
|
||||||
function(x1, x2)
|
function(x1, x2)
|
||||||
if
|
if
|
||||||
x1.start[1] == x2.start[1]
|
x1.start[1] == x2.start[1]
|
||||||
@ -407,10 +581,10 @@ function Renderer:get_pos_infos(pos0) -- {{{
|
|||||||
-- created extmarks in self.curr.extmarks, which also has which tag each
|
-- created extmarks in self.curr.extmarks, which also has which tag each
|
||||||
-- extmark is associated with. Cross-reference with that list to get a list
|
-- extmark is associated with. Cross-reference with that list to get a list
|
||||||
-- of tags that we need to fire events for:
|
-- of tags that we need to fire events for:
|
||||||
--- @type { extmark: RendererExtmark; tag: u.renderer.Tag }[]
|
--- @type { extmark: u.renderer.RendererExtmark; tag: u.renderer.Tag }[]
|
||||||
local matching_tags = vim
|
local matching_tags = vim
|
||||||
.iter(intersecting_extmarks)
|
.iter(intersecting_extmarks)
|
||||||
--- @param ext RendererExtmark
|
--- @param ext u.renderer.RendererExtmark
|
||||||
:map(function(ext)
|
:map(function(ext)
|
||||||
for _, extmark_cache in ipairs(self.curr.extmarks) do
|
for _, extmark_cache in ipairs(self.curr.extmarks) do
|
||||||
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
if extmark_cache.id == ext.id then return { extmark = ext, tag = extmark_cache.tag } end
|
||||||
@ -420,10 +594,23 @@ function Renderer:get_pos_infos(pos0) -- {{{
|
|||||||
|
|
||||||
return matching_tags
|
return matching_tags
|
||||||
end -- }}}
|
end -- }}}
|
||||||
|
|
||||||
|
--- @private
|
||||||
|
--- @param tag_or_id string | u.renderer.Tag
|
||||||
|
--- @return { start: [number, number]; stop: [number, number] } | nil
|
||||||
|
function Renderer:get_tag_bounds(tag_or_id) -- {{{
|
||||||
|
for _, x in ipairs(self.curr.extmarks) do
|
||||||
|
local pos = { start = x.start, stop = x.stop }
|
||||||
|
local does_tag_match = type(tag_or_id) == 'string' and x.tag.attributes.id == tag_or_id
|
||||||
|
or x.tag == tag_or_id
|
||||||
|
if does_tag_match then return pos end
|
||||||
|
end
|
||||||
|
end -- }}}
|
||||||
|
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- TreeBuilder {{{
|
-- TreeBuilder {{{
|
||||||
--- @class u.TreeBuilder
|
--- @class u.renderer.TreeBuilder
|
||||||
--- @field private nodes u.renderer.Node[]
|
--- @field private nodes u.renderer.Node[]
|
||||||
local TreeBuilder = {}
|
local TreeBuilder = {}
|
||||||
TreeBuilder.__index = TreeBuilder
|
TreeBuilder.__index = TreeBuilder
|
||||||
@ -435,7 +622,7 @@ function TreeBuilder.new()
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- @param nodes u.renderer.Tree
|
--- @param nodes u.renderer.Tree
|
||||||
--- @return u.TreeBuilder
|
--- @return u.renderer.TreeBuilder
|
||||||
function TreeBuilder:put(nodes)
|
function TreeBuilder:put(nodes)
|
||||||
table.insert(self.nodes, nodes)
|
table.insert(self.nodes, nodes)
|
||||||
return self
|
return self
|
||||||
@ -444,15 +631,15 @@ end
|
|||||||
--- @param name string
|
--- @param name string
|
||||||
--- @param attributes? table<string, any>
|
--- @param attributes? table<string, any>
|
||||||
--- @param children? u.renderer.Node | u.renderer.Node[]
|
--- @param children? u.renderer.Node | u.renderer.Node[]
|
||||||
--- @return u.TreeBuilder
|
--- @return u.renderer.TreeBuilder
|
||||||
function TreeBuilder:put_h(name, attributes, children)
|
function TreeBuilder:put_h(name, attributes, children)
|
||||||
local tag = M.h(name, attributes, children)
|
local tag = M.h(name, attributes, children)
|
||||||
table.insert(self.nodes, tag)
|
table.insert(self.nodes, tag)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param fn fun(TreeBuilder): any
|
--- @param fn fun(tb: u.renderer.TreeBuilder): any
|
||||||
--- @return u.TreeBuilder
|
--- @return u.renderer.TreeBuilder
|
||||||
function TreeBuilder:nest(fn)
|
function TreeBuilder:nest(fn)
|
||||||
local nested_writer = TreeBuilder.new()
|
local nested_writer = TreeBuilder.new()
|
||||||
fn(nested_writer)
|
fn(nested_writer)
|
||||||
@ -460,19 +647,40 @@ function TreeBuilder:nest(fn)
|
|||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- @generic T
|
||||||
|
--- @param arr T[]
|
||||||
|
--- @param f fun(tb: u.renderer.TreeBuilder, item: T, idx: number): any
|
||||||
|
function TreeBuilder:ipairs(arr, f)
|
||||||
|
return self:nest(function(tb)
|
||||||
|
for idx, item in ipairs(arr) do
|
||||||
|
f(tb, item, idx)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
--- @param tab table
|
||||||
|
--- @param f fun(tb: u.renderer.TreeBuilder, key: any, value: any): any
|
||||||
|
function TreeBuilder:pairs(tab, f)
|
||||||
|
return self:nest(function(tb)
|
||||||
|
for k, v in pairs(tab) do
|
||||||
|
f(tb, k, v)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
--- @return u.renderer.Tree
|
--- @return u.renderer.Tree
|
||||||
function TreeBuilder:tree() return self.nodes end
|
function TreeBuilder:tree() return self.nodes end
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
-- Levenshtein utility {{{
|
-- Levenshtein utility {{{
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
--- @alias LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
|
--- @alias u.renderer.LevenshteinChange<T> ({ kind: 'add'; item: T; index: number; } | { kind: 'delete'; item: T; index: number; } | { kind: 'change'; from: T; to: T; index: number; })
|
||||||
--- @private
|
--- @private
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param x `T`[]
|
--- @param x `T`[]
|
||||||
--- @param y T[]
|
--- @param y T[]
|
||||||
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
|
--- @param cost? { of_delete?: fun(x: T): number; of_add?: fun(x: T): number; of_change?: fun(x: T, y: T): number; }
|
||||||
--- @return LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
--- @return u.renderer.LevenshteinChange<T>[] The changes, from last (greatest index) to first (smallest index).
|
||||||
function H.levenshtein(x, y, cost)
|
function H.levenshtein(x, y, cost)
|
||||||
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
-- At the moment, this whole `cost` plumbing is not used. Deletes have the
|
||||||
-- same cost as Adds or Changes. I can imagine a future, however, where
|
-- same cost as Adds or Changes. I can imagine a future, however, where
|
||||||
@ -523,7 +731,7 @@ function H.levenshtein(x, y, cost)
|
|||||||
-- Backtrack to find the changes
|
-- Backtrack to find the changes
|
||||||
local i = m
|
local i = m
|
||||||
local j = n
|
local j = n
|
||||||
--- @type LevenshteinChange[]
|
--- @type u.renderer.LevenshteinChange[]
|
||||||
local changes = {}
|
local changes = {}
|
||||||
|
|
||||||
while i > 0 or j > 0 do
|
while i > 0 or j > 0 do
|
||||||
@ -540,7 +748,7 @@ function H.levenshtein(x, y, cost)
|
|||||||
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
if is_first_min(cost_of_change, cost_of_add, cost_of_delete) then
|
||||||
-- potential change
|
-- potential change
|
||||||
if x[i] ~= y[j] then
|
if x[i] ~= y[j] then
|
||||||
--- @type LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'change', from = x[i], index = i, to = y[j] }
|
local change = { kind = 'change', from = x[i], index = i, to = y[j] }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
end
|
end
|
||||||
@ -548,13 +756,13 @@ function H.levenshtein(x, y, cost)
|
|||||||
j = j - 1
|
j = j - 1
|
||||||
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
elseif is_first_min(cost_of_add, cost_of_change, cost_of_delete) then
|
||||||
-- addition
|
-- addition
|
||||||
--- @type LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'add', item = y[j], index = i + 1 }
|
local change = { kind = 'add', item = y[j], index = i + 1 }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
j = j - 1
|
j = j - 1
|
||||||
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
elseif is_first_min(cost_of_delete, cost_of_change, cost_of_add) then
|
||||||
-- deletion
|
-- deletion
|
||||||
--- @type LevenshteinChange
|
--- @type u.renderer.LevenshteinChange
|
||||||
local change = { kind = 'delete', item = x[i], index = i }
|
local change = { kind = 'delete', item = x[i], index = i }
|
||||||
table.insert(changes, change)
|
table.insert(changes, change)
|
||||||
i = i - 1
|
i = i - 1
|
||||||
|
169
lua/u/utils.lua
169
lua/u/utils.lua
@ -4,15 +4,63 @@ local M = {}
|
|||||||
-- Types
|
-- Types
|
||||||
--
|
--
|
||||||
|
|
||||||
--- @alias QfItem { col: number, filename: string, kind: string, lnum: number, text: string }
|
--- @class u.utils.QfItem
|
||||||
--- @alias KeyMaps table<string, fun(): any | string> }
|
--- @field col number
|
||||||
-- luacheck: ignore
|
--- @field filename string
|
||||||
--- @alias CmdArgs { args: string; bang: boolean; count: number; fargs: string[]; line1: number; line2: number; mods: string; name: string; range: 0|1|2; reg: string; smods: any; info: u.Range|nil }
|
--- @field kind string
|
||||||
|
--- @field lnum number
|
||||||
|
--- @field text string
|
||||||
|
|
||||||
|
--- @class u.utils.RawCmdArgs
|
||||||
|
--- @field args string
|
||||||
|
--- @field bang boolean
|
||||||
|
--- @field count number
|
||||||
|
--- @field fargs string[]
|
||||||
|
--- @field line1 number
|
||||||
|
--- @field line2 number
|
||||||
|
--- @field mods string
|
||||||
|
--- @field name string
|
||||||
|
--- @field range 0|1|2
|
||||||
|
--- @field reg string
|
||||||
|
--- @field smods any
|
||||||
|
|
||||||
|
--- @class u.utils.CmdArgs: u.utils.RawCmdArgs
|
||||||
|
--- @field info u.Range|nil
|
||||||
|
|
||||||
|
--- @class u.utils.UcmdArgs
|
||||||
|
--- @field nargs? 0|1|'*'|'?'|'+'
|
||||||
|
--- @field range? boolean|'%'|number
|
||||||
|
--- @field count? boolean|number
|
||||||
|
--- @field addr? string
|
||||||
|
--- @field completion? string
|
||||||
|
--- @field force? boolean
|
||||||
|
--- @field preview? fun(opts: u.utils.UcmdArgs, ns: integer, buf: integer):0|1|2
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Functions
|
||||||
|
--
|
||||||
|
|
||||||
|
--- Debug utility that prints a value and returns it unchanged.
|
||||||
|
--- Useful for debugging in the middle of expressions or function chains.
|
||||||
|
---
|
||||||
--- @generic T
|
--- @generic T
|
||||||
--- @param x `T`
|
--- @param x `T` The value to debug print
|
||||||
--- @param message? string
|
--- @param message? string Optional message to print alongside the value
|
||||||
--- @return T
|
--- @return T The original value, unchanged
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
|
--- ```lua
|
||||||
|
--- -- Debug a value in the middle of a chain:
|
||||||
|
--- local result = some_function()
|
||||||
|
--- :map(utils.dbg) -- prints the intermediate value
|
||||||
|
--- :filter(predicate)
|
||||||
|
---
|
||||||
|
--- -- Debug with a custom message:
|
||||||
|
--- local config = utils.dbg(get_config(), "Current config:")
|
||||||
|
---
|
||||||
|
--- -- Debug return values:
|
||||||
|
--- return utils.dbg(calculate_result(), "Final result")
|
||||||
|
--- ```
|
||||||
function M.dbg(x, message)
|
function M.dbg(x, message)
|
||||||
local t = {}
|
local t = {}
|
||||||
if message ~= nil then table.insert(t, message) end
|
if message ~= nil then table.insert(t, message) end
|
||||||
@ -21,22 +69,37 @@ function M.dbg(x, message)
|
|||||||
return x
|
return x
|
||||||
end
|
end
|
||||||
|
|
||||||
--- A utility for creating user commands that also pre-computes useful information
|
--- Creates a user command with enhanced argument processing.
|
||||||
--- and attaches it to the arguments.
|
--- Automatically computes range information and attaches it as `args.info`.
|
||||||
---
|
---
|
||||||
|
--- @param name string The command name (without the leading colon)
|
||||||
|
--- @param cmd string | fun(args: u.utils.CmdArgs): any Command implementation
|
||||||
|
--- @param opts? u.utils.UcmdArgs Command options (nargs, range, etc.)
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
--- ```lua
|
--- ```lua
|
||||||
--- -- Example:
|
--- -- Create a command that works with visual selections:
|
||||||
--- ucmd('MyCmd', function(args)
|
--- utils.ucmd('MyCmd', function(args)
|
||||||
--- -- print the visually selected text:
|
--- -- Print the visually selected text:
|
||||||
--- vim.print(args.info:text())
|
--- vim.print(args.info:text())
|
||||||
--- -- or get the vtext as an array of lines:
|
--- -- Or get the selection as an array of lines:
|
||||||
--- vim.print(args.info:lines())
|
--- vim.print(args.info:lines())
|
||||||
--- end, { nargs = '*', range = true })
|
--- end, { nargs = '*', range = true })
|
||||||
|
---
|
||||||
|
--- -- Create a command that processes the current line:
|
||||||
|
--- utils.ucmd('ProcessLine', function(args)
|
||||||
|
--- local line_text = args.info:text()
|
||||||
|
--- -- Process the line...
|
||||||
|
--- end, { range = '%' })
|
||||||
|
---
|
||||||
|
--- -- Create a command with arguments:
|
||||||
|
--- utils.ucmd('SearchReplace', function(args)
|
||||||
|
--- local pattern, replacement = args.fargs[1], args.fargs[2]
|
||||||
|
--- local text = args.info:text()
|
||||||
|
--- -- Perform search and replace...
|
||||||
|
--- end, { nargs = 2, range = true })
|
||||||
--- ```
|
--- ```
|
||||||
--- @param name string
|
|
||||||
--- @param cmd string | fun(args: CmdArgs): any
|
|
||||||
-- luacheck: ignore
|
-- luacheck: ignore
|
||||||
--- @param opts? { nargs?: 0|1|'*'|'?'|'+'; range?: boolean|'%'|number; count?: boolean|number, addr?: string; completion?: string }
|
|
||||||
function M.ucmd(name, cmd, opts)
|
function M.ucmd(name, cmd, opts)
|
||||||
local Range = require 'u.range'
|
local Range = require 'u.range'
|
||||||
|
|
||||||
@ -48,9 +111,81 @@ function M.ucmd(name, cmd, opts)
|
|||||||
return cmd(args)
|
return cmd(args)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
vim.api.nvim_create_user_command(name, cmd2, opts or {})
|
vim.api.nvim_create_user_command(name, cmd2, opts or {} --[[@as any]])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Creates command arguments for delegating from one command to another.
|
||||||
|
--- Preserves all relevant context (range, modifiers, bang, etc.) when
|
||||||
|
--- implementing a derived command in terms of a base command.
|
||||||
|
---
|
||||||
|
--- @param current_args vim.api.keyset.create_user_command.command_args|u.utils.RawCmdArgs The arguments from the current command
|
||||||
|
--- @return vim.api.keyset.cmd Arguments suitable for vim.cmd() calls
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
|
--- ```lua
|
||||||
|
--- -- Implement :MyEdit in terms of :edit, preserving all context:
|
||||||
|
--- utils.ucmd('MyEdit', function(args)
|
||||||
|
--- local delegated_args = utils.create_delegated_cmd_args(args)
|
||||||
|
--- -- Add custom logic here...
|
||||||
|
--- vim.cmd.edit(delegated_args)
|
||||||
|
--- end, { nargs = '*', range = true, bang = true })
|
||||||
|
---
|
||||||
|
--- -- Implement :MySubstitute that delegates to :substitute:
|
||||||
|
--- utils.ucmd('MySubstitute', function(args)
|
||||||
|
--- -- Pre-process arguments
|
||||||
|
--- local pattern = preprocess_pattern(args.fargs[1])
|
||||||
|
---
|
||||||
|
--- local delegated_args = utils.create_delegated_cmd_args(args)
|
||||||
|
--- delegated_args.args = { pattern, args.fargs[2] }
|
||||||
|
---
|
||||||
|
--- vim.cmd.substitute(delegated_args)
|
||||||
|
--- end, { nargs = 2, range = true, bang = true })
|
||||||
|
--- ```
|
||||||
|
function M.create_delegated_cmd_args(current_args)
|
||||||
|
--- @type vim.api.keyset.cmd
|
||||||
|
local args = {
|
||||||
|
range = current_args.range == 1 and { current_args.line1 }
|
||||||
|
or current_args.range == 2 and { current_args.line1, current_args.line2 }
|
||||||
|
or nil,
|
||||||
|
count = (current_args.count ~= -1 and current_args.range == 0) and current_args.count or nil,
|
||||||
|
reg = current_args.reg ~= '' and current_args.reg or nil,
|
||||||
|
bang = current_args.bang or nil,
|
||||||
|
args = #current_args.fargs > 0 and current_args.fargs or nil,
|
||||||
|
mods = current_args.smods,
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Gets the current editor dimensions.
|
||||||
|
--- Useful for positioning floating windows or calculating layout sizes.
|
||||||
|
---
|
||||||
|
--- @return { width: number, height: number } The editor dimensions in columns and lines
|
||||||
|
---
|
||||||
|
--- @usage
|
||||||
|
--- ```lua
|
||||||
|
--- -- Center a floating window:
|
||||||
|
--- local dims = utils.get_editor_dimensions()
|
||||||
|
--- local win_width = 80
|
||||||
|
--- local win_height = 20
|
||||||
|
--- local col = math.floor((dims.width - win_width) / 2)
|
||||||
|
--- local row = math.floor((dims.height - win_height) / 2)
|
||||||
|
---
|
||||||
|
--- vim.api.nvim_open_win(bufnr, true, {
|
||||||
|
--- relative = 'editor',
|
||||||
|
--- width = win_width,
|
||||||
|
--- height = win_height,
|
||||||
|
--- col = col,
|
||||||
|
--- row = row,
|
||||||
|
--- })
|
||||||
|
---
|
||||||
|
--- -- Check if editor is wide enough for side-by-side layout:
|
||||||
|
--- local dims = utils.get_editor_dimensions()
|
||||||
|
--- if dims.width >= 160 then
|
||||||
|
--- -- Use side-by-side layout
|
||||||
|
--- else
|
||||||
|
--- -- Use stacked layout
|
||||||
|
--- end
|
||||||
|
--- ```
|
||||||
function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end
|
function M.get_editor_dimensions() return { width = vim.go.columns, height = vim.go.lines } end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
pkgs ?
|
pkgs ?
|
||||||
import
|
import
|
||||||
# nixpkgs-unstable (neovim@0.11.2):
|
# nixos-25.05 (neovim@0.11.2):
|
||||||
(fetchTarball {
|
(fetchTarball {
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/e4b09e47ace7d87de083786b404bf232eb6c89d8.tar.gz";
|
url = "https://github.com/nixos/nixpkgs/archive/32a4e87942101f1c9f9865e04dc3ddb175f5f32e.tar.gz";
|
||||||
sha256 = "1a2qvp2yz8j1jcggl1yvqmdxicbdqq58nv7hihmw3bzg9cjyqm26";
|
sha256 = "1jvflnbrxa8gjxkwjq6kdpdzgwp0hs59h9l3xjasksv0v7xlwykz";
|
||||||
})
|
})
|
||||||
{ },
|
{ },
|
||||||
}:
|
}:
|
||||||
@ -19,5 +19,6 @@ pkgs.mkShell {
|
|||||||
pkgs.lua51Packages.nlua
|
pkgs.lua51Packages.nlua
|
||||||
pkgs.neovim
|
pkgs.neovim
|
||||||
pkgs.stylua
|
pkgs.stylua
|
||||||
|
pkgs.watchexec
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -238,6 +238,128 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('from_tsquery_caps', function()
|
||||||
|
withbuf({
|
||||||
|
'-- a comment',
|
||||||
|
'',
|
||||||
|
'function foo(bar) end',
|
||||||
|
'',
|
||||||
|
'-- a middle comment',
|
||||||
|
'',
|
||||||
|
'function bar(baz) end',
|
||||||
|
'',
|
||||||
|
'-- another comment',
|
||||||
|
}, function()
|
||||||
|
vim.cmd.setfiletype 'lua'
|
||||||
|
|
||||||
|
--- @param contains_cursor? boolean
|
||||||
|
local function get_caps(contains_cursor)
|
||||||
|
if contains_cursor == nil then contains_cursor = true end
|
||||||
|
return (Range.from_tsquery_caps(
|
||||||
|
0,
|
||||||
|
'(function_declaration) @f',
|
||||||
|
{ contains_cursor = contains_cursor }
|
||||||
|
)).f or {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local caps = get_caps(false)
|
||||||
|
assert.are.same(#caps, 2)
|
||||||
|
assert.are.same(vim.iter(caps):map(function(c) return c:text() end):totable(), {
|
||||||
|
'function foo(bar) end',
|
||||||
|
'function bar(baz) end',
|
||||||
|
})
|
||||||
|
|
||||||
|
Pos.new(0, 1, 1):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 0)
|
||||||
|
|
||||||
|
Pos.new(0, 3, 18):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 1)
|
||||||
|
assert.are.same(caps[1]:text(), 'function foo(bar) end')
|
||||||
|
|
||||||
|
Pos.new(0, 5, 1):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 0)
|
||||||
|
|
||||||
|
Pos.new(0, 7, 1):save_to_pos '.'
|
||||||
|
caps = get_caps()
|
||||||
|
assert.are.same(#caps, 1)
|
||||||
|
assert.are.same(caps[1]:text(), 'function bar(baz) end')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_tsquery_caps with string array filter', function()
|
||||||
|
withbuf({
|
||||||
|
'{',
|
||||||
|
' sample_key1 = "sample-value1",',
|
||||||
|
' sample_key2 = "sample-value2",',
|
||||||
|
'}',
|
||||||
|
}, function()
|
||||||
|
vim.cmd.setfiletype 'lua'
|
||||||
|
|
||||||
|
-- Place cursor in "sample-value1"
|
||||||
|
Pos.new(0, 2, 25):save_to_pos '.'
|
||||||
|
|
||||||
|
-- Query that captures both keys and values in pairs
|
||||||
|
local query = [[
|
||||||
|
(field
|
||||||
|
name: _ @key
|
||||||
|
value: _ @value)
|
||||||
|
]]
|
||||||
|
|
||||||
|
local ranges = Range.from_line(0, 2):tsquery(query)
|
||||||
|
|
||||||
|
-- Should have both @key and @value captures for the first pair only
|
||||||
|
-- (since cursor is in sample-value1)
|
||||||
|
assert(ranges, 'Range should not be nil')
|
||||||
|
assert(ranges.key, 'Range.key should not be nil')
|
||||||
|
assert(ranges.value, 'Range.value should not be nil')
|
||||||
|
|
||||||
|
-- Should have exactly one key and one value
|
||||||
|
assert.are.same(#ranges.key, 1)
|
||||||
|
assert.are.same(#ranges.value, 1)
|
||||||
|
|
||||||
|
-- Check that we got sample-key1 and sample-value1
|
||||||
|
assert.are.same(ranges.key[1]:text(), 'sample_key1')
|
||||||
|
assert.are.same(ranges.value[1]:text(), '"sample-value1"')
|
||||||
|
end)
|
||||||
|
|
||||||
|
-- Make sure this works when the match is on the last line:
|
||||||
|
withbuf({
|
||||||
|
'{sample_key1= "sample-value1",',
|
||||||
|
'sample_key2= "sample-value2"}',
|
||||||
|
}, function()
|
||||||
|
vim.cmd.setfiletype 'lua'
|
||||||
|
|
||||||
|
-- Place cursor in "sample-value1"
|
||||||
|
Pos.new(0, 2, 25):save_to_pos '.'
|
||||||
|
|
||||||
|
-- Query that captures both keys and values in pairs
|
||||||
|
local query = [[
|
||||||
|
(field
|
||||||
|
name: _ @key
|
||||||
|
value: _ @value)
|
||||||
|
]]
|
||||||
|
|
||||||
|
local ranges = Range.from_line(0, 2):tsquery(query)
|
||||||
|
|
||||||
|
-- Should have both @key and @value captures for the first pair only
|
||||||
|
-- (since cursor is in sample-value1)
|
||||||
|
assert(ranges, 'Range should not be nil')
|
||||||
|
assert(ranges.key, 'Range.key should not be nil')
|
||||||
|
assert(ranges.value, 'Range.value should not be nil')
|
||||||
|
|
||||||
|
-- Should have exactly one key and one value
|
||||||
|
assert.are.same(#ranges.key, 1)
|
||||||
|
assert.are.same(#ranges.value, 1)
|
||||||
|
|
||||||
|
-- Check that we got sample-key2 and sample-value2
|
||||||
|
assert.are.same(ranges.key[1]:text(), 'sample_key2')
|
||||||
|
assert.are.same(ranges.value[1]:text(), '"sample-value2"')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
it('should get nearest block', function()
|
it('should get nearest block', function()
|
||||||
withbuf({
|
withbuf({
|
||||||
'this is a {',
|
'this is a {',
|
||||||
@ -318,18 +440,89 @@ describe('Range', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('from_cmd_args', function()
|
it('from_cmd_args: range=0', function()
|
||||||
local args = { range = 1 }
|
local args = { range = 0 }
|
||||||
|
withbuf(
|
||||||
|
{ 'line one', 'and line two' },
|
||||||
|
function() assert.are.same(Range.from_cmd_args(args), nil) end
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_cmd_args: range=1', function()
|
||||||
|
local args = { range = 1, line1 = 1 }
|
||||||
withbuf({ 'line one', 'and line two' }, function()
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
local a = Pos.new(nil, 1, 1)
|
local a = Pos.new(nil, 1, 1)
|
||||||
local b = Pos.new(nil, 2, 2)
|
local b = Pos.new(nil, 1, Pos.MAX_COL)
|
||||||
|
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
|
assert.are.same(range.start, a)
|
||||||
|
assert.are.same(range.stop, b)
|
||||||
|
assert.are.same(range.mode, 'V')
|
||||||
|
assert.are.same(range:text(), 'line one')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_cmd_args: range=2: no-visual', function()
|
||||||
|
local args = { range = 2, line1 = 1, line2 = 2 }
|
||||||
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
|
assert.are.same(range.start, Pos.new(nil, 1, 1))
|
||||||
|
assert.are.same(range.stop, Pos.new(nil, 2, Pos.MAX_COL))
|
||||||
|
assert.are.same(range.mode, 'V')
|
||||||
|
assert.are.same(range:text(), 'line one\nand line two')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('from_cmd_args: range=2: visual: linewise', function()
|
||||||
|
local args = { range = 2, line1 = 1, line2 = 2 }
|
||||||
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
|
local a = Pos.new(nil, 1, 1)
|
||||||
|
local b = Pos.new(nil, 2, Pos.MAX_COL)
|
||||||
a:save_to_pos "'<"
|
a:save_to_pos "'<"
|
||||||
b:save_to_pos "'>"
|
b:save_to_pos "'>"
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
|
assert.are.same(range.start, a)
|
||||||
|
assert.are.same(range.stop, b)
|
||||||
|
assert.are.same(range.mode, 'V')
|
||||||
|
assert.are.same(range:text(), 'line one\nand line two')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
local range = Range.from_cmd_args(args)
|
it('from_cmd_args: range=2: visual: charwise', function()
|
||||||
|
local args = { range = 2, line1 = 1, line2 = 1 }
|
||||||
|
withbuf({ 'line one', 'and line two' }, function()
|
||||||
|
-- Simulate a visual selection:
|
||||||
|
local a = Pos.new(nil, 1, 1)
|
||||||
|
local b = Pos.new(nil, 1, 4)
|
||||||
|
a:save_to_pos "'<"
|
||||||
|
b:save_to_pos "'>"
|
||||||
|
Range.new(a, b, 'v'):set_visual_selection()
|
||||||
|
assert.are.same(vim.fn.mode(), 'v')
|
||||||
|
|
||||||
|
-- In this simulated setup, we need to force visualmode() to return
|
||||||
|
-- 'v' and histget() to return [['<,'>]]:
|
||||||
|
|
||||||
|
-- visualmode()
|
||||||
|
local orig_visualmode = vim.fn.visualmode
|
||||||
|
--- @diagnostic disable-next-line: duplicate-set-field
|
||||||
|
function vim.fn.visualmode() return 'v' end
|
||||||
|
assert.are.same(vim.fn.visualmode(), 'v')
|
||||||
|
|
||||||
|
-- histget()
|
||||||
|
local orig_histget = vim.fn.histget
|
||||||
|
--- @diagnostic disable-next-line: duplicate-set-field
|
||||||
|
function vim.fn.histget(x, y) return [['<,'>]] end
|
||||||
|
|
||||||
|
-- Now run the test:
|
||||||
|
local range = Range.from_cmd_args(args) --[[@as u.Range]]
|
||||||
assert.are.same(range.start, a)
|
assert.are.same(range.start, a)
|
||||||
assert.are.same(range.stop, b)
|
assert.are.same(range.stop, b)
|
||||||
assert.are.same(range.mode, 'v')
|
assert.are.same(range.mode, 'v')
|
||||||
|
assert.are.same(range:text(), 'line')
|
||||||
|
|
||||||
|
-- Reset visualmode() and histget():
|
||||||
|
vim.fn.visualmode = orig_visualmode
|
||||||
|
vim.fn.histget = orig_histget
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@ -617,4 +810,86 @@ describe('Range', function()
|
|||||||
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
}, vim.api.nvim_buf_get_lines(b, 0, -1, false))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('can save to extmark', function()
|
||||||
|
withbuf({
|
||||||
|
'The quick brown',
|
||||||
|
'fox',
|
||||||
|
'jumps',
|
||||||
|
'over',
|
||||||
|
'the lazy dog',
|
||||||
|
}, function()
|
||||||
|
-- Construct a range over 'fox jumps'
|
||||||
|
local r = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, 5), 'v')
|
||||||
|
local extrange = r:save_to_extmark()
|
||||||
|
assert.are.same({ 'fox', 'jumps' }, extrange:range():lines())
|
||||||
|
-- change 'jumps' to 'leaps':
|
||||||
|
vim.api.nvim_buf_set_text(extrange.bufnr, 2, 0, 2, 4, { 'leap' })
|
||||||
|
assert.are.same({
|
||||||
|
'The quick brown',
|
||||||
|
'fox',
|
||||||
|
'leaps',
|
||||||
|
'over',
|
||||||
|
'the lazy dog',
|
||||||
|
}, vim.api.nvim_buf_get_lines(extrange.bufnr, 0, -1, false))
|
||||||
|
assert.are.same({ 'fox', 'leaps' }, extrange:range():lines())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('can save linewise extmark', function()
|
||||||
|
withbuf({
|
||||||
|
'The quick brown',
|
||||||
|
'fox',
|
||||||
|
'jumps',
|
||||||
|
'over',
|
||||||
|
'the lazy dog',
|
||||||
|
}, function()
|
||||||
|
-- Construct a range over 'fox jumps'
|
||||||
|
local r = Range.new(Pos.new(nil, 2, 1), Pos.new(nil, 3, Pos.MAX_COL), 'V')
|
||||||
|
local extrange = r:save_to_extmark()
|
||||||
|
assert.are.same({ 'fox', 'jumps' }, extrange:range():lines())
|
||||||
|
|
||||||
|
local extmark = vim.api.nvim_buf_get_extmark_by_id(
|
||||||
|
extrange.bufnr,
|
||||||
|
vim.api.nvim_create_namespace 'u.range',
|
||||||
|
extrange.id,
|
||||||
|
{ details = true }
|
||||||
|
)
|
||||||
|
local row0, col0, details = unpack(extmark)
|
||||||
|
assert.are.same({ 1, 0 }, { row0, col0 })
|
||||||
|
assert.are.same({ 3, 0 }, { details.end_row, details.end_col })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('discerns range bounds from extmarks beyond the end of the buffer', function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
|
||||||
|
vim.cmd.vnew()
|
||||||
|
local left = Buffer.current()
|
||||||
|
left:set_tmp_options()
|
||||||
|
vim.cmd.vnew()
|
||||||
|
local right = Buffer.current()
|
||||||
|
right:set_tmp_options()
|
||||||
|
|
||||||
|
left:all():replace {
|
||||||
|
'one',
|
||||||
|
'two',
|
||||||
|
'three',
|
||||||
|
}
|
||||||
|
local left_all_ext = left:all():save_to_extmark()
|
||||||
|
|
||||||
|
right:all():replace {
|
||||||
|
'foo',
|
||||||
|
'bar',
|
||||||
|
}
|
||||||
|
|
||||||
|
vim.api.nvim_set_current_buf(right.bufnr)
|
||||||
|
vim.cmd [[normal! ggyG]]
|
||||||
|
vim.api.nvim_set_current_buf(left.bufnr)
|
||||||
|
vim.cmd [[normal! ggVGp]]
|
||||||
|
|
||||||
|
assert.are.same({ 'foo', 'bar' }, left_all_ext:range():lines())
|
||||||
|
vim.api.nvim_buf_delete(left.bufnr, { force = true })
|
||||||
|
vim.api.nvim_buf_delete(right.bufnr, { force = true })
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
@ -83,13 +83,13 @@ describe('Renderer', function()
|
|||||||
end)
|
end)
|
||||||
|
|
||||||
--
|
--
|
||||||
-- get_pos_infos
|
-- get_tags_at
|
||||||
--
|
--
|
||||||
|
|
||||||
it('should return no extmarks for an empty buffer', function()
|
it('should return no extmarks for an empty buffer', function()
|
||||||
withbuf({}, function()
|
withbuf({}, function()
|
||||||
local r = R.Renderer.new(0)
|
local r = R.Renderer.new(0)
|
||||||
local pos_infos = r:get_pos_infos { 0, 0 }
|
local pos_infos = r:get_tags_at { 0, 0 }
|
||||||
assert.are.same(pos_infos, {})
|
assert.are.same(pos_infos, {})
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@ -102,12 +102,28 @@ describe('Renderer', function()
|
|||||||
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
R.h('text', { hl = 'HighlightGroup2' }, ' World'),
|
||||||
}
|
}
|
||||||
|
|
||||||
local pos_infos = r:get_pos_infos { 0, 2 }
|
local pos_infos = r:get_tags_at { 0, 2 }
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 1)
|
assert.are.same(#pos_infos, 1)
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
||||||
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
||||||
|
pos_infos = r:get_tags_at { 0, 4 }
|
||||||
|
assert.are.same(#pos_infos, 1)
|
||||||
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
|
assert.are.same(pos_infos[1].extmark.start, { 0, 0 })
|
||||||
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 5 })
|
||||||
|
|
||||||
|
pos_infos = r:get_tags_at { 0, 5 }
|
||||||
|
assert.are.same(#pos_infos, 1)
|
||||||
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
||||||
|
assert.are.same(pos_infos[1].extmark.start, { 0, 5 })
|
||||||
|
assert.are.same(pos_infos[1].extmark.stop, { 0, 11 })
|
||||||
|
|
||||||
|
-- In insert mode, bounds are eagerly included:
|
||||||
|
pos_infos = r:get_tags_at({ 0, 5 }, 'i')
|
||||||
|
assert.are.same(#pos_infos, 2)
|
||||||
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup1')
|
||||||
|
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup2')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@ -125,11 +141,122 @@ describe('Renderer', function()
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
local pos_infos = r:get_pos_infos { 0, 5 }
|
local pos_infos = r:get_tags_at { 0, 5 }
|
||||||
|
|
||||||
assert.are.same(#pos_infos, 2)
|
assert.are.same(#pos_infos, 2)
|
||||||
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
assert.are.same(pos_infos[1].tag.attributes.hl, 'HighlightGroup2')
|
||||||
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
assert.are.same(pos_infos[2].tag.attributes.hl, 'HighlightGroup1')
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('repeated patch_lines calls should not change the buffer content', function()
|
||||||
|
local lines = {
|
||||||
|
[[{ {]],
|
||||||
|
[[ bounds = {]],
|
||||||
|
[[ start1 = { 1, 1 },]],
|
||||||
|
[[ stop1 = { 4, 1 }]],
|
||||||
|
[[ },]],
|
||||||
|
[[ end_right_gravity = true,]],
|
||||||
|
[[ id = 1,]],
|
||||||
|
[[ ns_id = 623,]],
|
||||||
|
[[ ns_name = "my.renderer:91",]],
|
||||||
|
[[ right_gravity = false]],
|
||||||
|
[[ } }]],
|
||||||
|
[[]],
|
||||||
|
}
|
||||||
|
withbuf(lines, function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
R.Renderer.patch_lines(0, nil, lines)
|
||||||
|
assert.are.same(Buffer.current():all():lines(), lines)
|
||||||
|
|
||||||
|
R.Renderer.patch_lines(0, lines, lines)
|
||||||
|
assert.are.same(Buffer.current():all():lines(), lines)
|
||||||
|
|
||||||
|
R.Renderer.patch_lines(0, lines, lines)
|
||||||
|
assert.are.same(Buffer.current():all():lines(), lines)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should fire text-changed events', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
local Buffer = require 'u.buffer'
|
||||||
|
local buf = Buffer.current()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
local captured_changed_text = ''
|
||||||
|
r:render {
|
||||||
|
R.h('text', {
|
||||||
|
on_change = function(txt) captured_changed_text = txt end,
|
||||||
|
}, {
|
||||||
|
'one\n',
|
||||||
|
'two\n',
|
||||||
|
'three\n',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
vim.fn.setreg('"', 'bleh')
|
||||||
|
vim.cmd [[normal! ggVGp]]
|
||||||
|
-- For some reason, the autocmd does not fire in the busted environment.
|
||||||
|
-- We'll call the handler ourselves:
|
||||||
|
r:_on_text_changed()
|
||||||
|
|
||||||
|
assert.are.same(buf:all():text(), 'bleh')
|
||||||
|
assert.are.same(captured_changed_text, 'bleh')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should find tags by position', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
r:render {
|
||||||
|
'pre',
|
||||||
|
R.h('text', {
|
||||||
|
id = 'outer',
|
||||||
|
}, {
|
||||||
|
'inner-pre',
|
||||||
|
R.h('text', {
|
||||||
|
id = 'inner',
|
||||||
|
}, {
|
||||||
|
'inner-text',
|
||||||
|
}),
|
||||||
|
'inner-post',
|
||||||
|
}),
|
||||||
|
'post',
|
||||||
|
}
|
||||||
|
|
||||||
|
local tags = r:get_tags_at { 0, 11 }
|
||||||
|
assert.are.same(#tags, 1)
|
||||||
|
assert.are.same(tags[1].tag.attributes.id, 'outer')
|
||||||
|
|
||||||
|
tags = r:get_tags_at { 0, 12 }
|
||||||
|
assert.are.same(#tags, 2)
|
||||||
|
assert.are.same(tags[1].tag.attributes.id, 'inner')
|
||||||
|
assert.are.same(tags[2].tag.attributes.id, 'outer')
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('should find tags by id', function()
|
||||||
|
withbuf({}, function()
|
||||||
|
local r = R.Renderer.new(0)
|
||||||
|
r:render {
|
||||||
|
R.h('text', {
|
||||||
|
id = 'outer',
|
||||||
|
}, {
|
||||||
|
'inner-pre',
|
||||||
|
R.h('text', {
|
||||||
|
id = 'inner',
|
||||||
|
}, {
|
||||||
|
'inner-text',
|
||||||
|
}),
|
||||||
|
'inner-post',
|
||||||
|
}),
|
||||||
|
'post',
|
||||||
|
}
|
||||||
|
|
||||||
|
local bounds = r:get_tag_bounds 'outer'
|
||||||
|
assert.are.same(bounds, { start = { 0, 0 }, stop = { 0, 29 } })
|
||||||
|
|
||||||
|
bounds = r:get_tag_bounds 'inner'
|
||||||
|
assert.are.same(bounds, { start = { 0, 9 }, stop = { 0, 19 } })
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user