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.
This commit is contained in:
benarcher2691
2026-01-08 02:20:53 +01:00
committed by GitHub
parent 16c1334399
commit 55a0843b7c
4 changed files with 68 additions and 1 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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);

View File

@@ -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()