Skip to content

Commit ebde1a3

Browse files
committed
feat: store.nvim picker
1 parent 995a3af commit ebde1a3

3 files changed

Lines changed: 297 additions & 0 deletions

File tree

lua/fzf-lua-extra/previewers.lua

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,78 @@ end
145145
---@diagnostic disable-next-line: unused
146146
function M.gitignore:key_from_entry(entry) return entry.key end
147147

148+
---@class fle.previewer.Store: fzf-lua.previewer.BufferOrFile
149+
---@field super fzf-lua.previewer.BufferOrFile
150+
---@field store_items table<number, store.Repository>
151+
M.store = previewer.buffer_or_file:extend()
152+
153+
function M.store:new(o, opts)
154+
M.store.super.new(self, o, opts)
155+
self.items = opts.previewer.items
156+
return self
157+
end
158+
159+
---@diagnostic disable-next-line: unused
160+
---@param entry_str string
161+
---@param cb function
162+
function M.store:parse_entry(entry_str, cb)
163+
local name = entry_str:match('[^%s]+')
164+
local err = {
165+
content = { 'Failed to find repository information for: ' .. name },
166+
filetype = 'markdown',
167+
}
168+
if not name then return cb(err) end
169+
local repo = self.items[name]
170+
if not repo then return cb(err) end
171+
local lines = {}
172+
173+
-- Define custom FzfLua highlight groups by linking them to standard Neovim groups.
174+
-- This ensures they automatically adapt to your Neovim colorscheme.
175+
api.nvim_set_hl(0, 'FleBlue', { link = 'Title' }) -- Typically used for important names or titles
176+
api.nvim_set_hl(0, 'FleYellow', { link = 'Comment' }) -- Often used for descriptions or less prominent text
177+
api.nvim_set_hl(0, 'FleMagenta', { link = 'Special' }) -- Suitable for symbols, special characters, or numeric indicators
178+
api.nvim_set_hl(0, 'FleGreen', { link = 'String' }) -- Good for lists of items like tags
179+
180+
-- Add meta info to the top
181+
lines[#lines + 1] = { '# ', { repo.full_name, 'FleBlue' } }
182+
lines[#lines + 1] = ''
183+
if repo.description and repo.description ~= '' then
184+
lines[#lines + 1] = { ' ', { repo.description, 'FleYellow' } }
185+
lines[#lines + 1] = ''
186+
end
187+
lines[#lines + 1] = {
188+
{ '' .. repo.stars, 'FleMagenta' },
189+
' ',
190+
{ '🚨' .. repo.issues, 'FleMagenta' },
191+
}
192+
lines[#lines + 1] = 'Updated: '
193+
.. repo.pretty.updated_at
194+
.. ' | Created: '
195+
.. repo.pretty.created_at
196+
if #repo.tags > 0 then
197+
lines[#lines + 1] = { 'Tags: ', { table.concat(repo.tags, ', '), 'FleGreen' } }
198+
end
199+
lines[#lines + 1] = '' -- Empty line for separation
200+
-- Fetch README if available
201+
utils.arun(function()
202+
local res = utils.gh({ endpoint = fs.joinpath('repos', name, 'readme') })
203+
local content = res.content or ''
204+
content = (content:gsub('[\n\r]', ''))
205+
if res.encoding == 'base64' then -- TODO: error now never bubble up in vim._async..
206+
content = vim.F.npcall(vim.base64.decode, content)
207+
else
208+
error('unimplemented encoding: ' .. res.encoding)
209+
end
210+
if content ~= '' then
211+
lines[#lines + 1] = '---' -- Separator
212+
lines[#lines + 1] = '# README'
213+
vim.list_extend(lines, vim.split(content, '[\r?\n]'))
214+
end
215+
cb({ name = name, filetype = 'markdown', content = lines })
216+
end)
217+
end
218+
219+
---@diagnostic disable-next-line: unused
220+
function M.store:key_from_entry(entry) return entry.name end
221+
148222
return M
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
-- tbh lazy load is not necessary now, just use alias here
2+
local utils = require('fzf-lua-extra.utils')
3+
4+
---@param cb fun(plugin: table, o: table)
5+
local p_do = function(cb)
6+
return function(selected, opts)
7+
vim.iter(selected):each(function(sel)
8+
sel = sel:match('[^%s]+')
9+
local bs_parts = vim.split(sel, '/')
10+
local name = bs_parts[#bs_parts]
11+
local plugin = utils.get_lazy_plugins()[name] or opts.previewer.items[sel]
12+
if plugin then cb(plugin, opts) end
13+
end)
14+
end
15+
end
16+
17+
-- https://github.com/folke/lazy.nvim/blob/c6a57a3534d3494bcc5ff9b0586e141bdb0280eb/lua/lazy/core/util.lua#L68
18+
---@param name string
19+
---@return string
20+
local normname = function(name)
21+
return (
22+
name:lower():gsub('^n?vim%-', ''):gsub('%.n?vim$', ''):gsub('[%.%-]lua', ''):gsub('[^a-z]+', '')
23+
)
24+
end
25+
26+
-- https://github.com/alex-popov-tech/store.nvim/blob/e3aea13c354de465ca3a879158a1752e0c9c13ea/lua/store/actions.lua#L293
27+
local write_conf = function(data)
28+
local repo = data.repo
29+
local filepath = vim.fn.expand(data.filepath)
30+
vim.fn.mkdir(vim.fn.fnamemodify(filepath, ':h'), 'p')
31+
local exist = vim.uv.fs_stat(filepath)
32+
local file = io.open(filepath, exist and 'a' or 'w')
33+
if not file then return vim.notify('Failed to open file: ' .. filepath) end
34+
if exist then file:write('\n\n') end
35+
file:write(data.config)
36+
file:flush()
37+
file:close()
38+
vim.notify(repo.full_name .. ' in: ' .. filepath)
39+
end
40+
41+
---Format repository information for display in a single line for the picker
42+
---@param repo store.Repository The repository to format
43+
---@param compact? boolean Whether to use compact formatting
44+
---@return string formatted_line
45+
local function format_repository_info(repo, compact)
46+
local fu = FzfLua.utils
47+
local cyan = fu.ansi_codes.cyan
48+
local yellow = fu.ansi_codes.yellow
49+
local magenta = fu.ansi_codes.magenta
50+
local green = fu.ansi_codes.green
51+
52+
local parts = {}
53+
parts[#parts + 1] = repo.full_name
54+
parts[#parts + 1] = '\t'
55+
-- Format stars: Truncate if longer than 8 bytes, then left-align to 8 bytes.
56+
local stars_str = '' .. repo.stars
57+
local display_stars = stars_str
58+
if #display_stars > 8 then display_stars = string.sub(display_stars, 1, 8) end
59+
parts[#parts + 1] = magenta(string.format('%-8s', display_stars))
60+
61+
-- Format issues: Truncate if longer than 8 bytes, then left-align to 8 bytes.
62+
local issues_str = '🚨' .. repo.issues
63+
local display_issues = issues_str
64+
if #display_issues > 8 then display_issues = string.sub(display_issues, 1, 8) end
65+
parts[#parts + 1] = magenta(string.format('%-8s', display_issues))
66+
67+
parts[#parts + 1] = cyan(repo.full_name)
68+
if (not compact or #repo.tags == 0) and repo.description and repo.description ~= '' then
69+
parts[#parts + 1] = yellow(repo.description)
70+
end
71+
if #repo.tags > 0 then parts[#parts + 1] = green('[' .. table.concat(repo.tags, ',') .. ']') end
72+
return table.concat(parts, ' ')
73+
end
74+
75+
local State = {}
76+
State.state = {
77+
all = function()
78+
State.encode = function(p) return format_repository_info(p) end
79+
end,
80+
compat = function()
81+
State.encode = function(p) return format_repository_info(p, true) end
82+
end,
83+
}
84+
State.cycle = function()
85+
State.key = next(State.state, State.key)
86+
if not State.key then State.key = next(State.state, State.key) end
87+
State.state[State.key]()
88+
end
89+
90+
---@return function, function
91+
State.get = function() return State.filter, State.encode end
92+
State.cycle()
93+
94+
---@class fle.config.Store: fzf-lua.config.Base
95+
local __DEFAULT__ = {
96+
-- https://github.com/alex-popov-tech/store.nvim/blob/43e574b5aac28891fe50316fc69727cfc27727a4/lua/store/config.lua#L186
97+
urls = {
98+
store = 'https://gist.githubusercontent.com/alex-popov-tech/92d1366bfeb168d767153a24be1475b5/raw/db.json', -- URL for plugin data
99+
['lazy.nvim'] = 'https://gist.githubusercontent.com/alex-popov-tech/6629a59e7910aa08b1aa5cdc0519b8b4/raw/lazy.nvim.json',
100+
['vim.pack'] = 'https://gist.githubusercontent.com/alex-popov-tech/18a46177d6473e12bc2c854e2548f127/raw/vim.pack.json',
101+
},
102+
---@param path string
103+
---@return boolean
104+
cache_invalid = function(path)
105+
local stat = vim.uv.fs_stat(path)
106+
return not stat or (os.time() - stat.ctime.sec) > 2 * 24 * 60 * 60
107+
end,
108+
previewer = {
109+
_ctor = function() return require('fzf-lua-extra.previewers').store end,
110+
items = {},
111+
},
112+
fzf_opts = {
113+
['--delimiter'] = '\t',
114+
['--with-nth'] = '2..',
115+
['--no-hscroll'] = true,
116+
},
117+
actions = {
118+
['enter'] = p_do(function(p, o)
119+
if p.dir and vim.uv.fs_stat(p.dir) then
120+
utils.chdir(p.dir)
121+
elseif p.full_name then
122+
local cmd = { 'curl', '-sL', o.urls['lazy.nvim'] }
123+
local opts = { cache_path = utils.path('store-lazy.json'), cache_invalid = o.cache_invalid }
124+
utils.arun(function()
125+
local plugins_folder = require('store.utils').get_plugins_folder()
126+
local filepath = plugins_folder .. '/' .. (normname(p.name) .. '.lua')
127+
local res = utils.run(cmd, opts).stdout or ''
128+
local items = vim.json.decode(res).items
129+
write_conf({ config = items[p.full_name], filepath = filepath, repo = p })
130+
end)
131+
end
132+
end),
133+
['ctrl-y'] = p_do(function(p) vim.fn.setreg('+', p.url) end),
134+
['ctrl-o'] = p_do(function(p) -- search cleaned plugins
135+
vim.ui.open(p.url or ('https://github.com/search?q=%s'):format(p.name))
136+
end),
137+
['ctrl-l'] = p_do(function(p)
138+
if p.dir and vim.uv.fs_stat(p.dir) then FzfLua.files { cwd = p.dir } end
139+
end),
140+
['ctrl-n'] = p_do(function(p)
141+
if p.dir then FzfLua.live_grep_native { cwd = p.dir } end
142+
end),
143+
['ctrl-r'] = p_do(
144+
function(p) require('lazy.core.loader')[p._ and p._.loaded and 'reload' or 'load'](p) end
145+
),
146+
['ctrl-g'] = { fn = State.cycle, reload = true },
147+
},
148+
}
149+
150+
-- https://github.com/alex-popov-tech/store.nvim/blob/0dad6788ac69531f37f7b65939c6ee22ac812757/lua/store/types.lua#L1
151+
---@class store.Repository
152+
---@field source string Repository source (e.g., "github")
153+
---@field author string Repository author/owner
154+
---@field name string Repository name
155+
---@field full_name string Repository full name (author/name)
156+
---@field url string Repository URL
157+
---@field description string Repository description
158+
---@field tags string[] Array of topic tags
159+
---@field stars number Number of stars
160+
---@field issues number Number of open issues
161+
---@field created_at string Creation timestamp (ISO format)
162+
---@field updated_at string Last update timestamp (ISO format)
163+
---@field pretty {stars: string, issues: string, created_at: string, updated_at: string} Formatted display values
164+
---@field readme? string README reference in the form "branch/path"
165+
166+
---@class store.Meta
167+
---@field created_at number Unix timestamp of database creation
168+
169+
---@class store.Database
170+
---@field meta store.Meta Metadata about the dataset
171+
---@field items store.Repository[] Array of repository objects
172+
173+
---@class store.RepositoryField
174+
---@field content string Display content for this field
175+
---@field limit number Maximum display width for this field
176+
177+
---@alias store.RepositoryRenderer fun(repo: store.Repository, isInstalled: boolean): store.RepositoryField[]
178+
-- {
179+
-- author = "Gaylord-kcf",
180+
-- created_at = "2025-12-18T19:57:58Z",
181+
-- description = "🎨 Enhance your Neovim experience with dookie.nvim, a unique color scheme inspired by Plan9's acme editor.",
182+
-- full_name = "Gaylord-kcf/dookie.nvim",
183+
-- issues = 1,
184+
-- name = "dookie.nvim",
185+
-- pretty = {
186+
-- created_at = "last month",
187+
-- issues = "1",
188+
-- stars = "2",
189+
-- updated_at = "today"
190+
-- },
191+
-- readme = "main/README.md",
192+
-- source = "github",
193+
-- stars = 2,
194+
-- tags = { "acme", "colorscheme", "plan9", "theme" },
195+
-- updated_at = "2026-01-31T10:04:17Z",
196+
-- url = "https://github.com/Gaylord-kcf/dookie.nvim"
197+
-- }
198+
199+
---@diagnostic disable-next-line: no-unknown
200+
return function(opts)
201+
assert(__DEFAULT__)
202+
local contents = function(cb)
203+
utils.arun(function()
204+
local db = vim.json.decode(
205+
utils.run(
206+
{ 'curl', '-sL', opts.urls['store'] },
207+
{ cache_path = utils.path('store.json'), cache_invalid = opts.cache_invalid }
208+
).stdout or ''
209+
)
210+
local items = db.items
211+
opts.previewer.items = opts.previewer.items or {}
212+
for _, item in ipairs(items) do
213+
opts.previewer.items[item.full_name] = item
214+
local filter, encode = State.get()
215+
if not filter or filter(item) then cb(encode(item)) end
216+
end
217+
cb(nil)
218+
end)
219+
end
220+
FzfLua.fzf_exec(contents, opts)
221+
end

test/main_spec.lua

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ describe('main', function()
8787
{ src = 'https://github.com/echasnovski/mini.nvim' },
8888
{ src = 'https://github.com/folke/lazy.nvim' },
8989
{ src = 'https://github.com/lewis6991/gitsigns.nvim' },
90+
{ src = 'https://github.com/alex-popov-tech/store.nvim' },
9091
}, { confirm = false })
9192
vim.pack.update(nil, { force = true })
9293
---@diagnostic disable-next-line: missing-parameter
@@ -97,6 +98,7 @@ describe('main', function()
9798
require('mini.visits').setup()
9899
require('mini.icons').setup()
99100
require('gitsigns').setup()
101+
require('store').setup({ plugin_manager = 'lazy.nvim' })
100102
vim.opt.rtp:append('.')
101103
vim.cmd.runtime { 'plugin/fzf-lua-extra.lua', bang = true }
102104
end)

0 commit comments

Comments
 (0)