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)