Transclusion (org-transclusion)
Stability: experimental (org-extensions-stability)
transclusion shows text from other files, or other parts of the same file, where a #+transclude: keyword points to, like Emacs org-transclusion and with its keyword syntax:
require("org").setup({ extensions = { transclusion = {} } })
* Meeting notes
#+transclude: [[file:projects.org::*Launch plan]] :level 2
#+transclude: [[file:src/main.py]] :lines 10-24 :src python
The link is required and comes first; properties follow it:
:level Nshift the text's headlines so the first is at level N;:levelalone puts it one below the keyword's headline:only-contentsleave out the headlines, keep their text:no-first-headingleave out the first headline only:exclude-elements "a b"leave out elements of these types (drawer,property-drawer,keyword,planning,src-block,table,comment, ...):expand-linksmake relativefile:links absolute:lines A-Blines A to B, both included, counted from the search target (from the file start without one);A-and-Bare open ranges:end "text"end before the first line after the start that matches text (/regexp/too); when nothing matches, the end of:linesapplies:thing-at-point THING(or:thingatpt) end after the THING that starts at the search target:sexp,list,defun,paragraph,line,word,symbolorsentence;:end "N"takes N of them:noweb-chunkthe search option names a noweb chunk: the lines after<<name>>=up to the next@or chunk:src LANGwrap the text in a#+begin_src LANGblock;:rest "ARGS"adds header arguments:disable-autonot shown or inserted automatically, only bytransclusion_add(and not exported)
Links can be file: links with a search option (::*Heading, ::#custom-id, ::name of a #+NAME: element or a <<target>> paragraph, ::/regexp/ or ::N for text files), id: links, or links into the same file ([[*Heading]]). A heading's whole subtree is taken, without its property drawer (exclude_elements). Text files come whole, from their search target to the end, or by :lines, :end or :thing-at-point, indented like the keyword; binary files are refused. Like org-transclusion, :lines, :end and :thing-at-point on an Org file take its lines as they are (no element left out, headlines as they are, except that an id: link still follows :level). #+transclude: lines inside the transcluded text are expanded too, max_depth deep; a transclusion that would include itself is left as a keyword and reported.
Virtual and inserted
By default (mode = "virtual") transclusions are drawn as virtual lines under their keyword, with a │ border, org highlighting (and tree-sitter colours for code) and the source at the end of the keyword line. The buffer text is untouched, so nothing can leak into the file, and the lines fold away with the keyword's headline.
transclusion_add inserts the text into the buffer instead, like Emacs: its lines get a sign in the sign column, the keyword line says "(inserted)", and folding, search and the agenda see them. Inserted text is read-only: an edit in place is put back (edit the source instead, see below), lines you type or paste among it are kept below it, deleting the whole text removes the transclusion, and deleting the keyword removes its text. Each inserted line carries a mark, so undo and redo bring the transclusion back with its text; while they walk the history, the text they bring back is left as it is. mode = "materialized" inserts every transclusion when a buffer is opened (org-transclusion-add-all-on-activate, and again after :e!); mode = false shows nothing until you ask.
The file only ever holds the keywords: inserted text is taken out while the buffer is written and put back afterwards (org-transclusion's before- and after-save hooks). That covers :w, :w {file}, :saveas, :wq, :w >> {file}, :{range}w {file} (the inserted lines of the range are left out) and org.nvim's own writes (refile, capture, archive, ...). A write that fails, or that another BufWritePre autocommand stops, puts the text back once the command is over. Autocommands run in the order they were defined: a formatter that runs before this extension's BufWritePre sees the inserted text, and the text is found again after it as long as the formatter left it as it was (conform.nvim and LSP formatting change only the lines that differ). :noautocmd write, and a write run from inside another autocommand that isn't ++nested (some auto-save plugins), skip all of this: the file is cleaned again when you leave the buffer, stay idle (CursorHold) or Neovim loses focus, but not after :noautocmd wq.
Transclusions are drawn when an org buffer is shown, redrawn when the buffer or a source buffer changes (live) or a source is written, and, for source files that aren't loaded, when they change on disk (watch, only while a buffer showing them is loaded; one watcher per directory, at most 64). Only the transclusions whose source changed are drawn again.
Editing the source
transclusion_edit (<CR> on a keyword or inserted text, edit_key) opens the source lines in a float (org-transclusion-live-sync-start): the whole subtree with its drawers, or the lines of a code file with its filetype. :w writes them back into the source: into its buffer when it is loaded (and writes that buffer when it had no other unsaved changes), else into the file, keeping its line endings. Every transclusion of the source is then redrawn and inserted copies are updated. If the source changed meanwhile so the text can't be found, nothing is written. <Esc> (in Normal mode) closes the float; it refuses while there are unwritten edits (:w them, or :q! to drop them). With edit = { live = true } the text goes into the source buffer as you type (the source is loaded if it isn't), so every transclusion of it follows, like org-transclusion-live-sync; the file is still written by :w. Elsewhere <CR> keeps its meaning.
Actions and keys (in org buffers)
<prefix>uatransclusion_add insert the text at point<prefix>uAtransclusion_add_all insert every transclusion<prefix>udtransclusion_remove take out the inserted text; on a virtual one, hide it<prefix>uDtransclusion_remove_all take out every inserted text<prefix>ugtransclusion_refresh re-read the sources, show hidden ones again<prefix>uetransclusion_edit edit the source (also<CR>)<prefix>uotransclusion_open_source visit the source (open_source)<prefix>uttransclusion_toggle virtual transclusions on/off<prefix>uctransclusion_detach replace the transclusion by a copy of its text<prefix>ultransclusion_make_from_link add a#+transclude:for the link at point<prefix>u<transclusion_promote:levelone up<prefix>u>transclusion_demote:levelone down:Org transclusion_insert [[link]] :level 2adds a keyword below the cursor (below the inserted text when the cursor is in it); it completesfile:paths and the properties.
Export
#+transclude: keywords are expanded when a buffer is exported, before #+INCLUDE: keywords, except :disable-auto ones (export = false turns this off). Inserted text is not exported twice.
For other extensions and plugins
Inserted text is in the buffer, so the parser and the agenda see it, as in Emacs. Code that indexes or checks what is in the file (a language server, a linter, an index of links) should skip it:
local ok, t = pcall(require, "org.extensions.transclusion")
for _, r in ipairs(ok and t.ranges(bufnr) or {}) do
-- lines r.first .. r.last (1-based, both included) are inserted
-- text; r.keyword is the line of its #+transclude:
end
ranges(bufnr) returns an empty list when nothing is inserted or the extension is off. clean_lines(bufnr) returns the lines as the file holds them (and a map from those lines to buffer lines), and without_inserted(bufnr, fn) runs fn (a write that skips autocommands) with the inserted text taken out.
Options
mode"virtual", "materialized" or false ("virtual")exclude_elementselement types always left out ({ "property-drawer" })include_first_sectionkeep the text before the first headline of a whole file (true)nestedexpand keywords in transcluded text (true)max_depthnesting limit (5)max_virtual_lineslongest virtual transclusion (400)borderleft border of virtual lines ("│ ")signsign on inserted lines, false for none ("▎")show_sourcesource at the end of the keyword line (true)liveredraw as source buffers change (true)watchwatch unloaded source files (true)debouncemilliseconds before redrawing (150)exportexpand keywords on export (true)edit_keykey editing the transclusion at point ("<CR>")edit{ window = "float" or an Ex command such as "split", width, height, border, live = false }open_source"edit", "split", "vsplit" or "tab" ("split")
Highlights: OrgTransclusionBorder, OrgTransclusionSource, OrgTransclusionSign, OrgTransclusionError.
Differences from Emacs org-transclusion
- Virtual lines are the default; Emacs always inserts the text.
- Inserted text keeps its keyword line above it (Emacs hides the keyword while the text is shown).
- Editing happens in a float written with
:w; withedit.livethe source buffer follows as you type, but the float is a copy, not an overlay on the source. - Nested keywords are expanded as the text is drawn; Emacs expands them when you add them.
:thing-at-pointfinds things by brackets, blank lines and indentation, not by the source's major mode, and:endis a text search (or/regexp/), not any Org search option.- Identical text right under a keyword that lost its marks (a formatter rewrote the buffer, or undo from an earlier session with 'undofile') is taken back as that keyword's inserted text.
- org-roam doesn't index links in
#+transclude:lines (like org-roam), and inserted text is never in the file, so it isn't indexed either.