@@ -377,20 +377,24 @@ void Module::setProjectScriptCode(const std::string& aCode) {
377377 lua_State* L = m_completionLuaPtr->lua_state ();
378378 const int topBefore = lua_gettop (L);
379379
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-
380+ // empty code path = explicit clear (e.g. project closed). Wipe + return.
389381 if (aCode.empty ()) {
382+ for (const auto & key : m_completionUserGlobals) {
383+ lua_pushnil (L);
384+ lua_setglobal (L, key.c_str ());
385+ }
386+ m_completionUserGlobals.clear ();
390387 lua_settop (L, topBefore);
391388 return ;
392389 }
393390
391+ // NOTE on wipe order — the wipe of previous globals is DEFERRED until after the new chunk has
392+ // both parsed and executed successfully (see step 5). Wiping eagerly here would break the
393+ // user's mid-edit experience: typing inside a function body briefly invalidates the syntax,
394+ // loadbuffer fails, we'd bail with previous globals already nil'd, and the autocomplete
395+ // popup typed during that gap finds no targets. The current order keeps `_G` in its last
396+ // good state across syntactically-invalid edits.
397+
394398 // 2) build a fresh sandbox table holding only side-effect-free pointers. ltg is replaced
395399 // by a metatable-driven stub that returns a no-op closure for any key access, so
396400 // `ltg:logInfo("x")` / `ltg:addSignalTag(...)` at top level of the user script don't
@@ -455,9 +459,16 @@ void Module::setProjectScriptCode(const std::string& aCode) {
455459 lua_pop (L, 1 ); // pop error message
456460 }
457461
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.
462+ // 5) NOW wipe previous user globals (deferred from step 1) and copy the fresh sandbox keys.
463+ // pcall succeeded -> the chunk is at least syntactically valid; partial state on a
464+ // mid-chunk runtime error is still better than the previous version, so we commit either
465+ // way as long as loadbuffer + setfenv + pcall didn't bail us out above.
466+ for (const auto & key : m_completionUserGlobals) {
467+ lua_pushnil (L);
468+ lua_setglobal (L, key.c_str ());
469+ }
470+ m_completionUserGlobals.clear ();
471+
461472 lua_pushnil (L);
462473 while (lua_next (L, sandboxIdx) != 0 ) {
463474 if (lua_type (L, -2 ) == LUA_TSTRING ) {
0 commit comments