// ideMain.c -- DVX BASIC Runner application
//
// A DVX app that loads, compiles, and runs BASIC programs.
// PRINT output goes to a scrollable TextArea widget. Compile
// errors are displayed with line numbers.
//
// This is Phase 3 of DVX BASIC: proving the compiler and VM
// work on real hardware inside the DVX windowing system.

#include "dvxApp.h"
#include "dvxCur.h"
#include "dvxPlat.h"
#include "dvxDlg.h"
#include "dvxPrefs.h"
#include "dvxWgt.h"
#include "dvxWgtP.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "box/box.h"
#include "checkbox/checkbox.h"
#include "imageButton/imgBtn.h"
#include "label/label.h"
#include "radio/radio.h"
#include "textInput/textInpt.h"
#include "dropdown/dropdown.h"
#include "canvas/canvas.h"
#include "listBox/listBox.h"
#include "slider/slider.h"
#include "tabControl/tabCtrl.h"
#include "button/button.h"
#include "splitter/splitter.h"
#include "statusBar/statBar.h"
#include "listView/listView.h"
#include "separator/separatr.h"
#include "toolbar/toolbar.h"

#include "ideDesigner.h"
#include "ideProject.h"
#include "ideMenuEditor.h"
#include "ideToolbox.h"
#include "ideProperties.h"

#include "../basBuild.h"
#include "../basRes.h"
#include "../compiler/obfuscate.h"
#include "../compiler/parser.h"
#include "../compiler/strip.h"
#include "../runtime/serialize.h"
#include "../formrt/formrt.h"
#include "dvxRes.h"
#include "../../../../libs/kpunch/sql/dvxSql.h"
#include "../runtime/vm.h"
#include "../runtime/values.h"

#include "stb_ds_wrap.h"

#include <dlfcn.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

// ============================================================
// Constants
// ============================================================

#define IDE_MAX_SOURCE   65536
#define IDE_MAX_OUTPUT   32768

// Menu command IDs
#define CMD_OPEN         100
#define CMD_RUN          101
#define CMD_STOP         102
#define CMD_CLEAR        103
#define CMD_EXIT         104
#define CMD_RUN_NOCMP    105
#define CMD_VIEW_CODE    106
#define CMD_VIEW_DESIGN  107
#define CMD_SAVE         108
#define CMD_WIN_CODE     109
#define CMD_WIN_OUTPUT   110
#define CMD_WIN_IMM      111
#define CMD_WIN_TOOLBOX  112
#define CMD_WIN_PROPS    113
#define CMD_DELETE       114
#define CMD_CUT          115
#define CMD_COPY         116
#define CMD_PASTE        117
#define CMD_SELECT_ALL   118
#define CMD_VIEW_TOOLBAR 119
#define CMD_VIEW_STATUS  120
#define CMD_SAVE_ALL     129
#define CMD_SAVE_ON_RUN  139
#define CMD_PRJ_NEW      130
#define CMD_PRJ_OPEN     131
#define CMD_PRJ_SAVE     132
#define CMD_PRJ_CLOSE    133
#define CMD_PRJ_ADD_MOD  134
#define CMD_PRJ_ADD_FRM  135
#define CMD_PRJ_REMOVE   136
#define CMD_PRJ_PROPS    138
#define CMD_WIN_PROJECT  137
#define CMD_HELP_ABOUT   140
#define CMD_FIND         141
#define CMD_REPLACE      142
#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_DEBUG         152
#define CMD_WIN_CALLSTACK 153
#define CMD_WIN_WATCH     154
#define CMD_WIN_BREAKPOINTS 155
#define CMD_DEBUG_LAYOUT    156
#define CMD_OUTPUT_TO_LOG   159
#define CMD_RECENT_BASE     160  // 160-175 reserved for recent files
#define CMD_RECENT_MAX      16   // Keep cap: menu command-ID range is a fixed slice
#define CMD_MAKE_EXE        176
#define CMD_HELP_CONTENTS   157
#define CMD_HELP_API        158
#define IDE_MAX_IMM      1024
#define IDE_DESIGN_W     400
#define IDE_DESIGN_H     300

// Syntax color indices (used by basicColorize / classifyWord)
#define SYNTAX_DEFAULT   0
#define SYNTAX_KEYWORD   1
#define SYNTAX_STRING    2
#define SYNTAX_COMMENT   3
#define SYNTAX_NUMBER    4
#define SYNTAX_TYPE      6

// View mode for activateFile
typedef enum {
    ViewAutoE,    // .frm -> design, .bas -> code
    ViewCodeE,    // force code view
    ViewDesignE   // force design view
} IdeViewModeE;

typedef enum {
    ScopeFuncE,
    ScopeObjE,
    ScopeFileE,
    ScopeProjE
} FindScopeE;

// ============================================================
// Prototypes
// ============================================================

static void activateFile(int32_t fileIdx, IdeViewModeE view);
static void applySyntaxColors(void);
static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx);
static void buildVmBreakpoints(void);
static void buildWindow(void);
static uint8_t classifyWord(const char *word, int32_t wordLen);
static void cleanupFormWin(void);
static void clearAllBreakpoints(void);
static void clearOutput(void);
static void closeFindDialog(void);
static void closeProject(void);
static int cmpEvtPtrs(const void *a, const void *b);
static int cmpStrPtrs(const void *a, const void *b);
static void compileAndRun(void);
static bool compileProject(void);
static int32_t countLines(const char *text);
static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx);
static void debugNavigateToLine(int32_t concatLine);
static void debugSetBreakTitles(bool paused);
static void debugStartOrResume(int32_t cmd);
static void debugUpdateWindows(void);
static bool doEventsCallback(void *ctx);
static void dsgnCopySelected(void);
static void dsgnPasteControl(void);
static void ensureProject(const char *filePath);
static void evaluateImmediate(const char *expr);
static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize);
static char *extractNewProcs(const char *buf);
static const BasDebugVarT *findDebugVar(const char *name);
static bool findInProject(const char *needle, bool caseSensitive);
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen);
static void formatValue(const BasValueT *v, char *buf, int32_t bufSize);
static void freeProcBufs(void);
static BasValueT *getDebugVarSlot(const BasDebugVarT *dv);
static const char *getEventExtraParams(const char *evtName);
static bool getFindForward(void);
static bool getFindMatchCase(void);
static FindScopeE getFindScope(void);
static const char *getFullSource(void);
static WindowT *getLastFocusWin(void);
static void handleEditCmd(int32_t cmd);
static void handleFileCmd(int32_t cmd);
static void handleProjectCmd(int32_t cmd);
static void handleRunCmd(int32_t cmd);
static void handleViewCmd(int32_t cmd);
static void handleWindowCmd(int32_t cmd);
static bool hasUnsavedData(void);
static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize);
static const char *helpLookupKeyword(const char *word);
static void helpQueryHandler(void *ctx);
static void helpSetCtrlTopic(const char *typeName);
void ideRenameInCode(const char *oldName, const char *newName);
static bool immParseScalarFromStr(const char *rhs, const BasValueT *target, BasValueT *outVal);
static void immPrintCallback(void *ctx, const char *text, bool newline);
static BasValueT *immResolveLhsSlot(const char *lhs, const char **endPtr);
static bool immTryAssign(const char *expr);
static void initSyntaxMap(void);
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static bool isCtrlArrayInDesigner(const char *ctrlName);
static bool isIdentChar(char c);
static bool isInStringOrComment(const char *src, int32_t i);
static bool isReplaceEnabled(void);
static void loadFile(void);
static void loadFormCodeIntoEditor(void);
static void loadFrmFiles(BasFormRtT *rt);
static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText);
static int32_t localToConcatLine(int32_t editorLine);
static bool lookupWatchVar(const char *expr, BasValueT *outVal);
static void makeExecutable(void);
static void navigateToBreakpoint(int32_t bpIdx);
static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *procName, bool setDbgLine);
static void navigateToEventSub(void);
static void navigateToNamedEventSub(const char *ctrlName, const char *eventName);
static void newProject(void);
static void onBreakpointHit(void *ctx, int32_t line);
static void onBreakpointListDblClick(WidgetT *w);
static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void onBreakpointWinClose(WindowT *win);
static void onCallStackClose(WindowT *win);
static void onClose(WindowT *win);
static void onCodeWinClose(WindowT *win);
static void onColorListChange(WidgetT *w);
static void onColorSliderChange(WidgetT *w);
static void onContentFocus(WindowT *win);
static void onEditorChange(WidgetT *w);
static void onEvtDropdownChange(WidgetT *w);
static void onFindClose(WindowT *win);
static void onFindCloseBtn(WidgetT *w);
static void onFindNext(WidgetT *w);
static void onFormWinClose(WindowT *win);
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y);
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod);
static void onFormWinMenu(WindowT *win, int32_t menuId);
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
static void onFormWinPaint(WindowT *win, RectT *dirtyArea);
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH);
static void onGutterClick(WidgetT *w, int32_t lineNum);
static void onImmediateChange(WidgetT *w);
static void onLocalsClose(WindowT *win);
static void onMenu(WindowT *win, int32_t menuId);
static void onObjDropdownChange(WidgetT *w);
static void onPrefsCancel(WidgetT *w);
static void onPrefsOk(WidgetT *w);
static void onPrjFileDblClick(int32_t fileIdx, bool isForm);
static void onProjectWinClose(WindowT *win);
static void onReplace(WidgetT *w);
static void onReplaceAll(WidgetT *w);
static void onReplCheckChange(WidgetT *w);
static void onTbCode(WidgetT *w);
static void onTbDebug(WidgetT *w);
static void onTbDesign(WidgetT *w);
static void onTbOpen(WidgetT *w);
static void onTbRun(WidgetT *w);
static void onTbRunToCur(WidgetT *w);
static void onTbSave(WidgetT *w);
static void onTbStepInto(WidgetT *w);
static void onTbStepOut(WidgetT *w);
static void onTbStepOver(WidgetT *w);
static void onTbStop(WidgetT *w);
static void onWatchClose(WindowT *win);
static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWatchListDblClick(WidgetT *w);
static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void openFindDialog(bool showReplace);
static void openProject(void);
static void parseProcs(const char *source);
static void prefsUpdateColorSliders(void);
static void prefsUpdateSwatch(void);
static void printCallback(void *ctx, const char *text, bool newline);
static bool promptAndSave(void);
static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal);
static void recentAdd(const char *path);
static void recentLoad(void);
static void recentOpen(int32_t index);
static void recentRebuildMenu(void);
static void recentSave(void);
static void removeBreakpointsForFile(int32_t fileIdx);
static char *renameInBuffer(const char *src, const char *oldName, const char *newName);
static void runCached(void);
static void runModule(BasModuleT *mod);
static void saveActiveFile(void);
static bool saveCurProc(void);
static void saveFile(void);
static void selectDropdowns(const char *objName, const char *evtName);
static void setOutputText(const char *text);
static void setStatus(const char *text);
static void showBreakpointWindow(void);
static void showCallStackWindow(void);
static void showCodeWindow(void);
static void showImmediateWindow(void);
static void showLocalsWindow(void);
static void showOutputWindow(void);
static void showPreferencesDialog(void);
static void showProc(int32_t procIdx);
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive);
static void showWatchWindow(void);
static void stashCurrentFile(void);
static void stashDesignerState(void);
static void stashFormCode(void);
static void switchToDesign(void);
static void teardownFormWin(void);
static void toggleBreakpoint(void);
static void toggleBreakpointLine(int32_t editorLine);
static int32_t toolbarBottom(void);
static const char *typeNameStr(uint8_t dt);
static void updateBreakpointWindow(void);
static void updateCallStackWindow(void);
static void updateDirtyIndicators(void);
static void updateDropdowns(void);
static void updateLocalsWindow(void);
static void updateProjectMenuState(void);
static void updateWatchWindow(void);
static void watchEditSelected(void);
static void watchPrintCallback(void *ctx, const char *text, bool newline);
int32_t appMain(DxeAppContextT *ctx);

// ============================================================
// Keyword-to-topic lookup for context-sensitive help (F1)
// ============================================================

typedef struct {
    const char *keyword;
    const char *topic;
} HelpMapEntryT;

static const HelpMapEntryT sHelpMap[] = {
    // Data types
    {"Boolean",     "lang.datatypes"},
    {"Double",      "lang.datatypes"},
    {"Integer",     "lang.datatypes"},
    {"Long",        "lang.datatypes"},
    {"Single",      "lang.datatypes"},
    {"String",      "lang.datatypes"},

    // Declarations
    {"ByRef",       "lang.declarations"},
    {"ByVal",       "lang.declarations"},
    {"Const",       "lang.declarations"},
    {"Declare",     "lang.declarations"},
    {"Dim",         "lang.declarations"},
    {"Option",      "lang.declarations"},
    {"ReDim",       "lang.declarations"},
    {"Shared",      "lang.declarations"},
    {"Static",      "lang.declarations"},
    {"Type",        "lang.declarations"},

    // Operators
    {"And",         "lang.operators"},
    {"Mod",         "lang.operators"},
    {"Not",         "lang.operators"},
    {"Or",          "lang.operators"},
    {"Xor",         "lang.operators"},

    // Conditionals
    {"Case",        "lang.conditionals"},
    {"Else",        "lang.conditionals"},
    {"ElseIf",      "lang.conditionals"},
    {"If",          "lang.conditionals"},
    {"Select",      "lang.conditionals"},
    {"Then",        "lang.conditionals"},

    // Loops
    {"Do",          "lang.loops"},
    {"For",         "lang.loops"},
    {"Loop",        "lang.loops"},
    {"Next",        "lang.loops"},
    {"Step",        "lang.loops"},
    {"Until",       "lang.loops"},
    {"Wend",        "lang.loops"},
    {"While",       "lang.loops"},

    // Procedures
    {"Call",        "lang.procedures"},
    {"Def",         "lang.procedures"},
    {"End",         "lang.procedures"},
    {"Function",    "lang.procedures"},
    {"Sub",         "lang.procedures"},

    // Flow control
    {"Exit",        "lang.flow"},
    {"GoSub",       "lang.flow"},
    {"GoTo",        "lang.flow"},
    {"On",          "lang.flow"},
    {"Resume",      "lang.flow"},
    {"Return",      "lang.flow"},

    // I/O statements
    {"Data",        "lang.io"},
    {"Input",       "lang.io"},
    {"Print",       "lang.io"},
    {"Read",        "lang.io"},
    {"Rem",         "lang.io"},
    {"Write",       "lang.io"},

    // Misc statements
    {"DoEvents",    "lang.misc"},
    {"Load",        "lang.misc"},
    {"Shell",       "lang.misc"},
    {"Sleep",       "lang.misc"},
    {"Unload",      "lang.misc"},

    // File I/O
    {"Close",       "lang.fileio"},
    {"Eof",         "lang.func.fileio"},
    {"FreeFile",    "lang.func.fileio"},
    {"Loc",         "lang.func.fileio"},
    {"Lof",         "lang.func.fileio"},
    {"Open",        "lang.fileio"},
    {"Seek",        "lang.func.fileio"},

    // String functions
    {"Asc",         "lang.func.string"},
    {"Chr",         "lang.func.string"},
    {"Chr$",        "lang.func.string"},
    {"Environ",     "lang.func.string"},
    {"Environ$",    "lang.func.string"},
    {"Format",      "lang.func.string"},
    {"Format$",     "lang.func.string"},
    {"InStr",       "lang.func.string"},
    {"LCase",       "lang.func.string"},
    {"LCase$",      "lang.func.string"},
    {"LTrim",       "lang.func.string"},
    {"LTrim$",      "lang.func.string"},
    {"Left",        "lang.func.string"},
    {"Left$",       "lang.func.string"},
    {"Len",         "lang.func.string"},
    {"Mid",         "lang.func.string"},
    {"Mid$",        "lang.func.string"},
    {"RTrim",       "lang.func.string"},
    {"RTrim$",      "lang.func.string"},
    {"Right",       "lang.func.string"},
    {"Right$",      "lang.func.string"},
    {"Spc",         "lang.func.string"},
    {"Space",       "lang.func.string"},
    {"Space$",      "lang.func.string"},
    {"String$",     "lang.func.string"},
    {"Tab",         "lang.func.string"},
    {"Trim",        "lang.func.string"},
    {"Trim$",       "lang.func.string"},
    {"UCase",       "lang.func.string"},
    {"UCase$",      "lang.func.string"},

    // Math functions
    {"Abs",         "lang.func.math"},
    {"Atn",         "lang.func.math"},
    {"Cos",         "lang.func.math"},
    {"Exp",         "lang.func.math"},
    {"Fix",         "lang.func.math"},
    {"Int",         "lang.func.math"},
    {"Log",         "lang.func.math"},
    {"Randomize",   "lang.func.math"},
    {"Rnd",         "lang.func.math"},
    {"Sgn",         "lang.func.math"},
    {"Sin",         "lang.func.math"},
    {"Sqr",         "lang.func.math"},
    {"Tan",         "lang.func.math"},
    {"Timer",       "lang.func.math"},

    // Conversion functions
    {"CBool",       "lang.func.conversion"},
    {"CDbl",        "lang.func.conversion"},
    {"CInt",        "lang.func.conversion"},
    {"CLng",        "lang.func.conversion"},
    {"CSng",        "lang.func.conversion"},
    {"CStr",        "lang.func.conversion"},
    {"Hex",         "lang.func.conversion"},
    {"Hex$",        "lang.func.conversion"},
    {"Str",         "lang.func.conversion"},
    {"Str$",        "lang.func.conversion"},
    {"Val",         "lang.func.conversion"},

    // Misc functions
    {"InputBox",    "lang.func.misc"},
    {"InputBox$",   "lang.func.misc"},
    {"MsgBox",      "lang.func.misc"},

    // SQL functions
    {"SQLAffected", "lang.sql"},
    {"SQLClose",    "lang.sql"},
    {"SQLEof",      "lang.sql"},
    {"SQLError",    "lang.sql"},
    {"SQLError$",   "lang.sql"},
    {"SQLExec",     "lang.sql"},
    {"SQLField",    "lang.sql"},
    {"SQLField$",   "lang.sql"},
    {"SQLFieldCount", "lang.sql"},
    {"SQLFieldDbl", "lang.sql"},
    {"SQLFieldInt", "lang.sql"},
    {"SQLFreeResult", "lang.sql"},
    {"SQLNext",     "lang.sql"},
    {"SQLOpen",     "lang.sql"},
    {"SQLQuery",    "lang.sql"},

    // App object
    {"App",         "lang.app"},

    // INI functions
    {"IniRead",     "lang.ini"},
    {"IniWrite",    "lang.ini"},

    // Constants
    {"False",       "lang.constants"},
    {"True",        "lang.constants"},
    {"vbCancel",    "lang.constants"},
    {"vbCritical",  "lang.constants"},
    {"vbModal",     "lang.constants"},
    {"vbOK",        "lang.constants"},
    {"vbOKCancel",  "lang.constants"},
    {"vbYesNo",     "lang.constants"},

    // Form/control statements
    {"Me",          "lang.forms"},
    {"Set",         "lang.forms"},
};

#define HELP_MAP_COUNT (sizeof(sHelpMap) / sizeof(sHelpMap[0]))

// ============================================================
// Module state
// ============================================================

static DxeAppContextT *sCtx     = NULL;
static char            sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run)
static AppContextT    *sAc      = NULL;
static PrefsHandleT  *sPrefs    = NULL;
static WindowT        *sWin     = NULL;    // Main toolbar window
static WindowT        *sCodeWin = NULL;    // Code editor window
static WindowT        *sOutWin  = NULL;    // Output window
static WindowT        *sImmWin  = NULL;    // Immediate window
static WidgetT        *sEditor      = NULL;
static WidgetT        *sOutput      = NULL;
static WidgetT        *sImmediate   = NULL;
static WidgetT        *sObjDropdown = NULL;
static WidgetT        *sEvtDropdown = NULL;
static WidgetT        *sToolbar     = NULL;
static WidgetT        *sStatusBar   = NULL;
static WidgetT        *sStatus      = NULL;
static WidgetT        *sTbRun       = NULL;
static WidgetT        *sTbStop      = NULL;
static WidgetT        *sTbDebug     = NULL;
static WidgetT        *sTbStepInto  = NULL;
static WidgetT        *sTbStepOver  = NULL;
static WidgetT        *sTbStepOut   = NULL;
static WidgetT        *sTbRunToCur  = NULL;
static WidgetT        *sTbCode      = NULL;
static WidgetT        *sTbDesign    = NULL;
static BasVmT         *sVm          = NULL;  // VM instance (non-NULL while running)
static BasModuleT     *sCachedModule = NULL; // Last compiled module (for Ctrl+F5)
static DsgnStateT     sDesigner;
static WindowT        *sFormWin     = NULL;  // Form designer window (separate)
static WindowT        *sToolboxWin  = NULL;
static WindowT        *sPropsWin    = NULL;
static WindowT        *sProjectWin  = NULL;
static PrjStateT       sProject;
static WindowT        *sLastFocusWin = NULL; // last focused non-toolbar window

static char sOutputBuf[IDE_MAX_OUTPUT];
static int32_t sOutputLen = 0;

// Procedure view state -- the editor shows one procedure at a time.
// Each procedure is stored in its own malloc'd buffer. The editor
// swaps directly between buffers with no splicing needed.
static char   *sGeneralBuf  = NULL;  // (General) section: module-level code
static char  **sProcBufs    = NULL;  // stb_ds array: one buffer per procedure
static int32_t sCurProcIdx    = -2;    // which buffer is in the editor (-1=General, -2=none)
static int32_t sEditorFileIdx = -1;   // which project file owns sProcBufs (-1=none)
static int32_t sEditorLineCount = 0;  // line count for breakpoint adjustment on edit

// Find/Replace state
static char sFindText[256]    = "";
static char sReplaceText[256] = "";
// Find/Replace dialog state (modeless)
static WindowT *sFindWin      = NULL;
static WidgetT *sFindInput    = NULL;
static WidgetT *sReplInput    = NULL;
static WidgetT *sReplCheck    = NULL;
static WidgetT *sBtnReplace   = NULL;
static WidgetT *sBtnReplAll   = NULL;
static WidgetT *sCaseCheck    = NULL;
static WidgetT *sScopeGroup   = NULL;  // radio group: 0=Func, 1=Obj, 2=File, 3=Proj
static WidgetT *sDirGroup     = NULL;  // radio group: 0=Fwd, 1=Back

// Procedure table for Object/Event dropdowns
typedef struct {
    char    objName[64];
    char    evtName[64];
    int32_t lineNum;
} IdeProcEntryT;

static IdeProcEntryT *sProcTable    = NULL; // stb_ds dynamic array
static const char   **sObjItems     = NULL; // stb_ds dynamic array
static const char   **sEvtItems     = NULL; // stb_ds dynamic array
static bool           sDropdownNavSuppressed = false;
static bool           sStopRequested         = false;
static bool           sOutputToLog           = false;

// Recent files list (stored in dvxbasic.ini [recent] section)
// stb_ds dynamic array of strdup'd paths. Capped at CMD_RECENT_MAX by the menu
// command-ID range.
static char         **sRecentFiles  = NULL;
static MenuT         *sFileMenu     = NULL;
static int32_t        sFileMenuBase = 0;  // item count before recent files

// Debug state
typedef enum {
    DBG_IDLE,       // no program loaded
    DBG_RUNNING,    // program executing
    DBG_PAUSED      // stopped at breakpoint/step
} IdeDebugStateE;

typedef struct {
    int32_t fileIdx;     // project file index
    int32_t codeLine;    // line within file's code section (1-based)
    int32_t procIdx;     // procedure index at time of toggle (-1 = general)
    char    procName[BAS_MAX_PROC_NAME * 2]; // "obj.evt" combined name
} IdeBreakpointT;

static IdeDebugStateE sDbgState        = DBG_IDLE;
static IdeBreakpointT *sBreakpoints    = NULL;  // stb_ds array
static int32_t        sBreakpointCount = 0;
static int32_t       *sVmBreakpoints   = NULL;  // stb_ds array of concat line numbers (built at compile time)
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 bool           sDbgEnabled      = false; // true = debug mode (breakpoints active)
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    = NULL;   // stb_ds dynamic array of strdup'd watch expressions
static WindowT       *sBreakpointWin  = NULL;  // Breakpoints window
static WidgetT       *sBreakpointList = NULL;  // Breakpoints ListView widget

// ============================================================
// Syntax highlighting tables (shared by several functions)
// ============================================================

#define SYNTAX_COLOR_COUNT 7

typedef struct {
    char    *key;
    uint8_t  value;
} SyntaxMapEntryT;

static SyntaxMapEntryT *sSyntaxMap = NULL;

// Color entry names for the Colors tab (matches SYNTAX_* indices)
static const char *sSyntaxColorNames[] = {
    "Default Text",  // 0 = SYNTAX_DEFAULT
    "Keywords",      // 1 = SYNTAX_KEYWORD
    "Strings",       // 2 = SYNTAX_STRING
    "Comments",      // 3 = SYNTAX_COMMENT
    "Numbers",       // 4 = SYNTAX_NUMBER
    "Operators",     // 5 = SYNTAX_OPERATOR
    "Types",         // 6 = SYNTAX_TYPE
};

// Default syntax colors (0x00RRGGBB; 0 = use widget default)
static const uint32_t sDefaultSyntaxColors[SYNTAX_COLOR_COUNT] = {
    0x00000000,  // default -- not used (widget fg)
    0x00000080,  // keyword -- dark blue
    0x00800000,  // string  -- dark red
    0x00008000,  // comment -- dark green
    0x00800080,  // number  -- purple
    0x00808000,  // operator -- dark yellow
    0x00008080,  // type    -- teal
};

// Preferences dialog state (used by several onPrefs* handlers)
static struct {
    bool     done;
    bool     accepted;
    // General tab
    WidgetT *renameSkipComments;
    WidgetT *optionExplicit;
    WidgetT *tabWidthInput;
    WidgetT *useSpaces;
    WidgetT *defAuthor;
    WidgetT *defPublisher;
    WidgetT *defVersion;
    WidgetT *defCopyright;
    WidgetT *defDescription;
    // Colors tab
    WidgetT *colorList;
    WidgetT *sliderR;
    WidgetT *sliderG;
    WidgetT *sliderB;
    WidgetT *lblR;
    WidgetT *lblG;
    WidgetT *lblB;
    WidgetT *colorSwatch;
    uint32_t syntaxColors[SYNTAX_COLOR_COUNT];
} sPrefsDlg;

// Watch-expression scratch buffer (used by evalWatchExpr + watchPrintCallback)
static char    sWatchPrintBuf[256];
static int32_t sWatchPrintLen;

// ============================================================
// App descriptor
// ============================================================

AppDescriptorT appDescriptor = {
    .name          = "DVX BASIC",
    .hasMainLoop   = false,
    .multiInstance  = false,
    .stackSize     = SHELL_STACK_DEFAULT,
    .priority      = 0
};


static void activateFile(int32_t fileIdx, IdeViewModeE view) {
    if (fileIdx < 0 || fileIdx >= sProject.fileCount) {
        return;
    }

    PrjFileT *target = &sProject.files[fileIdx];

    // Resolve ViewAutoE
    if (view == ViewAutoE) {
        view = target->isForm ? ViewDesignE : ViewCodeE;
    }

    // If already active, just ensure the right view is showing
    if (fileIdx == sProject.activeFileIdx) {
        if (view == ViewDesignE) {
            switchToDesign();
        } else if (view == ViewCodeE) {
            if (sCodeWin) {
                dvxRaiseWindow(sAc, sCodeWin);
            } else {
                // Code window was closed — reopen it
                showCodeWindow();

                if (sCodeWin) {
                    dvxRaiseWindow(sAc, sCodeWin);
                }

                // Reload the file content
                if (sEditor) {
                    const char *source = target->buffer;

                    if (source) {
                        parseProcs(source);
                        updateDropdowns();
                        showProc(-1);
                        sEditor->onChange = onEditorChange;
                    }
                }
            }
        }
        return;
    }

    // PHASE 1: Stash the outgoing file
    stashCurrentFile();

    // PHASE 2: Load the incoming file
    if (target->isForm) {
        // Load form into designer
        const char *frmSrc = target->buffer;
        char *diskBuf = NULL;

        if (!frmSrc) {
            // Try disk
            char fullPath[DVX_MAX_PATH];
            prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
            int32_t diskLen = 0;
            diskBuf = platformReadFile(fullPath, &diskLen);

            if (diskBuf && (diskLen <= 0 || diskLen >= IDE_MAX_SOURCE)) {
                free(diskBuf);
                diskBuf = NULL;
            }

            if (diskBuf) {
                frmSrc = diskBuf;
            }
        }

        teardownFormWin();
        dsgnFree(&sDesigner);

        if (frmSrc) {
            dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc));
            free(diskBuf);
        } else {
            // New blank form -- derive name from filename
            char formName[PRJ_MAX_NAME];
            const char *base = platformPathBaseName(target->path);
            int32_t bl = (int32_t)strlen(base);

            if (bl >= PRJ_MAX_NAME) {
                bl = PRJ_MAX_NAME - 1;
            }

            memcpy(formName, base, bl);
            formName[bl] = '\0';
            char *dot = strrchr(formName, '.');

            if (dot) {
                *dot = '\0';
            }

            dsgnNewForm(&sDesigner, formName);
            target->modified = true;
        }

        if (sDesigner.form) {
            snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
        }

        sProject.activeFileIdx = fileIdx;

        if (view == ViewDesignE) {
            switchToDesign();
        } else {
            loadFormCodeIntoEditor();
        }
    } else {
        // Load .bas into editor
        if (!sCodeWin) {
            showCodeWindow();
        }

        if (sCodeWin) {
            dvxRaiseWindow(sAc, sCodeWin);
        }

        const char *source = target->buffer;
        char *diskBuf = NULL;


        if (!source) {
            char fullPath[DVX_MAX_PATH];
            prjFullPath(&sProject, fileIdx, fullPath, sizeof(fullPath));
            int32_t diskLen = 0;
            diskBuf = platformReadFile(fullPath, &diskLen);

            if (diskBuf && (diskLen <= 0 || diskLen >= IDE_MAX_SOURCE)) {
                free(diskBuf);
                diskBuf = NULL;
            }

            if (diskBuf) {
                source = diskBuf;
            }
        }

        if (!source) {
            source = "";
            target->modified = true;
        }

        parseProcs(source);
        free(diskBuf);
        updateDropdowns();
        showProc(-1);

        if (sEditor && !sEditor->onChange) {
            sEditor->onChange = onEditorChange;
        }

        sEditorFileIdx = fileIdx;
        sProject.activeFileIdx = fileIdx;
    }

    updateProjectMenuState();
    updateDirtyIndicators();
}


static void applySyntaxColors(void) {
    if (!sEditor) {
        return;
    }

    wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, SYNTAX_COLOR_COUNT);
}


static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx) {
    (void)ctx;
    int32_t i = 0;

    while (i < lineLen) {
        char ch = line[i];

        // Comment: ' or REM
        if (ch == '\'') {
            while (i < lineLen) {
                colors[i++] = SYNTAX_COMMENT;
            }
            return;
        }

        // String literal
        if (ch == '"') {
            colors[i++] = SYNTAX_STRING;

            while (i < lineLen && line[i] != '"') {
                colors[i++] = SYNTAX_STRING;
            }

            if (i < lineLen) {
                colors[i++] = SYNTAX_STRING;
            }

            continue;
        }

        // Number
        if (isdigit((unsigned char)ch) || (ch == '.' && i + 1 < lineLen && isdigit((unsigned char)line[i + 1]))) {
            while (i < lineLen && (isdigit((unsigned char)line[i]) || line[i] == '.')) {
                colors[i++] = SYNTAX_NUMBER;
            }

            continue;
        }

        // Identifier or keyword
        if (isalpha((unsigned char)ch) || ch == '_') {
            int32_t start = i;

            while (i < lineLen && (isalnum((unsigned char)line[i]) || line[i] == '_' || line[i] == '$' || line[i] == '%' || line[i] == '&' || line[i] == '!' || line[i] == '#')) {
                i++;
            }

            int32_t wordLen = i - start;

            // Check for REM comment
            if (wordLen == 3 && (line[start] == 'R' || line[start] == 'r') && (line[start + 1] == 'E' || line[start + 1] == 'e') && (line[start + 2] == 'M' || line[start + 2] == 'm')) {
                for (int32_t j = start; j < lineLen; j++) {
                    colors[j] = SYNTAX_COMMENT;
                }
                return;
            }

            uint8_t c = classifyWord(line + start, wordLen);

            for (int32_t j = start; j < i; j++) {
                colors[j] = c;
            }

            continue;
        }

        // Operators
        if (ch == '=' || ch == '<' || ch == '>' || ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '\\' || ch == '&') {
            colors[i++] = 5; // SYNTAX_OPERATOR
            continue;
        }

        // Default (whitespace, parens, etc.)
        colors[i++] = 0;
    }
}


