Files
nvim/lua/ts.lua
T

252 lines
6.6 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
---@class ow.TS.RepoBase
---@field [1] 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.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 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
vim.wo[win].foldmethod = "expr"
vim.wo[win].foldexpr = "v:lua.vim.treesitter.foldexpr()"
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
vim.treesitter.language.add(parser.lang, { path = 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 pack_name string
---@return string
local function lang_from_name(pack_name)
local lang = pack_name:match("^tree%-sitter%-(.+)$")
if not lang then
error("cannot derive lang from pack name: " .. pack_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 entry string | ow.TS.RepoOpts
---@return ow.TS.Repo
local function normalize(entry)
---@type ow.TS.RepoOpts
local opts
if type(entry) == "string" then
opts = { entry } --[[@as ow.TS.RepoOpts]]
else
opts = entry
end
local name = opts.name or name_from_url(opts[1])
---@type ow.TS.Parser[]
local parsers = {}
local input_parsers = opts.parsers
if input_parsers and vim.islist(input_parsers) then
for _, s in ipairs(input_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),
})
end
else
table.insert(parsers, {
lang = opts.lang or lang_from_name(name),
location = opts.location,
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)
for _, repo in ipairs(repos) do
pack.register_hook(repo.name, function(ev)
repo.path = ev.path
for _, parser in ipairs(repo.parsers) do
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
build(repo, parser)
else
vim.treesitter.language.add(parser.lang, { path = so })
end
end
::continue::
end
vim.api.nvim_create_autocmd("FileType", {
callback = function(ev)
start_treesitter(ev.buf)
end,
})
end
return M