Skip to content

Commit ff99405

Browse files
madmaxieeeMax Chuang
authored andcommitted
feat: add snacks picker frontend
1 parent 550a905 commit ff99405

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

lua/fff/snacks.lua

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
local M = {}
2+
3+
local conf = require('fff.conf')
4+
local file_picker = require('fff.file_picker')
5+
6+
---@class FFFSnacksState
7+
---@field current_file_cache? string
8+
---@field config table FFF config
9+
M.state = { config = {} }
10+
11+
local staged_status = {
12+
staged_new = true,
13+
staged_modified = true,
14+
staged_deleted = true,
15+
renamed = true,
16+
}
17+
18+
local status_map = {
19+
untracked = 'untracked',
20+
modified = 'modified',
21+
deleted = 'deleted',
22+
renamed = 'renamed',
23+
staged_new = 'added',
24+
staged_modified = 'modified',
25+
staged_deleted = 'deleted',
26+
ignored = 'ignored',
27+
-- clean = "",
28+
-- clear = "",
29+
unknown = 'untracked',
30+
}
31+
32+
--- tweaked version of `Snacks.picker.format.file_git_status`
33+
--- @type snacks.picker.format
34+
local function format_file_git_status(item, picker)
35+
local ret = {} ---@type snacks.picker.Highlight[]
36+
local status = item.status
37+
38+
local hl = 'SnacksPickerGitStatus'
39+
if status.unmerged then
40+
hl = 'SnacksPickerGitStatusUnmerged'
41+
elseif status.staged then
42+
hl = 'SnacksPickerGitStatusStaged'
43+
else
44+
hl = 'SnacksPickerGitStatus' .. status.status:sub(1, 1):upper() .. status.status:sub(2)
45+
end
46+
47+
local icon = picker.opts.icons.git[status.status]
48+
if status.staged then icon = picker.opts.icons.git.staged end
49+
50+
local text_icon = status.status:sub(1, 1):upper()
51+
text_icon = status.status == 'untracked' and '?' or status.status == 'ignored' and '!' or text_icon
52+
53+
ret[#ret + 1] = { icon, hl }
54+
ret[#ret + 1] = { ' ', virtual = true }
55+
56+
ret[#ret + 1] = {
57+
col = 0,
58+
virt_text = { { text_icon, hl }, { ' ' } },
59+
virt_text_pos = 'right_align',
60+
hl_mode = 'combine',
61+
}
62+
return ret
63+
end
64+
65+
---@type snacks.picker.Config
66+
M.source = {
67+
title = 'FFFiles',
68+
finder = function(opts, ctx)
69+
-- initialization code from require('fff.picker_ui').open
70+
-- on_show does not seem to be called before finder
71+
if not M.state.current_file_cache then
72+
local current_buf = vim.api.nvim_get_current_buf()
73+
if current_buf and vim.api.nvim_buf_is_valid(current_buf) then
74+
local current_file = vim.api.nvim_buf_get_name(current_buf)
75+
if current_file ~= '' and vim.fn.filereadable(current_file) == 1 then
76+
M.state.current_file_cache = current_file
77+
else
78+
M.state.current_file_cache = nil
79+
end
80+
end
81+
end
82+
if not file_picker.is_initialized() then
83+
if not file_picker.setup() then
84+
vim.notify('Failed to initialize file picker', vim.log.levels.ERROR)
85+
return {}
86+
end
87+
end
88+
local config = conf.get()
89+
M.state.config = vim.tbl_deep_extend('force', config or {}, opts or {})
90+
91+
local fff_result = file_picker.search_files(
92+
ctx.filter.search,
93+
opts.limit or M.state.config.max_results,
94+
M.state.config.max_threads,
95+
M.state.current_file_cache,
96+
false
97+
)
98+
99+
---@type snacks.picker.finder.Item[]
100+
local items = {}
101+
for _, fff_item in ipairs(fff_result) do
102+
---@type snacks.picker.finder.Item
103+
local item = {
104+
text = fff_item.name,
105+
file = fff_item.path,
106+
score = fff_item.total_frecency_score,
107+
-- HACK: in original snacks implementation status is a string of
108+
-- `git status --porcelain` output
109+
status = status_map[fff_item.git_status] and {
110+
status = status_map[fff_item.git_status],
111+
staged = staged_status[fff_item.git_status] or false,
112+
unmerged = fff_item.git_status == 'unmerged',
113+
},
114+
}
115+
items[#items + 1] = item
116+
end
117+
118+
return items
119+
end,
120+
format = function(item, picker)
121+
---@type snacks.picker.Highlight[]
122+
local ret = {}
123+
124+
if item.label then
125+
ret[#ret + 1] = { item.label, 'SnacksPickerLabel' }
126+
ret[#ret + 1] = { ' ', virtual = true }
127+
end
128+
129+
if item.status then
130+
vim.list_extend(ret, format_file_git_status(item, picker))
131+
else
132+
ret[#ret + 1] = { ' ', virtual = true }
133+
end
134+
135+
vim.list_extend(ret, require('snacks').picker.format.filename(item, picker))
136+
137+
if item.line then
138+
require('snacks').picker.highlight.format(item, item.line, ret)
139+
table.insert(ret, { ' ' })
140+
end
141+
return ret
142+
end,
143+
on_close = function() M.state.current_file_cache = nil end,
144+
formatters = {
145+
file = {
146+
filename_first = true,
147+
},
148+
},
149+
live = true,
150+
}
151+
152+
return M

plugin/fff.lua

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ local init = vim.schedule_wrap(function()
99
-- PERF: We query the vim.g.fff config to avoid eagerly requiring Lua modules
1010
local lazy_sync = vim.tbl_get(vim.g, 'fff', 'lazy_sync')
1111
if lazy_sync == nil or not lazy_sync then require('fff.core').ensure_initialized() end
12+
if Snacks and pcall(require, 'snacks.picker') then
13+
-- Users can call Snacks.picker.fff() after this
14+
Snacks.picker.sources.fff = require('fff.snacks').source
15+
end
1216
end)
1317

1418
if vim.v.vim_did_enter == 1 then
@@ -102,3 +106,13 @@ vim.api.nvim_create_user_command('FFFOpenLog', function()
102106
end, {
103107
desc = 'Open FFF log file in new tab',
104108
})
109+
110+
vim.api.nvim_create_user_command('FFFSnacks', function()
111+
if Snacks and pcall(require, 'snacks.picker') then
112+
Snacks.picker(require('fff.snacks').source)
113+
else
114+
vim.notify('Snacks is not loaded', vim.log.levels.ERROR)
115+
end
116+
end, {
117+
desc = 'Open FFF in snacks picker',
118+
})

0 commit comments

Comments
 (0)