mirror of
https://github.com/folke/snacks.nvim
synced 2025-08-04 02:38:46 +00:00
feat(snacks): added snacks.picker
(#445)
## Description More info coming tomorrow. In short: - very fast. pretty much realtime filtering/sorting in huge repos (like 1.7 million files) - extensible - easy to customize the layout (and lots of presets) with `snacks.layout` - simple to create custom pickers - `vim.ui.select` - lots of builtin pickers - uses treesitter highlighting wherever it makes sense - fast lua fuzzy matcher which supports the [fzf syntax](https://junegunn.github.io/fzf/search-syntax/) and additionally supports field filters, like `file:lua$ 'function` There's no snacks picker command, just use lua. ```lua -- all pickers Snacks.picker() -- run files picker Snacks.picker.files(opts) Snacks.picker.pick("files", opts) Snacks.picker.pick({source = "files", ...}) ``` <!-- Describe the big picture of your changes to communicate to the maintainers why we should accept this pull request. --> ## Todo - [x] issue with preview loc not always correct when scrolling fast in list (probably due to `snacks.scroll`) - [x] `grep` (`live_grep`) is sometimes too fast in large repos and can impact ui rendering. Not very noticeable, but something I want to look at. - [x] docs - [x] treesitter highlights are broken. Messed something up somewhere ## Related Issue(s) <!-- If this PR fixes any issues, please link to the issue here. - Fixes #<issue_number> --> ## Screenshots <!-- Add screenshots of the changes if applicable. -->
This commit is contained in:
parent
1b7a57a0b1
commit
559d6c6bf2
67 changed files with 12013 additions and 126 deletions
|
@ -41,6 +41,16 @@ local query = vim.treesitter.query.parse(
|
|||
(expression_list
|
||||
value: (table_constructor) @example_config)
|
||||
) @example
|
||||
|
||||
;; props
|
||||
(assignment_statement
|
||||
(variable_list
|
||||
name: (dot_index_expression
|
||||
field: (identifier) @prop_name)
|
||||
@_pn (#lua-match? @_pn "^M%."))
|
||||
(expression_list
|
||||
value: (_) @prop_value)
|
||||
) @prop
|
||||
]]
|
||||
)
|
||||
|
||||
|
@ -56,14 +66,24 @@ local query = vim.treesitter.query.parse(
|
|||
---@field captures snacks.docs.Capture[]
|
||||
---@field comments string[]
|
||||
|
||||
---@class snacks.docs.Method
|
||||
---@field mod string
|
||||
---@field name string
|
||||
---@field args string
|
||||
---@field comment? string
|
||||
---@field types? string
|
||||
---@field type "method"|"function"}[]
|
||||
|
||||
---@class snacks.docs.Info
|
||||
---@field config? string
|
||||
---@field mod? string
|
||||
---@field methods {name: string, args: string, comment?: string, types?: string, type: "method"|"function"}[]
|
||||
---@field modname? string
|
||||
---@field methods snacks.docs.Method[]
|
||||
---@field types string[]
|
||||
---@field setup? string
|
||||
---@field examples table<string, string>
|
||||
---@field styles {name:string, opts:string, comment?:string}[]
|
||||
---@field props table<string, string>
|
||||
|
||||
---@param lines string[]
|
||||
function M.parse(lines)
|
||||
|
@ -85,6 +105,7 @@ function M.parse(lines)
|
|||
---@type snacks.docs.Parse
|
||||
local ret = { captures = {}, comments = {} }
|
||||
|
||||
local used_comments = {} ---@type table<number, boolean>
|
||||
for id, node in query:iter_captures(parser:trees()[1]:root(), source) do
|
||||
local name = query.captures[id]
|
||||
if not name:find("_") then
|
||||
|
@ -92,7 +113,7 @@ function M.parse(lines)
|
|||
local fields = {}
|
||||
for id2, node2 in query:iter_captures(node, source) do
|
||||
local c = query.captures[id2]
|
||||
if c:find(".+_") then
|
||||
if c:find(name .. "_") then
|
||||
fields[c:gsub("^.*_", "")] = vim.treesitter.get_node_text(node2, source)
|
||||
end
|
||||
end
|
||||
|
@ -101,7 +122,7 @@ function M.parse(lines)
|
|||
local comment = "" ---@type string
|
||||
if comments[node:start()] then
|
||||
comment = comments[node:start()]
|
||||
comments[node:start()] = nil
|
||||
used_comments[node:start()] = true
|
||||
end
|
||||
|
||||
table.insert(ret.captures, {
|
||||
|
@ -114,6 +135,9 @@ function M.parse(lines)
|
|||
})
|
||||
end
|
||||
end
|
||||
for l in pairs(used_comments) do
|
||||
comments[l] = nil
|
||||
end
|
||||
|
||||
-- remove comments that are followed by code
|
||||
for l in pairs(comments) do
|
||||
|
@ -131,7 +155,9 @@ function M.parse(lines)
|
|||
end
|
||||
|
||||
---@param lines string[]
|
||||
function M.extract(lines)
|
||||
---@param opts {prefix: string, name:string}
|
||||
function M.extract(lines, opts)
|
||||
local fqn = opts.prefix .. "." .. opts.name
|
||||
local parse = M.parse(lines)
|
||||
---@type snacks.docs.Info
|
||||
local ret = {
|
||||
|
@ -141,10 +167,16 @@ function M.extract(lines)
|
|||
end, parse.comments),
|
||||
styles = {},
|
||||
examples = {},
|
||||
props = {},
|
||||
}
|
||||
|
||||
for _, c in ipairs(parse.captures) do
|
||||
if c.comment:find("@private") then
|
||||
if
|
||||
c.comment:find("@private")
|
||||
or c.comment:find("@protected")
|
||||
or c.comment:find("@package")
|
||||
or c.comment:find("@hide")
|
||||
then
|
||||
-- skip private
|
||||
elseif c.name == "local" then
|
||||
if vim.tbl_contains({ "defaults", "config" }, c.fields.name) then
|
||||
|
@ -152,13 +184,23 @@ function M.extract(lines)
|
|||
elseif c.fields.name == "M" then
|
||||
ret.mod = c.comment
|
||||
end
|
||||
elseif c.name == "prop" then
|
||||
local name = c.fields.name:sub(1)
|
||||
local value = c.fields.value
|
||||
ret.props[name] = c.comment == "" and value or c.comment .. "\n" .. value
|
||||
elseif c.name == "fun" then
|
||||
local name = c.fields.name:sub(2)
|
||||
local args = (c.fields.params or ""):sub(2, -2)
|
||||
local type = name:sub(1, 1)
|
||||
name = name:sub(2)
|
||||
if not name:find("^_") then
|
||||
table.insert(ret.methods, { name = name, args = args, comment = c.comment, type = type })
|
||||
table.insert(ret.methods, {
|
||||
mod = type == ":" and opts.name or fqn,
|
||||
name = name,
|
||||
args = args,
|
||||
comment = c.comment,
|
||||
type = type,
|
||||
})
|
||||
end
|
||||
elseif c.name == "style" then
|
||||
table.insert(ret.styles, { name = c.fields.name, opts = c.fields.config, comment = c.comment })
|
||||
|
@ -167,6 +209,27 @@ function M.extract(lines)
|
|||
end
|
||||
end
|
||||
|
||||
if ret.mod then
|
||||
local mod_lines = vim.split(ret.mod, "\n")
|
||||
mod_lines = vim.tbl_filter(function(line)
|
||||
local overload = line:match("^%-%-%-%s*@overload (.*)(%s*)$") --[[@as string?]]
|
||||
if overload then
|
||||
table.insert(ret.methods, {
|
||||
mod = fqn,
|
||||
name = "",
|
||||
args = "",
|
||||
type = "",
|
||||
comment = "---@type " .. overload,
|
||||
})
|
||||
return false
|
||||
elseif line:find("^%s*$") then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end, mod_lines)
|
||||
ret.mod = table.concat(mod_lines, "\n")
|
||||
end
|
||||
|
||||
return ret
|
||||
end
|
||||
|
||||
|
@ -186,6 +249,7 @@ end
|
|||
---@param opts? {extract_comment: boolean} -- default true
|
||||
function M.md(str, opts)
|
||||
str = str or ""
|
||||
str = str:gsub("\r", "")
|
||||
opts = opts or {}
|
||||
if opts.extract_comment == nil then
|
||||
opts.extract_comment = true
|
||||
|
@ -223,13 +287,15 @@ function M.examples(name)
|
|||
return {}
|
||||
end
|
||||
local lines = vim.fn.readfile(fname)
|
||||
local info = M.extract(lines)
|
||||
local info = M.extract(lines, { prefix = "Snacks.examples", name = name })
|
||||
return info.examples
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param info snacks.docs.Info
|
||||
function M.render(name, info)
|
||||
---@param opts? {setup?:boolean, config?:boolean, styles?:boolean, types?:boolean, prefix?:string, examples?:boolean}
|
||||
function M.render(name, info, opts)
|
||||
opts = opts or {}
|
||||
local lines = {} ---@type string[]
|
||||
local function add(line)
|
||||
table.insert(lines, line)
|
||||
|
@ -239,8 +305,11 @@ function M.render(name, info)
|
|||
if name == "init" then
|
||||
prefix = "Snacks"
|
||||
end
|
||||
if info.modname then
|
||||
prefix = "local M"
|
||||
end
|
||||
|
||||
if name ~= "init" and (info.config or info.setup) then
|
||||
if name ~= "init" and (info.config or info.setup) and opts.setup ~= false then
|
||||
add("## 📦 Setup\n")
|
||||
add(([[
|
||||
```lua
|
||||
|
@ -260,24 +329,26 @@ function M.render(name, info)
|
|||
]]):format(info.setup or name, name))
|
||||
end
|
||||
|
||||
if info.config then
|
||||
if info.config and opts.config ~= false then
|
||||
add("## ⚙️ Config\n")
|
||||
add(M.md(info.config))
|
||||
end
|
||||
|
||||
local examples = M.examples(name)
|
||||
local names = vim.tbl_keys(examples)
|
||||
table.sort(names)
|
||||
if not vim.tbl_isempty(examples) then
|
||||
add("## 🚀 Examples\n")
|
||||
for _, n in ipairs(names) do
|
||||
local example = examples[n]
|
||||
add(("### `%s`\n"):format(n))
|
||||
add(M.md(example))
|
||||
if opts.examples ~= false then
|
||||
local examples = M.examples(name)
|
||||
local names = vim.tbl_keys(examples)
|
||||
table.sort(names)
|
||||
if not vim.tbl_isempty(examples) then
|
||||
add("## 🚀 Examples\n")
|
||||
for _, n in ipairs(names) do
|
||||
local example = examples[n]
|
||||
add(("### `%s`\n"):format(n))
|
||||
add(M.md(example))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #info.styles > 0 then
|
||||
if #info.styles > 0 and opts.styles ~= false then
|
||||
table.sort(info.styles, function(a, b)
|
||||
return a.name < b.name
|
||||
end)
|
||||
|
@ -303,57 +374,44 @@ docs for more information on how to customize these styles
|
|||
end
|
||||
end
|
||||
|
||||
if #info.types > 0 then
|
||||
if #info.types > 0 and opts.types ~= false then
|
||||
add("## 📚 Types\n")
|
||||
for _, t in ipairs(info.types) do
|
||||
add(M.md(t))
|
||||
end
|
||||
end
|
||||
|
||||
if info.mod or #info.methods > 0 then
|
||||
add("## 📦 Module\n")
|
||||
local mod_lines = info.mod and not info.mod:find("^%s*$") and vim.split(info.mod, "\n") or {}
|
||||
local hide = #mod_lines == 0 or (#mod_lines == 1 and mod_lines[1]:find("@class"))
|
||||
|
||||
if not hide or #info.methods > 0 then
|
||||
local title = info.modname and ("`%s`"):format(info.modname) or "Module"
|
||||
add(("## 📦 %s\n"):format(title))
|
||||
end
|
||||
|
||||
if info.mod then
|
||||
local mod_lines = vim.split(info.mod, "\n")
|
||||
mod_lines = vim.tbl_filter(function(line)
|
||||
local overload = line:match("^%-%-%-%s*@overload (.*)(%s*)$") --[[@as string?]]
|
||||
if overload then
|
||||
table.insert(info.methods, {
|
||||
name = "",
|
||||
args = "",
|
||||
type = "",
|
||||
comment = "---@type " .. overload,
|
||||
})
|
||||
return false
|
||||
elseif line:find("^%s*$") then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end, mod_lines)
|
||||
local hide = #mod_lines == 1 and mod_lines[1]:find("@class")
|
||||
if not hide then
|
||||
table.insert(mod_lines, prefix .. " = {}")
|
||||
add(M.md(table.concat(mod_lines, "\n")))
|
||||
end
|
||||
if info.mod and not hide then
|
||||
table.insert(mod_lines, prefix .. " = {}")
|
||||
add(M.md(table.concat(mod_lines, "\n")))
|
||||
end
|
||||
|
||||
table.sort(info.methods, function(a, b)
|
||||
if a.mod ~= b.mod then
|
||||
return a.mod < b.mod
|
||||
end
|
||||
if a.type == b.type then
|
||||
return a.name < b.name
|
||||
end
|
||||
return a.type < b.type
|
||||
end)
|
||||
|
||||
local last ---@type string?
|
||||
for _, method in ipairs(info.methods) do
|
||||
add(("### `%s%s%s()`\n"):format(method.type == ":" and name or prefix, method.type, method.name))
|
||||
local code = ("%s\n%s%s%s(%s)"):format(
|
||||
method.comment or "",
|
||||
method.type == ":" and name or prefix,
|
||||
method.type,
|
||||
method.name,
|
||||
method.args
|
||||
)
|
||||
local title = ("### `%s%s%s()`\n"):format(method.mod, method.type, method.name)
|
||||
if title ~= last then
|
||||
last = title
|
||||
add(title)
|
||||
end
|
||||
local code = ("%s\n%s%s%s(%s)"):format(method.comment or "", method.mod, method.type, method.name, method.args)
|
||||
add(M.md(code))
|
||||
end
|
||||
|
||||
|
@ -383,7 +441,33 @@ function M.write(name, lines)
|
|||
table.insert(top, "")
|
||||
vim.list_extend(top, lines)
|
||||
|
||||
vim.fn.writefile(top, path)
|
||||
vim.fn.writefile(vim.split(table.concat(top, "\n"), "\n"), path)
|
||||
end
|
||||
|
||||
---@param ret string[]
|
||||
function M.picker(ret)
|
||||
local lines = vim.fn.readfile("lua/snacks/picker/config/sources.lua")
|
||||
local info = M.extract(lines, { prefix = "Snacks.picker", name = "sources" })
|
||||
local sources = vim.tbl_keys(info.props)
|
||||
table.sort(sources)
|
||||
table.insert(ret, "## 🔍 Sources\n")
|
||||
for _, source in ipairs(sources) do
|
||||
local opts = info.props[source]
|
||||
table.insert(ret, ("### `%s`"):format(source))
|
||||
table.insert(ret, "")
|
||||
table.insert(ret, M.md(opts))
|
||||
end
|
||||
lines = vim.fn.readfile("lua/snacks/picker/config/layouts.lua")
|
||||
info = M.extract(lines, { prefix = "Snacks.picker", name = "layouts" })
|
||||
sources = vim.tbl_keys(info.props)
|
||||
table.sort(sources)
|
||||
table.insert(ret, "## 🖼️ Layouts\n")
|
||||
for _, source in ipairs(sources) do
|
||||
local opts = info.props[source]
|
||||
table.insert(ret, ("### `%s`"):format(source))
|
||||
table.insert(ret, "")
|
||||
table.insert(ret, M.md(opts))
|
||||
end
|
||||
end
|
||||
|
||||
function M._build()
|
||||
|
@ -401,6 +485,7 @@ function M._build()
|
|||
examples = {},
|
||||
styles = {},
|
||||
setup = "---@type table<string, snacks.win.Config>\n styles",
|
||||
props = {},
|
||||
}
|
||||
|
||||
for _, plugin in pairs(plugins) do
|
||||
|
@ -408,11 +493,56 @@ function M._build()
|
|||
local name = plugin.name
|
||||
print("[gen] " .. name .. ".md")
|
||||
local lines = vim.fn.readfile(plugin.file)
|
||||
local info = M.extract(lines)
|
||||
local info = M.extract(lines, { prefix = "Snacks", name = name })
|
||||
|
||||
local children = {} ---@type snacks.docs.Info[]
|
||||
for c, child in pairs(plugin.meta.merge or {}) do
|
||||
local child_name = type(c) == "number" and child or c --[[@as string]]
|
||||
local child_file = ("%s/%s/%s"):format(Snacks.meta.root, name, child:gsub("%.", "/"))
|
||||
for _, f in ipairs({ ".lua", "/init.lua" }) do
|
||||
if vim.uv.fs_stat(child_file .. f) then
|
||||
child_file = child_file .. f
|
||||
break
|
||||
end
|
||||
end
|
||||
assert(vim.uv.fs_stat(child_file), ("file not found: %s"):format(child_file))
|
||||
local child_lines = vim.fn.readfile(child_file)
|
||||
local child_info = M.extract(child_lines, { prefix = "Snacks." .. name, name = child_name })
|
||||
child_info.modname = "snacks." .. name .. "." .. child
|
||||
if child_info.config then
|
||||
assert(not info.config, "config already exists")
|
||||
info.config = child_info.config
|
||||
end
|
||||
vim.list_extend(info.types, child_info.types)
|
||||
table.insert(children, child_info)
|
||||
end
|
||||
|
||||
vim.list_extend(styles.styles, info.styles)
|
||||
info.config = name ~= "init" and info.config or nil
|
||||
plugin.meta.config = info.config ~= nil
|
||||
M.write(name, M.render(name, info))
|
||||
|
||||
local rendered = {} ---@type string[]
|
||||
vim.list_extend(rendered, M.render(name, info))
|
||||
if name == "picker" then
|
||||
M.picker(rendered)
|
||||
end
|
||||
|
||||
for _, child in ipairs(children) do
|
||||
table.insert(rendered, "")
|
||||
vim.list_extend(
|
||||
rendered,
|
||||
M.render(name, child, {
|
||||
setup = false,
|
||||
config = false,
|
||||
styles = false,
|
||||
types = false,
|
||||
examples = false,
|
||||
})
|
||||
)
|
||||
end
|
||||
|
||||
M.write(name, rendered)
|
||||
|
||||
if plugin.meta.types then
|
||||
table.insert(types.fields, ("---@field %s snacks.%s"):format(plugin.name, plugin.name))
|
||||
end
|
||||
|
@ -454,7 +584,7 @@ end
|
|||
function M.readme(plugins, types)
|
||||
local path = "lua/snacks/init.lua"
|
||||
local lines = vim.fn.readfile(path) --[[ @as string[] ]]
|
||||
local info = M.extract(lines)
|
||||
local info = M.extract(lines, { prefix = "Snacks", name = "init" })
|
||||
local readme = table.concat(vim.fn.readfile("README.md"), "\n")
|
||||
local example = table.concat(vim.fn.readfile("docs/examples/init.lua"), "\n")
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue