orgmode-babel.nvim/lua/orgmode-babel/init.lua
2025-04-16 21:44:39 -07:00

381 lines
8.8 KiB
Lua

local M = {
langs = {},
}
function M.setup(opts)
opts = opts or {}
M.langs = opts.langs and opts.langs or {}
M.load_paths = opts.load_paths and opts.load_paths or {}
M._here = vim.fn.fnamemodify(debug.getinfo(1).source:sub(2), ":p:h")
M._run_by_name = M._here .. "/run_by_name.el"
M._run_by_number = M._here .. "/run_by_number.el"
M._run_all = M._here .. "/run_all.el"
M._tangle_all = M._here .. "/tangle_all.el"
M._tangle_by_number = M._here .. "/tangle_by_number.el"
M._base_cmd = {
"emacs",
"--batch",
"--eval",
"(require 'org)",
"--eval",
"(require 'ob-tangle)",
"--eval",
"(setq make-backup-files nil)",
}
vim.list_extend(
M._base_cmd,
vim.fn.reduce(M.load_paths, function(acc, value)
vim.list_extend(acc, {
"--eval",
[[(add-to-list 'load-path "]] .. value .. [[")]],
})
return acc
end, {})
)
vim.list_extend(M._base_cmd, {
"--eval",
"(org-babel-do-load-languages 'org-babel-load-languages '(" .. vim.fn.reduce(M.langs, function(acc, value)
return acc .. "(" .. value .. " . t) "
end, "") .. "))",
})
end
local named_blocks_query = vim.treesitter.query.parse(
"org",
[[
(body
(block
directive: (directive
name: (expr) (#eq? "name")
value: (value) @name
)
name: (expr) (#eq? "src")
) @block
)
]]
)
local unnamed_blocks_query = vim.treesitter.query.parse(
"org",
[[
(body
(block
name: (expr) (#eq? "src")
) @block
)
]]
)
function M.get_names_in_buffer(bufnr, line1, line2)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local parser = vim.treesitter.get_parser(bufnr, "org", {})
local tree = parser:parse()[1]:root()
local root = tree:root()
local names = {}
local range = line1 ~= nil and line2 ~= nil
local single = range and line1 == line2
for _, match in named_blocks_query:iter_matches(root, bufnr, 0, -1) do
local passed = not range
local name
for id, nodes in pairs(match) do
for _, node in ipairs(nodes) do
local row1, col1, row2, col2 = node:range()
local text = vim.api.nvim_buf_get_text(bufnr, row1, col1, row2, col2, {})[1]
if named_blocks_query.captures[id] == "block" and not passed then
if single and line1 - 1 > row1 and line1 - 1 < row2 then
passed = true
elseif not single and line1 - 1 <= row1 and row2 <= line2 - 1 then
passed = true
end
end
if named_blocks_query.captures[id] == "name" then
name = text
end
end
if passed then
table.insert(names, name)
end
end
end
return names
end
function M.get_blocks_in_buffer(bufnr, line1, line2)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local parser = vim.treesitter.get_parser(bufnr, "org", {})
local tree = parser:parse()[1]:root()
local root = tree:root()
local indexes = {}
local range = line1 ~= nil and line2 ~= nil
local single = range and line1 == line2
local encountered = 0
for _, match in unnamed_blocks_query:iter_matches(root, bufnr, 0, -1) do
local passed = not range
for id, nodes in pairs(match) do
for _, node in ipairs(nodes) do
local row1, _, row2 = node:range()
if unnamed_blocks_query.captures[id] == "block" and not passed then
if single and line1 - 1 > row1 and line1 - 1 < row2 then
passed = true
elseif not single and line1 - 1 <= row1 and row2 <= line2 - 1 then
passed = true
end
end
end
end
if passed then
table.insert(indexes, encountered)
end
encountered = encountered + 1
end
return indexes
end
vim.api.nvim_create_user_command("OrgExecute", function(el)
local line1 = el.line1
local line2 = el.line2
local range = el.range
local arg = el.args
local bufnr = vim.api.nvim_get_current_buf()
local filename = vim.fn.expand(vim.api.nvim_buf_get_name(bufnr), ":p")
local total_lines = vim.api.nvim_buf_line_count(bufnr)
if vim.bo.modified then
vim.notify("Warning: OrgExecute in modified buffer!", vim.log.levels.WARN)
end
local script = M._run_all
local parameters = {}
local blocks = M.get_blocks_in_buffer(0, line1, line2)
local named_blocks = M.get_names_in_buffer(0, line1, line2)
if #blocks <= 0 then
vim.notify("No blocks", vim.log.levels.INFO)
return
end
if arg ~= "" then
if not vim.list_contains(named_blocks, arg) then
return vim.notify("Block not found (" .. arg .. ")", vim.log.levels.ERROR)
end
if el.bang then
vim.notify("Evaulating code block (" .. arg .. ") on your system", vim.log.levels.INFO)
elseif
vim.fn.input({
prompt = "Evaulate code block (" .. arg .. ") on your system? (y/N) ",
cancelreturn = "n",
}) ~= "y"
then
return
end
script = M._run_by_name
parameters = { arg }
elseif line1 == 1 and line2 == total_lines then
if el.bang then
vim.notify("Evaulating all blocks on your system", vim.log.levels.INFO)
elseif vim.fn.input({ prompt = "Evaulate all blocks on your system? (y/N) ", cancelreturn = "n" }) ~= "y" then
return
end
elseif range == 1 then
script = M._run_by_number
if #blocks > 1 then
vim.notify("Bailing on multiple blocks with single line range ?? (Report upstream)", vim.log.level.ERROR)
return
end
local name = ""
if #named_blocks > 0 then
name = "(" .. named_blocks[1] .. ") "
end
if el.bang then
vim.notify("Evaulating this code block " .. name .. "on your system", vim.log.levels.INFO)
elseif
vim.fn.input({
prompt = "Evaulate this code block " .. name .. "on your system? (y/N) ",
cancelreturn = "n",
}) ~= "y"
then
return
end
parameters = blocks
elseif range == 2 then
script = M._run_by_number
local names = M.get_names_in_buffer(0, line1, line2)
local names_string = #names > 0 and table.concat(names, ", ") or ""
local sep = #blocks > #names and #names > 0 and ", " or ""
local unnamed_string = #blocks > #names and #blocks - #names .. " unnamed" or ""
if el.bang then
vim.notify(
"Evaulating blocks (" .. names_string .. sep .. unnamed_string .. ") on your system",
vim.log.levels.INFO
)
elseif
vim.fn.input({
prompt = "Evaulate blocks (" .. names_string .. sep .. unnamed_string .. ") on your system? (y/N) ",
cancelreturn = "n",
}) ~= "y"
then
return
end
parameters = blocks
end
local cmd = vim.deepcopy(M._base_cmd)
vim.list_extend(cmd, {
"--load",
script,
filename,
})
vim.list_extend(cmd, parameters)
-- vim.notify(cmd, vim.log.levels.DEBUG)
local output = vim.fn.system(cmd)
vim.notify(output, vim.log.levels.DEBUG)
if not vim.bo[bufnr].modified then
vim.cmd(bufnr .. "bufdo edit")
end
end, {
nargs = "?",
range = "%",
bang = true,
complete = function()
return M.get_names_in_buffer()
end,
})
vim.api.nvim_create_user_command("OrgTangle", function(el)
local line1 = el.line1
local line2 = el.line2
local range = el.range
local bufnr = vim.api.nvim_get_current_buf()
local filename = vim.fn.expand(vim.api.nvim_buf_get_name(bufnr), ":p")
local total_lines = vim.api.nvim_buf_line_count(bufnr)
local script = M._tangle_all
local parameters = {}
local blocks = M.get_blocks_in_buffer(0, line1, line2)
if #blocks <= 0 then
vim.notify("No blocks", vim.log.levels.INFO)
return
end
if line1 == 1 and line2 == total_lines then
if el.bang then
vim.notify("Tangling whole file", vim.log.levels.INFO)
elseif vim.fn.input({ prompt = "Tangle whole file? (y/N) ", cancelreturn = "n" }) ~= "y" then
return
end
elseif range == 1 then
script = M._tangle_by_number
local named_blocks = M.get_names_in_buffer(0, line1, line2)
if #blocks > 1 then
vim.notify("Bailing on multiple blocks with single line range ?? (Report upstream)", vim.log.levinitOR)
return
end
local name = ""
if #named_blocks > 0 then
name = "(" .. named_blocks[1] .. ") "
end
if el.bang then
vim.notify("Tangling this code block " .. name, vim.log.levels.INFO)
elseif
vim.fn.input({
prompt = "Tangle this code block " .. name .. "? (y/N) ",
cancelreturn = "n",
}) ~= "y"
then
return
end
parameters = blocks
elseif range == 2 then
script = M._tangle_by_number
local names = M.get_names_in_buffer(0, line1, line2)
local names_string = #names > 0 and table.concat(names, ", ") or ""
local sep = #blocks > #names and #names > 0 and ", " or ""
local unnamed_string = #blocks > #names and #blocks - #names .. " unnamed" or ""
if el.bang then
vim.notify("Tangling blocks (" .. names_string .. sep .. unnamed_string .. ")", vim.log.levels.INFO)
elseif
vim.fn.input({
prompt = "Tangle blocks (" .. names_string .. sep .. unnamed_string .. ")? (y/N) ",
cancelreturn = "n",
}) ~= "y"
then
return
end
parameters = blocks
end
local cmd = vim.deepcopy(M._base_cmd)
vim.list_extend(cmd, {
"--load",
script,
filename,
})
vim.list_extend(cmd, parameters)
-- vim.notify(cmd, vim.log.levels.DEBUG)
local output = vim.fn.system(cmd)
vim.notify(output, vim.log.levels.DEBUG)
end, {
range = "%",
bang = true,
})
return M