From 726bf89c3385dc06bbe518e99543acd1ad7751b5 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Sun, 9 Feb 2025 10:58:44 -0500 Subject: [PATCH 1/5] implementing snack picker --- lua/obsidian/config.lua | 1 + lua/obsidian/pickers/_snacks.lua | 118 +++++++++++++++++++++++++++++++ lua/obsidian/pickers/init.lua | 4 +- 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 lua/obsidian/pickers/_snacks.lua diff --git a/lua/obsidian/config.lua b/lua/obsidian/config.lua index 4f3ae84d..1d32f361 100644 --- a/lua/obsidian/config.lua +++ b/lua/obsidian/config.lua @@ -343,6 +343,7 @@ config.Picker = { telescope = "telescope.nvim", fzf_lua = "fzf-lua", mini = "mini.pick", + snacks = "snacks.pick", } ---@class obsidian.config.PickerOpts diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua new file mode 100644 index 00000000..97b749fd --- /dev/null +++ b/lua/obsidian/pickers/_snacks.lua @@ -0,0 +1,118 @@ +local snacks_picker = require "snacks.picker" +local snacks = require "snacks" + + +local Path = require "obsidian.path" +local abc = require "obsidian.abc" +local Picker = require "obsidian.pickers.picker" + +---@param entry string +---@return string +local function clean_path(entry) + if type(entry) == "string" then + local path_end = assert(string.find(entry, ":", 1, true)) + return string.sub(entry, 1, path_end - 1) + end + vim.notify("entry: " .. table.concat(vim.tbl_keys(entry), ", ")) + return "" +end + +---@class obsidian.pickers.SnacksPicker : obsidian.Picker +local SnacksPicker = abc.new_class({ + ---@diagnostic disable-next-line: unused-local + __tostring = function(self) + return "SnacksPicker()" + end, +}, Picker) + +SnacksPicker.find_files = function(self, opts) + opts = opts and opts or {} + + ---@type obsidian.Path + local dir = opts.dir and Path:new(opts.dir) or self.client.dir + + local result = snacks_picker.pick("files", { + cwd = tostring(dir), + }) + + if result and opts.callback then + local path = clean_path(result) + opts.callback(tostring(dir / path)) + end +end + +SnacksPicker.grep = function(self, opts) + opts = opts and opts or {} + + ---@type obsidian.Path + local dir = opts.dir and Path:new(opts.dir) or self.client.dir + + local result = snacks_picker.pick("grep", { + cwd = tostring(dir), + }) + + if result and opts.callback then + local path = clean_path(result) + opts.callback(tostring(dir / path)) + end +end + +SnacksPicker.pick = function(self, values, opts) + + self.calling_bufnr = vim.api.nvim_get_current_buf() + + local buf = opts.buf or vim.api.nvim_get_current_buf() + + opts = opts and opts or {} + + local entries = {} + for _, value in ipairs(values) do + if type(value) == "string" then + table.insert(entries, { + text = value, + value = value, + }) + elseif value.valid ~= false then + local name = self:_make_display(value) + table.insert(entries, { + text = name, + buf = buf, + filename = value.filename, + value = value.value, + pos = { value.lnum, value.col }, + }) + end + end + + snacks_picker({ + tilte = opts.prompt_title, + items = entries, + layout = { + preview = false + }, + format = function(item, _) + local ret = {} + local a = snacks_picker.util.align + ret[#ret + 1] = { a(item.text, 20) } + return ret + end, + confirm = function(picker, item) + picker:close() + if item then + if opts.callback then + opts.callback(item.value) + elseif item then + vim.schedule(function() + if item["buf"] then + vim.api.nvim_set_current_buf(item["buf"]) + end + vim.api.nvim_win_set_cursor(0, {item["pos"][1], 0}) + end) + end + end + end, + -- sort = require("snacks.picker.sort").idx(), + }) +end + +return SnacksPicker diff --git a/lua/obsidian/pickers/init.lua b/lua/obsidian/pickers/init.lua index 7b2decd3..51d5ad88 100644 --- a/lua/obsidian/pickers/init.lua +++ b/lua/obsidian/pickers/init.lua @@ -13,7 +13,7 @@ M.get = function(client, picker_name) if picker_name then picker_name = string.lower(picker_name) else - for _, name in ipairs { PickerName.telescope, PickerName.fzf_lua, PickerName.mini } do + for _, name in ipairs { PickerName.telescope, PickerName.fzf_lua, PickerName.mini, PickerName.snacks } do local ok, res = pcall(M.get, client, name) if ok then return res @@ -28,6 +28,8 @@ M.get = function(client, picker_name) return require("obsidian.pickers._mini").new(client) elseif picker_name == string.lower(PickerName.fzf_lua) then return require("obsidian.pickers._fzf").new(client) + elseif picker_name == string.lower(PickerName.snacks) then + return require("obsidian.pickers._snacks").new(client) elseif picker_name then error("not implemented for " .. picker_name) end From d6490d5a05b7d5361484d363cf216b634207e839 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Wed, 12 Feb 2025 01:22:00 -0500 Subject: [PATCH 2/5] added documentation --- README.md | 3 ++- doc/obsidian.txt | 3 ++- lua/obsidian/commands/debug.lua | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d5d382b6..29647609 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ The only **required** plugin dependency is [plenary.nvim](https://github.com/nvi - **[recommended]** [nvim-telescope/telescope.nvim](https://github.com/nvim-telescope/telescope.nvim): for search and quick-switch functionality. - [Mini.Pick](https://github.com/echasnovski/mini.pick) from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. - [ibhagwan/fzf-lua](https://github.com/ibhagwan/fzf-lua): another alternative to telescope for search and quick-switch functionality. +- [Snacks.Picker](https://github.com/folke/snacks.nvim/blob/main/docs/picker.md) from the snacks.nvim library: an alternative to mini and telescope for search and quick-switch functionality. **Syntax highlighting:** @@ -412,7 +413,7 @@ This is a complete list of all of the options that can be passed to `require("ob open_app_foreground = false, picker = { - -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', or 'mini.pick'. + -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', 'mini.pick' or 'snacks.pick'. name = "telescope.nvim", -- Optional, configure key mappings for the picker. These are the defaults. -- Not all pickers support all mappings. diff --git a/doc/obsidian.txt b/doc/obsidian.txt index fc053994..9e5c1006 100644 --- a/doc/obsidian.txt +++ b/doc/obsidian.txt @@ -252,6 +252,7 @@ dependencies that enhance the obsidian.nvim experience. - **[recommended]** nvim-telescope/telescope.nvim : for search and quick-switch functionality. - Mini.Pick from the mini.nvim library: an alternative to telescope for search and quick-switch functionality. - ibhagwan/fzf-lua : another alternative to telescope for search and quick-switch functionality. +- Snacks.Pick : another alternative to telescope for search and quick-switch functionality. **Syntax highlighting:** @@ -468,7 +469,7 @@ carefully and customize it to your needs: open_app_foreground = false, picker = { - -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', or 'mini.pick'. + -- Set your preferred picker. Can be one of 'telescope.nvim', 'fzf-lua', 'mini.pick' or 'snacks.pick'. name = "telescope.nvim", -- Optional, configure key mappings for the picker. These are the defaults. -- Not all pickers support all mappings. diff --git a/lua/obsidian/commands/debug.lua b/lua/obsidian/commands/debug.lua index 3013bc38..e1d855a6 100644 --- a/lua/obsidian/commands/debug.lua +++ b/lua/obsidian/commands/debug.lua @@ -51,7 +51,7 @@ return function(client, data) end log.lazy_info "Dependencies:" - for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "telescope.nvim", "fzf-lua", "mini.pick" } do + for _, plugin in ipairs { "plenary.nvim", "nvim-cmp", "telescope.nvim", "fzf-lua", "mini.pick", "snacks.pick" } do local plugin_info = util.get_plugin_info(plugin) if plugin_info ~= nil then log.lazy_info(" ✓ %s: %s", plugin, plugin_info.commit or "unknown") From 1e1c912e5a3932f4e5ccbd300e8d61c84dc13f49 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Thu, 13 Feb 2025 16:33:34 -0500 Subject: [PATCH 3/5] refactor(pickers): improve snacks picker implementation and callback handling feat: add table utility functions for debugging fix: update grep and find_files to properly handle callbacks refactor: streamline picker options and configuration style: improve code organization and consistency Breaking changes: - Modified behavior of grep and find_files to prioritize callbacks --- lua/obsidian/pickers/_snacks.lua | 193 +++++++++++++++++++++---------- 1 file changed, 130 insertions(+), 63 deletions(-) diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index 97b749fd..b96eb25a 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -6,6 +6,45 @@ local Path = require "obsidian.path" local abc = require "obsidian.abc" local Picker = require "obsidian.pickers.picker" + +function print_table(t, indent) + indent = indent or 0 + local padding = string.rep(" ", indent) + + for key, value in pairs(t) do + if type(value) == "table" then + print(padding .. tostring(key) .. " = {") + print_table(value, indent + 1) + print(padding .. "}") + else + print(padding .. tostring(key) .. " = " .. tostring(value)) + end + end +end + +function table_to_string(t, indent) + if type(t) ~= "table" then return tostring(t) end + + indent = indent or 0 + local padding = string.rep(" ", indent) + local parts = {} + + for k, v in pairs(t) do + local key = type(k) == "number" and "[" .. k .. "]" or k + local value + if type(v) == "table" then + value = "{\n" .. table_to_string(v, indent + 1) .. padding .. "}" + elseif type(v) == "string" then + value = string.format("%q", v) + else + value = tostring(v) + end + parts[#parts + 1] = padding .. key .. " = " .. value + end + + return table.concat(parts, ",\n") .. "\n" +end + ---@param entry string ---@return string local function clean_path(entry) @@ -17,6 +56,24 @@ local function clean_path(entry) return "" end +local function map_actions(action) + if type(action) == "table" then + opts = { win = { input = { keys = {} } }, actions = {} }; + for k, v in pairs(action) do + local name = string.gsub(v.desc, " ", "_") + opts.win.input.keys = { + [k] = { name, mode = { "n", "i" }, desc = v.desc } + } + opts.actions[name] = function(picker, item) + vim.notify("action item: " .. table_to_string(item)) + v.callback({args: item.text}) + end + end + return opts + end + return {} +end + ---@class obsidian.pickers.SnacksPicker : obsidian.Picker local SnacksPicker = abc.new_class({ ---@diagnostic disable-next-line: unused-local @@ -26,93 +83,103 @@ local SnacksPicker = abc.new_class({ }, Picker) SnacksPicker.find_files = function(self, opts) - opts = opts and opts or {} + opts = opts or {} ---@type obsidian.Path local dir = opts.dir and Path:new(opts.dir) or self.client.dir - local result = snacks_picker.pick("files", { - cwd = tostring(dir), + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "files", + title = opts.prompt_title, + cwd = opts.dir.filename, + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + opts.callback(item._path) + else + snacks_picker.actions.jump(picker, item, action) + end + end + end, }) - - if result and opts.callback then - local path = clean_path(result) - opts.callback(tostring(dir / path)) - end + snacks_picker.pick(pick_opts) end -SnacksPicker.grep = function(self, opts) - opts = opts and opts or {} +SnacksPicker.grep = function(self, opts, action) + opts = opts or {} ---@type obsidian.Path local dir = opts.dir and Path:new(opts.dir) or self.client.dir - local result = snacks_picker.pick("grep", { - cwd = tostring(dir), + local pick_opts = vim.tbl_extend("force", map or {}, { + source = "grep", + title = opts.prompt_title, + cwd = opts.dir.filename, + confirm = function(picker, item, action) + picker:close() + if item then + if opts.callback then + opts.callback(item._path) + else + snacks_picker.actions.jump(picker, item, action) + end + end + end, }) - - if result and opts.callback then - local path = clean_path(result) - opts.callback(tostring(dir / path)) - end + snacks_picker.pick(pick_opts) end SnacksPicker.pick = function(self, values, opts) - - self.calling_bufnr = vim.api.nvim_get_current_buf() - - local buf = opts.buf or vim.api.nvim_get_current_buf() - - opts = opts and opts or {} - - local entries = {} - for _, value in ipairs(values) do - if type(value) == "string" then - table.insert(entries, { - text = value, - value = value, - }) - elseif value.valid ~= false then - local name = self:_make_display(value) - table.insert(entries, { + self.calling_bufnr = vim.api.nvim_get_current_buf() + + opts = opts or {} + + local buf = opts.buf or vim.api.nvim_get_current_buf() + + -- local map = vim.tbl_deep_extend("force", {}, + -- map_actions(opts.selection_mappings), + -- map_actions(opts.query_mappings)) + + local entries = {} + for _, value in ipairs(values) do + if type(value) == "string" then + table.insert(entries, { + text = value, + value = value, + }) + elseif value.valid ~= false then + local name = self:_make_display(value) + table.insert(entries, { text = name, buf = buf, filename = value.filename, value = value.value, pos = { value.lnum, value.col }, - }) + }) + end end - end - snacks_picker({ - tilte = opts.prompt_title, - items = entries, - layout = { + local pick_opts = vim.tbl_extend("force", map or {}, { + tilte = opts.prompt_title, + items = entries, + layout = { preview = false - }, - format = function(item, _) - local ret = {} - local a = snacks_picker.util.align - ret[#ret + 1] = { a(item.text, 20) } - return ret - end, - confirm = function(picker, item) - picker:close() - if item then - if opts.callback then - opts.callback(item.value) - elseif item then - vim.schedule(function() - if item["buf"] then - vim.api.nvim_set_current_buf(item["buf"]) + }, + format = "text", + confirm = function(picker, item) + picker:close() + if item and opts.callback then + if type(item) == "string" then + opts.callback(item) + else + opts.callback(item.value) end - vim.api.nvim_win_set_cursor(0, {item["pos"][1], 0}) - end) - end - end - end, - -- sort = require("snacks.picker.sort").idx(), - }) + end + end, + }) + + local entry = snacks_picker.pick(pick_opts) end return SnacksPicker From 62c546a6e866427e83c5bbbb9a41588d6a4f8788 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Fri, 14 Feb 2025 11:49:11 -0500 Subject: [PATCH 4/5] refactor(pickers): improve snacks picker implementation and error handling - Remove unused debug helper functions - Add proper type annotations for picker options - Improve action mapping logic with notes_mappings - Fix path handling in file and grep operations - Add debug logging for better troubleshooting - Enhance callback handling for picker actions --- lua/obsidian/pickers/_snacks.lua | 119 +++++++++++++------------------ 1 file changed, 48 insertions(+), 71 deletions(-) diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index b96eb25a..76bb1c43 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -1,72 +1,29 @@ local snacks_picker = require "snacks.picker" -local snacks = require "snacks" - local Path = require "obsidian.path" local abc = require "obsidian.abc" local Picker = require "obsidian.pickers.picker" - -function print_table(t, indent) - indent = indent or 0 - local padding = string.rep(" ", indent) - - for key, value in pairs(t) do - if type(value) == "table" then - print(padding .. tostring(key) .. " = {") - print_table(value, indent + 1) - print(padding .. "}") - else - print(padding .. tostring(key) .. " = " .. tostring(value)) - end - end -end - -function table_to_string(t, indent) - if type(t) ~= "table" then return tostring(t) end - - indent = indent or 0 - local padding = string.rep(" ", indent) - local parts = {} - - for k, v in pairs(t) do - local key = type(k) == "number" and "[" .. k .. "]" or k - local value - if type(v) == "table" then - value = "{\n" .. table_to_string(v, indent + 1) .. padding .. "}" - elseif type(v) == "string" then - value = string.format("%q", v) - else - value = tostring(v) - end - parts[#parts + 1] = padding .. key .. " = " .. value - end - - return table.concat(parts, ",\n") .. "\n" +local function debug_once(msg, ...) +-- vim.notify(msg .. vim.inspect(...)) end ----@param entry string ----@return string -local function clean_path(entry) - if type(entry) == "string" then - local path_end = assert(string.find(entry, ":", 1, true)) - return string.sub(entry, 1, path_end - 1) - end - vim.notify("entry: " .. table.concat(vim.tbl_keys(entry), ", ")) - return "" -end - -local function map_actions(action) - if type(action) == "table" then +---@param mapping table +---@return table +local function notes_mappings(mapping) + if type(mapping) == "table" then opts = { win = { input = { keys = {} } }, actions = {} }; - for k, v in pairs(action) do + for k, v in pairs(mapping) do local name = string.gsub(v.desc, " ", "_") opts.win.input.keys = { [k] = { name, mode = { "n", "i" }, desc = v.desc } } opts.actions[name] = function(picker, item) - vim.notify("action item: " .. table_to_string(item)) - v.callback({args: item.text}) + debug_once("mappings :", item) + picker:close() + vim.schedule(function() + v.callback(item.value or item._path) + end) end end return opts @@ -82,46 +39,60 @@ local SnacksPicker = abc.new_class({ end, }, Picker) +---@param opts obsidian.PickerFindOpts|? Options. SnacksPicker.find_files = function(self, opts) opts = opts or {} ---@type obsidian.Path - local dir = opts.dir and Path:new(opts.dir) or self.client.dir + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir + + local map = vim.tbl_deep_extend("force", {}, + notes_mappings(opts.selection_mappings)) local pick_opts = vim.tbl_extend("force", map or {}, { source = "files", title = opts.prompt_title, - cwd = opts.dir.filename, + cwd = tostring(dir), confirm = function(picker, item, action) picker:close() if item then if opts.callback then + debug_once("find files callback: ", item) opts.callback(item._path) else + debug_once("find files jump: ", item) snacks_picker.actions.jump(picker, item, action) end end end, }) - snacks_picker.pick(pick_opts) + local t = snacks_picker.pick(pick_opts) end -SnacksPicker.grep = function(self, opts, action) +---@param opts obsidian.PickerGrepOpts|? Options. +SnacksPicker.grep = function(self, opts) opts = opts or {} + debug_once("grep opts : ", opts) + ---@type obsidian.Path local dir = opts.dir and Path:new(opts.dir) or self.client.dir + local map = vim.tbl_deep_extend("force", {}, + notes_mappings(opts.selection_mappings)) + local pick_opts = vim.tbl_extend("force", map or {}, { source = "grep", title = opts.prompt_title, - cwd = opts.dir.filename, + cwd = dir, confirm = function(picker, item, action) picker:close() if item then if opts.callback then - opts.callback(item._path) + debug_once("grep callback: ", item) + opts.callback(item._path or item.filename) else + debug_once("grep jump: ", item) snacks_picker.actions.jump(picker, item, action) end end @@ -130,16 +101,17 @@ SnacksPicker.grep = function(self, opts, action) snacks_picker.pick(pick_opts) end +---@param values string[]|obsidian.PickerEntry[] +---@param opts obsidian.PickerPickOpts|? Options. +---@diagnostic disable-next-line: unused-local SnacksPicker.pick = function(self, values, opts) self.calling_bufnr = vim.api.nvim_get_current_buf() opts = opts or {} - local buf = opts.buf or vim.api.nvim_get_current_buf() + debug_once("pick opts: ", opts) - -- local map = vim.tbl_deep_extend("force", {}, - -- map_actions(opts.selection_mappings), - -- map_actions(opts.query_mappings)) + local buf = opts.buf or vim.api.nvim_get_current_buf() local entries = {} for _, value in ipairs(values) do @@ -155,11 +127,14 @@ SnacksPicker.pick = function(self, values, opts) buf = buf, filename = value.filename, value = value.value, - pos = { value.lnum, value.col }, + pos = { value.lnum, value.col or 0 }, }) end end + local map = vim.tbl_deep_extend("force", {}, + notes_mappings(opts.selection_mappings)) + local pick_opts = vim.tbl_extend("force", map or {}, { tilte = opts.prompt_title, items = entries, @@ -167,13 +142,15 @@ SnacksPicker.pick = function(self, values, opts) preview = false }, format = "text", - confirm = function(picker, item) + confirm = function(picker, item, action) picker:close() - if item and opts.callback then - if type(item) == "string" then - opts.callback(item) - else + if item then + if opts.callback then + debug_once("pick callback: ", item) opts.callback(item.value) + else + debug_once("pick jump: ", item) + snacks_picker.actions.jump(picker, item, action) end end end, From f7ba7ae51184e3cf83be00364bd5e6cae01ca919 Mon Sep 17 00:00:00 2001 From: Steve Beaulac Date: Fri, 14 Feb 2025 12:38:35 -0500 Subject: [PATCH 5/5] fix: correct Path handling in grep picker - access dir.filename property instead of dir directly - convert Path object to string for cwd option --- lua/obsidian/pickers/_snacks.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/obsidian/pickers/_snacks.lua b/lua/obsidian/pickers/_snacks.lua index 76bb1c43..4bf632bf 100644 --- a/lua/obsidian/pickers/_snacks.lua +++ b/lua/obsidian/pickers/_snacks.lua @@ -76,7 +76,7 @@ SnacksPicker.grep = function(self, opts) debug_once("grep opts : ", opts) ---@type obsidian.Path - local dir = opts.dir and Path:new(opts.dir) or self.client.dir + local dir = opts.dir.filename and Path:new(opts.dir.filename) or self.client.dir local map = vim.tbl_deep_extend("force", {}, notes_mappings(opts.selection_mappings)) @@ -84,7 +84,7 @@ SnacksPicker.grep = function(self, opts) local pick_opts = vim.tbl_extend("force", map or {}, { source = "grep", title = opts.prompt_title, - cwd = dir, + cwd = tostring(dir), confirm = function(picker, item, action) picker:close() if item then