feat: add snacks picker frontend#147
Conversation
|
@dmtrKovalenko this would be awesome! |
8690a3c to
1834d05
Compare
|
any plans to merge this would be amazing |
|
should happen in a week or two, you can use my fork if you want to try it now. |
|
@dmtrKovalenko are you planning to merge this? |
|
I'm using your fork as my plugin 😆 |
|
@madmaxieee it looks like latest master doesn't work with your plugin. Took your files from this PR, put to my local nvim config + pull latest master of dmtrKovalenko/fff.nvim. I did it like this: function start()
local M = {}
local conf = require('fff.conf')
local file_picker = require('fff.file_picker')
---@class FFFSnacksState
---@field current_file_cache? string
---@field config table FFF config
M.state = { config = {} }
local staged_status = {
staged_new = true,
staged_modified = true,
staged_deleted = true,
renamed = true,
}
local status_map = {
untracked = 'untracked',
modified = 'modified',
deleted = 'deleted',
renamed = 'renamed',
staged_new = 'added',
staged_modified = 'modified',
staged_deleted = 'deleted',
ignored = 'ignored',
-- clean = "",
-- clear = "",
unknown = 'untracked',
}
--- tweaked version of `Snacks.picker.format.file_git_status`
--- @type snacks.picker.format
local function format_file_git_status(item, picker)
local ret = {} ---@type snacks.picker.Highlight[]
local status = item.status
local hl = 'SnacksPickerGitStatus'
if status.unmerged then
hl = 'SnacksPickerGitStatusUnmerged'
elseif status.staged then
hl = 'SnacksPickerGitStatusStaged'
else
hl = 'SnacksPickerGitStatus' .. status.status:sub(1, 1):upper() .. status.status:sub(2)
end
local icon = picker.opts.icons.git[status.status]
if status.staged then icon = picker.opts.icons.git.staged end
local text_icon = status.status:sub(1, 1):upper()
text_icon = status.status == 'untracked' and '?' or status.status == 'ignored' and '!' or text_icon
ret[#ret + 1] = { icon, hl }
ret[#ret + 1] = { ' ', virtual = true }
ret[#ret + 1] = {
col = 0,
virt_text = { { text_icon, hl }, { ' ' } },
virt_text_pos = 'right_align',
hl_mode = 'combine',
}
return ret
end
---@type snacks.picker.Config
M.source = {
title = 'FFFiles',
finder = function(opts, ctx)
-- initialization code from require('fff.picker_ui').open
-- on_show does not seem to be called before finder
if not M.state.current_file_cache then
local current_buf = vim.api.nvim_get_current_buf()
if current_buf and vim.api.nvim_buf_is_valid(current_buf) then
local current_file = vim.api.nvim_buf_get_name(current_buf)
if current_file ~= '' and vim.fn.filereadable(current_file) == 1 then
M.state.current_file_cache = current_file
else
M.state.current_file_cache = nil
end
end
end
if not file_picker.is_initialized() then
if not file_picker.setup() then
vim.notify('Failed to initialize file picker', vim.log.levels.ERROR)
return {}
end
end
local config = conf.get()
M.state.config = vim.tbl_deep_extend('force', config or {}, opts or {})
local fff_result = file_picker.search_files(
ctx.filter.search,
opts.limit or M.state.config.max_results,
M.state.config.max_threads,
M.state.current_file_cache,
false
)
---@type snacks.picker.finder.Item[]
local items = {}
for _, fff_item in ipairs(fff_result) do
---@type snacks.picker.finder.Item
local item = {
text = fff_item.name,
file = fff_item.path,
score = fff_item.total_frecency_score,
-- HACK: in original snacks implementation status is a string of
-- `git status --porcelain` output
status = status_map[fff_item.git_status] and {
status = status_map[fff_item.git_status],
staged = staged_status[fff_item.git_status] or false,
unmerged = fff_item.git_status == 'unmerged',
},
}
items[#items + 1] = item
end
return items
end,
format = function(item, picker)
---@type snacks.picker.Highlight[]
local ret = {}
if item.label then
ret[#ret + 1] = { item.label, 'SnacksPickerLabel' }
ret[#ret + 1] = { ' ', virtual = true }
end
if item.status then
vim.list_extend(ret, format_file_git_status(item, picker))
else
ret[#ret + 1] = { ' ', virtual = true }
end
vim.list_extend(ret, require('snacks').picker.format.filename(item, picker))
if item.line then
require('snacks').picker.highlight.format(item, item.line, ret)
table.insert(ret, { ' ' })
end
return ret
end,
on_close = function() M.state.current_file_cache = nil end,
formatters = {
file = {
filename_first = true,
},
},
live = true,
}
return M
end
return {
'dmtrKovalenko/fff.nvim',
build = function()
-- this will download prebuild binary or try to use existing rustup toolchain to build from source
-- (if you are using lazy you can use gb for rebuilding a plugin if needed)
require("fff.download").download_or_build_binary()
end,
dependencies = { 'folke/snacks.nvim' },
-- if you are using nixos
-- build = "nix run .#release",
opts = { -- (optional)
debug = {
enabled = true, -- we expect your collaboration at least during the beta
show_scores = true, -- to help us optimize the scoring system, feel free to share your scores!
},
},
config = function(_, opts)
require('fff').setup(opts)
start()
end,
-- No need to lazy-load with lazy.nvim.
-- This plugin initializes itself lazily.
lazy = false,
keys = {
{
"ff", -- try it if you didn't it is a banger keybinding for a picker
function() require('fff').find_files() end,
desc = 'FFFind files',
}
}
}
And I get this error: And it's a bit weird, because the error is connected with The error itself somehow connected with this line. If I comment it, I don't see any error. local file_picker = require('fff.file_picker') |
|
I think I actually found a solution. Maybe some weird race condition happen, but this way, with lazy calling function start()
local M = {}
local conf = require('fff.conf')
local file_picker = require('fff.file_picker')
-- local file_picker = nil
-- local conf = nil
---@class FFFSnacksState
---@field current_file_cache? string
---@field config table FFF config
M.state = { config = {} }
local staged_status = {
staged_new = true,
staged_modified = true,
staged_deleted = true,
renamed = true,
}
local status_map = {
untracked = 'untracked',
modified = 'modified',
deleted = 'deleted',
renamed = 'renamed',
staged_new = 'added',
staged_modified = 'modified',
staged_deleted = 'deleted',
ignored = 'ignored',
-- clean = "",
-- clear = "",
unknown = 'untracked',
}
--- tweaked version of `Snacks.picker.format.file_git_status`
--- @type snacks.picker.format
local function format_file_git_status(item, picker)
local ret = {} ---@type snacks.picker.Highlight[]
local status = item.status
local hl = 'SnacksPickerGitStatus'
if status.unmerged then
hl = 'SnacksPickerGitStatusUnmerged'
elseif status.staged then
hl = 'SnacksPickerGitStatusStaged'
else
hl = 'SnacksPickerGitStatus' .. status.status:sub(1, 1):upper() .. status.status:sub(2)
end
local icon = picker.opts.icons.git[status.status]
if status.staged then icon = picker.opts.icons.git.staged end
local text_icon = status.status:sub(1, 1):upper()
text_icon = status.status == 'untracked' and '?' or status.status == 'ignored' and '!' or text_icon
ret[#ret + 1] = { icon, hl }
ret[#ret + 1] = { ' ', virtual = true }
ret[#ret + 1] = {
col = 0,
virt_text = { { text_icon, hl }, { ' ' } },
virt_text_pos = 'right_align',
hl_mode = 'combine',
}
return ret
end
---@type snacks.picker.Config
M.source = {
title = 'FFFiles',
finder = function(opts, ctx)
-- initialization code from require('fff.picker_ui').open
-- on_show does not seem to be called before finder
if not M.state.current_file_cache then
local current_buf = vim.api.nvim_get_current_buf()
if current_buf and vim.api.nvim_buf_is_valid(current_buf) then
local current_file = vim.api.nvim_buf_get_name(current_buf)
if current_file ~= '' and vim.fn.filereadable(current_file) == 1 then
M.state.current_file_cache = current_file
else
M.state.current_file_cache = nil
end
end
end
if not file_picker.is_initialized() then
if not file_picker.setup() then
vim.notify('Failed to initialize file picker', vim.log.levels.ERROR)
return {}
end
end
local config = conf.get()
M.state.config = vim.tbl_deep_extend('force', config or {}, opts or {})
local fff_result = file_picker.search_files(
ctx.filter.search,
opts.limit or M.state.config.max_results,
M.state.config.max_threads,
M.state.current_file_cache,
false
)
---@type snacks.picker.finder.Item[]
local items = {}
for _, fff_item in ipairs(fff_result) do
---@type snacks.picker.finder.Item
local item = {
text = fff_item.name,
file = fff_item.path,
score = fff_item.total_frecency_score,
-- HACK: in original snacks implementation status is a string of
-- `git status --porcelain` output
status = status_map[fff_item.git_status] and {
status = status_map[fff_item.git_status],
staged = staged_status[fff_item.git_status] or false,
unmerged = fff_item.git_status == 'unmerged',
},
}
items[#items + 1] = item
end
return items
end,
format = function(item, picker)
---@type snacks.picker.Highlight[]
local ret = {}
if item.label then
ret[#ret + 1] = { item.label, 'SnacksPickerLabel' }
ret[#ret + 1] = { ' ', virtual = true }
end
if item.status then
vim.list_extend(ret, format_file_git_status(item, picker))
else
ret[#ret + 1] = { ' ', virtual = true }
end
vim.list_extend(ret, require('snacks').picker.format.filename(item, picker))
if item.line then
require('snacks').picker.highlight.format(item, item.line, ret)
table.insert(ret, { ' ' })
end
return ret
end,
on_close = function() M.state.current_file_cache = nil end,
formatters = {
file = {
filename_first = true,
},
},
live = true,
}
return M
end
return {
'dmtrKovalenko/fff.nvim',
build = function()
-- this will download prebuild binary or try to use existing rustup toolchain to build from source
-- (if you are using lazy you can use gb for rebuilding a plugin if needed)
require("fff.download").download_or_build_binary()
end,
dependencies = { 'folke/snacks.nvim' },
-- if you are using nixos
-- build = "nix run .#release",
opts = { -- (optional)
debug = {
enabled = true, -- we expect your collaboration at least during the beta
show_scores = true, -- to help us optimize the scoring system, feel free to share your scores!
},
},
config = function(_, opts)
require('fff').setup(opts)
-- start() lazy load
vim.api.nvim_create_user_command('FFFSnacks', function()
if Snacks and pcall(require, 'snacks.picker') then
Snacks.picker(start().source)
else
vim.notify('Snacks is not loaded', vim.log.levels.ERROR)
end
end, {
desc = 'Open FFF in snacks picker',
})
end,
-- No need to lazy-load with lazy.nvim.
-- This plugin initializes itself lazily.
lazy = false,
keys = {
{
"ff", -- try it if you didn't it is a banger keybinding for a picker
function() require('fff').find_files() end,
desc = 'FFFind files',
}
}
}
|
|
fff is lazy loaded by default. Calling the setup function only puts your config into |
I thought I was the only one hitting this issue, so I patched it myself (it started after the patch that allowed downloading the binary directly). Both blink.cmp and fff.nvim pollute the Since I didn’t want to sacrifice lazy loading, I just hardcoded the local lib_path = '/home/<your_username>/.local/share/nvim/lazy/blink.cmp/target/release/libblink_cmp_fuzzy.so'
local lib, err = package.loadlib(lib_path, 'luaopen_blink_cmp_fuzzy')
if not lib then
error('Failed to load libblink_cmp_fuzzy: ' .. err)
end
return lib()I also ran into a git issue (it stopped detecting git changes). In let repo_path = repo.path().parent()?;to: let repo_path = repo.workdir();This somehow seemed to fix the problem. Hope this helps someone else |
There was a problem hiding this comment.
Hey, 👋🏼
Thanks for putting this together. The integration works well, but after thinking through the implications, I don't think this belongs in fff.nvim core. Let me explain why.
The maintenance trap: If we merge this, we're now responsible for tracking Snacks API changes.
Slippery slope: Once we accept one picker integration, what do we say when someone opens a Telescope PR? Or fzf-lua? Or the next popular picker framework?
Better approach: This should live as fff-snacks.nvim - a separate bridge plugin. Here's why that's actually better for everyone:
- Maintenance lives where it should: People who care about this integration maintain it
- Version pinning: You can specify compatible versions of both plugins
- Faster iteration: No need to wait for fff.nvim releases
- fff.nvim stays focused: One job, do it well
Here's how FFF can help: A public API in FFF to expose some of the functionality would be essential for bridge plugins:
fff.api.search(query, opts)- get resultsfff.api.format(item)- get text + highlightsfff.api.status(item)- get git status info
This would make your bridge plugin way simpler - probably ~30 lines instead of 166. Interested in collaborating on that?
|
@mrjared16 Thanks for the clarification, I never noticed this since I don't use prebuilt binary before. So that is a simpler fix (kinda?) for you. |
|
@sQVe I totally agree with these arguments. Honestly I was planning to create a standalone plugin in the first place. link: https://github.com/madmaxieee/fff-snacks.nvim. The API is really cool, I would love to see that! Thanks for your great work. |
|
@madmaxieee that's cool, thanks for the package! |
Adds a snacks picker frontend for fff.nvim. From this reddit post.