local log = require("log") local M = {} ---@class ow.TS.Alias ---@field lang string ---@field filetypes? string[] ---@class ow.TS.Parser ---@field lang string ---@field location? string ---@field generate? boolean ---@field from_json? boolean ---@field filetypes? string[] ---@field aliases? ow.TS.Alias[] ---@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[] ---@field aliases? ow.TS.Alias[] ---@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 lang string ---@param so string ---@param filetypes? string[] ---@param symbol_name? string local function register_lang(lang, so, filetypes, symbol_name) vim.treesitter.language.add( lang, { path = so, symbol_name = symbol_name } ) local fts = { lang } if filetypes then vim.treesitter.language.register(lang, filetypes) vim.list_extend(fts, filetypes) end vim.api.nvim_create_autocmd("FileType", { group = vim.api.nvim_create_augroup( "ow.ts.parser." .. lang, { clear = true } ), pattern = fts, callback = function(ev) start_treesitter(ev.buf) end, }) end ---@class ow.TS.LangEntry ---@field lang string ---@field filetypes? string[] ---@field symbol_name? string ---@param parser ow.TS.Parser ---@return ow.TS.LangEntry[] local function parser_langs(parser) local entries = { { lang = parser.lang, filetypes = parser.filetypes }, } for _, alias in ipairs(parser.aliases or {}) do table.insert(entries, { lang = alias.lang, filetypes = alias.filetypes, symbol_name = parser.lang, }) end return entries end ---@param parser ow.TS.Parser ---@param so string local function register(parser, so) for _, e in ipairs(parser_langs(parser)) do register_lang(e.lang, so, e.filetypes, e.symbol_name) 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) for _, e in ipairs(parser_langs(parser)) do activate_open_buffers(e.lang) end 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, aliases = s.aliases, }) 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, aliases = spec.aliases, }) 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 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