Skip to content

Commit

Permalink
move each command to its own module (#271)
Browse files Browse the repository at this point in the history
  • Loading branch information
epwalsh authored Dec 19, 2023
1 parent 319f060 commit 5e505eb
Show file tree
Hide file tree
Showing 19 changed files with 1,374 additions and 1,311 deletions.
1,309 changes: 0 additions & 1,309 deletions lua/obsidian/command.lua

This file was deleted.

43 changes: 43 additions & 0 deletions lua/obsidian/commands/backlinks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
local util = require "obsidian.util"
local log = require "obsidian.log"
local RefTypes = require("obsidian.search").RefTypes

---@param client obsidian.Client
return function(client, _)
---@type obsidian.Note|?
local note
local cursor_link, _, ref_type = util.cursor_link()
if cursor_link ~= nil and ref_type ~= RefTypes.NakedUrl then
note = client:resolve_note(cursor_link)
if note == nil then
log.err "Could not resolve link under cursor to a note ID, path, or alias"
return
end
end

local ok, backlinks = pcall(function()
return require("obsidian.backlinks").new(client, nil, nil, note)
end)

if ok then
backlinks:view(function(matches)
if not vim.tbl_isempty(matches) then
log.info(
"Showing backlinks to '%s'.\n\n"
.. "TIPS:\n\n"
.. "- Hit ENTER on a match to follow the backlink\n"
.. "- Hit ENTER on a group header to toggle the fold, or use normal fold mappings",
backlinks.note.id
)
else
if note ~= nil then
log.warn("No backlinks to '%s'", note.id)
else
log.warn "No backlinks to current note"
end
end
end)
else
log.err "Backlinks command can only be used from a valid note"
end
end
71 changes: 71 additions & 0 deletions lua/obsidian/commands/check.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
local Path = require "plenary.path"
local Note = require "obsidian.note"
local log = require "obsidian.log"
local iter = require("obsidian.itertools").iter
local AsyncExecutor = require("obsidian.async").AsyncExecutor
local scan = require "plenary.scandir"

---@param client obsidian.Client
return function(client, _)
local skip_dirs = {}
if client.opts.templates ~= nil and client.opts.templates.subdir ~= nil then
skip_dirs[#skip_dirs + 1] = Path:new(client.opts.templates.subdir)
end

local executor = AsyncExecutor.new()
local count = 0
local errors = {}
local warnings = {}

---@param path Path
local function check_note(path, relative_path)
local ok, res = pcall(Note.from_file_async, path, client.dir)
if not ok then
errors[#errors + 1] = "Failed to parse note '" .. relative_path .. "': " .. tostring(res)
elseif res.has_frontmatter == false then
warnings[#warnings + 1] = "'" .. relative_path .. "' missing frontmatter"
end
count = count + 1
end

---@diagnostic disable-next-line: undefined-field
local start_time = vim.loop.hrtime()

scan.scan_dir(vim.fs.normalize(tostring(client:vault_root())), {
hidden = false,
add_dirs = false,
respect_gitignore = true,
search_pattern = ".*%.md",
on_insert = function(entry)
local relative_path = assert(client:vault_relative_path(entry))
for skip_dir in iter(skip_dirs) do
if vim.startswith(relative_path, tostring(skip_dir) .. skip_dir._sep) then
return
end
end
executor:submit(check_note, nil, entry, relative_path)
end,
})

executor:join_and_then(5000, function()
---@diagnostic disable-next-line: undefined-field
local runtime = math.floor((vim.loop.hrtime() - start_time) / 1000000)
local messages = { "Checked " .. tostring(count) .. " notes in " .. runtime .. "ms" }
local log_level = vim.log.levels.INFO
if #warnings > 0 then
messages[#messages + 1] = "\nThere were " .. tostring(#warnings) .. " warning(s):"
log_level = vim.log.levels.WARN
for warning in iter(warnings) do
messages[#messages + 1] = "" .. warning
end
end
if #errors > 0 then
messages[#messages + 1] = "\nThere were " .. tostring(#errors) .. " error(s):"
for err in iter(errors) do
messages[#messages + 1] = "" .. err
end
log_level = vim.log.levels.ERROR
end
log.log(table.concat(messages, "\n"), log_level)
end)
end
72 changes: 72 additions & 0 deletions lua/obsidian/commands/follow_link.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
local Path = require "plenary.path"
local RefTypes = require("obsidian.search").RefTypes
local util = require "obsidian.util"
local log = require "obsidian.log"
local iter = require("obsidian.itertools").iter

---@param client obsidian.Client
return function(client, _)
local location, name, link_type = util.cursor_link(nil, nil, true)
if location == nil then
return
end

-- Check if it's a URL.
if util.is_url(location) then
if client.opts.follow_url_func ~= nil then
client.opts.follow_url_func(location)
else
log.warn "This looks like a URL. You can customize the behavior of URLs with the 'follow_url_func' option."
end
return
end

-- Remove links from the end if there are any.
local header_link = location:match "#[%a%d%s-_^]+$"
if header_link ~= nil then
location = location:sub(1, -header_link:len() - 1)
end

local buf_cwd = vim.fs.basename(vim.api.nvim_buf_get_name(0))

-- Search for matching notes.
-- TODO: handle case where there are multiple matches by prompting user to choose.
client:resolve_note_async(location, function(note)
if note == nil and (link_type == RefTypes.Wiki or link_type == RefTypes.WikiWithAlias) then
vim.schedule(function()
local confirmation = string.lower(vim.fn.input {
prompt = "Create new note '" .. location .. "'? [Y/n] ",
})
if confirmation == "y" or confirmation == "yes" then
-- Create a new note.
local aliases = name == location and {} or { name }
note = client:new_note(location, nil, nil, aliases)
vim.api.nvim_command("e " .. tostring(note.path))
else
log.warn "Aborting"
end
end)
elseif note ~= nil then
-- Go to resolved note.
local path = note.path
assert(path)
vim.schedule(function()
vim.api.nvim_command("e " .. tostring(path))
end)
else
local paths_to_check = { client:vault_root() / location, Path:new(location) }
if buf_cwd ~= nil then
paths_to_check[#paths_to_check + 1] = Path:new(buf_cwd) / location
end

for path in iter(paths_to_check) do
if path:is_file() then
return vim.schedule(function()
vim.api.nvim_command("e " .. tostring(path))
end)
end
end
return log.err("Failed to resolve file '" .. location .. "'")
end
end)
end
187 changes: 187 additions & 0 deletions lua/obsidian/commands/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
local Note = require "obsidian.note"
local util = require "obsidian.util"
local iter = require("obsidian.itertools").iter

local command_lookups = {
ObsidianCheck = "obsidian.commands.check",
ObsidianToday = "obsidian.commands.today",
ObsidianYesterday = "obsidian.commands.yesterday",
ObsidianTomorrow = "obsidian.commands.tomorrow",
ObsidianNew = "obsidian.commands.new",
ObsidianOpen = "obsidian.commands.open",
ObsidianBacklinks = "obsidian.commands.backlinks",
ObsidianSearch = "obsidian.commands.search",
ObsidianTemplate = "obsidian.commands.template",
ObsidianQuickSwitch = "obsidian.commands.quick_switch",
ObsidianLinkNew = "obsidian.commands.link_new",
ObsidianLink = "obsidian.commands.link",
ObsidianFollowLink = "obsidian.commands.follow_link",
ObsidianWorkspace = "obsidian.commands.workspace",
ObsidianRename = "obsidian.commands.rename",
ObsidianPasteImg = "obsidian.commands.paste_img",
}

local M = setmetatable({
commands = {},
}, {
__index = function(t, k)
local require_path = command_lookups[k]
if not require_path then
return
end

local mod = require(require_path)
t[k] = mod

return mod
end,
})

---@class obsidian.CommandConfig
---@field opts table
---@field complete function|?
---@field func function|? (obsidian.Client, table) -> nil

---Register a new command.
---@param name string
---@param config obsidian.CommandConfig
M.register = function(name, config)
if not config.func then
config.func = function(client, data)
return M[name](client, data)
end
end
M.commands[name] = config
end

---Install all commands.
---
---@param client obsidian.Client
M.install = function(client)
for command_name, command_config in pairs(M.commands) do
local func = function(data)
command_config.func(client, data)
end

if command_config.complete ~= nil then
command_config.opts.complete = function(arg_lead, cmd_line, cursor_pos)
return command_config.complete(client, arg_lead, cmd_line, cursor_pos)
end
end

vim.api.nvim_create_user_command(command_name, func, command_config.opts)
end
end

---@param client obsidian.Client
---@return string[]
M.complete_args_search = function(client, _, cmd_line, _)
local query
local cmd_arg, _ = util.lstrip_whitespace(string.gsub(cmd_line, "^.*Obsidian[A-Za-z0-9]+", ""))
if string.len(cmd_arg) > 0 then
if string.find(cmd_arg, "|", 1, true) then
return {}
else
query = cmd_arg
end
else
local _, csrow, cscol, _ = unpack(vim.fn.getpos "'<")
local _, cerow, cecol, _ = unpack(vim.fn.getpos "'>")
local lines = vim.fn.getline(csrow, cerow)
assert(type(lines) == "table")

if #lines > 1 then
lines[1] = string.sub(lines[1], cscol)
lines[#lines] = string.sub(lines[#lines], 1, cecol)
elseif #lines == 1 then
lines[1] = string.sub(lines[1], cscol, cecol)
else
return {}
end

query = table.concat(lines, " ")
end

local completions = {}
local query_lower = string.lower(query)
for note in iter(client:find_notes(query, { sort = true })) do
local note_path = assert(client:vault_relative_path(note.path))
if string.find(string.lower(note:display_name()), query_lower, 1, true) then
table.insert(completions, note:display_name() .. "" .. note_path)
else
for _, alias in pairs(note.aliases) do
if string.find(string.lower(alias), query_lower, 1, true) then
table.insert(completions, alias .. "" .. note_path)
break
end
end
end
end

return completions
end

M.complete_args_id = function(_, _, cmd_line, _)
local cmd_arg, _ = util.lstrip_whitespace(string.gsub(cmd_line, "^.*Obsidian[A-Za-z0-9]+", ""))
if string.len(cmd_arg) > 0 then
return {}
else
local note_id = util.cursor_link()
if note_id == nil then
local bufpath = vim.api.nvim_buf_get_name(vim.fn.bufnr())
local note = Note.from_file(bufpath)
note_id = tostring(note.id)
end
return { note_id }
end
end

---Check the directory for notes with missing/invalid frontmatter.
M.register("ObsidianCheck", { opts = { nargs = 0 } })

---Create or open a new daily note.
M.register("ObsidianToday", { opts = { nargs = "?" } })

---Create (or open) the daily note from the last weekday.
M.register("ObsidianYesterday", { opts = { nargs = 0 } })

---Create (or open) the daily note for the next weekday.
M.register("ObsidianTomorrow", { opts = { nargs = 0 } })

---Create a new note.
M.register("ObsidianNew", { opts = { nargs = "?" } })

---Open a note in the Obsidian app.
M.register("ObsidianOpen", { opts = { nargs = "?" }, complete = M.complete_args_search })

---Get backlinks to a note.
M.register("ObsidianBacklinks", { opts = { nargs = 0 } })

---Search notes.
M.register("ObsidianSearch", { opts = { nargs = "?" } })

--- Insert a template
M.register("ObsidianTemplate", { opts = { nargs = "?" } })

---Quick switch to an obsidian note
M.register("ObsidianQuickSwitch", { opts = { nargs = 0 } })

---Create a new note and link to it.
M.register("ObsidianLinkNew", { opts = { nargs = "?", range = true } })

---Create a link to an existing note on the current visual selection.
M.register("ObsidianLink", { opts = { nargs = "?", range = true }, complete = M.complete_args_search })

---Follow link under cursor.
M.register("ObsidianFollowLink", { opts = { nargs = 0 } })

---Switch to a different workspace.
M.register("ObsidianWorkspace", { opts = { nargs = "?" } })

---Rename a note and update all backlinks.
M.register("ObsidianRename", { opts = { nargs = 1 }, complete = M.complete_args_id })

---Paste an image into a note.
M.register("ObsidianPasteImg", { opts = { nargs = "?", complete = "file" } })

return M
Loading

0 comments on commit 5e505eb

Please sign in to comment.