// ============================================================
// buildVmBreakpoints -- convert IDE breakpoints to VM concat line numbers
// ============================================================
//
// Called after compilation when the source map is fresh. Converts
// each (fileIdx, codeLine) pair to a concatenated source line number
// that matches OP_LINE values in the compiled bytecode.

static void buildVmBreakpoints(void) {
    arrfree(sVmBreakpoints);
    sVmBreakpoints = NULL;

    for (int32_t i = 0; i < sBreakpointCount; i++) {
        int32_t fileIdx  = sBreakpoints[i].fileIdx;
        int32_t codeLine = sBreakpoints[i].codeLine;

        // Find this file in the source map
        for (int32_t m = 0; m < sProject.sourceMapCount; m++) {
            if (sProject.sourceMap[m].fileIdx == fileIdx) {
                int32_t base = sProject.sourceMap[m].startLine;

                // .frm files have an injected BEGINFORM line
                if (fileIdx >= 0 && fileIdx < sProject.fileCount &&
                    sProject.files[fileIdx].isForm) {
                    base++;
                }

                int32_t vmLine = base + codeLine - 1;
                arrput(sVmBreakpoints, vmLine);
                break;
            }
        }
    }

}


static void buildWindow(void) {
    // ---- Main toolbar window (top of screen) ----
    sWin = dvxCreateWindow(sAc, "DVX BASIC", 0, 0, sAc->display.width, 200, true);

    if (!sWin) {
        return;
    }

    sWin->onClose = onClose;
    sWin->onMenu  = onMenu;

    // Menu bar
    MenuBarT *menuBar = wmAddMenuBar(sWin);
    sFileMenu = wmAddMenu(menuBar, "&File");
    MenuT *fileMenu = sFileMenu;
    wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW);
    wmAddMenuItem(fileMenu, "&Open Project...", CMD_PRJ_OPEN);
    wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE);
    wmAddMenuItem(fileMenu, "&Close Project", CMD_PRJ_CLOSE);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "Project Propert&ies...", CMD_PRJ_PROPS);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN);
    wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE);
    wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "&Make Executable...", CMD_MAKE_EXE);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT);
    sFileMenuBase = sFileMenu->itemCount;

    MenuT *editMenu = wmAddMenu(menuBar, "&Edit");
    wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT);
    wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY);
    wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE);
    wmAddMenuSeparator(editMenu);
    wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL);
    wmAddMenuSeparator(editMenu);
    wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE);
    wmAddMenuSeparator(editMenu);
    wmAddMenuItem(editMenu, "&Find...\tCtrl+F", CMD_FIND);
    wmAddMenuItem(editMenu, "Find &Next\tF3", CMD_FIND_NEXT);
    wmAddMenuItem(editMenu, "&Replace...\tCtrl+H", CMD_REPLACE);

    MenuT *runMenu = wmAddMenu(menuBar, "&Run");
    wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
    wmAddMenuItem(runMenu, "&Debug\tShift+F5", CMD_DEBUG);
    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);
    wmAddMenuCheckItem(runMenu, "Output Window to &Log", CMD_OUTPUT_TO_LOG, false);
    wmAddMenuSeparator(runMenu);
    wmAddMenuItem(runMenu, "Toggle &Breakpoint\tF9", CMD_TOGGLE_BP);
    wmAddMenuSeparator(runMenu);
    wmAddMenuItem(runMenu, "Cl&ear Output", CMD_CLEAR);
    wmAddMenuSeparator(runMenu);
    wmAddMenuCheckItem(runMenu, "S&ave on Run", CMD_SAVE_ON_RUN, true);

    MenuT *viewMenu = wmAddMenu(menuBar, "&View");
    wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE);
    wmAddMenuItem(viewMenu, "&Designer\tShift+F7", CMD_VIEW_DESIGN);
    wmAddMenuSeparator(viewMenu);
    wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true);
    wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true);
    wmAddMenuSeparator(viewMenu);
    wmAddMenuItem(viewMenu, "&Menu Editor...\tCtrl+E", CMD_MENU_EDITOR);

    MenuT *winMenu = wmAddMenu(menuBar, "&Window");
    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);
    wmAddMenuItem(winMenu, "&Breakpoints", CMD_WIN_BREAKPOINTS);
    wmAddMenuSeparator(winMenu);
    wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT);
    wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX);
    wmAddMenuItem(winMenu, "P&roperties", CMD_WIN_PROPS);

    MenuT *toolsMenu = wmAddMenu(menuBar, "&Tools");
    wmAddMenuItem(toolsMenu, "&Preferences...", CMD_PREFERENCES);
    wmAddMenuSeparator(toolsMenu);
    wmAddMenuCheckItem(toolsMenu, "Debug &Layout", CMD_DEBUG_LAYOUT, false);

    MenuT *helpMenu = wmAddMenu(menuBar, "&Help");
    wmAddMenuItem(helpMenu, "&DVX BASIC Help\tF1", CMD_HELP_CONTENTS);
    wmAddMenuItem(helpMenu, "DVX &API Reference", CMD_HELP_API);
    wmAddMenuSeparator(helpMenu);
    wmAddMenuItem(helpMenu, "A&bout DVX BASIC...", CMD_HELP_ABOUT);

    AccelTableT *accel = dvxCreateAccelTable();
    dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
    dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE);
    dvxAddAccel(accel, KEY_F5, 0, CMD_RUN);
    dvxAddAccel(accel, KEY_F5, ACCEL_SHIFT, CMD_DEBUG);
    dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP);
    dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
    dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
    dvxAddAccel(accel, 0x1B, 0, CMD_STOP);
    dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
    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);
    sToolbar = wgtToolbar(tbRoot);
    WidgetT *tb = sToolbar;

    // File group
    WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open");
    tbOpen->onClick = onTbOpen;
    wgtSetTooltip(tbOpen, "Open (Ctrl+O)");

    WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save");
    tbSave->onClick = onTbSave;
    wgtSetTooltip(tbSave, "Save (Ctrl+S)");

    WidgetT *sep1 = wgtVSeparator(tb);
    sep1->minW = wgtPixels(10);

    // Run group
    sTbRun = loadTbIcon(tb, "tb_run", "Run");
    sTbRun->onClick = onTbRun;
    wgtSetTooltip(sTbRun, "Run (F5)");

    sTbStop = loadTbIcon(tb, "tb_stop", "Stop");
    sTbStop->onClick = onTbStop;
    wgtSetTooltip(sTbStop, "Stop (Esc)");

    WidgetT *sep2 = wgtVSeparator(tb);
    sep2->minW = wgtPixels(10);

    // Debug group
    sTbDebug = loadTbIcon(tb, "tb_debug", "Debug");
    sTbDebug->onClick = onTbDebug;
    wgtSetTooltip(sTbDebug, "Debug (Shift+F5)");

    sTbStepInto = loadTbIcon(tb, "tb_stepin", "Into");
    sTbStepInto->onClick = onTbStepInto;
    wgtSetTooltip(sTbStepInto, "Step Into (F8)");

    sTbStepOver = loadTbIcon(tb, "tb_stepov", "Over");
    sTbStepOver->onClick = onTbStepOver;
    wgtSetTooltip(sTbStepOver, "Step Over (Shift+F8)");

    sTbStepOut = loadTbIcon(tb, "tb_stepou", "Out");
    sTbStepOut->onClick = onTbStepOut;
    wgtSetTooltip(sTbStepOut, "Step Out (Ctrl+Shift+F8)");

    sTbRunToCur = loadTbIcon(tb, "tb_runtoc", "Cursor");
    sTbRunToCur->onClick = onTbRunToCur;
    wgtSetTooltip(sTbRunToCur, "Run to Cursor (Ctrl+F8)");

    WidgetT *sep3 = wgtVSeparator(tb);
    sep3->minW = wgtPixels(10);

    // View group
    sTbCode = loadTbIcon(tb, "tb_code", "Code");
    sTbCode->onClick = onTbCode;
    wgtSetTooltip(sTbCode, "Code View (F7)");

    sTbDesign = loadTbIcon(tb, "tb_design", "Design");
    sTbDesign->onClick = onTbDesign;
    wgtSetTooltip(sTbDesign, "Design View (Shift+F7)");

    sStatusBar = wgtStatusBar(tbRoot);
    WidgetT *statusBar = sStatusBar;
    sStatus = wgtLabel(statusBar, "");
    sStatus->weight = 100;

    // Fit height to content, keeping full screen width
    dvxFitWindowH(sAc, sWin);

    // Initialize designer (form window created on demand)
    dsgnInit(&sDesigner, sAc);
    sDesigner.projectDir = sProject.projectDir;

    showOutputWindow();
    showImmediateWindow();

    if (sWin) {
        dvxRaiseWindow(sAc, sWin);
    }
}


// classifyWord -- returns syntax color for an identifier.
// Converts to uppercase once, then does a single hash lookup.

static uint8_t classifyWord(const char *word, int32_t wordLen) {
    char upper[32];

    if (wordLen <= 0 || wordLen >= 32) {
        return SYNTAX_DEFAULT;
    }

    for (int32_t i = 0; i < wordLen; i++) {
        upper[i] = (char)toupper((unsigned char)word[i]);
    }

    upper[wordLen] = '\0';

    initSyntaxMap();

    int32_t idx = shgeti(sSyntaxMap, upper);

    if (idx >= 0) {
        return sSyntaxMap[idx].value;
    }

    return SYNTAX_DEFAULT;
}


// ============================================================
// onFormWinClose
// ============================================================

// cleanupFormWin -- release designer-related state without destroying
// the form window itself (the caller handles that).
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;
    }

    if (sPropsWin) {
        prpDestroy(sAc, sPropsWin);
        sPropsWin = NULL;
    }
}


// ============================================================
// debugLineDecorator -- highlight breakpoints and current debug line
// ============================================================


static void clearAllBreakpoints(void) {
    arrsetlen(sBreakpoints, 0);
    sBreakpointCount = 0;
    arrfree(sVmBreakpoints);
    sVmBreakpoints = NULL;
    updateBreakpointWindow();
}


static void clearOutput(void) {
    sOutputBuf[0] = '\0';
    sOutputLen    = 0;
    setOutputText("");
}


static void closeFindDialog(void) {
    if (sFindWin) {
        dvxDestroyWindow(sAc, sFindWin);
        sFindWin    = NULL;
        sFindInput  = NULL;
        sReplInput  = NULL;
        sReplCheck  = NULL;
        sBtnReplace = NULL;
        sBtnReplAll = NULL;
        sCaseCheck  = NULL;
        sScopeGroup = NULL;
        sDirGroup   = NULL;
    }
}


static void closeProject(void) {
    if (sProject.projectPath[0] == '\0') {
        return;
    }

    closeFindDialog();

    if (sProject.dirty) {
        prjSave(&sProject);
    }

    // Close designer windows
    if (sFormWin) {
        dvxDestroyWindow(sAc, sFormWin);
        cleanupFormWin();
    }

    dsgnFree(&sDesigner);

    // Close code editor
    if (sCodeWin) {
        dvxDestroyWindow(sAc, sCodeWin);
        sCodeWin     = NULL;
        sEditor      = NULL;
        sObjDropdown = NULL;
        sEvtDropdown = NULL;
    }

    freeProcBufs();
    clearAllBreakpoints();

    // Close project window
    prjClose(&sProject);

    if (sProjectWin) {
        prjDestroyWindow(sAc, sProjectWin);
        sProjectWin = NULL;
    }

    if (sWin) {
        dvxSetTitle(sAc, sWin, "DVX BASIC");
    }

    if (sStatus) {
        setStatus("Project closed.");
    }

    updateProjectMenuState();
}


// Sort event names: implemented (no brackets) first, then unimplemented
// ([brackets]), alphabetically within each group.

static int cmpEvtPtrs(const void *a, const void *b) {
    const char *sa = *(const char **)a;
    const char *sb = *(const char **)b;
    bool aImpl = (sa[0] != '[');
    bool bImpl = (sb[0] != '[');

    if (aImpl != bImpl) {
        return aImpl ? -1 : 1;
    }

    // Skip brackets for alphabetical comparison
    if (sa[0] == '[') { sa++; }
    if (sb[0] == '[') { sb++; }
    return strcasecmp(sa, sb);
}


static int cmpStrPtrs(const void *a, const void *b) {
    const char *sa = *(const char **)a;
    const char *sb = *(const char **)b;
    return strcasecmp(sa, sb);
}


static void compileAndRun(void) {
    if (compileProject()) {
        runModule(sCachedModule);
    }
}


static bool compileProject(void) {
    // Save all dirty files before compiling if Save on Run is enabled
    if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
        if (sProject.activeFileIdx >= 0) {
            saveActiveFile();
        }

        for (int32_t i = 0; i < sProject.fileCount; i++) {
            if (i == sProject.activeFileIdx) {
                continue;
            }

            if (sProject.files[i].modified && sProject.files[i].buffer) {
                char fullPath[DVX_MAX_PATH];
                prjFullPath(&sProject, i, fullPath, sizeof(fullPath));

                FILE *f = fopen(fullPath, "w");

                if (f) {
                    fputs(sProject.files[i].buffer, f);
                    fclose(f);
                    sProject.files[i].modified = false;
                }
            }
        }

        updateDirtyIndicators();
    }

    clearOutput();
    setStatus("Compiling...");
    dvxSetBusy(sAc, true);
    dvxUpdate(sAc);

    // Build source: either concatenate project files or use editor contents
    char *concatBuf = NULL;
    const char *src = NULL;
    int32_t srcLen  = 0;

    if (sProject.projectPath[0] != '\0' && sProject.fileCount > 0) {
        // Stash current editor/designer state into project buffers
        stashCurrentFile();
        stashFormCode();  // also stash form code if editor has it

        // Concatenate all .bas files from buffers (or disk if not yet loaded)
        concatBuf = (char *)malloc(IDE_MAX_SOURCE);

        if (!concatBuf) {
            setStatus("Out of memory.");
            dvxSetBusy(sAc, false);
            return false;
        }

        int32_t pos  = 0;
        int32_t line = 1;
        arrfree(sProject.sourceMap);
        sProject.sourceMap      = NULL;
        sProject.sourceMapCount = 0;

        // Two passes: .bas modules first (so CONST declarations are
        // available), then .frm code sections.
        for (int32_t pass = 0; pass < 2; pass++)
        for (int32_t i = 0; i < sProject.fileCount; i++) {
            // Pass 0: modules only.  Pass 1: forms only.
            if (pass == 0 && sProject.files[i].isForm)  { continue; }
            if (pass == 1 && !sProject.files[i].isForm) { continue; }

            const char *fileSrc = NULL;
            char *diskBuf = NULL;

            if (sProject.files[i].isForm) {
                // For .frm files, extract just the code section.
                // If this is the active form in the designer, use form->code.
                if (sDesigner.form && i == sProject.activeFileIdx) {
                    fileSrc = sDesigner.form->code;
                } else if (sProject.files[i].buffer) {
                    // Parse the .frm to extract the code section using
                    // the same parser as the designer (handles nested containers).
                    DsgnStateT tmpDs;
                    memset(&tmpDs, 0, sizeof(tmpDs));
                    dsgnLoadFrm(&tmpDs, sProject.files[i].buffer, (int32_t)strlen(sProject.files[i].buffer));

                    if (tmpDs.form && tmpDs.form->code) {
                        fileSrc = tmpDs.form->code;
                        diskBuf = tmpDs.form->code;
                        tmpDs.form->code = NULL;
                    }

                    dsgnFree(&tmpDs);
                }

                // If no code found from memory, fall through to disk read
            } else {
                fileSrc = sProject.files[i].buffer;
            }

            if (!fileSrc) {
                // Not yet loaded into memory -- read from disk
                char fullPath[DVX_MAX_PATH];
                prjFullPath(&sProject, i, fullPath, sizeof(fullPath));

                int32_t br = 0;
                diskBuf = platformReadFile(fullPath, &br);

                if (!diskBuf) {
                    continue;
                }

                if (br <= 0 || br >= IDE_MAX_SOURCE) {
                    free(diskBuf);
                    diskBuf = NULL;
                } else {
                    fileSrc = diskBuf;

                    // For .frm from disk, parse to extract code section
                    if (sProject.files[i].isForm) {
                        DsgnStateT tmpDs;
                        memset(&tmpDs, 0, sizeof(tmpDs));
                        dsgnLoadFrm(&tmpDs, diskBuf, br);

                        if (tmpDs.form && tmpDs.form->code) {
                            free(diskBuf);
                            diskBuf = tmpDs.form->code;
                            fileSrc = diskBuf;
                            tmpDs.form->code = NULL;
                        } else {
                            fileSrc = NULL;
                        }

                        dsgnFree(&tmpDs);
                    }
                }
            }

            if (!fileSrc) {
                continue;
            }

            // Inject BEGINFORM directive for .frm code sections
            if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
                int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos,
                    "BEGINFORM \"%s\"\n", sProject.files[i].formName);
                pos  += dirLen;
                line++;
            }

            // Record startLine AFTER injected directives so the source
            // map lines match what the editor shows (not the synthetic lines).
            int32_t startLine = line;

            int32_t fileLen = (int32_t)strlen(fileSrc);
            int32_t copyLen = fileLen;

            if (pos + copyLen >= IDE_MAX_SOURCE - 64) {
                copyLen = IDE_MAX_SOURCE - 64 - pos;
            }

            memcpy(concatBuf + pos, fileSrc, copyLen);
            pos += copyLen;

            // Count lines
            for (int32_t j = 0; j < copyLen; j++) {
                if (fileSrc[j] == '\n') {
                    line++;
                }
            }

            free(diskBuf);

            // Ensure a trailing newline between files
            if (copyLen > 0 && concatBuf[pos - 1] != '\n' && pos < IDE_MAX_SOURCE - 1) {
                concatBuf[pos++] = '\n';
                line++;
            }

            // Record source map BEFORE injected ENDFORM directive
            {
                PrjSourceMapT mapEntry;
                mapEntry.fileIdx   = i;
                mapEntry.startLine = startLine;
                mapEntry.lineCount = line - startLine;
                arrput(sProject.sourceMap, mapEntry);
                sProject.sourceMapCount = (int32_t)arrlen(sProject.sourceMap);
            }

            // Inject ENDFORM directive
            if (sProject.files[i].isForm && sProject.files[i].formName[0]) {
                int32_t dirLen = snprintf(concatBuf + pos, IDE_MAX_SOURCE - pos, "ENDFORM\n");
                pos  += dirLen;
                line++;
            }

            dvxUpdate(sAc);
        }

        concatBuf[pos] = '\0';
        src    = concatBuf;
        srcLen = pos;
    } else {
        // No project files -- compile the full source
        src = getFullSource();

        if (!src || *src == '\0') {
            setStatus("No source code to compile.");
            dvxSetBusy(sAc, false);
            return false;
        }

        srcLen = (int32_t)strlen(src);
    }

    // Compile (heap-allocated -- BasParserT is ~300KB, too large for stack)
    BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));

    if (!parser) {
        free(concatBuf);
        setStatus("Out of memory.");
        dvxSetBusy(sAc, false);
        return false;
    }

    basParserInit(parser, src, srcLen);
    parser->optionExplicit = prefsGetBool(sPrefs, "editor", "optionExplicit", false);

    if (!basParse(parser)) {
        // Translate global error line to local file/line for display
        int32_t errFileIdx   = -1;
        int32_t errLocalLine = parser->errorLine;
        const char *errFile  = "";

        if (parser->errorLine > 0 && sProject.fileCount > 0) {
            prjMapLine(&sProject, parser->errorLine, &errFileIdx, &errLocalLine);

            if (errFileIdx >= 0 && errFileIdx < sProject.fileCount) {
                errFile = sProject.files[errFileIdx].path;
            }
        }

        // Navigate to error location and build a user-friendly error message
        char procName[128] = {0};
        int32_t procLine   = errLocalLine;

        if (parser->errorLine > 0 && errFileIdx >= 0) {
            activateFile(errFileIdx, ViewCodeE);

            int32_t procCount = (int32_t)arrlen(sProcTable);

            for (int32_t i = procCount - 1; i >= 0; i--) {
                if (errLocalLine >= sProcTable[i].lineNum) {
                    snprintf(procName, sizeof(procName), "%s.%s",
                             sProcTable[i].objName, sProcTable[i].evtName);
                    procLine = errLocalLine - sProcTable[i].lineNum + 1;
                    break;
                }
            }

            navigateToCodeLine(errFileIdx, errLocalLine, procName[0] ? procName : NULL, false);
        }

        // Strip the "Line NNN: " prefix from the parser error
        const char *msg = parser->error;
        if (strncmp(msg, "Line ", 5) == 0) {
            while (*msg && *msg != ':') { msg++; }
            if (*msg == ':') { msg++; }
            while (*msg == ' ') { msg++; }
        }

        // Show the error with procedure name and proc-relative line
        int32_t n;
        if (procName[0]) {
            n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n",
                         procName, (int)procLine, msg);
        } else {
            n = snprintf(sOutputBuf, IDE_MAX_OUTPUT, "COMPILE ERROR:\n%s line %d: %s\n",
                         errFile, (int)errLocalLine, msg);
        }
        sOutputLen = n;
        setOutputText(sOutputBuf);

        // Ensure output window is visible
        showOutputWindow();
        if (sOutWin) {
            dvxRaiseWindow(sAc, sOutWin);
        }

        setStatus("Compilation failed.");
        dvxSetBusy(sAc, false);
        basParserFree(parser);
        free(parser);
        free(concatBuf);
        return false;
    }

    free(concatBuf);

    BasModuleT *mod = basParserBuildModule(parser);
    basParserFree(parser);
    free(parser);

    if (!mod) {
        setStatus("Failed to build module.");
        dvxSetBusy(sAc, false);
        return false;
    }

    dvxSetBusy(sAc, false);

    // Cache the compiled module for Ctrl+F5
    if (sCachedModule) {
        basModuleFree(sCachedModule);
    }

    sCachedModule = mod;

    // Update Object/Event dropdowns
    updateDropdowns();

    return true;
}


static int32_t countLines(const char *text) {
    if (!text || !text[0]) {
        return 1;
    }

    int32_t n = 1;

    for (const char *p = text; *p; p++) {
        if (*p == '\n') {
            n++;
        }
    }

    return n;
}


static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx) {
    AppContextT *ac = (AppContextT *)ctx;

    // Convert editor line to file code line
    int32_t codeLine = lineNum;

    if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
        codeLine = sProcTable[sCurProcIdx].lineNum + lineNum - 1;
    }

    int32_t fileIdx = sProject.activeFileIdx;

    // Breakpoint: red gutter dot
    for (int32_t i = 0; i < sBreakpointCount; i++) {
        if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) {
            *gutterColor = packColor(&ac->display, 200, 0, 0);
            break;
        }
    }

    // Current debug line: yellow background (sDbgCurrentLine is editor-local)
    if (sDbgState == DBG_PAUSED && lineNum == sDbgCurrentLine) {
        return packColor(&ac->display, 255, 255, 128);
    }

    return 0;
}


static void debugNavigateToLine(int32_t concatLine) {
    if (concatLine <= 0) {
        return;
    }

    int32_t fileIdx   = -1;
    int32_t localLine = concatLine;

    // Map concatenated line to file and file-local line (code section)
    if (sProject.sourceMapCount > 0) {
        prjMapLine(&sProject, concatLine, &fileIdx, &localLine);

        // For .frm files, subtract the injected BEGINFORM line
        if (fileIdx >= 0 && fileIdx < sProject.fileCount &&
            sProject.files[fileIdx].isForm) {
            localLine--;
        }
    }

    // Find which procedure we're in using the VM's PC and the compiled
    // module's proc table, then build a dot-separated name for navigateToCodeLine.
    const char *procName = NULL;
    char procBuf[128];

    if (sVm && sDbgModule) {
        const char *compiledName = NULL;
        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;
                compiledName = sDbgModule->procs[i].name;
            }
        }

        // Convert compiled name (Obj_Evt) to dot-separated (Obj.Evt) for matching
        if (compiledName) {
            int32_t procCount = (int32_t)arrlen(sProcTable);

            for (int32_t i = 0; i < procCount; i++) {
                char fullName[128];

                if (sProcTable[i].objName[0] &&
                    strcmp(sProcTable[i].objName, "(General)") != 0) {
                    snprintf(fullName, sizeof(fullName), "%s_%s",
                             sProcTable[i].objName, sProcTable[i].evtName);
                } else {
                    snprintf(fullName, sizeof(fullName), "%s", sProcTable[i].evtName);
                }

                if (strcasecmp(fullName, compiledName) == 0) {
                    snprintf(procBuf, sizeof(procBuf), "%s.%s",
                             sProcTable[i].objName, sProcTable[i].evtName);
                    procName = procBuf;
                    break;
                }
            }
        }
    }

    navigateToCodeLine(fileIdx, localLine, procName, true);
}

static void debugSetBreakTitles(bool paused) {
    if (!sDbgFormRt) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) {
        BasFormT *form = sDbgFormRt->forms[i];

        if (!form->window) {
            continue;
        }

        char *title = form->window->title;
        const char *tag = " [break]";
        int32_t tagLen = (int32_t)strlen(tag);
        int32_t titleLen = (int32_t)strlen(title);

        if (paused) {
            // Add [break] if not already there
            if (titleLen < tagLen || strcmp(title + titleLen - tagLen, tag) != 0) {
                if (titleLen + tagLen < MAX_TITLE_LEN) {
                    strcat(title, tag);
                    dvxInvalidateWindow(sAc, form->window);
                }
            }
        } else {
            // Remove [break] if present
            if (titleLen >= tagLen && strcmp(title + titleLen - tagLen, tag) == 0) {
                title[titleLen - tagLen] = '\0';
                dvxInvalidateWindow(sAc, form->window);
            }
        }
    }
}


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, localToConcatLine(wgtTextAreaGetCursorLine(sEditor)));
                }
                break;
        }

        sDbgState = DBG_RUNNING;
        sVm->running = true;
        sVm->debugPaused = false;
        debugSetBreakTitles(false);

        if (sEditor) {
            wgtInvalidatePaint(sEditor);
        }

        updateProjectMenuState();
        setStatus("Running...");
        return;
    }

    if (sDbgState == DBG_IDLE) {
        // Not running — compile and start in debug mode.
        sDbgEnabled      = true;
        sDbgBreakOnStart = true;
        compileAndRun();
        sDbgBreakOnStart = false;
    }
}


static void debugUpdateWindows(void) {
    // Auto-show debug windows if not already open
    if (!sLocalsWin) {
        showLocalsWindow();
    } else if (!sLocalsWin->visible) {
        dvxShowWindow(sAc, sLocalsWin);
    }

    if (!sCallStackWin) {
        showCallStackWindow();
    } else if (!sCallStackWin->visible) {
        dvxShowWindow(sAc, sCallStackWin);
    }

    updateLocalsWindow();
    updateCallStackWindow();
    updateWatchWindow();
}


static bool doEventsCallback(void *ctx) {
    (void)ctx;

    // Stop if IDE window was closed or DVX is shutting down
    if (!sWin || !sAc->running) {
        return false;
    }

    dvxUpdate(sAc);

    return sWin != NULL && sAc->running;
}


// ============================================================
// onFormWinMouse
// ============================================================
//
// Handle mouse events on the form designer window. Coordinates
// are relative to the window's client area (content box origin).


static void dsgnCopySelected(void) {
    if (!sDesigner.form || sDesigner.selectedIdx < 0) {
        return;
    }

    int32_t count = (int32_t)arrlen(sDesigner.form->controls);

    if (sDesigner.selectedIdx >= count) {
        return;
    }

    // Serialize the selected control to FRM text
    char buf[2048];
    int32_t pos = 0;
    DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx];

    pos += snprintf(buf + pos, sizeof(buf) - pos, "Begin %s %s\n", ctrl->typeName, ctrl->name);

    if (ctrl->index >= 0) {
        pos += snprintf(buf + pos, sizeof(buf) - pos, "    Index    = %d\n", (int)ctrl->index);
    }

    pos += snprintf(buf + pos, sizeof(buf) - pos, "    Caption  = \"%s\"\n", wgtGetText(ctrl->widget) ? wgtGetText(ctrl->widget) : "");

    if (ctrl->width > 0) {
        pos += snprintf(buf + pos, sizeof(buf) - pos, "    MinWidth = %d\n", (int)ctrl->width);
    }

    if (ctrl->height > 0) {
        pos += snprintf(buf + pos, sizeof(buf) - pos, "    MinHeight = %d\n", (int)ctrl->height);
    }

    if (ctrl->maxWidth > 0) {
        pos += snprintf(buf + pos, sizeof(buf) - pos, "    MaxWidth = %d\n", (int)ctrl->maxWidth);
    }

    if (ctrl->maxHeight > 0) {
        pos += snprintf(buf + pos, sizeof(buf) - pos, "    MaxHeight = %d\n", (int)ctrl->maxHeight);
    }

    if (ctrl->weight > 0) {
        pos += snprintf(buf + pos, sizeof(buf) - pos, "    Weight   = %d\n", (int)ctrl->weight);
    }

    for (int32_t i = 0; i < ctrl->propCount; i++) {
        if (strcasecmp(ctrl->props[i].name, "Caption") == 0) {
            continue;
        }

        pos += snprintf(buf + pos, sizeof(buf) - pos, "    %s = \"%s\"\n", ctrl->props[i].name, ctrl->props[i].value);
    }

    // Save interface properties
    if (ctrl->widget) {
        const char *wgtName = wgtFindByBasName(ctrl->typeName);
        const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;

        if (iface) {
            for (int32_t i = 0; i < iface->propCount; i++) {
                const WgtPropDescT *p = &iface->props[i];

                if (!p->getFn) {
                    continue;
                }

                bool already = false;

                for (int32_t j = 0; j < ctrl->propCount; j++) {
                    if (strcasecmp(ctrl->props[j].name, p->name) == 0) {
                        already = true;
                        break;
                    }
                }

                if (already) {
                    continue;
                }

                if (p->type == WGT_IFACE_ENUM && p->enumNames) {
                    int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
                    const char *name = (v >= 0 && p->enumNames[v]) ? p->enumNames[v] : NULL;

                    if (name) {
                        pos += snprintf(buf + pos, sizeof(buf) - pos, "    %s = %s\n", p->name, name);
                    }
                } else if (p->type == WGT_IFACE_INT) {
                    int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(ctrl->widget);
                    pos += snprintf(buf + pos, sizeof(buf) - pos, "    %s = %d\n", p->name, (int)v);
                } else if (p->type == WGT_IFACE_BOOL) {
                    bool v = ((bool (*)(const WidgetT *))p->getFn)(ctrl->widget);
                    pos += snprintf(buf + pos, sizeof(buf) - pos, "    %s = %s\n", p->name, v ? "True" : "False");
                }
            }
        }
    }

    pos += snprintf(buf + pos, sizeof(buf) - pos, "End\n");
    dvxClipboardCopy(buf, pos);
}


