init
This commit is contained in:
commit
a870d79674
8 changed files with 604 additions and 0 deletions
131
README.org
Normal file
131
README.org
Normal 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
367
lua/orgmode-babel/init.lua
Normal 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
|
||||
11
lua/orgmode-babel/run_all.el
Normal file
11
lua/orgmode-babel/run_all.el
Normal 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)
|
||||
|
||||
17
lua/orgmode-babel/run_by_name.el
Normal file
17
lua/orgmode-babel/run_by_name.el
Normal 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)
|
||||
|
||||
19
lua/orgmode-babel/run_by_number.el
Normal file
19
lua/orgmode-babel/run_by_number.el
Normal 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)
|
||||
|
||||
22
lua/orgmode-babel/tangle_all.el
Normal file
22
lua/orgmode-babel/tangle_all.el
Normal 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)
|
||||
|
||||
18
lua/orgmode-babel/tangle_by_name.el
Normal file
18
lua/orgmode-babel/tangle_by_name.el
Normal 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)
|
||||
|
||||
19
lua/orgmode-babel/tangle_by_number.el
Normal file
19
lua/orgmode-babel/tangle_by_number.el
Normal 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)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue