refactor(pack,ts): switch specs to src field and decentralize update hooks

This commit is contained in:
2026-05-06 20:52:49 +02:00
parent 3f3fdb2603
commit c7e0421e2a
4 changed files with 174 additions and 154 deletions
+10 -7
View File
@@ -37,13 +37,16 @@ require("pack").setup({
"https://github.com/hedyhli/outline.nvim", "https://github.com/hedyhli/outline.nvim",
"nvim.undotree", "nvim.undotree",
{ {
"https://github.com/saghen/blink.cmp", src = "https://github.com/saghen/blink.cmp",
version = vim.version.range("^1"), version = vim.version.range("^1"),
}, },
}) })
require("ts").setup({ require("ts").setup({
"https://github.com/tree-sitter/tree-sitter-bash", {
src = "https://github.com/tree-sitter/tree-sitter-bash",
filetypes = { "sh" },
},
-- required by cpp -- required by cpp
"https://github.com/tree-sitter/tree-sitter-c", "https://github.com/tree-sitter/tree-sitter-c",
"https://github.com/uyha/tree-sitter-cmake", "https://github.com/uyha/tree-sitter-cmake",
@@ -54,14 +57,14 @@ require("ts").setup({
"https://github.com/gbprod/tree-sitter-gitcommit", "https://github.com/gbprod/tree-sitter-gitcommit",
"https://github.com/tree-sitter/tree-sitter-go", "https://github.com/tree-sitter/tree-sitter-go",
{ {
"https://github.com/ngalaiko/tree-sitter-go-template", src = "https://github.com/ngalaiko/tree-sitter-go-template",
lang = "gotmpl", lang = "gotmpl",
}, },
"https://github.com/tree-sitter/tree-sitter-html", "https://github.com/tree-sitter/tree-sitter-html",
"https://github.com/tree-sitter/tree-sitter-json", "https://github.com/tree-sitter/tree-sitter-json",
"https://github.com/tree-sitter-grammars/tree-sitter-luadoc", "https://github.com/tree-sitter-grammars/tree-sitter-luadoc",
{ {
"https://github.com/tree-sitter/tree-sitter-php", src = "https://github.com/tree-sitter/tree-sitter-php",
parsers = { parsers = {
{ lang = "php", location = "php" }, { lang = "php", location = "php" },
{ lang = "php_only", location = "php_only" }, { lang = "php_only", location = "php_only" },
@@ -71,19 +74,19 @@ require("ts").setup({
"https://github.com/tree-sitter/tree-sitter-rust", "https://github.com/tree-sitter/tree-sitter-rust",
"https://github.com/serenadeai/tree-sitter-scss", "https://github.com/serenadeai/tree-sitter-scss",
{ {
"https://github.com/derekstride/tree-sitter-sql", src = "https://github.com/derekstride/tree-sitter-sql",
version = "gh-pages", version = "gh-pages",
}, },
"https://github.com/tree-sitter-grammars/tree-sitter-svelte", "https://github.com/tree-sitter-grammars/tree-sitter-svelte",
{ {
"https://github.com/tree-sitter/tree-sitter-typescript", src = "https://github.com/tree-sitter/tree-sitter-typescript",
parsers = { parsers = {
{ lang = "typescript", location = "typescript" }, { lang = "typescript", location = "typescript" },
{ lang = "tsx", location = "tsx" }, { lang = "tsx", location = "tsx" },
}, },
}, },
{ {
"https://github.com/tree-sitter-grammars/tree-sitter-xml", src = "https://github.com/tree-sitter-grammars/tree-sitter-xml",
parsers = { parsers = {
{ lang = "xml", location = "xml" }, { lang = "xml", location = "xml" },
{ lang = "dtd", location = "dtd" }, { lang = "dtd", location = "dtd" },
+1 -1
View File
@@ -42,7 +42,7 @@ local plugin_cmds = {
max_args = 1, max_args = 1,
complete = complete_plugin_names, complete = complete_plugin_names,
run = function(args) run = function(args)
require("pack").reload_plugin(args[1] --[[@as -nil]]) require("pack").reload_plugin(args[1] --[[@as -nil]], true)
end, end,
}, },
update = { update = {
+69 -70
View File
@@ -24,13 +24,11 @@ local function exec(path)
return true return true
end end
---@class ow.Pack.PluginSpec ---@class ow.Pack.Spec : vim.pack.Spec
---@field [1] string ---@field src string
---@field name? string
---@field version? string | vim.VersionRange
---@field build? string[] | fun(self: ow.Pack.Plugin) ---@field build? string[] | fun(self: ow.Pack.Plugin)
---@class ow.Pack.Plugin : ow.Pack.PluginSpec ---@class ow.Pack.Plugin : ow.Pack.Spec
---@field name string ---@field name string
---@field path string ---@field path string
@@ -81,7 +79,7 @@ local function is_url(src)
return src:find("://") ~= nil return src:find("://") ~= nil
end end
---@param spec string | ow.Pack.PluginSpec ---@param spec string | ow.Pack.Spec
---@return vim.pack.Spec ---@return vim.pack.Spec
local function to_pack_spec(spec) local function to_pack_spec(spec)
if type(spec) == "string" then if type(spec) == "string" then
@@ -89,7 +87,7 @@ local function to_pack_spec(spec)
end end
return { return {
src = spec[1], src = spec.src,
name = spec.name, name = spec.name,
version = spec.version, version = spec.version,
data = { data = {
@@ -130,62 +128,49 @@ local watcher = nil
---@type ow.Util.KeyedDebounceHandle<string>? ---@type ow.Util.KeyedDebounceHandle<string>?
local on_change_handle = nil local on_change_handle = nil
---@alias ow.Pack.Hook fun(ev: ow.Pack.Event.Data) ---@alias ow.Pack.Hook fun(data: ow.Pack.Event.Data)
---@type table<string, ow.Pack.Hook>
local hooks = {}
---@class ow.Pack ---@class ow.Pack
---@field plugins ow.Pack.Plugin[] ---@field plugins table<string, ow.Pack.Plugin> keyed by src
local M = { local M = {
plugins = {}, plugins = {},
} }
---@param name string
---@param fn ow.Pack.Hook
function M.register_hook(name, fn)
hooks[name] = fn
end
local function setup_event_listener()
local group =
vim.api.nvim_create_augroup("ow.Pack.events", { clear = true })
vim.api.nvim_create_autocmd("PackChanged", {
group = group,
---@param ev ow.Pack.Event
callback = function(ev)
local name = ev.data.spec.name
if not name then
return
end
if ev.data.kind ~= "install" and ev.data.kind ~= "update" then
return
end
local hook = hooks[name]
if hook then
hook(ev.data)
end
end,
})
end
---@return string[] ---@return string[]
function M.get_names() function M.get_names()
return vim.tbl_map(function(p) return vim.tbl_values(vim.tbl_map(function(p)
return p.name return p.name
end, M.plugins) end, M.plugins))
end end
---@return string[] ---@return string[]
function M.get_paths() function M.get_paths()
return vim.tbl_map(function(p) return vim.tbl_values(vim.tbl_map(function(p)
return p.path return p.path
end, M.plugins) end, M.plugins))
end
---@param plugin_path string
function M.unload(plugin_path)
local lua_dir = vim.fs.joinpath(plugin_path, "lua")
local search = lua_dir .. "/?.lua;" .. lua_dir .. "/?/init.lua"
for name in pairs(package.loaded) do
if package.searchpath(name, search) then
package.loaded[name] = nil
end
end
end end
---@param name string ---@param name string
function M.reload_plugin(name) ---@param required boolean
load(name, true) function M.reload_plugin(name, required)
for _, plugin in pairs(M.plugins) do
if plugin.name == name then
M.unload(plugin.path)
break
end
end
load(name, required)
end end
function M.watch() function M.watch()
@@ -195,7 +180,7 @@ function M.watch()
local w, err = vim.uv.new_fs_event() local w, err = vim.uv.new_fs_event()
if not w then if not w then
util.error("pack: failed to create fs_event: %s", err) log.error("pack: failed to create fs_event: %s", err)
return return
end end
local on_change, handle = util.keyed_debounce( local on_change, handle = util.keyed_debounce(
@@ -232,7 +217,7 @@ function M.watch()
end end
) )
if not ok then if not ok then
util.error("pack: failed to watch %s: %s", plugins_dir, err) log.error("pack: failed to watch %s: %s", plugins_dir, err)
w:close() w:close()
handle:close() handle:close()
return return
@@ -256,10 +241,10 @@ function M.unwatch()
end end
---@param specs vim.pack.Spec[] ---@param specs vim.pack.Spec[]
---@param on_load fun(data: { spec: vim.pack.Spec, path: string }) ---@param on_load boolean|fun(plug_data: {spec: vim.pack.Spec, path: string})
---@return table<string, true> changed names installed or updated during this call ---@return table<string, ow.Pack.Event.Data>
function M.install(specs, on_load) function M.install(specs, on_load)
---@type table<string, true> ---@type table<string, ow.Pack.Event.Data>
local changed = {} local changed = {}
local group = local group =
vim.api.nvim_create_augroup("ow.Pack.install", { clear = true }) vim.api.nvim_create_augroup("ow.Pack.install", { clear = true })
@@ -267,12 +252,9 @@ function M.install(specs, on_load)
group = group, group = group,
---@param ev ow.Pack.Event ---@param ev ow.Pack.Event
callback = function(ev) callback = function(ev)
local name = ev.data.spec.name local src = ev.data.spec.src
if if ev.data.kind == "install" or ev.data.kind == "update" then
name changed[src] = ev.data
and (ev.data.kind == "install" or ev.data.kind == "update")
then
changed[name] = true
end end
end, end,
}) })
@@ -281,11 +263,11 @@ function M.install(specs, on_load)
return changed return changed
end end
---@param specs (string | ow.Pack.PluginSpec)[] ---@param specs (string | ow.Pack.Spec)[]
function M.setup(specs) function M.setup(specs)
local pack_specs = {} local pack_specs = {}
for _, spec in ipairs(specs) do for _, spec in ipairs(specs) do
local src = type(spec) == "string" and spec or spec[1] local src = type(spec) == "string" and spec or spec.src
if is_url(src) then if is_url(src) then
table.insert(pack_specs, to_pack_spec(spec)) table.insert(pack_specs, to_pack_spec(spec))
else else
@@ -294,11 +276,11 @@ function M.setup(specs)
vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false) vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false)
---@type ow.Pack.Plugin ---@type ow.Pack.Plugin
local plugin = { local plugin = {
[1] = src, src = src,
name = src, name = src,
path = runtime[1] or "", path = runtime[1] or "",
} }
table.insert(M.plugins, plugin) M.plugins[plugin.src] = plugin
end end
end end
@@ -310,30 +292,47 @@ function M.setup(specs)
local d = data.spec.data or {} local d = data.spec.data or {}
---@type ow.Pack.Plugin ---@type ow.Pack.Plugin
local plugin = { local plugin = {
[1] = data.spec.src, src = data.spec.src,
name = data.spec.name, name = data.spec.name,
version = data.spec.version, version = data.spec.version,
build = d.build, build = d.build,
path = data.path, path = data.path,
} }
table.insert(M.plugins, plugin) M.plugins[plugin.src] = plugin
vim.cmd.packadd(plugin.name) vim.cmd.packadd(plugin.name)
end) end)
for _, plugin in ipairs(M.plugins) do for _, plugin in pairs(M.plugins) do
if plugin.build then if plugin.build then
if changed[plugin.name] then local data = changed[plugin.src]
if data then
plugin.path = data.path
run_build(plugin) run_build(plugin)
end end
M.register_hook(plugin.name, function(ev)
plugin.path = ev.path
run_build(plugin)
end)
end end
load(plugin.name, false) load(plugin.name, false)
end end
setup_event_listener() vim.api.nvim_create_autocmd("PackChanged", {
group = vim.api.nvim_create_augroup(
"ow.Pack.updates",
{ clear = true }
),
callback = function(ev)
if ev.data.kind ~= "update" then
return
end
local plugin = M.plugins[ev.data.spec.src]
if not plugin then
return
end
plugin.path = ev.data.path
if plugin.build then
run_build(plugin)
end
M.reload_plugin(plugin.name, false)
end,
})
end end
---@param names? string[] ---@param names? string[]
+94 -76
View File
@@ -7,31 +7,30 @@ local M = {}
---@field location? string ---@field location? string
---@field generate? boolean ---@field generate? boolean
---@field from_json? boolean ---@field from_json? boolean
---@field filetypes? string[]
---@class ow.TS.RepoBase ---@class ow.TS.RepoBase
---@field [1] string ---@field src string
---@field version? string | vim.VersionRange ---@field version? string | vim.VersionRange
---@class ow.TS.Repo : ow.TS.RepoBase ---@class ow.TS.Repo : ow.TS.RepoBase
---@field name string ---@field name string
---@field parsers ow.TS.Parser[] ---@field parsers ow.TS.Parser[]
---@class RepoState
---@field repo ow.TS.Repo
---@field path? string ---@field path? string
---@class ow.TS.SingleRepoOpts : ow.TS.RepoBase ---@class ow.TS.SingleSpec : ow.TS.RepoBase
---@field lang? string ---@field lang? string
---@field location? string ---@field location? string
---@field generate? boolean ---@field generate? boolean
---@field from_json? boolean ---@field from_json? boolean
---@field filetypes? string[]
---@class ow.TS.MultiRepoOpts : ow.TS.RepoBase ---@class ow.TS.MultiSpec : ow.TS.RepoBase
---@field parsers ow.TS.Parser[] ---@field parsers ow.TS.Parser[]
---@field generate? boolean ---@field generate? boolean
---@field from_json? boolean ---@field from_json? boolean
---@alias ow.TS.RepoOpts ow.TS.SingleRepoOpts | ow.TS.MultiRepoOpts ---@alias ow.TS.Spec ow.TS.SingleSpec | ow.TS.MultiSpec
---@param path string ---@param path string
---@param lang string ---@param lang string
@@ -53,6 +52,28 @@ local function start_treesitter(buf)
end end
end end
---@param parser ow.TS.Parser
---@param so string
local function register(parser, so)
vim.treesitter.language.add(parser.lang, { path = so })
local filetypes = { parser.lang }
if parser.filetypes then
vim.treesitter.language.register(parser.lang, parser.filetypes)
vim.list_extend(filetypes, parser.filetypes)
end
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup(
"ow.ts.parser." .. parser.lang,
{ clear = true }
),
pattern = filetypes,
callback = function(ev)
start_treesitter(ev.buf)
end,
})
end
---@param lang string ---@param lang string
local function activate_open_buffers(lang) local function activate_open_buffers(lang)
local fts = vim.treesitter.language.get_filetypes(lang) local fts = vim.treesitter.language.get_filetypes(lang)
@@ -66,10 +87,10 @@ local function activate_open_buffers(lang)
end end
end end
---@param state RepoState ---@param repo ow.TS.Repo
---@param parser ow.TS.Parser ---@param parser ow.TS.Parser
local function build(state, parser) local function build(repo, parser)
local path = assert(state.path, "repo not installed: " .. state.repo.name) local path = assert(repo.path, "repo not installed: " .. repo.name)
local cwd = parser.location and vim.fs.joinpath(path, parser.location) local cwd = parser.location and vim.fs.joinpath(path, parser.location)
or path or path
local out = parser_so(path, parser.lang) local out = parser_so(path, parser.lang)
@@ -88,7 +109,7 @@ local function build(state, parser)
) )
return return
end end
vim.treesitter.language.add(parser.lang, { path = out }) register(parser, out)
activate_open_buffers(parser.lang) activate_open_buffers(parser.lang)
end end
@@ -137,13 +158,13 @@ local function build(state, parser)
end end
end end
---@param pack_name string ---@param name string
---@return string? lang ---@return string? lang
---@return string? err ---@return string? err
local function lang_from_name(pack_name) local function lang_from_name(name)
local lang = pack_name:match("^tree%-sitter%-(.+)$") local lang = name:match("^tree%-sitter%-(.+)$")
if not lang then if not lang then
return nil, "cannot derive lang from pack name: " .. pack_name return nil, "cannot derive lang from pack name: " .. name
end end
return lang return lang
end end
@@ -166,34 +187,30 @@ local function pick(child, parent)
return parent return parent
end end
---@param entry string | ow.TS.RepoOpts ---@param spec string | ow.TS.Spec
---@return ow.TS.Repo? ---@return ow.TS.Repo?
local function normalize(entry) local function spec_to_repo(spec)
---@type ow.TS.RepoOpts if type(spec) == "string" then
local opts spec = { src = spec } --[[@as ow.TS.SingleSpec]]
if type(entry) == "string" then
opts = { entry } --[[@as ow.TS.SingleRepoOpts]]
else
opts = entry
end end
local name = name_from_url(opts[1]) local name = name_from_url(spec.src)
---@type ow.TS.Parser[] ---@type ow.TS.Parser[]
local parsers = {} local parsers = {}
local input_parsers = (opts --[[@as ow.TS.MultiRepoOpts]]).parsers if spec.parsers then
if input_parsers then ---@cast spec ow.TS.MultiSpec
---@cast opts ow.TS.MultiRepoOpts for _, s in ipairs(spec.parsers) do
for _, s in ipairs(opts.parsers) do
table.insert(parsers, { table.insert(parsers, {
lang = s.lang, lang = s.lang,
location = s.location, location = s.location,
generate = pick(s.generate, opts.generate), generate = pick(s.generate, spec.generate),
from_json = pick(s.from_json, opts.from_json), from_json = pick(s.from_json, spec.from_json),
filetypes = s.filetypes,
}) })
end end
else else
---@cast opts ow.TS.SingleRepoOpts ---@cast spec ow.TS.SingleSpec
local lang = opts.lang local lang = spec.lang
if not lang then if not lang then
local derived, err = lang_from_name(name) local derived, err = lang_from_name(name)
if not derived then if not derived then
@@ -204,79 +221,80 @@ local function normalize(entry)
end end
table.insert(parsers, { table.insert(parsers, {
lang = lang, lang = lang,
location = opts.location, location = spec.location,
generate = opts.generate, generate = spec.generate,
from_json = opts.from_json, from_json = spec.from_json,
filetypes = spec.filetypes,
}) })
end end
return { return {
[1] = opts[1], src = spec.src,
name = name, name = name,
version = opts.version, version = spec.version,
parsers = parsers, parsers = parsers,
path = nil,
} }
end end
---@param opts (string | ow.TS.RepoOpts)[] ---@param specs (string | ow.TS.Spec)[]
function M.setup(opts) function M.setup(specs)
---@type ow.TS.Repo[] ---@type table<string, ow.TS.Repo>
local repos = {} local repos = {}
for _, entry in ipairs(opts) do local pack = require("pack")
local repo = normalize(entry) for _, spec in ipairs(specs) do
local repo = spec_to_repo(spec)
if repo then if repo then
table.insert(repos, repo) repos[repo.src] = repo
end end
end end
---@type vim.pack.Spec[] ---@type vim.pack.Spec[]
local pack_specs = vim.tbl_map(function(p) local vim_specs = {}
return { src = p[1], name = p.name, version = p.version } for _, repo in pairs(repos) do
end, repos) table.insert(
vim_specs,
---@type RepoState[] { src = repo.src, name = repo.name, version = repo.version }
local states = {} )
---@type table<string, RepoState>
local by_src = {}
for _, repo in ipairs(repos) do
local state = { repo = repo }
table.insert(states, state)
by_src[repo[1]] = state
end end
local pack = require("pack") local changed = pack.install(vim_specs, function(data)
local changed = pack.install(pack_specs, function(data) local repo = repos[data.spec.src]
local state = by_src[data.spec.src] if repo then
if state then repo.path = data.path
state.path = data.path
end end
end) end)
for _, state in ipairs(states) do for _, repo in pairs(repos) do
pack.register_hook(state.repo.name, function(ev) if not repo.path then
state.path = ev.path log.error("Repo not installed: %s", repo.name)
for _, parser in ipairs(state.repo.parsers) do
build(state, parser)
end
end)
if not state.path then
log.error("Repo not installed: %s", state.repo.name)
goto continue goto continue
end end
for _, parser in ipairs(state.repo.parsers) do for _, parser in ipairs(repo.parsers) do
local so = parser_so(state.path, parser.lang) local so = parser_so(repo.path, parser.lang)
if changed[state.repo.name] or not vim.uv.fs_stat(so) then if changed[repo.src] or not vim.uv.fs_stat(so) then
build(state, parser) build(repo, parser)
else else
vim.treesitter.language.add(parser.lang, { path = so }) register(parser, so)
end end
end end
::continue:: ::continue::
end end
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("PackChanged", {
group = vim.api.nvim_create_augroup("ow.ts.updates", { clear = true }),
callback = function(ev) callback = function(ev)
start_treesitter(ev.buf) if ev.data.kind ~= "update" then
return
end
local repo = repos[ev.data.spec.src]
if not repo then
return
end
repo.path = ev.data.path
for _, parser in ipairs(repo.parsers) do
build(repo, parser)
end
end, end,
}) })
end end