From c9a0fa95d8bc091faa29708bff65dda6f8f78ee4 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Tue, 11 Mar 2025 18:54:58 +0000 Subject: [PATCH 01/19] WIP: Add task command --- lua/obsidian/client.lua | 32 +++++++++++++++ lua/obsidian/commands/init.lua | 17 ++++++++ lua/obsidian/commands/tasks.lua | 72 +++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 lua/obsidian/commands/tasks.lua diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index b456fbfb..0073e3f8 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2076,4 +2076,36 @@ Client.picker = function(self, picker_name) return require("obsidian.pickers").get(self, picker_name) end +---@class obsidian.Task +--- +---@field path string The path to the note. +---@field line integer The line number (1-indexed) where the task was found. +---@field description string The text of the line where the task was found. +---@field status string The status of the task. + +--todo: support for task status +--todo: make it async +--todo: can we reuse the search function? move the result parsing to the command +---@return obsidian.Task[] +Client.find_tasks = function(self) + local openTasks = search.search(self.dir, "- \\[(.)\\]", search.SearchOpts.default()) + --- @type obsidian.Task[] + local result = {} + + --- @type MatchData|? + local taskMatch = openTasks() + while taskMatch do + -- matching in lua since ripgrep doesn't support capturing groups in the --json output + local status, description = string.match(taskMatch.lines.text, "- %[(.)%] (.*)") + result[#result + 1] = { + status = status, + description = description, + line = taskMatch.line_number, + path = taskMatch.path.text, + } + taskMatch = openTasks() + end + return result +end + return Client diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index f31eaf8e..ee5bb76f 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -26,6 +26,7 @@ local command_lookups = { ObsidianExtractNote = "obsidian.commands.extract_note", ObsidianDebug = "obsidian.commands.debug", ObsidianTOC = "obsidian.commands.toc", + ObsidianTasks = "obsidian.commands.tasks", } local M = setmetatable({ @@ -190,5 +191,21 @@ M.register( M.register("ObsidianDebug", { opts = { nargs = 0, desc = "Log some information for debugging" } }) M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of contents into a picker" } }) +-- register the tasks command, the task should accept one argument that can be done or todo +M.register("ObsidianTasks", { + opts = { + nargs = "*", + complete = function(arglead, cmdline, cursorpos) + return { + "done", + "todo", + "cancelled", + "urgent", + "in progress", + } + end, + desc = "List all tasks", + }, +}) return M diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua new file mode 100644 index 00000000..cd86667f --- /dev/null +++ b/lua/obsidian/commands/tasks.lua @@ -0,0 +1,72 @@ + +-- TODO: Use config settings for icons +local task_configs = { + ["x"] = { name = "done", char = "" }, + [" "] = { name = "todo", char = "󰄱" }, + ["~"] = { name = "cancelled", char = "󰰱" }, + ["-"] = { name = "cancelled", char = "󰰱" }, + ["!"] = { name = "urgent", char = "" }, + [">"] = { name = "in progress", char = "" }, + ["/"] = { name = "in progress", char = "" }, +} + +-- Map states by name for quick lookup +local states = {} +for _, config in pairs(task_configs) do + states[config.name] = config +end + +--- Fetch the next task state in a cycle +---@param current_state string? +---@return string? next_state +local function get_next_state(current_state) + local state_names = vim.tbl_keys(states) + table.sort(state_names) -- Ensures predictable order + for i, name in ipairs(state_names) do + if name == current_state then + return state_names[i % #state_names + 1] + end + end + return state_names[1] -- Default to first state if none found +end + +--- Show tasks with optional filtering +---@param client obsidian.Client +---@param data table +local function showTasks(client, data) + assert(client, "Client is required") + + local filter = data.fargs[1] + local picker = assert(client:picker(), "No picker configured") + + local tasks = client:find_tasks() + local toShow = {} + + -- TODO: Hide filename, show only the task + for _, task in ipairs(tasks) do + local tStatus = task_configs[task.status] + if tStatus and (not filter or tStatus.name == filter) then + table.insert(toShow, { + display = string.format(" %s", task.description), + filename = task.path, + lnum = task.line, + icon = tStatus.char, + }) + end + end + + picker:pick(toShow, { + prompt_title = filter and (filter .. " tasks") or "tasks", + query_mappings = { + [""] = { + desc = "Toggle task status", + callback = function() + local next_state_name = get_next_state(filter) + showTasks(client, { fargs = { next_state_name } }) + end, + }, + }, + }) +end + +return showTasks From b0bebf0b12b18f899bdc6688f2fc6b87e21f6545 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Fri, 14 Mar 2025 06:45:32 +0000 Subject: [PATCH 02/19] remove hardcoded states from command --- lua/obsidian/commands/init.lua | 16 +--------------- lua/obsidian/commands/tasks.lua | 1 - 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index ee5bb76f..16438d7d 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -192,20 +192,6 @@ M.register("ObsidianDebug", { opts = { nargs = 0, desc = "Log some information f M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of contents into a picker" } }) -- register the tasks command, the task should accept one argument that can be done or todo -M.register("ObsidianTasks", { - opts = { - nargs = "*", - complete = function(arglead, cmdline, cursorpos) - return { - "done", - "todo", - "cancelled", - "urgent", - "in progress", - } - end, - desc = "List all tasks", - }, -}) +M.register("ObsidianTasks", { opts = { nargs = 0, desc = "List all tasks" } }) return M diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua index cd86667f..ea19a919 100644 --- a/lua/obsidian/commands/tasks.lua +++ b/lua/obsidian/commands/tasks.lua @@ -1,4 +1,3 @@ - -- TODO: Use config settings for icons local task_configs = { ["x"] = { name = "done", char = "" }, From 8ae86a5d766ac84983f855dfdfafafff0a5cded2 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Thu, 20 Mar 2025 08:22:26 +0000 Subject: [PATCH 03/19] Update task order --- lua/obsidian/commands/tasks.lua | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua index ea19a919..b3cd1362 100644 --- a/lua/obsidian/commands/tasks.lua +++ b/lua/obsidian/commands/tasks.lua @@ -1,17 +1,18 @@ -- TODO: Use config settings for icons local task_configs = { - ["x"] = { name = "done", char = "" }, - [" "] = { name = "todo", char = "󰄱" }, - ["~"] = { name = "cancelled", char = "󰰱" }, - ["-"] = { name = "cancelled", char = "󰰱" }, - ["!"] = { name = "urgent", char = "" }, - [">"] = { name = "in progress", char = "" }, - ["/"] = { name = "in progress", char = "" }, + -- in progress + ["/"] = { char = " ", order = 0, name = "in progress", hl_group = "ObsidianRightArrow" }, + -- todo + [" "] = { char = "󰄱 ", order = 1, name = "todo", hl_group = "ObsidianTodo" }, + -- done + ["x"] = { char = " ", order = 2, name = "done", hl_group = "ObsidianDone" }, + -- cancelled + ["-"] = { char = " ", order = 3, name = "cancelled", hl_group = "ObsidianTilde" }, } -- Map states by name for quick lookup local states = {} -for _, config in pairs(task_configs) do +for _, config in pairs(task_configs or {}) do states[config.name] = config end @@ -20,7 +21,10 @@ end ---@return string? next_state local function get_next_state(current_state) local state_names = vim.tbl_keys(states) - table.sort(state_names) -- Ensures predictable order + -- sort state names by order + table.sort(state_names, function(a, b) + return (states[a].order or 0) < (states[b].order or 0) + end) for i, name in ipairs(state_names) do if name == current_state then return state_names[i % #state_names + 1] From 46b9f44d83ca851b86c5451169cd3a456f57bc27 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Fri, 21 Mar 2025 06:55:21 +0000 Subject: [PATCH 04/19] Use configs for task ordering --- lua/obsidian/commands/tasks.lua | 63 +++++++++++++++++---------------- lua/obsidian/config.lua | 1 + 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua index b3cd1362..c5076676 100644 --- a/lua/obsidian/commands/tasks.lua +++ b/lua/obsidian/commands/tasks.lua @@ -1,36 +1,36 @@ --- TODO: Use config settings for icons -local task_configs = { - -- in progress - ["/"] = { char = " ", order = 0, name = "in progress", hl_group = "ObsidianRightArrow" }, - -- todo - [" "] = { char = "󰄱 ", order = 1, name = "todo", hl_group = "ObsidianTodo" }, - -- done - ["x"] = { char = " ", order = 2, name = "done", hl_group = "ObsidianDone" }, - -- cancelled - ["-"] = { char = " ", order = 3, name = "cancelled", hl_group = "ObsidianTilde" }, -} +---@class CheckboxConfig +---@field name string +---@field order integer --- Map states by name for quick lookup -local states = {} -for _, config in pairs(task_configs or {}) do - states[config.name] = config -end +-- Build the list of task status names sorted by order +---@param checkbox_config table +local function get_task_status_names(checkbox_config) + -- index by status name + ---@type table + local task_by_status_name = {} + local status_names = {} + for _, c in pairs(checkbox_config) do + task_by_status_name[c.name] = c + status_names[#status_names + 1] = c.name + end ---- Fetch the next task state in a cycle ----@param current_state string? ----@return string? next_state -local function get_next_state(current_state) - local state_names = vim.tbl_keys(states) - -- sort state names by order - table.sort(state_names, function(a, b) - return (states[a].order or 0) < (states[b].order or 0) + -- sort list of status names + table.sort(status_names, function(a, b) + return (task_by_status_name[a].order or 0) < (task_by_status_name[b].order or 0) end) - for i, name in ipairs(state_names) do - if name == current_state then - return state_names[i % #state_names + 1] + + return status_names +end + +---@param current string|nil +---@param status_names table +local function get_next_status(current, status_names) + for i, v in ipairs(status_names) do + if v == current then + return status_names[i + 1] end end - return state_names[1] -- Default to first state if none found + return status_names[1] end --- Show tasks with optional filtering @@ -42,12 +42,15 @@ local function showTasks(client, data) local filter = data.fargs[1] local picker = assert(client:picker(), "No picker configured") + local checkboxes = client.opts.ui.checkboxes + local status_names = get_task_status_names(checkboxes) + local tasks = client:find_tasks() local toShow = {} -- TODO: Hide filename, show only the task for _, task in ipairs(tasks) do - local tStatus = task_configs[task.status] + local tStatus = checkboxes[task.status] if tStatus and (not filter or tStatus.name == filter) then table.insert(toShow, { display = string.format(" %s", task.description), @@ -64,7 +67,7 @@ local function showTasks(client, data) [""] = { desc = "Toggle task status", callback = function() - local next_state_name = get_next_state(filter) + local next_state_name = get_next_status(filter, status_names) showTasks(client, { fargs = { next_state_name } }) end, }, diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 3a78caad..d1917dd3 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -431,6 +431,7 @@ config.UIOpts = {} ---@field char string ---@field hl_group string ---@field order integer +---@field name string ---@class obsidian.config.UIStyleSpec --- From 6cf1631334cd81ee7da6df7d1973add0f6e66914 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Fri, 21 Mar 2025 06:59:32 +0000 Subject: [PATCH 05/19] allow argument to tasks command --- lua/obsidian/commands/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 16438d7d..257aeaab 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -192,6 +192,6 @@ M.register("ObsidianDebug", { opts = { nargs = 0, desc = "Log some information f M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of contents into a picker" } }) -- register the tasks command, the task should accept one argument that can be done or todo -M.register("ObsidianTasks", { opts = { nargs = 0, desc = "List all tasks" } }) +M.register("ObsidianTasks", { opts = { nargs = "?", desc = "List all tasks" } }) return M From 6873cced82840a2a1b752a67e60ee4c1b82e6b55 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Fri, 21 Mar 2025 07:28:44 +0000 Subject: [PATCH 06/19] remove todos --- lua/obsidian/client.lua | 3 --- lua/obsidian/commands/init.lua | 2 +- lua/obsidian/commands/tasks.lua | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 0073e3f8..8c12c542 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2083,9 +2083,6 @@ end ---@field description string The text of the line where the task was found. ---@field status string The status of the task. ---todo: support for task status ---todo: make it async ---todo: can we reuse the search function? move the result parsing to the command ---@return obsidian.Task[] Client.find_tasks = function(self) local openTasks = search.search(self.dir, "- \\[(.)\\]", search.SearchOpts.default()) diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 257aeaab..2e127e6c 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -191,7 +191,7 @@ M.register( M.register("ObsidianDebug", { opts = { nargs = 0, desc = "Log some information for debugging" } }) M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of contents into a picker" } }) --- register the tasks command, the task should accept one argument that can be done or todo + M.register("ObsidianTasks", { opts = { nargs = "?", desc = "List all tasks" } }) return M diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua index c5076676..8be09958 100644 --- a/lua/obsidian/commands/tasks.lua +++ b/lua/obsidian/commands/tasks.lua @@ -48,7 +48,6 @@ local function showTasks(client, data) local tasks = client:find_tasks() local toShow = {} - -- TODO: Hide filename, show only the task for _, task in ipairs(tasks) do local tStatus = checkboxes[task.status] if tStatus and (not filter or tStatus.name == filter) then From 1a6fbffd486a58dd7062cdc2da73f51c3122cdcb Mon Sep 17 00:00:00 2001 From: andrewinci Date: Fri, 21 Mar 2025 07:33:45 +0000 Subject: [PATCH 07/19] update default config --- lua/obsidian/config.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index d1917dd3..a9d6eb73 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -444,11 +444,11 @@ config.UIOpts.default = function() update_debounce = 200, max_file_length = 5000, checkboxes = { - [" "] = { order = 1, char = "󰄱", hl_group = "ObsidianTodo" }, - ["~"] = { order = 2, char = "󰰱", hl_group = "ObsidianTilde" }, - ["!"] = { order = 3, char = "", hl_group = "ObsidianImportant" }, - [">"] = { order = 4, char = "", hl_group = "ObsidianRightArrow" }, - ["x"] = { order = 5, char = "", hl_group = "ObsidianDone" }, + [" "] = { order = 1, name = "todo", char = "󰄱", hl_group = "ObsidianTodo" }, + ["~"] = { order = 2, name = "doing", char = "󰰱", hl_group = "ObsidianTilde" }, + ["!"] = { order = 3, name = "important", char = "", hl_group = "ObsidianImportant" }, + [">"] = { order = 4, name = "next", char = "", hl_group = "ObsidianRightArrow" }, + ["x"] = { order = 5, name = "done", char = "", hl_group = "ObsidianDone" }, }, bullets = { char = "•", hl_group = "ObsidianBullet" }, external_link_icon = { char = "", hl_group = "ObsidianExtLinkIcon" }, From 052f285f825cbba1cbc7b24933cb28c797124749 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sat, 26 Apr 2025 19:26:37 +0100 Subject: [PATCH 08/19] support different list styles --- lua/obsidian/client.lua | 4 ++-- test/obsidian/client_spec.lua | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index c2761645..356bca01 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2084,7 +2084,7 @@ end ---@return obsidian.Task[] Client.find_tasks = function(self) - local openTasks = search.search(self.dir, "- \\[(.)\\]", search.SearchOpts.default()) + local openTasks = search.search(self.dir, "^\\s*([-+*]|\\d+\\.) \\[.\\]", search.SearchOpts.default()) --- @type obsidian.Task[] local result = {} @@ -2092,7 +2092,7 @@ Client.find_tasks = function(self) local taskMatch = openTasks() while taskMatch do -- matching in lua since ripgrep doesn't support capturing groups in the --json output - local status, description = string.match(taskMatch.lines.text, "- %[(.)%] (.*)") + local status, description = string.match(taskMatch.lines.text, "%[(.)%] (.*)") result[#result + 1] = { status = status, description = description, diff --git a/test/obsidian/client_spec.lua b/test/obsidian/client_spec.lua index a2bc1d1f..9c2c666e 100644 --- a/test/obsidian/client_spec.lua +++ b/test/obsidian/client_spec.lua @@ -24,7 +24,7 @@ local with_tmp_client = function(run) local ok, err = pcall(run, client) - dir:rmtree() + -- dir:rmtree() if not ok then error(err) @@ -227,3 +227,36 @@ describe("Client:daily_note_path()", function() end) end) end) + +describe("Client:find_tasks()", function() + it("should match any task list", function() + with_tmp_client(function(client) + -- create a test file with a task list + local test_file_name = tostring(client.dir) .. "/list.md" + local file = io.open(test_file_name, "a+") + if file == nil then + error("Failed to open file: " .. test_file_name) + end + file:write( + "- [ ] first\n" + .. " - [ ] second\n" + .. "+ [ ] plus\n" + .. "* [ ] star\n" + .. "- [x] normal x-ed\n" + .. "- [!] normal !-ed\n" + .. "- [~] normal ~-ed (render-markdown)\n" + .. "1. [ ] ordered\n" + .. " 22. [x] ordered x-ed\n" + ) + file:close() + + -- search for tasks + local tasks = client:find_tasks() + + -- len of tasks should be 1 + assert(#tasks == 9) + assert(tasks[1].description == "first\n") + assert(tasks[#tasks].description == "ordered x-ed\n") + end) + end) +end) From 3846ebb1ba4be5135d11f5d175eabc5f4a9c678b Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 14:43:50 +0100 Subject: [PATCH 09/19] suport 1) list style --- lua/obsidian/client.lua | 2 +- test/obsidian/client_spec.lua | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 356bca01..3340dc3b 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2084,7 +2084,7 @@ end ---@return obsidian.Task[] Client.find_tasks = function(self) - local openTasks = search.search(self.dir, "^\\s*([-+*]|\\d+\\.) \\[.\\]", search.SearchOpts.default()) + local openTasks = search.search(self.dir, "^\\s*([-+*]|\\d+[\\.)]) \\[.\\]", search.SearchOpts.default()) --- @type obsidian.Task[] local result = {} diff --git a/test/obsidian/client_spec.lua b/test/obsidian/client_spec.lua index 9c2c666e..0e96ccdb 100644 --- a/test/obsidian/client_spec.lua +++ b/test/obsidian/client_spec.lua @@ -24,7 +24,7 @@ local with_tmp_client = function(run) local ok, err = pcall(run, client) - -- dir:rmtree() + dir:rmtree() if not ok then error(err) @@ -246,6 +246,8 @@ describe("Client:find_tasks()", function() .. "- [!] normal !-ed\n" .. "- [~] normal ~-ed (render-markdown)\n" .. "1. [ ] ordered\n" + .. "1) [ ] ordered\n" + .. "12) [ ] ordered\n" .. " 22. [x] ordered x-ed\n" ) file:close() @@ -253,8 +255,8 @@ describe("Client:find_tasks()", function() -- search for tasks local tasks = client:find_tasks() - -- len of tasks should be 1 - assert(#tasks == 9) + -- len of tasks should be 11 + assert(#tasks == 11, "Wrong number of taks found:" .. #tasks) assert(tasks[1].description == "first\n") assert(tasks[#tasks].description == "ordered x-ed\n") end) From 64402ee06edccfd324f07e19eaac6010fef01b80 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 14:57:45 +0100 Subject: [PATCH 10/19] fix issue in telescope when the description includes a new line --- lua/obsidian/client.lua | 2 +- test/obsidian/client_spec.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 3340dc3b..d4954322 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2095,7 +2095,7 @@ Client.find_tasks = function(self) local status, description = string.match(taskMatch.lines.text, "%[(.)%] (.*)") result[#result + 1] = { status = status, - description = description, + description = string.gsub(description, "\n", ""), line = taskMatch.line_number, path = taskMatch.path.text, } diff --git a/test/obsidian/client_spec.lua b/test/obsidian/client_spec.lua index 0e96ccdb..0e3277c5 100644 --- a/test/obsidian/client_spec.lua +++ b/test/obsidian/client_spec.lua @@ -257,8 +257,8 @@ describe("Client:find_tasks()", function() -- len of tasks should be 11 assert(#tasks == 11, "Wrong number of taks found:" .. #tasks) - assert(tasks[1].description == "first\n") - assert(tasks[#tasks].description == "ordered x-ed\n") + assert(tasks[1].description == "first") + assert(tasks[#tasks].description == "ordered x-ed") end) end) end) From ed245bbc4d5b195038a58131705bb60592bd84c9 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 18:44:03 +0100 Subject: [PATCH 11/19] autocomplete ObsidianTasks with available options --- lua/obsidian/client.lua | 19 +++++++++++++++++++ lua/obsidian/commands/init-legacy.lua | 10 +++++++++- lua/obsidian/commands/tasks.lua | 26 +------------------------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index d4954322..0a7b55b6 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2104,4 +2104,23 @@ Client.find_tasks = function(self) return result end +-- Build the list of task status names sorted by order +Client.get_task_status_names = function(self) + local checkboxes = self.opts.ui.checkboxes + -- index by status name + local task_by_status_name = {} + local status_names = {} + for _, c in pairs(checkboxes) do + task_by_status_name[c.name] = c + status_names[#status_names + 1] = c.name + end + + -- sort list of status names + table.sort(status_names, function(a, b) + return (task_by_status_name[a].order or 0) < (task_by_status_name[b].order or 0) + end) + + return status_names +end + return Client diff --git a/lua/obsidian/commands/init-legacy.lua b/lua/obsidian/commands/init-legacy.lua index 4197ad57..f6ea05bb 100644 --- a/lua/obsidian/commands/init-legacy.lua +++ b/lua/obsidian/commands/init-legacy.lua @@ -129,6 +129,11 @@ M.complete_args_search = function(client, _, cmd_line, _) return completions end +M.complete_task_name = function(client, _, _, _) + local status_names = client:get_task_status_names() + return status_names +end + M.register("ObsidianCheck", { opts = { nargs = 0, desc = "Check for issues in your vault" } }) M.register("ObsidianToday", { opts = { nargs = "?", desc = "Open today's daily note" } }) @@ -192,6 +197,9 @@ M.register("ObsidianDebug", { opts = { nargs = 0, desc = "Log some information f M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of contents into a picker" } }) -M.register("ObsidianTasks", { opts = { nargs = "?", desc = "List all tasks" } }) +M.register("ObsidianTasks", { + opts = { nargs = "?", desc = "List all tasks" }, + complete = M.complete_task_name, +}) return M diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua index 8be09958..364d5913 100644 --- a/lua/obsidian/commands/tasks.lua +++ b/lua/obsidian/commands/tasks.lua @@ -1,27 +1,3 @@ ----@class CheckboxConfig ----@field name string ----@field order integer - --- Build the list of task status names sorted by order ----@param checkbox_config table -local function get_task_status_names(checkbox_config) - -- index by status name - ---@type table - local task_by_status_name = {} - local status_names = {} - for _, c in pairs(checkbox_config) do - task_by_status_name[c.name] = c - status_names[#status_names + 1] = c.name - end - - -- sort list of status names - table.sort(status_names, function(a, b) - return (task_by_status_name[a].order or 0) < (task_by_status_name[b].order or 0) - end) - - return status_names -end - ---@param current string|nil ---@param status_names table local function get_next_status(current, status_names) @@ -43,7 +19,7 @@ local function showTasks(client, data) local picker = assert(client:picker(), "No picker configured") local checkboxes = client.opts.ui.checkboxes - local status_names = get_task_status_names(checkboxes) + local status_names = client:get_task_status_names() local tasks = client:find_tasks() local toShow = {} From 6f470836ada01baf0fc4e8312b701f6092921ba2 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 18:48:26 +0100 Subject: [PATCH 12/19] change description for c-n --- lua/obsidian/commands/tasks.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/obsidian/commands/tasks.lua b/lua/obsidian/commands/tasks.lua index 364d5913..97fdf8c5 100644 --- a/lua/obsidian/commands/tasks.lua +++ b/lua/obsidian/commands/tasks.lua @@ -40,7 +40,7 @@ local function showTasks(client, data) prompt_title = filter and (filter .. " tasks") or "tasks", query_mappings = { [""] = { - desc = "Toggle task status", + desc = "Toggle task filter", callback = function() local next_state_name = get_next_status(filter, status_names) showTasks(client, { fargs = { next_state_name } }) From 5dfbd0ba810027b467f6f15b6cca3099432bd4b8 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 18:57:33 +0100 Subject: [PATCH 13/19] update init and init-legacy --- lua/obsidian/client.lua | 3 ++- lua/obsidian/commands/init-legacy.lua | 9 +++++---- lua/obsidian/commands/init.lua | 8 +++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 0a7b55b6..15b72cb5 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2104,7 +2104,8 @@ Client.find_tasks = function(self) return result end --- Build the list of task status names sorted by order +--- Build the list of task status names sorted by order +---@return string[] Client.get_task_status_names = function(self) local checkboxes = self.opts.ui.checkboxes -- index by status name diff --git a/lua/obsidian/commands/init-legacy.lua b/lua/obsidian/commands/init-legacy.lua index f6ea05bb..92dba1b6 100644 --- a/lua/obsidian/commands/init-legacy.lua +++ b/lua/obsidian/commands/init-legacy.lua @@ -129,9 +129,10 @@ M.complete_args_search = function(client, _, cmd_line, _) return completions end -M.complete_task_name = function(client, _, _, _) - local status_names = client:get_task_status_names() - return status_names +---@param client obsidian.Client +---@return string[] +M.task_status_name_complete = function(client, _, _, _) + return client:get_task_status_names() end M.register("ObsidianCheck", { opts = { nargs = 0, desc = "Check for issues in your vault" } }) @@ -199,7 +200,7 @@ M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of conten M.register("ObsidianTasks", { opts = { nargs = "?", desc = "List all tasks" }, - complete = M.complete_task_name, + complete = M.task_status_name_complete, }) return M diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index 16e159db..a40cce06 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -200,6 +200,12 @@ M.note_complete = function(client, cmd_arg) return completions end +---@param client obsidian.Client +---@return string[] +M.task_status_name_complete = function(client, _, _, _) + return client:get_task_status_names() +end + M.register("check", { nargs = 0 }) M.register("today", { nargs = "?" }) @@ -248,6 +254,6 @@ M.register("debug", { nargs = 0 }) M.register("toc", { nargs = 0 }) -M.register("tasks", { nargs = "?", desc = "List all tasks" }) +M.register("tasks", { nargs = "?", desc = "List all tasks", complete = M.task_status_name_complete }) return M From f698ae7d0cbd5c08f04fcf9b1eb0ed27c7869456 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 19:07:04 +0100 Subject: [PATCH 14/19] fix style --- mise.toml | 2 ++ test/obsidian/client_spec.lua | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 00000000..f2221b94 --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +stylua = "latest" diff --git a/test/obsidian/client_spec.lua b/test/obsidian/client_spec.lua index 0e3277c5..56d88e7f 100644 --- a/test/obsidian/client_spec.lua +++ b/test/obsidian/client_spec.lua @@ -160,7 +160,7 @@ describe("Client:parse_title_id_path()", function() with_tmp_client(function(client) client.opts.note_path_func = function(_) return "foo-bar-123.md" - end; + end (client.dir / "notes"):mkdir { exist_ok = true } From d7e0c7a4df3d19faa2bc99a0baea3cdbe6638ab9 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 19:13:45 +0100 Subject: [PATCH 15/19] update documentation --- README.md | 14 ++++++++------ lua/obsidian/config.lua | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5b0fb02e..776a6245 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ The fork aims to stay close to the original, but fix bugs, include and merge use - `:Obsidian toc` to load the table of contents of the current note into a picker list. +- `:Obsidian tasks [STATUS]` to load the list of tasks of the current vault into a picker list. + ### Demo [![2024-01-31 14 22 52](https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e)](https://github.com/epwalsh/obsidian.nvim/assets/8812459/2986e1d2-13e8-40e2-9c9e-75691a3b662e) @@ -520,12 +522,12 @@ This is a complete list of all of the options that can be passed to `require("ob max_file_length = 5000, -- disable UI features for files with more than this many lines -- Define how various check-boxes are displayed checkboxes = { - -- NOTE: the 'char' value has to be a single character, and the highlight groups are defined below. - [" "] = { char = "󰄱", hl_group = "ObsidianTodo" }, - ["x"] = { char = "", hl_group = "ObsidianDone" }, - [">"] = { char = "", hl_group = "ObsidianRightArrow" }, - ["~"] = { char = "󰰱", hl_group = "ObsidianTilde" }, - ["!"] = { char = "", hl_group = "ObsidianImportant" }, + -- NOTE: the 'char' value and the status has to be a single character, and the highlight groups are defined below. + [" "] = { char = "󰄱", order = 1, name = "todo", hl_group = "ObsidianTodo" }, + ["x"] = { char = "", order = 2, name = "done", hl_group = "ObsidianDone" }, + [">"] = { char = "", order = 3, name = "doing", hl_group = "ObsidianRightArrow" }, + ["~"] = { char = "󰰱", order = 4, name = "cancelled", hl_group = "ObsidianTilde" }, + ["!"] = { char = "", order = 5, name = "important", hl_group = "ObsidianImportant" }, -- Replace the above with this if you don't have a patched font: -- [" "] = { char = "☐", hl_group = "ObsidianTodo" }, -- ["x"] = { char = "✔", hl_group = "ObsidianDone" }, diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 5af9c69b..6e61d813 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -451,11 +451,11 @@ config.UIOpts.default = function() update_debounce = 200, max_file_length = 5000, checkboxes = { - [" "] = { order = 1, name = "todo", char = "󰄱", hl_group = "ObsidianTodo" }, - ["~"] = { order = 2, name = "doing", char = "󰰱", hl_group = "ObsidianTilde" }, - ["!"] = { order = 3, name = "important", char = "", hl_group = "ObsidianImportant" }, - [">"] = { order = 4, name = "next", char = "", hl_group = "ObsidianRightArrow" }, - ["x"] = { order = 5, name = "done", char = "", hl_group = "ObsidianDone" }, + [" "] = { char = "󰄱", order = 1, name = "todo", hl_group = "ObsidianTodo" }, + ["x"] = { char = "", order = 2, name = "done", hl_group = "ObsidianDone" }, + [">"] = { char = "", order = 3, name = "doing", hl_group = "ObsidianRightArrow" }, + ["~"] = { char = "󰰱", order = 4, name = "cancelled", hl_group = "ObsidianTilde" }, + ["!"] = { char = "", order = 5, name = "important", hl_group = "ObsidianImportant" }, }, bullets = { char = "•", hl_group = "ObsidianBullet" }, external_link_icon = { char = "", hl_group = "ObsidianExtLinkIcon" }, From b3b9cca7ba914c5134291285ca4ae33781e115cf Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 20:12:37 +0100 Subject: [PATCH 16/19] fix task list when the checkbox name is not specified --- lua/obsidian/client.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/client.lua b/lua/obsidian/client.lua index 15b72cb5..e1179f8e 100644 --- a/lua/obsidian/client.lua +++ b/lua/obsidian/client.lua @@ -2112,8 +2112,8 @@ Client.get_task_status_names = function(self) local task_by_status_name = {} local status_names = {} for _, c in pairs(checkboxes) do - task_by_status_name[c.name] = c - status_names[#status_names + 1] = c.name + task_by_status_name[c.name or c.char] = c + status_names[#status_names + 1] = c.name or c.char end -- sort list of status names From 72742d9b56d326c633c861a8592133e0b921a096 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 20:13:55 +0100 Subject: [PATCH 17/19] remove mise config file --- mise.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 mise.toml diff --git a/mise.toml b/mise.toml deleted file mode 100644 index f2221b94..00000000 --- a/mise.toml +++ /dev/null @@ -1,2 +0,0 @@ -[tools] -stylua = "latest" From 9a0051dac5282b4e378006974a3fa382a2a16c32 Mon Sep 17 00:00:00 2001 From: andrewinci Date: Sun, 27 Apr 2025 20:20:33 +0100 Subject: [PATCH 18/19] revert chars --- lua/obsidian/config.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 6e61d813..b6b344c5 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -451,11 +451,11 @@ config.UIOpts.default = function() update_debounce = 200, max_file_length = 5000, checkboxes = { - [" "] = { char = "󰄱", order = 1, name = "todo", hl_group = "ObsidianTodo" }, - ["x"] = { char = "", order = 2, name = "done", hl_group = "ObsidianDone" }, - [">"] = { char = "", order = 3, name = "doing", hl_group = "ObsidianRightArrow" }, - ["~"] = { char = "󰰱", order = 4, name = "cancelled", hl_group = "ObsidianTilde" }, - ["!"] = { char = "", order = 5, name = "important", hl_group = "ObsidianImportant" }, + [" "] = { order = 1, char = "󰄱", hl_group = "ObsidianTodo", name = "todo" }, + ["!"] = { order = 2, char = "", hl_group = "ObsidianImportant", name = "important" }, + [">"] = { order = 3, char = "", hl_group = "ObsidianRightArrow", name = "doing" }, + ["~"] = { order = 4, char = "󰰱", hl_group = "ObsidianTilde", name = "cancelled" }, + ["x"] = { order = 5, char = "", hl_group = "ObsidianDone", name = "done" }, }, bullets = { char = "•", hl_group = "ObsidianBullet" }, external_link_icon = { char = "", hl_group = "ObsidianExtLinkIcon" }, From d260a6b475a49cd05bab905bdfa202630ac91ee3 Mon Sep 17 00:00:00 2001 From: Andrea Vinci Date: Mon, 28 Apr 2025 18:17:51 +0100 Subject: [PATCH 19/19] generate docs --- doc/obsidian.txt | 14 ++++++++------ doc/obsidian_api.txt | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/doc/obsidian.txt b/doc/obsidian.txt index c5cba6fd..1a92b23f 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -135,6 +135,8 @@ COMMANDS *obsidian-commands* command has one optional argument: the title of the new note. - `:Obsidian toc` to load the table of contents of the current note into a picker list. +- `:Obsidian tasks [STATUS]` to load the list of tasks of the current vault into + a picker list. DEMO *obsidian-demo* @@ -580,12 +582,12 @@ carefully and customize it to your needs: max_file_length = 5000, -- disable UI features for files with more than this many lines -- Define how various check-boxes are displayed checkboxes = { - -- NOTE: the 'char' value has to be a single character, and the highlight groups are defined below. - [" "] = { char = "󰄱", hl_group = "ObsidianTodo" }, - ["x"] = { char = "", hl_group = "ObsidianDone" }, - [">"] = { char = "", hl_group = "ObsidianRightArrow" }, - ["~"] = { char = "󰰱", hl_group = "ObsidianTilde" }, - ["!"] = { char = "", hl_group = "ObsidianImportant" }, + -- NOTE: the 'char' value and the status has to be a single character, and the highlight groups are defined below. + [" "] = { char = "󰄱", order = 1, name = "todo", hl_group = "ObsidianTodo" }, + ["x"] = { char = "", order = 2, name = "done", hl_group = "ObsidianDone" }, + [">"] = { char = "", order = 3, name = "doing", hl_group = "ObsidianRightArrow" }, + ["~"] = { char = "󰰱", order = 4, name = "cancelled", hl_group = "ObsidianTilde" }, + ["!"] = { char = "", order = 5, name = "important", hl_group = "ObsidianImportant" }, -- Replace the above with this if you don't have a patched font: -- [" "] = { char = "☐", hl_group = "ObsidianTodo" }, -- ["x"] = { char = "✔", hl_group = "ObsidianDone" }, diff --git a/doc/obsidian_api.txt b/doc/obsidian_api.txt index 00b72f1c..308bc8b9 100644 --- a/doc/obsidian_api.txt +++ b/doc/obsidian_api.txt @@ -665,6 +665,29 @@ Parameters ~ Return ~ `(obsidian.Picker| `(optional))`` +------------------------------------------------------------------------------ +Class ~ +{obsidian.Task} + +Fields ~ +{path} `(string)` The path to the note. +{line} `(integer)` The line number (1-indexed) where the task was found. +{description} `(string)` The text of the line where the task was found. +{status} `(string)` The status of the task. + +------------------------------------------------------------------------------ + *obsidian.Client.find_tasks()* + `Client.find_tasks`({self}) +Return ~ +`(obsidian.Task[])` + +------------------------------------------------------------------------------ + *obsidian.Client.get_task_status_names()* + `Client.get_task_status_names`({self}) +Build the list of task status names sorted by order +Return ~ +`(string[])` + ------------------------------------------------------------------------------ Class ~ {obsidian.note.HeaderAnchor}