This commit is contained in:
marshmallow 2023-06-25 01:23:27 +10:00
commit a870d79674
No known key found for this signature in database
GPG key ID: 767B8880F5AAEB9C
8 changed files with 604 additions and 0 deletions

131
README.org Normal file
View file

@ -0,0 +1,131 @@
* orgmode-babel.nvim
An experimental plugin that evaluates and tangles code blocks in
[[https://github.com/nvim-orgmode/orgmode][nvim-orgmode]] using
[[https://orgmode.org/worg/org-contrib/babel/][babel]] itself.
It uses ~emacs~ under the hood for perfect compatibility, but does not require
you to add anything extra to your ~init.el~.
[[https://github.com/mrshmllow/BetterRecipeBook/assets/40532058/b1ca7384-4bb3-47d8-9148-b85f3a2ea54a][Demonstration]]
** Requirements
- [[https://github.com/nvim-orgmode/orgmode][nvim-orgmode]]
- [[https://github.com/nvim-treesitter/nvim-treesitter][nvim-treesitter]]
- A working ~emacs~ installation (does not require configuration)
** Setup
*** lazy.nvim
#+begin_src lua
{
"mrshmllow/orgmode-babel.nvim",
dependencies = {
"nvim-orgmode/orgmode",
"nvim-treesitter/nvim-treesitter"
},
cmd = { "OrgExecute", "OrgTangle" },
opts = {
-- by default, none are enabled
langs = { "python", "lua", ... }
}
},
#+end_src
** Usage
All commands accept a ~!~ to skip confirmation.
*** ~:OrgExecute~
See [[https://orgmode.org/manual/Working-with-Source-Code.html][Working with
Source Code]] in the org manual.
#+begin_src
:OrgE[xecute]
#+end_src
Evaluates every block in buffer.
#+begin_src
:{range}OrgE[xecute]
#+end_src
Evaluate every block in range.
#+begin_src
:OrgE[xecute] [name]
#+end_src
Evaluate ~[name]~ block.
*** ~:OrgTangle~
See [[https://orgmode.org/manual/Extracting-Source-Code.html][Extracting Source
Code]] in the org manual.
#+begin_src
:OrgT[angle]
#+end_src
Tangles whole file.
#+begin_src
:{range}OrgT[angle]
#+end_src
Tangles all blocks in range. If the range is NOT ~%~, the tangled file will
likely only contain the contents of the last block, which is expected
behaviour.
#+begin_src
:OrgT[angle] [name]
#+end_src
Tangles ~[name]~ block.
** Advanced Configuration
*** Adding extra org-mode languages
Your emacs ~init.el~ will be sourced during execution of ~:OrgExecute~ and
~:OrgTangle~, so packages you install there that provide extra babel
languages will be available!
Follow the package's installation steps, and if they tell you to include it in
~org-babel-load-languages~, additionally make sure that you include it in
~opts.langs~.
**** Example
As an example, lets add [[https://github.com/arnm/ob-mermaid][ob-mermaid]] for
mermaid functionality in ~orgmode-babel.nvim~!
First, lets create a =~/.emacs.d/init.el=.
#+begin_src lisp init.el
; ~/.emacs.d/init.el
; Add the melpa package manager
(require 'package)
(add-to-list 'package-archives
'("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
; Install ob-mermaid
(unless (package-installed-p 'ob-mermaid)
(package-install 'ob-mermaid))
#+end_src
Then, in our plugin configuration, we can add ~mermaid~ to our ~opts.langs~.
#+begin_src lua
{
"mrshmllow/orgmode-babel.nvim",
...
opts = {
langs = { ..., "mermaid" }
}
},
#+end_src

367
lua/orgmode-babel/init.lua Normal file
View file

@ -0,0 +1,367 @@
local M = {
langs = {},
}
function M.setup(opts)
opts = opts or opts
M.langs = opts.langs and opts.langs 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_by_name = M._here .. "/tangle_by_name.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, {
"--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, node in pairs(match) 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
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, node in pairs(match) 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
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 = {}
if arg ~= "" then
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
local blocks = M.get_blocks_in_buffer(0, line1, line2)
local named_blocks = M.get_names_in_buffer(0, line1, line2)
if #blocks ~= 1 then
if #blocks > 1 then
vim.notify("Bailing on multiple blocks with single line range ?? (Report upstream)", vim.log.levinitOR)
else
vim.notify("No blocks", vim.log.levels.ERROR)
end
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 blocks = M.get_blocks_in_buffer(0, line1, line2)
local names = M.get_names_in_buffer(0, line1, line2)
if #blocks <= 0 then
return
end
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 ouput = vim.system(cmd):wait()
vim.print(ouput.stdout, ouput.stderr)
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 = {}
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 blocks = M.get_blocks_in_buffer(0, line1, line2)
local named_blocks = M.get_names_in_buffer(0, line1, line2)
if #blocks ~= 1 then
if #blocks > 1 then
vim.notify("Bailing on multiple blocks with single line range ?? (Report upstream)", vim.log.levinitOR)
else
vim.notify("No blocks", vim.log.levels.ERROR)
end
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 blocks = M.get_blocks_in_buffer(0, line1, line2)
local names = M.get_names_in_buffer(0, line1, line2)
if #blocks <= 0 then
return
end
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 ouput = vim.system(cmd):wait()
vim.print(ouput.stdout, ouput.stderr)
end, {
range = "%",
bang = true,
})
return M

View file

@ -0,0 +1,11 @@
(defun run-all-org-babel-blocks ()
"Run all org-mode code blocks in the current buffer."
(interactive)
(org-babel-execute-buffer))
(setq org-confirm-babel-evaluate nil)
(find-file (nth 0 command-line-args-left))
(run-all-org-babel-blocks)
(save-buffer)
(save-buffers-kill-emacs)

View file

@ -0,0 +1,17 @@
(defun run-specific-org-babel-block (block-name)
"Run a specific org-mode code block, specified by name."
(save-excursion
(goto-char (point-min))
(let ((case-fold-search t))
(when (search-forward (concat "#+NAME: " block-name) nil t)
(org-babel-execute-src-block)))))
(setq org-confirm-babel-evaluate nil)
(find-file (nth 0 command-line-args-left))
(dolist (block-name (nthcdr 1 command-line-args-left))
(run-specific-org-babel-block block-name))
(save-buffer)
(save-buffers-kill-emacs)

View file

@ -0,0 +1,19 @@
(defun run-specific-org-babel-block-by-number (block-number)
"Run a specific org-mode code block, specified by number."
(save-excursion
(goto-char (point-min))
(let ((counter -1))
(while (re-search-forward "#\\+begin_src" nil t)
(setq counter (1+ counter))
(when (= counter block-number)
(org-babel-execute-src-block))))))
(setq org-confirm-babel-evaluate nil)
(find-file (nth 0 command-line-args-left))
(dolist (block-number (mapcar #'string-to-number (nthcdr 1 command-line-args-left)))
(run-specific-org-babel-block-by-number block-number))
(save-buffer)
(save-buffers-kill-emacs)

View file

@ -0,0 +1,22 @@
(defun tangle-org-babel-blocks (&optional block-names)
"Tangle org-mode code blocks in the current buffer. If BLOCK-NAMES is provided, only tangle those blocks."
(interactive)
(if block-names
(dolist (block block-names)
(save-restriction
(goto-char (point-min))
(while (re-search-forward (concat "#\\+NAME: " (regexp-quote block)) nil t)
(org-babel-tangle-block))))
(org-babel-tangle)))
(setq org-confirm-babel-evaluate nil)
(find-file (nth 0 command-line-args-left))
(let ((blocks (cdr command-line-args-left)))
(if blocks
(tangle-org-babel-blocks blocks)
(tangle-org-babel-blocks)))
(save-buffer)
(save-buffers-kill-emacs)

View file

@ -0,0 +1,18 @@
(defun tangle-block-by-name (block-name)
"Tangle the org-babel code block with name BLOCK-NAME."
(save-excursion
(goto-char (point-min))
(while (re-search-forward (format "#\\+NAME: %s" block-name) nil t)
(if (org-babel-get-src-block-info)
(org-babel-tangle-collect-block)))))
(setq org-confirm-babel-evaluate nil)
(find-file (nth 0 command-line-args-left))
(dolist (block-name (nthcdr 1 command-line-args-left))
(tangle-block-by-name block-name))
(save-buffer)
(save-buffers-kill-emacs)

View file

@ -0,0 +1,19 @@
(defun tangle-specific-org-babel-block-by-number (block-number)
"Tangle a specific org-mode code block, specified by number."
(save-excursion
(goto-char (point-min))
(let ((counter -1))
(while (re-search-forward "#\\+begin_src" nil t)
(setq counter (1+ counter))
(when (= counter block-number)
(org-babel-tangle '(4)))))))
(setq org-confirm-babel-evaluate nil)
(find-file (nth 0 command-line-args-left))
(dolist (block-number (mapcar #'string-to-number (nthcdr 1 command-line-args-left)))
(tangle-specific-org-babel-block-by-number block-number))
(save-buffer)
(save-buffers-kill-emacs)