@@ -219,12 +219,12 @@ bool Module::compileScript(const Ltg::ScriptFilePathName& vFilePathName, Ltg::Er
219219 }
220220 sol::function startFile = (*m_luaPtr)[" startFile" ];
221221 if (!startFile.valid ()) {
222- LogVarLightError (" Lua: %s" , " the lua function startFile() is missing" );
222+ LogVarLightError (" Lua: %s" , " the lua function startFile(filepath ) is missing" );
223223 res = false ;
224224 }
225225 sol::function endFile = (*m_luaPtr)[" endFile" ];
226226 if (!endFile.valid ()) {
227- LogVarLightError (" Lua: %s" , " the lua function endFile() is missing" );
227+ LogVarLightError (" Lua: %s" , " the lua function endFile(filepath ) is missing" );
228228 res = false ;
229229 }
230230 return res;
@@ -254,12 +254,12 @@ bool Module::compileScriptCode(const std::string& aCode, Ltg::ErrorContainer& vO
254254 }
255255 sol::function startFile = (*m_luaPtr)[" startFile" ];
256256 if (!startFile.valid ()) {
257- LogVarLightError (" Lua: %s" , " the lua function startFile() is missing" );
257+ LogVarLightError (" Lua: %s" , " the lua function startFile(filepath ) is missing" );
258258 res = false ;
259259 }
260260 sol::function endFile = (*m_luaPtr)[" endFile" ];
261261 if (!endFile.valid ()) {
262- LogVarLightError (" Lua: %s" , " the lua function endFile() is missing" );
262+ LogVarLightError (" Lua: %s" , " the lua function endFile(filepath ) is missing" );
263263 res = false ;
264264 }
265265 return res;
@@ -365,6 +365,115 @@ void Module::m_iterateLuaTable(lua_State* apLua, int aTableIndex, std::vector<Lt
365365 }
366366}
367367
368+ void Module::setProjectScriptCode (const std::string& aCode) {
369+ // Push the in-memory project script into the completion state so user globals (top-level
370+ // `function parse(...)`, `helpers = { ... }`, etc.) surface alongside the stdlib + `ltg`
371+ // bindings. Strategy: load the chunk, set its env to a fresh sandbox table pre-populated
372+ // with safe stdlib pointers + a no-op `ltg` stub, pcall it under that env, then steal the
373+ // sandbox keys back into _G of the completion state. Anything the user code does that the
374+ // sandbox doesn't allow (io.open, os.exit, print, ...) raises a runtime error inside the
375+ // pcall and is swallowed — the partial sandbox state is still useful.
376+ m_ensureCompletionState ();
377+ lua_State* L = m_completionLuaPtr->lua_state ();
378+ const int topBefore = lua_gettop (L);
379+
380+ // 1) wipe globals introduced by the previous push, so removed top-level definitions stop
381+ // appearing in autocomplete. bindings (math, string, ltg, ...) live in _G too but are
382+ // NOT in m_completionUserGlobals so they are preserved.
383+ for (const auto & key : m_completionUserGlobals) {
384+ lua_pushnil (L);
385+ lua_setglobal (L, key.c_str ());
386+ }
387+ m_completionUserGlobals.clear ();
388+
389+ if (aCode.empty ()) {
390+ lua_settop (L, topBefore);
391+ return ;
392+ }
393+
394+ // 2) build a fresh sandbox table holding only side-effect-free pointers. ltg is replaced
395+ // by a metatable-driven stub that returns a no-op closure for any key access, so
396+ // `ltg:logInfo("x")` / `ltg:addSignalTag(...)` at top level of the user script don't
397+ // spam the host's console / Messaging pane during autocomplete refresh.
398+ lua_newtable (L);
399+ const int sandboxIdx = lua_gettop (L);
400+ // clang-format off
401+ static const char * const kSafeStdlib [] = {
402+ " math" , " string" , " table" ,
403+ " tostring" , " tonumber" , " pairs" , " ipairs" , " type" , " select" , " next" , " unpack" ,
404+ " rawget" , " rawset" , " rawequal" , " setmetatable" , " getmetatable" ,
405+ " assert" , " error" , " pcall" , " xpcall" ,
406+ };
407+ // clang-format on
408+ for (const char * const name : kSafeStdlib ) {
409+ lua_getglobal (L, name);
410+ if (lua_isnil (L, -1 )) {
411+ lua_pop (L, 1 );
412+ } else {
413+ lua_setfield (L, sandboxIdx, name);
414+ }
415+ }
416+ // ltg stub built via a Lua chunk so we don't need a second C-binding registration:
417+ // setmetatable({}, { __index = function() return function() end end })
418+ static const char * const kLtgStubChunk =
419+ " local noop = function() end "
420+ " return setmetatable({}, { __index = function(_, _) return noop end })" ;
421+ if (luaL_loadstring (L, kLtgStubChunk ) == 0 && lua_pcall (L, 0 , 1 , 0 ) == 0 ) {
422+ lua_setfield (L, sandboxIdx, " ltg" );
423+ } else {
424+ lua_pop (L, 1 ); // pop error message or failed chunk
425+ }
426+
427+ // 3) snapshot binding key names so the copy-back step doesn't shadow the real bindings
428+ // living in _G (notably: the real `ltg` is the LuaDatasModel userdata used by
429+ // getCompletionEntries("ltg"), not the no-op stub we just put in the sandbox).
430+ std::unordered_set<std::string> bindingKeys;
431+ lua_pushnil (L);
432+ while (lua_next (L, sandboxIdx) != 0 ) {
433+ if (lua_type (L, -2 ) == LUA_TSTRING ) {
434+ bindingKeys.insert (std::string (lua_tostring (L, -2 )));
435+ }
436+ lua_pop (L, 1 ); // pop value, keep key for next iter
437+ }
438+
439+ // 4) load + setfenv + exec. parse error -> bail (the user is mid-edit, previous globals
440+ // have already been wiped above so autocomplete shows only the stdlib until the next
441+ // syntactically-valid edit). runtime error -> swallow, keep partial sandbox state.
442+ if (luaL_loadbuffer (L, aCode.data (), aCode.size (), " <project script completion>" ) != 0 ) {
443+ lua_pop (L, 1 ); // pop error message
444+ lua_settop (L, topBefore);
445+ return ;
446+ }
447+ lua_pushvalue (L, sandboxIdx);
448+ if (lua_setfenv (L, -2 ) == 0 ) {
449+ // chunk isn't a function/userdata/thread — should be impossible after a successful
450+ // loadbuffer, but bail safely if the runtime ever changes.
451+ lua_settop (L, topBefore);
452+ return ;
453+ }
454+ if (lua_pcall (L, 0 , 0 , 0 ) != 0 ) {
455+ lua_pop (L, 1 ); // pop error message
456+ }
457+
458+ // 5) copy sandbox keys into _G, skipping pre-populated bindings. Track names so the next
459+ // push can wipe them. Functions, tables, numbers, strings — all welcome; the completion
460+ // iterator filters by type at query time.
461+ lua_pushnil (L);
462+ while (lua_next (L, sandboxIdx) != 0 ) {
463+ if (lua_type (L, -2 ) == LUA_TSTRING ) {
464+ std::string key (lua_tostring (L, -2 ));
465+ if (bindingKeys.find (key) == bindingKeys.end ()) {
466+ lua_pushvalue (L, -1 ); // dup value
467+ lua_setglobal (L, key.c_str ());
468+ m_completionUserGlobals.insert (std::move (key));
469+ }
470+ }
471+ lua_pop (L, 1 ); // pop value, keep key for next iter
472+ }
473+
474+ lua_settop (L, topBefore);
475+ }
476+
368477void Module::getCompletionEntries (const std::string& aTarget, std::vector<Ltg::CompletionEntry>& aoEntries) {
369478 if (aTarget.empty ()) {
370479 return ;
@@ -375,9 +484,37 @@ void Module::getCompletionEntries(const std::string& aTarget, std::vector<Ltg::C
375484
376485 lua_getglobal (L, aTarget.c_str ());
377486 const int targetType = lua_type (L, -1 );
487+ const int targetIdx = lua_gettop (L);
378488 if (targetType == LUA_TTABLE ) {
379- // plain table (Lua stdlib namespace, user table) — iterate directly
380- m_iterateLuaTable (L, lua_gettop (L), aoEntries);
489+ // plain table (Lua stdlib namespace, user table, class instance, ...) — iterate the
490+ // direct keys first (e.g. instance fields `self.prefix`, `self.count`).
491+ m_iterateLuaTable (L, targetIdx, aoEntries);
492+ // then hop one level through the metatable's __index to surface inherited methods.
493+ // This is the standard Lua OOP idiom: `Logger.__index = Logger` + `setmetatable(self, Logger)`
494+ // means `log:info()` resolves to `Logger.info`. Without this hop, `log:` autocomplete
495+ // would only see direct instance fields and miss every method on the class table.
496+ if (lua_getmetatable (L, targetIdx) != 0 ) {
497+ lua_getfield (L, -1 , " __index" );
498+ if (lua_type (L, -1 ) == LUA_TTABLE ) {
499+ std::vector<Ltg::CompletionEntry> hopEntries;
500+ m_iterateLuaTable (L, lua_gettop (L), hopEntries);
501+ // dedup against direct fields — closer scopes win (a shadowing field on the
502+ // instance should hide the metatable's same-named method in the popup).
503+ for (auto & entry : hopEntries) {
504+ bool present = false ;
505+ for (const auto & existing : aoEntries) {
506+ if (existing.name == entry.name ) {
507+ present = true ;
508+ break ;
509+ }
510+ }
511+ if (!present) {
512+ aoEntries.push_back (std::move (entry));
513+ }
514+ }
515+ }
516+ lua_pop (L, 2 ); // __index + metatable
517+ }
381518 } else if (targetType == LUA_TUSERDATA ) {
382519 // sol2 usertype — methods are stored in the metatable's __index field, hop through it
383520 if (lua_getmetatable (L, -1 ) != 0 ) {
@@ -457,13 +594,13 @@ void Module::getSignatureInfo(const std::string& aTarget, const std::string& aFu
457594 // not in the catalog — leave aoSignature empty, the host won't open the tooltip
458595}
459596
460- bool Module::callScriptStart (Ltg::ErrorContainer& vOutErrors) {
597+ bool Module::callScriptStart (const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorContainer& vOutErrors) {
461598 sol::protected_function startFile = (*m_luaPtr)[" startFile" ];
462599 if (!startFile.valid ()) {
463- LogVarLightError (" Lua: %s" , " the lua function startFile() is missing" );
600+ LogVarLightError (" Lua: %s" , " the lua function startFile(filepath ) is missing" );
464601 return false ;
465602 }
466- sol::protected_function_result result = startFile ();
603+ sol::protected_function_result result = startFile (vOutDatas. filename , vOutDatas. filepath );
467604 if (!result.valid ()) {
468605 sol::error solErr = result;
469606 Ltg::ScriptingError err;
@@ -493,13 +630,13 @@ bool Module::callScriptExec(const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorCont
493630 return true ;
494631}
495632
496- bool Module::callScriptEnd (Ltg::ErrorContainer& vOutErrors) {
633+ bool Module::callScriptEnd (const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorContainer& vOutErrors) {
497634 sol::protected_function endFile = (*m_luaPtr)[" endFile" ];
498635 if (!endFile.valid ()) {
499- LogVarLightError (" %s" , " the lua function endFile() is missing" );
636+ LogVarLightError (" %s" , " the lua function endFile(filepath ) is missing" );
500637 return false ;
501638 }
502- sol::protected_function_result result = endFile ();
639+ sol::protected_function_result result = endFile (vOutDatas. filename , vOutDatas. filepath );
503640 if (!result.valid ()) {
504641 sol::error solErr = result;
505642 Ltg::ScriptingError err;
0 commit comments