From 55a0843b7cf73d90024733d243e93876476f1746 Mon Sep 17 00:00:00 2001 From: benarcher2691 Date: Thu, 8 Jan 2026 02:20:53 +0100 Subject: [PATCH] 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. --- runtime/doc/news.txt | 2 ++ runtime/lua/vim/_core/util.lua | 18 +++++++++++++++ src/nvim/runtime.c | 20 ++++++++++++++++- test/functional/ex_cmds/source_spec.lua | 29 +++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 78aa02fc3c..4f66320f27 100644 --- 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 index df86d04d20..b9eb9a81c9 100644 --- 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 index 9bc9ddcac3..c14655758a 100644 --- 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 index f3e00dee0f..3c6a8d2c8d 100644 --- 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()