static void dsgnPasteControl(void) {
    if (!sDesigner.form || !sDesigner.form->contentBox) {
        return;
    }

    int32_t clipLen = 0;
    const char *clip = dvxClipboardGet(&clipLen);

    if (!clip || clipLen <= 0) {
        return;
    }

    // Verify it looks like a control definition
    if (strncasecmp(clip, "Begin ", 6) != 0) {
        return;
    }

    // Parse type and name from "Begin TypeName CtrlName"
    const char *rest = clip + 6;
    char typeName[DSGN_MAX_NAME];
    char ctrlName[DSGN_MAX_NAME];

    int32_t ti = 0;

    while (*rest && *rest != ' ' && *rest != '\t' && ti < DSGN_MAX_NAME - 1) {
        typeName[ti++] = *rest++;
    }

    typeName[ti] = '\0';

    rest = dvxSkipWs(rest);

    int32_t ci = 0;

    while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < DSGN_MAX_NAME - 1) {
        ctrlName[ci++] = *rest++;
    }

    ctrlName[ci] = '\0';

    // Check if a control with the same name exists -- create control array
    char newName[DSGN_MAX_NAME];
    int32_t newIndex = -1;
    bool nameExists  = false;
    int32_t highIdx  = -1;
    int32_t existCount = (int32_t)arrlen(sDesigner.form->controls);

    for (int32_t i = 0; i < existCount; i++) {
        if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) {
            nameExists = true;
            if (sDesigner.form->controls[i]->index > highIdx) {
                highIdx = sDesigner.form->controls[i]->index;
            }
        }
    }

    if (nameExists) {
        // Already a control array -- just add the next element
        if (highIdx >= 0) {
            snprintf(newName, DSGN_MAX_NAME, "%s", ctrlName);
            newIndex = highIdx + 1;
        } else {
            // Not yet an array -- ask the user
            int32_t result = dvxMessageBox(sAc, "Paste",
                "A control with this name already exists.\n"
                "Create a control array?",
                MB_YESNO | MB_ICONQUESTION);

            if (result == ID_YES) {
                // Convert existing control to index 0
                for (int32_t i = 0; i < existCount; i++) {
                    if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0) {
                        sDesigner.form->controls[i]->index = 0;
                        break;
                    }
                }

                snprintf(newName, DSGN_MAX_NAME, "%s", ctrlName);
                newIndex = 1;
            } else {
                // Rename to a unique name
                dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME);
            }
        }
    } else {
        dsgnAutoName(&sDesigner, typeName, newName, DSGN_MAX_NAME);
    }

    // Create the control
    DsgnControlT ctrl;
    memset(&ctrl, 0, sizeof(ctrl));
    ctrl.index = newIndex;
    snprintf(ctrl.name, DSGN_MAX_NAME, "%s", newName);
    snprintf(ctrl.typeName, DSGN_MAX_NAME, "%s", typeName);

    // Parse properties from the clipboard text
    const char *line = rest;

    while (*line) {
        while (*line == '\r' || *line == '\n') {
            line++;
        }

        if (!*line) {
            break;
        }

        line = dvxSkipWs(line);

        // "End" terminates
        if (strncasecmp(line, "End", 3) == 0 && (line[3] == '\0' || line[3] == '\r' || line[3] == '\n')) {
            break;
        }

        // Parse "Key = Value"
        const char *eq = strchr(line, '=');

        if (eq) {
            char key[DSGN_MAX_NAME];
            int32_t klen = 0;
            const char *kp = line;

            while (kp < eq && *kp != ' ' && *kp != '\t' && klen < DSGN_MAX_NAME - 1) {
                key[klen++] = *kp++;
            }

            key[klen] = '\0';

            const char *vp = dvxSkipWs(eq + 1);

            char val[DSGN_MAX_TEXT];
            int32_t vi = 0;

            if (*vp == '"') {
                vp++;

                while (*vp && *vp != '"' && vi < DSGN_MAX_TEXT - 1) {
                    val[vi++] = *vp++;
                }
            } else {
                while (*vp && *vp != '\r' && *vp != '\n' && vi < DSGN_MAX_TEXT - 1) {
                    val[vi++] = *vp++;
                }

                while (vi > 0 && (val[vi - 1] == ' ' || val[vi - 1] == '\t')) {
                    vi--;
                }
            }

            val[vi] = '\0';

            if (strcasecmp(key, "MinWidth") == 0 || strcasecmp(key, "Width") == 0) {
                ctrl.width = atoi(val);
            } else if (strcasecmp(key, "MinHeight") == 0 || strcasecmp(key, "Height") == 0) {
                ctrl.height = atoi(val);
            } else if (strcasecmp(key, "MaxWidth") == 0) {
                ctrl.maxWidth = atoi(val);
            } else if (strcasecmp(key, "MaxHeight") == 0) {
                ctrl.maxHeight = atoi(val);
            } else if (strcasecmp(key, "Weight") == 0) {
                ctrl.weight = atoi(val);
            } else if (ctrl.propCount < DSGN_MAX_PROPS) {
                snprintf(ctrl.props[ctrl.propCount].name, DSGN_MAX_NAME, "%s", key);
                snprintf(ctrl.props[ctrl.propCount].value, DSGN_MAX_TEXT, "%s", val);
                ctrl.propCount++;
            }
        }

        // Advance to next line
        while (*line && *line != '\n') {
            line++;
        }
    }

    // Create the live widget
    WidgetT *parentWidget = sDesigner.form->contentBox;
    ctrl.widget = dsgnCreateDesignWidget(typeName, parentWidget);

    if (ctrl.widget) {
        if (ctrl.width > 0)     { ctrl.widget->minW = wgtPixels(ctrl.width); }
        if (ctrl.height > 0)    { ctrl.widget->minH = wgtPixels(ctrl.height); }
        if (ctrl.maxWidth > 0)  { ctrl.widget->maxW = wgtPixels(ctrl.maxWidth); }
        if (ctrl.maxHeight > 0) { ctrl.widget->maxH = wgtPixels(ctrl.maxHeight); }
        ctrl.widget->weight = ctrl.weight;
        wgtSetName(ctrl.widget, ctrl.name);

        const char *caption = NULL;

        for (int32_t pi = 0; pi < ctrl.propCount; pi++) {
            if (strcasecmp(ctrl.props[pi].name, "Caption") == 0) {
                caption = ctrl.props[pi].value;
                break;
            }
        }

        if (caption) {
            wgtSetText(ctrl.widget, caption);
        }

        // Apply interface properties (Alignment, etc.)
        const char *wgtName = wgtFindByBasName(typeName);
        const WgtIfaceT *iface = wgtName ? wgtGetIface(wgtName) : NULL;

        if (iface) {
            for (int32_t pi = 0; pi < iface->propCount; pi++) {
                const WgtPropDescT *p = &iface->props[pi];

                if (!p->setFn) {
                    continue;
                }

                const char *val = NULL;

                for (int32_t j = 0; j < ctrl.propCount; j++) {
                    if (strcasecmp(ctrl.props[j].name, p->name) == 0) {
                        val = ctrl.props[j].value;
                        break;
                    }
                }

                if (!val) {
                    continue;
                }

                if (p->type == WGT_IFACE_ENUM && p->enumNames) {
                    for (int32_t en = 0; p->enumNames[en]; en++) {
                        if (strcasecmp(p->enumNames[en], val) == 0) {
                            ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, en);
                            break;
                        }
                    }
                } else if (p->type == WGT_IFACE_INT) {
                    ((void (*)(WidgetT *, int32_t))p->setFn)(ctrl.widget, atoi(val));
                } else if (p->type == WGT_IFACE_BOOL) {
                    ((void (*)(WidgetT *, bool))p->setFn)(ctrl.widget, strcasecmp(val, "True") == 0);
                } else if (p->type == WGT_IFACE_STRING) {
                    ((void (*)(WidgetT *, const char *))p->setFn)(ctrl.widget, val);
                }
            }
        }
    }

    DsgnControlT *heapCtrl = malloc(sizeof(DsgnControlT));
    *heapCtrl = ctrl;
    arrput(sDesigner.form->controls, heapCtrl);
    sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1;
    sDesigner.form->dirty = true;

    prpRebuildTree(&sDesigner);
    prpRefresh(&sDesigner);

    if (sFormWin) {
        dvxInvalidateWindow(sAc, sFormWin);
    }
}


// Auto-create an implicit project when opening a file without one.
// Derives project name and directory from the file path. Also adds
// a matching .frm file if one exists alongside the .bas.

static void ensureProject(const char *filePath) {
    if (sProject.projectPath[0] != '\0') {
        return;
    }

    // Derive directory and base name from path
    char dir[DVX_MAX_PATH];
    char baseName[PRJ_MAX_NAME];
    snprintf(dir, sizeof(dir), "%s", filePath);

    char *sep = platformPathDirEnd(dir);

    const char *fileName = filePath;

    if (sep) {
        fileName = sep + 1;
        *sep = '\0';
    } else {
        dir[0] = '.';
        dir[1] = '\0';
    }

    // Strip extension for project name
    // Length-clamped memcpy instead of strncpy/snprintf because
    // GCC warns about both when source (DVX_MAX_PATH) exceeds
    // the buffer (PRJ_MAX_NAME), even though truncation is safe.
    int32_t nl = (int32_t)strlen(fileName);

    if (nl >= PRJ_MAX_NAME) {
        nl = PRJ_MAX_NAME - 1;
    }

    memcpy(baseName, fileName, nl);
    baseName[nl] = '\0';

    char *dot = strrchr(baseName, '.');

    if (dot) {
        *dot = '\0';
    }

    prjNew(&sProject, baseName, dir, sPrefs);

    // Determine if this is a .bas or .frm
    const char *ext = strrchr(filePath, '.');
    bool isForm = (ext && strcasecmp(ext, ".frm") == 0);

    prjAddFile(&sProject, fileName, isForm);

    // If it's a .bas, check for a matching .frm and add it too
    if (!isForm) {
        char frmPath[DVX_MAX_PATH];
        snprintf(frmPath, sizeof(frmPath), "%s", filePath);
        char *frmDot = strrchr(frmPath, '.');

        if (frmDot && (frmDot - frmPath) + 4 < DVX_MAX_PATH) {
            snprintf(frmDot, sizeof(frmPath) - (frmDot - frmPath), ".frm");
        } else if ((int32_t)strlen(frmPath) + 4 < DVX_MAX_PATH) {
            snprintf(frmPath + strlen(frmPath), sizeof(frmPath) - strlen(frmPath), ".frm");
        }

        FILE *frmFile = fopen(frmPath, "r");

        if (frmFile) {
            fclose(frmFile);

            // Get just the filename portion
            const char *frmName = platformPathBaseName(frmPath);
            prjAddFile(&sProject, frmName, true);
        }
    }

    sProject.dirty = false;
    sProject.activeFileIdx = -1;

    prjLoadAllFiles(&sProject, sAc);

    char title[300];
    snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);

    if (sWin) {
        dvxSetTitle(sAc, sWin, title);
    }

    setStatus("Project created.");
    updateProjectMenuState();
}


static void evaluateImmediate(const char *expr) {
    if (!expr || *expr == '\0') {
        return;
    }

    // Try assignment first when paused
    if (immTryAssign(expr)) {
        return;
    }

    char wrapped[1024];

    // If it already starts with a statement keyword, use as-is
    if (strncasecmp(expr, "PRINT", 5) == 0 || strncasecmp(expr, "DIM", 3) == 0 || strncasecmp(expr, "LET", 3) == 0) {
        snprintf(wrapped, sizeof(wrapped), "%s", expr);
    } else {
        snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr);
    }

    BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));

    if (!parser) {
        return;
    }

    basParserInit(parser, wrapped, (int32_t)strlen(wrapped));

    if (!basParse(parser)) {
        // Show error inline
        immPrintCallback(NULL, "Error: ", false);
        immPrintCallback(NULL, parser->error, true);
        basParserFree(parser);
        free(parser);
        return;
    }

    BasModuleT *mod = basParserBuildModule(parser);
    basParserFree(parser);
    free(parser);

    if (!mod) {
        return;
    }

    BasVmT *vm = basVmCreate();
    basVmLoadModule(vm, mod);
    vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
    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 && result != BAS_VM_BREAKPOINT) {
        immPrintCallback(NULL, "Error: ", false);
        immPrintCallback(NULL, basVmGetError(vm), true);
    }

    basVmDestroy(vm);
    basModuleFree(mod);
}


static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize) {
    if (!sVm || !sDbgModule || !sDbgModule->debugVars) {
        return false;
    }

    // Wrap expression: PRINT expr
    char wrapped[512];
    snprintf(wrapped, sizeof(wrapped), "PRINT %s", expr);

    BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));

    if (!parser) {
        return false;
    }

    basParserInit(parser, wrapped, (int32_t)strlen(wrapped));

    // Pre-populate the symbol table with debug vars from the paused VM.
    // All variables are added as globals so the expression can reference them.
    // Find current proc for local variable matching.
    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;
        }
    }

    // Track which debug vars we added and their assigned global indices
    int32_t varMap[BAS_VM_MAX_GLOBALS]; // maps temp global idx -> debug var idx
    int32_t varMapCount = 0;

    for (int32_t i = 0; i < sDbgModule->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) {
        const BasDebugVarT *dv = &sDbgModule->debugVars[i];

        // Skip locals from other procs
        if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) {
            continue;
        }

        if (dv->scope == SCOPE_FORM && !sVm->currentFormVars) {
            continue;
        }

        // Skip mangled names
        const char *dollar = strchr(dv->name, '$');

        if (dollar && dollar[1] != '\0') {
            continue;
        }

        // Add to parser's symbol table as a global
        BasSymbolT *sym = basSymTabAdd(&parser->sym, dv->name, SYM_VARIABLE, dv->dataType);

        if (sym) {
            sym->scope   = SCOPE_GLOBAL;
            sym->index   = varMapCount;
            varMap[varMapCount] = i;
            varMapCount++;
        }
    }

    parser->cg.globalCount = varMapCount;

    // Parse and compile
    if (!basParse(parser)) {
        basParserFree(parser);
        free(parser);
        return false;
    }

    BasModuleT *mod = basParserBuildModule(parser);
    basParserFree(parser);
    free(parser);

    if (!mod) {
        return false;
    }

    // Create temp VM
    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 values from the paused VM into the temp VM's globals
    for (int32_t g = 0; g < varMapCount; g++) {
        const BasDebugVarT *dv = &sDbgModule->debugVars[varMap[g]];
        BasValueT val;
        memset(&val, 0, sizeof(val));
        readDebugVar(dv, &val);
        tvm->globals[g] = basValCopy(val);
    }

    // Set up capture callback
    sWatchPrintBuf[0] = '\0';
    sWatchPrintLen     = 0;
    basVmSetPrintCallback(tvm, watchPrintCallback, NULL);

    // Run
    basVmRun(tvm);

    // Strip trailing newline
    if (sWatchPrintLen > 0 && sWatchPrintBuf[sWatchPrintLen - 1] == '\n') {
        sWatchPrintBuf[--sWatchPrintLen] = '\0';
    }

    snprintf(outBuf, outBufSize, "%s", sWatchPrintBuf);

    basVmDestroy(tvm);
    basModuleFree(mod);
    return sWatchPrintLen > 0;
}


// extractNewProcs -- scan a buffer for Sub/Function declarations that
// don't belong (e.g. user typed a new Sub in the General section).
// Extracts them into new sProcBufs entries and removes them from the
// source buffer. Returns a new buffer (caller frees) or NULL if no
// extraction was needed.

static char *extractNewProcs(const char *buf) {
    if (!buf || !buf[0]) {
        return NULL;
    }

    // Scan for Sub/Function at the start of a line
    bool found = false;
    const char *pos = buf;

    while (*pos) {
        const char *trimmed = dvxSkipWs(pos);

        bool isSub  = (strncasecmp(trimmed, "SUB ", 4) == 0);
        bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);

        if (isSub || isFunc) {
            found = true;
            break;
        }

        while (*pos && *pos != '\n') { pos++; }
        if (*pos == '\n') { pos++; }
    }

    if (!found) {
        return NULL;
    }

    // Build the remaining text (before the first proc) and extract procs
    int32_t bufLen    = (int32_t)strlen(buf);
    char *remaining   = (char *)malloc(bufLen + 1);

    if (!remaining) {
        return NULL;
    }

    int32_t remPos = 0;
    pos = buf;

    while (*pos) {
        const char *lineStart = pos;
        const char *trimmed   = dvxSkipWs(pos);

        bool isSub  = (strncasecmp(trimmed, "SUB ", 4) == 0);
        bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);

        if (isSub || isFunc) {
            // Extract procedure name for duplicate check
            const char *np = dvxSkipWs(trimmed + (isSub ? 4 : 9));

            char newName[128];
            int32_t nn = 0;

            while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
                newName[nn++] = *np++;
            }

            newName[nn] = '\0';

            // Check for duplicate against existing proc buffers
            bool isDuplicate = false;

            for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
                if (!sProcBufs[p]) { continue; }

                const char *ep = dvxSkipWs(sProcBufs[p]);

                if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
                else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }

                ep = dvxSkipWs(ep);

                char existName[128];
                int32_t en = 0;

                while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && en < 127) {
                    existName[en++] = *ep++;
                }

                existName[en] = '\0';

                if (strcasecmp(newName, existName) == 0) {
                    isDuplicate = true;
                    break;
                }
            }

            // Find End Sub / End Function
            const char *endTag    = isSub ? "END SUB" : "END FUNCTION";
            int32_t     endTagLen = isSub ? 7 : 12;
            const char *scan      = pos;

            while (*scan && *scan != '\n') { scan++; }
            if (*scan == '\n') { scan++; }

            while (*scan) {
                const char *sl = dvxSkipWs(scan);

                if (strncasecmp(sl, endTag, endTagLen) == 0) {
                    while (*scan && *scan != '\n') { scan++; }
                    if (*scan == '\n') { scan++; }
                    break;
                }

                while (*scan && *scan != '\n') { scan++; }
                if (*scan == '\n') { scan++; }
            }

            if (isDuplicate) {
                // Leave it in the General section -- compiler will report the error
                while (*pos && *pos != '\n') {
                    remaining[remPos++] = *pos++;
                }

                if (*pos == '\n') {
                    remaining[remPos++] = *pos++;
                }
            } else {
                // Extract this procedure into a new buffer
                int32_t procLen = (int32_t)(scan - lineStart);
                char *procBuf   = (char *)malloc(procLen + 1);

                if (procBuf) {
                    memcpy(procBuf, lineStart, procLen);
                    procBuf[procLen] = '\0';
                    arrput(sProcBufs, procBuf);
                }

                pos = scan;
            }

            continue;
        }

        // Copy non-proc lines to remaining
        while (*pos && *pos != '\n') {
            remaining[remPos++] = *pos++;
        }

        if (*pos == '\n') {
            remaining[remPos++] = *pos++;
        }
    }

    remaining[remPos] = '\0';
    return remaining;
}


// findDebugVar -- find a debug variable by name, respecting scope
static const BasDebugVarT *findDebugVar(const char *name) {
    if (!sDbgModule || !sDbgModule->debugVars) {
        return NULL;
    }

    // Find current proc index
    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;
        }
    }

    for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) {
        const BasDebugVarT *dv = &sDbgModule->debugVars[i];

        if (strcasecmp(dv->name, name) != 0) {
            continue;
        }

        if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) {
            continue;
        }

        if (dv->scope == SCOPE_FORM && !sVm->currentFormVars) {
            continue;
        }

        return dv;
    }

    return NULL;
}


static bool findInProject(const char *needle, bool caseSensitive) {
    if (!needle || !needle[0] || sProject.fileCount == 0) {
        return false;
    }

    int32_t needleLen = (int32_t)strlen(needle);

    // Stash current editor state so all buffers are up-to-date
    stashCurrentFile();

    // Start from the active file, searching from after the current selection
    int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0;

    // If the editor is open on the current file, try the current proc
    // first (no wrap — returns false if no more matches ahead).
    if (sEditor && sEditorFileIdx == startFile) {
        if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) {
            return true;
        }

        // No more matches in current proc — search remaining procs
        int32_t procCount = (int32_t)arrlen(sProcBufs);

        for (int32_t p = sCurProcIdx + 1; p < procCount; p++) {
            if (!sProcBufs[p]) {
                continue;
            }

            const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);

            if (found) {
                showProcAndFind(p, needle, caseSensitive);
                return true;
            }
        }

        // Search General section if we started in a proc
        if (sCurProcIdx >= 0 && sGeneralBuf) {
            const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);

            if (found) {
                showProcAndFind(-1, needle, caseSensitive);
                return true;
            }
        }

        // Search procs before the current one (wrap within file)
        for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) {
            if (!sProcBufs[p]) {
                continue;
            }

            const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);

            if (found) {
                showProcAndFind(p, needle, caseSensitive);
                return true;
            }
        }

        // Move to next file
        startFile = (startFile + 1) % sProject.fileCount;
    }

    // Search remaining files, proc by proc.
    // startFile was advanced past the current file if we already searched it.
    int32_t filesToSearch = sProject.fileCount;

    if (sEditor && sEditorFileIdx >= 0) {
        filesToSearch--; // skip the file we already searched above
    }

    for (int32_t attempt = 0; attempt < filesToSearch; attempt++) {
        int32_t fileIdx = (startFile + attempt) % sProject.fileCount;

        // Activate the file to load its proc buffers
        activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE);

        // Search General section
        if (sGeneralBuf) {
            const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen);

            if (found) {
                showProcAndFind(-1, needle, caseSensitive);
                return true;
            }
        }

        // Search each procedure
        int32_t procCount = (int32_t)arrlen(sProcBufs);

        for (int32_t p = 0; p < procCount; p++) {
            if (!sProcBufs[p]) {
                continue;
            }

            const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen);

            if (found) {
                showProcAndFind(p, needle, caseSensitive);
                return true;
            }
        }
    }

    return false;
}


// Case-insensitive strstr replacement (strcasestr is a GNU extension)
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) {
    for (; *haystack; haystack++) {
        if (strncasecmp(haystack, needle, needleLen) == 0) {
            return haystack;
        }
    }

    return NULL;
}


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) {
                snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - 3), v->strVal->data);
            } else {
                snprintf(buf, bufSize, "\"\"");
            }
            break;
        }
        case BAS_TYPE_ARRAY: {
            BasArrayT *arr = v->arrVal;

            if (!arr) {
                snprintf(buf, bufSize, "(uninitialized)");
                break;
            }

            int32_t pos = snprintf(buf, bufSize, "%s(", typeNameStr(arr->elementType));

            for (int32_t d = 0; d < arr->dims && pos < bufSize - 10; d++) {
                if (d > 0) {
                    pos += snprintf(buf + pos, bufSize - pos, ", ");
                }

                pos += snprintf(buf + pos, bufSize - pos, "%d To %d",
                                (int)arr->lbound[d], (int)arr->ubound[d]);
            }

            snprintf(buf + pos, bufSize - pos, ") [%d]", (int)arr->totalElements);
            break;
        }
        default: snprintf(buf, bufSize, "..."); break;
    }
}


// ============================================================
// updateDropdowns
// ============================================================
//
// Scan the source for SUB/FUNCTION declarations and populate
// the Object and Event dropdowns. Procedure names are split on
// '_' into ObjectName and EventName (e.g. "Command1_Click").

// freeProcBufs -- release all procedure buffers

static void freeProcBufs(void) {
    free(sGeneralBuf);
    sGeneralBuf = NULL;

    for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
        free(sProcBufs[i]);
    }

    arrfree(sProcBufs);
    sProcBufs      = NULL;
    sCurProcIdx    = -2;
    sEditorFileIdx = -1;
}


// getDebugVarSlot -- return a pointer to the actual BasValueT in the running VM
static BasValueT *getDebugVarSlot(const BasDebugVarT *dv) {
    if (!sVm) {
        return NULL;
    }

    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) {
            return &frame->locals[dv->index];
        }
    } else if (dv->scope == SCOPE_GLOBAL) {
        if (dv->index >= 0 && dv->index < BAS_VM_MAX_GLOBALS) {
            return &sVm->globals[dv->index];
        }
    } else if (dv->scope == SCOPE_FORM && sVm->currentFormVars) {
        if (dv->index >= 0 && dv->index < sVm->currentFormVarCount) {
            return &sVm->currentFormVars[dv->index];
        }
    }

    return NULL;
}


static const char *getEventExtraParams(const char *evtName) {
    if (strcasecmp(evtName, "KeyPress") == 0)  { return ", KeyAscii As Integer"; }
    if (strcasecmp(evtName, "KeyDown") == 0)   { return ", KeyCode As Integer, Shift As Integer"; }
    if (strcasecmp(evtName, "KeyUp") == 0)     { return ", KeyCode As Integer, Shift As Integer"; }
    if (strcasecmp(evtName, "MouseDown") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
    if (strcasecmp(evtName, "MouseUp") == 0)   { return ", Button As Integer, X As Integer, Y As Integer"; }
    if (strcasecmp(evtName, "MouseMove") == 0) { return ", Button As Integer, X As Integer, Y As Integer"; }
    if (strcasecmp(evtName, "Scroll") == 0)    { return ", Delta As Integer"; }
    return "";
}


static bool getFindForward(void) {
    if (!sDirGroup) {
        return true;
    }

    return wgtRadioGetIndex(sDirGroup) == 0;
}


static bool getFindMatchCase(void) {
    return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck);
}


// ============================================================
// Find/Replace dialog (modeless)
// ============================================================


static FindScopeE getFindScope(void) {
    if (!sScopeGroup) {
        return ScopeProjE;
    }

    int32_t idx = wgtRadioGetIndex(sScopeGroup);

    switch (idx) {
        case 0:  return ScopeFuncE;
        case 1:  return ScopeObjE;
        case 2:  return ScopeFileE;
        default: return ScopeProjE;
    }
}


// getFullSource -- reassemble all buffers into one source string
// Caller must free the returned buffer.

static char *sFullSourceCache = NULL;

static const char *getFullSource(void) {
    saveCurProc();

    free(sFullSourceCache);

    // Calculate total length
    int32_t totalLen = 0;

    if (sGeneralBuf && sGeneralBuf[0]) {
        totalLen += (int32_t)strlen(sGeneralBuf);
        totalLen += 2; // blank line separator
    }

    int32_t procCount = (int32_t)arrlen(sProcBufs);

    for (int32_t i = 0; i < procCount; i++) {
        if (sProcBufs[i]) {
            totalLen += (int32_t)strlen(sProcBufs[i]);
            totalLen += 2; // newline + blank line between procedures
        }
    }

    sFullSourceCache = (char *)malloc(totalLen + 1);

    if (!sFullSourceCache) {
        return "";
    }

    int32_t pos = 0;

    if (sGeneralBuf && sGeneralBuf[0]) {
        int32_t len = (int32_t)strlen(sGeneralBuf);
        memcpy(sFullSourceCache + pos, sGeneralBuf, len);
        pos += len;

        if (pos > 0 && sFullSourceCache[pos - 1] != '\n') {
            sFullSourceCache[pos++] = '\n';
        }

        sFullSourceCache[pos++] = '\n';
    }

    for (int32_t i = 0; i < procCount; i++) {
        if (sProcBufs[i]) {
            int32_t len = (int32_t)strlen(sProcBufs[i]);
            memcpy(sFullSourceCache + pos, sProcBufs[i], len);
            pos += len;

            if (pos > 0 && sFullSourceCache[pos - 1] != '\n') {
                sFullSourceCache[pos++] = '\n';
            }

            // Blank line between procedures
            if (i < procCount - 1) {
                sFullSourceCache[pos++] = '\n';
            }
        }
    }

    sFullSourceCache[pos] = '\0';
    return sFullSourceCache;
}


static WindowT *getLastFocusWin(void) {
    if (sLastFocusWin == sCodeWin ||
        sLastFocusWin == sOutWin ||
        sLastFocusWin == sImmWin) {
        return sLastFocusWin;
    }

    sLastFocusWin = NULL;
    return NULL;
}


static void handleEditCmd(int32_t cmd) {
    switch (cmd) {
        case CMD_CUT:
        case CMD_COPY:
        case CMD_PASTE:
        case CMD_SELECT_ALL: {
            static const int32_t keys[] = { 24, 3, 22, 1 };
            int32_t key = keys[cmd - CMD_CUT];
            WindowT *target = getLastFocusWin();

            if (target && target->onKey) {
                target->onKey(target, key, ACCEL_CTRL);
            }

            break;
        }

        case CMD_DELETE:
            if (sFormWin && sDesigner.selectedIdx >= 0) {
                int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
                dsgnOnKey(&sDesigner, KEY_DELETE);
                int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);

                if (newCount != prevCount) {
                    prpRebuildTree(&sDesigner);
                    prpRefresh(&sDesigner);
                    dvxInvalidateWindow(sAc, sFormWin);
                }
            }
            break;

        case CMD_FIND:
            openFindDialog(false);
            break;

        case CMD_FIND_NEXT:
            if (sFindText[0]) {
                onFindNext(NULL);
            } else {
                openFindDialog(false);
            }
            break;

        case CMD_REPLACE:
            openFindDialog(true);
            break;
    }
}


static void handleFileCmd(int32_t cmd) {
    switch (cmd) {
        case CMD_OPEN:
            loadFile();
            break;

        case CMD_SAVE:
            saveFile();
            break;

        case CMD_SAVE_ALL:
            saveFile();

            for (int32_t i = 0; i < sProject.fileCount; i++) {
                if (i == sProject.activeFileIdx) {
                    continue;
                }

                if (sProject.files[i].modified && sProject.files[i].buffer) {
                    char fullPath[DVX_MAX_PATH];
                    prjFullPath(&sProject, i, fullPath, sizeof(fullPath));

                    FILE *f = fopen(fullPath, "w");

                    if (f) {
                        fputs(sProject.files[i].buffer, f);
                        fclose(f);
                        sProject.files[i].modified = false;
                    }
                }
            }

            if (sProject.projectPath[0] != '\0') {
                prjSave(&sProject);
                sProject.dirty = false;
            }

            setStatus("All files saved.");
            updateDirtyIndicators();
            break;

        case CMD_MAKE_EXE:
            makeExecutable();
            break;

        case CMD_EXIT:
            if (sWin) {
                onClose(sWin);
            }
            break;

        default:
            // Recent files
            if (cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) {
                recentOpen(cmd - CMD_RECENT_BASE);
            }
            break;
    }
}


static void handleProjectCmd(int32_t cmd) {
    switch (cmd) {
        case CMD_PRJ_NEW:
            newProject();
            break;

        case CMD_PRJ_OPEN:
            openProject();
            break;

        case CMD_PRJ_SAVE:
            if (sProject.projectPath[0] != '\0') {
                prjSave(&sProject);
                sProject.dirty = false;
                setStatus("Project saved.");
            }
            break;

        case CMD_PRJ_CLOSE:
            if (promptAndSave()) {
                closeProject();
            }
            break;

        case CMD_PRJ_PROPS:
            if (sProject.projectPath[0] != '\0') {
                if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) {
                    char title[300];
                    snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
                    dvxSetTitle(sAc, sWin, title);

                    if (sProjectWin) {
                        prjRebuildTree(&sProject);
                    }
                }
            }
            break;

        case CMD_PRJ_REMOVE: {
            int32_t rmIdx = prjGetSelectedFileIdx();

            if (rmIdx >= 0 && rmIdx < sProject.fileCount) {
                PrjFileT *rmFile = &sProject.files[rmIdx];
                char rmMsg[DVX_MAX_PATH + 32];
                snprintf(rmMsg, sizeof(rmMsg), "Remove %s from the project?", rmFile->path);

                if (dvxMessageBox(sAc, "Remove File", rmMsg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
                    break;
                }

                if (rmFile->modified) {
                    int32_t result = dvxPromptSave(sAc, "DVX BASIC");

                    if (result == DVX_SAVE_CANCEL) {
                        break;
                    }

                    if (result == DVX_SAVE_YES) {
                        saveActiveFile();
                    }
                }

                removeBreakpointsForFile(rmIdx);
                prjRemoveFile(&sProject, rmIdx);

                if (sProject.activeFileIdx == rmIdx) {
                    sProject.activeFileIdx = -1;
                } else if (sProject.activeFileIdx > rmIdx) {
                    sProject.activeFileIdx--;
                }

                prjRebuildTree(&sProject);
                updateProjectMenuState();
            }
            break;
        }
    }
}


static void handleRunCmd(int32_t cmd) {
    switch (cmd) {
        case CMD_RUN:
            if (sDbgState == DBG_PAUSED) {
                // Resume from breakpoint — clear debug mode so it runs free
                sDbgCurrentLine = -1;
                sDbgState       = DBG_RUNNING;
                sDbgEnabled     = false;
                debugSetBreakTitles(false);
                if (sVm) {
                    sVm->debugPaused    = false;
                    sVm->debugBreak     = false;
                    sVm->stepOverDepth  = -1;
                    sVm->stepOutDepth   = -1;
                    sVm->runToCursorLine = -1;
                    basVmSetBreakpoints(sVm, NULL, 0);
                    sVm->running        = true;
                }
                if (sEditor) {
                    wgtInvalidatePaint(sEditor);
                }
                updateProjectMenuState();
                setStatus("Running...");
            } else {
                sDbgEnabled = false;
                compileAndRun();
            }
            break;

        case CMD_DEBUG:
            if (sDbgState == DBG_PAUSED) {
                // Already debugging — resume, run to next breakpoint
                sDbgCurrentLine = -1;
                sDbgState       = DBG_RUNNING;
                debugSetBreakTitles(false);
                if (sVm) {
                    sVm->debugPaused    = false;
                    sVm->debugBreak     = false;
                    sVm->stepOverDepth  = -1;
                    sVm->stepOutDepth   = -1;
                    sVm->runToCursorLine = -1;
                    sVm->running        = true;
                }
                if (sEditor) {
                    wgtInvalidatePaint(sEditor);
                }
                updateProjectMenuState();
                setStatus("Debugging...");
            } else if (sDbgState == DBG_IDLE) {
                // Start in debug mode with breakpoints
                sDbgEnabled = true;
                compileAndRun();
            }
            break;

        case CMD_RUN_NOCMP:
            runCached();
            break;

        case CMD_STOP:
            sStopRequested = true;
            if (sVm) {
                sVm->running     = false;
                sVm->debugPaused = false;
            }
            sDbgState       = DBG_IDLE;
            sDbgCurrentLine = -1;
            sDbgEnabled     = false;
            if (sEditor) {
                wgtInvalidatePaint(sEditor);
            }
            updateProjectMenuState();
            setStatus("Program stopped.");
            break;

        case CMD_OUTPUT_TO_LOG:
            if (sWin && sWin->menuBar) {
                sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG);
            }
            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;

        case CMD_SAVE_ON_RUN:
            if (sWin && sWin->menuBar) {
                bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN);
                prefsSetBool(sPrefs, "run", "saveOnRun", save);
                prefsSave(sPrefs);
            }
            break;
    }
}


