diff --git a/apps/dvxbasic/compiler/codegen.c b/apps/dvxbasic/compiler/codegen.c index e5c36ea..3bcd8fb 100644 --- a/apps/dvxbasic/compiler/codegen.c +++ b/apps/dvxbasic/compiler/codegen.c @@ -41,6 +41,23 @@ uint16_t basAddConstant(BasCodeGenT *cg, const char *text, int32_t len) { } +// ============================================================ +// basCodeGenAddDebugVar +// ============================================================ + +void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex) { + BasDebugVarT dv; + memset(&dv, 0, sizeof(dv)); + snprintf(dv.name, BAS_MAX_PROC_NAME, "%s", name); + dv.scope = scope; + dv.dataType = dataType; + dv.index = index; + dv.procIndex = procIndex; + arrput(cg->debugVars, dv); + cg->debugVarCount = (int32_t)arrlen(cg->debugVars); +} + + // ============================================================ // basCodeGenBuildModule // ============================================================ @@ -111,6 +128,17 @@ BasModuleT *basCodeGenBuildModule(BasCodeGenT *cg) { mod->formVarInfoCount = cg->formVarInfoCount; } + // Copy debug variable info + if (cg->debugVarCount > 0) { + mod->debugVars = (BasDebugVarT *)malloc(cg->debugVarCount * sizeof(BasDebugVarT)); + + if (mod->debugVars) { + memcpy(mod->debugVars, cg->debugVars, cg->debugVarCount * sizeof(BasDebugVarT)); + } + + mod->debugVarCount = cg->debugVarCount; + } + return mod; } @@ -160,6 +188,7 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab) { p->name[BAS_MAX_PROC_NAME - 1] = '\0'; p->codeAddr = s->codeAddr; p->paramCount = s->paramCount; + p->localCount = s->localCount; p->returnType = s->dataType; p->isFunction = (s->kind == SYM_FUNCTION); } @@ -187,14 +216,17 @@ void basCodeGenFree(BasCodeGenT *cg) { arrfree(cg->constants); arrfree(cg->dataPool); arrfree(cg->formVarInfo); + arrfree(cg->debugVars); cg->code = NULL; cg->constants = NULL; cg->dataPool = NULL; cg->formVarInfo = NULL; + cg->debugVars = NULL; cg->constCount = 0; cg->dataCount = 0; cg->codeLen = 0; cg->formVarInfoCount = 0; + cg->debugVarCount = 0; } @@ -332,6 +364,7 @@ void basModuleFree(BasModuleT *mod) { free(mod->procs); free(mod->formVarInfo); + free(mod->debugVars); free(mod); } diff --git a/apps/dvxbasic/compiler/codegen.h b/apps/dvxbasic/compiler/codegen.h index 988ac5e..6b535d4 100644 --- a/apps/dvxbasic/compiler/codegen.h +++ b/apps/dvxbasic/compiler/codegen.h @@ -29,6 +29,9 @@ typedef struct { int32_t dataCount; BasFormVarInfoT *formVarInfo; // stb_ds dynamic array int32_t formVarInfoCount; + BasDebugVarT *debugVars; // stb_ds dynamic array + int32_t debugVarCount; + int32_t debugProcCount; // incremented per SUB/FUNCTION for debug var tracking } BasCodeGenT; // ============================================================ @@ -76,6 +79,9 @@ BasModuleT *basCodeGenBuildModuleWithProcs(BasCodeGenT *cg, void *symtab); // Free a module built by basCodeGenBuildModule. void basModuleFree(BasModuleT *mod); +// Add a debug variable entry (for debugger variable display). +void basCodeGenAddDebugVar(BasCodeGenT *cg, const char *name, uint8_t scope, uint8_t dataType, int32_t index, int32_t procIndex); + // Find a procedure by name in a module's procedure table. // Case-insensitive. Returns NULL if not found. const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name); diff --git a/apps/dvxbasic/compiler/opcodes.h b/apps/dvxbasic/compiler/opcodes.h index 4e9ffa3..c2960ce 100644 --- a/apps/dvxbasic/compiler/opcodes.h +++ b/apps/dvxbasic/compiler/opcodes.h @@ -342,6 +342,9 @@ #define OP_INI_READ 0xE0 // pop default, pop key, pop section, pop file, push string #define OP_INI_WRITE 0xE1 // pop value, pop key, pop section, pop file +// Debug +#define OP_LINE 0xE2 // [uint16 lineNum] set current source line for debugger + // ============================================================ // Halt // ============================================================ diff --git a/apps/dvxbasic/compiler/parser.c b/apps/dvxbasic/compiler/parser.c index 143ac63..5f9fe60 100644 --- a/apps/dvxbasic/compiler/parser.c +++ b/apps/dvxbasic/compiler/parser.c @@ -575,6 +575,43 @@ static uint8_t resolveTypeName(BasParserT *p) { } +// Snapshot local variables for the debugger before leaving local scope. +// procIndex is the index into the proc table for the current procedure. +// Also saves the local count on the proc symbol for BasProcEntryT. +static void collectDebugLocals(BasParserT *p, int32_t procIndex) { + // Save localCount on the proc symbol + if (p->currentProc[0]) { + BasSymbolT *procSym = basSymTabFind(&p->sym, p->currentProc); + + if (procSym) { + procSym->localCount = p->sym.nextLocalIdx; + } + } + + for (int32_t i = 0; i < p->sym.count; i++) { + BasSymbolT *s = &p->sym.symbols[i]; + + if (s->scope == SCOPE_LOCAL && s->kind == SYM_VARIABLE) { + basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_LOCAL, s->dataType, s->index, procIndex); + } + } +} + + +// Snapshot global variables for the debugger at the end of compilation. +static void collectDebugGlobals(BasParserT *p) { + for (int32_t i = 0; i < p->sym.count; i++) { + BasSymbolT *s = &p->sym.symbols[i]; + + if (s->scope == SCOPE_GLOBAL && s->kind == SYM_VARIABLE) { + basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_GLOBAL, s->dataType, s->index, -1); + } else if (s->scope == SCOPE_FORM && s->kind == SYM_VARIABLE) { + basCodeGenAddDebugVar(&p->cg, s->name, SCOPE_FORM, s->dataType, s->index, -1); + } + } +} + + static void skipNewlines(BasParserT *p) { while (!p->hasError && check(p, TOK_NEWLINE)) { advance(p); @@ -1120,17 +1157,17 @@ static void parsePrimary(BasParserT *p) { advance(p); expect(p, TOK_DOT); - if (checkIdent(p, "Path")) { + if (checkKeyword(p,"Path")) { advance(p); basEmit8(&p->cg, OP_APP_PATH); - } else if (checkIdent(p, "Config")) { + } else if (checkKeyword(p,"Config")) { advance(p); basEmit8(&p->cg, OP_APP_CONFIG); - } else if (checkIdent(p, "Data")) { + } else if (checkKeyword(p,"Data")) { advance(p); basEmit8(&p->cg, OP_APP_DATA); } else { - parserError(p, "Expected 'Path', 'Config', or 'Data' after 'App.'"); + error(p, "Expected 'Path', 'Config', or 'Data' after 'App.'"); } return; @@ -2710,6 +2747,7 @@ static void parseDef(BasParserT *p) { parseExpression(p); basEmit8(&p->cg, OP_RET_VAL); + collectDebugLocals(p, p->cg.debugProcCount++); basSymTabLeaveLocal(&p->sym); uint8_t returnType = suffixToType(name); @@ -3338,6 +3376,7 @@ static void parseFunction(BasParserT *p) { basEmit8(&p->cg, OP_RET_VAL); // Leave local scope + collectDebugLocals(p, p->cg.debugProcCount++); basSymTabLeaveLocal(&p->sym); p->currentProc[0] = '\0'; @@ -4553,6 +4592,10 @@ static void parseStatement(BasParserT *p) { return; } + // Emit source line number for debugger (before statement code) + basEmit8(&p->cg, OP_LINE); + basEmitU16(&p->cg, (uint16_t)p->lex.token.line); + BasTokenTypeE tt = p->lex.token.type; switch (tt) { @@ -5233,6 +5276,7 @@ static void parseSub(BasParserT *p) { basEmit8(&p->cg, OP_RET); // Leave local scope + collectDebugLocals(p, p->cg.debugProcCount++); basSymTabLeaveLocal(&p->sym); p->currentProc[0] = '\0'; @@ -5581,6 +5625,9 @@ BasModuleT *basParserBuildModule(BasParserT *p) { return NULL; } + // Collect global and form-scope variables for the debugger + collectDebugGlobals(p); + p->cg.globalCount = p->sym.nextGlobalIdx; return basCodeGenBuildModuleWithProcs(&p->cg, &p->sym); } diff --git a/apps/dvxbasic/compiler/symtab.h b/apps/dvxbasic/compiler/symtab.h index c4c41f5..014ee89 100644 --- a/apps/dvxbasic/compiler/symtab.h +++ b/apps/dvxbasic/compiler/symtab.h @@ -58,6 +58,7 @@ typedef struct { uint8_t dataType; // BAS_TYPE_* for variables/functions int32_t index; // slot index (local or global) int32_t codeAddr; // PC address for SUB/FUNCTION/LABEL + int32_t localCount; // number of local variables (for SUB/FUNCTION, set on leave) bool isDefined; // false = forward-declared bool isArray; bool isShared; diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 641a7a3..e1681b7 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -26,6 +26,7 @@ #include "widgetButton.h" #include "widgetSplitter.h" #include "widgetStatusBar.h" +#include "widgetListView.h" #include "widgetToolbar.h" #include "ideDesigner.h" @@ -97,6 +98,14 @@ #define CMD_FIND_NEXT 143 #define CMD_MENU_EDITOR 144 #define CMD_PREFERENCES 145 +#define CMD_STEP_INTO 146 +#define CMD_STEP_OVER 147 +#define CMD_STEP_OUT 148 +#define CMD_TOGGLE_BP 149 +#define CMD_RUN_TO_CURSOR 150 +#define CMD_WIN_LOCALS 151 +#define CMD_WIN_CALLSTACK 152 +#define CMD_WIN_WATCH 153 #define IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -126,6 +135,8 @@ static void buildWindow(void); static void clearOutput(void); static int cmpStrPtrs(const void *a, const void *b); static void compileAndRun(void); +static void debugStartOrResume(int32_t cmd); +static void toggleBreakpoint(void); static void ensureProject(const char *filePath); static void freeProcBufs(void); static const char *getFullSource(void); @@ -166,6 +177,16 @@ static void handleViewCmd(int32_t cmd); static void handleWindowCmd(int32_t cmd); static void onMenu(WindowT *win, int32_t menuId); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); +static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); +static void onGutterClick(WidgetT *w, int32_t lineNum); +static void debugNavigateToLine(int32_t concatLine); +static void showCallStackWindow(void); +static void showLocalsWindow(void); +static void showWatchWindow(void); +static void toggleBreakpointLine(int32_t line); +static void updateCallStackWindow(void); +static void updateLocalsWindow(void); +static void updateWatchWindow(void); static void evaluateImmediate(const char *expr); static void loadFrmFiles(BasFormRtT *rt); static void onEvtDropdownChange(WidgetT *w); @@ -266,6 +287,30 @@ static const char **sEvtItems = NULL; // stb_ds dynamic array static bool sDropdownNavSuppressed = false; static bool sStopRequested = false; +// Debug state +typedef enum { + DBG_IDLE, // no program loaded + DBG_RUNNING, // program executing + DBG_PAUSED // stopped at breakpoint/step +} IdeDebugStateE; + +static IdeDebugStateE sDbgState = DBG_IDLE; +static int32_t *sBreakpoints = NULL; // stb_ds array of line numbers +static int32_t sBreakpointCount = 0; +static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none) +static BasFormRtT *sDbgFormRt = NULL; // form runtime for debug session +static BasModuleT *sDbgModule = NULL; // module for debug session +static bool sDbgBreakOnStart = false; // break at first statement +static WindowT *sLocalsWin = NULL; // Locals window +static WidgetT *sLocalsList = NULL; // Locals ListView widget +static WindowT *sCallStackWin = NULL; // Call stack window +static WidgetT *sCallStackList = NULL; // Call stack ListView widget +static WindowT *sWatchWin = NULL; // Watch window +static WidgetT *sWatchList = NULL; // Watch ListView widget +static WidgetT *sWatchInput = NULL; // Watch expression input +static char *sWatchExprs[16]; // watch expressions (strdup'd) +static int32_t sWatchExprCount = 0; + // ============================================================ // App descriptor // ============================================================ @@ -613,6 +658,13 @@ static void buildWindow(void) { wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP); wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP); wmAddMenuSeparator(runMenu); + wmAddMenuItem(runMenu, "Step &Into\tF8", CMD_STEP_INTO); + wmAddMenuItem(runMenu, "Step &Over\tShift+F8", CMD_STEP_OVER); + wmAddMenuItem(runMenu, "Step Ou&t\tCtrl+Shift+F8", CMD_STEP_OUT); + wmAddMenuItem(runMenu, "Run to &Cursor\tCtrl+F8", CMD_RUN_TO_CURSOR); + wmAddMenuSeparator(runMenu); + wmAddMenuItem(runMenu, "Toggle &Breakpoint\tF9", CMD_TOGGLE_BP); + wmAddMenuSeparator(runMenu); wmAddMenuItem(runMenu, "&Clear Output", CMD_CLEAR); wmAddMenuSeparator(runMenu); wmAddMenuCheckItem(runMenu, "Save on &Run", CMD_SAVE_ON_RUN, true); @@ -630,6 +682,9 @@ static void buildWindow(void) { wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE); wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT); wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM); + wmAddMenuItem(winMenu, "&Locals", CMD_WIN_LOCALS); + wmAddMenuItem(winMenu, "Call &Stack", CMD_WIN_CALLSTACK); + wmAddMenuItem(winMenu, "&Watch", CMD_WIN_WATCH); wmAddMenuSeparator(winMenu); wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT); wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX); @@ -653,6 +708,11 @@ static void buildWindow(void) { dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE); dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT); dvxAddAccel(accel, 'E', ACCEL_CTRL, CMD_MENU_EDITOR); + dvxAddAccel(accel, KEY_F8, 0, CMD_STEP_INTO); + dvxAddAccel(accel, KEY_F8, ACCEL_SHIFT, CMD_STEP_OVER); + dvxAddAccel(accel, KEY_F8, ACCEL_CTRL | ACCEL_SHIFT, CMD_STEP_OUT); + dvxAddAccel(accel, KEY_F8, ACCEL_CTRL, CMD_RUN_TO_CURSOR); + dvxAddAccel(accel, KEY_F9, 0, CMD_TOGGLE_BP); sWin->accelTable = accel; WidgetT *tbRoot = wgtInitWindow(sAc, sWin); @@ -888,6 +948,113 @@ static void clearOutput(void) { } +// ============================================================ +// debugLineDecorator -- highlight breakpoints and current debug line +// ============================================================ + +// ============================================================ +// toggleBreakpointLine -- toggle breakpoint on a specific line +// ============================================================ + +static void toggleBreakpointLine(int32_t line) { + for (int32_t i = 0; i < sBreakpointCount; i++) { + if (sBreakpoints[i] == line) { + arrdel(sBreakpoints, i); + sBreakpointCount = (int32_t)arrlen(sBreakpoints); + + if (sVm) { + basVmSetBreakpoints(sVm, sBreakpoints, sBreakpointCount); + } + + if (sEditor) { + wgtInvalidatePaint(sEditor); + } + + return; + } + } + + arrput(sBreakpoints, line); + sBreakpointCount = (int32_t)arrlen(sBreakpoints); + + if (sVm) { + basVmSetBreakpoints(sVm, sBreakpoints, sBreakpointCount); + } + + if (sEditor) { + wgtInvalidatePaint(sEditor); + } +} + + +// ============================================================ +// onGutterClick -- handle gutter click from TextArea +// ============================================================ + +static void onGutterClick(WidgetT *w, int32_t lineNum) { + (void)w; + toggleBreakpointLine(lineNum); +} + + +static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) { + (void)ctx; + + // Breakpoint: red gutter dot + for (int32_t i = 0; i < sBreakpointCount; i++) { + if (sBreakpoints[i] == lineNum) { + *gutterColor = 0x00CC0000; // red + break; + } + } + + // Current debug line: yellow background + if (sDbgState == DBG_PAUSED && lineNum == sDbgCurrentLine) { + return 0x00FFFF80; // yellow + } + + return 0; +} + + +// ============================================================ +// debugNavigateToLine -- map concatenated source line to file and navigate +// ============================================================ + +static void debugNavigateToLine(int32_t concatLine) { + if (concatLine <= 0) { + return; + } + + int32_t fileIdx = -1; + int32_t localLine = concatLine; + + // Try to map via source map (multi-file projects) + if (sProject.sourceMapCount > 0) { + prjMapLine(&sProject, concatLine, &fileIdx, &localLine); + } + + // Switch to the correct file if needed + if (fileIdx >= 0 && fileIdx != sProject.activeFileIdx) { + activateFile(fileIdx, ViewCodeE); + } + + // Show code window + if (sCodeWin && !sCodeWin->visible) { + dvxShowWindow(sAc, sCodeWin); + } + + // Store the local line for the decorator + sDbgCurrentLine = localLine; + + // Navigate and highlight + if (sEditor) { + wgtTextAreaGoToLine(sEditor, localLine); + wgtInvalidatePaint(sEditor); + } +} + + static int cmpStrPtrs(const void *a, const void *b) { const char *sa = *(const char **)a; const char *sb = *(const char **)b; @@ -1227,6 +1394,67 @@ static void runCached(void) { } +// ============================================================ +// toggleBreakpoint -- toggle a breakpoint on the current editor line +// ============================================================ + +static void toggleBreakpoint(void) { + if (!sEditor) { + return; + } + + toggleBreakpointLine(wgtTextAreaGetCursorLine(sEditor)); +} + + +// ============================================================ +// debugStartOrResume -- handle step/run-to-cursor commands +// ============================================================ + +static void debugStartOrResume(int32_t cmd) { + if (sDbgState == DBG_PAUSED && sVm) { + // Already paused — apply the appropriate step command and resume + sDbgCurrentLine = -1; + + switch (cmd) { + case CMD_STEP_INTO: + basVmStepInto(sVm); + break; + + case CMD_STEP_OVER: + basVmStepOver(sVm); + break; + + case CMD_STEP_OUT: + basVmStepOut(sVm); + break; + + case CMD_RUN_TO_CURSOR: + if (sEditor) { + basVmRunToCursor(sVm, wgtTextAreaGetCursorLine(sEditor)); + } + break; + } + + sDbgState = DBG_RUNNING; + + if (sEditor) { + wgtInvalidatePaint(sEditor); + } + + setStatus("Running..."); + return; + } + + if (sDbgState == DBG_IDLE) { + // Not running — compile and start with an immediate break. + sDbgBreakOnStart = true; + compileAndRun(); + sDbgBreakOnStart = false; + } +} + + // ============================================================ // runModule // ============================================================ @@ -1236,7 +1464,9 @@ static void runModule(BasModuleT *mod) { closeFindDialog(); - // Hide IDE windows while the program runs + // Hide designer windows while the program runs. + // Keep the code window visible if debugging (breakpoints or step-into). + bool debugging = sDbgBreakOnStart || sBreakpointCount > 0; bool hadFormWin = sFormWin && sFormWin->visible; bool hadToolbox = sToolboxWin && sToolboxWin->visible; bool hadProps = sPropsWin && sPropsWin->visible; @@ -1246,7 +1476,7 @@ static void runModule(BasModuleT *mod) { if (sFormWin) { dvxHideWindow(sAc, sFormWin); } if (sToolboxWin) { dvxHideWindow(sAc, sToolboxWin); } if (sPropsWin) { dvxHideWindow(sAc, sPropsWin); } - if (sCodeWin) { dvxHideWindow(sAc, sCodeWin); } + if (!debugging && sCodeWin) { dvxHideWindow(sAc, sCodeWin); } if (sProjectWin) { dvxHideWindow(sAc, sProjectWin); } // Create VM @@ -1323,7 +1553,18 @@ static void runModule(BasModuleT *mod) { basFormRtShowForm(formRt, startupForm, false); } - sVm = vm; + sVm = vm; + sDbgFormRt = formRt; + sDbgModule = mod; + sDbgState = DBG_RUNNING; + + // Pass breakpoints to the VM + basVmSetBreakpoints(vm, sBreakpoints, sBreakpointCount); + + // If starting via Step Into, break at the first OP_LINE + if (sDbgBreakOnStart) { + basVmStepInto(vm); + } // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, IDE_STEP_SLICE); @@ -1333,14 +1574,40 @@ static void runModule(BasModuleT *mod) { sStopRequested = false; for (;;) { + if (sDbgState == DBG_PAUSED) { + // Paused at breakpoint/step — spin on GUI events until user acts + dvxUpdate(sAc); + + if (!sWin || !sAc->running || sStopRequested) { + break; + } + + // User may have pressed F5 (continue), F8 (step), or Esc (stop) + if (sDbgState == DBG_RUNNING) { + vm->running = true; + } + + continue; + } + result = basVmRun(vm); totalSteps += vm->stepCount; + if (result == BAS_VM_BREAKPOINT) { + sDbgState = DBG_PAUSED; + sDbgCurrentLine = vm->currentLine; + debugNavigateToLine(vm->currentLine); + + updateLocalsWindow(); + updateCallStackWindow(); + updateWatchWindow(); + setStatus("Paused."); + continue; + } + if (result == BAS_VM_STEP_LIMIT) { - // Yield to DVX to keep the GUI responsive dvxUpdate(sAc); - // Stop if IDE window was closed, DVX is shutting down, or user hit Stop if (!sWin || !sAc->running || sStopRequested) { break; } @@ -1352,11 +1619,20 @@ static void runModule(BasModuleT *mod) { break; } - // Runtime error + // Runtime error — navigate to error line int32_t pos = sOutputLen; int32_t n = snprintf(sOutputBuf + pos, IDE_MAX_OUTPUT - pos, "\n[Runtime error: %s]\n", basVmGetError(vm)); sOutputLen += n; setOutputText(sOutputBuf); + + if (vm->currentLine > 0 && sEditor) { + wgtTextAreaGoToLine(sEditor, vm->currentLine); + + if (sCodeWin && !sCodeWin->visible) { + dvxShowWindow(sAc, sCodeWin); + } + } + break; } @@ -1368,11 +1644,33 @@ static void runModule(BasModuleT *mod) { sStopRequested = false; while (sWin && sAc->running && formRt->formCount > 0 && !sStopRequested && !vm->ended) { + if (sDbgState == DBG_PAUSED) { + // Paused inside an event handler + debugNavigateToLine(sDbgCurrentLine); + + updateLocalsWindow(); + setStatus("Paused."); + + // Wait for user to resume + while (sDbgState == DBG_PAUSED && sWin && sAc->running && !sStopRequested) { + dvxUpdate(sAc); + } + + if (sDbgState == DBG_RUNNING) { + vm->running = true; + setStatus("Running (event loop)..."); + } + } + dvxUpdate(sAc); } } - sVm = NULL; + sVm = NULL; + sDbgFormRt = NULL; + sDbgModule = NULL; + sDbgState = DBG_IDLE; + sDbgCurrentLine = -1; // Update output display setOutputText(sOutputBuf); @@ -1504,9 +1802,17 @@ static void evaluateImmediate(const char *expr) { vm->callDepth = 1; basVmSetPrintCallback(vm, immPrintCallback, NULL); + // If paused at a breakpoint, copy globals from the running VM + // so the immediate window can inspect current variable values + if (sDbgState == DBG_PAUSED && sVm && sDbgModule) { + for (int32_t g = 0; g < BAS_VM_MAX_GLOBALS && g < sDbgModule->globalCount; g++) { + vm->globals[g] = basValCopy(sVm->globals[g]); + } + } + BasVmResultE result = basVmRun(vm); - if (result != BAS_VM_HALTED && result != BAS_VM_OK) { + if (result != BAS_VM_HALTED && result != BAS_VM_OK && result != BAS_VM_BREAKPOINT) { immPrintCallback(NULL, "Error: ", false); immPrintCallback(NULL, basVmGetError(vm), true); } @@ -3010,7 +3316,17 @@ static void handleEditCmd(int32_t cmd) { static void handleRunCmd(int32_t cmd) { switch (cmd) { case CMD_RUN: - compileAndRun(); + if (sDbgState == DBG_PAUSED) { + // Resume from breakpoint + sDbgCurrentLine = -1; + sDbgState = DBG_RUNNING; + if (sEditor) { + wgtInvalidatePaint(sEditor); + } + setStatus("Running..."); + } else { + compileAndRun(); + } break; case CMD_RUN_NOCMP: @@ -3022,9 +3338,25 @@ static void handleRunCmd(int32_t cmd) { if (sVm) { sVm->running = false; } + sDbgState = DBG_IDLE; + sDbgCurrentLine = -1; + if (sEditor) { + wgtInvalidatePaint(sEditor); + } setStatus("Program stopped."); break; + case CMD_STEP_INTO: + case CMD_STEP_OVER: + case CMD_STEP_OUT: + case CMD_RUN_TO_CURSOR: + debugStartOrResume(cmd); + break; + + case CMD_TOGGLE_BP: + toggleBreakpoint(); + break; + case CMD_CLEAR: clearOutput(); break; @@ -3311,6 +3643,18 @@ static void handleWindowCmd(int32_t cmd) { showImmediateWindow(); break; + case CMD_WIN_LOCALS: + showLocalsWindow(); + break; + + case CMD_WIN_CALLSTACK: + showCallStackWindow(); + break; + + case CMD_WIN_WATCH: + showWatchWindow(); + break; + case CMD_WIN_TOOLBOX: if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner); @@ -4787,6 +5131,19 @@ static void cleanupFormWin(void) { sFormWin = NULL; sDesigner.formWin = NULL; + // Null out widget pointers — the widgets were destroyed with the window. + // Without this, dsgnCreateWidgets skips controls that have non-NULL + // widget pointers, resulting in an empty form on the second open. + if (sDesigner.form) { + int32_t count = (int32_t)arrlen(sDesigner.form->controls); + + for (int32_t i = 0; i < count; i++) { + sDesigner.form->controls[i].widget = NULL; + } + + sDesigner.form->contentBox = NULL; + } + if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; @@ -4804,8 +5161,10 @@ static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) { widgetOnResize(win, newW, newH); if (sDesigner.form) { - sDesigner.form->width = newW; - sDesigner.form->height = newH; + // Store the outer window dimensions (win->w/h), not the content + // dimensions (newW/newH), since dvxResizeWindow takes outer dims. + sDesigner.form->width = win->w; + sDesigner.form->height = win->h; sDesigner.form->dirty = true; prpRefresh(&sDesigner); } @@ -5001,6 +5360,8 @@ static void showCodeWindow(void) { sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE); sEditor->weight = 100; wgtTextAreaSetColorize(sEditor, basicColorize, NULL); + wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, NULL); + wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); wgtTextAreaSetCaptureTabs(sEditor, true); @@ -5087,6 +5448,548 @@ static void showImmediateWindow(void) { } +// ============================================================ +// showLocalsWindow +// ============================================================ + +static void onLocalsClose(WindowT *win) { + dvxHideWindow(sAc, win); +} + + +static void showLocalsWindow(void) { + if (sLocalsWin) { + dvxShowWindow(sAc, sLocalsWin); + dvxRaiseWindow(sAc, sLocalsWin); + return; + } + + int32_t winW = 250; + int32_t winH = 200; + int32_t winX = sAc->display.width - winW; + int32_t winY = toolbarBottom(); + + sLocalsWin = dvxCreateWindow(sAc, "Locals", winX, winY, winW, winH, true); + + if (sLocalsWin) { + sLocalsWin->onClose = onLocalsClose; + sLocalsWin->onMenu = onMenu; + sLocalsWin->accelTable = sWin ? sWin->accelTable : NULL; + sLocalsWin->resizable = true; + + WidgetT *root = wgtInitWindow(sAc, sLocalsWin); + + if (root) { + sLocalsList = wgtListView(root); + + if (sLocalsList) { + sLocalsList->weight = 100; + + static const ListViewColT cols[] = { + { "Name", wgtChars(12), ListViewAlignLeftE }, + { "Type", wgtChars(8), ListViewAlignLeftE }, + { "Value", wgtChars(16), ListViewAlignLeftE }, + }; + + wgtListViewSetColumns(sLocalsList, cols, 3); + } + } + } + + updateLocalsWindow(); +} + + +// ============================================================ +// updateLocalsWindow -- refresh locals display from VM state +// ============================================================ + +#define MAX_LOCALS_DISPLAY 64 + +// Static cell data for the locals ListView +static char sLocalsNames[MAX_LOCALS_DISPLAY][BAS_MAX_PROC_NAME]; +static char sLocalsTypes[MAX_LOCALS_DISPLAY][16]; +static char sLocalsValues[MAX_LOCALS_DISPLAY][64]; +static const char *sLocalsCells[MAX_LOCALS_DISPLAY * 3]; + +static const char *typeNameStr(uint8_t dt) { + switch (dt) { + case BAS_TYPE_INTEGER: return "Integer"; + case BAS_TYPE_LONG: return "Long"; + case BAS_TYPE_SINGLE: return "Single"; + case BAS_TYPE_DOUBLE: return "Double"; + case BAS_TYPE_STRING: return "String"; + case BAS_TYPE_BOOLEAN: return "Boolean"; + case BAS_TYPE_ARRAY: return "Array"; + case BAS_TYPE_UDT: return "UDT"; + default: return "?"; + } +} + + +static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) { + switch (v->type) { + case BAS_TYPE_INTEGER: snprintf(buf, bufSize, "%d", (int)v->intVal); break; + case BAS_TYPE_LONG: snprintf(buf, bufSize, "%ld", (long)v->longVal); break; + case BAS_TYPE_SINGLE: snprintf(buf, bufSize, "%.6g", (double)v->sngVal); break; + case BAS_TYPE_DOUBLE: snprintf(buf, bufSize, "%.10g", v->dblVal); break; + case BAS_TYPE_BOOLEAN: snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False"); break; + case BAS_TYPE_STRING: { + if (v->strVal && v->strVal->data) { + snprintf(buf, bufSize, "\"%.*s\"", bufSize - 3, v->strVal->data); + } else { + snprintf(buf, bufSize, "\"\""); + } + break; + } + default: snprintf(buf, bufSize, "..."); break; + } +} + + +static void updateLocalsWindow(void) { + if (!sLocalsList || !sLocalsWin || !sLocalsWin->visible) { + return; + } + + if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) { + wgtListViewSetData(sLocalsList, NULL, 0); + return; + } + + // Find which procedure we're in by matching PC to proc table + int32_t curProcIdx = -1; + int32_t bestAddr = -1; + + for (int32_t i = 0; i < sDbgModule->procCount; i++) { + int32_t addr = sDbgModule->procs[i].codeAddr; + + if (addr <= sVm->pc && addr > bestAddr) { + bestAddr = addr; + curProcIdx = i; + } + } + + // Collect matching debug vars + int32_t rowCount = 0; + + if (sDbgModule->debugVars) { + for (int32_t i = 0; i < sDbgModule->debugVarCount && rowCount < MAX_LOCALS_DISPLAY; i++) { + BasDebugVarT *dv = &sDbgModule->debugVars[i]; + + // Show locals for current proc, and all globals + if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) { + continue; + } + + snprintf(sLocalsNames[rowCount], BAS_MAX_PROC_NAME, "%s", dv->name); + snprintf(sLocalsTypes[rowCount], 16, "%s", typeNameStr(dv->dataType)); + + // Read the value + BasValueT val; + memset(&val, 0, sizeof(val)); + + if (dv->scope == SCOPE_LOCAL && sVm->callDepth > 0) { + BasCallFrameT *frame = &sVm->callStack[sVm->callDepth - 1]; + + if (dv->index >= 0 && dv->index < BAS_VM_MAX_LOCALS) { + val = frame->locals[dv->index]; + } + } else if (dv->scope == SCOPE_GLOBAL) { + if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) { + val = sVm->globals[dv->index]; + } + } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) { + if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) { + val = sVm->currentFormVars[dv->index]; + } + } + + formatValue(&val, sLocalsValues[rowCount], 64); + + sLocalsCells[rowCount * 3 + 0] = sLocalsNames[rowCount]; + sLocalsCells[rowCount * 3 + 1] = sLocalsTypes[rowCount]; + sLocalsCells[rowCount * 3 + 2] = sLocalsValues[rowCount]; + rowCount++; + } + } + + wgtListViewSetData(sLocalsList, sLocalsCells, rowCount); +} + + +// ============================================================ +// showCallStackWindow +// ============================================================ + +static void onCallStackClose(WindowT *win) { + dvxHideWindow(sAc, win); +} + + +static void showCallStackWindow(void) { + if (sCallStackWin) { + dvxShowWindow(sAc, sCallStackWin); + dvxRaiseWindow(sAc, sCallStackWin); + updateCallStackWindow(); + return; + } + + int32_t winW = 220; + int32_t winH = 180; + int32_t winX = sAc->display.width - winW; + int32_t winY = toolbarBottom() + 210; + + sCallStackWin = dvxCreateWindow(sAc, "Call Stack", winX, winY, winW, winH, true); + + if (sCallStackWin) { + sCallStackWin->onClose = onCallStackClose; + sCallStackWin->onMenu = onMenu; + sCallStackWin->accelTable = sWin ? sWin->accelTable : NULL; + sCallStackWin->resizable = true; + + WidgetT *root = wgtInitWindow(sAc, sCallStackWin); + + if (root) { + sCallStackList = wgtListView(root); + + if (sCallStackList) { + sCallStackList->weight = 100; + + static const ListViewColT cols[] = { + { "Procedure", wgtChars(16), ListViewAlignLeftE }, + { "Line", wgtChars(6), ListViewAlignRightE }, + }; + + wgtListViewSetColumns(sCallStackList, cols, 2); + } + } + } + + updateCallStackWindow(); +} + + +// ============================================================ +// updateCallStackWindow +// ============================================================ + +#define MAX_CALLSTACK_DISPLAY 32 + +static char sCallNames[MAX_CALLSTACK_DISPLAY][BAS_MAX_PROC_NAME]; +static char sCallLines[MAX_CALLSTACK_DISPLAY][16]; +static const char *sCallCells[MAX_CALLSTACK_DISPLAY * 2]; + +static void updateCallStackWindow(void) { + if (!sCallStackList || !sCallStackWin || !sCallStackWin->visible) { + return; + } + + if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) { + wgtListViewSetData(sCallStackList, NULL, 0); + return; + } + + int32_t rowCount = 0; + + // Current location first + if (sDbgCurrentLine > 0) { + // Find proc name for current PC + const char *procName = "(module)"; + + for (int32_t i = 0; i < sDbgModule->procCount; i++) { + if (sDbgModule->procs[i].codeAddr <= sVm->pc) { + bool best = true; + + for (int32_t j = 0; j < sDbgModule->procCount; j++) { + if (sDbgModule->procs[j].codeAddr > sDbgModule->procs[i].codeAddr && + sDbgModule->procs[j].codeAddr <= sVm->pc) { + best = false; + break; + } + } + + if (best) { + procName = sDbgModule->procs[i].name; + } + } + } + + snprintf(sCallNames[rowCount], BAS_MAX_PROC_NAME, "%s", procName); + snprintf(sCallLines[rowCount], 16, "%d", (int)sDbgCurrentLine); + sCallCells[rowCount * 2 + 0] = sCallNames[rowCount]; + sCallCells[rowCount * 2 + 1] = sCallLines[rowCount]; + rowCount++; + } + + // Walk call stack (skip frame 0 which is the implicit module frame) + for (int32_t d = sVm->callDepth - 2; d >= 0 && rowCount < MAX_CALLSTACK_DISPLAY; d--) { + int32_t retPc = sVm->callStack[d + 1].returnPc; + const char *name = "(module)"; + + for (int32_t i = 0; i < sDbgModule->procCount; i++) { + if (sDbgModule->procs[i].codeAddr <= retPc) { + bool best = true; + + for (int32_t j = 0; j < sDbgModule->procCount; j++) { + if (sDbgModule->procs[j].codeAddr > sDbgModule->procs[i].codeAddr && + sDbgModule->procs[j].codeAddr <= retPc) { + best = false; + break; + } + } + + if (best) { + name = sDbgModule->procs[i].name; + } + } + } + + snprintf(sCallNames[rowCount], BAS_MAX_PROC_NAME, "%s", name); + snprintf(sCallLines[rowCount], 16, ""); + sCallCells[rowCount * 2 + 0] = sCallNames[rowCount]; + sCallCells[rowCount * 2 + 1] = sCallLines[rowCount]; + rowCount++; + } + + wgtListViewSetData(sCallStackList, sCallCells, rowCount); +} + + +// ============================================================ +// showWatchWindow +// ============================================================ + +static void onWatchClose(WindowT *win) { + dvxHideWindow(sAc, win); +} + + +static void onWatchInputChange(WidgetT *w) { + (void)w; + + if (!sWatchInput) { + return; + } + + const char *text = wgtGetText(sWatchInput); + + if (!text) { + return; + } + + int32_t len = (int32_t)strlen(text); + + if (len < 2 || text[len - 1] != '\n') { + return; + } + + // Extract expression (strip trailing newline) + char expr[256]; + int32_t lineEnd = len - 1; + int32_t lineStart = lineEnd - 1; + + while (lineStart > 0 && text[lineStart - 1] != '\n') { + lineStart--; + } + + int32_t lineLen = lineEnd - lineStart; + + if (lineLen <= 0 || lineLen >= (int32_t)sizeof(expr)) { + return; + } + + memcpy(expr, text + lineStart, lineLen); + expr[lineLen] = '\0'; + + // Add to watch list + if (sWatchExprCount < 16) { + sWatchExprs[sWatchExprCount++] = strdup(expr); + updateWatchWindow(); + } + + // Clear input + wgtSetText(sWatchInput, ""); +} + + +static void showWatchWindow(void) { + if (sWatchWin) { + dvxShowWindow(sAc, sWatchWin); + dvxRaiseWindow(sAc, sWatchWin); + updateWatchWindow(); + return; + } + + int32_t winW = 280; + int32_t winH = 180; + int32_t winX = sAc->display.width - winW - 260; + int32_t winY = toolbarBottom(); + + sWatchWin = dvxCreateWindow(sAc, "Watch", winX, winY, winW, winH, true); + + if (sWatchWin) { + sWatchWin->onClose = onWatchClose; + sWatchWin->onMenu = onMenu; + sWatchWin->accelTable = sWin ? sWin->accelTable : NULL; + sWatchWin->resizable = true; + + WidgetT *root = wgtInitWindow(sAc, sWatchWin); + + if (root) { + // Expression input at top + sWatchInput = wgtTextInput(root, 256); + + if (sWatchInput) { + sWatchInput->onChange = onWatchInputChange; + } + + // Results list below + sWatchList = wgtListView(root); + + if (sWatchList) { + sWatchList->weight = 100; + + static const ListViewColT cols[] = { + { "Expression", wgtChars(14), ListViewAlignLeftE }, + { "Value", wgtChars(20), ListViewAlignLeftE }, + }; + + wgtListViewSetColumns(sWatchList, cols, 2); + } + } + } + + updateWatchWindow(); +} + + +// ============================================================ +// updateWatchWindow -- evaluate watch expressions +// ============================================================ + +#define MAX_WATCH_DISPLAY 16 + +static char sWatchExprBuf[MAX_WATCH_DISPLAY][256]; +static char sWatchValBuf[MAX_WATCH_DISPLAY][256]; +static const char *sWatchCells[MAX_WATCH_DISPLAY * 2]; + +static void updateWatchWindow(void) { + if (!sWatchList || !sWatchWin || !sWatchWin->visible) { + return; + } + + if (sWatchExprCount == 0) { + wgtListViewSetData(sWatchList, NULL, 0); + return; + } + + for (int32_t i = 0; i < sWatchExprCount; i++) { + snprintf(sWatchExprBuf[i], 256, "%s", sWatchExprs[i]); + snprintf(sWatchValBuf[i], 256, ""); + + if (sDbgState == DBG_PAUSED && sVm && sDbgModule) { + // Compile and evaluate the expression in a temp VM with state snapshot + char wrapped[512]; + snprintf(wrapped, sizeof(wrapped), "PRINT %s", sWatchExprs[i]); + + BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT)); + + if (parser) { + basParserInit(parser, wrapped, (int32_t)strlen(wrapped)); + + if (basParse(parser)) { + BasModuleT *mod = basParserBuildModule(parser); + + if (mod) { + BasVmT *tvm = basVmCreate(); + basVmLoadModule(tvm, mod); + tvm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount; + tvm->callDepth = 1; + + // Copy globals from the running VM + for (int32_t g = 0; g < BAS_VM_MAX_GLOBALS && g < sDbgModule->globalCount; g++) { + tvm->globals[g] = basValCopy(sVm->globals[g]); + } + + // Capture output + static char watchOut[256]; + static int32_t watchOutLen; + watchOut[0] = '\0'; + watchOutLen = 0; + + // Use a simple inline print callback + basVmSetPrintCallback(tvm, immPrintCallback, NULL); + + // Redirect immPrintCallback output temporarily + // Actually, let's use the output buffer directly + // For simplicity, capture via a static buffer + int32_t savedImmLen = 0; + char savedImm[64] = ""; + + // Save immediate buffer state + if (sImmediate) { + const char *immText = wgtGetText(sImmediate); + + if (immText) { + savedImmLen = (int32_t)strlen(immText); + } + } + + basVmRun(tvm); + + // Read what was printed (diff from saved) + if (sImmediate) { + const char *immText = wgtGetText(sImmediate); + + if (immText && (int32_t)strlen(immText) > savedImmLen) { + const char *newPart = immText + savedImmLen; + int32_t nl = (int32_t)strlen(newPart); + + // Strip trailing newline + if (nl > 0 && newPart[nl - 1] == '\n') { + nl--; + } + + if (nl > 255) { + nl = 255; + } + + memcpy(sWatchValBuf[i], newPart, nl); + sWatchValBuf[i][nl] = '\0'; + + // Restore immediate window text + char *restoreBuf = (char *)malloc(savedImmLen + 1); + + if (restoreBuf) { + memcpy(restoreBuf, immText, savedImmLen); + restoreBuf[savedImmLen] = '\0'; + wgtSetText(sImmediate, restoreBuf); + free(restoreBuf); + } + } + } + + basVmDestroy(tvm); + basModuleFree(mod); + } + } else { + snprintf(sWatchValBuf[i], 256, ""); + } + + basParserFree(parser); + free(parser); + } + } + + sWatchCells[i * 2 + 0] = sWatchExprBuf[i]; + sWatchCells[i * 2 + 1] = sWatchValBuf[i]; + } + + wgtListViewSetData(sWatchList, sWatchCells, sWatchExprCount); +} + + // ============================================================ // setStatus // ============================================================ diff --git a/apps/dvxbasic/runtime/vm.c b/apps/dvxbasic/runtime/vm.c index 24568f5..e9de43e 100644 --- a/apps/dvxbasic/runtime/vm.c +++ b/apps/dvxbasic/runtime/vm.c @@ -61,6 +61,15 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool break; } + if (result == BAS_VM_BREAKPOINT) { + // Pause the VM so the host's debug loop can take over. + // Don't restore savedPc/savedRunning — the VM stays at + // the breakpoint location. The host will resume execution + // which will eventually return here and complete the sub. + vm->running = false; + return true; + } + if (result != BAS_VM_OK) { vm->pc = savedPc; vm->callDepth = savedCallDepth; @@ -223,7 +232,10 @@ BasVmT *basVmCreate(void) { return NULL; } - vm->printFn = defaultPrint; + vm->printFn = defaultPrint; + vm->stepOverDepth = -1; + vm->stepOutDepth = -1; + vm->runToCursorLine = -1; basStringSystemInit(); return vm; } @@ -282,6 +294,53 @@ const char *basVmGetError(const BasVmT *vm) { } +// ============================================================ +// Debugger API +// ============================================================ + +void basVmSetBreakpoints(BasVmT *vm, int32_t *lines, int32_t count) { + if (!vm) { + return; + } + + vm->breakpoints = lines; + vm->breakpointCount = count; +} + + +void basVmStepInto(BasVmT *vm) { + if (vm) { + vm->debugBreak = true; + } +} + + +void basVmStepOver(BasVmT *vm) { + if (vm) { + vm->stepOverDepth = vm->callDepth; + } +} + + +void basVmStepOut(BasVmT *vm) { + if (vm) { + vm->stepOutDepth = vm->callDepth; + } +} + + +void basVmRunToCursor(BasVmT *vm, int32_t line) { + if (vm) { + vm->runToCursorLine = line; + } +} + + +int32_t basVmGetCurrentLine(const BasVmT *vm) { + return vm ? vm->currentLine : 0; +} + + // ============================================================ // basVmLoadModule // ============================================================ @@ -3271,6 +3330,44 @@ BasVmResultE basVmStep(BasVmT *vm) { break; } + case OP_LINE: { + uint16_t lineNum = readUint16(vm); + vm->currentLine = lineNum; + + // Step into: break at any OP_LINE + if (vm->debugBreak) { + vm->debugBreak = false; + return BAS_VM_BREAKPOINT; + } + + // Step over: break when call depth returns to target level + if (vm->stepOverDepth >= 0 && vm->callDepth <= vm->stepOverDepth) { + vm->stepOverDepth = -1; + return BAS_VM_BREAKPOINT; + } + + // Step out: break when call depth drops below target + if (vm->stepOutDepth >= 0 && vm->callDepth < vm->stepOutDepth) { + vm->stepOutDepth = -1; + return BAS_VM_BREAKPOINT; + } + + // Run to cursor + if (vm->runToCursorLine >= 0 && (int32_t)lineNum == vm->runToCursorLine) { + vm->runToCursorLine = -1; + return BAS_VM_BREAKPOINT; + } + + // Breakpoint check (linear scan, typically < 20 entries) + for (int32_t i = 0; i < vm->breakpointCount; i++) { + if (vm->breakpoints[i] == (int32_t)lineNum) { + return BAS_VM_BREAKPOINT; + } + } + + break; + } + case OP_APP_PATH: { push(vm, basValStringFromC(vm->appPath)); break; @@ -4943,5 +5040,10 @@ static uint16_t readUint16(BasVmT *vm) { static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg) { vm->errorNumber = errNum; - snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", (int)errNum, (int)vm->pc, msg); + + if (vm->currentLine > 0) { + snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d on line %d: %s", (int)errNum, (int)vm->currentLine, msg); + } else { + snprintf(vm->errorMsg, sizeof(vm->errorMsg), "Runtime error %d at PC %d: %s", (int)errNum, (int)vm->pc, msg); + } } diff --git a/apps/dvxbasic/runtime/vm.h b/apps/dvxbasic/runtime/vm.h index e044360..8e13274 100644 --- a/apps/dvxbasic/runtime/vm.h +++ b/apps/dvxbasic/runtime/vm.h @@ -50,7 +50,8 @@ typedef enum { BAS_VM_FILE_ERROR, BAS_VM_SUBSCRIPT_RANGE, BAS_VM_USER_ERROR, // ON ERROR raised - BAS_VM_STEP_LIMIT // step limit reached (not an error) + BAS_VM_STEP_LIMIT, // step limit reached (not an error) + BAS_VM_BREAKPOINT // hit breakpoint or step completed (not an error) } BasVmResultE; // ============================================================ @@ -243,10 +244,20 @@ typedef struct { char name[BAS_MAX_PROC_NAME]; // SUB/FUNCTION name (case-preserved) int32_t codeAddr; // entry point in code[] int32_t paramCount; // number of parameters + int32_t localCount; // number of local variables (for debugger) uint8_t returnType; // BAS_TYPE_* (0 for SUB) bool isFunction; // true = FUNCTION, false = SUB } BasProcEntryT; +// Debug variable info (preserved in module for debugger display) +typedef struct { + char name[BAS_MAX_PROC_NAME]; + uint8_t scope; // SCOPE_GLOBAL, SCOPE_LOCAL, SCOPE_FORM + uint8_t dataType; // BAS_TYPE_* + int32_t index; // variable slot index + int32_t procIndex; // -1 for globals, else index into procs[] +} BasDebugVarT; + // ============================================================ // Per-form variable info (how many form-scoped vars each form needs) // ============================================================ @@ -275,6 +286,8 @@ typedef struct { int32_t procCount; BasFormVarInfoT *formVarInfo; // per-form variable counts int32_t formVarInfoCount; + BasDebugVarT *debugVars; // variable names for debugger + int32_t debugVarCount; } BasModuleT; // ============================================================ @@ -292,6 +305,15 @@ typedef struct { bool yielded; int32_t stepLimit; // max steps per basVmRun (0 = unlimited) int32_t stepCount; // steps executed in last basVmRun + int32_t currentLine; // source line from last OP_LINE (debugger) + + // Debug state + int32_t *breakpoints; // sorted line numbers (host-managed) + int32_t breakpointCount; + bool debugBreak; // break at next OP_LINE (step into) + int32_t stepOverDepth; // call depth target for step over (-1 = off) + int32_t stepOutDepth; // call depth target for step out (-1 = off) + int32_t runToCursorLine; // target line for run-to-cursor (-1 = off) // Evaluation stack BasValueT stack[BAS_VM_STACK_SIZE]; @@ -407,6 +429,26 @@ bool basVmPop(BasVmT *vm, BasValueT *val); // Get the current error message. const char *basVmGetError(const BasVmT *vm); +// ---- Debugger API ---- + +// Set the breakpoint list (sorted array of source line numbers, host-owned). +void basVmSetBreakpoints(BasVmT *vm, int32_t *lines, int32_t count); + +// Step into: break at the next OP_LINE instruction. +void basVmStepInto(BasVmT *vm); + +// Step over: break when call depth returns to current level. +void basVmStepOver(BasVmT *vm); + +// Step out: break when call depth drops below current level. +void basVmStepOut(BasVmT *vm); + +// Run to cursor: break when reaching the specified source line. +void basVmRunToCursor(BasVmT *vm, int32_t line); + +// Get the current source line (from the last OP_LINE instruction). +int32_t basVmGetCurrentLine(const BasVmT *vm); + // Call a SUB by code address from the host. // Pushes a call frame, runs until the SUB returns, then restores // the previous execution state. Returns true if the SUB was called diff --git a/assets/splash.bmp b/assets/splash.bmp new file mode 100644 index 0000000..195b2d3 --- /dev/null +++ b/assets/splash.bmp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ef75dd66106220009cddc520a5b399e8ebad189928496b28f924d9a24490c31 +size 64986 diff --git a/core/platform/dvxPlatform.h b/core/platform/dvxPlatform.h index 3680292..650ff0d 100644 --- a/core/platform/dvxPlatform.h +++ b/core/platform/dvxPlatform.h @@ -312,6 +312,23 @@ void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal // also available for manual invocation if needed. void platformLogCrashDetail(int sig, PlatformLogFnT logFn); +// ============================================================ +// VGA splash screen (mode 13h, 320x200, 256-color) +// ============================================================ + +// Enter VGA mode 13h (320x200x256). +void platformSplashInit(void); + +// Return to text mode 03h. +void platformSplashShutdown(void); + +// Load and display a raw splash file (768 bytes palette + 64000 bytes pixels). +// Returns true on success, false if the file could not be loaded. +bool platformSplashLoadRaw(const char *path); + +// Fill a rectangle on the VGA mode 13h screen. +void platformSplashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t color); + // ============================================================ // DXE symbol overrides // ============================================================ diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index 9ebcf23..5fa0f2f 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -2761,3 +2761,83 @@ DXE_EXPORT_END void platformRegisterDxeExports(void) { dlregsym(sDxeExportTable); } + + +// ============================================================ +// VGA splash screen (mode 13h) +// ============================================================ + +#define SPLASH_VGA_W 320 +#define SPLASH_VGA_H 200 +#define SPLASH_VGA_ADDR 0xA0000 + +void platformSplashInit(void) { + __dpmi_regs r; + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0013; + __dpmi_int(0x10, &r); +} + + +void platformSplashShutdown(void) { + __dpmi_regs r; + memset(&r, 0, sizeof(r)); + r.x.ax = 0x0003; + __dpmi_int(0x10, &r); +} + + +void platformSplashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t color) { + for (int32_t row = y; row < y + h; row++) { + if (row < 0 || row >= SPLASH_VGA_H) { + continue; + } + + int32_t x0 = x < 0 ? 0 : x; + int32_t x1 = (x + w) > SPLASH_VGA_W ? SPLASH_VGA_W : (x + w); + + for (int32_t col = x0; col < x1; col++) { + _farpokeb(_dos_ds, SPLASH_VGA_ADDR + row * SPLASH_VGA_W + col, color); + } + } +} + + +bool platformSplashLoadRaw(const char *path) { + FILE *fp = fopen(path, "rb"); + + if (!fp) { + return false; + } + + // Read and set palette (768 bytes: 256 x RGB, 6-bit VGA values) + uint8_t pal[768]; + + if (fread(pal, 1, 768, fp) != 768) { + fclose(fp); + return false; + } + + outportb(0x3C8, 0); + + for (int32_t i = 0; i < 768; i++) { + outportb(0x3C9, pal[i]); + } + + // Read pixels directly into VGA memory (64000 bytes: 320x200) + uint8_t rowBuf[SPLASH_VGA_W]; + + for (int32_t y = 0; y < SPLASH_VGA_H; y++) { + if (fread(rowBuf, 1, SPLASH_VGA_W, fp) != SPLASH_VGA_W) { + fclose(fp); + return false; + } + + for (int32_t x = 0; x < SPLASH_VGA_W; x++) { + _farpokeb(_dos_ds, SPLASH_VGA_ADDR + y * SPLASH_VGA_W + x, rowBuf[x]); + } + } + + fclose(fp); + return true; +} diff --git a/loader/loaderMain.c b/loader/loaderMain.c index b2f2889..ed5cfed 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -15,14 +15,11 @@ #include #include #include -#include -#include #include #include #include #include #include -#include #include // Route stb_ds allocations through the tracking wrappers so that @@ -43,137 +40,30 @@ extern void dvxFree(void *ptr); #define LOG_PATH "dvx.log" // ============================================================ -// VGA mode 13h splash screen (320x200, 256-color) +// Splash screen (delegates to platformSplash* in dvxPlatformDos.c) // ============================================================ -#define VGA_WIDTH 320 -#define VGA_HEIGHT 200 -#define VGA_ADDR 0xA0000 -#define VGA_SIZE (VGA_WIDTH * VGA_HEIGHT) - -// Palette indices for splash colors -#define SPLASH_BG 0 // black -#define SPLASH_FG 15 // white -#define SPLASH_BAR_BG 8 // dark gray -#define SPLASH_BAR_FG 11 // cyan -#define SPLASH_DIM 7 // light gray +// Palette indices for progress bar (indices into SPLASH.RAW palette) +#define SPLASH_BAR_BG 50 // dark gray (RGB 68,68,68) +#define SPLASH_BAR_FG 135 // light gray (RGB 168,168,168) +#define SPLASH_BAR_OUT 45 // darker gray (RGB 60,60,60) static int32_t sSplashActive = 0; static int32_t sSplashTotal = 0; static int32_t sSplashLoaded = 0; // Progress bar geometry -#define PBAR_X 60 -#define PBAR_Y 140 -#define PBAR_W 200 -#define PBAR_H 10 - - -static void splashSetMode13h(void) { - __dpmi_regs r; - memset(&r, 0, sizeof(r)); - r.x.ax = 0x0013; - __dpmi_int(0x10, &r); - sSplashActive = 1; -} - - -static void splashRestoreTextMode(void) { - __dpmi_regs r; - memset(&r, 0, sizeof(r)); - r.x.ax = 0x0003; - __dpmi_int(0x10, &r); - sSplashActive = 0; -} - - -static void splashPutPixel(int32_t x, int32_t y, uint8_t color) { - if (x >= 0 && x < VGA_WIDTH && y >= 0 && y < VGA_HEIGHT) { - _farpokeb(_dos_ds, VGA_ADDR + y * VGA_WIDTH + x, color); - } -} - - -static void splashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t color) { - for (int32_t row = y; row < y + h; row++) { - if (row < 0 || row >= VGA_HEIGHT) { - continue; - } - - int32_t x0 = x < 0 ? 0 : x; - int32_t x1 = (x + w) > VGA_WIDTH ? VGA_WIDTH : (x + w); - - for (int32_t col = x0; col < x1; col++) { - _farpokeb(_dos_ds, VGA_ADDR + row * VGA_WIDTH + col, color); - } - } -} - - -// Get the VGA ROM 8x8 font address via INT 10h AH=11h BH=03h -static uint32_t splashGetFontAddr(void) { - __dpmi_regs r; - memset(&r, 0, sizeof(r)); - r.x.ax = 0x1130; - r.h.bh = 0x03; // 8x8 ROM font - __dpmi_int(0x10, &r); - return (uint32_t)r.x.es * 16 + r.x.bp; -} - - -static uint32_t sFontAddr = 0; - -static void splashDrawChar(int32_t x, int32_t y, char ch, uint8_t color) { - if (!sFontAddr) { - sFontAddr = splashGetFontAddr(); - } - - uint32_t charAddr = sFontAddr + (uint8_t)ch * 8; - - for (int32_t row = 0; row < 8; row++) { - uint8_t bits = _farpeekb(_dos_ds, charAddr + row); - - for (int32_t col = 0; col < 8; col++) { - if (bits & (0x80 >> col)) { - splashPutPixel(x + col, y + row, color); - } - } - } -} - - -static void splashDrawText(int32_t x, int32_t y, const char *text, uint8_t color) { - for (const char *c = text; *c; c++) { - splashDrawChar(x, y, *c, color); - x += 8; - } -} - - -static void splashDrawTextCentered(int32_t y, const char *text, uint8_t color) { - int32_t len = (int32_t)strlen(text); - int32_t x = (VGA_WIDTH - len * 8) / 2; - splashDrawText(x, y, text, color); -} +#define PBAR_X 10 +#define PBAR_Y 188 +#define PBAR_W 300 +#define PBAR_H 6 static void splashDrawScreen(void) { - // Clear to black - splashFillRect(0, 0, VGA_WIDTH, VGA_HEIGHT, SPLASH_BG); - - // Title - splashDrawTextCentered(60, "DOS Visual eXecutive", SPLASH_FG); - splashDrawTextCentered(75, "(DVX)", SPLASH_DIM); - - // Copyright - splashDrawTextCentered(100, "Copyright 2026 Scott Duensing", SPLASH_DIM); - - // Loading text - splashDrawTextCentered(125, "Loading...", SPLASH_FG); - - // Progress bar outline - splashFillRect(PBAR_X - 1, PBAR_Y - 1, PBAR_W + 2, PBAR_H + 2, SPLASH_DIM); - splashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG); + if (platformSplashLoadRaw("CONFIG/SPLASH.RAW")) { + platformSplashFillRect(PBAR_X - 1, PBAR_Y - 1, PBAR_W + 2, PBAR_H + 2, SPLASH_BAR_OUT); + platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG); + } } @@ -188,7 +78,7 @@ static void splashUpdateProgress(void) { fillW = PBAR_W; } - splashFillRect(PBAR_X, PBAR_Y, fillW, PBAR_H, SPLASH_BAR_FG); + platformSplashFillRect(PBAR_X, PBAR_Y, fillW, PBAR_H, SPLASH_BAR_FG); } // ============================================================ @@ -437,7 +327,8 @@ static void loadInOrder(ModuleT *mods) { if (!mods[i].handle) { const char *err = dlerror(); dvxLog(" FAILED: %s", err ? err : "(unknown)"); - splashRestoreTextMode(); + platformSplashShutdown(); + sSplashActive = 0; fprintf(stderr, "FATAL: Failed to load %s\n %s\n", mods[i].path, err ? err : "(unknown error)"); exit(1); } @@ -623,7 +514,8 @@ int main(int argc, char *argv[]) { platformInit(); // Switch to VGA mode 13h and show graphical splash - splashSetMode13h(); + platformSplashInit(); + sSplashActive = 1; splashDrawScreen(); dvxLog("DVX Loader starting..."); diff --git a/tools/Makefile b/tools/Makefile index 0e6119c..747dfa5 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -10,7 +10,9 @@ BINDIR = ../bin .PHONY: all clean -all: $(BINDIR)/dvxres $(BINDIR)/mkicon $(BINDIR)/mktbicon $(BINDIR)/mkwgticon +CONFIGDIR = ../bin/config + +all: $(BINDIR)/dvxres $(BINDIR)/mkicon $(BINDIR)/mktbicon $(BINDIR)/mkwgticon $(BINDIR)/bmp2raw $(CONFIGDIR)/SPLASH.RAW $(BINDIR)/dvxres: dvxres.c ../core/dvxResource.c ../core/dvxResource.h | $(BINDIR) $(CC) $(CFLAGS) -o $@ dvxres.c ../core/dvxResource.c @@ -24,8 +26,17 @@ $(BINDIR)/mktbicon: mktbicon.c | $(BINDIR) $(BINDIR)/mkwgticon: mkwgticon.c | $(BINDIR) $(CC) $(CFLAGS) -o $@ mkwgticon.c +$(BINDIR)/bmp2raw: bmp2raw.c | $(BINDIR) + $(CC) $(CFLAGS) -o $@ bmp2raw.c + +$(CONFIGDIR)/SPLASH.RAW: $(BINDIR)/bmp2raw ../assets/splash.bmp | $(CONFIGDIR) + $(BINDIR)/bmp2raw ../assets/splash.bmp $@ + $(BINDIR): mkdir -p $(BINDIR) +$(CONFIGDIR): + mkdir -p $(CONFIGDIR) + clean: rm -f $(BINDIR)/dvxres diff --git a/tools/bmp2raw.c b/tools/bmp2raw.c new file mode 100644 index 0000000..1701432 --- /dev/null +++ b/tools/bmp2raw.c @@ -0,0 +1,128 @@ +// bmp2raw.c -- Convert a 320x200x256 BMP to raw VGA splash format +// +// Output format: +// 768 bytes: palette (256 x 3 bytes RGB, 6-bit VGA values) +// 64000 bytes: pixels (320x200, top-to-bottom scanline order) +// +// Total output: 64768 bytes + +#include +#include +#include +#include +#include + +#define VGA_W 320 +#define VGA_H 200 +#define PALETTE_ENTRIES 256 +#define PIXEL_COUNT (VGA_W * VGA_H) + +int main(int argc, char **argv) { + if (argc != 3) { + fprintf(stderr, "Usage: bmp2raw \n"); + return 1; + } + + FILE *fp = fopen(argv[1], "rb"); + + if (!fp) { + fprintf(stderr, "Cannot open %s\n", argv[1]); + return 1; + } + + // Read BMP header + uint8_t hdr[54]; + + if (fread(hdr, 1, 54, fp) != 54) { + fprintf(stderr, "Bad BMP header\n"); + fclose(fp); + return 1; + } + + if (hdr[0] != 'B' || hdr[1] != 'M') { + fprintf(stderr, "Not a BMP file\n"); + fclose(fp); + return 1; + } + + uint32_t pixelOffset = hdr[10] | (hdr[11] << 8) | (hdr[12] << 16) | (hdr[13] << 24); + int32_t width = hdr[18] | (hdr[19] << 8) | (hdr[20] << 16) | (hdr[21] << 24); + int32_t height = hdr[22] | (hdr[23] << 8) | (hdr[24] << 16) | (hdr[25] << 24); + uint16_t bpp = hdr[28] | (hdr[29] << 8); + + if (width != VGA_W || abs(height) != VGA_H || bpp != 8) { + fprintf(stderr, "BMP must be %dx%d 8bpp (got %dx%d %dbpp)\n", VGA_W, VGA_H, width, abs(height), bpp); + fclose(fp); + return 1; + } + + bool topDown = (height < 0); + + if (height < 0) { + height = -height; + } + + // Read palette (starts at offset 54 for BITMAPINFOHEADER, may vary) + uint32_t dibSize = hdr[14] | (hdr[15] << 8) | (hdr[16] << 16) | (hdr[17] << 24); + uint32_t palOffset = 14 + dibSize; // BMP file header (14) + DIB header + + fseek(fp, palOffset, SEEK_SET); + + uint8_t bmpPal[PALETTE_ENTRIES * 4]; + + if (fread(bmpPal, 4, PALETTE_ENTRIES, fp) != PALETTE_ENTRIES) { + fprintf(stderr, "Cannot read palette\n"); + fclose(fp); + return 1; + } + + // Convert palette: BMP is BGRA 8-bit, VGA is RGB 6-bit + uint8_t vgaPal[PALETTE_ENTRIES * 3]; + + for (int i = 0; i < PALETTE_ENTRIES; i++) { + vgaPal[i * 3 + 0] = bmpPal[i * 4 + 2] >> 2; // R + vgaPal[i * 3 + 1] = bmpPal[i * 4 + 1] >> 2; // G + vgaPal[i * 3 + 2] = bmpPal[i * 4 + 0] >> 2; // B + } + + // Read pixel data + fseek(fp, pixelOffset, SEEK_SET); + + // BMP rows are padded to 4-byte boundaries (320 is already aligned) + uint8_t pixels[PIXEL_COUNT]; + + if (topDown) { + // Already top-to-bottom + if (fread(pixels, 1, PIXEL_COUNT, fp) != PIXEL_COUNT) { + fprintf(stderr, "Cannot read pixel data\n"); + fclose(fp); + return 1; + } + } else { + // Bottom-to-top — flip + for (int y = VGA_H - 1; y >= 0; y--) { + if (fread(pixels + y * VGA_W, 1, VGA_W, fp) != VGA_W) { + fprintf(stderr, "Cannot read row %d\n", VGA_H - 1 - y); + fclose(fp); + return 1; + } + } + } + + fclose(fp); + + // Write output + FILE *out = fopen(argv[2], "wb"); + + if (!out) { + fprintf(stderr, "Cannot create %s\n", argv[2]); + return 1; + } + + fwrite(vgaPal, 1, sizeof(vgaPal), out); + fwrite(pixels, 1, PIXEL_COUNT, out); + fclose(out); + + printf("Converted %s -> %s (%d bytes)\n", argv[1], argv[2], (int)(sizeof(vgaPal) + PIXEL_COUNT)); + return 0; +} diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index f0383f6..07c18bf 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -113,6 +113,15 @@ typedef struct { // The callback fills colors[0..lineLen-1]. void (*colorize)(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); void *colorizeCtx; + + // Line decorator callback (optional). Called for each visible line during paint. + // Returns background color override (0 = use default). Sets *gutterColor to a + // non-zero color to draw a filled circle in the gutter for breakpoints. + uint32_t (*lineDecorator)(int32_t lineNum, uint32_t *gutterColor, void *ctx); + void *lineDecoratorCtx; + + // Gutter click callback (optional). Fired when user clicks in the gutter. + void (*onGutterClick)(WidgetT *w, int32_t lineNum); } TextAreaDataT; #include @@ -1580,6 +1589,19 @@ void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { return; } + // Click on gutter — toggle breakpoint + if (gutterW > 0 && vx < innerX && ta->onGutterClick) { + int32_t relY = vy - innerY; + int32_t clickRow = ta->scrollRow + relY / font->charHeight; + + if (clickRow >= 0 && clickRow < totalLines) { + ta->onGutterClick(w, clickRow + 1); // 1-based line number + } + + wgtInvalidatePaint(w); + return; + } + // Click on text area -- place cursor int32_t relX = vx - innerX; int32_t relY = vy - innerY; @@ -1792,14 +1814,44 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } int32_t drawY = textY + i * font->charHeight; + // Line decorator: background highlight and gutter indicators + uint32_t lineBg = bg; + uint32_t gutterColor = 0; + + if (ta->lineDecorator) { + uint32_t decBg = ta->lineDecorator(row + 1, &gutterColor, ta->lineDecoratorCtx); + + if (decBg) { + lineBg = decBg; + rectFill(d, ops, textX, drawY, innerW, font->charHeight, lineBg); + } + } + // Draw line number in gutter if (gutterW > 0) { + // Gutter indicator (breakpoint dot) + if (gutterColor) { + int32_t dotR = font->charHeight / 4; + int32_t dotX = gutterX + 2 + dotR; + int32_t dotY = drawY + font->charHeight / 2; + + for (int32_t dy = -dotR; dy <= dotR; dy++) { + int32_t hw = dotR - (dy < 0 ? -dy : dy); + drawHLine(d, ops, dotX - hw, dotY + dy, hw * 2 + 1, gutterColor); + } + } + char numBuf[12]; int32_t numLen = snprintf(numBuf, sizeof(numBuf), "%d", (int)(row + 1)); int32_t numX = gutterX + gutterW - (numLen + 1) * font->charWidth; drawTextN(d, ops, font, numX, drawY, numBuf, numLen, colors->windowShadow, colors->windowFace, true); } + // Override bg for this line if the decorator set a custom background. + // This ensures text character backgrounds match the highlighted line. + uint32_t savedBg = bg; + bg = lineBg; + // Visible range within line int32_t scrollCol = ta->scrollCol; int32_t visStart = scrollCol; @@ -1887,6 +1939,9 @@ void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } } + // Restore bg for next line + bg = savedBg; + // Advance lineOff to the next line lineOff += lineL; if (lineOff < len && buf[lineOff] == '\n') { @@ -2635,6 +2690,37 @@ void wgtTextAreaSetColorize(WidgetT *w, void (*fn)(const char *, int32_t, uint8_ } +void wgtTextAreaSetGutterClickCallback(WidgetT *w, void (*fn)(WidgetT *, int32_t)) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->onGutterClick = fn; +} + + +int32_t wgtTextAreaGetCursorLine(const WidgetT *w) { + if (!w || w->type != sTextAreaTypeId) { + return 1; + } + + const TextAreaDataT *ta = (const TextAreaDataT *)w->data; + return ta->cursorRow + 1; // 0-based to 1-based +} + + +void wgtTextAreaSetLineDecorator(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx) { + if (!w || w->type != sTextAreaTypeId) { + return; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + ta->lineDecorator = fn; + ta->lineDecoratorCtx = ctx; +} + + void wgtTextAreaSetShowLineNumbers(WidgetT *w, bool show) { if (!w || w->type != sTextAreaTypeId) { return; @@ -2853,6 +2939,9 @@ static const struct { void (*setUseTabChar)(WidgetT *w, bool useChar); bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); + void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); + int32_t (*getCursorLine)(const WidgetT *w); + void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); } sApi = { .create = wgtTextInput, .password = wgtPasswordInput, @@ -2866,7 +2955,10 @@ static const struct { .setTabWidth = wgtTextAreaSetTabWidth, .setUseTabChar = wgtTextAreaSetUseTabChar, .findNext = wgtTextAreaFindNext, - .replaceAll = wgtTextAreaReplaceAll + .replaceAll = wgtTextAreaReplaceAll, + .setLineDecorator = wgtTextAreaSetLineDecorator, + .getCursorLine = wgtTextAreaGetCursorLine, + .setGutterClick = wgtTextAreaSetGutterClickCallback }; // Per-type APIs for the designer diff --git a/widgets/widgetTextInput.h b/widgets/widgetTextInput.h index 1931965..345c278 100644 --- a/widgets/widgetTextInput.h +++ b/widgets/widgetTextInput.h @@ -27,6 +27,9 @@ typedef struct { void (*setUseTabChar)(WidgetT *w, bool useChar); bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); + void (*setLineDecorator)(WidgetT *w, uint32_t (*fn)(int32_t, uint32_t *, void *), void *ctx); + int32_t (*getCursorLine)(const WidgetT *w); + void (*setGutterClick)(WidgetT *w, void (*fn)(WidgetT *, int32_t)); } TextInputApiT; static inline const TextInputApiT *dvxTextInputApi(void) { @@ -48,5 +51,8 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaSetUseTabChar(w, useChar) dvxTextInputApi()->setUseTabChar(w, useChar) #define wgtTextAreaFindNext(w, needle, caseSens, fwd) dvxTextInputApi()->findNext(w, needle, caseSens, fwd) #define wgtTextAreaReplaceAll(w, needle, repl, caseSens) dvxTextInputApi()->replaceAll(w, needle, repl, caseSens) +#define wgtTextAreaSetLineDecorator(w, fn, ctx) dvxTextInputApi()->setLineDecorator(w, fn, ctx) +#define wgtTextAreaGetCursorLine(w) dvxTextInputApi()->getCursorLine(w) +#define wgtTextAreaSetGutterClick(w, fn) dvxTextInputApi()->setGutterClick(w, fn) #endif // WIDGET_TEXTINPUT_H