Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/lua/bugabinga/treesitter.lua

Raw
local puk = require 'std.puk'
local auto = require 'std.auto'
local ignored = require 'std.ignored'
local dbg = require 'std.dbg'

local add_dependency = require 'bugabinga.health'.add_dependency
add_dependency { name_of_executable = 'zig' }
add_dependency { name = 'TreeSitter CLI', name_of_executable = 'tree-sitter' }
add_dependency { name_of_executable = 'tar' }
add_dependency { name_of_executable = 'curl' }

local function is_ignored( match )
	for _, v in ipairs( ignored.filetypes ) do
		if v == match or match:match( v ) then
			return true
		end
	end
	return false
end

local treesitter = puk 'nvim-treesitter'
treesitter.setup {}
treesitter.install { 'bash', 'nu', 'ini' }

-- TODO: in 0.12 check if this behaviour is builtin on vim.treesitter.start()
-- if so, remove my stuff
auto 'look_for_treesitter_parser' {
	description = 'Try loading a treesitter parser, if available.',
	events = 'FileType',
	pattern = '*',
	command = function ( data )
		local match = data.match
		if match == 'text' then return end
		if is_ignored( match ) then
			dbg( 'Ignore %s for treesitter.', match )
			return
		end
		local ok, _ = pcall( vim.treesitter.start, data.buf )
		if not ok then
			-- resolve filetype to its treesitter language (sh -> bash, quadlet -> ini,
			-- cucumber -> gherkin) before consulting the registry
			local lang = vim.treesitter.language.get_lang( match ) or match
			local available = require 'nvim-treesitter.parsers'
			local potential_parser = available[lang]
			if potential_parser then
				vim.notify( 'Found potential treesitter parser for current filetype. Trying to install it. ' .. lang )
				local task = require 'nvim-treesitter.install'.install( lang, { force = false } )
				task:await( function ( err, success )
					if not success then
						vim.notify( 'Failed to install treesitter parser: ' .. lang, vim.log.levels.ERROR )
						return
					end
					vim.schedule( function ()
						if pcall( vim.treesitter.start, data.buf ) then
							vim.opt_local.foldmethod = 'expr'
							vim.opt_local.foldexpr = 'v:lua.vim.treesitter.foldexpr()'
						else
							-- revision marker lied (e.g. .so wiped out of band): force rebuild once
							local repair = require 'nvim-treesitter.install'.install( { lang }, { force = true } )
							repair:await( function ( _, repaired )
								if repaired then
									vim.schedule( function()
										pcall( vim.treesitter.start, data.buf )
										vim.opt_local.foldmethod = 'expr'
										vim.opt_local.foldexpr = 'v:lua.vim.treesitter.foldexpr()'
									end )
								end
							end )
						end
					end )
				end )
			end
			return
		end
		vim.opt_local.foldmethod = 'expr'
		vim.opt_local.foldexpr = 'v:lua.vim.treesitter.foldexpr()'
	end,
}
-- add d2 parser; the .so and queries are installed out of band by puk (puk.parsers.nuon)
vim.treesitter.language.register( 'd2', 'd2' )
-- systemd units and podman quadlets are INI-dialect files
vim.treesitter.language.register( 'ini', 'systemd' )
-- podman quadlet units get their own semantic filetype, same INI grammar
vim.treesitter.language.register( 'ini', 'quadlet' )
-- sh files are POSIX-ish, the bash grammar covers them
vim.treesitter.language.register( 'bash', 'sh' )
-- opentofu files are HCL
vim.treesitter.language.register( 'hcl', { 'opentofu', 'opentofu-vars' } )
-- nvim calls the gherkin filetype 'cucumber'
vim.treesitter.language.register( 'gherkin', 'cucumber' )

-- surface treesitter parse errors as ERROR diagnostics in every language;
-- refreshed on change/insert-leave/save, so typing stays flicker-free
local ts_errors = vim.api.nvim_create_namespace 'treesitter-errors'
local function collect_errors( node, out )
	if node:type() == 'ERROR' or node:missing() then
		local lnum, col = node:range()
		out[#out + 1] = { lnum = lnum, col = col, end_lnum = lnum, end_col = col, message = 'syntax error', severity = vim.diagnostic.severity.ERROR, source = 'treesitter' }
		return
	end
	for child in node:iter_children() do
		collect_errors( child, out )
	end
end
local function show_ts_errors( args )
	local ok, parser = pcall( vim.treesitter.get_parser, args.buf )
	if not ok or not parser then return end
	local trees = parser:parse()
	if not trees then return end
	local tree = trees[1]
	if not tree then return end
	local out = {}
	local root = tree:root()
	if not root then return end
	collect_errors( root, out )
	vim.diagnostic.set( ts_errors, args.buf, out )
end
auto 'treesitter_error_diagnostics' {
	description = 'Report treesitter parse errors as ERROR diagnostics.',
	events = { 'FileType', 'TextChanged', 'InsertLeave', 'BufWritePost' },
	pattern = '*',
	command = function( args )
		vim.schedule( function() show_ts_errors( args ) end )
	end,
}

-- this module loads deferred, so the startup file's FileType event has already
-- passed: catch up on every loaded buffer once
vim.schedule( function()
	for _, buf in ipairs( vim.api.nvim_list_bufs() ) do
		if vim.api.nvim_buf_is_loaded( buf ) then
			show_ts_errors( { buf = buf } )
		end
	end
end )

-- runs out of band via puk post-install (headless nvim, editor closed):
-- updates nvim-treesitter-managed parsers to the revisions pinned upstream.
local function post_install()
	require 'nvim-treesitter.install'.update():wait()
end

_G.BugabingaTreesitter = {
	is_ignored = is_ignored,
	post_install = post_install,
}

return BugabingaTreesitter