static void handleViewCmd(int32_t cmd) {
    switch (cmd) {
        case CMD_VIEW_CODE: {
            int32_t selFileIdx = prjGetSelectedFileIdx();

            if (selFileIdx >= 0 && selFileIdx != sProject.activeFileIdx) {
                activateFile(selFileIdx, ViewCodeE);
            } else if (sProject.activeFileIdx >= 0) {
                stashDesignerState();
            }

            break;
        }

        case CMD_VIEW_DESIGN: {
            int32_t selFileIdx = prjGetSelectedFileIdx();

            if (selFileIdx >= 0 && selFileIdx != sProject.activeFileIdx &&
                selFileIdx < sProject.fileCount && sProject.files[selFileIdx].isForm) {
                activateFile(selFileIdx, ViewDesignE);
            } else if (sProject.activeFileIdx >= 0) {
                switchToDesign();
            }

            break;
        }

        case CMD_VIEW_TOOLBAR:
            if (sToolbar && sWin->menuBar) {
                bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR);
                sToolbar->visible = show;
                dvxFitWindowH(sAc, sWin);
                prefsSetBool(sPrefs, "view", "toolbar", show);
                prefsSave(sPrefs);
            }
            break;

        case CMD_VIEW_STATUS:
            if (sStatusBar && sWin->menuBar) {
                bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS);
                sStatusBar->visible = show;
                dvxFitWindowH(sAc, sWin);
                prefsSetBool(sPrefs, "view", "statusbar", show);
                prefsSave(sPrefs);
            }
            break;

        case CMD_MENU_EDITOR: {
            // Activate selected form if not already active
            int32_t selMenuIdx = prjGetSelectedFileIdx();

            if (selMenuIdx >= 0 && selMenuIdx != sProject.activeFileIdx &&
                selMenuIdx < sProject.fileCount && sProject.files[selMenuIdx].isForm) {
                activateFile(selMenuIdx, ViewDesignE);
            }

            if (sDesigner.form) {
                // Snapshot old menu names for rename detection
                char **oldNames = NULL;
                int32_t oldCount = (int32_t)arrlen(sDesigner.form->menuItems);

                for (int32_t mi = 0; mi < oldCount; mi++) {
                    arrput(oldNames, strdup(sDesigner.form->menuItems[mi].name));
                }

                if (mnuEditorDialog(sAc, sDesigner.form)) {
                    sDesigner.form->dirty = true;

                    // Detect renames: match by position (items may have been
                    // reordered, but renamed items keep their index)
                    int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems);
                    int32_t minCount = oldCount < newCount ? oldCount : newCount;

                    for (int32_t mi = 0; mi < minCount; mi++) {
                        if (oldNames[mi][0] && sDesigner.form->menuItems[mi].name[0] &&
                            strcasecmp(oldNames[mi], sDesigner.form->menuItems[mi].name) != 0) {
                            ideRenameInCode(oldNames[mi], sDesigner.form->menuItems[mi].name);
                        }
                    }

                    // Rebuild menu bar preview
                    if (sFormWin) {
                        wmDestroyMenuBar(sFormWin);
                        dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
                        dvxInvalidateWindow(sAc, sFormWin);
                    }

                    // Rebuild Object dropdown to reflect added/removed/renamed items
                    updateDropdowns();
                }

                for (int32_t mi = 0; mi < oldCount; mi++) {
                    free(oldNames[mi]);
                }

                arrfree(oldNames);
            }
            break;
        }
    }
}


static void handleWindowCmd(int32_t cmd) {
    switch (cmd) {
        case CMD_WIN_CODE:
            showCodeWindow();
            if (sEditor && !sEditor->onChange) {
                sEditor->onChange = onEditorChange;
            }
            break;

        case CMD_WIN_OUTPUT:
            showOutputWindow();
            break;

        case CMD_WIN_IMM:
            showImmediateWindow();
            break;

        case CMD_WIN_LOCALS:
            showLocalsWindow();
            break;

        case CMD_WIN_CALLSTACK:
            showCallStackWindow();
            break;

        case CMD_WIN_WATCH:
            showWatchWindow();
            break;

        case CMD_WIN_BREAKPOINTS:
            showBreakpointWindow();
            break;

        case CMD_WIN_TOOLBOX:
            if (!sToolboxWin) {
                sToolboxWin = tbxCreate(sAc, &sDesigner);

                if (sToolboxWin) {
                    sToolboxWin->y          = toolbarBottom();
                    sToolboxWin->onMenu     = onMenu;
                    sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
                }
            }
            break;

        case CMD_WIN_PROPS:
            if (!sPropsWin) {
                sPropsWin = prpCreate(sAc, &sDesigner);

                if (sPropsWin) {
                    sPropsWin->y          = toolbarBottom();
                    sPropsWin->onMenu     = onMenu;
                    sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
                }
            }
            break;

        case CMD_WIN_PROJECT:
            if (!sProjectWin) {
                sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);

                if (sProjectWin) {
                    sProjectWin->y       = toolbarBottom() + 25;
                    sProjectWin->onClose = onProjectWinClose;
                }
            }
            break;
    }
}


static bool hasUnsavedData(void) {
    // Check the active editor/designer
    if (sDesigner.form && sDesigner.form->dirty) {
        return true;
    }

    // Check all project files
    for (int32_t i = 0; i < sProject.fileCount; i++) {
        if (sProject.files[i].modified) {
            return true;
        }
    }

    return sProject.dirty;
}

// Build control help topic from type name.
// Topic IDs in .bhs files follow the pattern ctrl.<lowercase(basName)>.
// Generated dynamically so third-party widgets get help automatically.
static void helpBuildCtrlTopic(const char *typeName, char *buf, int32_t bufSize) {
    int32_t off = snprintf(buf, bufSize, "ctrl.");
    for (int32_t i = 0; typeName[i] && off < bufSize - 1; i++) {
        buf[off++] = tolower((unsigned char)typeName[i]);
    }
    buf[off] = '\0';
}


static const char *helpLookupKeyword(const char *word) {
    for (int32_t i = 0; i < (int32_t)HELP_MAP_COUNT; i++) {
        if (strcasecmp(sHelpMap[i].keyword, word) == 0) {
            return sHelpMap[i].topic;
        }
    }
    return NULL;
}


static void helpQueryHandler(void *ctx) {
    (void)ctx;

    sCtx->helpTopic[0] = '\0';

    // Restore IDE help file (may have been swapped for a BASIC program's)
    snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sIdeHelpFile);

    // Determine which window is focused
    WindowT *focusWin = NULL;
    if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) {
        focusWin = sAc->stack.windows[sAc->stack.focusedIdx];
    }

    // Running BASIC program: check if focused window belongs to a form
    if (sDbgFormRt && sDbgFormRt->helpFile[0] && focusWin) {
        for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) {
            BasFormT *form = sDbgFormRt->forms[i];

            if (form->window == focusWin) {
                // Swap to the project's help file
                snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sDbgFormRt->helpFile);

                // Find the focused widget's HelpTopic
                WidgetT *focused = wgtGetFocused();

                if (focused && focused->userData) {
                    BasControlT *ctrl = (BasControlT *)focused->userData;

                    if (ctrl->helpTopic[0]) {
                        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", ctrl->helpTopic);
                        return;
                    }
                }

                // Fall back to form-level HelpTopic
                if (form->helpTopic[0]) {
                    snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", form->helpTopic);
                    return;
                }

                // No topic set -- open help at default
                return;
            }
        }
    }

    // Code editor: look up the word under the cursor
    if (focusWin == sCodeWin && sEditor) {
        char word[128];
        if (wgtTextAreaGetWordAtCursor(sEditor, word, sizeof(word)) > 0) {
            const char *topic = helpLookupKeyword(word);
            if (topic) {
                snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
                return;
            }
        }
        // No keyword match -- open language reference
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.editor");
        return;
    }

    // Immediate window: look up the word under the cursor
    if (focusWin == sImmWin && sImmediate) {
        char word[128];
        if (wgtTextAreaGetWordAtCursor(sImmediate, word, sizeof(word)) > 0) {
            const char *topic = helpLookupKeyword(word);
            if (topic) {
                snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
                return;
            }
        }
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.immediate");
        return;
    }

    // Form designer: help for the selected control type
    if (focusWin == sFormWin && sDesigner.form) {
        if (sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) {
            helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName);
        } else {
            snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.form");
        }
        return;
    }

    // Toolbox: help for the active tool
    if (focusWin == sToolboxWin) {
        if (sDesigner.activeTool[0]) {
            helpSetCtrlTopic(sDesigner.activeTool);
        } else {
            snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.toolbox");
        }
        return;
    }

    // Properties panel
    if (focusWin == sPropsWin) {
        if (sDesigner.form && sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) {
            helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName);
        } else {
            snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.common.props");
        }
        return;
    }

    // Output window
    if (focusWin == sOutWin) {
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.output");
        return;
    }

    // Project window
    if (focusWin == sProjectWin) {
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.project");
        return;
    }

    // Debugger windows
    if (focusWin == sLocalsWin) {
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.locals");
        return;
    }
    if (focusWin == sCallStackWin) {
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.callstack");
        return;
    }
    if (focusWin == sWatchWin) {
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.watch");
        return;
    }
    if (focusWin == sBreakpointWin) {
        snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.debug.breakpoints");
        return;
    }

    // Default: IDE overview
    snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.overview");
}


static void helpSetCtrlTopic(const char *typeName) {
    helpBuildCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic));
}


void ideRenameInCode(const char *oldName, const char *newName) {
    if (!oldName || !newName || strcasecmp(oldName, newName) == 0) {
        return;
    }

    // Rename in the per-procedure buffers (form code currently being edited)
    if (sGeneralBuf) {
        char *replaced = renameInBuffer(sGeneralBuf, oldName, newName);

        if (replaced) {
            free(sGeneralBuf);
            sGeneralBuf = replaced;
        }
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
        if (sProcBufs[i]) {
            char *replaced = renameInBuffer(sProcBufs[i], oldName, newName);

            if (replaced) {
                free(sProcBufs[i]);
                sProcBufs[i] = replaced;
            }
        }
    }

    // Update the editor if it's showing a procedure
    if (sEditor && sCurProcIdx >= -1) {
        if (sCurProcIdx == -1 && sGeneralBuf) {
            wgtSetText(sEditor, sGeneralBuf);
        } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
            wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
        }
    }

    // Update form->code from the renamed buffers (only if editor has this form's code)
    if (sDesigner.form && sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount &&
        sProject.files[sEditorFileIdx].isForm &&
        strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) == 0) {
        free(sDesigner.form->code);
        sDesigner.form->code  = strdup(getFullSource());
        sDesigner.form->dirty = true;
    }

    // Update cached formName if the active file is a form being renamed
    if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) {
        PrjFileT *cur = &sProject.files[sProject.activeFileIdx];

        if (cur->isForm && strcasecmp(cur->formName, oldName) == 0) {
            snprintf(cur->formName, sizeof(cur->formName), "%s", newName);
        }
    }

    // Rename in all project .bas file buffers (and non-active .frm code)
    for (int32_t i = 0; i < sProject.fileCount; i++) {
        // Skip the active file (already handled above)
        if (i == sProject.activeFileIdx) {
            continue;
        }

        char *buf = sProject.files[i].buffer;

        if (!buf) {
            // Load from disk
            char fullPath[DVX_MAX_PATH];
            prjFullPath(&sProject, i, fullPath, sizeof(fullPath));

            int32_t br = 0;
            buf = platformReadFile(fullPath, &br);

            if (!buf) {
                continue;
            }

            if (br <= 0 || br >= IDE_MAX_SOURCE) {
                free(buf);
                buf = NULL;
                continue;
            }

            sProject.files[i].buffer = buf;
        }

        char *replaced = renameInBuffer(buf, oldName, newName);

        if (replaced) {
            free(sProject.files[i].buffer);
            sProject.files[i].buffer   = replaced;
            sProject.files[i].modified = true;
        }
    }

    // Refresh dropdowns to reflect renamed procedures
    updateDropdowns();
}


// immTryAssign -- handle "varName = expr" when paused at a breakpoint.
// Evaluates the RHS, looks up the variable, and writes the value back
// to the running VM.  Returns true if handled as an assignment.

// immParseScalarFromStr -- parse a string into a typed BasValueT based on the
// target slot's current type.  Returns true on success.

static bool immParseScalarFromStr(const char *rhs, const BasValueT *target, BasValueT *outVal) {
    memset(outVal, 0, sizeof(*outVal));

    switch (target->type) {
        case BAS_TYPE_INTEGER:
            outVal->type   = BAS_TYPE_INTEGER;
            outVal->intVal = (int16_t)atoi(rhs);
            return true;

        case BAS_TYPE_LONG:
            outVal->type    = BAS_TYPE_LONG;
            outVal->longVal = (int32_t)atol(rhs);
            return true;

        case BAS_TYPE_SINGLE:
            outVal->type   = BAS_TYPE_SINGLE;
            outVal->sngVal = (float)atof(rhs);
            return true;

        case BAS_TYPE_DOUBLE:
            outVal->type   = BAS_TYPE_DOUBLE;
            outVal->dblVal = atof(rhs);
            return true;

        case BAS_TYPE_BOOLEAN:
            outVal->type = BAS_TYPE_BOOLEAN;

            if (strcasecmp(rhs, "TRUE") == 0 || strcasecmp(rhs, "-1") == 0) {
                outVal->intVal = -1;
            } else {
                outVal->intVal = (atoi(rhs) != 0) ? -1 : 0;
            }

            return true;

        case BAS_TYPE_STRING: {
            const char *s = rhs;
            int32_t sLen  = (int32_t)strlen(s);

            if (sLen >= 2 && s[0] == '"' && s[sLen - 1] == '"') {
                s++;
                sLen -= 2;
            }

            outVal->type   = BAS_TYPE_STRING;
            outVal->strVal = basStringNew(s, sLen);
            return true;
        }

        default:
            return false;
    }
}


// ============================================================
// evaluateImmediate
// ============================================================
//
// Compile and execute a single line from the Immediate window.
// If the line doesn't start with PRINT, wrap it in PRINT so
// expressions produce visible output.

static void immPrintCallback(void *ctx, const char *text, bool newline) {
    (void)ctx;

    if (!sImmediate) {
        return;
    }

    // Append output to the immediate window
    const char *cur = wgtGetText(sImmediate);
    int32_t curLen  = cur ? (int32_t)strlen(cur) : 0;
    int32_t textLen = text ? (int32_t)strlen(text) : 0;

    if (curLen + textLen + 2 < IDE_MAX_IMM) {
        static char immBuf[IDE_MAX_IMM];
        memcpy(immBuf, cur, curLen);
        memcpy(immBuf + curLen, text, textLen);
        curLen += textLen;

        if (newline) {
            immBuf[curLen++] = '\n';
        }

        immBuf[curLen] = '\0';
        wgtSetText(sImmediate, immBuf);

        // Move cursor to end so user can keep typing
        int32_t lines = 1;

        for (int32_t i = 0; i < curLen; i++) {
            if (immBuf[i] == '\n') {
                lines++;
            }
        }

        wgtTextAreaGoToLine(sImmediate, lines);
    }
}


// immResolveLhsSlot -- parse the LHS of an assignment and resolve it to a
// pointer into the running VM's live data.  Handles:
//   varName            -- scalar variable
//   varName(i)         -- array element
//   varName.field      -- UDT field
//   varName(i).field   -- array element UDT field
// Returns NULL if the LHS can't be resolved.  *endPtr is set past the LHS.

static BasValueT *immResolveLhsSlot(const char *lhs, const char **endPtr) {
    const char *p = lhs;

    // Extract variable name
    const char *nameStart = p;

    while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') ||
           (*p >= '0' && *p <= '9') || *p == '_') {
        p++;
    }

    if (p == nameStart) {
        return NULL;
    }

    char varName[128];
    int32_t nameLen = (int32_t)(p - nameStart);

    if (nameLen >= (int32_t)sizeof(varName)) {
        return NULL;
    }

    memcpy(varName, nameStart, nameLen);
    varName[nameLen] = '\0';

    // Look up the base variable
    const BasDebugVarT *dv = findDebugVar(varName);

    if (!dv) {
        return NULL;
    }

    BasValueT *slot = getDebugVarSlot(dv);

    if (!slot) {
        return NULL;
    }

    // Parse optional array subscript: (idx1, idx2, ...)
    if (*p == '(') {
        p++; // skip '('

        if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) {
            return NULL;
        }

        int32_t indices[BAS_ARRAY_MAX_DIMS];
        int32_t numIndices = 0;

        while (*p && *p != ')' && numIndices < BAS_ARRAY_MAX_DIMS) {
            while (*p == ' ') { p++; }
            indices[numIndices++] = atoi(p);

            // Skip past the number
            if (*p == '-') { p++; }

            while (*p >= '0' && *p <= '9') { p++; }

            while (*p == ' ') { p++; }

            if (*p == ',') { p++; }
        }

        if (*p == ')') { p++; }

        int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices);

        if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) {
            return NULL;
        }

        slot = &slot->arrVal->elements[flatIdx];
    }

    // Parse optional UDT field: .fieldName
    if (*p == '.') {
        p++; // skip '.'

        if (slot->type != BAS_TYPE_UDT || !slot->udtVal) {
            return NULL;
        }

        const char *fieldStart = p;

        while ((*p >= 'A' && *p <= 'Z') || (*p >= 'a' && *p <= 'z') ||
               (*p >= '0' && *p <= '9') || *p == '_') {
            p++;
        }

        char fieldName[128];
        int32_t fieldLen = (int32_t)(p - fieldStart);

        if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) {
            return NULL;
        }

        memcpy(fieldName, fieldStart, fieldLen);
        fieldName[fieldLen] = '\0';

        // Find field index from debug UDT definitions
        int32_t fieldIdx = -1;

        for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) {
            if (sDbgModule->debugUdtDefs[t].typeId == slot->udtVal->typeId) {
                for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) {
                    if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) {
                        fieldIdx = f;
                        break;
                    }
                }

                break;
            }
        }

        if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) {
            return NULL;
        }

        slot = &slot->udtVal->fields[fieldIdx];
    }

    if (endPtr) {
        *endPtr = p;
    }

    return slot;
}


static bool immTryAssign(const char *expr) {
    if (sDbgState != DBG_PAUSED || !sVm || !sDbgModule) {
        return false;
    }

    // Skip leading whitespace
    const char *p = dvxSkipWs(expr);

    // Skip optional LET keyword
    if (strncasecmp(p, "LET ", 4) == 0) {
        p = dvxSkipWs(p + 4);
    }

    // Resolve the LHS to a live slot in the VM
    const char *afterLhs = NULL;
    BasValueT *slot = immResolveLhsSlot(p, &afterLhs);

    if (!slot || !afterLhs) {
        return false;
    }

    // Build display name from LHS
    char lhsName[256];
    int32_t lhsLen = (int32_t)(afterLhs - p);

    if (lhsLen >= (int32_t)sizeof(lhsName)) {
        lhsLen = (int32_t)sizeof(lhsName) - 1;
    }

    memcpy(lhsName, p, lhsLen);
    lhsName[lhsLen] = '\0';

    // Skip whitespace after LHS
    p = dvxSkipWs(afterLhs);

    // Must have '=' (but not '==')
    if (*p != '=' || p[1] == '=') {
        return false;
    }

    p = dvxSkipWs(p + 1); // skip '='

    if (*p == '\0') {
        return false;
    }

    // Parse the RHS into a value matching the target slot's type
    BasValueT newVal;

    if (!immParseScalarFromStr(p, slot, &newVal)) {
        immPrintCallback(NULL, "Cannot assign to this variable type", true);
        return true;
    }

    // Write the value directly to the slot
    basValRelease(slot);
    *slot = newVal; // transfer ownership — don't release newVal

    // Show confirmation
    char confirm[256];
    snprintf(confirm, sizeof(confirm), "%s = ", lhsName);
    immPrintCallback(NULL, confirm, false);
    formatValue(slot, confirm, sizeof(confirm));
    immPrintCallback(NULL, confirm, true);

    // Update debug windows to reflect the change
    updateLocalsWindow();
    updateWatchWindow();
    return true;
}


// ============================================================
// basicColorize
// ============================================================
//
// Syntax colorizer callback for BASIC source code. Scans a single
// line and fills the colors array with syntax color indices.

// Hash-based keyword/type lookup using stb_ds.
// Key = uppercase word, value = SYNTAX_KEYWORD or SYNTAX_TYPE.
// Built once on first use, then O(1) per lookup.

static void initSyntaxMap(void) {
    if (sSyntaxMap) {
        return;
    }

    sh_new_arena(sSyntaxMap);

    // Pull the canonical keyword list from the lexer -- one source of truth.
    int32_t n = basLexerKeywordCount();

    for (int32_t i = 0; i < n; i++) {
        const char *kw = basLexerKeywordAt(i);

        // Skip dollar-suffixed aliases (DIR$, CURDIR$, etc.) -- the
        // unsuffixed form is already in the list and the colorizer
        // strips the $ before lookup.
        if (kw[strlen(kw) - 1] == '$') {
            continue;
        }

        BasKeywordClassE cls = basLexerKeywordClass(i);

        if (cls == BAS_KW_CLASS_TYPE || cls == BAS_KW_CLASS_LITERAL) {
            shput(sSyntaxMap, kw, SYNTAX_TYPE);
        } else {
            shput(sSyntaxMap, kw, SYNTAX_KEYWORD);
        }
    }

    // LIBRARY is a contextual keyword in DECLARE LIBRARY and not a
    // real token, but the colorizer still treats it as a keyword.
    shput(sSyntaxMap, "LIBRARY", SYNTAX_KEYWORD);
    shput(sSyntaxMap, "STOP",    SYNTAX_KEYWORD);
}


static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
    (void)ctx;

    // Append prompt to output
    if (prompt && sOutputLen < IDE_MAX_OUTPUT - 1) {
        int32_t n = snprintf(sOutputBuf + sOutputLen, IDE_MAX_OUTPUT - sOutputLen, "%s", prompt);
        sOutputLen += n;
        setOutputText(sOutputBuf);
    }

    return dvxInputBox(sAc, "DVX BASIC", prompt ? prompt : "Enter value:", NULL, buf, bufSize);
}


static bool isCtrlArrayInDesigner(const char *ctrlName) {
    if (!sDesigner.form) {
        return false;
    }

    int32_t count = (int32_t)arrlen(sDesigner.form->controls);

    for (int32_t i = 0; i < count; i++) {
        if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0 && sDesigner.form->controls[i]->index >= 0) {
            return true;
        }
    }

    return false;
}


// ============================================================
// ideRenameInCode -- rename form/control references in all .bas files
// ============================================================
//
// Case-insensitive replacement of OldName followed by '.' or '_' with
// NewName followed by the same delimiter. This handles:
//   ControlName.Property    -> NewName.Property
//   ControlName_Click       -> NewName_Click  (event handlers)
//   FormName.ControlName.X  -> NewFormName.ControlName.X
//   Sub FormName_Load       -> Sub NewFormName_Load
//
// Word-boundary check on the left: the character before the match must
// be a non-identifier character (space, tab, newline, '.', '(', start
// of string) to avoid replacing "Command1" inside "MyCommand1".

static bool isIdentChar(char c) {
    return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
           (c >= '0' && c <= '9') || c == '_';
}


// Check if position i is inside a string literal or comment.
// Scans from the start of the line containing i.
static bool isInStringOrComment(const char *src, int32_t i) {
    // Find start of line
    int32_t lineStart = i;

    while (lineStart > 0 && src[lineStart - 1] != '\n') {
        lineStart--;
    }

    bool inString = false;

    for (int32_t j = lineStart; j < i; j++) {
        if (src[j] == '"') {
            inString = !inString;
        } else if (src[j] == '\'' && !inString) {
            // Rest of line is a comment
            return true;
        }
    }

    return inString;
}


static bool isReplaceEnabled(void) {
    return sReplCheck && wgtCheckboxIsChecked(sReplCheck);
}


static void loadFile(void) {
    FileFilterT filters[] = {
        { "BASIC Files (*.bas)" },
        { "Form Files (*.frm)" },
        { "All Files (*.*)" }
    };

    char path[DVX_MAX_PATH];

    if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, 3, path, sizeof(path))) {
        return;
    }

    const char *ext = strrchr(path, '.');
    bool isForm = (ext && strcasecmp(ext, ".frm") == 0);

    if (sProject.projectPath[0] != '\0') {
        // Add the file to the current project
        const char *fileName = platformPathBaseName(path);

        prjAddFile(&sProject, fileName, isForm);
        prjRebuildTree(&sProject);
        activateFile(sProject.fileCount - 1, ViewAutoE);
    } else {
        // No project -- create one from this file
        if (!promptAndSave()) {
            return;
        }

        ensureProject(path);

        if (!sProjectWin) {
            sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);

            if (sProjectWin) {
                sProjectWin->y       = toolbarBottom() + 25;
                sProjectWin->onClose = onProjectWinClose;
                dvxRaiseWindow(sAc, sProjectWin);
            }
        }
    }

    recentAdd(path);
}


static void loadFormCodeIntoEditor(void) {
    if (!sDesigner.form) {
        return;
    }

    stashFormCode();
    parseProcs(sDesigner.form->code ? sDesigner.form->code : "");
    sEditorFileIdx = sProject.activeFileIdx;

    if (!sCodeWin) {
        showCodeWindow();
    }

    bool saved = sDropdownNavSuppressed;
    sDropdownNavSuppressed = true;
    updateDropdowns();
    sDropdownNavSuppressed = saved;

    showProc(-1);

    if (sEditor && !sEditor->onChange) {
        sEditor->onChange = onEditorChange;
    }
}


// ============================================================
// loadFrmFiles
// ============================================================
//
// Load all .frm files listed in the current project into the
// form runtime for execution.

static void loadFrmFiles(BasFormRtT *rt) {
    for (int32_t i = 0; i < sProject.fileCount; i++) {
        if (!sProject.files[i].isForm) {
            continue;
        }

        char fullPath[DVX_MAX_PATH];
        prjFullPath(&sProject, i, fullPath, sizeof(fullPath));

        int32_t bytesRead = 0;
        char   *frmBuf    = platformReadFile(fullPath, &bytesRead);

        if (!frmBuf) {
            continue;
        }

        if (bytesRead <= 0 || bytesRead >= IDE_MAX_SOURCE) {
            free(frmBuf);
            continue;
        }

        BasFormT *form = basFormRtLoadFrm(rt, frmBuf, bytesRead);
        free(frmBuf);

        // Cache the form object name in the project file entry
        if (form && form->name[0]) {
            snprintf(sProject.files[i].formName, sizeof(sProject.files[i].formName), "%s", form->name);
        }
    }
}


static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText) {
    int32_t iconW     = 0;
    int32_t iconH     = 0;
    int32_t iconPitch = 0;
    uint8_t *data = dvxResLoadIcon(sAc, sCtx->appPath, resName, &iconW, &iconH, &iconPitch);

    if (data) {
        return wgtImageButton(parent, data, iconW, iconH, iconPitch);
    }

    // Fallback to text button if icon not found
    return wgtButton(parent, fallbackText);
}


// ============================================================
// localToConcatLine -- convert editor-local line to concatenated source line
// ============================================================

// Convert an editor-local line number to a full-source line number.
// The editor shows one procedure at a time (sCurProcIdx), so we need
// to add the procedure's starting line offset within the file's code.
// For multi-file projects, we also add the file's offset in the
// concatenated source. For .frm files, an injected BEGINFORM directive
// adds one extra line before the code.
static int32_t localToConcatLine(int32_t editorLine) {
    int32_t fileLine = editorLine;

    // Add the current procedure's start offset within the file's code.
    // sCurProcIdx == -1 means (General) section which starts at line 1
    // of the file's code (not the concatenated source).
    if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
        fileLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1;
    }

    // For projects, add the file's offset in the concatenated source.
    // For .frm files, startLine points to the injected BEGINFORM line,
    // so the actual code starts at startLine + 1.
    if (sProject.sourceMapCount > 0 && sProject.activeFileIdx >= 0) {
        for (int32_t i = 0; i < sProject.sourceMapCount; i++) {
            if (sProject.sourceMap[i].fileIdx == sProject.activeFileIdx) {
                int32_t base = sProject.sourceMap[i].startLine;

                // .frm files have an injected BEGINFORM line before the code
                if (sProject.activeFileIdx < sProject.fileCount &&
                    sProject.files[sProject.activeFileIdx].isForm) {
                    base++;
                }

                return base + fileLine - 1;
            }
        }
    }

    return fileLine;
}


// lookupWatchVar -- evaluate a watch expression
// Supports: varName, varName(idx), varName(idx1, idx2), varName.field
static bool lookupWatchVar(const char *expr, BasValueT *outVal) {
    if (!sVm || !sDbgModule) {
        return false;
    }

    char buf[256];
    snprintf(buf, sizeof(buf), "%s", expr);

    // Split on '.' for UDT field access: "varName.fieldName"
    char *dot       = strchr(buf, '.');
    char *fieldName = NULL;

    if (dot && dot > buf && !strchr(buf, '(')) {
        // Only treat as field access if no subscript before the dot
        *dot      = '\0';
        fieldName = dot + 1;
    }

    // Split on '(' for array subscript: "varName(idx1, idx2, ...)"
    char *paren     = strchr(buf, '(');
    int32_t indices[BAS_ARRAY_MAX_DIMS];
    int32_t numIndices = 0;

    if (paren) {
        char *close = strchr(paren, ')');

        if (close) {
            *close = '\0';
        }

        *paren = '\0';
        char *arg = paren + 1;

        // Parse comma-separated indices
        while (*arg && numIndices < BAS_ARRAY_MAX_DIMS) {
            while (*arg == ' ') { arg++; }
            indices[numIndices++] = atoi(arg);
            char *comma = strchr(arg, ',');

            if (comma) {
                arg = comma + 1;
            } else {
                break;
            }
        }

        // Check for ".field" after the closing paren
        if (close && close[1] == '.') {
            fieldName = close + 2;
        }
    }

    // Look up the variable
    const BasDebugVarT *dv = findDebugVar(buf);

    if (!dv) {
        return false;
    }

    BasValueT val;

    if (!readDebugVar(dv, &val)) {
        return false;
    }

    // Apply array subscript
    if (numIndices > 0) {
        if (val.type != BAS_TYPE_ARRAY || !val.arrVal) {
            return false;
        }

        int32_t flatIdx = basArrayIndex(val.arrVal, indices, numIndices);

        if (flatIdx < 0 || flatIdx >= val.arrVal->totalElements) {
            return false;
        }

        val = val.arrVal->elements[flatIdx];
    }

    // Apply UDT field access
    if (fieldName && fieldName[0]) {
        if (val.type != BAS_TYPE_UDT || !val.udtVal) {
            return false;
        }

        // Find the field by name using debug UDT definitions
        int32_t fieldIdx = -1;

        for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) {
            if (sDbgModule->debugUdtDefs[t].typeId == val.udtVal->typeId) {
                for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) {
                    if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) {
                        fieldIdx = f;
                        break;
                    }
                }

                break;
            }
        }

        if (fieldIdx < 0 || fieldIdx >= val.udtVal->fieldCount) {
            return false;
        }

        val = val.udtVal->fields[fieldIdx];
    }

    *outVal = val;
    return true;
}


// ============================================================
// onMenu
// ============================================================


