From a06c3d422b83a177e22a4f8842f2c9cc4c9c1263 Mon Sep 17 00:00:00 2001 From: jblsp <48526917+jblsp@users.noreply.github.com> Date: Sat, 15 Mar 2025 19:08:57 -0600 Subject: [PATCH] refactor(Commands): Merge all commands into one :Obsidian command --- CHANGELOG.md | 6 +- README.md | 66 ++++---- doc/obsidian.txt | 48 +++--- lua/obsidian/commands/dailies.lua | 2 +- lua/obsidian/commands/init-legacy.lua | 194 ++++++++++++++++++++++++ lua/obsidian/commands/init.lua | 208 ++++++++++++++++---------- lua/obsidian/commands/rename.lua | 2 +- lua/obsidian/config.lua | 7 + lua/obsidian/init.lua | 4 + 9 files changed, 400 insertions(+), 137 deletions(-) create mode 100644 lua/obsidian/commands/init-legacy.lua diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e740c11e..bfafb75c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `opts.follow_img_func` option for customizing how to handle image paths. - Added better handling for undefined template fields, which will now be prompted for. -- Added support for the [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) picker +- Added support for the [`snacks.picker`](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) picker. - Added support for the [`blink.cmp`](https://github.com/Saghen/blink.cmp) completion plugin. +- Added `opts.legacy_commands` option which enables the old commands. ### Changed - Renamed `opts.image_name_func` to `opts.attachments.img_name_func`. -- Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present +- Default to not activate ui render when `render-markdown.nvim` or `markview.nvim` is present. +- Moved all commands into one `:Obsidian` command. ### Fixed diff --git a/README.md b/README.md index 1e928092c..3b17ca16f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ If you're new to Obsidian we highly recommend watching [this excellent YouTube v _Keep in mind this plugin is not meant to replace Obsidian, but to complement it._ The Obsidian app is very powerful in its own way; it comes with a mobile app and has a lot of functionality that's not feasible to implement in Neovim, such as the graph explorer view. That said, this plugin stands on its own as well. You don't necessarily need to use it alongside the Obsidian app. ## About the fork + The original project has not been actively maintained for quite a while and with the ever-changing Neovim ecosystem, new widely used tools such as [blink.cmp](https://github.com/Saghen/blink.cmp) or [snacks.picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) were not supported. With bugs, issues and pull requests piling up, people from the community decided to fork and maintain the project. The fork aims to stay close to the original, but fix bugs, include and merge useful improvements, and ensure long term robustness. @@ -47,54 +48,54 @@ The fork aims to stay close to the original, but fix bugs, include and merge use ### Commands -- `:ObsidianOpen [QUERY]` to open a note in the Obsidian app. +- `:Obsidian open [QUERY]` to open a note in the Obsidian app. This command has one optional argument: a query used to resolve the note to open by ID, path, or alias. If not given, the note corresponding to the current buffer is opened. -- `:ObsidianNew [TITLE]` to create a new note. +- `:Obsidian new [TITLE]` to create a new note. This command has one optional argument: the title of the new note. -- `:ObsidianQuickSwitch` to quickly switch to (or open) another note in your vault, searching by its name using [ripgrep](https://github.com/BurntSushi/ripgrep) with your preferred picker (see [plugin dependencies](#plugin-dependencies) below). +- `:Obsidian quickswitch` to quickly switch to (or open) another note in your vault, searching by its name using [ripgrep](https://github.com/BurntSushi/ripgrep) with your preferred picker (see [plugin dependencies](#plugin-dependencies) below). -- `:ObsidianFollowLink [vsplit|hsplit]` to follow a note reference under the cursor, optionally opening it in a vertical or horizontal split. +- `:Obsidian followlink [vsplit|hsplit]` to follow a note reference under the cursor, optionally opening it in a vertical or horizontal split. -- `:ObsidianBacklinks` for getting a picker list of references to the current buffer. +- `:Obsidian backlinks` for getting a picker list of references to the current buffer. -- `:ObsidianTags [TAG ...]` for getting a picker list of all occurrences of the given tags. +- `:Obsidian tags [TAG ...]` for getting a picker list of all occurrences of the given tags. -- `:ObsidianToday [OFFSET]` to open/create a new daily note. This command also takes an optional offset in days, e.g. use `:ObsidianToday -1` to go to yesterday's note. Unlike `:ObsidianYesterday` and `:ObsidianTomorrow` this command does not differentiate between weekdays and weekends. +- `:Obsidian today [OFFSET]` to open/create a new daily note. This command also takes an optional offset in days, e.g. use `:Obsidian today -1` to go to yesterday's note. Unlike `:Obsidian yesterday` and `:Obsidian tomorrow` this command does not differentiate between weekdays and weekends. -- `:ObsidianYesterday` to open/create the daily note for the previous working day. +- `:Obsidian yesterday` to open/create the daily note for the previous working day. -- `:ObsidianTomorrow` to open/create the daily note for the next working day. +- `:Obsidian tomorrow` to open/create the daily note for the next working day. -- `:ObsidianDailies [OFFSET ...]` to open a picker list of daily notes. For example, `:ObsidianDailies -2 1` to list daily notes from 2 days ago until tomorrow. +- `:Obsidian dailies [OFFSET ...]` to open a picker list of daily notes. For example, `:Obsidian dailies -2 1` to list daily notes from 2 days ago until tomorrow. -- `:ObsidianTemplate [NAME]` to insert a template from the templates folder, selecting from a list using your preferred picker. See ["using templates"](#using-templates) for more information. +- `:Obsidian template [NAME]` to insert a template from the templates folder, selecting from a list using your preferred picker. See ["using templates"](#using-templates) for more information. -- `:ObsidianSearch [QUERY]` to search for (or create) notes in your vault using `ripgrep` with your preferred picker. +- `:Obsidian search [QUERY]` to search for (or create) notes in your vault using `ripgrep` with your preferred picker. -- `:ObsidianLink [QUERY]` to link an inline visual selection of text to a note. +- `:Obsidian link [QUERY]` to link an inline visual selection of text to a note. This command has one optional argument: a query that will be used to resolve the note by ID, path, or alias. If not given, the selected text will be used as the query. -- `:ObsidianLinkNew [TITLE]` to create a new note and link it to an inline visual selection of text. +- `:Obsidian linknew [TITLE]` to create a new note and link it to an inline visual selection of text. This command has one optional argument: the title of the new note. If not given, the selected text will be used as the title. -- `:ObsidianLinks` to collect all links within the current buffer into a picker window. +- `:Obsidian links` to collect all links within the current buffer into a picker window. -- `:ObsidianExtractNote [TITLE]` to extract the visually selected text into a new note and link to it. +- `:Obsidian extractnote [TITLE]` to extract the visually selected text into a new note and link to it. -- `:ObsidianWorkspace [NAME]` to switch to another workspace. +- `:Obsidian workspace [NAME]` to switch to another workspace. -- `:ObsidianPasteImg [IMGNAME]` to paste an image from the clipboard into the note at the cursor position by saving it to the vault and adding a markdown image link. You can configure the default folder to save images to with the `attachments.img_folder` option. +- `:Obsidian pasteimg [IMGNAME]` to paste an image from the clipboard into the note at the cursor position by saving it to the vault and adding a markdown image link. You can configure the default folder to save images to with the `attachments.img_folder` option. -- `:ObsidianRename [NEWNAME] [--dry-run]` to rename the note of the current buffer or reference under the cursor, updating all backlinks across the vault. Since this command is still relatively new and could potentially write a lot of changes to your vault, I highly recommend committing the current state of your vault (if you're using version control) before running it, or doing a dry-run first by appending "--dry-run" to the command, e.g. `:ObsidianRename new-id --dry-run`. +- `:Obsidian rename [NEWNAME] [--dry-run]` to rename the note of the current buffer or reference under the cursor, updating all backlinks across the vault. Since this command is still relatively new and could potentially write a lot of changes to your vault, I highly recommend committing the current state of your vault (if you're using version control) before running it, or doing a dry-run first by appending "--dry-run" to the command, e.g. `:Obsidian rename new-id --dry-run`. -- `:ObsidianToggleCheckbox` to cycle through checkbox options. +- `:Obsidian togglecheckbox` to cycle through checkbox options. -- `:ObsidianNewFromTemplate [TITLE]` to create a new note from a template in the templates folder. Selecting from a list using your preferred picker. +- `:Obsidian newfromtemplate [TITLE]` to create a new note from a template in the templates folder. Selecting from a list using your preferred picker. This command has one optional argument: the title of the new note. -- `:ObsidianTOC` to load the table of contents of the current note into a picker list. +- `:Obsidian toc` to load the table of contents of the current note into a picker list. ### Demo @@ -110,11 +111,11 @@ The fork aims to stay close to the original, but fix bugs, include and merge use Specific operating systems also require additional dependencies in order to use all of obsidian.nvim's functionality: -- **Windows WSL** users need [`wsl-open`](https://gitlab.com/4U6U57/wsl-open) for the `:ObsidianOpen` command. -- **MacOS** users need [`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`) for the `:ObsidianPasteImg` command. -- **Linux** users need xclip (X11) or wl-clipboard (Wayland) for the `:ObsidianPasteImg` command. +- **Windows WSL** users need [`wsl-open`](https://gitlab.com/4U6U57/wsl-open) for the `:Obsidian open` command. +- **MacOS** users need [`pngpaste`](https://github.com/jcsalterego/pngpaste) (`brew install pngpaste`) for the `:Obsidian pasteimg` command. +- **Linux** users need xclip (X11) or wl-clipboard (Wayland) for the `:Obsidian pasteimg` command. -Search functionality (e.g. via the `:ObsidianSearch` and `:ObsidianQuickSwitch` commands) also requires a picker such [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) (see [plugin dependencies](#plugin-dependencies) below). +Search functionality (e.g. via the `:Obsidian search` and `:Obsidian quickswitch` commands) also requires a picker such [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) (see [plugin dependencies](#plugin-dependencies) below). ### Install and configure @@ -419,7 +420,7 @@ This is a complete list of all of the options that can be passed to `require("ob substitutions = {}, }, - -- Optional, by default when you use `:ObsidianFollowLink` on a link to an external + -- Optional, by default when you use `:Obsidian followlink` on a link to an external -- URL it will be ignored but you can customize this behavior here. ---@param url string follow_url_func = function(url) @@ -430,7 +431,7 @@ This is a complete list of all of the options that can be passed to `require("ob -- vim.ui.open(url) -- need Neovim 0.10.0+ end, - -- Optional, by default when you use `:ObsidianFollowLink` on a link to an image + -- Optional, by default when you use `:Obsidian followlink` on a link to an image -- file it will be ignored but you can customize this behavior here. ---@param img string follow_img_func = function(img) @@ -443,7 +444,7 @@ This is a complete list of all of the options that can be passed to `require("ob -- https://github.com/Vinzent03/obsidian-advanced-uri use_advanced_uri = false, - -- Optional, set to true to force ':ObsidianOpen' to bring the app to the foreground. + -- Optional, set to true to force ':Obsidian open' to bring the app to the foreground. open_app_foreground = false, picker = { @@ -467,7 +468,7 @@ This is a complete list of all of the options that can be passed to `require("ob -- Optional, sort search results by "path", "modified", "accessed", or "created". -- The recommend value is "modified" and `true` for `sort_reversed`, which means, for example, - -- that `:ObsidianQuickSwitch` will show the notes sorted by latest modified time + -- that `:Obsidian quickswitch` will show the notes sorted by latest modified time sort_by = "modified", sort_reversed = true, @@ -554,12 +555,12 @@ This is a complete list of all of the options that can be passed to `require("ob -- Specify how to handle attachments. attachments = { - -- The default folder to place images in via `:ObsidianPasteImg`. + -- The default folder to place images in via `:Obsidian pasteimg`. -- If this is a relative path it will be interpreted as relative to the vault root. -- You can always override this per image by passing a full path to the command instead of just a filename. img_folder = "assets/imgs", -- This is the default - -- Optional, customize the default name or prefix when pasting images via `:ObsidianPasteImg`. + -- Optional, customize the default name or prefix when pasting images via `:Obsidian pasteimg`. ---@return string img_name_func = function() -- Prefix image names with timestamp. @@ -807,4 +808,5 @@ And keep in mind that to reset a configuration option to `nil` you'll have to us Please read the [CONTRIBUTING](https://github.com/obsidian-nvim/obsidian.nvim/blob/main/.github/CONTRIBUTING.md) guide before submitting a pull request. ## Acknowledgement + We would like to thank [epwalsh](https://github.com/epwalsh) for creating this beautiful plugin. If you're feeling especially generous, [he still appreciates some coffee funds! ❤️](https://www.buymeacoffee.com/epwalsh). diff --git a/doc/obsidian.txt b/doc/obsidian.txt index 899605193..8b3dd385f 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -76,64 +76,64 @@ extmarks for references, tags, and check-boxes. COMMANDS *obsidian-commands* -- `:ObsidianOpen [QUERY]` to open a note in the Obsidian app. This command has +- `:Obsidian open [QUERY]` to open a note in the Obsidian app. This command has one optional argument: a query used to resolve the note to open by ID, path, or alias. If not given, the note corresponding to the current buffer is opened. -- `:ObsidianNew [TITLE]` to create a new note. This command has one optional +- `:Obsidian new [TITLE]` to create a new note. This command has one optional argument: the title of the new note. -- `:ObsidianQuickSwitch` to quickly switch to (or open) another note in your +- `:Obsidian quickswitch` to quickly switch to (or open) another note in your vault, searching by its name using ripgrep with your preferred picker (see |obsidian-plugin-dependencies| below). -- `:ObsidianFollowLink [vsplit|hsplit]` to follow a note reference under the +- `:Obsidian followlink [vsplit|hsplit]` to follow a note reference under the cursor, optionally opening it in a vertical or horizontal split. -- `:ObsidianBacklinks` for getting a picker list of references to the current +- `:Obsidian backlinks` for getting a picker list of references to the current buffer. -- `:ObsidianTags [TAG ...]` for getting a picker list of all occurrences of the +- `:Obsidian tags [TAG ...]` for getting a picker list of all occurrences of the given tags. -- `:ObsidianToday [OFFSET]` to open/create a new daily note. This command also +- `:Obsidian today [OFFSET]` to open/create a new daily note. This command also takes an optional offset in days, e.g. use `:ObsidianToday -1` to go to yesterday’s note. Unlike `:ObsidianYesterday` and `:ObsidianTomorrow` this command does not differentiate between weekdays and weekends. -- `:ObsidianYesterday` to open/create the daily note for the previous working +- `:Obsidian yesterday` to open/create the daily note for the previous working day. -- `:ObsidianTomorrow` to open/create the daily note for the next working day. -- `:ObsidianDailies [OFFSET ...]` to open a picker list of daily notes. For +- `:Obsidian tomorrow` to open/create the daily note for the next working day. +- `:Obsidian dailies [OFFSET ...]` to open a picker list of daily notes. For example, `:ObsidianDailies -2 1` to list daily notes from 2 days ago until tomorrow. -- `:ObsidianTemplate [NAME]` to insert a template from the templates folder, +- `:Obsidian template [NAME]` to insert a template from the templates folder, selecting from a list using your preferred picker. See |obsidian-"using-templates"| for more information. -- `:ObsidianSearch [QUERY]` to search for (or create) notes in your vault using +- `:Obsidian search [QUERY]` to search for (or create) notes in your vault using `ripgrep` with your preferred picker. -- `:ObsidianLink [QUERY]` to link an inline visual selection of text to a note. +- `:Obsidian link [QUERY]` to link an inline visual selection of text to a note. This command has one optional argument: a query that will be used to resolve the note by ID, path, or alias. If not given, the selected text will be used as the query. -- `:ObsidianLinkNew [TITLE]` to create a new note and link it to an inline visual +- `:Obsidian linknew [TITLE]` to create a new note and link it to an inline visual selection of text. This command has one optional argument: the title of the new note. If not given, the selected text will be used as the title. -- `:ObsidianLinks` to collect all links within the current buffer into a picker +- `:Obsidian links` to collect all links within the current buffer into a picker window. -- `:ObsidianExtractNote [TITLE]` to extract the visually selected text into a new +- `:Obsidian extractnote [TITLE]` to extract the visually selected text into a new note and link to it. -- `:ObsidianWorkspace [NAME]` to switch to another workspace. -- `:ObsidianPasteImg [IMGNAME]` to paste an image from the clipboard into the +- `:Obsidian workspace [NAME]` to switch to another workspace. +- `:Obsidian pasteimg [IMGNAME]` to paste an image from the clipboard into the note at the cursor position by saving it to the vault and adding a markdown image link. You can configure the default folder to save images to with the `attachments.img_folder` option. -- `:ObsidianRename [NEWNAME] [--dry-run]` to rename the note of the current +- `:Obsidian rename [NEWNAME] [--dry-run]` to rename the note of the current buffer or reference under the cursor, updating all backlinks across the vault. Since this command is still relatively new and could potentially write a lot of changes to your vault, I highly recommend committing the current state of your vault (if you’re using version control) before running it, or doing a dry-run first by appending "–dry-run" to the command, e.g. `:ObsidianRename new-id --dry-run`. -- `:ObsidianToggleCheckbox` to cycle through checkbox options. -- `:ObsidianNewFromTemplate [TITLE]` to create a new note from a template in the +- `:Obsidian togglecheckbox` to cycle through checkbox options. +- `:Obsidian newfromtemplate [TITLE]` to create a new note from a template in the templates folder. Selecting from a list using your preferred picker. This command has one optional argument: the title of the new note. -- `:ObsidianTOC` to load the table of contents of the current note into a picker +- `:Obsidian toc` to load the table of contents of the current note into a picker list. @@ -160,8 +160,8 @@ all of obsidian.nvim’s functionality: - **MacOS** users need `pngpaste` (`brew install pngpaste`) for the `:ObsidianPasteImg` command. - **Linux** users need xclip (X11) or wl-clipboard (Wayland) for the `:ObsidianPasteImg` command. -Search functionality (e.g. via the `:ObsidianSearch` and -`:ObsidianQuickSwitch` commands) also requires a picker such telescope.nvim +Search functionality (e.g. via the `:Obsidian search` and +`:Obsidian quickswitch` commands) also requires a picker such telescope.nvim (see |obsidian-plugin-dependencies| below). diff --git a/lua/obsidian/commands/dailies.lua b/lua/obsidian/commands/dailies.lua index 2a553fede..64c9521a6 100644 --- a/lua/obsidian/commands/dailies.lua +++ b/lua/obsidian/commands/dailies.lua @@ -31,7 +31,7 @@ return function(client, data) offset_start = offsets[1] offset_end = offsets[2] else - error ":ObsidianDailies expected at most 2 arguments" + error ":Obsidian dailies expected at most 2 arguments" end end diff --git a/lua/obsidian/commands/init-legacy.lua b/lua/obsidian/commands/init-legacy.lua new file mode 100644 index 000000000..f31eaf8ea --- /dev/null +++ b/lua/obsidian/commands/init-legacy.lua @@ -0,0 +1,194 @@ +local util = require "obsidian.util" +local iter = require("obsidian.itertools").iter + +local command_lookups = { + ObsidianCheck = "obsidian.commands.check", + ObsidianToggleCheckbox = "obsidian.commands.toggle_checkbox", + ObsidianToday = "obsidian.commands.today", + ObsidianYesterday = "obsidian.commands.yesterday", + ObsidianTomorrow = "obsidian.commands.tomorrow", + ObsidianDailies = "obsidian.commands.dailies", + ObsidianNew = "obsidian.commands.new", + ObsidianOpen = "obsidian.commands.open", + ObsidianBacklinks = "obsidian.commands.backlinks", + ObsidianSearch = "obsidian.commands.search", + ObsidianTags = "obsidian.commands.tags", + ObsidianTemplate = "obsidian.commands.template", + ObsidianNewFromTemplate = "obsidian.commands.new_from_template", + ObsidianQuickSwitch = "obsidian.commands.quick_switch", + ObsidianLinkNew = "obsidian.commands.link_new", + ObsidianLink = "obsidian.commands.link", + ObsidianLinks = "obsidian.commands.links", + ObsidianFollowLink = "obsidian.commands.follow_link", + ObsidianWorkspace = "obsidian.commands.workspace", + ObsidianRename = "obsidian.commands.rename", + ObsidianPasteImg = "obsidian.commands.paste_img", + ObsidianExtractNote = "obsidian.commands.extract_note", + ObsidianDebug = "obsidian.commands.debug", + ObsidianTOC = "obsidian.commands.toc", +} + +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(assert(vim.fn.getpos "'<")) + local _, cerow, cecol, _ = unpack(assert(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, { search = { sort = true } })) do + local note_path = assert(client:vault_relative_path(note.path, { strict = true })) + 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.register("ObsidianCheck", { opts = { nargs = 0, desc = "Check for issues in your vault" } }) + +M.register("ObsidianToday", { opts = { nargs = "?", desc = "Open today's daily note" } }) + +M.register("ObsidianYesterday", { opts = { nargs = 0, desc = "Open the daily note for the previous working day" } }) + +M.register("ObsidianTomorrow", { opts = { nargs = 0, desc = "Open the daily note for the next working day" } }) + +M.register("ObsidianDailies", { opts = { nargs = "*", desc = "Open a picker with daily notes" } }) + +M.register("ObsidianNew", { opts = { nargs = "?", complete = "file", desc = "Create a new note" } }) + +M.register( + "ObsidianOpen", + { opts = { nargs = "?", desc = "Open in the Obsidian app" }, complete = M.complete_args_search } +) + +M.register("ObsidianBacklinks", { opts = { nargs = 0, desc = "Collect backlinks" } }) + +M.register("ObsidianTags", { opts = { nargs = "*", range = true, desc = "Find tags" } }) + +M.register("ObsidianSearch", { opts = { nargs = "?", desc = "Search vault" } }) + +M.register("ObsidianTemplate", { opts = { nargs = "?", desc = "Insert a template" } }) + +M.register("ObsidianNewFromTemplate", { opts = { nargs = "?", desc = "Create a new note from a template" } }) + +M.register("ObsidianQuickSwitch", { opts = { nargs = "?", desc = "Switch notes" } }) + +M.register("ObsidianLinkNew", { opts = { nargs = "?", range = true, desc = "Link selected text to a new note" } }) + +M.register("ObsidianLink", { + opts = { nargs = "?", range = true, desc = "Link selected text to an existing note" }, + complete = M.complete_args_search, +}) + +M.register("ObsidianLinks", { opts = { nargs = 0, desc = "Collect all links within the current buffer" } }) + +M.register("ObsidianFollowLink", { opts = { nargs = "?", desc = "Follow reference or link under cursor" } }) + +M.register("ObsidianToggleCheckbox", { opts = { nargs = 0, desc = "Toggle checkbox" } }) + +M.register("ObsidianWorkspace", { opts = { nargs = "?", desc = "Check or switch workspace" } }) + +M.register( + "ObsidianRename", + { opts = { nargs = "?", complete = "file", desc = "Rename note and update all references to it" } } +) + +M.register( + "ObsidianPasteImg", + { opts = { nargs = "?", complete = "file", desc = "Paste an image from the clipboard" } } +) + +M.register( + "ObsidianExtractNote", + { opts = { nargs = "?", range = true, desc = "Extract selected text to a new note and link to it" } } +) + +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" } }) + +return M diff --git a/lua/obsidian/commands/init.lua b/lua/obsidian/commands/init.lua index f31eaf8ea..c37f9f15a 100644 --- a/lua/obsidian/commands/init.lua +++ b/lua/obsidian/commands/init.lua @@ -1,31 +1,32 @@ -local util = require "obsidian.util" local iter = require("obsidian.itertools").iter +local log = require "obsidian.log" +local legacycommands = require "obsidian.commands.init-legacy" local command_lookups = { - ObsidianCheck = "obsidian.commands.check", - ObsidianToggleCheckbox = "obsidian.commands.toggle_checkbox", - ObsidianToday = "obsidian.commands.today", - ObsidianYesterday = "obsidian.commands.yesterday", - ObsidianTomorrow = "obsidian.commands.tomorrow", - ObsidianDailies = "obsidian.commands.dailies", - ObsidianNew = "obsidian.commands.new", - ObsidianOpen = "obsidian.commands.open", - ObsidianBacklinks = "obsidian.commands.backlinks", - ObsidianSearch = "obsidian.commands.search", - ObsidianTags = "obsidian.commands.tags", - ObsidianTemplate = "obsidian.commands.template", - ObsidianNewFromTemplate = "obsidian.commands.new_from_template", - ObsidianQuickSwitch = "obsidian.commands.quick_switch", - ObsidianLinkNew = "obsidian.commands.link_new", - ObsidianLink = "obsidian.commands.link", - ObsidianLinks = "obsidian.commands.links", - ObsidianFollowLink = "obsidian.commands.follow_link", - ObsidianWorkspace = "obsidian.commands.workspace", - ObsidianRename = "obsidian.commands.rename", - ObsidianPasteImg = "obsidian.commands.paste_img", - ObsidianExtractNote = "obsidian.commands.extract_note", - ObsidianDebug = "obsidian.commands.debug", - ObsidianTOC = "obsidian.commands.toc", + check = "obsidian.commands.check", + togglecheckbox = "obsidian.commands.toggle_checkbox", + today = "obsidian.commands.today", + yesterday = "obsidian.commands.yesterday", + tomorrow = "obsidian.commands.tomorrow", + dailies = "obsidian.commands.dailies", + new = "obsidian.commands.new", + open = "obsidian.commands.open", + backlinks = "obsidian.commands.backlinks", + search = "obsidian.commands.search", + tags = "obsidian.commands.tags", + template = "obsidian.commands.template", + newfromtemplate = "obsidian.commands.new_from_template", + quickswitch = "obsidian.commands.quick_switch", + linknew = "obsidian.commands.link_new", + link = "obsidian.commands.link", + links = "obsidian.commands.links", + followlink = "obsidian.commands.follow_link", + workspace = "obsidian.commands.workspace", + rename = "obsidian.commands.rename", + pasteimg = "obsidian.commands.paste_img", + extractnote = "obsidian.commands.extract_note", + debug = "obsidian.commands.debug", + toc = "obsidian.commands.toc", } local M = setmetatable({ @@ -45,8 +46,9 @@ local M = setmetatable({ }) ---@class obsidian.CommandConfig ----@field opts table ----@field complete function|? +---@field complete function|string|? +---@field nargs string|integer|? +---@field range boolean|? ---@field func function|? (obsidian.Client, table) -> nil ---Register a new command. @@ -65,26 +67,93 @@ end --- ---@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 + vim.api.nvim_create_user_command("Obsidian", function(data) + M.handle_command(client, data) + end, { + nargs = "+", + complete = function(_, cmdline, _) + return M.get_completions(client, cmdline) + end, + range = 2, + }) +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 +M.install_legacy = legacycommands.install + +---@param client obsidian.Client +M.handle_command = function(client, data) + local cmd = data.fargs[1] + table.remove(data.fargs, 1) + data.args = table.concat(data.fargs, " ") + local nargs = #data.fargs + + local cmdconfig = M.commands[cmd] + if cmdconfig == nil then + log.err("Command '" .. cmd .. "' not found") + return + end + + local exp_nargs = cmdconfig.nargs + local range_allowed = cmdconfig.range + + if exp_nargs == "?" then + if nargs > 1 then + log.err("Command '" .. cmd .. "' expects 0 or 1 arguments, but " .. nargs .. " were provided") + return end + elseif exp_nargs == "+" then + if nargs == 0 then + log.err("Command '" .. cmd .. "' expects at least one argument, but none were provided") + return + end + elseif exp_nargs ~= "*" and exp_nargs ~= nargs then + log.err("Command '" .. cmd .. "' expects " .. exp_nargs .. " arguments, but " .. nargs .. " were provided") + return + end - vim.api.nvim_create_user_command(command_name, func, command_config.opts) + if not range_allowed and data.range > 0 then + log.error("Command '" .. cmd .. "' does not accept a range") + return end + + cmdconfig.func(client, data) end +---@param client obsidian.Client +---@param cmdline string +M.get_completions = function(client, cmdline) + local obspat = "^['<,'>]*Obsidian[!]?" + local splitcmd = vim.split(cmdline, " ", { plain = true, trimempty = true }) + local obsidiancmd = splitcmd[2] + if cmdline:match(obspat .. "%s$") then + return vim.tbl_keys(M.commands) + end + if cmdline:match(obspat .. "%s%S+$") then + return vim.tbl_filter(function(s) + return s:sub(1, #obsidiancmd) == obsidiancmd + end, vim.tbl_keys(M.commands)) + end + local cmdconfig = M.commands[obsidiancmd] + if cmdconfig == nil then + return + end + if cmdline:match(obspat .. "%s%S*%s%S*$") then + local cmd_arg = table.concat(vim.list_slice(splitcmd, 3), " ") + local complete_type = type(cmdconfig.complete) + if complete_type == "function" then + return cmdconfig.complete(client, cmd_arg) + end + if complete_type == "string" then + return vim.fn.getcompletion(cmd_arg, cmdconfig.complete) + end + end +end + +--TODO: Note completion is currently broken (see: https://github.com/epwalsh/obsidian.nvim/issues/753) ---@param client obsidian.Client ---@return string[] -M.complete_args_search = function(client, _, cmd_line, _) +M.note_complete = function(client, cmd_arg) 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 {} @@ -128,67 +197,52 @@ M.complete_args_search = function(client, _, cmd_line, _) return completions end -M.register("ObsidianCheck", { opts = { nargs = 0, desc = "Check for issues in your vault" } }) +M.register("check", { nargs = 0 }) -M.register("ObsidianToday", { opts = { nargs = "?", desc = "Open today's daily note" } }) +M.register("today", { nargs = "?" }) -M.register("ObsidianYesterday", { opts = { nargs = 0, desc = "Open the daily note for the previous working day" } }) +M.register("yesterday", { nargs = 0 }) -M.register("ObsidianTomorrow", { opts = { nargs = 0, desc = "Open the daily note for the next working day" } }) +M.register("tomorrow", { nargs = 0 }) -M.register("ObsidianDailies", { opts = { nargs = "*", desc = "Open a picker with daily notes" } }) +M.register("dailies", { nargs = "*" }) -M.register("ObsidianNew", { opts = { nargs = "?", complete = "file", desc = "Create a new note" } }) +M.register("new", { nargs = "?", complete = "file" }) -M.register( - "ObsidianOpen", - { opts = { nargs = "?", desc = "Open in the Obsidian app" }, complete = M.complete_args_search } -) +M.register("open", { nargs = "?", complete = M.note_complete }) -M.register("ObsidianBacklinks", { opts = { nargs = 0, desc = "Collect backlinks" } }) +M.register("backlinks", { nargs = 0 }) -M.register("ObsidianTags", { opts = { nargs = "*", range = true, desc = "Find tags" } }) +M.register("tags", { nargs = "*", range = true }) -M.register("ObsidianSearch", { opts = { nargs = "?", desc = "Search vault" } }) +M.register("search", { nargs = "?" }) -M.register("ObsidianTemplate", { opts = { nargs = "?", desc = "Insert a template" } }) +M.register("template", { nargs = "?" }) -M.register("ObsidianNewFromTemplate", { opts = { nargs = "?", desc = "Create a new note from a template" } }) +M.register("newfromtemplate", { nargs = "?" }) -M.register("ObsidianQuickSwitch", { opts = { nargs = "?", desc = "Switch notes" } }) +M.register("quickswitch", { nargs = "?" }) -M.register("ObsidianLinkNew", { opts = { nargs = "?", range = true, desc = "Link selected text to a new note" } }) +M.register("linknew", { nargs = "?", range = true }) -M.register("ObsidianLink", { - opts = { nargs = "?", range = true, desc = "Link selected text to an existing note" }, - complete = M.complete_args_search, -}) +M.register("link", { nargs = "?", range = true, complete = M.note_complete }) -M.register("ObsidianLinks", { opts = { nargs = 0, desc = "Collect all links within the current buffer" } }) +M.register("links", { nargs = 0 }) -M.register("ObsidianFollowLink", { opts = { nargs = "?", desc = "Follow reference or link under cursor" } }) +M.register("followlink", { nargs = "?" }) -M.register("ObsidianToggleCheckbox", { opts = { nargs = 0, desc = "Toggle checkbox" } }) +M.register("togglecheckbox", { nargs = 0 }) -M.register("ObsidianWorkspace", { opts = { nargs = "?", desc = "Check or switch workspace" } }) +M.register("workspace", { nargs = "?" }) -M.register( - "ObsidianRename", - { opts = { nargs = "?", complete = "file", desc = "Rename note and update all references to it" } } -) +M.register("rename", { nargs = "?", complete = "file" }) -M.register( - "ObsidianPasteImg", - { opts = { nargs = "?", complete = "file", desc = "Paste an image from the clipboard" } } -) +M.register("pasteimg", { nargs = "?", complete = "file" }) -M.register( - "ObsidianExtractNote", - { opts = { nargs = "?", range = true, desc = "Extract selected text to a new note and link to it" } } -) +M.register("extractnote", { nargs = "?", range = true }) -M.register("ObsidianDebug", { opts = { nargs = 0, desc = "Log some information for debugging" } }) +M.register("debug", { nargs = 0 }) -M.register("ObsidianTOC", { opts = { nargs = 0, desc = "Load the table of contents into a picker" } }) +M.register("toc", { nargs = 0 }) return M diff --git a/lua/obsidian/commands/rename.lua b/lua/obsidian/commands/rename.lua index b356d971d..5ac67a541 100644 --- a/lua/obsidian/commands/rename.lua +++ b/lua/obsidian/commands/rename.lua @@ -113,7 +113,7 @@ return function(client, data) .. "'...\n" .. "This will write all buffers and potentially modify a lot of files. If you're using version control " .. "with your vault it would be a good idea to commit the current state of your vault before running this.\n" - .. "You can also do a dry run of this by running ':ObsidianRename " + .. "You can also do a dry run of this by running ':Obsidian rename " .. arg .. " --dry-run'.\n" .. "Do you want to continue?" diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 3a78caad5..8686615f5 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -32,6 +32,7 @@ local config = {} ---@field ui obsidian.config.UIOpts | table ---@field attachments obsidian.config.AttachmentsOpts ---@field callbacks obsidian.config.CallbackConfig +---@field legacy_commands boolean config.ClientOpts = {} --- Get defaults. @@ -65,6 +66,7 @@ config.ClientOpts.default = function() ui = config.UIOpts.default(), attachments = config.AttachmentsOpts.default(), callbacks = config.CallbackConfig.default(), + legacy_commands = false, } end @@ -215,6 +217,11 @@ config.ClientOpts.normalize = function(opts, defaults) opts.image_name_func = nil end + if opts.legacy_commands then + log.warn_once "The 'legacy_commands' config option is deprecated and will be removed in a future update." + opts.tags = nil + end + -------------------------- -- Merge with defaults. -- -------------------------- diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 7cc8dab83..bf2878182 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -99,6 +99,10 @@ obsidian.setup = function(opts) -- These will be available across all buffers, not just note buffers in the vault. obsidian.commands.install(client) + if opts.legacy_commands then + obsidian.commands.install_legacy(client) + end + -- Register completion sources, providers if opts.completion.nvim_cmp then require("obsidian.completion.plugin_initializers.nvim_cmp").register_sources()