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[] ---@class RepoState ---@field repo ow.TS.Repo ---@field path? string ---@class ow.TS.SingleRepoOpts : ow.TS.RepoBase ---@field lang? string ---@field location? string ---@field generate? boolean ---@field from_json? boolean ---@class ow.TS.MultiRepoOpts : 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 ---@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 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 state RepoState ---@param parser ow.TS.Parser local function build(state, parser) local path = assert(state.path, "repo not installed: " .. state.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? lang ---@return string? err local function lang_from_name(pack_name) local lang = pack_name:match("^tree%-sitter%-(.+)$") if not lang then return nil, "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.SingleRepoOpts]] else opts = entry end local name = name_from_url(opts[1]) ---@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 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 ---@cast opts ow.TS.SingleRepoOpts local lang = opts.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 = 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 = {} for _, entry in ipairs(opts) do local repo = normalize(entry) if repo then table.insert(repos, 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 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 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) 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) 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