static void makeExecutable(void) {
    if (sProject.projectPath[0] == '\0') {
        setStatus("Save the project first.");
        return;
    }

    // Compile (always recompile to ensure latest code)
    if (!compileProject()) {
        return;
    }

    // Ask for output path
    FileFilterT filters[] = {
        { "DVX Applications (*.app)" },
        { "All Files (*.*)" }
    };
    char outPath[DVX_MAX_PATH];
    outPath[0] = '\0';

    if (!dvxFileDialog(sAc, "Make Executable", FD_SAVE, NULL, filters, 2, outPath, sizeof(outPath))) {
        return;
    }

    // Ask debug or release
    const char *modeItems[] = { "Debug (include error info)" };
    int32_t modeChoice = 0;

    if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, 2, 0, &modeChoice)) {
        return;
    }

    bool release = (modeChoice == 1);

    setStatus(release ? "Building release executable..." : "Building debug executable...");
    dvxSetBusy(sAc, true);
    dvxUpdate(sAc);

    // Make a copy of the module for potential stripping
    int32_t modLen = 0;
    uint8_t *modData = basModuleSerialize(sCachedModule, &modLen);

    if (!modData) {
        setStatus("Failed to serialize module.");
        dvxSetBusy(sAc, false);
        return;
    }

    // For release, deserialize, strip, re-serialize
    if (release) {
        BasModuleT *modCopy = basModuleDeserialize(modData, modLen);
        free(modData);

        if (!modCopy) {
            setStatus("Failed to prepare release module.");
            dvxSetBusy(sAc, false);
            return;
        }

        basStripModule(modCopy);
        modData = basModuleSerialize(modCopy, &modLen);
        basModuleFree(modCopy);

        if (!modData) {
            setStatus("Failed to serialize stripped module.");
            dvxSetBusy(sAc, false);
            return;
        }
    }

    // Serialize debug info from the original (unstripped) module
    int32_t  dbgLen  = 0;
    uint8_t *dbgData = NULL;

    if (!release) {
        dbgData = basDebugSerialize(sCachedModule, &dbgLen);
    }

    // Extract stub from our own resources
    DvxResHandleT *selfRes = dvxResOpen(sCtx->appPath);

    if (!selfRes) {
        setStatus("Cannot read IDE resources.");
        free(modData);
        free(dbgData);
        dvxSetBusy(sAc, false);
        return;
    }

    uint32_t stubSize = 0;
    void *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubSize);
    dvxResClose(selfRes);

    if (!stubData || stubSize == 0) {
        setStatus("Stub not found in IDE resources.");
        free(modData);
        free(dbgData);
        dvxSetBusy(sAc, false);
        return;
    }

    // Write stub to output file
    FILE *outFile = fopen(outPath, "wb");

    if (!outFile) {
        setStatus("Cannot create output file.");
        free(stubData);
        free(modData);
        free(dbgData);
        dvxSetBusy(sAc, false);
        return;
    }

    fwrite(stubData, 1, stubSize, outFile);
    fclose(outFile);
    free(stubData);

    // Build parallel form arrays: load each form, strip comments.
    // The loop preserves the prior quirk where a malloc failure skips the
    // resource but still advances the index -- formData[i] is left NULL
    // in that case and basBuildEmitResources drops it.
    int32_t  frmCapacity = 0;

    for (int32_t i = 0; i < sProject.fileCount; i++) {
        if (sProject.files[i].isForm) {
            frmCapacity++;
        }
    }

    uint8_t **frmData = NULL;
    int32_t  *frmLens = NULL;
    int32_t   frmCount = 0;

    if (frmCapacity > 0) {
        frmData = (uint8_t **)calloc((size_t)frmCapacity, sizeof(uint8_t *));
        frmLens = (int32_t  *)calloc((size_t)frmCapacity, sizeof(int32_t));
    }

    for (int32_t i = 0; i < sProject.fileCount; i++) {
        if (!sProject.files[i].isForm) {
            continue;
        }

        char *frmSrc = NULL;

        if (sProject.files[i].buffer) {
            frmSrc = strdup(sProject.files[i].buffer);
        } else {
            char fp[DVX_MAX_PATH];
            prjFullPath(&sProject, i, fp, sizeof(fp));
            frmSrc = platformReadFile(fp, NULL);
        }

        if (!frmSrc) {
            continue;
        }

        int32_t frmLen = (int32_t)strlen(frmSrc);

        // Strip comments and leading whitespace from embedded form text.
        int32_t  stripCap = frmLen + 16;
        uint8_t *stripped = (uint8_t *)malloc(stripCap);

        if (stripped) {
            int32_t strippedLen = basStripFrmComments(frmSrc, frmLen, stripped, stripCap);

            if (frmData && frmLens) {
                frmData[frmCount] = stripped;
                frmLens[frmCount] = strippedLen;
            } else {
                free(stripped);
            }
        }

        free(frmSrc);
        frmCount++;
    }

    // Resolve icon bytes once -- if the project has no icon path, fall back
    // to the IDE's embedded "noicon" so compiled apps always have something.
    char    *iconDiskPath = NULL;
    char     iconFullPath[DVX_MAX_PATH];
    void    *iconFallback = NULL;
    uint32_t iconFallbackSize = 0;
    DvxResHandleT *ideRes = NULL;

    if (sProject.iconPath[0]) {
        snprintf(iconFullPath, sizeof(iconFullPath), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.iconPath);
        iconDiskPath = iconFullPath;
    } else {
        ideRes = dvxResOpen(sCtx->appPath);

        if (ideRes) {
            iconFallback = dvxResRead(ideRes, "noicon", &iconFallbackSize);
        }
    }

    // Hand everything to the shared emitter.
    BasBuildSpecT spec;
    memset(&spec, 0, sizeof(spec));
    spec.projName    = sProject.name;
    spec.author      = sProject.author;
    spec.publisher   = sProject.publisher;
    spec.version     = sProject.version;
    spec.copyright   = sProject.copyright;
    spec.description = sProject.description;
    spec.helpFile    = sProject.helpFile;
    spec.iconPath    = iconDiskPath;
    spec.iconData    = iconFallback;
    spec.iconSize    = (int32_t)iconFallbackSize;
    spec.moduleData  = modData;
    spec.moduleLen   = modLen;
    spec.debugData   = dbgData;
    spec.debugLen    = dbgLen;
    spec.formCount   = frmCount;
    spec.formData    = (const uint8_t *const *)frmData;
    spec.formLens    = frmLens;

    basBuildEmitResources(outPath, &spec);

    free(modData);
    free(dbgData);
    free(iconFallback);

    if (ideRes) {
        dvxResClose(ideRes);
    }

    for (int32_t i = 0; i < frmCount; i++) {
        if (frmData) {
            free(frmData[i]);
        }
    }

    free(frmData);
    free(frmLens);

    // Copy help file alongside the output app (the resource entry itself was
    // already written by basBuildEmitResources).
    if (sProject.helpFile[0]) {
        const char *helpBase = platformPathBaseName(sProject.helpFile);

        char helpSrc[DVX_MAX_PATH];
        snprintf(helpSrc, sizeof(helpSrc), "%s" DVX_PATH_SEP "%s", sProject.projectDir, sProject.helpFile);

        char outDir[DVX_MAX_PATH];
        snprintf(outDir, sizeof(outDir), "%s", outPath);
        char *lastSep = platformPathDirEnd(outDir);

        if (lastSep) {
            *lastSep = '\0';
        }

        char helpDst[DVX_MAX_PATH];
        snprintf(helpDst, sizeof(helpDst), "%s" DVX_PATH_SEP "%s", outDir, helpBase);

        FILE *hSrc = fopen(helpSrc, "rb");

        if (hSrc) {
            FILE *hDst = fopen(helpDst, "wb");

            if (hDst) {
                char cpBuf[4096];
                size_t n;

                while ((n = fread(cpBuf, 1, sizeof(cpBuf), hSrc)) > 0) {
                    fwrite(cpBuf, 1, n, hDst);
                }

                fclose(hDst);
            }

            fclose(hSrc);
        }
    }

    dvxSetBusy(sAc, false);

    char msg[512];
    snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug");
    setStatus(msg);
}


static void navigateToBreakpoint(int32_t bpIdx) {
    if (bpIdx < 0 || bpIdx >= sBreakpointCount) {
        return;
    }

    IdeBreakpointT *bp = &sBreakpoints[bpIdx];
    const char *procName = (bp->procIdx >= 0 && bp->procName[0]) ? bp->procName : NULL;
    navigateToCodeLine(bp->fileIdx, bp->codeLine, procName, false);
}


// ============================================================
// navigateToCodeLine -- show a specific file/line/proc in the code editor
// ============================================================
//
// fileIdx:   project file index (-1 = current)
// codeLine:  1-based line within the file's code section
// procName:  "Obj.Evt" (dot-separated) to match editor proc table, or NULL for General
// setDbgLine: if true, update sDbgCurrentLine for the debug decorator

static void navigateToCodeLine(int32_t fileIdx, int32_t codeLine, const char *procName, bool setDbgLine) {
    // Track whether the file changed so we can force showProc
    bool fileChanged = (fileIdx >= 0 && fileIdx != sProject.activeFileIdx);

    // Switch to the correct file if needed
    if (fileChanged) {
        activateFile(fileIdx, ViewCodeE);
    }

    // Ensure code window exists
    if (!sCodeWin) {
        showCodeWindow();
        updateDropdowns();
        sCurProcIdx = -2;
    }

    if (sCodeWin && !sCodeWin->visible) {
        dvxShowWindow(sAc, sCodeWin);
    }

    if (sCodeWin) {
        dvxRaiseWindow(sAc, sCodeWin);
    }

    // Find the target procedure in the editor's proc table
    int32_t targetProcIdx = -1;
    int32_t procCount     = (int32_t)arrlen(sProcTable);

    if (procName) {
        for (int32_t i = 0; i < procCount; i++) {
            char fullName[128];
            snprintf(fullName, sizeof(fullName), "%s.%s",
                     sProcTable[i].objName, sProcTable[i].evtName);

            if (strcasecmp(fullName, procName) == 0) {
                targetProcIdx = i;
                break;
            }
        }
    }

    // Switch to the target procedure (always force after a file change)
    if (targetProcIdx != sCurProcIdx || fileChanged) {
        showProc(targetProcIdx);
    }

    // Update dropdowns to match
    if (targetProcIdx >= 0 && targetProcIdx < procCount) {
        selectDropdowns(sProcTable[targetProcIdx].objName,
                        sProcTable[targetProcIdx].evtName);
    } else if (sObjDropdown) {
        wgtDropdownSetSelected(sObjDropdown, 0);
    }

    // Compute editor-local line within the current proc
    int32_t editorLine = codeLine;

    if (sCurProcIdx >= 0 && sCurProcIdx < procCount) {
        editorLine = codeLine - sProcTable[sCurProcIdx].lineNum + 1;
    }

    if (setDbgLine) {
        sDbgCurrentLine = editorLine;
    }

    if (sEditor) {
        wgtTextAreaGoToLine(sEditor, editorLine);
        wgtInvalidatePaint(sEditor);
    }
}


static void navigateToEventSub(void) {
    if (!sDesigner.form) {
        return;
    }

    // Determine control name and default event
    const char *ctrlName  = NULL;
    const char *eventName = NULL;

    if (sDesigner.selectedIdx >= 0 &&
        sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) {
        DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx];
        ctrlName  = ctrl->name;
        eventName = dsgnDefaultEvent(ctrl->typeName);
    } else {
        ctrlName  = sDesigner.form->name;
        eventName = dsgnDefaultEvent("Form");
    }

    navigateToNamedEventSub(ctrlName, eventName);
}


// navigateToEventSub -- open code editor at the default event sub for the
// selected control (or form). Creates the sub skeleton if it doesn't exist.
// Code is stored in the .frm file's code section (sDesigner.form->code).

static void navigateToNamedEventSub(const char *ctrlName, const char *eventName) {
    if (!sDesigner.form || !ctrlName || !eventName) {
        return;
    }

    char subName[128];
    snprintf(subName, sizeof(subName), "%s_%s", ctrlName, eventName);

    // Load form code into editor (stashes existing code, parses procs,
    // populates dropdowns without triggering navigation)
    loadFormCodeIntoEditor();

    if (!sEditor) {
        return;
    }

    // Search for existing procedure
    int32_t procCount = (int32_t)arrlen(sProcTable);

    for (int32_t i = 0; i < procCount; i++) {
        char fullName[128];
        snprintf(fullName, sizeof(fullName), "%s_%s", sProcTable[i].objName, sProcTable[i].evtName);

        if (strcasecmp(fullName, subName) == 0) {
            stashDesignerState();
            showProc(i);
            if (sEditor && !sEditor->onChange) {
                sEditor->onChange = onEditorChange;
            }
            selectDropdowns(ctrlName, eventName);
            return;
        }
    }

    // Not found -- create a new sub skeleton for editing.
    // Don't mark dirty yet; saveCurProc will discard it if the
    // user doesn't add any code.
    char skeleton[512];

    if (isCtrlArrayInDesigner(ctrlName)) {
        snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(eventName));
    } else {
        snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
    }

    arrput(sProcBufs, strdup(skeleton));

    // Show the new procedure (it's the last one)
    stashDesignerState();
    showProc((int32_t)arrlen(sProcBufs) - 1);
    if (sEditor && !sEditor->onChange) {
        sEditor->onChange = onEditorChange;
    }
    selectDropdowns(ctrlName, eventName);
}


static void newProject(void) {
    char name[PRJ_MAX_NAME];

    if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) {
        return;
    }

    if (name[0] == '\0') {
        return;
    }

    // Ask for directory via save dialog (file = name.dbp)
    FileFilterT filters[] = {
        { "Project Files (*.dbp)" }
    };

    char dbpPath[DVX_MAX_PATH];
    snprintf(dbpPath, sizeof(dbpPath), "%s.dbp", name);

    if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) {
        return;
    }

    closeProject();

    // Derive directory from chosen path
    char dir[DVX_MAX_PATH];
    snprintf(dir, sizeof(dir), "%s", dbpPath);
    char *sep = platformPathDirEnd(dir);

    if (sep) {
        *sep = '\0';
    } else {
        dir[0] = '.';
        dir[1] = '\0';
    }

    prjNew(&sProject, name, dir, sPrefs);
    snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath);
    prjSave(&sProject);
    sProject.dirty = false;

    // Create and show project window
    if (!sProjectWin) {
        sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);

        if (sProjectWin) {
            sProjectWin->y          = toolbarBottom() + 25;
            sProjectWin->onClose    = onProjectWinClose;
            sProjectWin->onMenu     = onMenu;
            sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
        }
    } else {
        prjRebuildTree(&sProject);
    }

    char title[300];
    snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
    dvxSetTitle(sAc, sWin, title);

    setStatus("New project created.");
    updateProjectMenuState();
}


static void onBreakpointHit(void *ctx, int32_t line) {
    (void)ctx;
    sDbgState = DBG_PAUSED;

    if (sVm) {
        sVm->debugPaused = true;
    }

    if (line > 0) {
        sDbgCurrentLine = line;
        debugNavigateToLine(line);
    }

    debugSetBreakTitles(true);
    debugUpdateWindows();
    updateProjectMenuState();
    setStatus("Paused.");
}


static void onBreakpointListDblClick(WidgetT *w) {
    (void)w;

    if (!sBreakpointList) {
        return;
    }

    int32_t sel = wgtListViewGetSelected(sBreakpointList);
    navigateToBreakpoint(sel);
}


static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
    (void)w;
    (void)shift;

    // Delete key
    if (keyCode != (0x53 | 0x100) && keyCode != 127) {
        return;
    }

    if (!sBreakpointList || sBreakpointCount == 0) {
        return;
    }

    // Remove selected breakpoints in reverse order to preserve indices
    for (int32_t i = sBreakpointCount - 1; i >= 0; i--) {
        if (wgtListViewIsItemSelected(sBreakpointList, i)) {
            arrdel(sBreakpoints, i);
        }
    }

    sBreakpointCount = (int32_t)arrlen(sBreakpoints);
    updateBreakpointWindow();

    // Repaint editor to remove breakpoint dots
    if (sEditor) {
        wgtInvalidatePaint(sEditor);
    }
}


// ============================================================
// showBreakpointWindow
// ============================================================

// Dynamic per-refresh display buffers (stb_ds). Strings are strdup'd and freed
// on each refresh.
static char        **sBpFiles = NULL;
static char        **sBpProcs = NULL;
static char        **sBpLines = NULL;
static const char  **sBpCells = NULL;

static char        **sLocalsNames  = NULL;
static char        **sLocalsTypes  = NULL;
static char        **sLocalsValues = NULL;
static const char  **sLocalsCells  = NULL;

static char        **sCallNames = NULL;
static char        **sCallLines = NULL;
static const char  **sCallCells = NULL;

static char        **sWatchExprBuf = NULL;
static char        **sWatchValBuf  = NULL;
static const char  **sWatchCells   = NULL;

static void onBreakpointWinClose(WindowT *win) {
    dvxHideWindow(sAc, win);
}


static void onCallStackClose(WindowT *win) {
    dvxHideWindow(sAc, win);
}


static void onClose(WindowT *win) {
    if (!promptAndSave()) {
        return;
    }

    // Stop any running program
    sStopRequested = true;

    if (sVm) {
        sVm->running     = false;
        sVm->debugPaused = false;
    }

    sDbgState       = DBG_IDLE;
    sDbgCurrentLine = -1;
    sDbgEnabled     = false;

    // Prevent stale focus tracking during shutdown
    sLastFocusWin = NULL;

    // Null widget pointers first so nothing references destroyed widgets
    sEditor      = NULL;
    sOutput      = NULL;
    sImmediate   = NULL;
    sObjDropdown = NULL;
    sEvtDropdown = NULL;
    sStatus      = NULL;
    sToolbar     = NULL;
    sStatusBar   = NULL;

    // Close all child windows
    // Close all child windows
    if (sCodeWin && sCodeWin != win) {
        dvxDestroyWindow(sAc, sCodeWin);
    }

    sCodeWin = NULL;

    if (sOutWin && sOutWin != win) {
        dvxDestroyWindow(sAc, sOutWin);
    }

    sOutWin = NULL;

    if (sImmWin && sImmWin != win) {
        dvxDestroyWindow(sAc, sImmWin);
    }

    sImmWin = NULL;

    if (sLocalsWin && sLocalsWin != win) {
        dvxDestroyWindow(sAc, sLocalsWin);
    }

    sLocalsWin  = NULL;
    sLocalsList = NULL;

    if (sCallStackWin && sCallStackWin != win) {
        dvxDestroyWindow(sAc, sCallStackWin);
    }

    sCallStackWin  = NULL;
    sCallStackList = NULL;

    if (sWatchWin && sWatchWin != win) {
        dvxDestroyWindow(sAc, sWatchWin);
    }

    sWatchWin   = NULL;
    sWatchList  = NULL;
    sWatchInput = NULL;

    if (sBreakpointWin && sBreakpointWin != win) {
        dvxDestroyWindow(sAc, sBreakpointWin);
    }

    sBreakpointWin  = NULL;
    sBreakpointList = NULL;

    if (sFormWin) {
        dvxDestroyWindow(sAc, sFormWin);
        cleanupFormWin();
    }

    if (sToolboxWin) {
        tbxDestroy(sAc, sToolboxWin);
        sToolboxWin = NULL;
    }

    if (sPropsWin) {
        prpDestroy(sAc, sPropsWin);
        sPropsWin = NULL;
    }

    if (sProjectWin) {
        prjDestroyWindow(sAc, sProjectWin);
        sProjectWin = NULL;
    }

    closeProject();

    // Don't destroy win here -- the shell manages it. Destroying
    // it from inside onClose crashes because the calling code in
    // dvxApp.c still references the window after the callback returns.
    sWin = NULL;

    if (sCachedModule) {
        basModuleFree(sCachedModule);
        sCachedModule = NULL;
    }

    dsgnFree(&sDesigner);

    freeProcBufs();

    arrfree(sProcTable);

    for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) {
        free((void *)sObjItems[i]);
    }

    arrfree(sObjItems);
    arrfree(sEvtItems);
    sProcTable = NULL;
    sObjItems  = NULL;
    sEvtItems  = NULL;

    // Free dynamic strdup'd cell buffers for debug windows
    for (int32_t i = 0; i < (int32_t)arrlen(sBpFiles); i++) {
        free(sBpFiles[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sBpProcs); i++) {
        free(sBpProcs[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sBpLines); i++) {
        free(sBpLines[i]);
    }

    arrfree(sBpFiles);
    arrfree(sBpProcs);
    arrfree(sBpLines);
    arrfree(sBpCells);

    for (int32_t i = 0; i < (int32_t)arrlen(sLocalsNames); i++) {
        free(sLocalsNames[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sLocalsTypes); i++) {
        free(sLocalsTypes[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sLocalsValues); i++) {
        free(sLocalsValues[i]);
    }

    arrfree(sLocalsNames);
    arrfree(sLocalsTypes);
    arrfree(sLocalsValues);
    arrfree(sLocalsCells);

    for (int32_t i = 0; i < (int32_t)arrlen(sCallNames); i++) {
        free(sCallNames[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sCallLines); i++) {
        free(sCallLines[i]);
    }

    arrfree(sCallNames);
    arrfree(sCallLines);
    arrfree(sCallCells);

    for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprs); i++) {
        free(sWatchExprs[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprBuf); i++) {
        free(sWatchExprBuf[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sWatchValBuf); i++) {
        free(sWatchValBuf[i]);
    }

    arrfree(sWatchExprs);
    arrfree(sWatchExprBuf);
    arrfree(sWatchValBuf);
    arrfree(sWatchCells);

    for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
        free(sRecentFiles[i]);
    }

    arrfree(sRecentFiles);

    dvxDestroyWindow(sAc, win);
}


static void onCodeWinClose(WindowT *win) {
    // Stash code back before the window is destroyed.
    stashFormCode();

    dvxDestroyWindow(sAc, win);
    sCodeWin     = NULL;
    sEditor      = NULL;
    sObjDropdown = NULL;
    sEvtDropdown = NULL;

    if (sLastFocusWin == win) {
        sLastFocusWin = NULL;
    }

    updateProjectMenuState();
}


static void onColorListChange(WidgetT *w) {
    (void)w;
    prefsUpdateColorSliders();
}


static void onColorSliderChange(WidgetT *w) {
    (void)w;

    int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);

    if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) {
        return;
    }

    uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR);
    uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
    uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);

    static char rBuf[8];
    static char gBuf[8];
    static char bBuf[8];
    snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
    snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
    snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
    wgtSetText(sPrefsDlg.lblR, rBuf);
    wgtSetText(sPrefsDlg.lblG, gBuf);
    wgtSetText(sPrefsDlg.lblB, bBuf);

    sPrefsDlg.syntaxColors[idx] = ((uint32_t)r << 16) | ((uint32_t)g << 8) | (uint32_t)b;
    prefsUpdateSwatch();
}


static void onContentFocus(WindowT *win) {
    sLastFocusWin = win;
}


static void onEditorChange(WidgetT *w) {
    (void)w;

    // Adjust breakpoints when lines are added or removed
    if (sEditor && sBreakpointCount > 0) {
        const char *text = wgtGetText(sEditor);
        int32_t newLineCount = countLines(text);
        int32_t delta = newLineCount - sEditorLineCount;

        if (delta != 0) {
            int32_t fileIdx    = sProject.activeFileIdx;
            int32_t cursorLine = wgtTextAreaGetCursorLine(sEditor);

            // Convert editor cursor line to file code line
            int32_t editCodeLine = cursorLine;

            if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
                editCodeLine = sProcTable[sCurProcIdx].lineNum + cursorLine - 1;
            }

            bool changed = false;

            for (int32_t i = sBreakpointCount - 1; i >= 0; i--) {
                if (sBreakpoints[i].fileIdx != fileIdx) {
                    continue;
                }

                if (sBreakpoints[i].codeLine >= editCodeLine) {
                    sBreakpoints[i].codeLine += delta;

                    // Remove if shifted to invalid line
                    if (sBreakpoints[i].codeLine < 1) {
                        arrdel(sBreakpoints, i);
                        sBreakpointCount = (int32_t)arrlen(sBreakpoints);
                    }

                    changed = true;
                }
            }

            if (changed) {
                updateBreakpointWindow();
            }

            sEditorLineCount = newLineCount;
        }
    }

    // Mark the active file as modified
    if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < sProject.fileCount) {
        sProject.files[sProject.activeFileIdx].modified = true;

        // Only mark form dirty when editing the form's code, not a .bas file
        if (sProject.files[sProject.activeFileIdx].isForm && sDesigner.form) {
            sDesigner.form->dirty = true;
        }
    }

    updateDirtyIndicators();
}


// ============================================================
// onEvtDropdownChange
// ============================================================
//
// Navigate to the selected procedure when the event dropdown changes.

static void onEvtDropdownChange(WidgetT *w) {
    (void)w;

    if (sDropdownNavSuppressed) {
        return;
    }

    if (!sObjDropdown || !sEvtDropdown || !sEditor) {
        return;
    }

    int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
    int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown);

    if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) ||
        evtIdx < 0 || evtIdx >= (int32_t)arrlen(sEvtItems)) {
        return;
    }

    const char *selObj = sObjItems[objIdx];
    const char *selEvt = sEvtItems[evtIdx];

    // (Global) shows the General module-level section
    if (strcasecmp(selEvt, "(Global)") == 0) {
        showProc(-1);
        return;
    }

    // Strip brackets if present (unimplemented event)
    char evtName[64];
    if (selEvt[0] == '[') {
        snprintf(evtName, sizeof(evtName), "%s", selEvt + 1);
        int32_t len = (int32_t)strlen(evtName);

        if (len > 0 && evtName[len - 1] == ']') {
            evtName[len - 1] = '\0';
        }
    } else {
        snprintf(evtName, sizeof(evtName), "%s", selEvt);
    }

    // Search for an existing proc matching object + event
    int32_t procCount = (int32_t)arrlen(sProcTable);

    for (int32_t i = 0; i < procCount; i++) {
        if (strcasecmp(sProcTable[i].objName, selObj) == 0 &&
            strcasecmp(sProcTable[i].evtName, evtName) == 0) {
            showProc(i);
            return;
        }
    }

    // Not found -- create a new sub skeleton for editing.
    // Don't mark dirty yet; saveCurProc will discard it if the
    // user doesn't add any code.
    char subName[128];
    snprintf(subName, sizeof(subName), "%s_%s", selObj, evtName);

    char skeleton[512];

    if (isCtrlArrayInDesigner(selObj)) {
        snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s)\n\nEnd Sub\n", subName, getEventExtraParams(evtName));
    } else {
        snprintf(skeleton, sizeof(skeleton), "Sub %s ()\n\nEnd Sub\n", subName);
    }

    arrput(sProcBufs, strdup(skeleton));
    showProc((int32_t)arrlen(sProcBufs) - 1);
}


static void onFindClose(WindowT *win) {
    (void)win;
    closeFindDialog();
}


static void onFindCloseBtn(WidgetT *w) {
    (void)w;
    closeFindDialog();
}


static void onFindNext(WidgetT *w) {
    (void)w;

    if (!sFindInput) {
        return;
    }

    const char *needle = wgtGetText(sFindInput);

    if (!needle || !needle[0]) {
        return;
    }

    snprintf(sFindText, sizeof(sFindText), "%s", needle);

    FindScopeE scope = getFindScope();
    bool caseSens    = getFindMatchCase();
    bool forward     = getFindForward();

    if (scope == ScopeFuncE && sEditor) {
        // Search current procedure only
        if (!wgtTextAreaFindNext(sEditor, sFindText, caseSens, forward)) {
            setStatus("Not found.");
        }
    } else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) {
        if (!findInProject(sFindText, caseSens)) {
            setStatus("Not found.");
        }
    }
}


// onFormWinClose -- shell callback when user clicks X on the form window.
static void onFormWinClose(WindowT *win) {
    dvxDestroyWindow(sAc, win);
    cleanupFormWin();
}


static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) {
    (void)win;

    if (!sDesigner.form) {
        return 0;
    }

    // Crosshair when placing a new control
    if (sDesigner.activeTool[0] != '\0') {
        return CURSOR_CROSSHAIR;
    }

    if (!sDesigner.form->controls) {
        return 0;
    }

    int32_t count = (int32_t)arrlen(sDesigner.form->controls);

    if (sDesigner.selectedIdx < 0 || sDesigner.selectedIdx >= count) {
        return 0;
    }

    DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx];

    if (!ctrl->widget || !ctrl->widget->visible || ctrl->widget->w <= 0 || ctrl->widget->h <= 0) {
        return 0;
    }

    int32_t cx = ctrl->widget->x;
    int32_t cy = ctrl->widget->y;
    int32_t cw = ctrl->widget->w;
    int32_t ch = ctrl->widget->h;
    int32_t hs = DSGN_HANDLE_SIZE;

    // SE handle (check first -- overlaps with E and S)
    if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
        return CURSOR_RESIZE_DIAG_NWSE;
    }

    // E handle (right edge center)
    if (x >= cx + cw - hs/2 && x < cx + cw + hs/2 && y >= cy + ch/2 - hs/2 && y < cy + ch/2 + hs/2) {
        return CURSOR_RESIZE_H;
    }

    // S handle (bottom center)
    if (x >= cx + cw/2 - hs/2 && x < cx + cw/2 + hs/2 && y >= cy + ch - hs/2 && y < cy + ch + hs/2) {
        return CURSOR_RESIZE_V;
    }

    return 0;
}


static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
    // Ctrl+C: copy selected control
    if (key == 3 && (mod & ACCEL_CTRL)) {
        dsgnCopySelected();
        return;
    }

    // Ctrl+X: cut selected control
    if (key == 24 && (mod & ACCEL_CTRL)) {
        dsgnCopySelected();

        if (sDesigner.selectedIdx >= 0) {
            dsgnOnKey(&sDesigner, KEY_DELETE);
            prpRebuildTree(&sDesigner);
            prpRefresh(&sDesigner);

            if (sFormWin) {
                dvxInvalidateWindow(sAc, sFormWin);
            }
        }

        return;
    }

    // Ctrl+V: paste control
    if (key == 22 && (mod & ACCEL_CTRL)) {
        dsgnPasteControl();
        return;
    }

    if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) {
        int32_t prevCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;
        dsgnOnKey(&sDesigner, KEY_DELETE);
        int32_t newCount = sDesigner.form ? (int32_t)arrlen(sDesigner.form->controls) : 0;

        if (newCount != prevCount) {
            prpRebuildTree(&sDesigner);
            prpRefresh(&sDesigner);

            if (sFormWin) {
                dvxInvalidateWindow(sAc, sFormWin);
            }
        }

        return;
    }

    // Forward unhandled keys to the widget system
    widgetOnKey(win, key, mod);
}


// onFormWinMenu -- menu click on the designer form preview.
// Designer menu items use IDs starting at DSGN_MENU_ID_BASE.
// Clicking one navigates to the menu item's Click event code.
// Other IDs (from shared accel table) fall through to onMenu.
static void onFormWinMenu(WindowT *win, int32_t menuId) {
    if (sDesigner.form && menuId >= DSGN_MENU_ID_BASE) {
        int32_t idx = menuId - DSGN_MENU_ID_BASE;
        int32_t menuCount = (int32_t)arrlen(sDesigner.form->menuItems);

        if (idx >= 0 && idx < menuCount) {
            const char *name = sDesigner.form->menuItems[idx].name;

            if (name[0]) {
                navigateToNamedEventSub(name, "Click");
                return;
            }
        }
    }

    onMenu(win, menuId);
}


static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
    (void)win;
    static int32_t lastButtons = 0;

    bool wasDown = (lastButtons & MOUSE_LEFT) != 0;
    bool isDown  = (buttons & MOUSE_LEFT) != 0;

    if (!sDesigner.form || !sFormWin) {
        lastButtons = buttons;
        return;
    }

    if (isDown && !wasDown) {
        // Detect double-click using the system-wide setting
        int32_t clicks = multiClickDetect(x, y);

        int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
        bool wasDirty     = sDesigner.form->dirty;
        dsgnOnMouse(&sDesigner, x, y, false);
        int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
        bool nowDirty    = sDesigner.form->dirty;

        if (newCount != prevCount || (nowDirty && !wasDirty)) {
            prpRebuildTree(&sDesigner);
        }

        prpRefresh(&sDesigner);

        if (sFormWin) {
            dvxInvalidateWindow(sAc, sFormWin);
        }

        if (clicks >= 2 && sDesigner.activeTool[0] == '\0') {
            navigateToEventSub();
        }
    } else if (isDown && wasDown) {
        // Drag
        dsgnOnMouse(&sDesigner, x, y, true);
        prpRefresh(&sDesigner);

        if (sFormWin) {
            dvxInvalidateWindow(sAc, sFormWin);
        }
    } else if (!isDown && wasDown) {
        // Release
        dsgnOnMouse(&sDesigner, x, y, false);
        prpRefresh(&sDesigner);

        if (sFormWin) {
            dvxInvalidateWindow(sAc, sFormWin);
        }
    }

    lastButtons = buttons;

    updateDirtyIndicators();
}


// ============================================================
// onFormWinPaint
// ============================================================
//
// Draw selection handles after widgets have painted.

