-
Notifications
You must be signed in to change notification settings - Fork 54
feat: add snacks picker frontend #147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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! |
Current way of configuring cpath use the full path of the downloaded binary without any '?', this causes any require statement that can't resolve before this cpath component to always resolve to the downloaded libfff_nvim, thus causing some unexpected bug in some other plugins or user code.
Adds a snacks picker frontend for fff.nvim. From this reddit post.