From c7503ea0032e78126a9c34b060fbdfbbd7a0c886 Mon Sep 17 00:00:00 2001 From: Chiarandini Date: Wed, 2 Sep 2026 13:53:41 -0400 Subject: [PATCH] fix(loaders): keep recording sources across a nested lua-load `_luasnip_load_file` puts `__luasnip_get_loaded_file_frame_debuginfo` into the global environment before running a snippet-file and set it back to nil afterwards. The function is re-entrant, so the inner call cleared the getter for the outer file as well, and every snippet the outer file defined after that point was stored without a source. Re-entering is ordinary: a snippet-file that requires a module from a plugin loaded on demand runs that plugin's setup mid-file, and a setup that registers its own collection loads a file. The result is that `ls.snippet_source.get` returns nil, so `snip_location` cannot jump to the definition and anything else built on sources sees a gap it cannot explain. Save the previous value and restore it instead of clearing. The getter itself already resolves the right frame by walking the stack for the nearest `_luasnip_load_file`, so nesting needs nothing further. --- lua/luasnip/loaders/from_lua.lua | 12 +++++-- .../lua-snippets/nested-load/inner/all.lua | 1 + .../lua-snippets/nested-load/outer/all.lua | 13 ++++++++ tests/integration/source_spec.lua | 32 +++++++++++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 tests/data/lua-snippets/nested-load/inner/all.lua create mode 100644 tests/data/lua-snippets/nested-load/outer/all.lua diff --git a/lua/luasnip/loaders/from_lua.lua b/lua/luasnip/loaders/from_lua.lua index 8900921ed..7669c7529 100644 --- a/lua/luasnip/loaders/from_lua.lua +++ b/lua/luasnip/loaders/from_lua.lua @@ -43,7 +43,7 @@ local M = {} -- It is not exported, because it will (in its current state) only ever be used -- in one place, and it feels a bit wrong to expose put a function into `M`. -- Instead, it is inserted into the global environment before a luasnippet-file --- is loaded, and removed from it immediately when this is done +-- is loaded, and the previous value restored immediately when this is done local function get_loaded_file_debuginfo() -- we can skip looking at the first four stackframes, since -- 1 is this function @@ -155,14 +155,20 @@ local function _luasnip_load_file(file) -- Since this function has to reach the snippet-constructor, and fenvs -- aren't inherited by called functions, we have to set it in the global -- environment. + -- Save and restore, don't unconditionally nil: this function is re-entrant. + -- A snippet-file may, while it is executing, cause another file to be + -- loaded (for example by requiring a module from a plugin that registers a + -- collection in its setup), and clearing the global on return from that + -- inner load would silence source-recording for the rest of the outer file. + local prev_get_debuginfo = _G.__luasnip_get_loaded_file_frame_debuginfo _G.__luasnip_get_loaded_file_frame_debuginfo = util.ternary( session.config.loaders_store_source, get_loaded_file_debuginfo, nil ) local run_ok, file_snippets_or_err, file_autosnippets = pcall(func) - -- immediately nil it. - _G.__luasnip_get_loaded_file_frame_debuginfo = nil + -- immediately restore it. + _G.__luasnip_get_loaded_file_frame_debuginfo = prev_get_debuginfo if not run_ok then local err = file_snippets_or_err diff --git a/tests/data/lua-snippets/nested-load/inner/all.lua b/tests/data/lua-snippets/nested-load/inner/all.lua new file mode 100644 index 000000000..c9d8d5133 --- /dev/null +++ b/tests/data/lua-snippets/nested-load/inner/all.lua @@ -0,0 +1 @@ +return { s("nested_inner", t("inner")) } diff --git a/tests/data/lua-snippets/nested-load/outer/all.lua b/tests/data/lua-snippets/nested-load/outer/all.lua new file mode 100644 index 000000000..603a98d78 --- /dev/null +++ b/tests/data/lua-snippets/nested-load/outer/all.lua @@ -0,0 +1,13 @@ +-- Loading this file triggers another load while it is still executing, as +-- happens when a snippet-file requires a module from a plugin that registers +-- its own collection. +local before = s("nested_before", t("before")) + +require("luasnip.loaders.from_lua").load({ + paths = os.getenv("LUASNIP_SOURCE") + .. "/tests/data/lua-snippets/nested-load/inner", +}) + +local after = s("nested_after", t("after")) + +return { before, after } diff --git a/tests/integration/source_spec.lua b/tests/integration/source_spec.lua index 65cfecbd3..a204d82b7 100644 --- a/tests/integration/source_spec.lua +++ b/tests/integration/source_spec.lua @@ -2,6 +2,7 @@ local ls_helpers = require("helpers") local exec_lua, feed, exec = ls_helpers.exec_lua, ls_helpers.feed, ls_helpers.exec local Screen = require("test.functional.ui.screen") +local assert = ls_helpers.assert describe("loaders:", function() local screen @@ -59,6 +60,37 @@ describe("loaders:", function() }) end) + it("lua: stores source after a nested load.", function() + setup({ + no_snip_globals = true, + setup_extend = { loaders_store_source = true }, + }) + exec_lua( + string.format( + [[require("luasnip.loaders.from_lua").load({paths="%s"})]], + os.getenv("LUASNIP_SOURCE") + .. "/tests/data/lua-snippets/nested-load/outer" + ) + ) + + -- `nested_after` is defined after the outer file triggers a load of + -- the inner collection, and used to end up without a source. + assert.eq( + { + nested_before = true, + nested_inner = true, + nested_after = true, + }, + exec_lua([[ + local has_source = {} + for _, snip in ipairs(ls.get_snippets("all")) do + has_source[snip.trigger] = ls.snippet_source.get(snip) ~= nil + end + return has_source + ]]) + ) + end) + it("vscode: error-message when parser not installed.", function() setup({ no_snip_globals = true,