static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
    if (!win) {
        return;
    }

    // Force measure + relayout only on structural changes (control
    // add/remove/resize). Selection clicks just need repaint + overlay.
    if (win->paintNeeded >= PAINT_FULL && win->widgetRoot) {
        widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
        win->widgetRoot->w = 0; // force layout pass
    }

    // Designer always needs full repaint (handles must be erased/redrawn)
    win->paintNeeded = PAINT_FULL;
    widgetOnPaint(win, dirtyArea);

    // Then draw selection handles on top
    int32_t winX = win->contentX;
    int32_t winY = win->contentY;

    dsgnPaintOverlay(&sDesigner, winX, winY);
}


// onFormWinResize -- update form dimensions when the design window is resized
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) {
    // Let the widget system handle the layout recalculation
    widgetOnResize(win, newW, newH);

    if (sDesigner.form) {
        // 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);
    }
}


static void onGutterClick(WidgetT *w, int32_t lineNum) {
    (void)w;
    toggleBreakpointLine(lineNum);
}


// ============================================================
// onImmediateChange
// ============================================================
//
// Detect Enter in the Immediate window and evaluate the last line.

static void onImmediateChange(WidgetT *w) {
    (void)w;

    if (!sImmediate) {
        return;
    }

    const char *text = wgtGetText(sImmediate);

    if (!text) {
        return;
    }

    int32_t len = (int32_t)strlen(text);

    if (len < 2 || text[len - 1] != '\n') {
        return;
    }

    // Find the start of the line before the trailing newline
    int32_t lineEnd = len - 1;
    int32_t lineStart = lineEnd - 1;

    while (lineStart > 0 && text[lineStart - 1] != '\n') {
        lineStart--;
    }

    if (lineStart >= lineEnd) {
        return;
    }

    // Extract the line
    char expr[512];
    int32_t lineLen = lineEnd - lineStart;

    if (lineLen >= (int32_t)sizeof(expr)) {
        lineLen = (int32_t)sizeof(expr) - 1;
    }

    memcpy(expr, text + lineStart, lineLen);
    expr[lineLen] = '\0';

    evaluateImmediate(expr);
}


static void onLocalsClose(WindowT *win) {
    dvxHideWindow(sAc, win);
}


static void onMenu(WindowT *win, int32_t menuId) {
    (void)win;

    handleFileCmd(menuId);
    handleEditCmd(menuId);
    handleRunCmd(menuId);
    handleViewCmd(menuId);
    handleWindowCmd(menuId);
    handleProjectCmd(menuId);

    if (menuId == CMD_PREFERENCES) {
        showPreferencesDialog();
    }

    if (menuId == CMD_DEBUG_LAYOUT && sWin && sWin->menuBar) {
        wgtSetDebugLayout(sAc, wmMenuItemIsChecked(sWin->menuBar, CMD_DEBUG_LAYOUT));
    }

    if (menuId == CMD_HELP_CONTENTS) {
        char hlpPath[DVX_MAX_PATH];
        snprintf(hlpPath, sizeof(hlpPath), "%s" DVX_PATH_SEP "%s", sCtx->appDir, "dvxbasic.hlp");
        shellLoadAppWithArgs(sAc, DVX_HELP_VIEWER_APP_LITERAL, hlpPath);
    }

    if (menuId == CMD_HELP_API) {
        char sysHlp[DVX_MAX_PATH];
        snprintf(sysHlp, sizeof(sysHlp), "APPS" DVX_PATH_SEP "KPUNCH" DVX_PATH_SEP "PROGMAN" DVX_PATH_SEP "DVXHELP.HLP");
        shellLoadAppWithArgs(sAc, DVX_HELP_VIEWER_APP_LITERAL, sysHlp);
    }

    if (menuId == CMD_HELP_ABOUT) {
        dvxMessageBox(sAc, "About DVX BASIC",
            "DVX BASIC 1.0\n"
            "Visual BASIC Development Environment\n"
            "for the DVX GUI System\n"
            "\n"
            "Copyright 2026 Scott Duensing",
            MB_OK | MB_ICONINFO);
    }
}


// ============================================================
// onObjDropdownChange
// ============================================================
//
// Update the Event dropdown when the Object selection changes.

// Event subsets shown in the Object/Event dropdowns.  The master list
// of every event the runtime can fire lives in basEventSuffixes[] in
// compiler/basEvents.h; these are filtered views for the designer.

// Control/widget events (everything except form-lifecycle events)
static const char *sCommonEvents[] = {
    "Click", "DblClick", "Change", "GotFocus", "LostFocus",
    "KeyPress", "KeyDown", "KeyUp",
    "MouseDown", "MouseUp", "MouseMove", "Scroll",
    "Reposition", "Validate",
    NULL
};

// Form-specific events (lifecycle + input)
static const char *sFormEvents[] = {
    "Load", "QueryUnload", "Unload", "Resize", "Activate", "Deactivate",
    "KeyPress", "KeyDown", "KeyUp",
    "MouseDown", "MouseUp", "MouseMove",
    NULL
};

// Buffer for event dropdown labels (with [] for unimplemented)
static char sEvtLabelBufs[64][32];

static void onObjDropdownChange(WidgetT *w) {
    (void)w;

    if (!sObjDropdown || !sEvtDropdown) {
        return;
    }

    int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);

    if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems)) {
        return;
    }

    const char *selObj = sObjItems[objIdx];

    // Collect which events already have code
    arrsetlen(sEvtItems, 0);

    int32_t procCount = (int32_t)arrlen(sProcTable);
    const char **existingEvts = NULL; // stb_ds temp array

    for (int32_t i = 0; i < procCount; i++) {
        if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
            arrput(existingEvts, sProcTable[i].evtName);
        }
    }

    // Determine which event list to use
    const char **availEvents = sCommonEvents;

    if (strcasecmp(selObj, "(General)") == 0) {
        // Always include (Global) to access module-level code
        arrput(sEvtItems, "(Global)");

        for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
            arrput(sEvtItems, existingEvts[i]);
        }

        arrfree(existingEvts);

        int32_t evtCount = (int32_t)arrlen(sEvtItems);

        // Sort procs after (Global)
        if (evtCount > 2) {
            qsort(sEvtItems + 1, evtCount - 1, sizeof(const char *), cmpStrPtrs);
        }

        wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
        wgtDropdownSetSelected(sEvtDropdown, 0);
        onEvtDropdownChange(sEvtDropdown);

        return;
    }

    // Check if this is a form name
    bool isForm = false;

    if (sDesigner.form && strcasecmp(selObj, sDesigner.form->name) == 0) {
        isForm    = true;
        availEvents = sFormEvents;
    }

    // Check if this is a menu item (only event is Click)
    bool isMenuItem = false;

    if (!isForm && sDesigner.form) {
        for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
            if (strcasecmp(sDesigner.form->menuItems[i].name, selObj) == 0) {
                isMenuItem = true;
                break;
            }
        }
    }

    if (isMenuItem) {
        static const char *sMenuEvents[] = { "Click", NULL };
        availEvents = sMenuEvents;
    }

    // Get widget-specific events from the interface
    const WgtIfaceT *iface = NULL;

    if (!isForm && !isMenuItem && sDesigner.form) {
        for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
            if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) {
                const char *wgtName = wgtFindByBasName(sDesigner.form->controls[i]->typeName);

                if (wgtName) {
                    iface = wgtGetIface(wgtName);
                }

                break;
            }
        }
    }

    // Build the event list: standard events + widget-specific events
    int32_t labelIdx = 0;

    // Add standard events (common or form)
    for (int32_t i = 0; availEvents[i]; i++) {
        bool hasCode = false;

        for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
            if (strcasecmp(existingEvts[j], availEvents[i]) == 0) {
                hasCode = true;
                break;
            }
        }

        if (labelIdx < 64) {
            if (hasCode) {
                snprintf(sEvtLabelBufs[labelIdx], 32, "%s", availEvents[i]);
            } else {
                snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", availEvents[i]);
            }

            arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
            labelIdx++;
        }
    }

    // Add widget-specific events
    if (iface) {
        for (int32_t i = 0; i < iface->eventCount; i++) {
            const char *evtName = iface->events[i].name;

            // Skip if already in the standard list
            bool alreadyListed = false;

            for (int32_t j = 0; availEvents[j]; j++) {
                if (strcasecmp(availEvents[j], evtName) == 0) {
                    alreadyListed = true;
                    break;
                }
            }

            if (alreadyListed) {
                continue;
            }

            bool hasCode = false;

            for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
                if (strcasecmp(existingEvts[j], evtName) == 0) {
                    hasCode = true;
                    break;
                }
            }

            if (labelIdx < 64) {
                if (hasCode) {
                    snprintf(sEvtLabelBufs[labelIdx], 32, "%s", evtName);
                } else {
                    snprintf(sEvtLabelBufs[labelIdx], 32, "[%s]", evtName);
                }

                arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
                labelIdx++;
            }
        }
    }

    // Add any existing events not in the standard/widget list (custom subs)
    for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
        bool alreadyListed = false;
        int32_t evtCount = (int32_t)arrlen(sEvtItems);

        for (int32_t j = 0; j < evtCount; j++) {
            const char *label = sEvtItems[j];

            // Strip brackets for comparison
            if (label[0] == '[') { label++; }

            if (strncasecmp(label, existingEvts[i], strlen(existingEvts[i])) == 0) {
                alreadyListed = true;
                break;
            }
        }

        if (!alreadyListed && labelIdx < 64) {
            snprintf(sEvtLabelBufs[labelIdx], 32, "%s", existingEvts[i]);
            arrput(sEvtItems, sEvtLabelBufs[labelIdx]);
            labelIdx++;
        }
    }

    arrfree(existingEvts);

    int32_t evtCount = (int32_t)arrlen(sEvtItems);

    // Sort: implemented events first, then unimplemented, alphabetical within each
    if (evtCount > 1) {
        qsort(sEvtItems, evtCount, sizeof(const char *), cmpEvtPtrs);
    }

    wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);

    if (evtCount > 0) {
        // Find the first implemented event (no brackets)
        int32_t selectIdx = -1;

        for (int32_t i = 0; i < evtCount; i++) {
            if (sEvtItems[i][0] != '[') {
                selectIdx = i;
                break;
            }
        }

        if (selectIdx >= 0) {
            // Navigate to the first implemented event
            wgtDropdownSetSelected(sEvtDropdown, selectIdx);
            onEvtDropdownChange(sEvtDropdown);
        } else {
            // No implemented events -- find and select the default event
            // for this widget type, which will create its skeleton
            const char *defEvt = NULL;

            if (isForm) {
                defEvt = dsgnDefaultEvent("Form");
            } else if (sDesigner.form) {
                for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
                    if (strcasecmp(sDesigner.form->controls[i]->name, selObj) == 0) {
                        defEvt = dsgnDefaultEvent(sDesigner.form->controls[i]->typeName);
                        break;
                    }
                }
            }

            if (!defEvt) {
                defEvt = "Click";
            }

            // Find the default event in the list (it will be bracketed)
            for (int32_t i = 0; i < evtCount; i++) {
                const char *label = sEvtItems[i];

                if (label[0] == '[') {
                    label++;
                }

                if (strncasecmp(label, defEvt, strlen(defEvt)) == 0) {
                    wgtDropdownSetSelected(sEvtDropdown, i);
                    onEvtDropdownChange(sEvtDropdown);
                    break;
                }
            }
        }
    }
}


static void onPrefsCancel(WidgetT *w) {
    (void)w;
    sPrefsDlg.done = true;
}


// ============================================================
// Preferences dialog
// ============================================================

static void onPrefsOk(WidgetT *w) {
    (void)w;
    sPrefsDlg.accepted = true;
    sPrefsDlg.done     = true;
}


static void onPrjFileDblClick(int32_t fileIdx, bool isForm) {
    (void)isForm;
    activateFile(fileIdx, ViewAutoE);
}


static void onProjectWinClose(WindowT *win) {
    prjDestroyWindow(sAc, win);
    sProjectWin = NULL;
}


static void onReplace(WidgetT *w) {
    (void)w;

    if (!sEditor || !sFindInput || !sReplInput || !isReplaceEnabled()) {
        return;
    }

    const char *needle = wgtGetText(sFindInput);
    const char *repl   = wgtGetText(sReplInput);

    if (!needle || !needle[0]) {
        return;
    }

    snprintf(sFindText, sizeof(sFindText), "%s", needle);
    snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");

    // If text is selected and matches the search, replace it, then find next
    const char *edText = wgtGetText(sEditor);

    if (edText) {
        // TODO: replace current selection if it matches, then find next
        // For now, just do find next
        onFindNext(w);
    }
}


static void onReplaceAll(WidgetT *w) {
    (void)w;

    if (!sFindInput || !sReplInput || !isReplaceEnabled()) {
        return;
    }

    const char *needle = wgtGetText(sFindInput);
    const char *repl   = wgtGetText(sReplInput);

    if (!needle || !needle[0]) {
        return;
    }

    snprintf(sFindText, sizeof(sFindText), "%s", needle);
    snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");

    bool caseSens    = getFindMatchCase();
    FindScopeE scope = getFindScope();
    int32_t totalCount = 0;

    if (scope == ScopeFuncE && sEditor) {
        totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
    } else if (scope == ScopeProjE) {
        stashCurrentFile();

        for (int32_t i = 0; i < sProject.fileCount; i++) {
            activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE);

            if (sEditor) {
                totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
            }
        }
    } else if (scope == ScopeFileE && sEditor) {
        // Replace in all procs of current file
        int32_t procCount = (int32_t)arrlen(sProcBufs);

        for (int32_t p = -1; p < procCount; p++) {
            showProc(p);

            if (sEditor) {
                totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
            }
        }
    } else if (scope == ScopeObjE && sEditor && sObjDropdown) {
        // Replace in all procs belonging to current object
        int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);

        if (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) {
            const char *objName = sObjItems[objIdx];
            int32_t procCount = (int32_t)arrlen(sProcTable);

            for (int32_t p = 0; p < procCount; p++) {
                if (strcasecmp(sProcTable[p].objName, objName) == 0) {
                    showProc(p);

                    if (sEditor) {
                        totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
                    }
                }
            }
        }
    }

    char statusBuf[64];
    snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
    setStatus(statusBuf);
}


static void onReplCheckChange(WidgetT *w) {
    (void)w;
    bool show = isReplaceEnabled();

    if (sReplInput) { sReplInput->enabled = show; }
    if (sBtnReplace) { sBtnReplace->enabled = show; }
    if (sBtnReplAll) { sBtnReplAll->enabled = show; }

    if (sFindWin) {
        dvxInvalidateWindow(sAc, sFindWin);
    }
}

static void onTbCode(WidgetT *w)      { (void)w; handleViewCmd(CMD_VIEW_CODE); }


static void onTbDebug(WidgetT *w)     { (void)w; handleRunCmd(CMD_DEBUG); }

static void onTbDesign(WidgetT *w)    { (void)w; handleViewCmd(CMD_VIEW_DESIGN); }


static void onTbOpen(WidgetT *w)      { (void)w; handleProjectCmd(CMD_PRJ_OPEN); }

static void onTbRun(WidgetT *w)       { (void)w; handleRunCmd(CMD_RUN); }

static void onTbRunToCur(WidgetT *w)  { (void)w; handleRunCmd(CMD_RUN_TO_CURSOR); }

static void onTbSave(WidgetT *w)      { (void)w; handleFileCmd(CMD_SAVE); }

static void onTbStepInto(WidgetT *w)  { (void)w; handleRunCmd(CMD_STEP_INTO); }

static void onTbStepOut(WidgetT *w)   { (void)w; handleRunCmd(CMD_STEP_OUT); }

static void onTbStepOver(WidgetT *w)  { (void)w; handleRunCmd(CMD_STEP_OVER); }

static void onTbStop(WidgetT *w)      { (void)w; handleRunCmd(CMD_STOP); }


static void onWatchClose(WindowT *win) {
    dvxHideWindow(sAc, win);
}


static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
    (void)w;
    (void)shift;

    if (keyCode != '\r' && keyCode != '\n') {
        return;
    }

    if (!sWatchInput) {
        return;
    }

    const char *text = wgtGetText(sWatchInput);

    if (!text || !text[0]) {
        return;
    }

    // Add to watch list
    arrput(sWatchExprs, strdup(text));
    updateWatchWindow();

    // Clear input
    wgtSetText(sWatchInput, "");
}


static void onWatchListDblClick(WidgetT *w) {
    (void)w;
    watchEditSelected();
}


static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
    (void)w;
    (void)shift;

    // Enter — edit selected item
    if (keyCode == '\r' || keyCode == '\n') {
        watchEditSelected();
        return;
    }

    // Delete key (scancode 0x53 with extended flag)
    if (keyCode != (0x53 | 0x100) && keyCode != 127) {
        return;
    }

    if (!sWatchList) {
        return;
    }

    int32_t sel = wgtListViewGetSelected(sWatchList);

    if (sel < 0 || sel >= (int32_t)arrlen(sWatchExprs)) {
        return;
    }

    free(sWatchExprs[sel]);
    arrdel(sWatchExprs, sel);

    updateWatchWindow();
}


static void openFindDialog(bool showReplace) {
    if (sFindWin) {
        // Already open — just toggle replace mode and raise
        if (sReplCheck) {
            wgtCheckboxSetChecked(sReplCheck, showReplace);
            onReplCheckChange(sReplCheck);
        }

        dvxRaiseWindow(sAc, sFindWin);
        return;
    }

    sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", 320, 210, false);

    if (!sFindWin) {
        return;
    }

    sFindWin->onClose    = onFindClose;
    sFindWin->onMenu     = onMenu;
    sFindWin->accelTable = sWin ? sWin->accelTable : NULL;

    WidgetT *root = wgtInitWindow(sAc, sFindWin);
    root->spacing = wgtPixels(3);

    // Find row
    WidgetT *findRow = wgtHBox(root);
    findRow->spacing = wgtPixels(4);
    wgtLabel(findRow, "Find:");
    sFindInput = wgtTextInput(findRow, 256);
    sFindInput->weight = 100;
    wgtSetText(sFindInput, sFindText);

    // Replace checkbox + input
    WidgetT *replRow = wgtHBox(root);
    replRow->spacing = wgtPixels(4);
    sReplCheck = wgtCheckbox(replRow, "Replace:");
    wgtCheckboxSetChecked(sReplCheck, showReplace);
    sReplCheck->onChange = onReplCheckChange;
    sReplInput = wgtTextInput(replRow, 256);
    sReplInput->weight = 100;
    wgtSetText(sReplInput, sReplaceText);

    // Options row: scope + direction + case
    WidgetT *optRow = wgtHBox(root);
    optRow->spacing = wgtPixels(8);

    // Scope
    WidgetT *scopeFrame = wgtFrame(optRow, "Scope");
    WidgetT *scopeBox   = wgtVBox(scopeFrame);
    sScopeGroup = wgtRadioGroup(scopeBox);
    wgtRadio(sScopeGroup, "Function");
    wgtRadio(sScopeGroup, "Object");
    wgtRadio(sScopeGroup, "File");
    wgtRadio(sScopeGroup, "Project");
    wgtRadioGroupSetSelected(sScopeGroup, 3); // Project

    // Direction
    WidgetT *dirFrame = wgtFrame(optRow, "Direction");
    WidgetT *dirBox   = wgtVBox(dirFrame);
    sDirGroup = wgtRadioGroup(dirBox);
    wgtRadio(sDirGroup, "Forward");
    wgtRadio(sDirGroup, "Backward");
    wgtRadioGroupSetSelected(sDirGroup, 0); // Forward

    // Match Case
    WidgetT *caseBox = wgtVBox(optRow);
    sCaseCheck = wgtCheckbox(caseBox, "Match Case");

    // Buttons
    WidgetT *btnRow = wgtHBox(root);
    btnRow->spacing = wgtPixels(4);
    btnRow->align   = AlignEndE;

    WidgetT *btnFind = wgtButton(btnRow, "Find Next");
    btnFind->onClick = onFindNext;

    sBtnReplace = wgtButton(btnRow, "Replace");
    sBtnReplace->onClick = onReplace;

    sBtnReplAll = wgtButton(btnRow, "Replace All");
    sBtnReplAll->onClick = onReplaceAll;

    WidgetT *btnClose = wgtButton(btnRow, "Close");
    btnClose->onClick = onFindCloseBtn;

    // Set initial replace enable state
    onReplCheckChange(sReplCheck);

    dvxFitWindow(sAc, sFindWin);
}


static void openProject(void) {
    FileFilterT filters[] = {
        { "Project Files (*.dbp)" },
        { "All Files (*.*)" }
    };

    char path[DVX_MAX_PATH];

    if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
        return;
    }

    closeProject();

    if (!prjLoad(&sProject, path)) {
        dvxErrorBox(sAc, NULL, "Could not open project file.");
        return;
    }

    prjLoadAllFiles(&sProject, sAc);

    // Create and show project window
    if (!sProjectWin) {
        sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);

        if (sProjectWin) {
            sProjectWin->y          = toolbarBottom() + 25;
            sProjectWin->onClose    = onProjectWinClose;
            sProjectWin->onMenu     = onMenu;
            sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
        }
    } else {
        prjRebuildTree(&sProject);
    }

    char title[300];
    snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
    dvxSetTitle(sAc, sWin, title);

    setStatus("Project loaded.");
    updateProjectMenuState();
    recentAdd(path);
}


// parseProcs -- split source into (General) + per-procedure buffers

static void parseProcs(const char *source) {
    freeProcBufs();

    if (!source) {
        sGeneralBuf = strdup("");
        return;
    }

    const char *pos     = source;
    const char *genEnd  = source; // end of (General) section

    while (*pos) {
        const char *lineStart = pos;

        // Skip leading whitespace
        const char *trimmed = dvxSkipWs(pos);

        bool isSub  = (strncasecmp(trimmed, "SUB ", 4) == 0);
        bool isFunc = (strncasecmp(trimmed, "FUNCTION ", 9) == 0);

        if (isSub || isFunc) {
            // On first proc, mark end of (General) section
            if (arrlen(sProcBufs) == 0) {
                genEnd = lineStart;
            }

            // Find End Sub / End Function
            const char *endTag = isSub ? "END SUB" : "END FUNCTION";
            int32_t endTagLen  = isSub ? 7 : 12;
            const char *scan   = pos;

            // Advance past the Sub/Function line
            while (*scan && *scan != '\n') { scan++; }
            if (*scan == '\n') { scan++; }

            // Scan for End Sub/Function
            while (*scan) {
                const char *sl = dvxSkipWs(scan);

                if (strncasecmp(sl, endTag, endTagLen) == 0) {
                    while (*scan && *scan != '\n') { scan++; }
                    if (*scan == '\n') { scan++; }
                    break;
                }

                while (*scan && *scan != '\n') { scan++; }
                if (*scan == '\n') { scan++; }
            }

            // Extract this procedure
            int32_t procLen = (int32_t)(scan - lineStart);
            char *procBuf = (char *)malloc(procLen + 1);

            if (procBuf) {
                memcpy(procBuf, lineStart, procLen);
                procBuf[procLen] = '\0';
            }

            arrput(sProcBufs, procBuf);
            pos = scan;
            continue;
        }

        // Advance to next line
        while (*pos && *pos != '\n') { pos++; }
        if (*pos == '\n') { pos++; }
    }

    // If no procs found, the entire source is the (General) section
    if (arrlen(sProcBufs) == 0) {
        genEnd = pos; // pos is at the end of the source
    }

    // Extract (General) section
    int32_t genLen = (int32_t)(genEnd - source);

    // Trim trailing blank lines
    while (genLen > 0 && (source[genLen - 1] == '\n' || source[genLen - 1] == '\r' ||
                          source[genLen - 1] == ' '  || source[genLen - 1] == '\t')) {
        genLen--;
    }

    sGeneralBuf = (char *)malloc(genLen + 2);

    if (sGeneralBuf) {
        memcpy(sGeneralBuf, source, genLen);
        sGeneralBuf[genLen] = '\n';
        sGeneralBuf[genLen + 1] = '\0';

        if (genLen == 0) {
            sGeneralBuf[0] = '\0';
        }
    }
}


static void prefsUpdateColorSliders(void) {
    int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);

    if (idx < 0 || idx >= SYNTAX_COLOR_COUNT) {
        return;
    }

    uint32_t c = sPrefsDlg.syntaxColors[idx];
    uint8_t  r = (c >> 16) & 0xFF;
    uint8_t  g = (c >> 8) & 0xFF;
    uint8_t  b = c & 0xFF;

    wgtSliderSetValue(sPrefsDlg.sliderR, r);
    wgtSliderSetValue(sPrefsDlg.sliderG, g);
    wgtSliderSetValue(sPrefsDlg.sliderB, b);

    static char rBuf[8];
    static char gBuf[8];
    static char bBuf[8];
    snprintf(rBuf, sizeof(rBuf), "%d", (int)r);
    snprintf(gBuf, sizeof(gBuf), "%d", (int)g);
    snprintf(bBuf, sizeof(bBuf), "%d", (int)b);
    wgtSetText(sPrefsDlg.lblR, rBuf);
    wgtSetText(sPrefsDlg.lblG, gBuf);
    wgtSetText(sPrefsDlg.lblB, bBuf);

    prefsUpdateSwatch();
}


static void prefsUpdateSwatch(void) {
    if (!sPrefsDlg.colorSwatch) {
        return;
    }

    uint8_t r = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderR);
    uint8_t g = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderG);
    uint8_t b = (uint8_t)wgtSliderGetValue(sPrefsDlg.sliderB);

    uint32_t color = packColor(&sAc->display, r, g, b);
    wgtCanvasClear(sPrefsDlg.colorSwatch, color);
}


// ============================================================
// onOpenClick
// ============================================================


static void printCallback(void *ctx, const char *text, bool newline) {
    (void)ctx;

    if (!text) {
        return;
    }

    int32_t textLen = (int32_t)strlen(text);

    // Append to output buffer
    if (sOutputLen + textLen < IDE_MAX_OUTPUT - 2) {
        memcpy(sOutputBuf + sOutputLen, text, textLen);
        sOutputLen += textLen;
    }

    if (newline && sOutputLen < IDE_MAX_OUTPUT - 2) {
        sOutputBuf[sOutputLen++] = '\n';
    }

    sOutputBuf[sOutputLen] = '\0';

    // Update the output textarea immediately so PRINT is visible
    if (sOutput) {
        setOutputText(sOutputBuf);
    }
}


// ============================================================
// promptAndSave -- ask user to save, discard, or cancel
// ============================================================
//
// Returns true if the caller should proceed (user saved or discarded).
// Returns false if the user cancelled.

static bool promptAndSave(void) {
    if (!hasUnsavedData()) {
        return true;
    }

    int32_t result = dvxPromptSave(sAc, "DVX BASIC");

    if (result == DVX_SAVE_YES) {
        saveFile();
        return true;
    }

    return result == DVX_SAVE_NO;
}


// ============================================================
// updateWatchWindow -- evaluate watch expressions
// ============================================================

// readDebugVar -- read a debug variable's value from the paused VM
static bool readDebugVar(const BasDebugVarT *dv, BasValueT *outVal) {
    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];
        }
    }

    *outVal = val;
    return true;
}


static void recentAdd(const char *path) {
    if (!path || !path[0]) {
        return;
    }

    // If already in the list, move it to the top
    for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
        if (strcasecmp(sRecentFiles[i], path) == 0) {
            char *existing = sRecentFiles[i];
            arrdel(sRecentFiles, i);
            arrins(sRecentFiles, 0, existing);
            recentSave();
            recentRebuildMenu();
            return;
        }
    }

    // If at cap, drop the oldest entry (last)
    while ((int32_t)arrlen(sRecentFiles) >= CMD_RECENT_MAX) {
        int32_t last = (int32_t)arrlen(sRecentFiles) - 1;
        free(sRecentFiles[last]);
        arrdel(sRecentFiles, last);
    }

    // Insert new entry at the top
    arrins(sRecentFiles, 0, strdup(path));
    recentSave();
    recentRebuildMenu();
}


static void recentLoad(void) {
    // Clear any prior entries
    for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
        free(sRecentFiles[i]);
    }

    arrsetlen(sRecentFiles, 0);

    if (!sPrefs) {
        return;
    }

    for (int32_t i = 0; i < CMD_RECENT_MAX; i++) {
        char key[16];
        snprintf(key, sizeof(key), "file%ld", (long)i);
        const char *val = prefsGetString(sPrefs, "recent", key, "");

        if (val[0]) {
            arrput(sRecentFiles, strdup(val));
        }
    }
}


static void recentOpen(int32_t index) {
    if (index < 0 || index >= (int32_t)arrlen(sRecentFiles)) {
        return;
    }

    const char *path = sRecentFiles[index];
    const char *ext  = strrchr(path, '.');

    if (ext && strcasecmp(ext, ".dbp") == 0) {
        // Project file
        closeProject();

        if (!prjLoad(&sProject, path)) {
            dvxErrorBox(sAc, NULL, "Could not open project file.");
            return;
        }

        prjLoadAllFiles(&sProject, sAc);

        if (!sProjectWin) {
            sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);

            if (sProjectWin) {
                sProjectWin->y          = toolbarBottom() + 25;
                sProjectWin->onClose    = onProjectWinClose;
                sProjectWin->onMenu     = onMenu;
                sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
            }
        } else {
            prjRebuildTree(&sProject);
        }

        char title[300];
        snprintf(title, sizeof(title), "DVX BASIC - [%s]", sProject.name);
        dvxSetTitle(sAc, sWin, title);
        setStatus("Project loaded.");
    } else {
        // Single file
        if (!promptAndSave()) {
            return;
        }

        ensureProject(path);

        if (!sProjectWin) {
            sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);

            if (sProjectWin) {
                sProjectWin->y       = toolbarBottom() + 25;
                sProjectWin->onClose = onProjectWinClose;
                dvxRaiseWindow(sAc, sProjectWin);
            }
        }
    }

    updateProjectMenuState();
    recentAdd(path);
}


static void recentRebuildMenu(void) {
    if (!sFileMenu) {
        return;
    }

    // Truncate menu back to the base items (everything through Exit)
    sFileMenu->itemCount = sFileMenuBase;

    int32_t count = (int32_t)arrlen(sRecentFiles);

    if (count == 0) {
        return;
    }

    // Append separator + recent file items after Exit
    wmAddMenuSeparator(sFileMenu);

    for (int32_t i = 0; i < count; i++) {
        // Show just the filename for shorter labels
        const char *name = platformPathBaseName(sRecentFiles[i]);

        char label[MAX_MENU_LABEL];
        snprintf(label, sizeof(label), "&%ld %s", (long)(i + 1), name);
        wmAddMenuItem(sFileMenu, label, CMD_RECENT_BASE + i);
    }
}


static void recentSave(void) {
    if (!sPrefs) {
        return;
    }

    int32_t count = (int32_t)arrlen(sRecentFiles);

    for (int32_t i = 0; i < CMD_RECENT_MAX; i++) {
        char key[16];
        snprintf(key, sizeof(key), "file%ld", (long)i);

        if (i < count) {
            prefsSetString(sPrefs, "recent", key, sRecentFiles[i]);
        } else {
            prefsSetString(sPrefs, "recent", key, "");
        }
    }

    prefsSave(sPrefs);
}


static void removeBreakpointsForFile(int32_t fileIdx) {
    // Remove breakpoints for the given file and adjust indices for
    // files above the removed one (since file indices shift down).
    for (int32_t i = sBreakpointCount - 1; i >= 0; i--) {
        if (sBreakpoints[i].fileIdx == fileIdx) {
            arrdel(sBreakpoints, i);
        } else if (sBreakpoints[i].fileIdx > fileIdx) {
            sBreakpoints[i].fileIdx--;
        }
    }

    sBreakpointCount = (int32_t)arrlen(sBreakpoints);
    updateBreakpointWindow();
}


static char *renameInBuffer(const char *src, const char *oldName, const char *newName) {
    if (!src || !oldName || !newName || !oldName[0]) {
        return NULL;
    }

    int32_t oldLen = (int32_t)strlen(oldName);
    int32_t newLen = (int32_t)strlen(newName);
    int32_t srcLen = (int32_t)strlen(src);

    // First pass: count replacements to compute output size
    int32_t count = 0;

    for (int32_t i = 0; i <= srcLen - oldLen; i++) {
        if (strncasecmp(&src[i], oldName, oldLen) != 0) {
            continue;
        }

        char after = src[i + oldLen];

        if (after != '.' && after != '_') {
            continue;
        }

        if (i > 0 && isIdentChar(src[i - 1])) {
            continue;
        }

        if (prefsGetBool(sPrefs, "editor", "renameSkipComments", true) && isInStringOrComment(src, i)) {
            continue;
        }

        count++;
    }

    if (count == 0) {
        return NULL;
    }

    // Allocate output
    int32_t outLen = srcLen + count * (newLen - oldLen);
    char *out = (char *)malloc(outLen + 1);

    if (!out) {
        return NULL;
    }

    // Second pass: build output
    int32_t op = 0;

    for (int32_t i = 0; i < srcLen; ) {
        if (i <= srcLen - oldLen &&
            strncasecmp(&src[i], oldName, oldLen) == 0) {
            char after = src[i + oldLen];

            if ((after == '.' || after == '_') &&
                (i == 0 || !isIdentChar(src[i - 1])) &&
                (!prefsGetBool(sPrefs, "editor", "renameSkipComments", true) || !isInStringOrComment(src, i))) {
                memcpy(out + op, newName, newLen);
                op += newLen;
                i  += oldLen;
                continue;
            }
        }

        out[op++] = src[i++];
    }

    out[op] = '\0';
    return out;
}


