commit a870d7967434ff21b15c09f348bc03ea1542cc02 Author: marshmallow Date: Sun Jun 25 01:23:27 2023 +1000 init diff --git a/README.org b/README.org new file mode 100644 index 0000000..4e3dbac --- /dev/null +++ b/README.org @@ -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 diff --git a/lua/orgmode-babel/init.lua b/lua/orgmode-babel/init.lua new file mode 100644 index 0000000..a0da0f7 --- /dev/null +++ b/lua/orgmode-babel/init.lua @@ -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 diff --git a/lua/orgmode-babel/run_all.el b/lua/orgmode-babel/run_all.el new file mode 100644 index 0000000..441a327 --- /dev/null +++ b/lua/orgmode-babel/run_all.el @@ -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) + diff --git a/lua/orgmode-babel/run_by_name.el b/lua/orgmode-babel/run_by_name.el new file mode 100644 index 0000000..5485e9b --- /dev/null +++ b/lua/orgmode-babel/run_by_name.el @@ -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) + diff --git a/lua/orgmode-babel/run_by_number.el b/lua/orgmode-babel/run_by_number.el new file mode 100644 index 0000000..dbb10de --- /dev/null +++ b/lua/orgmode-babel/run_by_number.el @@ -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) + diff --git a/lua/orgmode-babel/tangle_all.el b/lua/orgmode-babel/tangle_all.el new file mode 100644 index 0000000..1abb04b --- /dev/null +++ b/lua/orgmode-babel/tangle_all.el @@ -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) + diff --git a/lua/orgmode-babel/tangle_by_name.el b/lua/orgmode-babel/tangle_by_name.el new file mode 100644 index 0000000..c0fad1c --- /dev/null +++ b/lua/orgmode-babel/tangle_by_name.el @@ -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) + diff --git a/lua/orgmode-babel/tangle_by_number.el b/lua/orgmode-babel/tangle_by_number.el new file mode 100644 index 0000000..5587e25 --- /dev/null +++ b/lua/orgmode-babel/tangle_by_number.el @@ -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) +