init.lua/lua/scratch/tasks.lua(view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
local cache = {}
local config = {
label = "done:%Y%m%d-%H%M",
archive_header = "# Archive",
tasks_file = vim.fn.stdpath "config" .. "/tasks.json",
}
-- TODO: add support for multiple line tasks
-- TODO: add "review" mode, show list of tasks that was done during this/previous week
-- TODO: undoing tasks, if task is marked as done, and has `done` label, it should replace done with `undone`
-- TODO: show the progress of tasks(if task has subtasks, show in virtual text how many of them is done) sub tasks should be only archived with the parent task
---@return string[]|nil
local function get_files()
if cache.files then
return cache.files
end
local f = io.open(config.tasks_file, "r")
if f then
local data = f:read "*a"
local ok, files = pcall(vim.json.decode, data)
if ok then
cache.files = files.files
return files.files
end
f:close()
end
return nil
end
---@return string
local function get_done_label()
return os.date(config.label) --[[@as string]]
end
---@param str string
---@return boolean
local function is_task(str)
return str:match "^%s*%- %[[x ]%]" ~= nil
end
---@param str string
---@return boolean res
local function is_task_labled(str)
return str:match "^%s*%- %[[x ]%] `%" ~= nil
end
---@param str string
---@return boolean
local function has_next_tag(str)
return str:match "%#next" ~= nil
end
---@param str string
---@return boolean
local function check_task_status(str)
return str:match "^(%s*%- )%[x%]" ~= nil
end
---@param str string
---@return string
local function remove_task_prefix(str)
local res = str:gsub("^%- %[ %] ", "")
return res
end
---@param str string
---@return string
local function remove_next_tag(str)
local res = str:gsub(" %#next", "")
return res
end
---@param str string
---@return string
local function remove_file_extension(str)
local res = (str:match "^(.-)%.%w+$" or str)
return res
end
-- converts a like with markdown task to completed task, and removes `#next` in it, if there's one
---@param str string
---@return string?
local function to_complete_task(str)
local label = get_done_label()
local task_prefix = str:match "^(%s*%- %[[x ]%])"
if not task_prefix then
return nil
end
str = task_prefix .. " `" .. label .. "`" .. str:sub(#task_prefix + 1)
str = str:gsub("^(%s*%- )%[%s*%]", "%1[x]")
str = remove_next_tag(str)
str = str:gsub("%s+$", "")
return str
end
---@param lines string[]
---@return number? - Line of the heading, nil if not found
local function find_archive_heading(lines)
local heading_line = nil
for i, line in ipairs(lines) do
if line:match("^%s*" .. config.archive_header) then
heading_line = i
break
end
end
return heading_line
end
local tasks = {}
function tasks.agenda()
-- parse all `task_files` for `#next` tag
-- FIXME: that's probably should be cached
local task_files = get_files()
if not task_files then
return
end
---@type table<string, {text: string, line: number}[]>
local agenda_tasks = {}
for _, fname in ipairs(task_files) do
local lines = vim.fn.readfile(fname)
for i, line in ipairs(lines) do
if is_task(line) and has_next_tag(line) then
agenda_tasks[fname] = agenda_tasks[fname] or {}
table.insert(agenda_tasks[fname], {
text = line,
line = i,
})
end
end
end
-- build the output
local output = {}
for fname, ftasks in pairs(agenda_tasks) do
for _, ftask in pairs(ftasks) do
local task = remove_file_extension(ftask.text)
task = remove_next_tag(task)
task = remove_task_prefix(task)
table.insert(output, {
lnum = ftask.line,
filename = fname,
text = task,
}--[[ @as vim.quickfix.entry ]])
end
end
vim.fn.setqflist(output, "r")
vim.cmd.copen()
end
function tasks.complete()
vim.cmd.mkview() -- saves current folds/scroll
local bufnr = vim.api.nvim_get_current_buf()
local cur_pos = vim.api.nvim_win_get_cursor(0)
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
local task_index = cur_pos[1]
-- if cursor is beyond last line, exit
if task_index > #lines then
vim.cmd.loadview()
return
end
if not is_task(lines[task_index]) then
vim.notify("Not a task", vim.log.levels.WARN)
vim.cmd.loadview()
return
end
if
check_task_status(lines[task_index])
and is_task_labled(lines[task_index])
then
vim.notify("Task already completed", vim.log.levels.ERROR)
vim.cmd.loadview()
return
end
local archived_heading = find_archive_heading(lines)
if archived_heading == nil then
table.insert(lines, "")
table.insert(lines, config.archive_header)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
archived_heading = #lines
end
local completed_task = to_complete_task(lines[task_index])
table.remove(lines, task_index)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
table.insert(lines, archived_heading, completed_task)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines)
vim.cmd "silent update"
vim.cmd.loadview()
end
return tasks
|