Skip to content

Commit 2a54195

Browse files
committed
-
1 parent 683caa0 commit 2a54195

25 files changed

Lines changed: 558 additions & 191 deletions

apis/LtgPluginApi.h

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ limitations under the License.
2828
// no imguipack/glad/glfw. The host owns all UI. The only host objects a plugin borrows are
2929
// forwarded by pointer/interface at instantiation (ez::Log via init(), IDatasModel via load(),
3030
// IScriptDebugHost via enableDebug()). This is what lets LogToGraph build fully static. The host
31-
// settings-dialog interface (ISettings) is NOT here anymore — it moved to src/systems/ISettings.h
31+
// settings-dialog interface (ISettings) is NOT here anymore — it moved to src/settings/ISettings.h
3232
// since plugins no longer provide settings.
3333
#include "IScriptDebugger.h"
3434

@@ -142,6 +142,8 @@ typedef std::shared_ptr<IDatasModel> IDatasModelPtr;
142142
typedef std::weak_ptr<IDatasModel> IDatasModelWeak;
143143

144144
struct ScriptingDatas {
145+
std::string filename;
146+
std::string filepath;
145147
std::string buffer;
146148
};
147149
typedef std::string ScriptingModuleName;
@@ -160,16 +162,23 @@ struct ScriptingModule : public PluginModule, public IScriptDebugger {
160162
(void)aOutErrors;
161163
return false;
162164
}
163-
// will call the start function from script and return errors
164-
virtual bool callScriptStart(ErrorContainer& vOutErrors) = 0;
165+
// will call the start function from script with a filepath and return errors
166+
virtual bool callScriptStart(const ScriptingDatas& vOutDatas, ErrorContainer& vOutErrors) = 0;
165167
// will call the exec function from script with a buffer and return errors
166168
virtual bool callScriptExec(const ScriptingDatas& vOutDatas, ErrorContainer& vErrors) = 0;
167-
// will call the end function from script and return errors
168-
virtual bool callScriptEnd(ErrorContainer& vOutErrors) = 0;
169+
// will call the end function from script with a filepath and return errors
170+
virtual bool callScriptEnd(const ScriptingDatas& vOutDatas, ErrorContainer& vOutErrors) = 0;
169171
// will set the row index
170172
virtual void setRowIndex(int32_t vRowIndex) = 0;
171173
// will set the row count
172174
virtual void setRowCount(int32_t vRowCount) = 0;
175+
// pushes the in-memory project script source into the plugin's completion state so user-defined
176+
// globals (top-level `function foo(...)`, `helpers = {...}`, etc.) surface in autocomplete next
177+
// to the stdlib + `ltg` bindings. default no-op so non-Lua plugins compile unchanged. safe to
178+
// call frequently (host calls it on every edit transaction): the implementation should be cheap
179+
// and side-effect-free regardless of what the user code contains (sandbox the execution so a
180+
// mid-edit script can't open files / spam logs / etc.).
181+
virtual void setProjectScriptCode(const std::string& /*aCode*/) {}
173182
// introspects the plugin's live scripting state and returns the members of `aTarget` (a global
174183
// table or sol2 usertype). default no-op so non-Lua plugins compile unchanged. consumed by the
175184
// host's autocompletion popup; called from the UI thread, fast (one Lua table walk).

plugins/LuaScripting/src/modules/Module.cpp

Lines changed: 149 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
368477
void 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;

plugins/LuaScripting/src/modules/Module.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <atomic>
99
#include <cstdint>
1010
#include <unordered_map>
11+
#include <unordered_set>
1112

