my neovim config, who would've thought
at main 5.2 kB view raw
1local config = { 2 label = "done:%Y%m%d-%H%M", 3 archive_header = "# Archive", 4 tasks_file = vim.fn.stdpath "config" .. "/tasks.json", 5} 6 7local ns = vim.api.nvim_create_namespace "scratch.tasks" 8 9---@return string[] 10local function get_tasks_files() 11 local f = io.open(config.tasks_file, "r") 12 if not f then 13 error("cannot read " .. config.tasks_file) 14 end 15 return vim.json.decode(f:read "*a")["files"] or error "'files' is not found" 16end 17 18local function display_filename(str) 19 local res = vim.fs.basename(str) 20 res = res:gsub("%.%w+$", "") 21 return res 22end 23 24---@param str string 25local function is_task(str) 26 return str:match "^%s*%- %[[x ]%]" ~= nil 27end 28 29---@param str string 30local function is_task_labled(str) 31 return str:match "^%s*%- %[[x ]%] `" ~= nil 32end 33 34---@param str string 35local function has_next_tag(str) 36 return str:match "%#n[ext]*" ~= nil 37end 38 39---@param str string 40local function is_task_complete(str) 41 return str:match "^(%s*%- )%[x%]" ~= nil 42end 43 44---@param str string 45local function remove_task_link(str) 46 local res = str:gsub("%[%[(.-)%]%]", "[%1]") 47 return res 48end 49 50---@param str string 51local function remove_next_tag(str) 52 local res = str:gsub(" %#n[ext]*", "") 53 return res 54end 55 56---@param str string 57local function to_complete_task(str) 58 local task_prefix = str:match "^(%s*%- %[[x ]%])" 59 if not task_prefix then 60 return nil 61 end 62 63 local label = os.date(config.label) --[[@as string]] 64 str = task_prefix .. " `" .. label .. "`" .. str:sub(#task_prefix + 1) 65 str = str:gsub("^(%s*%- )%[%s*%]", "%1[x]") -- mark task as complete 66 str = remove_task_link(str) 67 str = remove_next_tag(str) 68 str = str:gsub("%s+$", "") -- white space in the end 69 return str 70end 71 72---@param lines string[] 73---@return number? - Line of the heading, nil if not found 74local function find_archive_heading(lines) 75 return vim.iter(ipairs(lines)):find(function(lnum, line) 76 return line:match("^%s*" .. config.archive_header) ~= nil and lnum 77 end) 78end 79 80local tasks = {} 81function tasks.agenda() 82 local qf_items = vim 83 .iter(get_tasks_files()) 84 :map(function(fname) 85 return vim 86 .iter(ipairs(vim.fn.readfile(fname))) 87 :filter(function(_, line) 88 return is_task(line) and has_next_tag(line) 89 end) 90 :map(function(lnum, line) 91 local task = remove_next_tag(line) 92 task = remove_task_link(task) 93 task = task:gsub("^%- %[ %] ", "") -- remove task prefix 94 task = task:gsub("`", "") 95 96 return { 97 lnum = lnum, 98 filename = fname, 99 text = task, 100 user_data = { filename = fname }, 101 } --[[@as vim.quickfix.entry]] 102 end) 103 :totable() 104 end) 105 :flatten() 106 :totable() 107 108 vim.fn.setqflist({}, "r", { 109 nr = "$", 110 title = "scratch.tasks", 111 items = qf_items, 112 quickfixtextfunc = function(info) 113 local items = vim.fn.getqflist({ id = info.id, items = 1 }).items 114 local lines, highlights = {}, {} 115 for item = info.start_idx, info.end_idx do 116 local entry = items[item] 117 local fname = display_filename(entry.user_data.filename) 118 local fname_paded = fname .. string.rep(" ", 7 - #fname) .. " " 119 120 table.insert(lines, fname_paded .. entry.text) 121 table.insert(highlights, { fname_len = #fname_paded }) 122 end 123 124 vim.schedule(function() 125 vim.api.nvim_buf_clear_namespace(0, ns, 0, -1) 126 for i, hl in ipairs(highlights) do 127 local line = info.start_idx + i - 2 128 vim.hl.range(0, ns, "qfFileName", { line, 0 }, { line, hl.fname_len }) 129 vim.hl.range(0, ns, "Bold", { line, hl.fname_len }, { line, -1 }) 130 end 131 end) 132 133 return lines 134 end, 135 }) 136 137 vim.cmd.copen() 138end 139 140function tasks.complete() 141 local bufnr = vim.api.nvim_get_current_buf() 142 local task_idx = vim.api.nvim_win_get_cursor(0)[1] 143 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 144 local task = lines[task_idx] 145 146 if not is_task(task) then 147 vim.notify("Not a task", vim.log.levels.ERROR) 148 return 149 end 150 151 if is_task_complete(task) and is_task_labled(task) then 152 vim.notify("Task already completed", vim.log.levels.ERROR) 153 return 154 end 155 156 local archived_heading = find_archive_heading(lines) 157 if archived_heading == nil then 158 table.insert(lines, "") 159 table.insert(lines, config.archive_header) 160 archived_heading = #lines 161 end 162 163 local completed_task = to_complete_task(task) 164 table.remove(lines, task_idx) 165 table.insert(lines, archived_heading, completed_task) 166 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) 167 168 vim.cmd.update() 169end 170 171function tasks.clear_archive() 172 local bufnr = vim.api.nvim_get_current_buf() 173 local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 174 175 local archived_heading = find_archive_heading(lines) 176 if not archived_heading then 177 vim.notify("Looks like there's no archive of tasks", vim.log.levels.ERROR) 178 end 179 180 -- minus 2 because = 1(the archive heading) + 1(empty line before it) 181 lines = vim.list_slice(lines, 1, archived_heading - 2) 182 vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) 183 184 vim.cmd.update() 185end 186 187return tasks