From c7e0421e2a1eb569b76f5884cf5978c56e5690e0 Mon Sep 17 00:00:00 2001 From: Oscar Wallberg Date: Wed, 6 May 2026 20:52:49 +0200 Subject: [PATCH] refactor(pack,ts): switch specs to src field and decentralize update hooks --- init.lua | 17 +++-- lua/core/commands.lua | 2 +- lua/pack.lua | 139 +++++++++++++++++----------------- lua/ts.lua | 170 +++++++++++++++++++++++------------------- 4 files changed, 174 insertions(+), 154 deletions(-) diff --git a/init.lua b/init.lua index 5e971b5..1912fb4 100644 --- a/init.lua +++ b/init.lua @@ -37,13 +37,16 @@ require("pack").setup({ "https://github.com/hedyhli/outline.nvim", "nvim.undotree", { - "https://github.com/saghen/blink.cmp", + src = "https://github.com/saghen/blink.cmp", version = vim.version.range("^1"), }, }) 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 "https://github.com/tree-sitter/tree-sitter-c", "https://github.com/uyha/tree-sitter-cmake", @@ -54,14 +57,14 @@ require("ts").setup({ "https://github.com/gbprod/tree-sitter-gitcommit", "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", }, "https://github.com/tree-sitter/tree-sitter-html", "https://github.com/tree-sitter/tree-sitter-json", "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 = { { lang = "php", location = "php" }, { lang = "php_only", location = "php_only" }, @@ -71,19 +74,19 @@ require("ts").setup({ "https://github.com/tree-sitter/tree-sitter-rust", "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", }, "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 = { { lang = "typescript", location = "typescript" }, { lang = "tsx", location = "tsx" }, }, }, { - "https://github.com/tree-sitter-grammars/tree-sitter-xml", + src = "https://github.com/tree-sitter-grammars/tree-sitter-xml", parsers = { { lang = "xml", location = "xml" }, { lang = "dtd", location = "dtd" }, diff --git a/lua/core/commands.lua b/lua/core/commands.lua index 6d23e9a..e94f0d5 100644 --- a/lua/core/commands.lua +++ b/lua/core/commands.lua @@ -42,7 +42,7 @@ local plugin_cmds = { max_args = 1, complete = complete_plugin_names, run = function(args) - require("pack").reload_plugin(args[1] --[[@as -nil]]) + require("pack").reload_plugin(args[1] --[[@as -nil]], true) end, }, update = { diff --git a/lua/pack.lua b/lua/pack.lua index 884a27f..bf46e01 100644 --- a/lua/pack.lua +++ b/lua/pack.lua @@ -24,13 +24,11 @@ local function exec(path) return true end ----@class ow.Pack.PluginSpec ----@field [1] string ----@field name? string ----@field version? string | vim.VersionRange +---@class ow.Pack.Spec : vim.pack.Spec +---@field src string ---@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 path string @@ -81,7 +79,7 @@ local function is_url(src) return src:find("://") ~= nil end ----@param spec string | ow.Pack.PluginSpec +---@param spec string | ow.Pack.Spec ---@return vim.pack.Spec local function to_pack_spec(spec) if type(spec) == "string" then @@ -89,7 +87,7 @@ local function to_pack_spec(spec) end return { - src = spec[1], + src = spec.src, name = spec.name, version = spec.version, data = { @@ -130,62 +128,49 @@ local watcher = nil ---@type ow.Util.KeyedDebounceHandle? local on_change_handle = nil ----@alias ow.Pack.Hook fun(ev: ow.Pack.Event.Data) - ----@type table -local hooks = {} +---@alias ow.Pack.Hook fun(data: ow.Pack.Event.Data) ---@class ow.Pack ----@field plugins ow.Pack.Plugin[] +---@field plugins table keyed by src local M = { 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[] function M.get_names() - return vim.tbl_map(function(p) + return vim.tbl_values(vim.tbl_map(function(p) return p.name - end, M.plugins) + end, M.plugins)) end ---@return string[] function M.get_paths() - return vim.tbl_map(function(p) + return vim.tbl_values(vim.tbl_map(function(p) 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 ---@param name string -function M.reload_plugin(name) - load(name, true) +---@param required boolean +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 function M.watch() @@ -195,7 +180,7 @@ function M.watch() local w, err = vim.uv.new_fs_event() 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 end local on_change, handle = util.keyed_debounce( @@ -232,7 +217,7 @@ function M.watch() end ) 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() handle:close() return @@ -256,10 +241,10 @@ function M.unwatch() end ---@param specs vim.pack.Spec[] ----@param on_load fun(data: { spec: vim.pack.Spec, path: string }) ----@return table changed names installed or updated during this call +---@param on_load boolean|fun(plug_data: {spec: vim.pack.Spec, path: string}) +---@return table function M.install(specs, on_load) - ---@type table + ---@type table local changed = {} local group = vim.api.nvim_create_augroup("ow.Pack.install", { clear = true }) @@ -267,12 +252,9 @@ function M.install(specs, on_load) group = group, ---@param ev ow.Pack.Event callback = function(ev) - local name = ev.data.spec.name - if - name - and (ev.data.kind == "install" or ev.data.kind == "update") - then - changed[name] = true + local src = ev.data.spec.src + if ev.data.kind == "install" or ev.data.kind == "update" then + changed[src] = ev.data end end, }) @@ -281,11 +263,11 @@ function M.install(specs, on_load) return changed end ----@param specs (string | ow.Pack.PluginSpec)[] +---@param specs (string | ow.Pack.Spec)[] function M.setup(specs) local pack_specs = {} 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 table.insert(pack_specs, to_pack_spec(spec)) else @@ -294,11 +276,11 @@ function M.setup(specs) vim.api.nvim_get_runtime_file("pack/*/opt/" .. src, false) ---@type ow.Pack.Plugin local plugin = { - [1] = src, + src = src, name = src, path = runtime[1] or "", } - table.insert(M.plugins, plugin) + M.plugins[plugin.src] = plugin end end @@ -310,30 +292,47 @@ function M.setup(specs) local d = data.spec.data or {} ---@type ow.Pack.Plugin local plugin = { - [1] = data.spec.src, + src = data.spec.src, name = data.spec.name, version = data.spec.version, build = d.build, path = data.path, } - table.insert(M.plugins, plugin) + M.plugins[plugin.src] = plugin vim.cmd.packadd(plugin.name) end) - for _, plugin in ipairs(M.plugins) do + for _, plugin in pairs(M.plugins) do if plugin.build then - if changed[plugin.name] then + local data = changed[plugin.src] + if data then + plugin.path = data.path run_build(plugin) end - M.register_hook(plugin.name, function(ev) - plugin.path = ev.path - run_build(plugin) - end) end load(plugin.name, false) 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 ---@param names? string[] diff --git a/lua/ts.lua b/lua/ts.lua index 7331c05..37d73fd 100644 --- a/lua/ts.lua +++ b/lua/ts.lua @@ -7,31 +7,30 @@ local M = {} ---@field location? string ---@field generate? boolean ---@field from_json? boolean +---@field filetypes? string[] ---@class ow.TS.RepoBase ----@field [1] string +---@field src string ---@field version? string | vim.VersionRange ---@class ow.TS.Repo : ow.TS.RepoBase ---@field name string ---@field parsers ow.TS.Parser[] - ----@class RepoState ----@field repo ow.TS.Repo ---@field path? string ----@class ow.TS.SingleRepoOpts : ow.TS.RepoBase +---@class ow.TS.SingleSpec : ow.TS.RepoBase ---@field lang? string ---@field location? string ---@field generate? 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 generate? 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 lang string @@ -53,6 +52,28 @@ local function start_treesitter(buf) 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 local function activate_open_buffers(lang) local fts = vim.treesitter.language.get_filetypes(lang) @@ -66,10 +87,10 @@ local function activate_open_buffers(lang) end end ----@param state RepoState +---@param repo ow.TS.Repo ---@param parser ow.TS.Parser -local function build(state, parser) - local path = assert(state.path, "repo not installed: " .. state.repo.name) +local function build(repo, parser) + local path = assert(repo.path, "repo not installed: " .. repo.name) local cwd = parser.location and vim.fs.joinpath(path, parser.location) or path local out = parser_so(path, parser.lang) @@ -88,7 +109,7 @@ local function build(state, parser) ) return end - vim.treesitter.language.add(parser.lang, { path = out }) + register(parser, out) activate_open_buffers(parser.lang) end @@ -137,13 +158,13 @@ local function build(state, parser) end end ----@param pack_name string +---@param name string ---@return string? lang ---@return string? err -local function lang_from_name(pack_name) - local lang = pack_name:match("^tree%-sitter%-(.+)$") +local function lang_from_name(name) + local lang = name:match("^tree%-sitter%-(.+)$") if not lang then - return nil, "cannot derive lang from pack name: " .. pack_name + return nil, "cannot derive lang from pack name: " .. name end return lang end @@ -166,34 +187,30 @@ local function pick(child, parent) return parent end ----@param entry string | ow.TS.RepoOpts +---@param spec string | ow.TS.Spec ---@return ow.TS.Repo? -local function normalize(entry) - ---@type ow.TS.RepoOpts - local opts - if type(entry) == "string" then - opts = { entry } --[[@as ow.TS.SingleRepoOpts]] - else - opts = entry +local function spec_to_repo(spec) + if type(spec) == "string" then + spec = { src = spec } --[[@as ow.TS.SingleSpec]] end - local name = name_from_url(opts[1]) + local name = name_from_url(spec.src) ---@type ow.TS.Parser[] local parsers = {} - local input_parsers = (opts --[[@as ow.TS.MultiRepoOpts]]).parsers - if input_parsers then - ---@cast opts ow.TS.MultiRepoOpts - for _, s in ipairs(opts.parsers) do + if spec.parsers then + ---@cast spec ow.TS.MultiSpec + for _, s in ipairs(spec.parsers) do table.insert(parsers, { lang = s.lang, location = s.location, - generate = pick(s.generate, opts.generate), - from_json = pick(s.from_json, opts.from_json), + generate = pick(s.generate, spec.generate), + from_json = pick(s.from_json, spec.from_json), + filetypes = s.filetypes, }) end else - ---@cast opts ow.TS.SingleRepoOpts - local lang = opts.lang + ---@cast spec ow.TS.SingleSpec + local lang = spec.lang if not lang then local derived, err = lang_from_name(name) if not derived then @@ -204,79 +221,80 @@ local function normalize(entry) end table.insert(parsers, { lang = lang, - location = opts.location, - generate = opts.generate, - from_json = opts.from_json, + location = spec.location, + generate = spec.generate, + from_json = spec.from_json, + filetypes = spec.filetypes, }) end return { - [1] = opts[1], + src = spec.src, name = name, - version = opts.version, + version = spec.version, parsers = parsers, + path = nil, } end ----@param opts (string | ow.TS.RepoOpts)[] -function M.setup(opts) - ---@type ow.TS.Repo[] +---@param specs (string | ow.TS.Spec)[] +function M.setup(specs) + ---@type table local repos = {} - for _, entry in ipairs(opts) do - local repo = normalize(entry) + local pack = require("pack") + for _, spec in ipairs(specs) do + local repo = spec_to_repo(spec) if repo then - table.insert(repos, repo) + repos[repo.src] = repo end end ---@type vim.pack.Spec[] - local pack_specs = vim.tbl_map(function(p) - return { src = p[1], name = p.name, version = p.version } - end, repos) - - ---@type RepoState[] - local states = {} - ---@type table - local by_src = {} - for _, repo in ipairs(repos) do - local state = { repo = repo } - table.insert(states, state) - by_src[repo[1]] = state + local vim_specs = {} + for _, repo in pairs(repos) do + table.insert( + vim_specs, + { src = repo.src, name = repo.name, version = repo.version } + ) end - local pack = require("pack") - local changed = pack.install(pack_specs, function(data) - local state = by_src[data.spec.src] - if state then - state.path = data.path + local changed = pack.install(vim_specs, function(data) + local repo = repos[data.spec.src] + if repo then + repo.path = data.path end end) - for _, state in ipairs(states) do - pack.register_hook(state.repo.name, function(ev) - state.path = ev.path - 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) + for _, repo in pairs(repos) do + if not repo.path then + log.error("Repo not installed: %s", repo.name) goto continue end - for _, parser in ipairs(state.repo.parsers) do - local so = parser_so(state.path, parser.lang) - if changed[state.repo.name] or not vim.uv.fs_stat(so) then - build(state, parser) + for _, parser in ipairs(repo.parsers) do + local so = parser_so(repo.path, parser.lang) + if changed[repo.src] or not vim.uv.fs_stat(so) then + build(repo, parser) else - vim.treesitter.language.add(parser.lang, { path = so }) + register(parser, so) end end ::continue:: 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) - 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