From 74137179323bdfc4d2ae29028318fea6181876be Mon Sep 17 00:00:00 2001 From: fanyunqian Date: Sun, 2 Jun 2024 18:09:47 +0800 Subject: [PATCH] feat(core): support org-enforce-todo-dependencies add warning for blocked tasks --- lua/orgmode/config/defaults.lua | 1 + lua/orgmode/org/mappings.lua | 25 +- tests/plenary/object/todo_dependency_spec.lua | 329 ++++++++++++++++++ 3 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 tests/plenary/object/todo_dependency_spec.lua diff --git a/lua/orgmode/config/defaults.lua b/lua/orgmode/config/defaults.lua index 37bebd999..afa2f5dae 100644 --- a/lua/orgmode/config/defaults.lua +++ b/lua/orgmode/config/defaults.lua @@ -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 diff --git a/lua/orgmode/org/mappings.lua b/lua/orgmode/org/mappings.lua index a12dc20a3..42c730a19 100644 --- a/lua/orgmode/org/mappings.lua +++ b/lua/orgmode/org/mappings.lua @@ -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 diff --git a/tests/plenary/object/todo_dependency_spec.lua b/tests/plenary/object/todo_dependency_spec.lua new file mode 100644 index 000000000..b0b06edb5 --- /dev/null +++ b/tests/plenary/object/todo_dependency_spec.lua @@ -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)