Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): support org-enforce-todo-dependencies #741

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this local variable? It is not used later on, isn't it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if not was_done then
if not h:is_done() then
return true
end

return true
end
if OrgMappings:_has_unfinished_children(h) then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I stumbled over this recursion and after some moments I recognized, that not every headline is a todo. I think this is worth a comment.

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')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate, that you wrote tests. I saw that some of them are actually testing multiple things. I'm my experience this can make a regression harder to track, although it's much better than having no test. Would you mind to split the tests up a little bit?

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove dead code.

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo?

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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate test name? Can you reflect the difference to the former test in the name?

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)
Loading