Skip to content

Commit 7413717

Browse files
author
fanyunqian
committed
feat(core): support org-enforce-todo-dependencies
add warning for blocked tasks
1 parent c34ae3c commit 7413717

File tree

3 files changed

+354
-1
lines changed

3 files changed

+354
-1
lines changed

Diff for: lua/orgmode/config/defaults.lua

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local DefaultConfig = {
99
org_todo_keywords = { 'TODO', '|', 'DONE' },
1010
org_todo_repeat_to_state = nil,
1111
org_todo_keyword_faces = {},
12+
org_enforce_todo_dependencies = false,
1213
org_deadline_warning_days = 14,
1314
org_agenda_min_height = 16,
1415
org_agenda_span = 'week', -- day/week/month/year/number of days

Diff for: lua/orgmode/org/mappings.lua

+24-1
Original file line numberDiff line numberDiff line change
@@ -376,12 +376,35 @@ function OrgMappings:toggle_heading()
376376
vim.fn.setline('.', line)
377377
end
378378

379+
---@param headline OrgHeadline
380+
function OrgMappings:_has_unfinished_children(headline)
381+
for _, h in ipairs(headline:get_child_headlines()) do
382+
local was_done = h:is_done()
383+
if not was_done then
384+
return true
385+
end
386+
if OrgMappings:_has_unfinished_children(h) then
387+
return true
388+
end
389+
end
390+
return false
391+
end
392+
379393
function OrgMappings:_todo_change_state(direction)
380394
local headline = self.files:get_closest_headline()
381395
local old_state = headline:get_todo()
382396
local was_done = headline:is_done()
383-
local changed = self:_change_todo_state(direction, true)
397+
local force_dependent = config.org_enforce_todo_dependencies or false
384398

399+
if force_dependent then
400+
local has_unfinished_children = OrgMappings:_has_unfinished_children(headline)
401+
if has_unfinished_children then
402+
utils.echo_warning(tostring(old_state) .. ' is blocked by unfinished sub-tasks.')
403+
return
404+
end
405+
end
406+
407+
local changed = self:_change_todo_state(direction, true)
385408
if not changed then
386409
return
387410
end

Diff for: tests/plenary/object/todo_dependency_spec.lua

+329
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
local config = require('orgmode.config')
2+
local TodoState = require('orgmode.objects.todo_state')
3+
local TodoKeyword = require('orgmode.objects.todo_keywords.todo_keyword')
4+
5+
local helpers = require('tests.plenary.helpers')
6+
local api = require('orgmode.api')
7+
local Date = require('orgmode.objects.date')
8+
local OrgId = require('orgmode.org.id')
9+
local orgmode = require('orgmode')
10+
11+
local M = {}
12+
-- @param headline OrgApiHeadline
13+
-- local M.vis_head = function (headline, indent)
14+
function M:vis_head(headline, indent)
15+
if headline == nil then
16+
return
17+
end
18+
print(string.rep('>', indent or 0) .. ' ' .. headline.title)
19+
for _, h in ipairs(headline.headlines) do
20+
M:vis_head(h, (indent or 0) + 1)
21+
end
22+
end
23+
24+
function M:headline_has_unfinished_child(headline)
25+
for _, h in ipairs(headline.headlines) do
26+
if h.todo_type == 'TODO' then
27+
return true
28+
end
29+
if M:headline_has_unfinished_child(h) then
30+
return true
31+
end
32+
end
33+
return false
34+
end
35+
36+
describe('Todo mappings unfer force dependency', function()
37+
before_each(function()
38+
config:extend({ org_enforce_todo_dependencies = true })
39+
end)
40+
after_each(function()
41+
vim.cmd([[silent! %bw!]])
42+
end)
43+
it('should change todo state of a headline forward (org_todo)', function()
44+
helpers.create_agenda_file({
45+
'#TITLE: Test',
46+
'',
47+
'* TODO Test orgmode',
48+
' DEADLINE: <2021-07-21 Wed 22:02>',
49+
})
50+
assert.are.same({
51+
'* TODO Test orgmode',
52+
' DEADLINE: <2021-07-21 Wed 22:02>',
53+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
54+
vim.fn.cursor(3, 1)
55+
56+
-- Changing to DONE and adding closed date
57+
vim.cmd([[norm cit]])
58+
59+
assert.are.same({
60+
'* DONE Test orgmode',
61+
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
62+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
63+
64+
-- Removing todo keyword and removing closed date
65+
vim.cmd([[norm cit]])
66+
assert.are.same({
67+
'* Test orgmode',
68+
' DEADLINE: <2021-07-21 Wed 22:02>',
69+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
70+
71+
-- Setting TODO keyword, initial state
72+
vim.cmd([[norm cit]])
73+
assert.are.same({
74+
'* TODO Test orgmode',
75+
' DEADLINE: <2021-07-21 Wed 22:02>',
76+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
77+
end)
78+
79+
it('should change todo state of a headline forward (org_todo)', function()
80+
helpers.create_agenda_file({
81+
'#TITLE: Test',
82+
'',
83+
'* TODO Test orgmode',
84+
' DEADLINE: <2021-07-21 Wed 22:02>',
85+
})
86+
assert.are.same({
87+
'* TODO Test orgmode',
88+
' DEADLINE: <2021-07-21 Wed 22:02>',
89+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
90+
vim.fn.cursor(3, 1)
91+
92+
-- Changing to DONE and adding closed date
93+
vim.cmd([[norm cit]])
94+
assert.are.same({
95+
'* DONE Test orgmode',
96+
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
97+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
98+
99+
-- Removing todo keyword and removing closed date
100+
vim.cmd([[norm cit]])
101+
assert.are.same({
102+
'* Test orgmode',
103+
' DEADLINE: <2021-07-21 Wed 22:02>',
104+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
105+
106+
-- Setting TODO keyword, initial state
107+
vim.cmd([[norm cit]])
108+
assert.are.same({
109+
'* TODO Test orgmode',
110+
' DEADLINE: <2021-07-21 Wed 22:02>',
111+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
112+
end)
113+
114+
it('should change todo state of repeatable task and add last repeat property and state change (org_todo)', function()
115+
helpers.create_agenda_file({
116+
'#TITLE: Test',
117+
'',
118+
'* TODO Test orgmode',
119+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
120+
'',
121+
'* TODO Another task',
122+
})
123+
124+
assert.are.same({
125+
'* TODO Test orgmode',
126+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
127+
'',
128+
'* TODO Another task',
129+
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
130+
vim.fn.cursor(3, 1)
131+
vim.cmd([[norm cit]])
132+
vim.wait(50)
133+
assert.are.same({
134+
'* TODO Test orgmode',
135+
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
136+
' :PROPERTIES:',
137+
' :LAST_REPEAT: [' .. Date.now():to_string() .. ']',
138+
' :END:',
139+
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
140+
'',
141+
'* TODO Another task',
142+
}, vim.api.nvim_buf_get_lines(0, 2, 10, false))
143+
end)
144+
145+
it('should change todo state of repeatable task and not log last repeat date if disabled', function()
146+
helpers.create_agenda_file({
147+
'#TITLE: Test',
148+
'',
149+
'* TODO Test orgmode',
150+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
151+
'',
152+
'* TODO Another task',
153+
}, {
154+
org_log_repeat = false,
155+
})
156+
157+
assert.are.same({
158+
'* TODO Test orgmode',
159+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
160+
'',
161+
'* TODO Another task',
162+
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
163+
vim.fn.cursor(3, 1)
164+
vim.cmd([[norm cit]])
165+
vim.wait(50)
166+
assert.are.same({
167+
'* TODO Test orgmode',
168+
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
169+
'',
170+
'* TODO Another task',
171+
}, vim.api.nvim_buf_get_lines(0, 2, 10, false))
172+
173+
config.org_log_repeat = 'time'
174+
end)
175+
176+
it('should add last repeat property and state change to drawer (org_log_into_drawer)', function()
177+
config:extend({
178+
org_log_into_drawer = 'LOGBOOK',
179+
})
180+
181+
helpers.create_agenda_file({
182+
'#TITLE: Test',
183+
'',
184+
'* TODO Test orgmode',
185+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
186+
'',
187+
'* TODO Another task',
188+
})
189+
190+
assert.are.same({
191+
'* TODO Test orgmode',
192+
' DEADLINE: <2021-09-07 Tue 12:00 +1w>',
193+
'',
194+
'* TODO Another task',
195+
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
196+
vim.fn.cursor(3, 1)
197+
vim.cmd([[norm cit]])
198+
vim.wait(50)
199+
assert.are.same({
200+
'* TODO Test orgmode',
201+
' DEADLINE: <2021-09-14 Tue 12:00 +1w>',
202+
' :PROPERTIES:',
203+
' :LAST_REPEAT: [' .. Date.now():to_string() .. ']',
204+
' :END:',
205+
' :LOGBOOK:',
206+
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
207+
' :END:',
208+
'',
209+
'* TODO Another task',
210+
}, vim.api.nvim_buf_get_lines(0, 2, 12, false))
211+
212+
vim.fn.cursor(3, 1)
213+
vim.cmd([[norm cit]])
214+
vim.wait(200)
215+
assert.are.same({
216+
'* TODO Test orgmode',
217+
' DEADLINE: <2021-09-21 Tue 12:00 +1w>',
218+
' :PROPERTIES:',
219+
' :LAST_REPEAT: [' .. Date.now():to_string() .. ']',
220+
' :END:',
221+
' :LOGBOOK:',
222+
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
223+
' - State "DONE" from "TODO" [' .. Date.now():to_string() .. ']',
224+
' :END:',
225+
'',
226+
'* TODO Another task',
227+
}, vim.api.nvim_buf_get_lines(0, 2, 13, false))
228+
end)
229+
230+
it('should change todo state of a headline backward (org_todo_prev)', function()
231+
helpers.create_agenda_file({
232+
'#TITLE: Test',
233+
'',
234+
'* TODO Test orgmode',
235+
' DEADLINE: <2021-07-21 Wed 22:02>',
236+
})
237+
238+
assert.are.same({
239+
'* TODO Test orgmode',
240+
' DEADLINE: <2021-07-21 Wed 22:02>',
241+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
242+
vim.fn.cursor(3, 1)
243+
244+
-- Removing todo keyword
245+
vim.cmd([[norm ciT]])
246+
assert.are.same({
247+
'* Test orgmode',
248+
' DEADLINE: <2021-07-21 Wed 22:02>',
249+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
250+
251+
-- Changing to DONE and adding closed date
252+
vim.cmd([[norm ciT]])
253+
assert.are.same({
254+
'* DONE Test orgmode',
255+
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
256+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
257+
258+
-- Setting TODO keyword, initial state
259+
vim.cmd([[norm ciT]])
260+
assert.are.same({
261+
'* TODO Test orgmode',
262+
' DEADLINE: <2021-07-21 Wed 22:02>',
263+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
264+
end)
265+
266+
it('should change todo state of a headline backward (org_todo_prev)', function()
267+
helpers.create_agenda_file({
268+
'#TITLE: Test',
269+
'',
270+
'* TODO Test orgmode',
271+
' DEADLINE: <2021-07-21 Wed 22:02>',
272+
'** TODO Test orgmode 1',
273+
})
274+
275+
assert.are.same({
276+
'* TODO Test orgmode',
277+
' DEADLINE: <2021-07-21 Wed 22:02>',
278+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
279+
vim.fn.cursor(3, 1)
280+
281+
-- Removing todo keyword, but will fail because of dependency
282+
vim.cmd([[norm ciT]])
283+
assert.are.same({
284+
'* TODO Test orgmode',
285+
' DEADLINE: <2021-07-21 Wed 22:02>',
286+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
287+
288+
-- Changing to DONE and adding closed date, but will fail because of dependency
289+
vim.cmd([[norm ciT]])
290+
assert.are.same({
291+
'* TODO Test orgmode',
292+
' DEADLINE: <2021-07-21 Wed 22:02>',
293+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
294+
295+
-- remove TODO
296+
vim.fn.cursor(5, 1)
297+
vim.cmd([[norm ciT]])
298+
assert.are.same({
299+
'* TODO Test orgmode',
300+
' DEADLINE: <2021-07-21 Wed 22:02>',
301+
'** Test orgmode 1',
302+
}, vim.api.nvim_buf_get_lines(0, 2, 5, false))
303+
304+
-- toggle done
305+
vim.cmd([[norm ciT]])
306+
assert.are.same({
307+
'* TODO Test orgmode',
308+
' DEADLINE: <2021-07-21 Wed 22:02>',
309+
'** DONE Test orgmode 1',
310+
' CLOSED: [' .. Date.now():to_string() .. ']',
311+
}, vim.api.nvim_buf_get_lines(0, 2, 6, false))
312+
313+
-- remove todo for parent
314+
vim.fn.cursor(3, 1)
315+
vim.cmd([[norm ciT]])
316+
assert.are.same({
317+
'* Test orgmode',
318+
' DEADLINE: <2021-07-21 Wed 22:02>',
319+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
320+
321+
-- toggle done
322+
323+
vim.cmd([[norm ciT]])
324+
assert.are.same({
325+
'* DONE Test orgmode',
326+
' DEADLINE: <2021-07-21 Wed 22:02> CLOSED: [' .. Date.now():to_string() .. ']',
327+
}, vim.api.nvim_buf_get_lines(0, 2, 4, false))
328+
end)
329+
end)

0 commit comments

Comments
 (0)