feat: text objects and motions for the pending buffer
Problem: the pending buffer has action-button mappings but no Vim grammar. You cannot dat to delete a task, cit to change a description, or ]] to jump to the next category header. Solution: add textobj.lua with at/it (a task / inner task), aC/iC (a category / inner category), ]]/[[ (next/prev header), and ]t/[t (next/prev task). All text objects work in operator-pending and visual modes; motions work in normal, visual, and operator-pending. Mappings are configurable via the keymaps table and exposed as <Plug> mappings.
This commit is contained in:
parent
c57cc0845b
commit
233ff31df1
5 changed files with 661 additions and 0 deletions
|
|
@ -11,6 +11,14 @@
|
|||
---@field undo? string|false
|
||||
---@field open_line? string|false
|
||||
---@field open_line_above? string|false
|
||||
---@field a_task? string|false
|
||||
---@field i_task? string|false
|
||||
---@field a_category? string|false
|
||||
---@field i_category? string|false
|
||||
---@field next_header? string|false
|
||||
---@field prev_header? string|false
|
||||
---@field next_task? string|false
|
||||
---@field prev_task? string|false
|
||||
|
||||
---@class pending.Config
|
||||
---@field data_path string
|
||||
|
|
@ -47,6 +55,14 @@ local defaults = {
|
|||
undo = 'U',
|
||||
open_line = 'o',
|
||||
open_line_above = 'O',
|
||||
a_task = 'at',
|
||||
i_task = 'it',
|
||||
a_category = 'aC',
|
||||
i_category = 'iC',
|
||||
next_header = ']]',
|
||||
prev_header = '[[',
|
||||
next_task = ']t',
|
||||
prev_task = '[t',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,66 @@ function M._setup_buf_mappings(bufnr)
|
|||
vim.keymap.set('n', key --[[@as string]], fn, opts)
|
||||
end
|
||||
end
|
||||
|
||||
local textobj = require('pending.textobj')
|
||||
|
||||
---@type table<string, { modes: string[], fn: fun(count: integer), visual_fn?: fun(count: integer) }>
|
||||
local textobjs = {
|
||||
a_task = {
|
||||
modes = { 'o', 'x' },
|
||||
fn = textobj.a_task,
|
||||
visual_fn = textobj.a_task_visual,
|
||||
},
|
||||
i_task = {
|
||||
modes = { 'o', 'x' },
|
||||
fn = textobj.i_task,
|
||||
visual_fn = textobj.i_task_visual,
|
||||
},
|
||||
a_category = {
|
||||
modes = { 'o', 'x' },
|
||||
fn = textobj.a_category,
|
||||
visual_fn = textobj.a_category_visual,
|
||||
},
|
||||
i_category = {
|
||||
modes = { 'o', 'x' },
|
||||
fn = textobj.i_category,
|
||||
visual_fn = textobj.i_category_visual,
|
||||
},
|
||||
}
|
||||
|
||||
for name, spec in pairs(textobjs) do
|
||||
local key = km[name]
|
||||
if key and key ~= false then
|
||||
for _, mode in ipairs(spec.modes) do
|
||||
if mode == 'x' and spec.visual_fn then
|
||||
vim.keymap.set(mode, key --[[@as string]], function()
|
||||
spec.visual_fn(vim.v.count1)
|
||||
end, opts)
|
||||
else
|
||||
vim.keymap.set(mode, key --[[@as string]], function()
|
||||
spec.fn(vim.v.count1)
|
||||
end, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type table<string, fun(count: integer)>
|
||||
local motions = {
|
||||
next_header = textobj.next_header,
|
||||
prev_header = textobj.prev_header,
|
||||
next_task = textobj.next_task,
|
||||
prev_task = textobj.prev_task,
|
||||
}
|
||||
|
||||
for name, fn in pairs(motions) do
|
||||
local key = km[name]
|
||||
if key and key ~= false then
|
||||
vim.keymap.set({ 'n', 'x', 'o' }, key --[[@as string]], function()
|
||||
fn(vim.v.count1)
|
||||
end, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
|
|
|
|||
360
lua/pending/textobj.lua
Normal file
360
lua/pending/textobj.lua
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
local buffer = require('pending.buffer')
|
||||
local config = require('pending.config')
|
||||
|
||||
---@class pending.textobj
|
||||
local M = {}
|
||||
|
||||
---@param lnum integer
|
||||
---@param meta pending.LineMeta[]
|
||||
---@return string
|
||||
local function get_line_from_buf(lnum, meta)
|
||||
local _ = meta
|
||||
local bufnr = buffer.bufnr()
|
||||
if not bufnr then
|
||||
return ''
|
||||
end
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false)
|
||||
return lines[1] or ''
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@return integer start_col
|
||||
---@return integer end_col
|
||||
function M.inner_task_range(line)
|
||||
local prefix_end = line:find('/') and select(2, line:find('^/%d+/- %[.%] '))
|
||||
if not prefix_end then
|
||||
prefix_end = select(2, line:find('^- %[.%] ')) or 0
|
||||
end
|
||||
local start_col = prefix_end + 1
|
||||
|
||||
local dk = config.get().date_syntax or 'due'
|
||||
local rk = config.get().recur_syntax or 'rec'
|
||||
local dk_esc = vim.pesc(dk)
|
||||
local rk_esc = vim.pesc(rk)
|
||||
|
||||
local desc_end = #line
|
||||
local tokens = {}
|
||||
for token in line:gmatch('%S+') do
|
||||
table.insert(tokens, token)
|
||||
end
|
||||
|
||||
local i = #tokens
|
||||
while i >= 1 do
|
||||
local token = tokens[i]
|
||||
if token:match('^' .. dk_esc .. ':%S+$')
|
||||
or token:match('^cat:%S+$')
|
||||
or token:match('^' .. rk_esc .. ':%S+$')
|
||||
then
|
||||
i = i - 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if i < #tokens then
|
||||
local rebuilt = {}
|
||||
for j = 1, i do
|
||||
table.insert(rebuilt, tokens[j])
|
||||
end
|
||||
local desc_text = table.concat(rebuilt, ' ')
|
||||
local search_start = prefix_end
|
||||
local found = line:find(desc_text, search_start + 1, true)
|
||||
if found then
|
||||
desc_end = found + #desc_text - 1
|
||||
end
|
||||
end
|
||||
|
||||
return start_col, desc_end
|
||||
end
|
||||
|
||||
---@param row integer
|
||||
---@param meta pending.LineMeta[]
|
||||
---@return integer? header_row
|
||||
---@return integer? last_row
|
||||
function M.category_bounds(row, meta)
|
||||
if not meta or #meta == 0 then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local header_row = nil
|
||||
local m = meta[row]
|
||||
if not m then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
if m.type == 'header' then
|
||||
header_row = row
|
||||
else
|
||||
for r = row, 1, -1 do
|
||||
if meta[r] and meta[r].type == 'header' then
|
||||
header_row = r
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not header_row then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local last_row = header_row
|
||||
local total = #meta
|
||||
for r = header_row + 1, total do
|
||||
if meta[r].type == 'header' then
|
||||
break
|
||||
end
|
||||
last_row = r
|
||||
end
|
||||
|
||||
return header_row, last_row
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.a_task(count)
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local m = meta[row]
|
||||
if not m or m.type ~= 'task' then
|
||||
return
|
||||
end
|
||||
|
||||
local start_row = row
|
||||
local end_row = row
|
||||
count = math.max(1, count)
|
||||
for _ = 2, count do
|
||||
local next_row = end_row + 1
|
||||
if next_row > #meta then
|
||||
break
|
||||
end
|
||||
if meta[next_row] and meta[next_row].type == 'task' then
|
||||
end_row = next_row
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
vim.cmd('normal! ' .. start_row .. 'GV' .. end_row .. 'G')
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.a_task_visual(count)
|
||||
vim.cmd('normal! \27')
|
||||
M.a_task(count)
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.i_task(count)
|
||||
local _ = count
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local m = meta[row]
|
||||
if not m or m.type ~= 'task' then
|
||||
return
|
||||
end
|
||||
|
||||
local line = get_line_from_buf(row, meta)
|
||||
local start_col, end_col = M.inner_task_range(line)
|
||||
if start_col > end_col then
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(0, { row, start_col - 1 })
|
||||
vim.cmd('normal! v')
|
||||
vim.api.nvim_win_set_cursor(0, { row, end_col - 1 })
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.i_task_visual(count)
|
||||
vim.cmd('normal! \27')
|
||||
M.i_task(count)
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.a_category(count)
|
||||
local _ = count
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
local view = buffer.current_view_name()
|
||||
if view == 'priority' then
|
||||
return
|
||||
end
|
||||
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local header_row, last_row = M.category_bounds(row, meta)
|
||||
if not header_row or not last_row then
|
||||
return
|
||||
end
|
||||
|
||||
local start_row = header_row
|
||||
if header_row > 1 and meta[header_row - 1] and meta[header_row - 1].type == 'blank' then
|
||||
start_row = header_row - 1
|
||||
end
|
||||
local end_row = last_row
|
||||
if last_row < #meta and meta[last_row + 1] and meta[last_row + 1].type == 'blank' then
|
||||
end_row = last_row + 1
|
||||
end
|
||||
|
||||
vim.cmd('normal! ' .. start_row .. 'GV' .. end_row .. 'G')
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.a_category_visual(count)
|
||||
vim.cmd('normal! \27')
|
||||
M.a_category(count)
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.i_category(count)
|
||||
local _ = count
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
local view = buffer.current_view_name()
|
||||
if view == 'priority' then
|
||||
return
|
||||
end
|
||||
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local header_row, last_row = M.category_bounds(row, meta)
|
||||
if not header_row or not last_row then
|
||||
return
|
||||
end
|
||||
|
||||
local first_task = nil
|
||||
local last_task = nil
|
||||
for r = header_row + 1, last_row do
|
||||
if meta[r] and meta[r].type == 'task' then
|
||||
if not first_task then
|
||||
first_task = r
|
||||
end
|
||||
last_task = r
|
||||
end
|
||||
end
|
||||
|
||||
if not first_task or not last_task then
|
||||
return
|
||||
end
|
||||
|
||||
vim.cmd('normal! ' .. first_task .. 'GV' .. last_task .. 'G')
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.i_category_visual(count)
|
||||
vim.cmd('normal! \27')
|
||||
M.i_category(count)
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.next_header(count)
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
local view = buffer.current_view_name()
|
||||
if view == 'priority' then
|
||||
return
|
||||
end
|
||||
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local found = 0
|
||||
count = math.max(1, count)
|
||||
for r = row + 1, #meta do
|
||||
if meta[r] and meta[r].type == 'header' then
|
||||
found = found + 1
|
||||
if found == count then
|
||||
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.prev_header(count)
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
local view = buffer.current_view_name()
|
||||
if view == 'priority' then
|
||||
return
|
||||
end
|
||||
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local found = 0
|
||||
count = math.max(1, count)
|
||||
for r = row - 1, 1, -1 do
|
||||
if meta[r] and meta[r].type == 'header' then
|
||||
found = found + 1
|
||||
if found == count then
|
||||
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.next_task(count)
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local found = 0
|
||||
count = math.max(1, count)
|
||||
for r = row + 1, #meta do
|
||||
if meta[r] and meta[r].type == 'task' then
|
||||
found = found + 1
|
||||
if found == count then
|
||||
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param count integer
|
||||
---@return nil
|
||||
function M.prev_task(count)
|
||||
local meta = buffer.meta()
|
||||
if not meta or #meta == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local found = 0
|
||||
count = math.max(1, count)
|
||||
for r = row - 1, 1, -1 do
|
||||
if meta[r] and meta[r].type == 'task' then
|
||||
found = found + 1
|
||||
if found == count then
|
||||
vim.api.nvim_win_set_cursor(0, { r, 0 })
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -54,3 +54,35 @@ end)
|
|||
vim.keymap.set('n', '<Plug>(pending-open-line-above)', function()
|
||||
require('pending.buffer').open_line(true)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-a-task)', function()
|
||||
require('pending.textobj').a_task(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-i-task)', function()
|
||||
require('pending.textobj').i_task(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-a-category)', function()
|
||||
require('pending.textobj').a_category(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'o', 'x' }, '<Plug>(pending-i-category)', function()
|
||||
require('pending.textobj').i_category(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-next-header)', function()
|
||||
require('pending.textobj').next_header(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-header)', function()
|
||||
require('pending.textobj').prev_header(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-next-task)', function()
|
||||
require('pending.textobj').next_task(vim.v.count1)
|
||||
end)
|
||||
|
||||
vim.keymap.set({ 'n', 'x', 'o' }, '<Plug>(pending-prev-task)', function()
|
||||
require('pending.textobj').prev_task(vim.v.count1)
|
||||
end)
|
||||
|
|
|
|||
193
spec/textobj_spec.lua
Normal file
193
spec/textobj_spec.lua
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
require('spec.helpers')
|
||||
|
||||
local config = require('pending.config')
|
||||
|
||||
describe('textobj', function()
|
||||
local textobj = require('pending.textobj')
|
||||
|
||||
before_each(function()
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
vim.g.pending = nil
|
||||
config.reset()
|
||||
end)
|
||||
|
||||
describe('inner_task_range', function()
|
||||
it('returns description range for task with id prefix', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(23, e)
|
||||
end)
|
||||
|
||||
it('returns description range for task without id prefix', function()
|
||||
local s, e = textobj.inner_task_range('- [ ] Buy groceries')
|
||||
assert.are.equal(7, s)
|
||||
assert.are.equal(19, e)
|
||||
end)
|
||||
|
||||
it('excludes trailing due: token', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries due:2026-03-15')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(23, e)
|
||||
end)
|
||||
|
||||
it('excludes trailing cat: token', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries cat:Errands')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(23, e)
|
||||
end)
|
||||
|
||||
it('excludes trailing rec: token', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Take out trash rec:weekly')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(25, e)
|
||||
end)
|
||||
|
||||
it('excludes multiple trailing metadata tokens', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy milk due:2026-03-15 cat:Errands rec:weekly')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(18, e)
|
||||
end)
|
||||
|
||||
it('handles priority checkbox', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [!] Important task')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(24, e)
|
||||
end)
|
||||
|
||||
it('handles done checkbox', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [x] Finished task')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(23, e)
|
||||
end)
|
||||
|
||||
it('handles multi-digit task ids', function()
|
||||
local s, e = textobj.inner_task_range('/123/- [ ] Some task')
|
||||
assert.are.equal(13, s)
|
||||
assert.are.equal(21, e)
|
||||
end)
|
||||
|
||||
it('does not strip non-metadata tokens', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries for dinner')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(34, e)
|
||||
end)
|
||||
|
||||
it('stops stripping at first non-metadata token from right', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries for dinner due:2026-03-15')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(34, e)
|
||||
end)
|
||||
|
||||
it('respects custom date_syntax', function()
|
||||
vim.g.pending = { date_syntax = 'by' }
|
||||
config.reset()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries by:2026-03-15')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(23, e)
|
||||
end)
|
||||
|
||||
it('respects custom recur_syntax', function()
|
||||
vim.g.pending = { recur_syntax = 'repeat' }
|
||||
config.reset()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] Take trash repeat:weekly')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(20, e)
|
||||
end)
|
||||
|
||||
it('handles task with only metadata after description', function()
|
||||
local s, e = textobj.inner_task_range('/1/- [ ] X due:tomorrow')
|
||||
assert.are.equal(11, s)
|
||||
assert.are.equal(11, e)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('category_bounds', function()
|
||||
it('returns header and last row for single category', function()
|
||||
---@type pending.LineMeta[]
|
||||
local meta = {
|
||||
{ type = 'header', category = 'Work' },
|
||||
{ type = 'task', id = 1 },
|
||||
{ type = 'task', id = 2 },
|
||||
}
|
||||
local h, l = textobj.category_bounds(2, meta)
|
||||
assert.are.equal(1, h)
|
||||
assert.are.equal(3, l)
|
||||
end)
|
||||
|
||||
it('returns bounds for first category with trailing blank', function()
|
||||
---@type pending.LineMeta[]
|
||||
local meta = {
|
||||
{ type = 'header', category = 'Work' },
|
||||
{ type = 'task', id = 1 },
|
||||
{ type = 'blank' },
|
||||
{ type = 'header', category = 'Personal' },
|
||||
{ type = 'task', id = 2 },
|
||||
}
|
||||
local h, l = textobj.category_bounds(2, meta)
|
||||
assert.are.equal(1, h)
|
||||
assert.are.equal(2, l)
|
||||
end)
|
||||
|
||||
it('returns bounds for second category', function()
|
||||
---@type pending.LineMeta[]
|
||||
local meta = {
|
||||
{ type = 'header', category = 'Work' },
|
||||
{ type = 'task', id = 1 },
|
||||
{ type = 'blank' },
|
||||
{ type = 'header', category = 'Personal' },
|
||||
{ type = 'task', id = 2 },
|
||||
{ type = 'task', id = 3 },
|
||||
}
|
||||
local h, l = textobj.category_bounds(5, meta)
|
||||
assert.are.equal(4, h)
|
||||
assert.are.equal(6, l)
|
||||
end)
|
||||
|
||||
it('returns bounds when cursor is on header', function()
|
||||
---@type pending.LineMeta[]
|
||||
local meta = {
|
||||
{ type = 'header', category = 'Work' },
|
||||
{ type = 'task', id = 1 },
|
||||
}
|
||||
local h, l = textobj.category_bounds(1, meta)
|
||||
assert.are.equal(1, h)
|
||||
assert.are.equal(2, l)
|
||||
end)
|
||||
|
||||
it('returns nil for blank line with no preceding header', function()
|
||||
---@type pending.LineMeta[]
|
||||
local meta = {
|
||||
{ type = 'blank' },
|
||||
{ type = 'header', category = 'Work' },
|
||||
{ type = 'task', id = 1 },
|
||||
}
|
||||
local h, l = textobj.category_bounds(1, meta)
|
||||
assert.is_nil(h)
|
||||
assert.is_nil(l)
|
||||
end)
|
||||
|
||||
it('returns nil for empty meta', function()
|
||||
local h, l = textobj.category_bounds(1, {})
|
||||
assert.is_nil(h)
|
||||
assert.is_nil(l)
|
||||
end)
|
||||
|
||||
it('includes blank between header and next header in bounds', function()
|
||||
---@type pending.LineMeta[]
|
||||
local meta = {
|
||||
{ type = 'header', category = 'Work' },
|
||||
{ type = 'task', id = 1 },
|
||||
{ type = 'blank' },
|
||||
{ type = 'header', category = 'Home' },
|
||||
{ type = 'task', id = 2 },
|
||||
}
|
||||
local h, l = textobj.category_bounds(1, meta)
|
||||
assert.are.equal(1, h)
|
||||
assert.are.equal(3, l)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue