Skip to content

Commit 423b30b

Browse files
committed
display script error in code editor
1 parent 53a92d8 commit 423b30b

8 files changed

Lines changed: 127 additions & 9 deletions

File tree

apis/LtgPluginApi.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,15 @@ struct ScriptingError {
8888
ScriptFilePathName file;
8989
size_t line = 0;
9090
size_t column = 0;
91+
std::string message; // full error text, used as the per-line marker tooltip in the editor
9192
};
9293
typedef std::vector<ScriptingError> ErrorContainer;
9394

95+
// chunk name passed to the scripting runtime when compiling the in-memory project script.
96+
// the host's CodePane uses the SAME string as the sheet id, so a ScriptingError's `file` field
97+
// can be routed back to the matching sheet. keep both sides in sync.
98+
static constexpr const char* sc_PROJECT_SCRIPT_CHUNK = "<project script>";
99+
94100
// lua_register(lua_state_ptr, "print", lua_int_print_args);
95101
struct IDatasModel {
96102
// add a signal tag with date, color a name. the help will be displayed when mouse over the tag

plugins/LuaScripting/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ target_include_directories(${PROJECT} PRIVATE
127127
target_link_libraries(${PROJECT}
128128
${LUA_JIT_LIBRARIES}
129129
${SOL2_LIBRARIES}
130+
# boost_regex is created by imguipack when USE_IMGUI_COLOR_TEXT_EDIT is ON (forced ON by the host
131+
# in cmake/imguipack.cmake). Linking it here gives the plugin a real regex engine without dragging
132+
# imguipack, and keeps the host + plugin on the SAME regex library for future ltg:regex bindings.
133+
boost_regex
130134
)
131135

132136
# add plugin name in parent LOADED_STROCKER_PLUGINS list

plugins/LuaScripting/src/modules/Module.cpp

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
#include <unordered_map>
1414
#include <unordered_set>
1515

16+
// boost::regex (vendored by imguipack via USE_IMGUI_COLOR_TEXT_EDIT) — same engine the host will use
17+
// when the future shared ltg:regex(...) brick lands. linked through the boost_regex CMake target.
18+
#include <boost/regex.hpp>
19+
1620
#include <lua.hpp>
1721
#include <sol/sol.hpp>
1822

@@ -25,6 +29,33 @@ namespace {
2529
// plugin DLL's unload.
2630
const char* const kDebugModuleRegistryKey = "ltg_debug_module";
2731

32+
// Parse a Lua/sol2 error message and extract (file, line). Sol2 wraps an in-memory chunk as
33+
// `[string "NAME"]:LINE: MSG`; a file-based script reads `NAME:LINE: MSG`. On a match, the full
34+
// original message is kept as `aoErr.message` (it stays useful for the tooltip even if regex only
35+
// found a prefix). On no match, the message lands on `aFallbackChunk:0` so the host can still display
36+
// it (just unanchored). Returns true on a regex match.
37+
bool parseLuaError(const std::string& aMsg, const std::string& aFallbackChunk, Ltg::ScriptingError& aoErr) {
38+
// delimiter `re(...)re` mandatory: the chunkRe pattern contains a literal `)"` (the `+)"` after
39+
// the `[^"]+` group), which would otherwise close an empty-delimiter R"(...)" prematurely.
40+
static const boost::regex chunkRe(R"re(\[string\s+"([^"]+)"\]:(\d+):\s*(.*))re");
41+
static const boost::regex plainRe(R"re(([^:\[\]\s]+):(\d+):\s*(.*))re");
42+
boost::smatch match;
43+
aoErr.message = aMsg;
44+
if (boost::regex_search(aMsg, match, chunkRe)) {
45+
aoErr.file = match[1].str();
46+
aoErr.line = static_cast<size_t>(std::stoul(match[2].str()));
47+
return true;
48+
}
49+
if (boost::regex_search(aMsg, match, plainRe)) {
50+
aoErr.file = match[1].str();
51+
aoErr.line = static_cast<size_t>(std::stoul(match[2].str()));
52+
return true;
53+
}
54+
aoErr.file = aFallbackChunk;
55+
aoErr.line = 0;
56+
return false;
57+
}
58+
2859
// Stringify a value on the Lua stack WITHOUT luaL_tolstring (absent from LuaJIT 5.1).
2960
// The type is checked first so lua_tostring is only called on real strings (it would
3061
// otherwise coerce a number in place and disturb the stack inside the hook).
@@ -198,7 +229,9 @@ bool Module::compileScript(const Ltg::ScriptFilePathName& vFilePathName, Ltg::Er
198229

199230
bool Module::compileScriptCode(const std::string& aCode, Ltg::ErrorContainer& vOutErrors) {
200231
try {
201-
m_luaPtr->script(aCode); // compile + run the chunk from memory (defines parse/startFile/endFile)
232+
// pass an explicit chunk name so sol2's error messages identify our project script
233+
// (otherwise sol2 uses the first line of code as the chunk name, which breaks routing).
234+
m_luaPtr->script(aCode, Ltg::sc_PROJECT_SCRIPT_CHUNK); // compile + run the chunk (defines parse/startFile/endFile)
202235
bool res = true;
203236
sol::function parse = (*m_luaPtr)["parse"];
204237
if (!parse.valid()) {
@@ -209,10 +242,20 @@ bool Module::compileScriptCode(const std::string& aCode, Ltg::ErrorContainer& vO
209242
res &= callScriptEnd(vOutErrors);
210243
return res;
211244
} catch (const sol::error& ex) {
245+
Ltg::ScriptingError err;
246+
parseLuaError(ex.what(), Ltg::sc_PROJECT_SCRIPT_CHUNK, err);
247+
vOutErrors.push_back(err);
212248
LogVarError("Lua: Error in the Lua script : %s", ex.what());
213249
} catch (const std::exception& ex) {
250+
Ltg::ScriptingError err;
251+
parseLuaError(ex.what(), Ltg::sc_PROJECT_SCRIPT_CHUNK, err);
252+
vOutErrors.push_back(err);
214253
LogVarError("Lua: Error in the Lua script : %s", ex.what());
215254
} catch (...) {
255+
Ltg::ScriptingError err;
256+
err.file = Ltg::sc_PROJECT_SCRIPT_CHUNK;
257+
err.message = "Unknown error in the Lua script";
258+
vOutErrors.push_back(err);
216259
LogVarError("Lua: %s", "Unknown error in the Lua script");
217260
}
218261
return false;
@@ -226,8 +269,11 @@ bool Module::callScriptStart(Ltg::ErrorContainer& vOutErrors) {
226269
}
227270
sol::protected_function_result result = startFile();
228271
if (!result.valid()) {
229-
sol::error err = result;
230-
LogVarLightError("Lua: error in startFile func call : %s", err.what());
272+
sol::error solErr = result;
273+
Ltg::ScriptingError err;
274+
parseLuaError(solErr.what(), Ltg::sc_PROJECT_SCRIPT_CHUNK, err);
275+
vOutErrors.push_back(err);
276+
LogVarLightError("Lua: error in startFile func call : %s", solErr.what());
231277
return false;
232278
}
233279
return true;
@@ -241,8 +287,11 @@ bool Module::callScriptExec(const Ltg::ScriptingDatas& vOutDatas, Ltg::ErrorCont
241287
}
242288
sol::protected_function_result result = parse(vOutDatas.buffer);
243289
if (!result.valid()) {
244-
sol::error err = result;
245-
LogVarLightError("Lua: error in parse func call : %s", err.what());
290+
sol::error solErr = result;
291+
Ltg::ScriptingError err;
292+
parseLuaError(solErr.what(), Ltg::sc_PROJECT_SCRIPT_CHUNK, err);
293+
vErrors.push_back(err);
294+
LogVarLightError("Lua: error in parse func call : %s", solErr.what());
246295
return false;
247296
}
248297
return true;
@@ -256,8 +305,11 @@ bool Module::callScriptEnd(Ltg::ErrorContainer& vOutErrors) {
256305
}
257306
sol::protected_function_result result = endFile();
258307
if (!result.valid()) {
259-
sol::error err = result;
260-
LogVarLightError("Lua: error in endFile func call : %s", err.what());
308+
sol::error solErr = result;
309+
Ltg::ScriptingError err;
310+
parseLuaError(solErr.what(), Ltg::sc_PROJECT_SCRIPT_CHUNK, err);
311+
vOutErrors.push_back(err);
312+
LogVarLightError("Lua: error in endFile func call : %s", solErr.what());
261313
return false;
262314
}
263315
return true;

src/frontend/Components/CodeEditor.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ void CodeEditor::m_RebuildMarkers() {
299299
m_Editor.ClearMarkers();
300300
// breakpoints are drawn by the line decorator (a red dot); markers carry errors + current line
301301
for (const auto& errorMarker : m_ErrorMarkers) {
302-
m_Editor.AddMarker(errorMarker.first, 0, IM_COL32(200, 0, 40, 80), "", errorMarker.second);
302+
m_Editor.AddMarker(errorMarker.first - 1, 0, IM_COL32(200, 0, 40, 80), "", errorMarker.second);
303303
}
304304
// current paused line: amber line number + translucent amber text
305305
if (m_CurrentExecLine >= 0) {

src/models/script/ScriptingEngine.cpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ void ScriptingEngine::m_run(std::atomic<double>& vProgress, std::atomic<bool>& v
6767

6868
vGenerationTime = 0.0f;
6969

70+
// wipe errors from the previous run + bump revision so the UI clears its editor markers before
71+
// we start producing new ones (release order; UI reads via acquire load — no mutex on hot path).
72+
{
73+
std::lock_guard<std::mutex> lock(m_ErrorsMutex);
74+
m_LastRunErrors.clear();
75+
m_ErrorsRevision.fetch_add(1, std::memory_order_release);
76+
}
77+
7078
const int64_t firstTimeMark = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
7179

7280
Ltg::ScriptingModulePtr scriptingPtr = nullptr;
@@ -148,6 +156,13 @@ void ScriptingEngine::m_run(std::atomic<double>& vProgress, std::atomic<bool>& v
148156
ScriptDebugger::ref()->unbindPlugin();
149157
}
150158
}
159+
// publish whatever errors accumulated (compile + runtime) for the UI to consume.
160+
// errorContainer goes out of scope right after — this is the last chance to capture.
161+
{
162+
std::lock_guard<std::mutex> lock(m_ErrorsMutex);
163+
m_LastRunErrors = errorContainer;
164+
m_ErrorsRevision.fetch_add(1, std::memory_order_release);
165+
}
151166
scriptingPtr->unload();
152167
}
153168
}
@@ -156,6 +171,15 @@ void ScriptingEngine::m_run(std::atomic<double>& vProgress, std::atomic<bool>& v
156171
vWorking = false;
157172
}
158173

174+
int64_t ScriptingEngine::GetErrorsRevision() const {
175+
return m_ErrorsRevision.load(std::memory_order_acquire);
176+
}
177+
178+
std::vector<Ltg::ScriptingError> ScriptingEngine::GetLastRunErrors() const {
179+
std::lock_guard<std::mutex> lock(m_ErrorsMutex);
180+
return m_LastRunErrors;
181+
}
182+
159183
///////////////////////////////////////////////////
160184
/// INIT/UNIT /////////////////////////////////////
161185
///////////////////////////////////////////////////

src/models/script/ScriptingEngine.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class ScriptingEngine : public Ltg::IDatasModel, public ez::xml::Config {
5959
private: // thread
6060
std::thread m_WorkerThread;
6161

62+
private: // errors collected during the last run (worker writes, UI reads via revision pattern)
63+
mutable std::mutex m_ErrorsMutex;
64+
std::vector<Ltg::ScriptingError> m_LastRunErrors;
65+
std::atomic<int64_t> m_ErrorsRevision{0};
66+
6267
public:
6368
void Clear();
6469

@@ -106,6 +111,10 @@ class ScriptingEngine : public Ltg::IDatasModel, public ez::xml::Config {
106111
void Join();
107112
bool FinishIfRequired();
108113

114+
// errors collected during the last run — UI reads them via revision-cache pattern (worker fills under mutex)
115+
int64_t GetErrorsRevision() const; // atomic acquire load; no mutex
116+
std::vector<Ltg::ScriptingError> GetLastRunErrors() const; // mutex-locked copy by value (worker mutates)
117+
109118
bool drawMenu();
110119
bool isValidScriptingSelected() const;
111120

src/panes/misc/CodePane.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ void CodePane::Clear() {
3232
m_Breakpoints0BasedCache.clear();
3333
m_LastStateRevision = -1;
3434
m_StateCache = Ltg::DebugState{};
35+
m_LastErrorsRevisionSeen = -1;
36+
m_ErrorsCache.clear();
3537
}
3638

3739
///////////////////////////////////////////////////////////////////////////////////
@@ -74,6 +76,24 @@ bool CodePane::drawPanes(bool* apOpened, LayoutPaneUserDatas apUserDatas) {
7476
m_LastStateRevision = stateRevision;
7577
}
7678

79+
// scripting errors — refreshed when ScriptingEngine bumps GetErrorsRevision(). on a bump,
80+
// wipe all sheets' error markers then push the new ones to each matching sheet (route by err.file).
81+
const int64_t errorsRevision = ScriptingEngine::ref()->GetErrorsRevision();
82+
if (errorsRevision != m_LastErrorsRevisionSeen) {
83+
m_ErrorsCache = ScriptingEngine::ref()->GetLastRunErrors();
84+
m_LastErrorsRevisionSeen = errorsRevision;
85+
for (auto& sheet : m_CodeSheets) {
86+
sheet.codeEditor.ClearErrorMarkers();
87+
}
88+
for (const auto& errorEntry : m_ErrorsCache) {
89+
for (auto& sheet : m_CodeSheets) {
90+
if (sheet.filepathName == errorEntry.file) {
91+
sheet.codeEditor.AddErrorMarker(errorEntry.line, errorEntry.message);
92+
}
93+
}
94+
}
95+
}
96+
7797
const bool isPaused = (ScriptDebugger::ref()->getMode() == ScriptDebugger::Mode::Paused);
7898
const bool isDebugArmed = ScriptDebugger::ref()->isDebugArmed();
7999
int32_t currentExecLine0Based = -1;

src/panes/misc/CodePane.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include <ezlibs/ezSingleton.hpp>
55
#include <imguipack.h>
66
#include <frontend/Components/CodeEditor.h>
7-
#include <apis/IScriptDebugger.h>
7+
#include <apis/LtgPluginApi.h> // also pulls IScriptDebugger.h transitively; brings ScriptingError + sc_PROJECT_SCRIPT_CHUNK
88
#include <cstdint>
99
#include <memory>
1010
#include <string>
@@ -33,6 +33,9 @@ class CodePane : public AbstractPane {
3333
// paused state cache — refreshed only when ScriptDebugger::getStateRevision() advances (i.e. on a new pause)
3434
int64_t m_LastStateRevision = -1;
3535
Ltg::DebugState m_StateCache;
36+
// last-run scripting errors — refreshed when ScriptingEngine::GetErrorsRevision() advances
37+
int64_t m_LastErrorsRevisionSeen = -1;
38+
std::vector<Ltg::ScriptingError> m_ErrorsCache;
3639

3740
public:
3841
bool init() final;

0 commit comments

Comments
 (0)