Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)