my neovim config, who would've thought

feat(tasks): add agenda view

olexsmir.xyz b57da0a7 9cdf4383

verified
Changed files
+116 -6
after
ftplugin
lua
core
scratch
+1
after/ftplugin/markdown.lua
··· 17 17 refactor = { pattern = "refactor%:", group = "@comment.info" }, 18 18 fix = { pattern = "fix%:", group = "@comment.error" }, 19 19 docs = { pattern = "docs%:", group = "@label" }, 20 + test = { pattern = "test%:", group = "@comment.note" }, 20 21 }, 21 22 }
+1
lua/core/keymaps.lua
··· 12 12 -- notes 13 13 u.map("n", "<leader>ot", "<cmd>e $HOME/org/todo.txt<cr>") --codespell:ignore 14 14 u.map("n", "<leader>oi", "<cmd>e $HOME/org/notes/Inbox/Inbox.md<cr>") 15 + u.map("n", "<leader>a", require("scratch.tasks").agenda) 15 16 16 17 -- general 17 18 u.map("n", "<leader>q", "<cmd>quit!<cr>")
+114 -6
lua/scratch/tasks.lua
··· 1 + ---@param name string 2 + ---@return string 3 + local function task_file(name) 4 + return vim.fs.joinpath(vim.fn.expand "~/org/notes", name .. ".md") 5 + end 6 + 1 7 local config = { 2 8 label = "done:%Y%m%d-%H%M", 3 9 archive_header = "# Archive", 10 + task_files = { 11 + task_file "TODO", 12 + task_file "Onasty", 13 + task_file "GBAC", 14 + task_file "Projects/gopher.nvim", 15 + }, 4 16 } 5 17 6 - -- TODO: highlight the `feat:`, `docs:`, `fix:`, probably should be done with `mini.hipatterns` 7 18 -- TODO: add support for multiple line tasks 8 19 -- TODO: undoing tasks, if task is marked as done, and has `done` label, it should replace done with `undone` 9 20 -- TODO: sort tasks under `# Tasks`, and move tasks with `#next` tag, on top 10 21 -- TODO: show the progress of tasks(if task has subtasks, show in virtual text how many of them is done) 11 22 -- sub tasks should be only archived with the parent task 23 + 24 + ---@param fpath string 25 + ---@return string 26 + local function file_name_from_path(fpath) 27 + return fpath:match "^.+/(.+)$" or fpath 28 + end 29 + 30 + ---@param short_name string 31 + ---@return string? 32 + local function full_file_path_from_short_name(short_name) 33 + for _, fname in ipairs(config.task_files) do 34 + if file_name_from_path(fname) == short_name .. ".md" then 35 + return fname 36 + end 37 + end 38 + return nil 39 + end 12 40 13 41 ---@return string 14 42 local function get_done_label() ··· 23 51 24 52 ---@param str string 25 53 ---@return boolean 54 + local function has_next_tag(str) 55 + return str:match "%#next" ~= nil 56 + end 57 + 58 + ---@param str string 59 + ---@return boolean 26 60 local function check_task_status(str) 27 61 return str:match "^(%s*%- )%[x%]" ~= nil 62 + end 63 + 64 + ---@param str string 65 + ---@return string 66 + local function remove_task_prefix(str) 67 + local res = str:gsub("^%- %[ %] ", "") 68 + return res 69 + end 70 + 71 + ---@param fname string 72 + ---@param line number 73 + ---@return string 74 + local function display_file(fname, line) 75 + local str = "" .. (fname:match "^(.-)%.%w+$" or fname) .. ":" .. line 76 + return (str .. string.rep(" ", 14 - #str)) 28 77 end 29 78 30 79 -- converts a like with markdown task to completed task, and removes `#next` in it, if there's one ··· 60 109 61 110 local tasks = {} 62 111 63 - function tasks.list_undone() 64 - error "unimplemented" 65 - end 112 + function tasks.agenda() 113 + -- parse all `task_files` for `#next` tag 114 + -- FIXME: that's probably should be cached 66 115 67 - function tasks.list_done() 68 - error "unimplemented" 116 + ---@type table<string, {task: string, line: number}[]> 117 + local agenda_tasks = {} 118 + for _, fname in ipairs(config.task_files) do 119 + local lines = vim.fn.readfile(fname) 120 + for i, line in ipairs(lines) do 121 + if is_task(line) and has_next_tag(line) then 122 + local short_name = file_name_from_path(fname) 123 + agenda_tasks[short_name] = agenda_tasks[short_name] or {} 124 + table.insert(agenda_tasks[short_name], { task = line, line = i }) 125 + end 126 + end 127 + end 128 + 129 + -- build the output 130 + local output = { "# Agenda" } 131 + for fname, ftasks in pairs(agenda_tasks) do 132 + for _, ftask in pairs(ftasks) do 133 + table.insert( 134 + output, 135 + display_file(fname, ftask.line) .. " " .. remove_task_prefix(ftask.task) 136 + ) 137 + end 138 + end 139 + 140 + -- open the agenda view 141 + vim.cmd.new() 142 + local buf = vim.api.nvim_get_current_buf() 143 + vim.api.nvim_buf_set_name(buf, "scratch.tasks:agenda") 144 + vim.api.nvim_set_option_value("filetype", "markdown", { buf = buf }) 145 + vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = buf }) 146 + vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf }) 147 + vim.api.nvim_set_option_value("swapfile", false, { buf = buf }) 148 + vim.api.nvim_buf_set_lines(buf, 0, -1, false, output) 149 + vim.api.nvim_set_option_value("modifiable", false, { buf = buf }) 150 + vim.api.nvim_exec_autocmds("FileType", { buffer = buf }) -- loads the ftplugins 151 + vim.api.nvim_win_set_height(0, 10) 152 + vim.api.nvim_win_set_cursor(0, { 2, 0 }) 153 + 154 + -- FIXME: this should be a separate function 155 + vim.keymap.set("n", "<CR>", function() 156 + local line = vim.api.nvim_get_current_line() 157 + local fname, lineno = line:match "^([^:]+):(%d+)" 158 + if fname == nil or lineno == nil then 159 + vim.notify( 160 + "No file name or line number found in the line", 161 + vim.log.levels.WARN 162 + ) 163 + return 164 + end 165 + 166 + local fpath = full_file_path_from_short_name(fname) 167 + if fpath == nil then 168 + vim.notify("No file name found in the line", vim.log.levels.WARN) 169 + return 170 + end 171 + 172 + vim.cmd.edit(fpath) 173 + vim.api.nvim_win_set_cursor(0, { tonumber(lineno), 0 }) 174 + end, { buffer = buf, desc = "Open file under cursor", silent = true }) 69 175 end 176 + 177 + tasks.agenda() 70 178 71 179 function tasks.complete() 72 180 vim.cmd.mkview() -- saves current folds/scroll