static void runCached(void) {
    if (!sCachedModule) {
        setStatus("No compiled program. Press F5 to compile first.");
        return;
    }

    clearOutput();
    runModule(sCachedModule);
}


static void runModule(BasModuleT *mod) {
    setStatus("Running...");

    closeFindDialog();

    // Hide designer windows while the program runs.
    // Keep the code window visible if debugging (breakpoints or step-into).
    bool hadFormWin   = sFormWin    && sFormWin->visible;
    bool hadToolbox   = sToolboxWin && sToolboxWin->visible;
    bool hadProps     = sPropsWin   && sPropsWin->visible;
    bool hadCodeWin   = sCodeWin    && sCodeWin->visible;
    bool hadPrjWin    = sProjectWin && sProjectWin->visible;

    if (sFormWin)    { dvxHideWindow(sAc, sFormWin); }
    if (sToolboxWin) { dvxHideWindow(sAc, sToolboxWin); }
    if (sPropsWin)   { dvxHideWindow(sAc, sPropsWin); }
    if (sCodeWin) { dvxHideWindow(sAc, sCodeWin); }
    if (sProjectWin) { dvxHideWindow(sAc, sProjectWin); }

    // Create VM
    BasVmT *vm = basVmCreate();
    basVmLoadModule(vm, mod);

    // Set App.Path/Config/Data. In the IDE, config and data live under
    // the project directory so everything stays together during development.
    // Standalone apps use the DVX root-level CONFIG/ and DATA/ directories
    // since the app directory (on CD-ROM) is read-only.
    snprintf(vm->appPath, sizeof(vm->appPath), "%s", sProject.projectDir);
    snprintf(vm->appConfig, sizeof(vm->appConfig), "%s/CONFIG", sProject.projectDir);
    snprintf(vm->appData, sizeof(vm->appData), "%s/DATA", sProject.projectDir);

    platformMkdirRecursive(vm->appConfig);
    platformMkdirRecursive(vm->appData);

    // Set up implicit main frame
    vm->callStack[0].localCount = mod->globalCount > BAS_VM_MAX_LOCALS ? BAS_VM_MAX_LOCALS : mod->globalCount;
    vm->callDepth = 1;

    // Set I/O callbacks
    basVmSetPrintCallback(vm, printCallback, NULL);
    basVmSetInputCallback(vm, inputCallback, NULL);
    basVmSetDoEventsCallback(vm, doEventsCallback, NULL);
    vm->breakpointFn  = onBreakpointHit;
    vm->breakpointCtx = NULL;

    // Set SQL callbacks
    BasSqlCallbacksT sqlCb;
    memset(&sqlCb, 0, sizeof(sqlCb));
    sqlCb.sqlOpen         = dvxSqlOpen;
    sqlCb.sqlClose        = dvxSqlClose;
    sqlCb.sqlExec         = dvxSqlExec;
    sqlCb.sqlError        = dvxSqlError;
    sqlCb.sqlQuery        = dvxSqlQuery;
    sqlCb.sqlNext         = dvxSqlNext;
    sqlCb.sqlEof          = dvxSqlEof;
    sqlCb.sqlFieldCount   = dvxSqlFieldCount;
    sqlCb.sqlFieldName    = dvxSqlFieldName;
    sqlCb.sqlFieldText    = dvxSqlFieldText;
    sqlCb.sqlFieldByName  = dvxSqlFieldByName;
    sqlCb.sqlFieldInt     = dvxSqlFieldInt;
    sqlCb.sqlFieldDbl     = dvxSqlFieldDbl;
    sqlCb.sqlFreeResult   = dvxSqlFreeResult;
    sqlCb.sqlAffectedRows = dvxSqlAffectedRows;
    basVmSetSqlCallbacks(vm, &sqlCb);

    // Set extern library callbacks (DECLARE LIBRARY support)
    BasExternCallbacksT extCb;
    extCb.resolveExtern = basExternResolve;
    extCb.callExtern    = basExternCall;
    extCb.ctx           = NULL;
    basVmSetExternCallbacks(vm, &extCb);

    // Create form runtime (bridges UI opcodes to DVX widgets)
    BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod);

    sVm        = vm;
    sDbgFormRt = formRt;
    sDbgModule = mod;
    sDbgState  = DBG_RUNNING;

    // Set project help file on form runtime for F1 context help
    if (sProject.helpFile[0]) {
        snprintf(formRt->helpFile, sizeof(formRt->helpFile), "%s" DVX_PATH_SEP "%s",
                 sProject.projectDir, sProject.helpFile);
    }
    updateProjectMenuState();

    // Set breakpoints BEFORE loading forms so breakpoints in form
    // init code (module-level statements inside BEGINFORM) fire.
    if (sDbgEnabled) {
        buildVmBreakpoints();
        basVmSetBreakpoints(vm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints));

        if (sDbgBreakOnStart) {
            basVmStepInto(vm);
        }
    }

    // Load forms from project files (AFTER breakpoints are set so
    // init code breakpoints fire), then show the startup form.
    loadFrmFiles(formRt);
    basFormRtLoadAllForms(formRt, sProject.startupForm);

    // Run in slices of 10000 steps, yielding to DVX between slices
    basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);

    BasVmResultE result;
    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);

        if (result == BAS_VM_BREAKPOINT) {
            sDbgState       = DBG_PAUSED;
            sDbgCurrentLine = vm->currentLine;
            debugNavigateToLine(vm->currentLine);
            debugUpdateWindows();
            setStatus("Paused.");
            continue;
        }

        if (result == BAS_VM_STEP_LIMIT) {
            dvxUpdate(sAc);

            if (!sWin || !sAc->running || sStopRequested) {
                break;
            }

            continue;
        }

        if (result == BAS_VM_HALTED) {
            break;
        }

        // 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;
    }

    // VB-style event loop: after module-level code finishes,
    // keep processing events as long as any form is loaded.
    // The program ends when all forms are unloaded (closed).
    if (result == BAS_VM_HALTED && (int32_t)arrlen(formRt->forms) > 0) {
        setStatus("Running (event loop)...");
        sStopRequested = false;

        while (sWin && sAc->running && (int32_t)arrlen(formRt->forms) > 0 && !sStopRequested && !vm->ended) {

            if (sDbgState == DBG_PAUSED) {
                // Paused inside an event handler
                debugNavigateToLine(sDbgCurrentLine);
                debugUpdateWindows();
                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;
    sDbgFormRt      = NULL;
    sDbgModule      = NULL;
    sDbgState       = DBG_IDLE;
    sDbgCurrentLine = -1;
    sDbgEnabled     = false;

    basFormRtDestroy(formRt);
    basVmDestroy(vm);

    // If the IDE was closed while the program was running, skip
    // all UI updates — the windows are already destroyed.
    if (!sWin) {
        return;
    }

    updateProjectMenuState();
    setOutputText(sOutputBuf);

    setStatus("Done.");

    // Restore IDE windows
    if (hadFormWin   && sFormWin)    { dvxShowWindow(sAc, sFormWin); }
    if (hadToolbox   && sToolboxWin) { dvxShowWindow(sAc, sToolboxWin); }
    if (hadProps     && sPropsWin)   { dvxShowWindow(sAc, sPropsWin); }
    if (hadCodeWin   && sCodeWin)    { dvxShowWindow(sAc, sCodeWin); }
    if (hadPrjWin    && sProjectWin) { dvxShowWindow(sAc, sProjectWin); }

    // Repaint to clear destroyed runtime forms and restore designer
    dvxUpdate(sAc);
}


static void saveActiveFile(void) {
    if (sProject.projectPath[0] == '\0') {
        return;
    }

    int32_t idx = sProject.activeFileIdx;

    if (idx < 0 || idx >= sProject.fileCount) {
        return;
    }

    // Ensure buffer is up-to-date with editor/designer state
    stashCurrentFile();

    PrjFileT *file = &sProject.files[idx];
    char fullPath[DVX_MAX_PATH];
    prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));

    if (file->buffer) {
        FILE *f = fopen(fullPath, "w");

        if (f) {
            fputs(file->buffer, f);
            fclose(f);
            file->modified = false;

            if (file->isForm && sDesigner.form) {
                sDesigner.form->dirty = false;
            }
        } else {
            dvxErrorBox(sAc, NULL, "Could not write file.");
            return;
        }
    }

    setStatus("Saved.");
    updateDirtyIndicators();
}


// saveCurProc -- save editor contents back to the current buffer.
// Returns true if the proc list was modified (skeleton discarded or
// new procs extracted), meaning sProcBufs indices may have shifted.

static bool saveCurProc(void) {
    if (!sEditor) {
        return false;
    }

    const char *edText = wgtGetText(sEditor);

    if (!edText) {
        return false;
    }

    if (sCurProcIdx == -1) {
        // General section -- check for embedded proc declarations
        char *cleaned = extractNewProcs(edText);

        free(sGeneralBuf);
        sGeneralBuf = cleaned ? cleaned : strdup(edText);

        if (cleaned) {
            wgtSetText(sEditor, sGeneralBuf);
            return true;
        }

        return false;
    } else if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcBufs)) {
        // Get the name of the current proc so we can identify its block
        // regardless of position in the editor.
        char ownName[128] = "";

        if (sCurProcIdx < (int32_t)arrlen(sProcTable)) {
            snprintf(ownName, sizeof(ownName), "%s_%s",
                     sProcTable[sCurProcIdx].objName,
                     sProcTable[sCurProcIdx].evtName);
        }

        // If ownName is empty (General proc without underscore), extract
        // it from the first Sub/Function line of the existing buffer.
        if (ownName[0] == '\0' || strcmp(ownName, "(General)_") == 0) {
            const char *ep = dvxSkipWs(sProcBufs[sCurProcIdx]);
            if (strncasecmp(ep, "SUB ", 4) == 0) { ep += 4; }
            else if (strncasecmp(ep, "FUNCTION ", 9) == 0) { ep += 9; }
            ep = dvxSkipWs(ep);
            int32_t n = 0;
            while (*ep && *ep != '(' && *ep != ' ' && *ep != '\t' && *ep != '\n' && n < 127) {
                ownName[n++] = *ep++;
            }
            ownName[n] = '\0';
        }

        // Count Sub/Function blocks in the editor text
        int32_t blockCount = 0;
        const char *scan = edText;

        while (*scan) {
            const char *tl = dvxSkipWs(scan);
            if (strncasecmp(tl, "SUB ", 4) == 0 || strncasecmp(tl, "FUNCTION ", 9) == 0) {
                blockCount++;
            }
            while (*scan && *scan != '\n') { scan++; }
            if (*scan == '\n') { scan++; }
        }

        if (blockCount <= 1) {
            // Single proc (or none) -- check for empty skeleton
            const char *p = dvxSkipWs(edText);
            bool isSub = (strncasecmp(p, "SUB ", 4) == 0);
            const char *endTag    = isSub ? "END SUB" : "END FUNCTION";
            int32_t     endTagLen = isSub ? 7 : 12;

            // Skip declaration line
            while (*p && *p != '\n') { p++; }
            if (*p == '\n') { p++; }

            bool bodyEmpty = true;

            while (*p) {
                const char *line = dvxSkipWs(p);
                if (strncasecmp(line, endTag, endTagLen) == 0) { break; }
                while (*p && *p != '\n') {
                    if (*p != ' ' && *p != '\t' && *p != '\r') { bodyEmpty = false; }
                    p++;
                }
                if (*p == '\n') { p++; }
            }

            if (bodyEmpty) {
                free(sProcBufs[sCurProcIdx]);
                arrdel(sProcBufs, sCurProcIdx);
                sCurProcIdx = -2;
                return true;
            } else {
                free(sProcBufs[sCurProcIdx]);
                sProcBufs[sCurProcIdx] = strdup(edText);
                return false;
            }
        } else {
            // Multiple proc blocks in the editor.  Find the one matching
            // ownName, keep it in this buffer, extract the rest.
            const char *pos        = edText;
            const char *ownStart   = NULL;
            const char *ownEnd     = NULL;
            char *extras           = (char *)malloc(strlen(edText) + 1);
            int32_t extPos         = 0;

            if (!extras) {
                free(sProcBufs[sCurProcIdx]);
                sProcBufs[sCurProcIdx] = strdup(edText);
            } else {
                while (*pos) {
                    const char *lineStart = pos;
                    const char *tl = dvxSkipWs(pos);

                    bool isSub  = (strncasecmp(tl, "SUB ", 4) == 0);
                    bool isFunc = (strncasecmp(tl, "FUNCTION ", 9) == 0);

                    if (isSub || isFunc) {
                        // Extract the proc name
                        const char *np = dvxSkipWs(tl + (isSub ? 4 : 9));
                        char name[128];
                        int32_t nn = 0;
                        while (*np && *np != '(' && *np != ' ' && *np != '\t' && *np != '\n' && nn < 127) {
                            name[nn++] = *np++;
                        }
                        name[nn] = '\0';

                        // Find the matching End tag
                        const char *endTag    = isSub ? "END SUB" : "END FUNCTION";
                        int32_t     endTagLen = isSub ? 7 : 12;
                        const char *s = pos;
                        while (*s && *s != '\n') { s++; }
                        if (*s == '\n') { s++; }

                        while (*s) {
                            const char *sl = dvxSkipWs(s);
                            if (strncasecmp(sl, endTag, endTagLen) == 0) {
                                while (*s && *s != '\n') { s++; }
                                if (*s == '\n') { s++; }
                                break;
                            }
                            while (*s && *s != '\n') { s++; }
                            if (*s == '\n') { s++; }
                        }

                        if (strcasecmp(name, ownName) == 0) {
                            ownStart = lineStart;
                            ownEnd   = s;
                        } else {
                            // Copy this block to extras
                            int32_t blen = (int32_t)(s - lineStart);
                            memcpy(extras + extPos, lineStart, blen);
                            extPos += blen;
                        }

                        pos = s;
                        continue;
                    }

                    // Non-proc line (blank lines between blocks etc.) -- skip
                    while (*pos && *pos != '\n') { pos++; }
                    if (*pos == '\n') { pos++; }
                }

                extras[extPos] = '\0';

                // Update this buffer with just the owned proc
                if (ownStart && ownEnd) {
                    int32_t keepLen = (int32_t)(ownEnd - ownStart);
                    char *kept = (char *)malloc(keepLen + 1);
                    if (kept) {
                        memcpy(kept, ownStart, keepLen);
                        kept[keepLen] = '\0';
                        free(sProcBufs[sCurProcIdx]);
                        sProcBufs[sCurProcIdx] = kept;
                    }
                }

                // Extract extra procs into their own buffers
                if (extPos > 0) {
                    extractNewProcs(extras);
                }

                free(extras);

                // Update editor to show only this proc
                wgtSetText(sEditor, sProcBufs[sCurProcIdx]);
                return true;
            }
        }
    }

    return false;
}


static void saveFile(void) {
    if (sProject.projectPath[0] == '\0' || sProject.activeFileIdx < 0) {
        return;
    }

    // Save the active project file
    saveActiveFile();

    // Also save any other dirty forms
    for (int32_t i = 0; i < sProject.fileCount; i++) {
        if (i == sProject.activeFileIdx) {
            continue;
        }

        if (sProject.files[i].isForm && sProject.files[i].modified && sProject.files[i].buffer) {
            char fullPath[DVX_MAX_PATH];
            prjFullPath(&sProject, i, fullPath, sizeof(fullPath));

            FILE *f = fopen(fullPath, "w");

            if (f) {
                fputs(sProject.files[i].buffer, f);
                fclose(f);
                sProject.files[i].modified = false;
            }
        }
    }

    updateDirtyIndicators();
}


// selectDropdowns -- set the Object and Event dropdowns to match a
// given control name and event name.

// selectDropdowns -- set the Object and Event dropdown selections to
// match a given control/event without triggering navigation callbacks.
// The caller is responsible for having already navigated to the proc.

static void selectDropdowns(const char *objName, const char *evtName) {
    if (!sObjDropdown || !sEvtDropdown) {
        return;
    }

    // Select the object
    int32_t objCount = (int32_t)arrlen(sObjItems);

    for (int32_t i = 0; i < objCount; i++) {
        if (strcasecmp(sObjItems[i], objName) == 0) {
            wgtDropdownSetSelected(sObjDropdown, i);
            break;
        }
    }

    // Rebuild the event list for this object but suppress navigation
    bool savedSuppress = sDropdownNavSuppressed;
    sDropdownNavSuppressed = true;
    onObjDropdownChange(sObjDropdown);
    sDropdownNavSuppressed = savedSuppress;

    // Now select the specific event
    int32_t evtCount = (int32_t)arrlen(sEvtItems);

    for (int32_t i = 0; i < evtCount; i++) {
        const char *label = sEvtItems[i];

        if (label[0] == '[') {
            label++;
        }

        if (strncasecmp(label, evtName, strlen(evtName)) == 0) {
            wgtDropdownSetSelected(sEvtDropdown, i);
            break;
        }
    }
}


static void setOutputText(const char *text) {
    if (!sOutWin) {
        showOutputWindow();
    }

    if (sOutput) {
        wgtSetText(sOutput, text);
    }

    if (sOutputToLog && text && text[0]) {
        dvxLog("BASIC: %s", text);
    }
}


static void setStatus(const char *text) {
    if (sStatus) {
        wgtSetText(sStatus, text);
    }
}


static void showBreakpointWindow(void) {
    if (sBreakpointWin) {
        dvxShowWindow(sAc, sBreakpointWin);
        dvxRaiseWindow(sAc, sBreakpointWin);
        updateBreakpointWindow();
        return;
    }

    int32_t winW = 320;
    int32_t winH = 180;
    int32_t winX = sAc->display.width - winW - 10;
    int32_t winY = sAc->display.height - winH - 10;

    sBreakpointWin = dvxCreateWindow(sAc, "Breakpoints", winX, winY, winW, winH, true);

    if (sBreakpointWin) {
        sBreakpointWin->onClose    = onBreakpointWinClose;
        sBreakpointWin->onMenu     = onMenu;
        sBreakpointWin->accelTable = sWin ? sWin->accelTable : NULL;
        sBreakpointWin->resizable  = true;

        WidgetT *root = wgtInitWindow(sAc, sBreakpointWin);

        if (root) {
            sBreakpointList = wgtListView(root);

            if (sBreakpointList) {
                sBreakpointList->weight     = 100;
                sBreakpointList->onKeyDown  = onBreakpointListKeyDown;
                sBreakpointList->onDblClick = onBreakpointListDblClick;
                wgtListViewSetMultiSelect(sBreakpointList, true);

                static const ListViewColT cols[] = {
                    { "File",      wgtChars(12), ListViewAlignLeftE },
                    { "Procedure", wgtChars(16), ListViewAlignLeftE },
                    { "Line",      wgtChars(6),  ListViewAlignRightE },
                };

                wgtListViewSetColumns(sBreakpointList, cols, 3);
            }
        }
    }

    updateBreakpointWindow();
}


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();
}


static void showCodeWindow(void) {
    if (sCodeWin) {
        return; // already open
    }

    int32_t codeY = toolbarBottom();
    int32_t codeH = sAc->display.height - codeY - 122;

    sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true);

    // Ensure position is below the toolbar (dvxCreateWindow may adjust)
    if (sCodeWin) {
        sCodeWin->y = codeY;
    }

    if (sCodeWin) {
        sCodeWin->onMenu     = onMenu;
        sCodeWin->onFocus    = onContentFocus;
        sCodeWin->onClose    = onCodeWinClose;
        sCodeWin->accelTable = sWin ? sWin->accelTable : NULL;
        sLastFocusWin        = sCodeWin;

        WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin);

        WidgetT *dropdownRow = wgtHBox(codeRoot);
        dropdownRow->spacing = wgtPixels(4);

        wgtLabel(dropdownRow, "Object:");

        sObjDropdown = wgtDropdown(dropdownRow);
        sObjDropdown->weight   = 100;
        sObjDropdown->onChange = onObjDropdownChange;
        wgtDropdownSetItems(sObjDropdown, NULL, 0);

        wgtLabel(dropdownRow, "Function:");

        sEvtDropdown = wgtDropdown(dropdownRow);
        sEvtDropdown->weight   = 100;
        sEvtDropdown->onChange = onEvtDropdownChange;
        wgtDropdownSetItems(sEvtDropdown, NULL, 0);

        sEditor = wgtTextArea(codeRoot, IDE_MAX_SOURCE);
        sEditor->weight = 100;
        wgtTextAreaSetColorize(sEditor, basicColorize, NULL);

        // Apply saved syntax colors
        {
            uint32_t initColors[SYNTAX_COLOR_COUNT];

            for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
                char key[32];
                snprintf(key, sizeof(key), "color%d", (int)i);
                initColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]);
            }

            wgtTextAreaSetSyntaxColors(sEditor, initColors, SYNTAX_COLOR_COUNT);
        }

        wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc);
        wgtTextAreaSetGutterClick(sEditor, onGutterClick);
        wgtTextAreaSetShowLineNumbers(sEditor, true);
        wgtTextAreaSetAutoIndent(sEditor, true);
        wgtTextAreaSetCaptureTabs(sEditor, true);
        wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, "editor", "tabWidth", 3));
        wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, "editor", "useSpaces", true));

        // onChange is set after initial content is loaded by the caller
        // (navigateToEventSub, onPrjFileDblClick, etc.) to prevent false dirty marking.

        updateProjectMenuState();
        updateDirtyIndicators();
    }
}


static void showImmediateWindow(void) {
    if (sImmWin) {
        return;
    }

    int32_t outH = 120;
    int32_t outY = sAc->display.height - outH;

    sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true);

    if (sImmWin) {
        sImmWin->onFocus     = onContentFocus;
        sImmWin->onMenu      = onMenu;
        sImmWin->accelTable  = sWin ? sWin->accelTable : NULL;
        sLastFocusWin        = sImmWin;

        WidgetT *immRoot = wgtInitWindow(sAc, sImmWin);

        if (immRoot) {
            sImmediate = wgtTextArea(immRoot, IDE_MAX_IMM);

            if (sImmediate) {
                sImmediate->weight    = 100;
                sImmediate->readOnly  = false;
                sImmediate->onChange  = onImmediateChange;
            } else {
                dvxLog("IDE: failed to create immediate TextArea");
            }

        } else {
            dvxLog("IDE: failed to init immediate window root");
        }
    }
}


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();
}


static void showOutputWindow(void) {
    if (sOutWin) {
        return;
    }

    int32_t outH = 120;
    int32_t outY = sAc->display.height - outH;

    sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true);

    if (sOutWin) {
        sOutWin->onFocus     = onContentFocus;
        sOutWin->onMenu      = onMenu;
        sOutWin->accelTable  = sWin ? sWin->accelTable : NULL;
        sLastFocusWin        = sOutWin;

        WidgetT *outRoot = wgtInitWindow(sAc, sOutWin);
        sOutput = wgtTextArea(outRoot, IDE_MAX_OUTPUT);
        sOutput->weight   = 100;
        sOutput->readOnly = true;

        if (sOutputLen > 0) {
            setOutputText(sOutputBuf);
        }
    }
}


static void showPreferencesDialog(void) {
    memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));

    WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", 420, 440, false);

    if (!win) {
        return;
    }

    win->maxW = win->w;
    win->maxH = win->h;

    WidgetT *root = wgtInitWindow(sAc, win);
    root->spacing = wgtPixels(4);

    // ---- Tab control ----
    WidgetT *tabs = wgtTabControl(root);
    tabs->weight = 100;

    // ======== General tab ========
    WidgetT *generalPage = wgtTabPage(tabs, "General");
    generalPage->spacing = wgtPixels(4);

    // Editor section
    WidgetT *edFrame = wgtFrame(generalPage, "Editor");
    edFrame->spacing = wgtPixels(2);

    sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
    wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, "editor", "renameSkipComments", true));

    sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "Require variable declaration (OPTION EXPLICIT)");
    wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, "editor", "optionExplicit", false));

    WidgetT *tabRow = wgtHBox(edFrame);
    tabRow->spacing = wgtPixels(4);
    wgtLabel(tabRow, "Tab width:");
    sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, 4);
    sPrefsDlg.tabWidthInput->maxW = wgtPixels(40);

    char tabBuf[8];
    snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, "editor", "tabWidth", 3));
    wgtSetText(sPrefsDlg.tabWidthInput, tabBuf);

    sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs");
    wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, "editor", "useSpaces", true));

    // Project Defaults section
    WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults");
    prjFrame->spacing = wgtPixels(2);
    prjFrame->weight  = 100;

    WidgetT *r1 = wgtHBox(prjFrame);
    r1->spacing = wgtPixels(4);
    WidgetT *l1 = wgtLabel(r1, "Author:");
    l1->minW = wgtPixels(80);
    sPrefsDlg.defAuthor = wgtTextInput(r1, 64);
    sPrefsDlg.defAuthor->weight = 100;
    wgtSetText(sPrefsDlg.defAuthor, prefsGetString(sPrefs, "defaults", "author", ""));

    WidgetT *r2 = wgtHBox(prjFrame);
    r2->spacing = wgtPixels(4);
    WidgetT *l2 = wgtLabel(r2, "Publisher:");
    l2->minW = wgtPixels(80);
    sPrefsDlg.defPublisher = wgtTextInput(r2, 64);
    sPrefsDlg.defPublisher->weight = 100;
    wgtSetText(sPrefsDlg.defPublisher, prefsGetString(sPrefs, "defaults", "publisher", ""));

    WidgetT *r3 = wgtHBox(prjFrame);
    r3->spacing = wgtPixels(4);
    WidgetT *l3 = wgtLabel(r3, "Version:");
    l3->minW = wgtPixels(80);
    sPrefsDlg.defVersion = wgtTextInput(r3, 16);
    sPrefsDlg.defVersion->weight = 100;
    wgtSetText(sPrefsDlg.defVersion, prefsGetString(sPrefs, "defaults", "version", "1.0"));

    WidgetT *r4 = wgtHBox(prjFrame);
    r4->spacing = wgtPixels(4);
    WidgetT *l4 = wgtLabel(r4, "Copyright:");
    l4->minW = wgtPixels(80);
    sPrefsDlg.defCopyright = wgtTextInput(r4, 64);
    sPrefsDlg.defCopyright->weight = 100;
    wgtSetText(sPrefsDlg.defCopyright, prefsGetString(sPrefs, "defaults", "copyright", ""));

    wgtLabel(prjFrame, "Description:");
    sPrefsDlg.defDescription = wgtTextArea(prjFrame, 512);
    sPrefsDlg.defDescription->weight = 100;
    sPrefsDlg.defDescription->minH  = wgtPixels(48);
    wgtSetText(sPrefsDlg.defDescription, prefsGetString(sPrefs, "defaults", "description", ""));

    // ======== Colors tab ========
    WidgetT *colorsPage = wgtTabPage(tabs, "Colors");
    colorsPage->spacing = wgtPixels(4);

    // Load current colors from prefs (or defaults)
    for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
        char key[32];
        snprintf(key, sizeof(key), "color%d", (int)i);
        sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, "syntax", key, (int32_t)sDefaultSyntaxColors[i]);
    }

    WidgetT *colorsHBox = wgtHBox(colorsPage);
    colorsHBox->spacing = wgtPixels(8);
    colorsHBox->weight  = 100;

    // Left: color list
    sPrefsDlg.colorList = wgtListBox(colorsHBox);
    sPrefsDlg.colorList->weight   = 100;
    sPrefsDlg.colorList->onChange = onColorListChange;

    wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, SYNTAX_COLOR_COUNT);

    // Right: RGB sliders + value labels + swatch preview
    WidgetT *sliderBox = wgtVBox(colorsHBox);
    sliderBox->spacing = wgtPixels(2);
    sliderBox->weight  = 100;

    wgtLabel(sliderBox, "Red:");
    sPrefsDlg.sliderR = wgtSlider(sliderBox, 0, 255);
    sPrefsDlg.sliderR->onChange = onColorSliderChange;
    sPrefsDlg.lblR = wgtLabel(sliderBox, "0");
    wgtLabelSetAlign(sPrefsDlg.lblR, AlignEndE);

    wgtLabel(sliderBox, "Green:");
    sPrefsDlg.sliderG = wgtSlider(sliderBox, 0, 255);
    sPrefsDlg.sliderG->onChange = onColorSliderChange;
    sPrefsDlg.lblG = wgtLabel(sliderBox, "0");
    wgtLabelSetAlign(sPrefsDlg.lblG, AlignEndE);

    wgtLabel(sliderBox, "Blue:");
    sPrefsDlg.sliderB = wgtSlider(sliderBox, 0, 255);
    sPrefsDlg.sliderB->onChange = onColorSliderChange;
    sPrefsDlg.lblB = wgtLabel(sliderBox, "0");
    wgtLabelSetAlign(sPrefsDlg.lblB, AlignEndE);

    wgtLabel(sliderBox, "Preview:");
    sPrefsDlg.colorSwatch = wgtCanvas(sliderBox, 64, 24);

    // Select first color entry and load sliders
    wgtListBoxSetSelected(sPrefsDlg.colorList, 1);
    prefsUpdateColorSliders();

    wgtTabControlSetActive(tabs, 0);

    // ---- OK / Cancel ----
    WidgetT *btnRow = wgtHBox(root);
    btnRow->spacing = wgtPixels(8);
    btnRow->align   = AlignEndE;

    WidgetT *btnOk = wgtButton(btnRow, "OK");
    btnOk->minW    = wgtPixels(60);
    btnOk->onClick = onPrefsOk;

    WidgetT *btnCancel = wgtButton(btnRow, "Cancel");
    btnCancel->minW    = wgtPixels(60);
    btnCancel->onClick = onPrefsCancel;

    dvxFitWindow(sAc, win);

    WindowT *prevModal = sAc->modalWindow;
    sAc->modalWindow   = win;

    while (!sPrefsDlg.done && sAc->running) {
        dvxUpdate(sAc);
    }

    if (sPrefsDlg.accepted) {
        // General tab
        prefsSetBool(sPrefs, "editor", "renameSkipComments", wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
        prefsSetBool(sPrefs, "editor", "optionExplicit", wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
        prefsSetBool(sPrefs, "editor", "useSpaces", wgtCheckboxIsChecked(sPrefsDlg.useSpaces));

        const char *tw = wgtGetText(sPrefsDlg.tabWidthInput);
        int32_t tabW   = tw ? atoi(tw) : 3;

        if (tabW < 1) { tabW = 1; }
        if (tabW > 8) { tabW = 8; }

        prefsSetInt(sPrefs, "editor", "tabWidth", tabW);

        if (sEditor) {
            wgtTextAreaSetTabWidth(sEditor, tabW);
            wgtTextAreaSetUseTabChar(sEditor, !wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
        }

        const char *val;

        val = wgtGetText(sPrefsDlg.defAuthor);
        prefsSetString(sPrefs, "defaults", "author", val ? val : "");

        val = wgtGetText(sPrefsDlg.defPublisher);
        prefsSetString(sPrefs, "defaults", "publisher", val ? val : "");

        val = wgtGetText(sPrefsDlg.defVersion);
        prefsSetString(sPrefs, "defaults", "version", val ? val : "1.0");

        val = wgtGetText(sPrefsDlg.defCopyright);
        prefsSetString(sPrefs, "defaults", "copyright", val ? val : "");

        val = wgtGetText(sPrefsDlg.defDescription);
        prefsSetString(sPrefs, "defaults", "description", val ? val : "");

        // Colors tab
        for (int32_t i = 0; i < SYNTAX_COLOR_COUNT; i++) {
            char key[32];
            snprintf(key, sizeof(key), "color%d", (int)i);
            prefsSetInt(sPrefs, "syntax", key, (int32_t)sPrefsDlg.syntaxColors[i]);
        }

        applySyntaxColors();
        prefsSave(sPrefs);
    }

    sAc->modalWindow = prevModal;
    dvxDestroyWindow(sAc, win);
}


// showProc -- display a procedure buffer in the editor

static void showProc(int32_t procIdx) {
    if (!sEditor) {
        return;
    }

    // Save whatever is currently in the editor.
    // If a buffer was deleted (empty skeleton discard), adjust the
    // target index since arrdel shifts everything after it.
    if (sCurProcIdx >= -1) {
        int32_t deletedIdx = sCurProcIdx;
        bool changed = saveCurProc();

        if (changed && deletedIdx >= 0 && procIdx > deletedIdx) {
            procIdx--;
        }
    }

    // Suppress onChange while loading -- setting text is not a user edit
    void (*savedOnChange)(WidgetT *) = sEditor->onChange;
    sEditor->onChange = NULL;

    if (procIdx == -1) {
        wgtSetText(sEditor, sGeneralBuf ? sGeneralBuf : "");
        sCurProcIdx = -1;
    } else if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcBufs)) {
        wgtSetText(sEditor, sProcBufs[procIdx] ? sProcBufs[procIdx] : "");
        sCurProcIdx = procIdx;
    }

    sEditorLineCount = countLines(wgtGetText(sEditor));
    sEditor->onChange = savedOnChange;
}