1213
namespace sol {
1314
class state;
@@ -51,13 +52,14 @@ class Module : public Ltg::ScriptingModule {
5152
void unload() final;
5253
bool compileScript(const Ltg::ScriptFilePathName& vFilePathName, Ltg::ErrorContainer& vOutErrors) final;
5354
bool compileScriptCode(const std::string& aCode, Ltg::ErrorContainer& vOutErrors) final;
54-
bool callScriptStart(Ltg::ErrorContainer& vOutErrors) final;
55+
bool callScriptStart(const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorContainer& vOutErrors) final;
5556
bool callScriptExec(const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorContainer& vErrors) final;
56-
bool callScriptEnd(Ltg::ErrorContainer& vOutErrors) final;
57+
bool callScriptEnd(const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorContainer& vOutErrors) final;
5758

5859
void setRowIndex(int32_t vRowIndex) final;
5960
void setRowCount(int32_t vRowCount) final;
6061

62+
void setProjectScriptCode(const std::string& aCode) final;
6163
void getCompletionEntries(const std::string& aTarget, std::vector<Ltg::CompletionEntry>& aoEntries) final;
6264
void getSignatureInfo(const std::string& aTarget, const std::string& aFunctionName, Ltg::SignatureInfo& aoSignature) final;
6365

@@ -81,4 +83,8 @@ class Module : public Ltg::ScriptingModule {
8183

8284
void m_ensureCompletionState(); // lazy init of m_completionLuaPtr on first getCompletionEntries call
8385
static void m_iterateLuaTable(lua_State* apLua, int aTableIndex, std::vector<Ltg::CompletionEntry>& aoEntries);
86+
87+
// keys we currently expose as user globals in the completion state's _G — wiped before each
88+
// setProjectScriptCode refresh so removed top-level definitions stop appearing in autocomplete.
89+
std::unordered_set<std::string> m_completionUserGlobals;
8490
};

src/App.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
#include <frontend/MainFrontend.h>
99
#include <project/ProjectFile.h>
1010
#include <systems/PluginManager.h>
11-
#include <systems/SettingsDialog.h>
12-
#include <systems/AppSettings.h>
11+
#include <settings/SettingsDialog.h>
12+
#include <settings/AppSettings.h>
13+
#include <settings/DebugSettings.h>
1314
#include <systems/TranslationHelper.h>
1415
#include <models/log/LogEngine.h>
1516
#include <models/script/ScriptingEngine.h>
@@ -100,6 +101,7 @@ void App::m_InitSingletons() {
100101
TranslationHelper::initSingleton();
101102
// app-level settings — shared_ptr because SettingsDialog stores it as weak_ptr<ISettings>
102103
AppSettings::initSingleton();
104+
DebugSettings::initSingleton();
103105
// models (shared_ptr based)
104106
ProjectFile::initSingleton();
105107
LogEngine::initSingleton();
@@ -164,6 +166,7 @@ void App::m_UnitSingletons() {
164166
LogEngine::unitSingleton();
165167
ProjectFile::unitSingleton();
166168
// app-level settings
169+
DebugSettings::unitSingleton();
167170
AppSettings::unitSingleton();
168171
// managers
169172
TranslationHelper::unitSingleton();

src/backend/MainBackend.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232

3333
#include <panes/misc/ConsolePane.h>
3434

35-
#include <systems/SettingsDialog.h>
36-
#include <systems/AppSettings.h>
35+
#include <settings/SettingsDialog.h>
36+
#include <settings/AppSettings.h>
3737

3838
// we include the cpp just for embedded fonts
3939
#include <fonts/fontIcons.cpp>

src/frontend/Components/CodeEditor.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include "CodeEditor.h"
22
#include <ezlibs/ezTools.hpp>
33
#include <models/script/ScriptingEngine.h>
4+
#include <settings/DebugSettings.h>
45

56
#include <filesystem>
67
#include <fstream>
@@ -264,6 +265,10 @@ bool CodeEditor::IsModified() const {
264265
return m_Editor.GetUndoIndex() != static_cast<size_t>(m_UndoIndexInDisk);
265266
}
266267

268+
size_t CodeEditor::GetUndoIndex() const {
269+
return m_Editor.GetUndoIndex();
270+
}
271+
267272
void CodeEditor::MarkSaved() {
268273
m_UndoIndexInDisk = static_cast<int>(m_Editor.GetUndoIndex());
269274
}
@@ -440,11 +445,11 @@ void CodeEditor::m_OnCharacterTyped(ImWchar aCharacter, int aLine, int aColumn)
440445
m_Editor.CloseCompletionPopup();
441446
m_OnCompletionCancelled();
442447
// fall through — a non-ident char may also be a `(` that triggers the signature
443-
} else if (aCharacter == '.' || aCharacter == ':') {
448+
} else if ((aCharacter == '.' || aCharacter == ':') && DebugSettings::ref()->isAutoCompletionEnabled()) {
444449
// === completion trigger: `.` or `:` after a known catalog key ======================
445450
// `aColumn` is the cursor RIGHT AFTER the trigger char. The identifier we want is to
446451
// the LEFT of the trigger, so we look one column further back (the trigger itself
447-
// is at column - 1).
452+
// is at column - 1). gated by DebugSettings — when off, no popup ever opens.
448453
const std::string target = m_ExtractTokenAt(aLine, aColumn - 2);
449454
if (target.empty()) {
450455
return;
@@ -466,8 +471,8 @@ void CodeEditor::m_OnCharacterTyped(ImWchar aCharacter, int aLine, int aColumn)
466471
// === signature trigger: `(` when no tooltip is currently open ==========================
467472
// we skip the trigger when the signature is already open: the first block above already
468473
// bumped the paren depth (nested call), and a nested signature tooltip would compete for
469-
// screen space with no clean UI to disambiguate.
470-
if (aCharacter == '(' && !m_Editor.IsSignatureTooltipOpen()) {
474+
// screen space with no clean UI to disambiguate. gated by DebugSettings.
475+
if (aCharacter == '(' && !m_Editor.IsSignatureTooltipOpen() && DebugSettings::ref()->isSignatureHelpEnabled()) {
471476
std::string target;
472477
std::string funcName;
473478
if (m_ExtractCallTargetAt(aLine, aColumn - 1, target, funcName)) {

src/frontend/Components/CodeEditor.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ class CodeEditor {
8383
std::string GetCode() const;
8484
bool IsModified() const; // true if edited since the last SetCode / MarkSaved
8585
void MarkSaved();
86+
size_t GetUndoIndex() const; // monotonic edit counter — used to drive completion refresh on edit
8687

8788
void ClearErrorMarkers();
8889
void AddErrorMarker(const size_t& vErrorLine, const std::string& vErrorMsg);

0 commit comments

Comments
 (0)