Skip to content

Commit

Permalink
feat(core): support org-enforce-todo-dependencies
Browse files Browse the repository at this point in the history
add warning for blocked tasks
  • Loading branch information
fanyunqian committed Jun 3, 2024
1 parent c34ae3c commit 7413717
Show file tree
Hide file tree
Showing 3 changed files with 354 additions and 1 deletion.
1 change: 1 addition & 0 deletions lua/orgmode/config/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local DefaultConfig = {
org_todo_keywords = { 'TODO', '|', 'DONE' },
org_todo_repeat_to_state = nil,
org_todo_keyword_faces = {},
org_enforce_todo_dependencies = false,
org_deadline_warning_days = 14,
org_agenda_min_height = 16,
org_agenda_span = 'week', -- day/week/month/year/number of days
Expand Down
25 changes: 24 additions & 1 deletion lua/orgmode/org/mappings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -376,12 +376,35 @@ function OrgMappings:toggle_heading()
vim.fn.setline('.', line)
end

---@param headline OrgHeadline
function OrgMappings:_has_unfinished_children(headline)
for _, h in ipairs(headline:get_child_headlines()) do
local was_done = h:is_done()
if not was_done then
return true
end
if OrgMappings:_has_unfinished_children(h) then
return true
end
end
return false
end

function OrgMappings:_todo_change_state(direction)
local headline = self.files:get_closest_headline()
local old_state = headline:get_todo()
local was_done = headline:is_done()
local changed = self:_change_todo_state(direction, true)
local force_dependent = config.org_enforce_todo_dependencies or false

if force_dependent then
local has_unfinished_children = OrgMappings:_has_unfinished_children(headline)
if has_unfinished_children then
utils.echo_warning(tostring(old_state) .. ' is blocked by unfinished sub-tasks.')
return
end
end

local changed = self:_change_todo_state(direction, true)
if not changed then
return
end
Expand Down
329 changes: 329 additions & 0 deletions tests/plenary/object/todo_dependency_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,329 @@
local config = require('orgmode.config')
local TodoState = require('orgmode.objects.todo_state')
local TodoKeyword = require('orgmode.objects.todo_keywords.todo_keyword')

local helpers = require('tests.plenary.helpers')
local api = require('orgmode.api')
local Date = require('orgmode.objects.date')
local OrgId = require('orgmode.org.id')
local orgmode = require('orgmode')

local M = {}
-- @param headline OrgApiHeadline
-- local M.vis_head = function (headline, indent)
function M:vis_head(headline, indent)
if headline == nil then
return
end
print(string.rep('>', indent or 0) .. ' ' .. headline.title)
for _, h in ipairs(headline.headlines) do
M:vis_head(h, (indent or 0) + 1)
end
end

function M:headline_has_unfinished_child(headline)
for _, h in ipairs(headline.headlines) do
if h.todo_type == 'TODO' then
return true
end
if M:headline_has_unfinished_child(h) then
return true
end
end
return false
end

describe('Todo mappings unfer force dependency', function()
before_each(function()
config:extend({ org_enforce_todo_dependencies = true })
end)
after_each(function()
vim.cmd([[silent! %bw!]])
end)
it('should change todo state of a headline forward (org_todo)', function()
helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
})
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
vim.fn.cursor(3, 1)

-- Changing to DONE and adding closed date
vim.cmd([[norm cit]])

assert.are.same({
'* DONE Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Removing todo keyword and removing closed date
vim.cmd([[norm cit]])
assert.are.same({
'* Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Setting TODO keyword, initial state
vim.cmd([[norm cit]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
end)

it('should change todo state of a headline forward (org_todo)', function()
helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
})
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
vim.fn.cursor(3, 1)

-- Changing to DONE and adding closed date
vim.cmd([[norm cit]])
assert.are.same({
'* DONE Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Removing todo keyword and removing closed date
vim.cmd([[norm cit]])
assert.are.same({
'* Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Setting TODO keyword, initial state
vim.cmd([[norm cit]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
end)

it('should change todo state of repeatable task and add last repeat property and state change (org_todo)', function()
helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
'',
'* TODO Another task',
})

assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
vim.fn.cursor(3, 1)
vim.cmd([[norm cit]])
vim.wait(50)
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
' :PROPERTIES:',
' :LAST_REPEAT: [' .. Date.now():to_string() .. ']',
' :END:',
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 10, false))
end)

it('should change todo state of repeatable task and not log last repeat date if disabled', function()
helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
'',
'* TODO Another task',
}, {
org_log_repeat = false,
})

assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
vim.fn.cursor(3, 1)
vim.cmd([[norm cit]])
vim.wait(50)
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 10, false))

config.org_log_repeat = 'time'
end)

it('should add last repeat property and state change to drawer (org_log_into_drawer)', function()
config:extend({
org_log_into_drawer = 'LOGBOOK',
})

helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
'',
'* TODO Another task',
})

assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
vim.fn.cursor(3, 1)
vim.cmd([[norm cit]])
vim.wait(50)
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
' :PROPERTIES:',
' :LAST_REPEAT: [' .. Date.now():to_string() .. ']',
' :END:',
' :LOGBOOK:',
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
' :END:',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 12, false))

vim.fn.cursor(3, 1)
vim.cmd([[norm cit]])
vim.wait(200)
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-09-21 Tue 12:00 +1w>',
' :PROPERTIES:',
' :LAST_REPEAT: [' .. Date.now():to_string() .. ']',
' :END:',
' :LOGBOOK:',
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
' :END:',
'',
'* TODO Another task',
}, vim.api.nvim_buf_get_lines(0, 2, 13, false))
end)

it('should change todo state of a headline backward (org_todo_prev)', function()
helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
})

assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
vim.fn.cursor(3, 1)

-- Removing todo keyword
vim.cmd([[norm ciT]])
assert.are.same({
'* Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Changing to DONE and adding closed date
vim.cmd([[norm ciT]])
assert.are.same({
'* DONE Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Setting TODO keyword, initial state
vim.cmd([[norm ciT]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
end)

it('should change todo state of a headline backward (org_todo_prev)', function()
helpers.create_agenda_file({
'#TITLE: Test',
'',
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
'** TODO Test orgmode 1',
})

assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
vim.fn.cursor(3, 1)

-- Removing todo keyword, but will fail because of dependency
vim.cmd([[norm ciT]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- Changing to DONE and adding closed date, but will fail because of dependency
vim.cmd([[norm ciT]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- remove TODO
vim.fn.cursor(5, 1)
vim.cmd([[norm ciT]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
'** Test orgmode 1',
}, vim.api.nvim_buf_get_lines(0, 2, 5, false))

-- toggle done
vim.cmd([[norm ciT]])
assert.are.same({
'* TODO Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
'** DONE Test orgmode 1',
' CLOSED: [' .. Date.now():to_string() .. ']',
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))

-- remove todo for parent
vim.fn.cursor(3, 1)
vim.cmd([[norm ciT]])
assert.are.same({
'* Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02>',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))

-- toggle done

vim.cmd([[norm ciT]])
assert.are.same({
'* DONE Test orgmode',
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
end)
end)

0 comments on commit 7413717

Please sign in to comment.