303 lines
7.9 KiB
Lua
303 lines
7.9 KiB
Lua
local log = require("log")
|
|
|
|
local M = {}
|
|
|
|
---@class ow.TS.Parser
|
|
---@field lang string
|
|
---@field location? string
|
|
---@field generate? boolean
|
|
---@field from_json? boolean
|
|
---@field filetypes? string[]
|
|
|
|
---@class ow.TS.RepoBase
|
|
---@field src string
|
|
---@field version? string | vim.VersionRange
|
|
|
|
---@class ow.TS.Repo : ow.TS.RepoBase
|
|
---@field name string
|
|
---@field parsers ow.TS.Parser[]
|
|
---@field path? string
|
|
|
|
---@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.MultiSpec : ow.TS.RepoBase
|
|
---@field parsers ow.TS.Parser[]
|
|
---@field generate? boolean
|
|
---@field from_json? boolean
|
|
|
|
---@alias ow.TS.Spec ow.TS.SingleSpec | ow.TS.MultiSpec
|
|
|
|
---@param path string
|
|
---@param lang string
|
|
---@return string
|
|
local function parser_so(path, lang)
|
|
return vim.fs.joinpath(path, "parser", lang .. ".so")
|
|
end
|
|
|
|
---@param buf integer
|
|
local function start_treesitter(buf)
|
|
if not pcall(vim.treesitter.start, buf) then
|
|
return
|
|
end
|
|
for _, win in ipairs(vim.fn.win_findbuf(buf)) do
|
|
if not vim.wo[win].diff then
|
|
vim.wo[win].foldmethod = "expr"
|
|
vim.wo[win].foldexpr = "v:lua.vim.treesitter.foldexpr()"
|
|
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
|
|
local function activate_open_buffers(lang)
|
|
local fts = vim.treesitter.language.get_filetypes(lang)
|
|
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
|
if
|
|
vim.api.nvim_buf_is_loaded(buf)
|
|
and vim.list_contains(fts, vim.bo[buf].filetype)
|
|
then
|
|
start_treesitter(buf)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param repo ow.TS.Repo
|
|
---@param parser ow.TS.Parser
|
|
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)
|
|
vim.fn.mkdir(vim.fs.dirname(out), "p")
|
|
|
|
local function on_build(r)
|
|
if r.code ~= 0 then
|
|
local detail = r.stderr ~= "" and r.stderr
|
|
or r.stdout ~= "" and r.stdout
|
|
or "(no output)"
|
|
log.error(
|
|
"Failed to build parser for %s (exit %d): %s",
|
|
parser.lang,
|
|
r.code,
|
|
detail
|
|
)
|
|
return
|
|
end
|
|
register(parser, out)
|
|
activate_open_buffers(parser.lang)
|
|
end
|
|
|
|
local function do_build()
|
|
vim.system(
|
|
{ "tree-sitter", "build", "-o", out },
|
|
{ cwd = cwd },
|
|
vim.schedule_wrap(on_build)
|
|
)
|
|
end
|
|
|
|
if parser.generate then
|
|
local cmd = {
|
|
"tree-sitter",
|
|
"generate",
|
|
"--abi",
|
|
tostring(vim.treesitter.language_version),
|
|
}
|
|
if parser.from_json ~= false then
|
|
table.insert(cmd, "src/grammar.json")
|
|
end
|
|
vim.system(
|
|
cmd,
|
|
{
|
|
cwd = cwd,
|
|
env = { TREE_SITTER_JS_RUNTIME = "native" },
|
|
},
|
|
vim.schedule_wrap(function(r)
|
|
if r.code ~= 0 then
|
|
local detail = r.stderr ~= "" and r.stderr
|
|
or r.stdout ~= "" and r.stdout
|
|
or "(no output)"
|
|
log.error(
|
|
"Failed to generate parser for %s (exit %d): %s",
|
|
parser.lang,
|
|
r.code,
|
|
detail
|
|
)
|
|
return
|
|
end
|
|
do_build()
|
|
end)
|
|
)
|
|
else
|
|
do_build()
|
|
end
|
|
end
|
|
|
|
---@param name string
|
|
---@return string? lang
|
|
---@return string? err
|
|
local function lang_from_name(name)
|
|
local lang = name:match("^tree%-sitter%-(.+)$")
|
|
if not lang then
|
|
return nil, "cannot derive lang from pack name: " .. name
|
|
end
|
|
return lang
|
|
end
|
|
|
|
---@param url string
|
|
---@return string
|
|
local function name_from_url(url)
|
|
local name = url:match("([^/]+)$") or url
|
|
return (name:gsub("%.git$", ""))
|
|
end
|
|
|
|
---@generic T
|
|
---@param child T?
|
|
---@param parent T?
|
|
---@return T?
|
|
local function pick(child, parent)
|
|
if child ~= nil then
|
|
return child
|
|
end
|
|
return parent
|
|
end
|
|
|
|
---@param spec string | ow.TS.Spec
|
|
---@return ow.TS.Repo?
|
|
local function spec_to_repo(spec)
|
|
if type(spec) == "string" then
|
|
spec = { src = spec } --[[@as ow.TS.SingleSpec]]
|
|
end
|
|
local name = name_from_url(spec.src)
|
|
|
|
---@type ow.TS.Parser[]
|
|
local parsers = {}
|
|
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, spec.generate),
|
|
from_json = pick(s.from_json, spec.from_json),
|
|
filetypes = s.filetypes,
|
|
})
|
|
end
|
|
else
|
|
---@cast spec ow.TS.SingleSpec
|
|
local lang = spec.lang
|
|
if not lang then
|
|
local derived, err = lang_from_name(name)
|
|
if not derived then
|
|
log.error("Skipping %s: %s", name, err)
|
|
return nil
|
|
end
|
|
lang = derived
|
|
end
|
|
table.insert(parsers, {
|
|
lang = lang,
|
|
location = spec.location,
|
|
generate = spec.generate,
|
|
from_json = spec.from_json,
|
|
filetypes = spec.filetypes,
|
|
})
|
|
end
|
|
|
|
return {
|
|
src = spec.src,
|
|
name = name,
|
|
version = spec.version,
|
|
parsers = parsers,
|
|
path = nil,
|
|
}
|
|
end
|
|
|
|
---@param specs (string | ow.TS.Spec)[]
|
|
function M.setup(specs)
|
|
---@type table<string, ow.TS.Repo>
|
|
local repos = {}
|
|
local pack = require("pack")
|
|
for _, spec in ipairs(specs) do
|
|
local repo = spec_to_repo(spec)
|
|
if repo then
|
|
repos[repo.src] = repo
|
|
end
|
|
end
|
|
|
|
---@type vim.pack.Spec[]
|
|
local vim_specs = {}
|
|
for _, repo in pairs(repos) do
|
|
table.insert(
|
|
vim_specs,
|
|
{ src = repo.src, name = repo.name, version = repo.version }
|
|
)
|
|
end
|
|
|
|
local changed = pack.install(vim_specs, function(data)
|
|
local repo = repos[data.spec.src]
|
|
if repo then
|
|
repo.path = data.path
|
|
end
|
|
end)
|
|
|
|
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(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
|
|
register(parser, so)
|
|
end
|
|
end
|
|
::continue::
|
|
end
|
|
|
|
vim.api.nvim_create_autocmd("PackChanged", {
|
|
group = vim.api.nvim_create_augroup("ow.ts.updates", { clear = true }),
|
|
callback = function(ev)
|
|
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
|
|
|
|
return M
|