refactor: remove - prefix from task line rendering

Problem: task lines rendered as `- [ ] description` with a redundant
markdown list marker prefix that added visual noise.

Solution: render task lines as `[ ] description` instead. Update all
line generation in `views.lua`, parsing patterns in `buffer.lua`,
`diff.lua`, `textobj.lua`, syntax rules, and corresponding specs.
This commit is contained in:
Barrett Ruth 2026-03-15 13:19:39 -04:00
parent 9830ab84a1
commit a22e09b6a1
9 changed files with 123 additions and 123 deletions

View file

@ -35,16 +35,16 @@ function M.parse_buffer(lines)
for i = start, #lines do for i = start, #lines do
local line = lines[i] local line = lines[i]
local id, body = line:match('^/(%d+)/(- %[.?%] .*)$') local id, body = line:match('^/(%d+)/(%[.?%] .*)$')
if not id then if not id then
body = line:match('^(- %[.?%] .*)$') body = line:match('^(%[.?%] .*)$')
end end
if line == '' then if line == '' then
table.insert(result, { type = 'blank', lnum = i }) table.insert(result, { type = 'blank', lnum = i })
elseif id or body then elseif id or body then
local stripped = body:match('^- %[.?%] (.*)$') or body local stripped = body:match('^%[.?%] (.*)$') or body
local icons = config.get().icons local icons = config.get().icons
local state_char = body:match('^- %[(.-)%]') or icons.pending local state_char = body:match('^%[(.-)%]') or icons.pending
local priority = state_char == icons.priority and 1 or 0 local priority = state_char == icons.priority and 1 or 0
local status local status
if state_char == icons.done then if state_char == icons.done then

View file

@ -28,9 +28,9 @@ end
---@return integer start_col ---@return integer start_col
---@return integer end_col ---@return integer end_col
function M.inner_task_range(line) function M.inner_task_range(line)
local prefix_end = line:find('/') and select(2, line:find('^/%d+/%- %[.%] ')) local prefix_end = line:find('/') and select(2, line:find('^/%d+/%[.%] '))
if not prefix_end then if not prefix_end then
prefix_end = select(2, line:find('^%- %[.%] ')) or 0 prefix_end = select(2, line:find('^%[.%] ')) or 0
end end
local start_col = prefix_end + 1 local start_col = prefix_end + 1

View file

