refactor(ts): rewrite around Repo/Parser types

This commit is contained in:
2026-04-21 23:56:41 +02:00
parent bb06f1818d
commit 4b278ef672
2 changed files with 151 additions and 184 deletions
+46 -79
View File
@@ -44,85 +44,52 @@ require("pack").setup({
}) })
require("ts").setup({ require("ts").setup({
parsers = { "https://github.com/tree-sitter/tree-sitter-bash",
"https://github.com/tree-sitter/tree-sitter-bash", -- 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", "https://github.com/stsewd/tree-sitter-comment",
"https://github.com/stsewd/tree-sitter-comment", "https://github.com/tree-sitter/tree-sitter-cpp",
"https://github.com/tree-sitter/tree-sitter-cpp", -- required by scss
-- required by scss "https://github.com/tree-sitter/tree-sitter-css",
"https://github.com/tree-sitter/tree-sitter-css", "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",
"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-json",
"https://github.com/tree-sitter-grammars/tree-sitter-luadoc",
{
"https://github.com/tree-sitter/tree-sitter-php",
specs = {
{ lang = "php", location = "php" },
{ lang = "php_only", location = "php_only" },
},
},
"https://github.com/tree-sitter/tree-sitter-python",
"https://github.com/tree-sitter/tree-sitter-rust",
"https://github.com/serenadeai/tree-sitter-scss",
{
"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",
specs = {
{ lang = "typescript", location = "typescript" },
{ lang = "tsx", location = "tsx" },
},
},
{
"https://github.com/tree-sitter-grammars/tree-sitter-xml",
specs = {
{ lang = "xml", location = "xml" },
{ lang = "dtd", location = "dtd" },
},
},
"https://github.com/tree-sitter-grammars/tree-sitter-yaml",
"https://github.com/georgeharker/tree-sitter-zsh",
}, },
languages = { "https://github.com/tree-sitter/tree-sitter-html",
"bash", "https://github.com/tree-sitter/tree-sitter-json",
"c", -- builtin "https://github.com/tree-sitter-grammars/tree-sitter-luadoc",
"cmake", {
"comment", "https://github.com/tree-sitter/tree-sitter-php",
"cpp", parsers = {
"css", { lang = "php", location = "php" },
"gitcommit", { lang = "php_only", location = "php_only" },
"go", },
"gotmpl",
"html",
"json",
"lua", -- builtin
"luadoc",
"markdown", -- builtin
"markdown_inline", -- builtin
"php",
"python",
"query", -- builtin
"rust",
"scss",
"sql",
"svelte",
"tsx",
"typescript",
"vim", -- builtin
"vimdoc", -- builtin
"xml",
"yaml",
"zsh",
}, },
"https://github.com/tree-sitter/tree-sitter-python",
"https://github.com/tree-sitter/tree-sitter-rust",
"https://github.com/serenadeai/tree-sitter-scss",
{
"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",
parsers = {
{ lang = "typescript", location = "typescript" },
{ lang = "tsx", location = "tsx" },
},
},
{
"https://github.com/tree-sitter-grammars/tree-sitter-xml",
parsers = {
{ lang = "xml", location = "xml" },
{ lang = "dtd", location = "dtd" },
},
},
"https://github.com/tree-sitter-grammars/tree-sitter-yaml",
"https://github.com/georgeharker/tree-sitter-zsh",
}) })
+105 -105
View File
@@ -2,33 +2,27 @@ local log = require("log")
local M = {} local M = {}
---@class ow.TS.ParserSpec ---@class ow.TS.Parser
---@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
---@class ow.TS.SingleParser ---@class ow.TS.RepoBase
---@field [1] string ---@field [1] string
---@field name? string
---@field version? string | vim.VersionRange ---@field version? string | vim.VersionRange
---@field lang? string inferred from URL (strip "tree-sitter-" prefix) if omitted
---@field location? string
---@field generate? boolean
---@field from_json? boolean
---@class ow.TS.MultiParser ---@class ow.TS.Repo : ow.TS.RepoBase
---@field [1] string
---@field name? string
---@field version? string | vim.VersionRange
---@field specs ow.TS.ParserSpec[]
---@alias ow.TS.ParserEntry string | ow.TS.SingleParser | ow.TS.MultiParser
---@class ow.TS.Parser
---@field name string ---@field name string
---@field path string ---@field parsers ow.TS.Parser[]
---@field specs ow.TS.ParserSpec[] ---@field path? string
---@class ow.TS.ParserOpts : ow.TS.Parser
---@field lang? string
---@class ow.TS.RepoOpts : ow.TS.RepoBase, ow.TS.ParserOpts
---@field name? string
---@field parsers? ow.TS.Parser[]
---@param path string ---@param path string
---@param lang string ---@param lang string
@@ -39,9 +33,7 @@ end
---@param buf integer ---@param buf integer
local function start_treesitter(buf) local function start_treesitter(buf)
local ok, err = pcall(vim.treesitter.start, buf) if not pcall(vim.treesitter.start, buf) then
if not ok then
log.error("Failed to enable treesitter for buffer %d: %s", buf, err)
return return
end end
for _, win in ipairs(vim.fn.win_findbuf(buf)) do for _, win in ipairs(vim.fn.win_findbuf(buf)) do
@@ -63,25 +55,26 @@ local function activate_open_buffers(lang)
end end
end end
---@param repo ow.TS.Repo
---@param parser ow.TS.Parser ---@param parser ow.TS.Parser
---@param spec ow.TS.ParserSpec function M.build(repo, parser)
function M.build(parser, spec) local path = assert(repo.path, "repo not installed: " .. repo.name)
local cwd = spec.location and vim.fs.joinpath(parser.path, spec.location) local cwd = parser.location and vim.fs.joinpath(path, parser.location)
or parser.path or path
local out = parser_so(parser.path, spec.lang) local out = parser_so(path, parser.lang)
vim.fn.mkdir(vim.fs.dirname(out), "p") vim.fn.mkdir(vim.fs.dirname(out), "p")
local function on_build(r) local function on_build(r)
if r.code ~= 0 then if r.code ~= 0 then
log.error( log.error(
"Failed to build parser for %s: %s", "Failed to build parser for %s: %s",
spec.lang, parser.lang,
r.stderr or "" r.stderr or ""
) )
return return
end end
vim.treesitter.language.add(spec.lang, { path = out }) vim.treesitter.language.add(parser.lang, { path = out })
activate_open_buffers(spec.lang) activate_open_buffers(parser.lang)
end end
local function do_build() local function do_build()
@@ -92,14 +85,14 @@ function M.build(parser, spec)
) )
end end
if spec.generate then if parser.generate then
local cmd = { local cmd = {
"tree-sitter", "tree-sitter",
"generate", "generate",
"--abi", "--abi",
tostring(vim.treesitter.language_version), tostring(vim.treesitter.language_version),
} }
if spec.from_json ~= false then if parser.from_json ~= false then
table.insert(cmd, "src/grammar.json") table.insert(cmd, "src/grammar.json")
end end
vim.system( vim.system(
@@ -112,7 +105,7 @@ function M.build(parser, spec)
if r.code ~= 0 then if r.code ~= 0 then
log.error( log.error(
"Failed to generate parser for %s: %s", "Failed to generate parser for %s: %s",
spec.lang, parser.lang,
r.stderr or "" r.stderr or ""
) )
return return
@@ -135,95 +128,102 @@ local function lang_from_name(pack_name)
return lang return lang
end end
---@param entry ow.TS.ParserEntry ---@param url string
---@return vim.pack.Spec ---@return string
local function entry_pack_spec(entry) local function name_from_url(url)
local name = url:match("([^/]+)$") or url
return (name:gsub("%.git$", ""))
end
---@param entry string | ow.TS.RepoOpts
---@return ow.TS.Repo
local function normalize(entry)
---@type ow.TS.RepoOpts
local opts
if type(entry) == "string" then if type(entry) == "string" then
return { src = entry } opts = { entry } --[[@as ow.TS.RepoOpts]]
end else
return { opts = entry
src = entry[1],
name = entry.name,
version = entry.version,
}
end
---@param entry ow.TS.ParserEntry
---@param pack_name string fallback for lang when the entry doesn't set it
---@return ow.TS.ParserSpec[]
local function entry_specs(entry, pack_name)
if type(entry) == "string" then
return { { lang = lang_from_name(pack_name) } }
end
if entry.specs then
return entry.specs
end
---@cast entry ow.TS.SingleParser
return {
{
lang = entry.lang or lang_from_name(pack_name),
location = entry.location,
generate = entry.generate,
from_json = entry.from_json,
},
}
end
---@param languages string[]
---@return string[]
local function collect_filetypes(languages)
local seen = {}
for _, lang in ipairs(languages) do
for _, ft in ipairs(vim.treesitter.language.get_filetypes(lang)) do
seen[ft] = true
end
end
return vim.tbl_keys(seen)
end
---@param opts { parsers: ow.TS.ParserEntry[], languages: string[] }
function M.setup(opts)
---@type vim.pack.Spec[]
local pack_specs = {}
---@type table<string, ow.TS.ParserEntry>
local entries_by_src = {}
for _, entry in ipairs(opts.parsers) do
local pack_spec = entry_pack_spec(entry)
table.insert(pack_specs, pack_spec)
entries_by_src[pack_spec.src] = entry
end end
local name = opts.name or name_from_url(opts[1])
---@type ow.TS.Parser[] ---@type ow.TS.Parser[]
local parsers = {} local parsers = {}
local changed = require("pack").install(pack_specs, function(data) local input_parsers = opts.parsers
if not data.spec.name then if input_parsers and vim.islist(input_parsers) then
log.error("Missing name for parser: %s", data.spec.src) for _, s in ipairs(input_parsers) do
return table.insert(parsers, {
end lang = s.lang,
local entry = entries_by_src[data.spec.src] location = s.location,
if not entry then generate = s.generate ~= nil and s.generate or opts.generate,
return from_json = s.from_json ~= nil and s.from_json
or opts.from_json,
})
end end
else
table.insert(parsers, { table.insert(parsers, {
name = data.spec.name, lang = opts.lang or lang_from_name(name),
path = data.path, location = opts.location,
specs = entry_specs(entry, data.spec.name), generate = opts.generate,
from_json = opts.from_json,
}) })
end
return {
[1] = opts[1],
name = name,
version = opts.version,
parsers = parsers,
}
end
---@param opts (string | ow.TS.RepoOpts)[]
function M.setup(opts)
---@type ow.TS.Repo[]
local repos = vim.tbl_map(normalize, opts)
---@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 table<string, ow.TS.Repo>
local by_src = {}
for _, p in ipairs(repos) do
by_src[p[1]] = p
end
local pack = require("pack")
local changed = pack.install(pack_specs, function(data)
local repo = by_src[data.spec.src]
if repo then
repo.path = data.path
end
end) end)
for _, parser in ipairs(parsers) do for _, repo in ipairs(repos) do
for _, spec in ipairs(parser.specs) do pack.register_hook(repo.name, function(ev)
local so = parser_so(parser.path, spec.lang) repo.path = ev.path
if changed[parser.name] or not vim.uv.fs_stat(so) then for _, parser in ipairs(repo.parsers) do
M.build(parser, spec) M.build(repo, parser)
end
end)
if not repo.path then
log.error("Repo not installed: %s", repo.name)
goto continue
end
for _, parser in ipairs(repo.parsers) do
local so = parser_so(repo.path, parser.lang)
if changed[repo.name] or not vim.uv.fs_stat(so) then
M.build(repo, parser)
else else
vim.treesitter.language.add(spec.lang, { path = so }) vim.treesitter.language.add(parser.lang, { path = so })
end end
end end
::continue::
end end
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = collect_filetypes(opts.languages),
callback = function(ev) callback = function(ev)
start_treesitter(ev.buf) start_treesitter(ev.buf)
end, end,