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 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