commit 55a0843b7cf73d90024733d243e93876476f1746
parent 16c133439958acc0656204d7127b815d268e51d4
Author: benarcher2691 <ben.archer2691@gmail.com>
Date: Thu, 8 Jan 2026 02:20:53 +0100
feat(editor): :source can run Lua codeblock / ts injection #36799
Problem:
Can't use `:source` to run a Lua codeblock (treesitter injection) in
a help (vimdoc) file.
Solution:
Use treesitter to parse the range and treat it as Lua if detected as
such.
Diffstat:
4 files changed, 68 insertions(+), 1 deletion(-)
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -215,6 +215,8 @@ EDITOR
"(v)iew" then run `:trust`.
• |gx| in help buffers opens the online documentation for the tag under the
cursor.
+• |:source| with a range in non-Lua files (e.g., vimdoc) now detects Lua
+ codeblocks via treesitter and executes them as Lua instead of Vimscript.
• |:Undotree| for visually navigating the |undo-tree|
• |:wall| permits a |++p| option for creating parent directories when writing
changed buffers.
diff --git a/runtime/lua/vim/_core/util.lua b/runtime/lua/vim/_core/util.lua
@@ -63,4 +63,22 @@ function M.read_chunk(file, size)
return tostring(chunk)
end
+--- Check if a range in a buffer is inside a Lua codeblock via treesitter injection.
+--- Used by :source to detect Lua code in non-Lua files (e.g., vimdoc).
+--- @param bufnr integer Buffer number
+--- @param line1 integer Start line (1-indexed)
+--- @param line2 integer End line (1-indexed)
+--- @return boolean True if the range is in a Lua injection
+function M.source_is_lua(bufnr, line1, line2)
+ local ok, parser = pcall(vim.treesitter.get_parser, bufnr)
+ if not ok or not parser then
+ return false
+ end
+ -- Parse from buffer start through one line past line2 to include injection closing markers
+ local range = { line1 - 1, 0, line2 - 1, -1 }
+ parser:parse({ 0, 0, line2, -1 })
+ local lang_tree = parser:language_for_range(range)
+ return lang_tree:lang() == 'lua'
+end
+
return M
diff --git a/src/nvim/runtime.c b/src/nvim/runtime.c
@@ -2275,8 +2275,26 @@ static int do_source_ext(char *const fname, const bool check_other, const int is
cookie.conv.vc_type = CONV_NONE; // no conversion
+ // Check if treesitter detects this range as Lua (for injections like vimdoc codeblocks)
+ bool ts_lua = false;
+ if (fname == NULL && eap != NULL && !ex_lua
+ && !strequal(curbuf->b_p_ft, "lua")
+ && !(curbuf->b_fname && path_with_extension(curbuf->b_fname, "lua"))) {
+ MAXSIZE_TEMP_ARRAY(args, 3);
+ ADD_C(args, INTEGER_OBJ(curbuf->handle));
+ ADD_C(args, INTEGER_OBJ(eap->line1));
+ ADD_C(args, INTEGER_OBJ(eap->line2));
+ Error err = ERROR_INIT;
+ Object result = NLUA_EXEC_STATIC("return require('vim._core.util').source_is_lua(...)",
+ args, kRetNilBool, NULL, &err);
+ if (!ERROR_SET(&err) && LUARET_TRUTHY(result)) {
+ ts_lua = true;
+ }
+ api_clear_error(&err);
+ }
+
if (fname == NULL
- && (ex_lua || strequal(curbuf->b_p_ft, "lua")
+ && (ex_lua || ts_lua || strequal(curbuf->b_p_ft, "lua")
|| (curbuf->b_fname && path_with_extension(curbuf->b_fname, "lua")))) {
// Source lines from the current buffer as lua
nlua_exec_ga(&cookie.buflines, fname_exp);
diff --git a/test/functional/ex_cmds/source_spec.lua b/test/functional/ex_cmds/source_spec.lua
@@ -296,6 +296,35 @@ describe(':source', function()
eq(nil, result:find('E484'))
os.remove(test_file)
end)
+
+ it('sources Lua/Vimscript codeblocks based on treesitter injection', function()
+ insert([[
+ *test.txt* Test help file
+
+ Lua example: >lua
+ vim.g.test_lua = 42
+ <
+
+ Vim example: >vim
+ let g:test_vim = 99
+ <]])
+ command('setlocal filetype=help')
+
+ -- Source Lua codeblock (line 4 contains the Lua code)
+ command(':4source')
+ eq(42, eval('g:test_lua'))
+
+ -- Source Vimscript codeblock (line 8 contains the Vim code)
+ command(':8source')
+ eq(99, eval('g:test_vim'))
+
+ -- Test fallback without treesitter
+ command('enew')
+ insert([[let g:test_no_ts = 123]])
+ command('setlocal filetype=')
+ command('source')
+ eq(123, eval('g:test_no_ts'))
+ end)
end)
it('$HOME is not shortened in filepath in v:stacktrace from sourced file', function()