// findInProject -- search all project files for a text match.
// Starts from the current editor position in the current file,
// then continues through subsequent files, wrapping around.
// Opens the file and selects the match when found.

// showProcAndFind -- switch to a procedure, sync the dropdowns, and
// select the search match in the editor.

static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive) {
    showProc(procIdx);

    // Sync the Object/Event dropdowns to match
    if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
        selectDropdowns(sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
    } else if (procIdx == -1 && sObjDropdown) {
        wgtDropdownSetSelected(sObjDropdown, 0); // (General)
    }

    if (sEditor) {
        return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true);
    }

    return false;
}


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->onKeyDown = onWatchInputKeyDown;
            }

            // Results list below
            sWatchList = wgtListView(root);

            if (sWatchList) {
                sWatchList->weight       = 100;
                sWatchList->onKeyDown    = onWatchListKeyDown;
                sWatchList->onDblClick   = onWatchListDblClick;

                static const ListViewColT cols[] = {
                    { "Expression", wgtChars(14), ListViewAlignLeftE },
                    { "Value",      wgtChars(20), ListViewAlignLeftE },
                };

                wgtListViewSetColumns(sWatchList, cols, 2);
            }
        }
    }

    updateWatchWindow();
}


// stashCurrentFile -- stash the currently active file's editor/designer
// state back into its project buffer.  This is caching only -- does not
// mark modified.

static void stashCurrentFile(void) {
    if (sProject.activeFileIdx < 0 || sProject.activeFileIdx >= sProject.fileCount) {
        return;
    }

    PrjFileT *cur = &sProject.files[sProject.activeFileIdx];

    if (cur->isForm && sDesigner.form) {
        // Save editor code back to form->code before serializing
        stashFormCode();

        // Serialize form designer state to .frm text
        char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);

        if (frmBuf) {
            int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);

            free(cur->buffer);

            if (frmLen > 0) {
                frmBuf[frmLen] = '\0';
                cur->buffer = frmBuf;
            } else {
                free(frmBuf);
                cur->buffer = NULL;
            }
        }
    } else if (!cur->isForm && sEditorFileIdx == sProject.activeFileIdx) {
        // Stash full source (only if editor has this file's code)
        saveCurProc();
        const char *src = getFullSource();
        free(cur->buffer);
        cur->buffer = src ? strdup(src) : NULL;
    }
}


static void stashDesignerState(void) {
    // If a form is open in the designer, switch to its code view
    if (sDesigner.form && sProject.activeFileIdx >= 0 &&
        sProject.activeFileIdx < sProject.fileCount &&
        sProject.files[sProject.activeFileIdx].isForm) {
        loadFormCodeIntoEditor();

        if (sCodeWin) {
            dvxRaiseWindow(sAc, sCodeWin);
        }

        setStatus("Code view.");
        return;
    }

    stashCurrentFile();

    // Show code window if hidden
    if (sCodeWin && !sCodeWin->visible) {
        dvxShowWindow(sAc, sCodeWin);
    }

    if (sCodeWin) {
        dvxRaiseWindow(sAc, sCodeWin);
    }

    setStatus("Code view.");
}


// stashFormCode -- if the proc buffers belong to the designer's form,
// save them back to form->code.  Uses sEditorFileIdx to know which
// file the proc buffers actually belong to.

static void stashFormCode(void) {
    if (!sDesigner.form || sEditorFileIdx < 0) {
        return;
    }

    if (sEditorFileIdx >= sProject.fileCount || !sProject.files[sEditorFileIdx].isForm) {
        return;
    }

    if (strcasecmp(sProject.files[sEditorFileIdx].formName, sDesigner.form->name) != 0) {
        return;
    }

    saveCurProc();
    free(sDesigner.form->code);
    sDesigner.form->code = strdup(getFullSource());
}


static void switchToDesign(void) {
    stashFormCode();

    // If already open, just bring to front
    if (sFormWin) {
        return;
    }

    // If no form is loaded, create a blank one
    if (!sDesigner.form) {
        dsgnNewForm(&sDesigner, "Form1");
    }

    // Create the form designer window using the shared form window builder
    const char *formName = sDesigner.form ? sDesigner.form->name : "Form1";
    DsgnFormT  *form     = sDesigner.form;

    char title[128];
    snprintf(title, sizeof(title), "%s [Design]", formName);

    WidgetT *root;
    WidgetT *contentBox;

    sFormWin = dsgnCreateFormWindow(sAc, title,
        form ? form->layout : "VBox",
        form ? form->resizable : true,
        false,
        false,
        form ? form->width : IDE_DESIGN_W,
        form ? form->height : IDE_DESIGN_H,
        0, 0,
        &root, &contentBox);

    if (!sFormWin) {
        return;
    }

    sFormWin->visible      = true;
    sFormWin->onClose      = onFormWinClose;
    sFormWin->onMenu       = onFormWinMenu;
    sFormWin->accelTable   = sWin ? sWin->accelTable : NULL;
    sDesigner.formWin      = sFormWin;

    // Build preview menu bar from form's menu items
    dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);

    // Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint)
    sFormWin->onPaint       = onFormWinPaint;
    sFormWin->onMouse       = onFormWinMouse;
    sFormWin->onKey         = onFormWinKey;
    sFormWin->onResize      = onFormWinResize;
    sFormWin->onCursorQuery = onFormWinCursorQuery;

    // Create live widgets for each control
    dsgnCreateWidgets(&sDesigner, contentBox);

    // Set form caption as window title
    if (sDesigner.form && sDesigner.form->caption[0]) {
        char winTitle[280];
        snprintf(winTitle, sizeof(winTitle), "%s [Design]", sDesigner.form->caption);
        dvxSetTitle(sAc, sFormWin, winTitle);
    }

    // Size the form window
    if (sDesigner.form && sDesigner.form->autoSize) {
        dvxFitWindow(sAc, sFormWin);
        sDesigner.form->width  = sFormWin->w;
        sDesigner.form->height = sFormWin->h;
    } else if (sDesigner.form) {
        dvxResizeWindow(sAc, sFormWin, sDesigner.form->width, sDesigner.form->height);
    }

    // Create toolbox and properties windows
    if (!sToolboxWin) {
        sToolboxWin = tbxCreate(sAc, &sDesigner);

        if (sToolboxWin) {
            sToolboxWin->y          = toolbarBottom();
            sToolboxWin->onMenu     = onMenu;
            sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
        }
    }

    if (!sPropsWin) {
        sPropsWin = prpCreate(sAc, &sDesigner);

        if (sPropsWin) {
            sPropsWin->y          = toolbarBottom();
            sPropsWin->onMenu     = onMenu;
            sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
        }
    }

    dvxInvalidateWindow(sAc, sFormWin);
    updateProjectMenuState();
    setStatus("Design view open.");
}


static void teardownFormWin(void) {
    if (sFormWin) {
        dvxDestroyWindow(sAc, sFormWin);
        cleanupFormWin();
    }
}


static void toggleBreakpoint(void) {
    if (!sEditor) {
        return;
    }

    toggleBreakpointLine(wgtTextAreaGetCursorLine(sEditor));
}


static void toggleBreakpointLine(int32_t editorLine) {
    int32_t fileIdx = sProject.activeFileIdx;

    // Convert editor line to file code line by adding proc offset
    int32_t codeLine = editorLine;

    if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
        codeLine = sProcTable[sCurProcIdx].lineNum + editorLine - 1;
    }

    // Check if this breakpoint already exists — remove it
    for (int32_t i = 0; i < sBreakpointCount; i++) {
        if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) {
            arrdel(sBreakpoints, i);
            sBreakpointCount = (int32_t)arrlen(sBreakpoints);

            if (sEditor) {
                wgtInvalidatePaint(sEditor);
            }

            updateBreakpointWindow();
            return;
        }
    }

    // Validate that this line is breakable (not blank, comment, or SUB/FUNCTION decl/end)
    if (sEditor) {
        const char *text = wgtGetText(sEditor);

        if (text) {
            // Find the start of editorLine (1-based)
            const char *p = text;
            int32_t ln = 1;

            while (*p && ln < editorLine) {
                if (*p == '\n') {
                    ln++;
                }

                p++;
            }

            // Skip leading whitespace
            p = dvxSkipWs(p);

            // Blank line
            if (*p == '\0' || *p == '\n' || *p == '\r') {
                return;
            }

            // Comment (single quote or REM)
            if (*p == '\'') {
                return;
            }

            if (strncasecmp(p, "REM ", 4) == 0 || strncasecmp(p, "REM\n", 4) == 0 ||
                strncasecmp(p, "REM\r", 4) == 0 || strcasecmp(p, "REM") == 0) {
                return;
            }

            // SUB/FUNCTION declaration or END SUB/FUNCTION
            if (strncasecmp(p, "SUB ", 4) == 0 || strncasecmp(p, "FUNCTION ", 9) == 0) {
                return;
            }

            if (strncasecmp(p, "END SUB", 7) == 0 || strncasecmp(p, "END FUNCTION", 12) == 0) {
                return;
            }
        }
    }

    // Add new breakpoint
    IdeBreakpointT bp;
    memset(&bp, 0, sizeof(bp));
    bp.fileIdx  = fileIdx;
    bp.codeLine = codeLine;
    bp.procIdx  = sCurProcIdx;

    if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
        snprintf(bp.procName, sizeof(bp.procName), "%s.%s",
                 sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName);
    } else {
        snprintf(bp.procName, sizeof(bp.procName), "(General)");
    }

    arrput(sBreakpoints, bp);
    sBreakpointCount = (int32_t)arrlen(sBreakpoints);

    if (sEditor) {
        wgtInvalidatePaint(sEditor);
    }

    updateBreakpointWindow();
}


static int32_t toolbarBottom(void) {
    return sWin ? sWin->y + sWin->h + 2 : 60;
}


// ============================================================
// updateLocalsWindow -- refresh locals display from VM state
// ============================================================


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 updateBreakpointWindow(void) {
    if (!sBreakpointList || !sBreakpointWin || !sBreakpointWin->visible) {
        return;
    }

    // Free previous pass's strdup'd strings
    for (int32_t i = 0; i < (int32_t)arrlen(sBpFiles); i++) {
        free(sBpFiles[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sBpProcs); i++) {
        free(sBpProcs[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sBpLines); i++) {
        free(sBpLines[i]);
    }

    arrsetlen(sBpFiles, 0);
    arrsetlen(sBpProcs, 0);
    arrsetlen(sBpLines, 0);
    arrsetlen(sBpCells, 0);

    if (sBreakpointCount == 0) {
        wgtListViewSetData(sBreakpointList, NULL, 0);
        return;
    }

    int32_t count = sBreakpointCount;

    for (int32_t i = 0; i < count; i++) {
        char  fileBuf[DVX_MAX_PATH];
        char  procBuf[BAS_MAX_PROC_NAME * 2];
        char  lineBuf[12];

        // File name
        if (sBreakpoints[i].fileIdx >= 0 && sBreakpoints[i].fileIdx < sProject.fileCount) {
            snprintf(fileBuf, sizeof(fileBuf), "%s", sProject.files[sBreakpoints[i].fileIdx].path);
        } else {
            snprintf(fileBuf, sizeof(fileBuf), "?");
        }

        // Procedure name
        snprintf(procBuf, sizeof(procBuf), "%s", sBreakpoints[i].procName);

        // Line number
        snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sBreakpoints[i].codeLine);

        arrput(sBpFiles, strdup(fileBuf));
        arrput(sBpProcs, strdup(procBuf));
        arrput(sBpLines, strdup(lineBuf));
    }

    for (int32_t i = 0; i < count; i++) {
        arrput(sBpCells, sBpFiles[i]);
        arrput(sBpCells, sBpProcs[i]);
        arrput(sBpCells, sBpLines[i]);
    }

    wgtListViewSetData(sBreakpointList, sBpCells, count);
}


// ============================================================
// updateCallStackWindow
// ============================================================


static void updateCallStackWindow(void) {
    if (!sCallStackList || !sCallStackWin || !sCallStackWin->visible) {
        return;
    }

    // Free previous pass's strdup'd strings
    for (int32_t i = 0; i < (int32_t)arrlen(sCallNames); i++) {
        free(sCallNames[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sCallLines); i++) {
        free(sCallLines[i]);
    }

    arrsetlen(sCallNames, 0);
    arrsetlen(sCallLines, 0);
    arrsetlen(sCallCells, 0);

    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;
                }
            }
        }

        char nameBuf[BAS_MAX_PROC_NAME];
        char lineBuf[16];

        snprintf(nameBuf, sizeof(nameBuf), "%s", procName);
        snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sDbgCurrentLine);
        arrput(sCallNames, strdup(nameBuf));
        arrput(sCallLines, strdup(lineBuf));
        rowCount++;
    }

    // Walk call stack (skip frame 0 which is the implicit module frame)
    for (int32_t d = sVm->callDepth - 2; d >= 0; 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;
                }
            }
        }

        char nameBuf[BAS_MAX_PROC_NAME];

        snprintf(nameBuf, sizeof(nameBuf), "%s", name);
        arrput(sCallNames, strdup(nameBuf));
        arrput(sCallLines, strdup(""));
        rowCount++;
    }

    for (int32_t i = 0; i < rowCount; i++) {
        arrput(sCallCells, sCallNames[i]);
        arrput(sCallCells, sCallLines[i]);
    }

    wgtListViewSetData(sCallStackList, sCallCells, rowCount);
}


static void updateDirtyIndicators(void) {
    // Sync form->dirty to the project file entry so the tree, title
    // bar, and save logic all see a single consistent modified flag.
    if (sDesigner.form && sDesigner.form->dirty) {
        int32_t idx = sProject.activeFileIdx;
        if (idx >= 0 && idx < sProject.fileCount && sProject.files[idx].isForm) {
            sProject.files[idx].modified = true;
        }
    }

    // Toolbar title: "DVX BASIC - [ProjectName] *"
    if (sWin && sProject.projectPath[0] != '\0') {
        char title[300];
        bool anyDirty = sProject.dirty;

        if (!anyDirty) {
            for (int32_t i = 0; i < sProject.fileCount; i++) {
                if (sProject.files[i].modified) {
                    anyDirty = true;
                    break;
                }
            }
        }

        snprintf(title, sizeof(title), "DVX BASIC - [%s]%s",
                 sProject.name, anyDirty ? " *" : "");
        dvxSetTitle(sAc, sWin, title);
    }

    // Code window title -- only shows * when code has been edited
    if (sCodeWin) {
        bool codeDirty = false;

        const char *codeFile = "";

        if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) {
            codeDirty = sProject.files[sEditorFileIdx].modified;
            codeFile  = sProject.files[sEditorFileIdx].path;
        } else if (sProject.activeFileIdx >= 0) {
            codeDirty = sProject.files[sProject.activeFileIdx].modified;
            codeFile  = sProject.files[sProject.activeFileIdx].path;
        }

        char codeTitle[DVX_MAX_PATH + 16];
        snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : "");
        dvxSetTitle(sAc, sCodeWin, codeTitle);
    }

    // Design window title
    if (sFormWin && sDesigner.form) {
        char title[280];
        snprintf(title, sizeof(title), "%s [Design]%s",
                 sDesigner.form->caption[0] ? sDesigner.form->caption : sDesigner.form->name,
                 sDesigner.form->dirty ? " *" : "");
        dvxSetTitle(sAc, sFormWin, title);
    }

    // Project tree: rebuild with "*" on dirty files
    if (sProjectWin) {
        prjRebuildTree(&sProject);
    }
}


static void updateDropdowns(void) {
    // Reset dynamic arrays.  sObjItems owns strdup'd copies -- free them
    // first so no entry survives a subsequent controls/menuItems mutation
    // that could invalidate the source pointer.
    arrsetlen(sProcTable, 0);

    for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) {
        free((void *)sObjItems[i]);
    }

    arrsetlen(sObjItems, 0);
    arrsetlen(sEvtItems, 0);

    if (!sObjDropdown || !sEvtDropdown) {
        return;
    }

    // Scan the reassembled full source
    const char *src = getFullSource();

    if (!src) {
        return;
    }

    // Scan line by line for SUB / FUNCTION
    const char *pos     = src;
    int32_t     lineNum = 1;

    while (*pos) {
        const char *lineStart = pos;

        // Skip leading whitespace
        pos = dvxSkipWs(pos);

        // Check for SUB or FUNCTION keyword
        bool isSub  = (strncasecmp(pos, "SUB ", 4) == 0);
        bool isFunc = (strncasecmp(pos, "FUNCTION ", 9) == 0);

        if (isSub || isFunc) {
            pos = dvxSkipWs(pos + (isSub ? 4 : 9));

            char procName[64];
            int32_t nameLen = 0;

            while (*pos && *pos != '(' && *pos != ' ' && *pos != '\t' && *pos != '\n' && *pos != '\r' && nameLen < 63) {
                procName[nameLen++] = *pos++;
            }

            procName[nameLen] = '\0';

            // Find End Sub / End Function
            const char *endTag = isSub ? "END SUB" : "END FUNCTION";
            int32_t endTagLen  = isSub ? 7 : 12;
            const char *scan   = pos;

            while (*scan) {
                const char *sl = dvxSkipWs(scan);

                if (strncasecmp(sl, endTag, endTagLen) == 0) {
                    // Advance past the End line
                    while (*scan && *scan != '\n') {
                        scan++;
                    }

                    if (*scan == '\n') {
                        scan++;
                    }

                    break;
                }

                while (*scan && *scan != '\n') {
                    scan++;
                }

                if (*scan == '\n') {
                    scan++;
                }
            }

            IdeProcEntryT entry;
            memset(&entry, 0, sizeof(entry));
            entry.lineNum = lineNum;

            // Match proc name against known objects: form name,
            // controls, menu items. Try each as a prefix followed
            // by "_". This handles names with multiple underscores
            // correctly (e.g., "cmdOK_Click" matches "cmdOK", not
            // "This_Is_A_Dumb_Name" matching a control named "This").
            bool isEvent = false;

            if (sDesigner.form) {
                // Collect all known object names
                const char *objNames[512];
                int32_t objNameCount = 0;

                objNames[objNameCount++] = sDesigner.form->name;

                for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls) && objNameCount < 511; ci++) {
                    objNames[objNameCount++] = sDesigner.form->controls[ci]->name;
                }

                for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems) && objNameCount < 511; mi++) {
                    objNames[objNameCount++] = sDesigner.form->menuItems[mi].name;
                }

                // Try each object name as prefix + "_"
                for (int32_t oi = 0; oi < objNameCount; oi++) {
                    int32_t nameLen = (int32_t)strlen(objNames[oi]);

                    if (nameLen > 0 && strncasecmp(procName, objNames[oi], nameLen) == 0 && procName[nameLen] == '_') {
                        snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]);
                        snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + nameLen + 1);
                        isEvent = true;
                        break;
                    }
                }
            }

            if (!isEvent) {
                snprintf(entry.objName, sizeof(entry.objName), "%s", "(General)");
                snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
            }

            arrput(sProcTable, entry);

            // Skip to end of this proc (already scanned)
            pos = scan;

            // Count lines we skipped
            for (const char *c = lineStart; c < scan; c++) {
                if (*c == '\n') {
                    lineNum++;
                }
            }

            continue;
        }

        // Advance to end of line
        while (*pos && *pos != '\n') {
            pos++;
        }

        if (*pos == '\n') {
            pos++;
        }

        lineNum++;
    }

    // Build object names for the Object dropdown.  All entries are
    // strdup'd so sObjItems owns its strings -- the designer's menuItems
    // array in particular is an stb_ds struct array whose backing
    // storage can move on the next arrput, which would dangle any name
    // pointer we borrowed from &menuItems[i].name.
    arrput(sObjItems, strdup("(General)"));

    if (sDesigner.form) {
        arrput(sObjItems, strdup(sDesigner.form->name));

        for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
            arrput(sObjItems, strdup(sDesigner.form->controls[i]->name));
        }

        // Add menu item names (non-separator, non-top-level-header-only)
        for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
            DsgnMenuItemT *mi = &sDesigner.form->menuItems[i];

            if (mi->name[0] && mi->caption[0] != '-') {
                arrput(sObjItems, strdup(mi->name));
            }
        }
    }

    // Sort object items alphabetically, keeping (General) first
    int32_t objCount = (int32_t)arrlen(sObjItems);

    if (objCount > 1) {
        qsort(sObjItems + 1, objCount - 1, sizeof(const char *), cmpStrPtrs);
    }

    wgtDropdownSetItems(sObjDropdown, sObjItems, objCount);

    if (objCount > 0) {
        wgtDropdownSetSelected(sObjDropdown, 0);
        onObjDropdownChange(sObjDropdown);
    } else {
        wgtDropdownSetItems(sEvtDropdown, NULL, 0);
    }
}


static void updateLocalsWindow(void) {
    if (!sLocalsList || !sLocalsWin || !sLocalsWin->visible) {
        return;
    }

    // Free previous pass's strdup'd strings
    for (int32_t i = 0; i < (int32_t)arrlen(sLocalsNames); i++) {
        free(sLocalsNames[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sLocalsTypes); i++) {
        free(sLocalsTypes[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sLocalsValues); i++) {
        free(sLocalsValues[i]);
    }

    arrsetlen(sLocalsNames, 0);
    arrsetlen(sLocalsTypes, 0);
    arrsetlen(sLocalsValues, 0);
    arrsetlen(sLocalsCells, 0);

    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; i++) {
            BasDebugVarT *dv = &sDbgModule->debugVars[i];

            // Show locals for current proc, and globals/form vars
            if (dv->scope == SCOPE_LOCAL && dv->procIndex != curProcIdx) {
                continue;
            }

            // Skip internal mangled names (e.g. "DoCount$Count" for Static vars).
            // String variable names end with $ (e.g. "name$") — those are fine.
            // Mangled names have $ in the middle.
            {
                const char *dollar = strchr(dv->name, '$');

                if (dollar && dollar[1] != '\0') {
                    continue;
                }
            }

            // For form-scope vars, only show if we're in that form's context
            // and the form name matches the current form.
            if (dv->scope == SCOPE_FORM) {
                if (!sVm->currentFormVars) {
                    continue;
                }

                // Match against current form name
                if (dv->formName[0] && sVm->currentForm) {
                    BasFormT *curForm = (BasFormT *)sVm->currentForm;

                    if (strcasecmp(dv->formName, curForm->name) != 0) {
                        continue;
                    }
                }
            }

            char nameBuf[BAS_MAX_PROC_NAME];
            char typeBuf[16];
            char valueBuf[64];

            snprintf(nameBuf, sizeof(nameBuf), "%s", dv->name);

            // Read the value first so we can use it for the type column
            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];
                }
            }

            // Type column — arrays show "Array(type)" with element type
            if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) {
                snprintf(typeBuf, sizeof(typeBuf), "%s()", typeNameStr(val.arrVal->elementType));
            } else {
                snprintf(typeBuf, sizeof(typeBuf), "%s", typeNameStr(dv->dataType));
            }

            formatValue(&val, valueBuf, sizeof(valueBuf));

            arrput(sLocalsNames,  strdup(nameBuf));
            arrput(sLocalsTypes,  strdup(typeBuf));
            arrput(sLocalsValues, strdup(valueBuf));
            rowCount++;
        }
    }

    for (int32_t i = 0; i < rowCount; i++) {
        arrput(sLocalsCells, sLocalsNames[i]);
        arrput(sLocalsCells, sLocalsTypes[i]);
        arrput(sLocalsCells, sLocalsValues[i]);
    }

    wgtListViewSetData(sLocalsList, sLocalsCells, rowCount);
}


static void updateProjectMenuState(void) {
    if (!sWin || !sWin->menuBar) {
        return;
    }

    bool hasProject = (sProject.projectPath[0] != '\0');
    bool hasFile    = (hasProject && sProject.activeFileIdx >= 0);
    bool hasForm    = (hasFile && sProject.files[sProject.activeFileIdx].isForm);
    bool isIdle     = (sDbgState == DBG_IDLE);
    bool isPaused   = (sDbgState == DBG_PAUSED);
    bool isRunning  = (sDbgState == DBG_RUNNING);
    bool canRun     = hasProject && (isIdle || isPaused);
    bool canStop    = isRunning || isPaused;

    // Project menu
    wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE,   hasProject && sProject.dirty);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE,  hasProject);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS,  hasProject);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, prjGetSelectedFileIdx() >= 0);

    // Save: only when active file is dirty
    bool fileDirty = hasFile && sProject.files[sProject.activeFileIdx].modified;
    bool formDirty = hasFile && sDesigner.form && sDesigner.form->dirty;

    wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, fileDirty || formDirty);

    // Save All: only when any file is dirty
    bool anyDirty = false;

    for (int32_t i = 0; i < sProject.fileCount && !anyDirty; i++) {
        if (sProject.files[i].modified) {
            anyDirty = true;
        }
    }

    if (sDesigner.form && sDesigner.form->dirty) {
        anyDirty = true;
    }

    wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyDirty);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, hasProject && isIdle);

    // Edit menu
    wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND,       hasProject);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT,  hasProject);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE,    hasProject);

    // View menu — consider both active file and project tree selection
    int32_t selIdx     = prjGetSelectedFileIdx();
    bool    selIsFile  = (hasProject && selIdx >= 0 && selIdx < sProject.fileCount);
    bool    selIsForm  = (selIsFile && sProject.files[selIdx].isForm);
    bool    canCode    = hasFile || selIsFile;
    bool    canDesign  = hasForm || selIsForm;

    wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_CODE,    canCode);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_DESIGN,  canDesign);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_MENU_EDITOR,  canDesign);

    // Run menu
    wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN,           canRun);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_NOCMP,    canRun && sCachedModule != NULL);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_DEBUG,         canRun);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_STOP,          canStop);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_INTO,     canRun);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_OVER,     isPaused);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_OUT,      isPaused);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_TO_CURSOR, isPaused);
    wmMenuItemSetEnabled(sWin->menuBar, CMD_TOGGLE_BP,     hasFile);

    // Toolbar buttons
    if (sTbRun)      { wgtSetEnabled(sTbRun,      canRun); }
    if (sTbStop)     { wgtSetEnabled(sTbStop,     canStop); }
    if (sTbDebug)    { wgtSetEnabled(sTbDebug,    canRun); }
    if (sTbStepInto) { wgtSetEnabled(sTbStepInto, canRun); }
    if (sTbStepOver) { wgtSetEnabled(sTbStepOver, isPaused); }
    if (sTbStepOut)  { wgtSetEnabled(sTbStepOut,  isPaused); }
    if (sTbRunToCur) { wgtSetEnabled(sTbRunToCur, isPaused); }
    if (sTbCode)     { wgtSetEnabled(sTbCode,     canCode); }
    if (sTbDesign)   { wgtSetEnabled(sTbDesign,   canDesign); }
}


static void updateWatchWindow(void) {
    if (!sWatchList || !sWatchWin || !sWatchWin->visible) {
        return;
    }

    // Free previous pass's strdup'd strings
    for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprBuf); i++) {
        free(sWatchExprBuf[i]);
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sWatchValBuf); i++) {
        free(sWatchValBuf[i]);
    }

    arrsetlen(sWatchExprBuf, 0);
    arrsetlen(sWatchValBuf,  0);
    arrsetlen(sWatchCells,   0);

    int32_t count = (int32_t)arrlen(sWatchExprs);

    if (count == 0) {
        wgtListViewSetData(sWatchList, NULL, 0);
        return;
    }

    for (int32_t i = 0; i < count; i++) {
        char exprBuf[256];
        char valBuf[256];

        snprintf(exprBuf, sizeof(exprBuf), "%s", sWatchExprs[i]);

        if (sDbgState == DBG_PAUSED && sVm && sDbgModule) {
            BasValueT val;
            memset(&val, 0, sizeof(val));

            if (lookupWatchVar(sWatchExprs[i], &val)) {
                // Simple variable/field/subscript lookup succeeded
                formatValue(&val, valBuf, sizeof(valBuf));
            } else if (evalWatchExpr(sWatchExprs[i], valBuf, sizeof(valBuf))) {
                // Expression compiled and evaluated successfully
            } else {
                snprintf(valBuf, sizeof(valBuf), "<error>");
            }
        } else {
            valBuf[0] = '\0';
        }

        arrput(sWatchExprBuf, strdup(exprBuf));
        arrput(sWatchValBuf,  strdup(valBuf));
    }

    for (int32_t i = 0; i < count; i++) {
        arrput(sWatchCells, sWatchExprBuf[i]);
        arrput(sWatchCells, sWatchValBuf[i]);
    }

    wgtListViewSetData(sWatchList, sWatchCells, count);
}


static void watchEditSelected(void) {
    if (!sWatchList || !sWatchInput) {
        return;
    }

    int32_t sel = wgtListViewGetSelected(sWatchList);

    if (sel < 0 || sel >= (int32_t)arrlen(sWatchExprs)) {
        return;
    }

    // Put expression text into the input box
    wgtSetText(sWatchInput, sWatchExprs[sel]);

    // Remove from list
    free(sWatchExprs[sel]);
    arrdel(sWatchExprs, sel);

    updateWatchWindow();

    // Focus the input box
    wgtSetFocused(sWatchInput);
}


// evalWatchExpr -- compile and evaluate an expression using the paused VM's state.
// Used as a fallback when lookupWatchVar can't handle the expression.
// Returns the printed result in outBuf.

static void watchPrintCallback(void *ctx, const char *text, bool newline) {
    (void)ctx;

    if (text) {
        int32_t tl = (int32_t)strlen(text);

        if (sWatchPrintLen + tl < (int32_t)sizeof(sWatchPrintBuf) - 1) {
            memcpy(sWatchPrintBuf + sWatchPrintLen, text, tl);
            sWatchPrintLen += tl;
        }
    }

    if (newline && sWatchPrintLen < (int32_t)sizeof(sWatchPrintBuf) - 1) {
        sWatchPrintBuf[sWatchPrintLen++] = '\n';
    }

    sWatchPrintBuf[sWatchPrintLen] = '\0';
}


int32_t appMain(DxeAppContextT *ctx) {
    sCtx = ctx;
    sAc  = ctx->shellCtx;

    // Set help file and context-sensitive F1 handler
    snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "dvxbasic.hlp");
    snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile);
    ctx->onHelpQuery  = helpQueryHandler;
    ctx->helpQueryCtx = NULL;

    basStringSystemInit();
    prjInit(&sProject);
    buildWindow();

    // Load persisted settings
    shellEnsureConfigDir(sCtx);
    char prefsPath[DVX_MAX_PATH];
    shellConfigPath(sCtx, "dvxbasic.ini", prefsPath, sizeof(prefsPath));
    sPrefs = prefsLoad(prefsPath);

    if (!sPrefs) {
        sPrefs = prefsCreate();
        prefsSaveAs(sPrefs, prefsPath);
    }

    if (sToolbar && sWin && sWin->menuBar) {
        bool showTb = prefsGetBool(sPrefs, "view", "toolbar", true);
        sToolbar->visible = showTb;
        wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
    }

    if (sStatusBar && sWin && sWin->menuBar) {
        bool showSb = prefsGetBool(sPrefs, "view", "statusbar", true);
        sStatusBar->visible = showSb;
        wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
    }

    if (sWin && sWin->menuBar) {
        bool saveOnRun = prefsGetBool(sPrefs, "run", "saveOnRun", true);
        wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
    }

    // Load recent files list and populate menu
    recentLoad();
    recentRebuildMenu();

    if (sWin) {
        dvxFitWindowH(sAc, sWin);
    }

    sOutputBuf[0] = '\0';
    sOutputLen    = 0;

    updateProjectMenuState();
    setStatus("Ready.");
    return 0;
}

