From 7b07a6c8baa847ab93bbe009fed457da7b692073 Mon Sep 17 00:00:00 2001 From: Idan Arye Date: Sun, 7 May 2023 15:49:07 +0300 Subject: [PATCH] feat: add `finder` option for choosing a preferred finder backend (#129) --- CHANGELOG.md | 4 + README.md | 8 ++ lua/obsidian/command.lua | 280 +++++++++++++++++++++------------------ lua/obsidian/config.lua | 2 + lua/obsidian/init.lua | 21 +++ lua/obsidian/util.lua | 68 ++++++++++ 6 files changed, 253 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b50276f..793dfd71d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed annoying "skipped updating frontmatter" message on file write. +### Added + +- Added `finder` option for choosing a preferred finder backend. + ## [v1.9.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v1.9.0) - 2023-04-22 ### Added diff --git a/README.md b/README.md index 3ff7deda3..307944fba 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,14 @@ return { -- Optional, set to true to force ':ObsidianOpen' to bring the app to the foreground. open_app_foreground = false, + + -- Optional, by default commands like `:ObsidianSearch` will attempt - + -- in that order - telescope.nvim, fzf-lua, and fzf.nvim, and use the + -- first one they find. By setting this option to your preferred + -- finder, you can attempt it first. Note that if the specified finder + -- is not installed, or if it the command does not support it, the + -- remaining finders will be attempted in the original order. + finder = "fzf-lua", }, config = function(_, opts) require("obsidian").setup(opts) diff --git a/lua/obsidian/command.lua b/lua/obsidian/command.lua index efa1a275e..37aa0e640 100644 --- a/lua/obsidian/command.lua +++ b/lua/obsidian/command.lua @@ -192,50 +192,60 @@ end command.search = function(client, data) local base_cmd = vim.tbl_flatten { util.SEARCH_CMD, { "--smart-case", "--column", "--line-number", "--no-heading" } } - local has_telescope, telescope = pcall(require, "telescope.builtin") + client:_run_with_finder_backend(":ObsidianSearch", { + ["telescope.nvim"] = function() + local has_telescope, telescope = pcall(require, "telescope.builtin") - if has_telescope then - -- Search with telescope.nvim - local vimgrep_arguments = vim.tbl_flatten { base_cmd, { - "--with-filename", - "--color=never", - } } - - if data.args:len() > 0 then - telescope.grep_string { cwd = tostring(client.dir), search = data.args, vimgrep_arguments = vimgrep_arguments } - else - telescope.live_grep { cwd = tostring(client.dir), vimgrep_arguments = vimgrep_arguments } - end - return - end - - local has_fzf_lua, fzf_lua = pcall(require, "fzf-lua") - - if has_fzf_lua then - if data.args:len() > 0 then - fzf_lua.grep { cwd = tostring(client.dir), search = data.args } - else - fzf_lua.live_grep { cwd = tostring(client.dir), exec_empty_query = true } - end - return - end - - -- Fall back to trying with fzf.vim - local has_fzf, _ = pcall(function() - local grep_cmd = - vim.tbl_flatten { base_cmd, { "--color=always", "--", vim.fn.shellescape(data.args), tostring(client.dir) } } - - vim.api.nvim_call_function("fzf#vim#grep", { - table.concat(grep_cmd, " "), - true, - vim.api.nvim_call_function("fzf#vim#with_preview", {}), - false, - }) - end) + if not has_telescope then + util.implementation_unavailable() + end + -- Search with telescope.nvim + local vimgrep_arguments = + vim.tbl_flatten { base_cmd, { + "--with-filename", + "--color=never", + } } + + if data.args:len() > 0 then + telescope.grep_string { + cwd = tostring(client.dir), + search = data.args, + vimgrep_arguments = vimgrep_arguments, + } + else + telescope.live_grep { cwd = tostring(client.dir), vimgrep_arguments = vimgrep_arguments } + end + end, + ["fzf-lua"] = function() + local has_fzf_lua, fzf_lua = pcall(require, "fzf-lua") - if not has_fzf then - echo.err "Either telescope.nvim, fzf-lua or fzf.vim is required for :ObsidianSearch command" - end + if not has_fzf_lua then + util.implementation_unavailable() + end + if data.args:len() > 0 then + fzf_lua.grep { cwd = tostring(client.dir), search = data.args } + else + fzf_lua.live_grep { cwd = tostring(client.dir), exec_empty_query = true } + end + end, + ["fzf.vim"] = function() + -- Fall back to trying with fzf.vim + local has_fzf, _ = pcall(function() + local grep_cmd = + vim.tbl_flatten { base_cmd, { "--color=always", "--", vim.fn.shellescape(data.args), tostring(client.dir) } } + + vim.api.nvim_call_function("fzf#vim#grep", { + table.concat(grep_cmd, " "), + true, + vim.api.nvim_call_function("fzf#vim#with_preview", {}), + false, + }) + end) + if not has_fzf then + util.implementation_unavailable() + end + end, + }) end --- Insert a template @@ -287,70 +297,74 @@ command.insert_template = function(client, data) vim.api.nvim_win_set_cursor(0, { new_row, 0 }) end - -- try with telescope.nvim - local has_telescope, _ = pcall(require, "telescope.builtin") - if has_telescope then - local choose_template = function() - local opts = { + client:_run_with_finder_backend(":ObsidianTemplate", { + ["telescope.nvim"] = function() + -- try with telescope.nvim + local has_telescope, _ = pcall(require, "telescope.builtin") + if not has_telescope then + util.implementation_unavailable() + end + local choose_template = function() + local opts = { + cwd = tostring(templates_dir), + attach_mappings = function(_, map) + map({ "i", "n" }, "", function(prompt_bufnr) + local template = require("telescope.actions.state").get_selected_entry() + require("telescope.actions").close(prompt_bufnr) + apply_template(template[1]) + end) + end, + } + require("telescope.builtin").find_files(opts) + end + choose_template() + end, + ["fzf-lua"] = function() + -- try with fzf-lua + local has_fzf_lua, fzf_lua = pcall(require, "fzf-lua") + if not has_fzf_lua then + util.implementation_unavailable() + end + local cmd = vim.tbl_flatten { util.FIND_CMD, { ".", "-name", "'*.md'" } } + cmd = util.table_params_to_str(cmd) + fzf_lua.files { + cmd = cmd, cwd = tostring(templates_dir), - attach_mappings = function(_, map) - map({ "i", "n" }, "", function(prompt_bufnr) - local template = require("telescope.actions.state").get_selected_entry() - require("telescope.actions").close(prompt_bufnr) - apply_template(template[1]) - end) - return true - end, + file_icons = false, + actions = { + ["default"] = function(entry) + -- for some reason fzf-lua passes the filename with 6 characters + -- at the start that appear on screen as 2 whitespace characters + -- so we need to start on the 7th character + local template = entry[1]:sub(7) + apply_template(template) + end, + }, } - require("telescope.builtin").find_files(opts) - end - choose_template() - return - end - - -- try with fzf-lua - local has_fzf_lua, fzf_lua = pcall(require, "fzf-lua") - if has_fzf_lua then - local cmd = vim.tbl_flatten { util.FIND_CMD, { ".", "-name", "'*.md'" } } - cmd = util.table_params_to_str(cmd) - fzf_lua.files { - cmd = cmd, - cwd = tostring(templates_dir), - file_icons = false, - actions = { - ["default"] = function(entry) - -- for some reason fzf-lua passes the filename with 6 characters - -- at the start that appear on screen as 2 whitespace characters - -- so we need to start on the 7th character - local template = entry[1]:sub(7) + end, + ["fzf.vim"] = function() + -- try with fzf + local has_fzf, _ = pcall(function() + vim.api.nvim_create_user_command("ApplyTemplate", function(path) + -- remove escaped whitespace and extract the file name + local file_path = string.gsub(path.args, "\\ ", " ") + local template = vim.fs.basename(file_path) apply_template(template) - end, - }, - } - return - end - - -- try with fzf - local has_fzf, _ = pcall(function() - vim.api.nvim_create_user_command("ApplyTemplate", function(path) - -- remove escaped whitespace and extract the file name - local file_path = string.gsub(path.args, "\\ ", " ") - local template = vim.fs.basename(file_path) - apply_template(template) - vim.api.nvim_del_user_command "ApplyTemplate" - end, { nargs = 1, bang = true }) - - local base_cmd = vim.tbl_flatten { util.FIND_CMD, { tostring(templates_dir), "-name", "'*.md'" } } - base_cmd = util.table_params_to_str(base_cmd) - local fzf_options = { source = base_cmd, sink = "ApplyTemplate" } - vim.api.nvim_call_function("fzf#run", { - vim.api.nvim_call_function("fzf#wrap", { fzf_options }), - }) - end) - - if not has_fzf then - echo.err "Either telescope.nvim or fzf.vim is required for :ObsidianTemplate command" - end + vim.api.nvim_del_user_command "ApplyTemplate" + end, { nargs = 1, bang = true }) + + local base_cmd = vim.tbl_flatten { util.FIND_CMD, { tostring(templates_dir), "-name", "'*.md'" } } + base_cmd = util.table_params_to_str(base_cmd) + local fzf_options = { source = base_cmd, sink = "ApplyTemplate" } + vim.api.nvim_call_function("fzf#run", { + vim.api.nvim_call_function("fzf#wrap", { fzf_options }), + }) + end) + if not has_fzf then + util.implementation_unavailable() + end + end, + }) end ---Quick switch to an obsidian note @@ -359,36 +373,42 @@ end ---@param data table command.quick_switch = function(client, data) local dir = tostring(client.dir) - local has_telescope, telescope = pcall(require, "telescope.builtin") - - if has_telescope then - -- Search with telescope.nvim - telescope.find_files { cwd = dir, search_file = "*.md" } - return - end - local has_fzf_lua, fzf_lua = pcall(require, "fzf-lua") + client:_run_with_finder_backend(":ObsidianQuickSwitch", { + ["telescope.nvim"] = function() + local has_telescope, telescope = pcall(require, "telescope.builtin") - if has_fzf_lua then - local cmd = vim.tbl_flatten { util.FIND_CMD, { ".", "-name", "'*.md'" } } - cmd = util.table_params_to_str(cmd) - fzf_lua.files { cmd = cmd, cwd = tostring(client.dir) } - return - end - - -- Fall back to trying with fzf.vim - local has_fzf, _ = pcall(function() - local base_cmd = vim.tbl_flatten { util.FIND_CMD, { dir, "-name", "'*.md'" } } - base_cmd = util.table_params_to_str(base_cmd) - local fzf_options = { source = base_cmd, sink = "e" } - vim.api.nvim_call_function("fzf#run", { - vim.api.nvim_call_function("fzf#wrap", { fzf_options }), - }) - end) + if not has_telescope then + util.implementation_unavailable() + end + -- Search with telescope.nvim + telescope.find_files { cwd = dir, search_file = "*.md" } + end, + ["fzf-lua"] = function() + local has_fzf_lua, fzf_lua = pcall(require, "fzf-lua") - if not has_fzf then - echo.err "Either telescope.nvim or fzf.vim is required for :ObsidianQuickSwitch command" - end + if not has_fzf_lua then + util.implementation_unavailable() + end + local cmd = vim.tbl_flatten { util.FIND_CMD, { ".", "-name", "'*.md'" } } + cmd = util.table_params_to_str(cmd) + fzf_lua.files { cmd = cmd, cwd = tostring(client.dir) } + end, + ["fzf.vim"] = function() + -- Fall back to trying with fzf.vim + local has_fzf, _ = pcall(function() + local base_cmd = vim.tbl_flatten { util.FIND_CMD, { dir, "-name", "'*.md'" } } + base_cmd = util.table_params_to_str(base_cmd) + local fzf_options = { source = base_cmd, sink = "e" } + vim.api.nvim_call_function("fzf#run", { + vim.api.nvim_call_function("fzf#wrap", { fzf_options }), + }) + end) + if not has_fzf then + util.implementation_unavailable() + end + end, + }) end command.link_new = function(client, data) diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 1cf6ca24e..73aca5750 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -17,6 +17,7 @@ local config = {} ---@field daily_notes obsidian.config.DailyNotesOpts ---@field use_advanced_uri boolean|? ---@field open_app_foreground boolean|? +---@field finder string|? config.ClientOpts = {} ---Get defaults. @@ -34,6 +35,7 @@ config.ClientOpts.default = function() daily_notes = config.DailyNotesOpts.default(), use_advanced_uri = nil, open_app_foreground = false, + finder = nil, } end diff --git a/lua/obsidian/init.lua b/lua/obsidian/init.lua index 560f11893..cc054f55c 100644 --- a/lua/obsidian/init.lua +++ b/lua/obsidian/init.lua @@ -357,4 +357,25 @@ client.resolve_note = function(self, query) return nil end +client._run_with_finder_backend = function(self, command_name, implementations) + local finders_order = { "telescope.nvim", "fzf-lua", "fzf.vim" } + if self.opts.finder then + for idx, finder in ipairs(finders_order) do + if finder == self.opts.finder then + table.remove(finders_order, idx) + break + end + end + table.insert(finders_order, 1, self.opts.finder) + end + local success, err = pcall(obsidian.util.run_first_supported, command_name, finders_order, implementations) + if not success then + if type(err) == "string" then + echo.err(err) + else + error(err) + end + end +end + return obsidian diff --git a/lua/obsidian/util.lua b/lua/obsidian/util.lua index 2b54dfd47..1097af87b 100644 --- a/lua/obsidian/util.lua +++ b/lua/obsidian/util.lua @@ -433,4 +433,72 @@ util.working_day_before = function(time) end end +local IMPLEMENTATION_UNAVAILABLE = { "implementation_unavailable called from outside run_first_supported" } + +---Try implementations one by one in the given order, until finding one that is supported +--- +---Implementations are given as functions. If the backend of the implementation +---is unavailable (usually because a plugin is not installed), the function +---should call the `implementation_unavailable()` function from the +---`obsidian.util` module so that the next implementation in order will be +---attempted. +--- +---If the implementation's backend is installed but for some reason the +---operation fails, the error will bubble up normally and the next +---implementation will not be attempted. +--- +---@param command_name string - name of the command, used for formatting the error message +---@param order table - list of implementation names in the order in which they should be attempted +---@param implementations table - map of implementation name to implementation function +util.run_first_supported = function(command_name, order, implementations) + local unavailable = {} + local not_supported = {} + for _, impl_name in ipairs(order) do + local impl_function = implementations[impl_name] + if impl_function then + local result = { pcall(impl_function) } + if result[1] then + return select(2, unpack(result)) + elseif result[2] == IMPLEMENTATION_UNAVAILABLE then + table.insert(unavailable, impl_name) + else + error(result[2]) + end + else + table.insert(not_supported, impl_name) + end + end + + if next(unavailable) == nil then + error(command_name .. " cannot be run with " .. table.concat(not_supported, " or ")) + end + + local error_message + if #unavailable == 1 then + error_message = unavailable[1] .. " is required for " .. command_name .. " command" + elseif #unavailable then + error_message = "Either " .. table.concat(unavailable, " or ") .. " is required for " .. command_name .. " command" + end + + if next(not_supported) ~= nil then + if #not_supported == 1 then + error_message = error_message .. ". " .. not_supported[1] .. " is not a viable option for this command" + else + error_message = error_message + .. ". " + .. table.concat(not_supported, " and ") + .. " are not viable options for this command" + end + end + + error(error_message) +end + +---Should be called inside implementation functions passed to +---`run_first_supported` when the implementation's backend is unavailable +---(usually because a plugin is not installed) +util.implementation_unavailable = function() + error(IMPLEMENTATION_UNAVAILABLE) +end + return util