Skip to content

Commit c1b8918

Browse files
committed
-
1 parent 0107649 commit c1b8918

16 files changed

Lines changed: 459 additions & 61 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ you also have this dialog when you quit the app
165165
-- ltg:logDebug(infos_string) : will log the message in the in app console
166166
-- ltg:addSignalTag(date, r, g, b, a, name, help) : add a signal tag with date, color a name (color is linear [0:1]. the help will be displayed when mouse over the tag
167167
-- ltg:addSignalStatus(signal_category, signal_name, signal_epoch_time, signal_status) : will add a signal string status
168-
-- ltg:addSignalValue(signal_category, signal_name, signal_epoch_time, signal_value) : will add a signal numerical value
168+
-- ltg:addSignalValue(signal_category, signal_name, signal_epoch_time, signal_value, description_string_optional) : will add a signal numerical value
169169
-- ltg:addSignalStartZone(signal_category, signal_name, signal_epoch_time, signal_string) : will add a signal start zone
170170
-- ltg:addSignalEndZone(signal_category, signal_name, signal_epoch_time, signal_string) : will add a signal end zone
171171
-- get/set epoch time from datetime in format "YYYY-MM-DD HH:MM:SS,MS" or "YYYY-MM-DD HH:MM:SS.MS" with hour offset in second param

apis/IScriptDebugger.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@ struct DebugState {
7171
int32_t logRowIndex = 0;
7272
std::vector<DebugFrame> callStack; // innermost frame first; each frame holds its vars
7373
std::vector<DebugVar> globals; // top-level _G entries (no filter)
74+
// true when the pause was triggered by the auto-bp-on-error path (plugin caught a
75+
// sol2/Lua exception, queried IScriptDebugHost::shouldPauseOnError, and called onPause
76+
// synchronously with the throwing frame still on the Lua stack). Lets the host distinguish
77+
// an error pause from a breakpoint hit — used to auto-set a breakpoint at the error line
78+
// so subsequent runs can re-investigate.
79+
bool errorPause = false;
80+
std::string errorMessage; // full Lua error text (used by the host as the auto-set bp tooltip / log)
7481
};
7582

7683
// Set of breakpoint lines (1-based) for the active script.
@@ -114,6 +121,10 @@ struct IScriptDebugHost {
114121
// queried by the plugin hook on each line: is there a breakpoint on this line ?
115122
// live + thread-safe, so add/remove during a session takes effect immediately
116123
virtual bool isBreakpoint(int32_t aLine) = 0;
124+
// queried by the plugin catch block when a runtime error fires: should the worker pause
125+
// synchronously (state.errorPause = true) instead of just logging the error and moving on?
126+
// live so the toggle takes effect mid-run.
127+
virtual bool shouldPauseOnError() const = 0;
117128
};
118129

119130
// Implemented by the plugin (extended by ScriptingModule), called by the host from

apis/LtgPluginApi.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,6 @@ typedef std::shared_ptr<IDatasModel> IDatasModelPtr;
142142
typedef std::weak_ptr<IDatasModel> IDatasModelWeak;
143143

144144
struct ScriptingDatas {
145-
std::string filename;
146145
std::string filepath;
147146
std::string buffer;
148147
};

plugins/LuaScripting/src/modules/Module.cpp

Lines changed: 184 additions & 16 deletions
Large diffs are not rendered by default.

plugins/LuaScripting/src/modules/Module.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,29 @@ class Module : public Ltg::ScriptingModule {
7171

7272
private:
7373
static void sLuaHook(lua_State* apLua, lua_Debug* apDebug);
74+
// Lua-level message handler — invoked by lua_pcall BEFORE the stack unwinds when the called
75+
// function (or anything it calls) raises a Lua error (string.match(nil), nil:method(), etc.).
76+
// Distinct from the sol2 exception_handler which only fires for C++ exceptions thrown by
77+
// bindings. Recovers `this` from the lua_State registry (same key as the line hook), checks
78+
// shouldPauseOnError, and calls m_pauseOnError synchronously while the throwing frame is
79+
// still alive — so locals/upvalues/call-stack are inspectable in the debug UI.
80+
static int sLuaErrorHandler(lua_State* apLua);
7481
void m_onHook(lua_State* apLua, lua_Debug* apDebug);
7582
bool m_shouldBreak(lua_State* apLua, int32_t aLine);
7683
Ltg::DebugState m_buildState(lua_State* apLua, lua_Debug* apDebug);
84+
// Build a paused-on-error snapshot from the sol2 exception_handler. Same call-stack + globals
85+
// walk as m_buildState, but line/source come from the parsed error message (which carries the
86+
// Lua-reported throw site), and the state is flagged errorPause = true so the host can auto-set
87+
// a breakpoint at that line.
88+
Ltg::DebugState m_buildErrorState(lua_State* apLua, const std::string& aErrorMessage);
89+
// Shared helper — walks the Lua call stack (innermost first, capped at 64) and the _G table,
90+
// filling state.callStack and state.globals. Used by both m_buildState and m_buildErrorState.
91+
void m_fillCallStackAndGlobals(lua_State* apLua, Ltg::DebugState& aoState);
92+
// Drives the onPause -> waitAction loop and applies the returned command. Used by both the
93+
// line-hook path (m_onHook) and the exception_handler path (m_pauseOnError).
94+
void m_runPauseLoop(lua_State* apLua, Ltg::DebugState aState);
95+
// Called from the sol2 exception_handler when shouldPauseOnError() is true.
96+
void m_pauseOnError(lua_State* apLua, const std::string& aErrorMessage);
7797
void m_applyCommand(lua_State* apLua, Ltg::DebugCommand aCommand);
7898
int32_t m_makeRef(lua_State* apLua, int32_t aIndex);
7999
Ltg::DebugVar m_makeVar(lua_State* apLua, int32_t aIndex, const std::string& aName, const std::string& aKeyType);

src/frontend/Components/CodeEditor.cpp

Lines changed: 68 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,48 @@ bool CodeEditor::init() {
6666
m_Editor.SetCompletionCallbacks(
6767
[this](size_t aSelectedIndex) { m_OnCompletionAccepted(aSelectedIndex); },
6868
[this]() { m_OnCompletionCancelled(); });
69-
// a narrow gutter decorator: a red dot marks a breakpoint, double-click toggles it
69+
// a narrow gutter decorator: a red dot marks a breakpoint, single click toggles it; an amber
70+
// right-pointing arrow marks the current paused line (VS-style). When both apply (paused on a
71+
// breakpoint line) the arrow is drawn ON TOP of the dot so both states are visible at once.
72+
//
73+
// Breakpoint TOGGLING is always allowed — the user can set/remove BPs even when Debug is off
74+
// (they're stored, just dormant until shouldArmDebug() installs the line hook). The visual
75+
// alpha fades when Debug is off as a hint that those BPs won't fire yet — see
76+
// m_BreakpointInteractionEnabled (renamed semantically to "BPs will actually fire").
7077
m_Editor.SetLineDecorator(16.0f, [this](TextEditor::Decorator& aDecorator) {
7178
const int32_t line0 = aDecorator.line; // zero-based
7279
ImGui::InvisibleButton("##bp", ImVec2(aDecorator.width, aDecorator.height));
7380
const bool hovered = ImGui::IsItemHovered();
74-
if (m_BreakpointInteractionEnabled && hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
81+
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
7582
const bool has = (m_BreakpointLines.find(line0) != m_BreakpointLines.end());
7683
if (m_OnBreakpointToggled) {
77-
m_OnBreakpointToggled(line0, !has); // toggle
84+
m_OnBreakpointToggled(line0, !has); // toggle — allowed regardless of debug state
7885
}
7986
}
8087
const bool isBreakpoint = (m_BreakpointLines.find(line0) != m_BreakpointLines.end());
81-
const bool drawHoverHalo = hovered && m_BreakpointInteractionEnabled; // no false affordance when interaction is off
82-
if (isBreakpoint || drawHoverHalo) {
83-
const ImVec2 rectMin = ImGui::GetItemRectMin();
88+
const ImVec2 rectMin = ImGui::GetItemRectMin();
89+
const float centerX = rectMin.x + aDecorator.width * 0.5f;
90+
const float centerY = rectMin.y + aDecorator.height * 0.5f;
91+
ImDrawList* drawList = ImGui::GetWindowDrawList();
92+
if (isBreakpoint || hovered) {
8493
const float radius = (aDecorator.height - 6.0f) * 0.5f;
85-
const uint8_t bpAlpha = m_BreakpointInteractionEnabled ? 255 : 110; // fade existing dot when toggling is off
94+
// when debug is off the user can still toggle BPs but they won't fire yet — fade the
95+
// dot alpha as a visual hint.
96+
const uint8_t bpAlpha = m_BreakpointInteractionEnabled ? 255 : 110;
8697
const ImU32 color = isBreakpoint ? IM_COL32(220, 40, 40, bpAlpha) : IM_COL32(220, 40, 40, 90);
87-
ImGui::GetWindowDrawList()->AddCircleFilled(ImVec2(rectMin.x + aDecorator.width * 0.5f, rectMin.y + aDecorator.height * 0.5f), radius, color);
98+
drawList->AddCircleFilled(ImVec2(centerX, centerY), radius, color);
99+
}
100+
// current paused line: amber right-pointing arrow (VS-style). Drawn after the bp dot so it
101+
// sits on top when both coexist — the high-contrast amber/red pair stays readable.
102+
if (m_CurrentExecLine == line0) {
103+
const float arrowHalfWidth = aDecorator.width * 0.30f;
104+
const float arrowHalfHeight = aDecorator.height * 0.28f;
105+
const ImVec2 p1(centerX - arrowHalfWidth, centerY - arrowHalfHeight); // top-left
106+
const ImVec2 p2(centerX - arrowHalfWidth, centerY + arrowHalfHeight); // bottom-left
107+
const ImVec2 p3(centerX + arrowHalfWidth, centerY); // right-middle (tip)
108+
drawList->AddTriangleFilled(p1, p2, p3, IM_COL32(255, 200, 0, 255));
109+
// thin black outline so the arrow stays visible against any line background tint.
110+
drawList->AddTriangle(p1, p2, p3, IM_COL32(0, 0, 0, 200), 1.0f);
88111
}
89112
});
90113
return true;
@@ -280,10 +303,12 @@ void CodeEditor::ClearErrorMarkers() {
280303

281304
void CodeEditor::AddErrorMarker(const size_t& vErrorLine, const std::string& vErrorMsg) {
282305
m_ErrorMarkers[(int32_t)vErrorLine] = vErrorMsg;
283-
// AddMarker's textTooltip carries the message again, so the per-line error tooltip
284-
// is back (regression #2/#4 fixed); the cursor still jumps to the error line.
306+
// AddMarker's textTooltip carries the message again, so the per-line error tooltip is back
307+
// (regression #2/#4 fixed). The caret is NOT moved here — that used to jump the caret to
308+
// the error line on every error-revision bump (incl. the end-of-run bump from
309+
// ScriptingEngine::m_run), which fought with CodePane's VS-style caret sync on Step. The
310+
// host now drives caret movement exclusively via MoveCursorTo on state-revision bumps.
285311
m_RebuildMarkers();
286-
m_Editor.SetCursor((int32_t)vErrorLine, 0);
287312
}
288313

289314
// Commands
@@ -390,9 +415,15 @@ void CodeEditor::SetCurrentExecLine(int32_t aZeroBasedLine) {
390415
if (m_CurrentExecLine != aZeroBasedLine) {
391416
m_CurrentExecLine = aZeroBasedLine;
392417
m_RebuildMarkers();
393-
if (m_CurrentExecLine >= 0) {
394-
m_Editor.SetCursor(m_CurrentExecLine, 0); // scroll to the paused line
395-
}
418+
}
419+
}
420+
421+
void CodeEditor::MoveCursorTo(int32_t aZeroBasedLine, int32_t aZeroBasedColumn) {
422+
// explicit caret jump — used by CodePane to sync the caret to the active line on each new
423+
// pause event (state revision bump). Separated from SetCurrentExecLine so the marker update
424+
// (drawn every frame) doesn't fight a user who manually moved the caret between pauses.
425+
if (aZeroBasedLine >= 0) {
426+
m_Editor.SetCursor(aZeroBasedLine, aZeroBasedColumn);
396427
}
397428
}
398429

@@ -410,13 +441,31 @@ float CodeEditor::GetCurrentFontScale() const {
410441

411442
void CodeEditor::m_RebuildMarkers() {
412443
m_Editor.ClearMarkers();
413-
// breakpoints are drawn by the line decorator (a red dot); markers carry errors + current line
444+
// breakpoints are drawn by the line decorator (a red dot); markers carry errors + current line.
445+
//
446+
// Line-number gutter color: leave default (0 = no override). Only the code-line background is
447+
// tinted, so the gutter numbers stay readable on dark themes (the amber tint behind white text
448+
// washed out the line numbers on the previous design).
449+
//
450+
// TextEditor::AddMarker keys by line, so two calls on the same line — the second wins. On a
451+
// pause-on-error the error line IS the current exec line, so we'd lose either the amber
452+
// pause hint or the red error styling + tooltip. Merge them: red text background (error) +
453+
// error message as the text tooltip + "current line" gutter tooltip on hover so the dual
454+
// role of the line is still surfaced.
414455
for (const auto& errorMarker : m_ErrorMarkers) {
415-
m_Editor.AddMarker(errorMarker.first - 1, 0, IM_COL32(200, 0, 40, 80), "", errorMarker.second);
416-
}
417-
// current paused line: amber line number + translucent amber text
456+
const int32_t errorLine0Based = errorMarker.first - 1;
457+
const bool isAlsoCurrentLine = (m_CurrentExecLine == errorLine0Based);
458+
const char* lineNumberTooltip = isAlsoCurrentLine ? "current line" : "";
459+
m_Editor.AddMarker(errorLine0Based, 0, IM_COL32(200, 0, 40, 80), lineNumberTooltip, errorMarker.second);
460+
}
461+
// current paused line: translucent amber over the code line — ONLY when not already emitted
462+
// as a merged error marker above. Alpha kept low so the syntax-highlighted text reads cleanly
463+
// on top.
418464
if (m_CurrentExecLine >= 0) {
419-
m_Editor.AddMarker(m_CurrentExecLine, IM_COL32(255, 200, 0, 255), IM_COL32(255, 200, 0, 60), "current line", "");
465+
const bool alreadyEmittedAsError = (m_ErrorMarkers.find(m_CurrentExecLine + 1) != m_ErrorMarkers.end());
466+
if (!alreadyEmittedAsError) {
467+
m_Editor.AddMarker(m_CurrentExecLine, 0, IM_COL32(255, 200, 0, 60), "current line", "");
468+
}
420469
}
421470
}
422471

src/frontend/Components/CodeEditor.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ class CodeEditor {
9898
void SetHoverTokenCallback(std::function<void(const std::string& aToken)> aCallback);
9999
void SetBreakpoints(const std::unordered_set<int32_t>& aZeroBasedLines, int64_t aRevision);
100100
void SetCurrentExecLine(int32_t aZeroBasedLine);
101+
void MoveCursorTo(int32_t aZeroBasedLine, int32_t aZeroBasedColumn); // jump caret; caller decides when to sync
101102

102103
// per-editor persistent font scale — pushes a one-shot value into the underlying TextEditor
103104
// (applied on the next render after BeginChild). GetCurrentFontScale reads back the effective

src/models/debug/ScriptDebugger.cpp

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,34 @@ limitations under the License.
1616

1717
#include "ScriptDebugger.h"
1818

19+
#include <settings/DebugSettings.h>
20+
#include <models/script/ScriptingEngine.h>
21+
1922
///////////////////////////////////////////////////////////////////////////////////
2023
//// RENDEZVOUS (worker thread) /////////////////////////////////////////////////////
2124
///////////////////////////////////////////////////////////////////////////////////
2225

2326
Ltg::DebugAction ScriptDebugger::onPause(const Ltg::DebugState& aState) {
27+
// auto-bp-on-error path: when the plugin paused us synchronously from its catch block, persist
28+
// a breakpoint at the error line BEFORE handing off to the UI. The user can step / continue
29+
// normally, and on the next run the same line stays armed so they can re-investigate without
30+
// re-triggering the original throw. Also publish the error to ScriptingEngine NOW so the
31+
// CodePane red marker + tooltip show up during the pause (the worker's normal publication
32+
// path only fires when m_run finishes the whole source-file loop — way after the user clicks
33+
// Continue).
34+
if (aState.errorPause && aState.line > 0 && !aState.sourceFile.empty()) {
35+
setBreakpoint(aState.sourceFile, aState.line, true);
36+
Ltg::ScriptingError err;
37+
err.file = aState.sourceFile;
38+
err.line = static_cast<size_t>(aState.line);
39+
err.message = aState.errorMessage;
40+
ScriptingEngine::ref()->AddRuntimeError(err);
41+
// Auto-arm the master Debug toggle. The CodePane toolbar gates Continue/Step on isDebugArmed,
42+
// and the user opted into pause-on-error already — so we flip the master switch ON to keep
43+
// the toolbar consistent (otherwise the user would be paused with all debug buttons greyed
44+
// out). This persists across runs; user can flip it off manually after they're done.
45+
m_DebugArmed.store(true, std::memory_order_release);
46+
}
2447
{
2548
std::lock_guard<std::mutex> lock(m_Mutex);
2649
m_State = aState;
@@ -78,7 +101,13 @@ bool ScriptDebugger::shouldArmDebug() const {
78101
// installed and the existing breakpoints are kept in memory but stay inert (they are honoured
79102
// again as soon as the user re-arms Debug). this keeps `Debug off` synonymous with `full JIT speed`,
80103
// regardless of leftover breakpoints from a previous debug session.
81-
return m_DebugArmed.load(std::memory_order_acquire);
104+
// Exception: when the auto-bp-on-error toggle is on, we also arm the debugger — the plugin's
105+
// catch block needs a non-null IScriptDebugHost to call onPause synchronously. Without this,
106+
// the user would have to manually arm Debug to get pause-on-error, defeating the toggle.
107+
if (m_DebugArmed.load(std::memory_order_acquire)) {
108+
return true;
109+
}
110+
return DebugSettings::ref()->isAutoBreakpointOnErrorEnabled();
82111
}
83112

84113
const Ltg::BreakpointLines& ScriptDebugger::getBreakpoints() const {
@@ -94,6 +123,12 @@ bool ScriptDebugger::isBreakpoint(int32_t aLine) {
94123
return m_Breakpoints.find(aLine) != m_Breakpoints.end();
95124
}
96125

126+
bool ScriptDebugger::shouldPauseOnError() const {
127+
// live read of the user toggle — plugin queries this from its catch block each time an error
128+
// fires, so flipping the toggle mid-run takes effect on the next exception (no need to reload).
129+
return DebugSettings::ref()->isAutoBreakpointOnErrorEnabled();
130+
}
131+
97132
///////////////////////////////////////////////////////////////////////////////////
98133
//// BREAKPOINTS (UI thread) ////////////////////////////////////////////////////////
99134
///////////////////////////////////////////////////////////////////////////////////

src/models/debug/ScriptDebugger.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class ScriptDebugger : public Ltg::IScriptDebugHost {
8181
void publishExpansion(int32_t aRef, const std::vector<Ltg::DebugVar>& aChildren) final;
8282
void publishEvalResult(int32_t aEvalId, const Ltg::EvalResult& aResult) final;
8383
bool isBreakpoint(int32_t aLine) final;
84+
bool shouldPauseOnError() const final;
8485

8586
// session binding — called by ScriptingEngine around the parse run
8687
void bindPlugin(Ltg::IScriptDebugger* apPluginDebugger);

0 commit comments

Comments
 (0)