Problem: inner_task_range used unescaped '-' in Lua patterns, which acts as a lazy quantifier instead of matching a literal hyphen. The metadata-stripping logic also tokenized the full line including the prefix, so the rebuilt string could never be found after the prefix. All test column expectations were off by one. Solution: escape hyphens with %-, rewrite metadata stripping to tokenize only the description portion after the prefix, and correct all test assertions to match actual rendered column positions.
193 lines
5.9 KiB
Lua
193 lines
5.9 KiB
Lua
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(10, s)
|
|
assert.are.equal(22, 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(10, s)
|
|
assert.are.equal(22, e)
|
|
end)
|
|
|
|
it('excludes trailing cat: token', function()
|
|
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries cat:Errands')
|
|
assert.are.equal(10, s)
|
|
assert.are.equal(22, e)
|
|
end)
|
|
|
|
it('excludes trailing rec: token', function()
|
|
local s, e = textobj.inner_task_range('/1/- [ ] Take out trash rec:weekly')
|
|
assert.are.equal(10, s)
|
|
assert.are.equal(23, 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(10, s)
|
|
assert.are.equal(17, e)
|
|
end)
|
|
|
|
it('handles priority checkbox', function()
|
|
local s, e = textobj.inner_task_range('/1/- [!] Important task')
|
|
assert.are.equal(10, s)
|
|
assert.are.equal(23, e)
|
|
end)
|
|
|
|
it('handles done checkbox', function()
|
|
local s, e = textobj.inner_task_range('/1/- [x] Finished task')
|
|
assert.are.equal(10, s)
|
|
assert.are.equal(22, e)
|
|
end)
|
|
|
|
it('handles multi-digit task ids', function()
|
|
local s, e = textobj.inner_task_range('/123/- [ ] Some task')
|
|
assert.are.equal(12, s)
|
|
assert.are.equal(20, 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(10, s)
|
|
assert.are.equal(33, 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(10, s)
|
|
assert.are.equal(33, 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(10, s)
|
|
assert.are.equal(22, 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(10, s)
|
|
assert.are.equal(19, 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(10, s)
|
|
assert.are.equal(10, 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(3, 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)
|