Skip to content

feat: add tasks command #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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" },
Expand Down
14 changes: 8 additions & 6 deletions doc/obsidian.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand Down Expand Up @@ -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" },
Expand Down
23 changes: 23 additions & 0 deletions doc/obsidian_api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
49 changes: 49 additions & 0 deletions lua/obsidian/client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2075,4 +2075,53 @@ 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.

---@return obsidian.Task[]
Client.find_tasks = function(self)
local openTasks = search.search(self.dir, "^\\s*([-+*]|\\d+[\\.)]) \\[.\\]", 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 = string.gsub(description, "\n", ""),
line = taskMatch.line_number,
path = taskMatch.path.text,
}
taskMatch = openTasks()
end
return result
end

--- 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
local task_by_status_name = {}
local status_names = {}
for _, c in pairs(checkboxes) do
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
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
12 changes: 12 additions & 0 deletions lua/obsidian/commands/init-legacy.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -128,6 +129,12 @@ M.complete_args_search = function(client, _, cmd_line, _)
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("ObsidianCheck", { opts = { nargs = 0, desc = "Check for issues in your vault" } })

M.register("ObsidianToday", { opts = { nargs = "?", desc = "Open today's daily note" } })
Expand Down Expand Up @@ -191,4 +198,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" },
complete = M.task_status_name_complete,
})

return M
9 changes: 9 additions & 0 deletions lua/obsidian/commands/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ local cmds = {
"tomorrow",
"workspace",
"yesterday",
"tasks",
}

-- TODO: this will be context-sensitive in the future
Expand Down Expand Up @@ -199,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 = "?" })
Expand Down Expand Up @@ -247,4 +254,6 @@ M.register("debug", { nargs = 0 })

M.register("toc", { nargs = 0 })

M.register("tasks", { nargs = "?", desc = "List all tasks", complete = M.task_status_name_complete })

return M
53 changes: 53 additions & 0 deletions lua/obsidian/commands/tasks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---@param current string|nil
---@param status_names table<integer, string>
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 status_names[1]
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 checkboxes = client.opts.ui.checkboxes
local status_names = client:get_task_status_names()

local tasks = client:find_tasks()
local toShow = {}

for _, task in ipairs(tasks) do
local tStatus = checkboxes[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 = {
["<C-n>"] = {
desc = "Toggle task filter",
callback = function()
local next_state_name = get_next_status(filter, status_names)
showTasks(client, { fargs = { next_state_name } })
end,
},
},
})
end

return showTasks
11 changes: 6 additions & 5 deletions lua/obsidian/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ config.UIOpts = {}
---@field char string
---@field hl_group string
---@field order integer
---@field name string

---@class obsidian.config.UIStyleSpec
---
Expand All @@ -450,11 +451,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, 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" },
Expand Down
37 changes: 36 additions & 1 deletion test/obsidian/client_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down Expand Up @@ -227,3 +227,38 @@ 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"
.. "1) [ ] ordered\n"
.. "12) [ ] ordered\n"
.. " 22. [x] ordered x-ed\n"
)
file:close()

-- search for tasks
local tasks = client:find_tasks()

-- len of tasks should be 11
assert(#tasks == 11, "Wrong number of taks found:" .. #tasks)
assert(tasks[1].description == "first")
assert(tasks[#tasks].description == "ordered x-ed")
end)
end)
end)