@ -263,8 +263,8 @@ function M.category_view(tasks)
for _, task in ipairs(all) do for _, task in ipairs(all) do
local prefix = '/' .. task.id .. '/' local prefix = '/' .. task.id .. '/'
local state = state_char(task) local state = state_char(task)
local line = prefix .. '- [' .. state .. '] ' .. task.description local line = prefix .. '[' .. state .. '] ' .. task.description
local prefix_len = #prefix + #('- [' .. state .. '] ') local prefix_len = #prefix + #('[' .. state .. '] ')
table.insert(lines, line) table.insert(lines, line)
table.insert(meta, { table.insert(meta, {
type = 'task', type = 'task',
@ -320,8 +320,8 @@ function M.priority_view(tasks)
for _, task in ipairs(all) do for _, task in ipairs(all) do
local prefix = '/' .. task.id .. '/' local prefix = '/' .. task.id .. '/'
local state = state_char(task) local state = state_char(task)
local line = prefix .. '- [' .. state .. '] ' .. task.description local line = prefix .. '[' .. state .. '] ' .. task.description
local prefix_len = #prefix + #('- [' .. state .. '] ') local prefix_len = #prefix + #('[' .. state .. '] ')
table.insert(lines, line) table.insert(lines, line)
table.insert(meta, { table.insert(meta, {
type = 'task', type = 'task',

View file

@ -27,39 +27,39 @@ describe('complete', function()
describe('findstart', function() describe('findstart', function()
it('returns column after colon for cat: prefix', function() it('returns column after colon for cat: prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat:Wo' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task cat:Wo' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 }) vim.api.nvim_win_set_cursor(0, { 1, 14 })
local result = complete.omnifunc(1, '') local result = complete.omnifunc(1, '')
assert.are.equal(15, result) assert.are.equal(13, result)
vim.api.nvim_buf_delete(bufnr, { force = true }) vim.api.nvim_buf_delete(bufnr, { force = true })
end) end)
it('returns column after colon for due: prefix', function() it('returns column after colon for due: prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task due:to' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task due:to' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 }) vim.api.nvim_win_set_cursor(0, { 1, 14 })
local result = complete.omnifunc(1, '') local result = complete.omnifunc(1, '')
assert.are.equal(15, result) assert.are.equal(13, result)
vim.api.nvim_buf_delete(bufnr, { force = true }) vim.api.nvim_buf_delete(bufnr, { force = true })
end) end)
it('returns column after colon for rec: prefix', function() it('returns column after colon for rec: prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task rec:we' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task rec:we' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 }) vim.api.nvim_win_set_cursor(0, { 1, 14 })
local result = complete.omnifunc(1, '') local result = complete.omnifunc(1, '')
assert.are.equal(15, result) assert.are.equal(13, result)
vim.api.nvim_buf_delete(bufnr, { force = true }) vim.api.nvim_buf_delete(bufnr, { force = true })
end) end)
it('returns -1 for non-token position', function() it('returns -1 for non-token position', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] some task ' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] some task ' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 14 }) vim.api.nvim_win_set_cursor(0, { 1, 12 })
local result = complete.omnifunc(1, '') local result = complete.omnifunc(1, '')
assert.are.equal(-1, result) assert.are.equal(-1, result)
vim.api.nvim_buf_delete(bufnr, { force = true }) vim.api.nvim_buf_delete(bufnr, { force = true })
@ -72,9 +72,9 @@ describe('complete', function()
s:add({ description = 'B', category = 'Home' }) s:add({ description = 'B', category = 'Home' })
s:add({ description = 'C', category = 'Work' }) s:add({ description = 'C', category = 'Work' })
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat: x' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task cat: x' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 }) vim.api.nvim_win_set_cursor(0, { 1, 13 })
complete.omnifunc(1, '') complete.omnifunc(1, '')
local result = complete.omnifunc(0, '') local result = complete.omnifunc(0, '')
local words = {} local words = {}
@ -90,9 +90,9 @@ describe('complete', function()
s:add({ description = 'A', category = 'Work' }) s:add({ description = 'A', category = 'Work' })
s:add({ description = 'B', category = 'Home' }) s:add({ description = 'B', category = 'Home' })
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task cat:W' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task cat:W' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 }) vim.api.nvim_win_set_cursor(0, { 1, 13 })
complete.omnifunc(1, '') complete.omnifunc(1, '')
local result = complete.omnifunc(0, 'W') local result = complete.omnifunc(0, 'W')
assert.are.equal(1, #result) assert.are.equal(1, #result)
@ -102,9 +102,9 @@ describe('complete', function()
it('returns named dates for due:', function() it('returns named dates for due:', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task due: x' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task due: x' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 }) vim.api.nvim_win_set_cursor(0, { 1, 13 })
complete.omnifunc(1, '') complete.omnifunc(1, '')
local result = complete.omnifunc(0, '') local result = complete.omnifunc(0, '')
assert.is_true(#result > 0) assert.is_true(#result > 0)
@ -120,9 +120,9 @@ describe('complete', function()
it('filters dates by base prefix', function() it('filters dates by base prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task due:to' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task due:to' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 }) vim.api.nvim_win_set_cursor(0, { 1, 14 })
complete.omnifunc(1, '') complete.omnifunc(1, '')
local result = complete.omnifunc(0, 'to') local result = complete.omnifunc(0, 'to')
local words = {} local words = {}
@ -137,9 +137,9 @@ describe('complete', function()
it('returns recurrence shorthands for rec:', function() it('returns recurrence shorthands for rec:', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task rec: x' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task rec: x' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 15 }) vim.api.nvim_win_set_cursor(0, { 1, 13 })
complete.omnifunc(1, '') complete.omnifunc(1, '')
local result = complete.omnifunc(0, '') local result = complete.omnifunc(0, '')
assert.is_true(#result > 0) assert.is_true(#result > 0)
@ -155,9 +155,9 @@ describe('complete', function()
it('filters recurrence by base prefix', function() it('filters recurrence by base prefix', function()
local bufnr = vim.api.nvim_create_buf(false, true) local bufnr = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '- [ ] task rec:we' }) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { '[ ] task rec:we' })
vim.api.nvim_set_current_buf(bufnr) vim.api.nvim_set_current_buf(bufnr)
vim.api.nvim_win_set_cursor(0, { 1, 16 }) vim.api.nvim_win_set_cursor(0, { 1, 14 })
complete.omnifunc(1, '') complete.omnifunc(1, '')
local result = complete.omnifunc(0, 'we') local result = complete.omnifunc(0, 'we')
local words = {} local words = {}

View file

@ -22,11 +22,11 @@ describe('diff', function()
it('parses headers and tasks', function() it('parses headers and tasks', function()
local lines = { local lines = {
'# School', '# School',
'/1/- [ ] Do homework', '/1/[ ] Do homework',
'/2/- [!] Read chapter 5', '/2/[!] Read chapter 5',
'', '',
'# Errands', '# Errands',
'/3/- [ ] Buy groceries', '/3/[ ] Buy groceries',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal(6, #result) assert.are.equal(6, #result)
@ -45,7 +45,7 @@ describe('diff', function()
it('handles new tasks without ids', function() it('handles new tasks without ids', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] New task here', '[ ] New task here',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal(2, #result) assert.are.equal(2, #result)
@ -57,7 +57,7 @@ describe('diff', function()
it('inline cat: token overrides header category', function() it('inline cat: token overrides header category', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Buy milk cat:Work', '/1/[ ] Buy milk cat:Work',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal(2, #result) assert.are.equal(2, #result)
@ -68,7 +68,7 @@ describe('diff', function()
it('extracts rec: token from buffer line', function() it('extracts rec: token from buffer line', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Take trash out rec:weekly', '/1/[ ] Take trash out rec:weekly',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal('weekly', result[2].recur) assert.are.equal('weekly', result[2].recur)
@ -77,7 +77,7 @@ describe('diff', function()
it('extracts rec: with completion mode', function() it('extracts rec: with completion mode', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Water plants rec:!daily', '/1/[ ] Water plants rec:!daily',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal('daily', result[2].recur) assert.are.equal('daily', result[2].recur)
@ -87,7 +87,7 @@ describe('diff', function()
it('inline due: token is parsed', function() it('inline due: token is parsed', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Buy milk due:2026-03-15', '/1/[ ] Buy milk due:2026-03-15',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal(2, #result) assert.are.equal(2, #result)
@ -100,8 +100,8 @@ describe('diff', function()
it('creates new tasks from buffer lines', function() it('creates new tasks from buffer lines', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] First task', '[ ] First task',
'- [ ] Second task', '[ ] Second task',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -117,7 +117,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Keep me', '/1/[ ] Keep me',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -133,7 +133,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Renamed', '/1/[ ] Renamed',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -147,7 +147,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Renamed', '/1/[ ] Renamed',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -161,8 +161,8 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Original', '/1/[ ] Original',
'/1/- [ ] Copy of original', '/1/[ ] Copy of original',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -175,7 +175,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Work', '# Work',
'/1/- [ ] Moving task', '/1/[ ] Moving task',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -188,7 +188,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Stable task', '/1/[ ] Stable task',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -204,7 +204,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Pay bill', '/1/[ ] Pay bill',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -217,7 +217,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Pay bill due:2026-04-01', '/1/[ ] Pay bill due:2026-04-01',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -228,7 +228,7 @@ describe('diff', function()
it('stores recur field on new tasks from buffer', function() it('stores recur field on new tasks from buffer', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] Take out trash rec:weekly', '[ ] Take out trash rec:weekly',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -242,7 +242,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Todo', '# Todo',
'/1/- [ ] Task rec:weekly', '/1/[ ] Task rec:weekly',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -255,7 +255,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Todo', '# Todo',
'/1/- [ ] Task', '/1/[ ] Task',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -266,7 +266,7 @@ describe('diff', function()
it('parses rec: with completion mode prefix', function() it('parses rec: with completion mode prefix', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] Water plants rec:!weekly', '[ ] Water plants rec:!weekly',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -278,7 +278,7 @@ describe('diff', function()
it('returns forge refs for new tasks', function() it('returns forge refs for new tasks', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] Fix bug gh:user/repo#42', '[ ] Fix bug gh:user/repo#42',
} }
local refs = diff.apply(lines, s) local refs = diff.apply(lines, s)
assert.are.equal(1, #refs) assert.are.equal(1, #refs)
@ -303,7 +303,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Todo', '# Todo',
'/1/- [ ] Fix bug gh:user/repo#99', '/1/[ ] Fix bug gh:user/repo#99',
} }
local refs = diff.apply(lines, s) local refs = diff.apply(lines, s)
assert.are.equal(1, #refs) assert.are.equal(1, #refs)
@ -327,7 +327,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Todo', '# Todo',
'/1/- [ ] Fix bug gh:user/repo#42', '/1/[ ] Fix bug gh:user/repo#42',
} }
local refs = diff.apply(lines, s) local refs = diff.apply(lines, s)
assert.are.equal(0, #refs) assert.are.equal(0, #refs)
@ -336,7 +336,7 @@ describe('diff', function()
it('returns empty for tasks without forge refs', function() it('returns empty for tasks without forge refs', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] Plain task', '[ ] Plain task',
} }
local refs = diff.apply(lines, s) local refs = diff.apply(lines, s)
assert.are.equal(0, #refs) assert.are.equal(0, #refs)
@ -359,8 +359,8 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Todo', '# Todo',
'/1/- [ ] Fix bug gh:user/repo#42', '/1/[ ] Fix bug gh:user/repo#42',
'/1/- [ ] Fix bug gh:user/repo#42', '/1/[ ] Fix bug gh:user/repo#42',
} }
local refs = diff.apply(lines, s) local refs = diff.apply(lines, s)
assert.are.equal(1, #refs) assert.are.equal(1, #refs)
@ -372,7 +372,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [ ] Task name', '/1/[ ] Task name',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -383,7 +383,7 @@ describe('diff', function()
it('sets priority from +!! token', function() it('sets priority from +!! token', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] Pay bills +!!', '[ ] Pay bills +!!',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -396,7 +396,7 @@ describe('diff', function()
s:save() s:save()
local lines = { local lines = {
'# Inbox', '# Inbox',
'/1/- [!] Task name', '/1/[!] Task name',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()
@ -407,7 +407,7 @@ describe('diff', function()
it('parses metadata with forge ref on same line', function() it('parses metadata with forge ref on same line', function()
local lines = { local lines = {
'# Inbox', '# Inbox',
'- [ ] Fix bug due:2026-03-15 gh:user/repo#42', '[ ] Fix bug due:2026-03-15 gh:user/repo#42',
} }
diff.apply(lines, s) diff.apply(lines, s)
s:load() s:load()

View file

@ -230,7 +230,7 @@ describe('filter', function()
end end
local hidden_ids = { [hidden_task.id] = true } local hidden_ids = { [hidden_task.id] = true }
local lines = { local lines = {
'/1/- [ ] Visible task', '/1/[ ] Visible task',
} }
diff.apply(lines, s, hidden_ids) diff.apply(lines, s, hidden_ids)
s:load() s:load()
@ -254,7 +254,7 @@ describe('filter', function()
end end
end end
local lines = { local lines = {
'/' .. keep_task.id .. '/- [ ] Keep task', '/' .. keep_task.id .. '/[ ] Keep task',
} }
diff.apply(lines, s, {}) diff.apply(lines, s, {})
s:load() s:load()
@ -270,7 +270,7 @@ describe('filter', function()
local task = tasks[1] local task = tasks[1]
local lines = { local lines = {
'FILTER: cat:Work', 'FILTER: cat:Work',
'/' .. task.id .. '/- [ ] My task', '/' .. task.id .. '/[ ] My task',
} }
diff.apply(lines, s, {}) diff.apply(lines, s, {})
s:load() s:load()
@ -281,7 +281,7 @@ describe('filter', function()
it('parse_buffer skips FILTER: header line', function() it('parse_buffer skips FILTER: header line', function()
local lines = { local lines = {
'FILTER: overdue', 'FILTER: overdue',
'/1/- [ ] A task', '/1/[ ] A task',
} }
local result = diff.parse_buffer(lines) local result = diff.parse_buffer(lines)
assert.are.equal(1, #result) assert.are.equal(1, #result)

View file

@ -590,7 +590,7 @@ describe('forge diff integration', function()
local tmp = os.tmpname() local tmp = os.tmpname()
local s = store.new(tmp) local s = store.new(tmp)
s:load() s:load()
diff.apply({ '- [ ] Fix bug gh:user/repo#42' }, s) diff.apply({ '[ ] Fix bug gh:user/repo#42' }, s)
local tasks = s:active_tasks() local tasks = s:active_tasks()
assert.equals(1, #tasks) assert.equals(1, #tasks)
assert.equals('Fix bug gh:user/repo#42', tasks[1].description) assert.equals('Fix bug gh:user/repo#42', tasks[1].description)
@ -607,7 +607,7 @@ describe('forge diff integration', function()
s:load() s:load()
local task = s:add({ description = 'Fix bug' }) local task = s:add({ description = 'Fix bug' })
s:save() s:save()
diff.apply({ '/' .. task.id .. '/- [ ] Fix bug gh:user/repo#10' }, s) diff.apply({ '/' .. task.id .. '/[ ] Fix bug gh:user/repo#10' }, s)
local updated = s:get(task.id) local updated = s:get(task.id)
assert.equals('Fix bug gh:user/repo#10', updated.description) assert.equals('Fix bug gh:user/repo#10', updated.description)
assert.is_not_nil(updated._extra) assert.is_not_nil(updated._extra)
@ -634,7 +634,7 @@ describe('forge diff integration', function()
}, },
}) })
s:save() s:save()
diff.apply({ '/' .. task.id .. '/- [ ] Fix bug' }, s) diff.apply({ '/' .. task.id .. '/[ ] Fix bug' }, s)
local updated = s:get(task.id) local updated = s:get(task.id)
assert.is_not_nil(updated._extra._forge_ref) assert.is_not_nil(updated._extra._forge_ref)
assert.equals(1, updated._extra._forge_ref.number) assert.equals(1, updated._extra._forge_ref.number)
@ -645,7 +645,7 @@ describe('forge diff integration', function()
local tmp = os.tmpname() local tmp = os.tmpname()
local s = store.new(tmp) local s = store.new(tmp)
s:load() s:load()
diff.apply({ '- [ ] Check out gh:user/repo' }, s) diff.apply({ '[ ] Check out gh:user/repo' }, s)
local tasks = s:active_tasks() local tasks = s:active_tasks()
assert.equals(1, #tasks) assert.equals(1, #tasks)
assert.is_not_nil(tasks[1]._extra) assert.is_not_nil(tasks[1]._extra)

View file

@ -17,92 +17,92 @@ describe('textobj', function()
describe('inner_task_range', function() describe('inner_task_range', function()
it('returns description range for task with id prefix', function() it('returns description range for task with id prefix', function()
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries') local s, e = textobj.inner_task_range('/1/[ ] Buy groceries')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(22, e) assert.are.equal(20, e)
end) end)
it('returns description range for task without id prefix', function() it('returns description range for task without id prefix', function()
local s, e = textobj.inner_task_range('- [ ] Buy groceries') local s, e = textobj.inner_task_range('[ ] Buy groceries')
assert.are.equal(7, s) assert.are.equal(5, s)
assert.are.equal(19, e) assert.are.equal(17, e)
end) end)
it('excludes trailing due: token', function() it('excludes trailing due: token', function()
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries due:2026-03-15') local s, e = textobj.inner_task_range('/1/[ ] Buy groceries due:2026-03-15')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(22, e) assert.are.equal(20, e)
end) end)
it('excludes trailing cat: token', function() it('excludes trailing cat: token', function()
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries cat:Errands') local s, e = textobj.inner_task_range('/1/[ ] Buy groceries cat:Errands')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(22, e) assert.are.equal(20, e)
end) end)
it('excludes trailing rec: token', function() it('excludes trailing rec: token', function()
local s, e = textobj.inner_task_range('/1/- [ ] Take out trash rec:weekly') local s, e = textobj.inner_task_range('/1/[ ] Take out trash rec:weekly')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(23, e) assert.are.equal(21, e)
end) end)
it('excludes multiple trailing metadata tokens', function() it('excludes multiple trailing metadata tokens', function()
local s, e = local s, e =
textobj.inner_task_range('/1/- [ ] Buy milk due:2026-03-15 cat:Errands rec:weekly') textobj.inner_task_range('/1/[ ] Buy milk due:2026-03-15 cat:Errands rec:weekly')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(17, e) assert.are.equal(15, e)
end) end)
it('handles priority checkbox', function() it('handles priority checkbox', function()
local s, e = textobj.inner_task_range('/1/- [!] Important task') local s, e = textobj.inner_task_range('/1/[!] Important task')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(23, e) assert.are.equal(21, e)
end) end)
it('handles done checkbox', function() it('handles done checkbox', function()
local s, e = textobj.inner_task_range('/1/- [x] Finished task') local s, e = textobj.inner_task_range('/1/[x] Finished task')
assert.are.equal(10, s) assert.are.equal(8, 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) assert.are.equal(20, e)
end) end)
it('does not strip non-metadata tokens', function() it('handles multi-digit task ids', function()
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries for dinner') local s, e = textobj.inner_task_range('/123/[ ] Some task')
assert.are.equal(10, s) assert.are.equal(10, s)
assert.are.equal(33, e) assert.are.equal(18, 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(8, s)
assert.are.equal(31, e)
end) end)
it('stops stripping at first non-metadata token from right', function() 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') local s, e = textobj.inner_task_range('/1/[ ] Buy groceries for dinner due:2026-03-15')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(33, e) assert.are.equal(31, e)
end) end)
it('respects custom date_syntax', function() it('respects custom date_syntax', function()
vim.g.pending = { date_syntax = 'by' } vim.g.pending = { date_syntax = 'by' }
config.reset() config.reset()
local s, e = textobj.inner_task_range('/1/- [ ] Buy groceries by:2026-03-15') local s, e = textobj.inner_task_range('/1/[ ] Buy groceries by:2026-03-15')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(22, e) assert.are.equal(20, e)
end) end)
it('respects custom recur_syntax', function() it('respects custom recur_syntax', function()
vim.g.pending = { recur_syntax = 'repeat' } vim.g.pending = { recur_syntax = 'repeat' }
config.reset() config.reset()
local s, e = textobj.inner_task_range('/1/- [ ] Take trash repeat:weekly') local s, e = textobj.inner_task_range('/1/[ ] Take trash repeat:weekly')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(19, e) assert.are.equal(17, e)
end) end)
it('handles task with only metadata after description', function() it('handles task with only metadata after description', function()
local s, e = textobj.inner_task_range('/1/- [ ] X due:tomorrow') local s, e = textobj.inner_task_range('/1/[ ] X due:tomorrow')
assert.are.equal(10, s) assert.are.equal(8, s)
assert.are.equal(10, e) assert.are.equal(8, e)
end) end)
end) end)

View file

@ -112,10 +112,10 @@ describe('views', function()
task_line = lines[i] task_line = lines[i]
end end
end end
assert.are.equal('/1/- [ ] My task', task_line) assert.are.equal('/1/[ ] My task', task_line)
end) end)
it('formats priority task lines as /ID/- [!] description', function() it('formats priority task lines as /ID/[!] description', function()
s:add({ description = 'Important', category = 'Inbox', priority = 1 }) s:add({ description = 'Important', category = 'Inbox', priority = 1 })
local lines, meta = views.category_view(s:active_tasks()) local lines, meta = views.category_view(s:active_tasks())
local task_line local task_line
@ -124,7 +124,7 @@ describe('views', function()
task_line = lines[i] task_line = lines[i]
end end
end end
assert.are.equal('/1/- [!] Important', task_line) assert.are.equal('/1/[!] Important', task_line)
end) end)
it('sets LineMeta type=header for header lines with correct category', function() it('sets LineMeta type=header for header lines with correct category', function()
@ -352,10 +352,10 @@ describe('views', function()
assert.is_true(earlier_row < later_row) assert.is_true(earlier_row < later_row)
end) end)
it('formats task lines as /ID/- [ ] description', function() it('formats task lines as /ID/[ ] description', function()
s:add({ description = 'My task', category = 'Inbox' }) s:add({ description = 'My task', category = 'Inbox' })
local lines, _ = views.priority_view(s:active_tasks()) local lines, _ = views.priority_view(s:active_tasks())
assert.are.equal('/1/- [ ] My task', lines[1]) assert.are.equal('/1/[ ] My task', lines[1])
end) end)
it('sets show_category=true for all task meta entries', function() it('sets show_category=true for all task meta entries', function()