// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // ideMain.c -- DVX BASIC IDE application // // A DVX app that edits, compiles, runs and debugs BASIC projects. // PRINT output goes to a scrollable TextArea widget. Compile and // runtime errors are displayed with file/procedure line numbers. #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 "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/basEvents.h" #include "../compiler/compiler.h" #include "../formrt/formrt.h" #include "../formrt/frmParser.h" #include "dvxRes.h" #include "../../../../libs/kpunch/sql/dvxSql.h" #include "../../../../libs/kpunch/texthelp/textHelp.h" #include "../runtime/vm.h" #include "../runtime/values.h" #include "stb_ds_wrap.h" #include #include #include #include #include #include #include #include // ============================================================ // Constants // ============================================================ #define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file #define IDE_MAX_OUTPUT 32768 #define IDE_WORD_BUF 32 // single-word scratch buffer #define IDE_SOURCE_MARGIN 64 #define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names #define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *" #define IDE_FORM_TITLE_BUF (DSGN_MAX_TEXT + 32) // "caption [Design] *" #define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message #define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report #define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys #define IDE_IMM_LINE_BUF 512 // one Immediate-window line #define IDE_IMM_WRAP_BUF (IDE_IMM_LINE_BUF + 8) // line plus "PRINT " prefix #define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton #define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text #define IDE_RENAME_TITLE "Rename File" #define IDE_VALUE_BUF 256 // formatted variable value / watch text #define IDE_FIND_TEXT_BUF 256 // find / replace text #define IDE_LINE_NUM_BUF 16 // decimal line number text #define IDE_TYPE_NAME_BUF (BAS_MAX_IDENT + 16) // "Integer()" / "Variant/ClassName" type column text #define IDE_RGB_LABEL_BUF 16 // "Green 255" #define IDE_TAB_WIDTH_BUF 8 // tab width text input #define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line #define IDE_QUOTE_OVERHEAD 3 // two quotes plus NUL around a string value #define IDE_ARRAY_DIM_RESERVE 10 // room kept for ") [n]" after array dims #define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log #define IDE_SNIPPET_SLICE 10000 // Immediate/Watch snippet steps per slice #define IDE_SNIPPET_MAX_SLICES 100 // slices before a snippet is abandoned // sSyntaxMap flag: a color-only (not reserved) word, left uncolored where // it names a member (obj.Width). #define IDE_SYNTAX_UNRESERVED 0x80 #define IDE_MAIN_TITLE "DVX BASIC" #define IDE_GENERAL_SECTION "(General)" #define IDE_GLOBAL_SECTION "(Global)" #define IDE_MODULE_FRAME "(module)" #define IDE_COLLECTION_TYPE "Collection" // debugger type name of a Collection #define IDE_OBJECT_TYPE "Object" // debugger type name of any other object #define IDE_BREAK_TAG " [break]" #define IDE_DEFAULT_FORM "Form1" #define IDE_HELP_FILE "dvxbasic.hlp" #define IDE_PREFS_FILE "dvxbasic.ini" #define IDE_DBP_EXT ".dbp" #define IDE_FORM_TYPE_NAME FRM_KW_FORM // designer/validator type name of a form #define IDE_MENU_TYPE_NAME FRM_KW_MENU // validator type name of a menu item #define IDE_MENU_EVENT BAS_EVT_CLICK // the only event a menu item fires #define IDE_FALLBACK_EVENT BAS_EVT_CLICK // default event when a type names none #define IDE_IMM_RHS_VAR "IDE_IMM_RHS" // Immediate assignment scratch variable #define IDE_RENAME_TEMP_FMT "IDE_MNU_REN%d" // placeholder during menu renames #define IDE_PASTE_FORM "IdePaste" // scratch form wrapping a pasted control #define IDE_NEW_CLASS_FMT "Class%d" // name of a new class module // Text of a new class module (%s = its name) #define IDE_NEW_CLASS_TEXT BAS_CLS_KW_VERSION " DVX 1.00 CLASS\n" BAS_CLS_KW_ATTRIBUTE " " BAS_CLS_ATTR_NAME " = \"%s\"\n\nOption Explicit\n" // Keyword tags for procedure scanning (case-insensitive prefix match) #define KW_SUB "SUB " #define KW_FUNCTION "FUNCTION " #define KW_END_SUB "END SUB" #define KW_END_FUNCTION "END FUNCTION" #define KW_PUBLIC "PUBLIC " #define KW_PRIVATE "PRIVATE " #define KW_REM "REM" #define KW_LET "LET " #define KW_PRINT "PRINT" #define KW_DIM "DIM" #define KW_LINE_PREFIX "Line " #define FRM_BEGIN_TAG "Begin " #define FRM_END_TAG "End" #define KW_LEN(s) ((int32_t)sizeof(s) - 1) // Preference sections/keys used from more than one place #define IDE_PREF_KEY_TAB_WIDTH "tabWidth" #define IDE_PREF_KEY_USE_SPACES "useSpaces" #define IDE_PREF_KEY_RENAME_SKIP "renameSkipComments" #define PREF_SEC_SYNTAX "syntax" #define PREF_KEY_COLOR_FMT "color%d" #define PREF_SEC_RUN "run" #define PREF_KEY_SAVE_ON_RUN "saveOnRun" #define PREF_KEY_OUTPUT_TO_LOG "outputToLog" #define PREF_SEC_VIEW "view" #define PREF_KEY_TOOLBAR "toolbar" #define PREF_KEY_STATUSBAR "statusbar" #define PREF_SEC_RECENT "recent" #define PREF_KEY_RECENT_FMT "file%ld" #define IDE_DEFAULT_TAB_WIDTH 3 #define IDE_MIN_TAB_WIDTH 1 #define IDE_MAX_TAB_WIDTH 8 // Editor colors #define IDE_BP_GUTTER_R 200 #define IDE_BP_GUTTER_G 0 #define IDE_BP_GUTTER_B 0 #define IDE_DBG_LINE_R 255 #define IDE_DBG_LINE_G 255 #define IDE_DBG_LINE_B 128 // Window geometry #define IDE_WIN_MARGIN 10 // gap from screen edge #define IDE_TOOLBAR_GAP 2 // gap below the toolbar window #define IDE_TOOLBAR_FALLBACK_Y 60 // toolbar bottom when no toolbar window #define IDE_TOOLBAR_SEP_W 10 // toolbar group separator width #define IDE_TOOLBAR_INIT_H 200 // toolbar window height before fit #define IDE_PROJECT_WIN_OFFSET 25 // project window y below toolbar bottom #define IDE_BP_WIN_W 320 #define IDE_DEBUG_WIN_H 180 // breakpoints / call stack / watch height #define IDE_CALLSTACK_WIN_W 220 #define IDE_CALLSTACK_WIN_Y_OFF 210 // below toolbar bottom #define IDE_LOCALS_WIN_W 250 #define IDE_LOCALS_WIN_H 200 #define IDE_WATCH_WIN_W 280 #define IDE_WATCH_WIN_X_GAP 260 // watch window sits left of locals #define IDE_FIND_WIN_W 320 #define IDE_FIND_WIN_H 210 #define IDE_FIND_ROOT_SPACING 3 #define IDE_PREFS_WIN_W 420 #define IDE_PREFS_WIN_H 440 #define IDE_PREFS_LABEL_W 80 #define IDE_PREFS_TAB_INPUT_W 40 #define IDE_PREFS_TAB_INPUT_MAX 4 #define IDE_PREFS_TEXT_MAX 64 #define IDE_PREFS_VERSION_MAX 16 #define IDE_PREFS_BUTTON_W 60 #define IDE_SWATCH_W 64 #define IDE_SWATCH_H 24 #define IDE_WIDGET_SPACING 4 #define IDE_WIDGET_SPACING_SM 2 #define IDE_WIDGET_SPACING_LG 8 // Debug ListView column widths (characters) #define IDE_COL_W_FILE 12 #define IDE_COL_W_PROC 16 #define IDE_COL_W_LINE 6 #define IDE_COL_W_NAME 12 #define IDE_COL_W_TYPE 8 #define IDE_COL_W_VALUE 16 #define IDE_COL_W_EXPR 14 #define IDE_COL_W_WATCH_VALUE 20 // 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_NEW_CLASS 134 #define CMD_PRJ_RENAME 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_IMM_HEADROOM IDE_IMM_LINE_BUF // Immediate space output never takes, left for typing #define IDE_SECTION_GENERAL (-1) // section index of (General) (sCurProcIdx, find, navigation) #define IDE_SECTION_NONE (-2) // editor bound to no section / scan every section / not activated #define IDE_SECTION_GAP 1 // blank lines between sections in the canonical layout #define IDE_NO_LINE (-1) // no line (dropped anchor, unmapped line) #define IDE_CARET_LOAD 0 // editorSetText: leave the caret where loading puts it #define IDE_CARET_KEEP (-1) // editorSetText: keep the caret on its current line number #define IDE_OUTPUT_WIN_H 120 // Output/Immediate window height #define IDE_OUTPUT_WIN_GAP 2 // gap between code window and output strip // View mode for activateFile typedef enum { ViewAutoE, // .frm -> design, .bas -> code ViewCodeE, // force code view ViewDesignE // force design view } IdeViewModeE; // Find scope: also the radio index in the Find dialog's Scope group typedef enum { ScopeFuncE, ScopeObjE, ScopeFileE, ScopeProjE, ScopeCountE } FindScopeE; // Find direction: also the radio index in the Direction group typedef enum { FindDirFwdE, FindDirBackE, FindDirCountE } FindDirE; // Debug state typedef enum { DbgIdleE, // no program loaded DbgRunningE, // program executing DbgPausedE, // stopped at breakpoint/step DbgStoppingE // Stop requested; runModule has not unwound yet } IdeDebugStateE; typedef struct { int32_t fileIdx; // project file index int32_t codeLine; // line within file's code section (1-based) char procName[IDE_FULL_NAME_BUF]; // "obj.evt" combined name } IdeBreakpointT; // Procedure table for Object/Event dropdowns typedef struct { char objName[BAS_MAX_IDENT]; char evtName[BAS_MAX_IDENT]; int32_t lineNum; // line of the SUB / FUNCTION declaration int32_t endLineNum; // line of the END SUB / END FUNCTION } IdeProcEntryT; // A WithEvents variable declared in the file in the code window: an // entry of the Object dropdown whose events are its class's typedef struct { char name[BAS_MAX_IDENT]; char className[BAS_MAX_IDENT]; } IdeWithEventsT; // An Event a class module declares: its name and parameter list text // (the handler skeleton takes the same parameters) typedef struct { char name[BAS_MAX_IDENT]; char params[IDE_EVENT_PARAMS_BUF]; } IdeClassEventT; // Where one line of split source went (see splitProcs) typedef struct { int32_t piece; // IDE_SECTION_GENERAL or an index into the proc array int32_t line; // 0-based line within that piece } IdeLineDestT; // A line pinned to its section while saveCurProc rearranges the buffers: // a line of the edited text (edLine), or a line of a stored section. typedef struct { int32_t edLine; // 0-based line of the edited text, or IDE_NO_LINE const char *section; // owning proc buffer; NULL = (General) int32_t offset; // 0-based line within section, or IDE_NO_LINE (gone) } IdeLineAnchorT; typedef struct { char *key; uint8_t value; } SyntaxMapEntryT; typedef struct { const char *keyword; const char *topic; } HelpMapEntryT; // Row storage for the debug ListViews. Cell text is strdup'd row-major // into strs; cells holds the same pointers in the layout // wgtListViewSetData expects. typedef struct { char **strs; // stb_ds: owned cell text const char **cells; // stb_ds: borrowed pointers into strs } IdeListRowsT; // Compile-time CtrlName.Member validator context typedef struct { char name[BAS_MAX_IDENT]; char wgtType[BAS_MAX_IDENT]; // "Form" for the form itself, else iface basName char formName[BAS_MAX_IDENT]; // form that declares it (a form: its own name) } IdeCtrlMapEntryT; typedef struct { char ctrlName[BAS_MAX_IDENT]; // control that has the bad type char typeName[BAS_MAX_IDENT]; // the unrecognised type name char formName[BAS_MAX_IDENT]; // enclosing form (for error message) } IdeBadTypeT; typedef struct { IdeCtrlMapEntryT *entries; // stb_ds: known controls IdeBadTypeT *badTypes; // stb_ds: unknown widget types encountered const BasCompilerT *compiler; // compile in progress (form scope), or NULL char currentForm[BAS_MAX_IDENT]; // most recent Begin Form } IdeValidatorCtxT; // One .frm parse during compile: the validator map plus the code section typedef struct { IdeValidatorCtxT *validator; // NULL = do not collect controls char *code; // malloc'd code section (NULL = none) } IdeFrmScanT; // A running form's caption saved while " [break]" is appended to it typedef struct { const BasFormT *form; char title[MAX_TITLE_LEN]; } IdeBreakTitleT; // Preferences dialog state (used by several onPrefs* handlers) typedef struct { WindowT *win; // non-NULL while the dialog is open 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 *lblR; WidgetT *lblG; WidgetT *lblB; WidgetT *colorSwatch; uint32_t syntaxColors[TEXT_SYNTAX_MAX]; } IdePrefsDlgT; // ============================================================ // Prototypes // ============================================================ static bool activateFailed(void); static bool activateFile(int32_t fileIdx, IdeViewModeE view); static bool activeIsForm(void); static bool addFileToProject(const char *path); static void anchorCodeLine(IdeLineAnchorT *a, int32_t codeLine, int32_t cur, int32_t edLines, int32_t delta, const int32_t *starts); static void anchorEditedLine(IdeLineAnchorT *a, const IdeLineDestT *map, char **procs, int32_t genBase); static int32_t anchorLine(const IdeLineAnchorT *a, const int32_t *starts); static bool anyFileModified(void); static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, const char *ctrlList, BasSourceMapT *entry); static int32_t appendDroppingLines(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen); static void applySyntaxColors(void); static void basicColorize(const char *line, int32_t lineLen, uint8_t *colors, void *ctx); static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf, int32_t errBufSize, const char *captureName, int32_t *outCaptureIdx); static void buildVmBreakpoints(void); static void buildWindow(void); static void classEventsOf(const char *className, IdeClassEventT **events); static bool classFollowsFile(int32_t fileIdx, const char *text); static uint8_t classifyWord(const char *word, int32_t wordLen, bool afterDot); static bool classNameUsed(const char *name, const char *fileName); static void cleanupFormWin(void); static void clearAllBreakpoints(void); static void clearOutput(void); static void clearVmStepState(void); static void closeFindDialog(void); static void closeProject(void); static void closeProjectUi(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 void createEventSubSkeleton(const char *objName, const char *evtName); static void currentObjName(char *out, int32_t outSize); static uint32_t debugLineDecorator(int32_t lineNum, uint32_t *gutterColor, void *ctx); static void debugNavigateToLine(int32_t concatLine); static void debugResume(const char *status); static void debugSetBreakTitles(bool paused); static void debugStartOrResume(int32_t cmd); static void debugStop(void); static void debugUpdateWindows(void); static bool debugVarVisible(const BasDebugVarT *dv, int32_t curProcIdx); static bool dedupeBreakpoints(void); static void deleteSelectedControl(void); static void designerEditCmd(int32_t cmd); static void designerRelease(void); static bool doEventsCallback(void *ctx); static void dsgnCopySelected(void); static void dsgnPasteControl(void); static int32_t editCmdKey(int32_t cmd); static int32_t editorLineToCodeLine(int32_t editorLine); static void editorSetText(const char *text, int32_t caretLine); static void editorSnapshot(int32_t lineCount); static void ensureProject(const char *filePath); static void ensureProjectWindow(bool raise); static void evaluateImmediate(const char *expr); static bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize); static void evtItemAdd(const char *name, bool bracketed); static void evtItemsReset(void); static bool evtLabelMatches(const char *label, const char *evtName); static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needleLen, bool caseSensitive); static const BasDebugVarT *findDebugVar(const char *name); static bool findDialogBuild(bool showReplace); static bool findInEditorSections(int32_t fromIdx, const char *needle, bool caseSensitive, bool forward, const char *objFilter); static bool findInProject(const char *needle, bool caseSensitive, bool forward, FindScopeE scope); static int32_t findMemberVarIdx(const BasObjectT *obj, const char *name); static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen); static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName); static const IdeWithEventsT *findWithEvents(const char *name); static void flushOutput(void); static void formatValue(const BasValueT *v, char *buf, int32_t bufSize); static void freeProcBufs(void); static void frmScan(const char *frmText, IdeFrmScanT *scan); static void frmScanOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen); static char *generalAppend(const char *general, const char *text); static BasValueT *getDebugVarSlot(const BasDebugVarT *dv); static const char *getEventParams(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 hasProject(void); 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); static void ideErrorBox(const char *msg); static void idePump(void); static bool immEvalRhs(const char *rhs, uint8_t targetType, BasValueT *outVal); static void immPrintCallback(void *ctx, const char *text, bool newline); static void immReportSnippetResult(BasVmT *vm, BasVmResultE result); static bool immTryAssign(const char *expr); static void initSyntaxMap(void); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); static const char *instanceTypeName(const BasObjectT *obj); static void invalidateCompiled(void); static bool isCtrlArrayInDesigner(const char *ctrlName); static bool isInStringOrComment(const char *src, int32_t i); static bool isMangledDebugName(const char *name); static bool isRemAt(const char *line, int32_t pos, int32_t lineLen); static bool isReplaceEnabled(void); static char *joinProcArrays(const char *general, char **procs); static const char *joinProcBufs(void); static bool kwMatch(const char *p, const char *kw); static int32_t lineCopy(const char *text, int32_t line, char *out, int32_t outSize); static int32_t lineStartOffset(const char *text, int32_t line); static void listRowsAdd(IdeListRowsT *rows, const char *text); static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount); static void listRowsFree(IdeListRowsT *rows); static void listRowsReset(IdeListRowsT *rows); static char *loadCompileSource(int32_t fileIdx, IdeValidatorCtxT *validator, char **outCtrlList); static bool loadDesignerFile(int32_t fileIdx); static void loadFile(void); static void loadFormCodeIntoEditor(void); static void loadFrmFiles(BasFormRtT *rt); static void loadModuleIntoEditor(int32_t fileIdx, const char *source); static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText); static int32_t localToConcatLine(int32_t editorLine); static void logFlushLine(void); static bool lookupWatchVar(const char *expr, BasValueT *outVal); static void makeExecutable(void); static BasValueT *memberVarSlot(const BasDebugVarT *dv); static void navigateToBreakpoint(int32_t bpIdx); static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbgLine); static void navigateToEventSub(void); static void navigateToNamedEventSub(const char *ctrlName, const char *eventName); static void newClassModule(void); static void newProject(void); static const char *objectClassName(const BasObjectT *obj); 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 onColorChange(WidgetT *w); static void onColorListChange(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 onImmediateKeyDown(WidgetT *w, int32_t keyCode, int32_t shift); static void onImmWinClose(WindowT *win); static void onLocalsClose(WindowT *win); static void onMenu(WindowT *win, int32_t menuId); static void onObjDropdownChange(WidgetT *w); static void onOutWinClose(WindowT *win); static void onPrefsCancel(WidgetT *w); static void onPrefsClose(WindowT *win); static void onPrefsOk(WidgetT *w); static void onPrjFileDblClick(int32_t fileIdx); 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 openAnyPath(const char *path); static void openFindDialog(bool showReplace); static void openProject(void); static void openProjectPath(const char *path); static void openSinglePath(const char *path); static void outputAppend(const char *text, int32_t len); static void outputLog(const char *text); static char *packProcLayout(const char *source); static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount); static void parseProcs(const char *source); static DsgnControlT *pasteParseControl(const char *clip, int32_t clipLen); static bool prefsBuild(WindowT *win); static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value); static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLen, const char *value); static void prefsUpdateColorSwatch(void); static void printCallback(void *ctx, const char *text, bool newline); static bool procBodyEmpty(const char *procText); static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive); static const char *procDeclAt(const char *p, bool *outIsSub); static int32_t procIndexByName(const char *objName, const char *evtName); static int32_t procIndexForCodeLine(int32_t codeLine); static void procNameFromDecl(const char *afterKw, char *out, int32_t outSize); static void procNameOf(const char *procText, char *out, int32_t outSize); static int32_t *procStartLines(const char *general, char **procs); static const char *projectFileText(int32_t fileIdx, char **outOwned, bool *outMissing); static bool promptAndSave(void); static void rebuildProcTable(void); static void recentAdd(const char *path); static void recentLoad(void); static void recentOpen(int32_t index); static void recentRebuildMenu(void); static void recentRemove(const char *path); static void recentSave(void); static void remapBreakpoints(const IdeLineAnchorT *anchors, const int32_t *starts); static void removeBreakpointsForFile(int32_t fileIdx); static char *renameInBuffer(const char *src, const char *oldName, const char *newName); static bool renameMatchAt(const char *src, int32_t i, const char *oldName, int32_t oldLen, bool skipComments); static void renameMenuItemsInCode(char **oldNames, int32_t oldCount); static void renameProjectFile(int32_t fileIdx); static bool renameProjectFileTo(int32_t fileIdx, const char *newName); static int32_t replaceInSections(const char *objFilter, bool caseSensitive); static void reportUnreadable(const char *path); static BasValueT *resolveVarPath(const char *path, const char **endPtr); static void runCached(void); static void runModule(BasModuleT *mod); static BasVmResultE runProgramSlices(BasVmT *vm); static BasVmResultE runSnippetVm(BasVmT *vm); static bool saveActiveFile(void); static bool saveAllModified(void); static bool saveCurProc(bool discardSkeleton); static void saveFile(void); static bool saveProjectFile(void); static void scanClassEvents(const char *text, IdeClassEventT **events); static void scanWithEvents(const char *src); static bool sectionInScope(int32_t p, const char *objFilter); static int32_t sectionLineCount(const char *text); static void selectDropdowns(const char *objName, const char *evtName); static void setBreakpointProcName(IdeBreakpointT *bp, int32_t procIdx); static bool setClassNameAttr(int32_t fileIdx, const char *name); static void setOutputText(const char *text); static void setStatus(const char *text); static void shiftBreakpointsForEdit(const char *oldText, const char *newText, int32_t delta); static void showBreakpointWindow(void); static void showCallStackWindow(void); static void showCodeView(int32_t fileIdx); static void showCodeWindow(void); static void showCompileError(const char *status); 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, bool forward); static void showWatchWindow(void); static const char *skipLine(const char *p); static const char *skipToEndProc(const char *declLine, bool isSub, int32_t *outEndLineOffset); static bool splitProcs(const char *source, char **outGeneral, char ***outProcs, IdeLineDestT **outMap); static bool stashCurrentFile(void); static void stashEditor(void); static void stashFormCode(void); static void switchToDesign(void); static void syncFormDirty(void); static void syncVmBreakpoints(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 updateMainTitle(void); static void updateProjectMenuState(void); static void updateWatchWindow(void); static void validatorFree(IdeValidatorCtxT *ctx); static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *methodName); static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *propName); static const char *validatorLookupCtrlType(void *ctx, const char *ctrlName); static void validatorOnCtrlBegin(void *ud, const char *typeName, const char *name); static bool validatorOnFormBegin(void *ud, const char *name); static void validatorOnMenuBegin(void *ud, const char *name, int32_t level); static int32_t vmCurProcIdx(void); static int32_t vmProcIndexForPc(int32_t pc); static const char *vmProcNameForPc(int32_t pc); static void watchEditSelected(void); static void watchPrintCallback(void *ctx, const char *text, bool newline); static bool writeProjectFile(int32_t idx); int32_t appMain(DxeAppContextT *ctx); // ============================================================ // Keyword-to-topic lookup for context-sensitive help (F1) // ============================================================ 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"}, {"Like", "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"}, {"Err", "lang.misc"}, {"Error", "lang.misc"}, {"Error$", "lang.misc"}, {"Load", "lang.misc"}, {"Shell", "lang.misc"}, {"Sleep", "lang.misc"}, {"Unload", "lang.misc"}, // Sound, joysticks and frames {"Beep", "lang.sound"}, {"FrameWait", "lang.sound"}, {"Play", "lang.sound"}, {"Sound", "lang.sound"}, {"Stick", "lang.sound"}, {"Strig", "lang.sound"}, // Console and graphics {"Circle", "lang.screen"}, {"Cls", "lang.screen"}, {"Color", "lang.screen"}, {"CsrLin", "lang.screen"}, {"Draw", "lang.screen"}, {"FullScreen", "lang.screen"}, {"InKey$", "lang.screen"}, {"Locate", "lang.screen"}, {"Paint", "lang.screen"}, {"Palette", "lang.screen"}, {"PCopy", "lang.screen"}, {"Pen", "lang.screen"}, {"PMap", "lang.screen"}, {"Point", "lang.screen"}, {"Pos", "lang.screen"}, {"PReset", "lang.screen"}, {"PSet", "lang.screen"}, {"Screen", "lang.screen"}, {"VarPtr$", "lang.screen"}, {"View", "lang.screen"}, {"Width", "lang.screen"}, {"Window", "lang.screen"}, // File I/O {"Close", "lang.fileio"}, {"Eof", "lang.func.fileio"}, {"FileAttr", "lang.func.fileio"}, {"FreeFile", "lang.func.fileio"}, {"Loc", "lang.func.fileio"}, {"Lof", "lang.func.fileio"}, {"Open", "lang.fileio"}, {"Reset", "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"}, {"LoadPicture", "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"}, // Variants {"Array", "lang.variants"}, {"CVar", "lang.variants"}, {"Empty", "lang.variants"}, {"IsArray", "lang.variants"}, {"IsEmpty", "lang.variants"}, {"IsMissing", "lang.procedures"}, {"IsNull", "lang.variants"}, {"IsNumeric", "lang.variants"}, {"IsObject", "lang.variants"}, {"Null", "lang.variants"}, {"TypeName", "lang.variants"}, {"VarType", "lang.variants"}, {"Variant", "lang.variants"}, // Objects and class modules {"Class_Initialize", "lang.classes"}, {"Class_Terminate", "lang.classes"}, {"Event", "lang.events"}, {"Friend", "lang.classes"}, {"Implements", "lang.interfaces"}, {"Is", "lang.classes"}, {"New", "lang.classes"}, {"Nothing", "lang.classes"}, {"Private", "lang.classes"}, {"Property", "lang.classes"}, {"Public", "lang.classes"}, {"RaiseEvent", "lang.events"}, {"Set", "lang.classes"}, {"TypeOf", "lang.classes"}, {"With", "lang.classes"}, {"WithEvents", "lang.events"}, // Collections {"Collection", "lang.collection"}, {"Each", "lang.collection"}, // Form/control statements {"Me", "lang.forms"}, }; #define HELP_MAP_COUNT DVX_ARRAY_LEN(sHelpMap) // ============================================================ // 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. // // The canonical text of a file is joinProcArrays(sGeneralBuf, sProcBufs): // the (General) section, a blank line, then each procedure separated by // one blank line. Everything that reads line numbers (sProcTable, // breakpoints, the compiler's source map) works in that layout, and // compileProject normalizes every file into it, so there is exactly one // line-number space. static char *sGeneralBuf = NULL; // (General) section: module-level code static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure static char *sFullSourceCache = NULL; // last joinProcBufs() result (owned here) static int32_t sCurProcIdx = IDE_SECTION_NONE; // which buffer is in the editor (or IDE_SECTION_GENERAL / _NONE) static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none) static int32_t sDesignerFileIdx = -1; // which project file owns sDesigner.form (-1=none) static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit static char *sEditorPrevText = NULL; // editor text before the latest edit (malloc'd) static bool sEditorTruncated = false; // section exceeds the editor: shown read-only static char sSkeletonName[IDE_FULL_NAME_BUF] = ""; // untouched generated event Sub // Find/Replace state static char sFindText[IDE_FIND_TEXT_BUF] = ""; static char sReplaceText[IDE_FIND_TEXT_BUF] = ""; // 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 indexed by FindScopeE static WidgetT *sDirGroup = NULL; // radio group indexed by FindDirE static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array (rebuilt by joinProcBufs) static const char **sObjItems = NULL; // stb_ds dynamic array of owned (strdup'd) names static const char **sEvtItems = NULL; // stb_ds dynamic array of owned (strdup'd) labels static IdeWithEventsT *sWithEvents = NULL; // stb_ds: the editor file's WithEvents variables (updateDropdowns) static bool sDropdownNavSuppressed = false; static bool sStopRequested = false; static bool sOutputToLog = false; static bool sOutputPending = false; // sOutputBuf not yet shown static char sLogLine[IDE_LOG_LINE_BUF]; // PRINT text not yet logged static int32_t sLogLineLen = 0; // 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 static IdeDebugStateE sDbgState = DbgIdleE; static IdeBreakTitleT *sBreakTitles = NULL; // stb_ds: captions tagged " [break]" static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array 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 // Per-refresh display rows for the debug ListViews static IdeListRowsT sBpRows; static IdeListRowsT sLocalsRows; static IdeListRowsT sCallRows; static IdeListRowsT sWatchRows; // ============================================================ // Syntax highlighting tables (shared by several functions) // ============================================================ static SyntaxMapEntryT *sSyntaxMap = NULL; // Color entry names for the Colors tab (matches TEXT_SYNTAX_* indices) static const char *sSyntaxColorNames[TEXT_SYNTAX_MAX] = { [TEXT_SYNTAX_DEFAULT] = "Default Text", [TEXT_SYNTAX_KEYWORD] = "Keywords", [TEXT_SYNTAX_STRING] = "Strings", [TEXT_SYNTAX_COMMENT] = "Comments", [TEXT_SYNTAX_NUMBER] = "Numbers", [TEXT_SYNTAX_OPERATOR] = "Operators", [TEXT_SYNTAX_TYPE] = "Types", }; static IdePrefsDlgT sPrefsDlg; // Watch-expression scratch buffer (used by evalWatchExpr + watchPrintCallback) static char sWatchPrintBuf[IDE_VALUE_BUF]; static int32_t sWatchPrintLen; // ============================================================ // Event table for the Object/Event dropdowns and handler skeletons // ============================================================ // // Expanded from the master BAS_EVENT_LIST in compiler/basEvents.h so the // dropdowns, the generated parameter lists and the runtime dispatcher // can never disagree. static const BasEventInfoT sEventTable[] = { BAS_EVENT_LIST(BAS_EVENT_ROW) }; #define IDE_EVENT_COUNT DVX_ARRAY_LEN(sEventTable) // ============================================================ // App descriptor // ============================================================ SHELL_APP_DESCRIPTOR( .name = "DVX BASIC", .hasMainLoop = false, .multiInstance = false, .stackSize = SHELL_STACK_DEFAULT, .priority = 0 ); // activateFailed -- the switch did not happen: when the skeleton the // editor showed was dropped on the way out, show the outgoing file's // (General) so the editor is never left bound to no section. Returns // false for activateFile to pass on. static bool activateFailed(void) { if (sCurProcIdx == IDE_SECTION_NONE && sEditorFileIdx >= 0) { updateDropdowns(); showProc(IDE_SECTION_GENERAL); } return false; } // activateFile -- make fileIdx the active file and show it in the given // view. Returns false (leaving the current file active) when the // outgoing file cannot be stashed or the incoming one cannot be loaded. static bool activateFile(int32_t fileIdx, IdeViewModeE view) { if (fileIdx < 0 || fileIdx >= prjFileCount(&sProject)) { return false; } PrjFileT *target = &sProject.files[fileIdx]; // Resolve ViewAutoE if (view == ViewAutoE) { view = target->kind == PrjKindFormE ? ViewDesignE : ViewCodeE; } // If already active, just ensure the right view is showing if (fileIdx == sProject.activeFileIdx) { if (view == ViewDesignE) { switchToDesign(); } else { showCodeView(fileIdx); } return true; } // PHASE 1: Stash the outgoing file. The editor is being rebound, so an // untouched event skeleton is dropped first. A failed stash keeps the // current file active rather than throwing its edits away. saveCurProc(true); if (!stashCurrentFile()) { return activateFailed(); } // PHASE 2: Load the incoming file if (target->kind == PrjKindFormE) { // A form already in the designer keeps its live (possibly unsaved) state if ((fileIdx != sDesignerFileIdx || !sDesigner.form) && !loadDesignerFile(fileIdx)) { return activateFailed(); } sProject.activeFileIdx = fileIdx; if (view == ViewDesignE) { switchToDesign(); // The editor must never stay bound to the previous file: an open // code window now shows this form's code, otherwise the stale // proc buffers are released so nothing can be edited into limbo. if (sCodeWin) { loadFormCodeIntoEditor(); } else { freeProcBufs(); } } else { loadFormCodeIntoEditor(); } } else { bool missing = false; char *owned = NULL; const char *source = projectFileText(fileIdx, &owned, &missing); if (!source && !missing) { reportUnreadable(target->path); return activateFailed(); } if (!sCodeWin) { showCodeWindow(); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } // Only a file that does not exist yet starts out empty (and unsaved) if (!source) { target->modified = true; } sProject.activeFileIdx = fileIdx; loadModuleIntoEditor(fileIdx, source ? source : ""); free(owned); } updateProjectMenuState(); updateDirtyIndicators(); return true; } // activeIsForm -- true when the active project file is a .frm static bool activeIsForm(void) { return sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject) && sProject.files[sProject.activeFileIdx].kind == PrjKindFormE; } // Adds path to the open project (or activates it when already listed). // Returns false when the file lives outside the project directory -- // PrjFileT.path is project-relative, so such a file could never be // read back and the next save would silently create an empty copy -- // or when it exists but cannot be loaded. static bool addFileToProject(const char *path) { const char *fileName = platformPathBaseName(path); char dir[DVX_MAX_PATH]; snprintf(dir, sizeof(dir), "%s", path); char *sep = platformPathDirEnd(dir); if (sep) { *sep = '\0'; } else { dir[0] = '.'; dir[1] = '\0'; } if (strcasecmp(dir, sProject.projectDir) != 0) { ideErrorBox("Files must be located in the project directory."); return false; } for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (strcasecmp(sProject.files[i].path, fileName) == 0) { return activateFile(i, ViewAutoE); } } bool wasDirty = sProject.dirty; prjAddFile(&sProject, fileName, prjKindForPath(path)); // A file that exists but cannot be loaded is not kept: its empty // stand-in would overwrite the real file on the next save. if (!activateFile(prjFileCount(&sProject) - 1, ViewAutoE)) { prjRemoveFile(&sProject, prjFileCount(&sProject) - 1); sProject.dirty = wasDirty; return false; } invalidateCompiled(); prjRebuildTree(&sProject); return true; } // anchorCodeLine -- pin whole-file line codeLine of the editor's file to // the section holding it. The layout is the stored buffers with section // cur replaced by the edited text: its edLines lines start at cur's // start, and every later section was moved by delta while it was edited // (starts: procStartLines of the stored buffers). static void anchorCodeLine(IdeLineAnchorT *a, int32_t codeLine, int32_t cur, int32_t edLines, int32_t delta, const int32_t *starts) { int32_t curStart = cur == IDE_SECTION_GENERAL ? 1 : starts[cur]; a->edLine = IDE_NO_LINE; a->section = NULL; a->offset = IDE_NO_LINE; if (codeLine >= curStart && codeLine < curStart + edLines) { a->edLine = codeLine - curStart; return; } if (codeLine >= curStart + edLines) { codeLine -= delta; } if (cur != IDE_SECTION_GENERAL && codeLine >= 1 && codeLine <= sectionLineCount(sGeneralBuf)) { a->offset = codeLine - 1; return; } for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) { if (p != cur && codeLine >= starts[p] && codeLine < starts[p] + sectionLineCount(sProcBufs[p])) { a->section = sProcBufs[p]; a->offset = codeLine - starts[p]; return; } } } // anchorEditedLine -- pin a line of the edited text (a->edLine) to where // splitProcs sent it: a proc buffer, or (General) at genBase plus its // line in the General piece. A line that was not stored (a discarded // skeleton's, whose procs[] entry is NULL) is gone. static void anchorEditedLine(IdeLineAnchorT *a, const IdeLineDestT *map, char **procs, int32_t genBase) { a->section = NULL; a->offset = IDE_NO_LINE; if (a->edLine < 0 || a->edLine >= (int32_t)arrlen(map)) { return; } const IdeLineDestT *d = &map[a->edLine]; if (d->piece == IDE_SECTION_GENERAL) { a->offset = genBase + d->line; } else if (procs[d->piece]) { a->section = procs[d->piece]; a->offset = d->line; } } // anchorLine -- whole-file line of an anchor in the current layout // (starts: procStartLines of the current buffers), or IDE_NO_LINE when // its line no longer exists static int32_t anchorLine(const IdeLineAnchorT *a, const int32_t *starts) { if (a->offset < 0) { return IDE_NO_LINE; } if (!a->section) { return a->offset < sectionLineCount(sGeneralBuf) ? a->offset + 1 : IDE_NO_LINE; } for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) { if (sProcBufs[p] == a->section) { return a->offset < sectionLineCount(a->section) ? starts[p] + a->offset : IDE_NO_LINE; } } return IDE_NO_LINE; } // anyFileModified -- whether any project file (or the designer's form) // has unsaved edits; the one rule for Save All, the title and the exit // prompt (which also counts the .dbp itself) static bool anyFileModified(void) { bool modified = sDesigner.form && sDesigner.form->dirty; for (int32_t i = 0; i < prjFileCount(&sProject) && !modified; i++) { modified = sProject.files[i].modified; } return modified; } // appendCompileFile -- append project file fileIdx's compile text (text, // len bytes: its canonical layout, a form's code section only) to ps the // way basBuildConcatProject does: a form's code wrapped in BEGINFORM with // its controls (ctrlList, see basFrmControlList), a class module's code // after its header wrapped in BEGINCLASS, a module in BEGINMODULE. Fills // entry's line fields; false with ps->error set on failure. static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, const char *ctrlList, BasSourceMapT *entry) { PrjFileT *file = &sProject.files[fileIdx]; const char *name = ""; BasBuildWrapE wrap = BAS_BUILD_WRAP_MODULE; int32_t codePos = 0; char className[BAS_MAX_IDENT]; entry->firstFileLine = 1; if (file->kind == PrjKindFormE) { name = file->formName; wrap = BAS_BUILD_WRAP_FORM; } else if (file->kind == PrjKindClassE) { if (!prjClassName(&sProject, fileIdx, text, className, (int32_t)sizeof(className))) { snprintf(ps->error, sizeof(ps->error), "No class name for %s", file->path); return false; } codePos = prjClassCodePos(text, len, &entry->firstFileLine); name = className; wrap = BAS_BUILD_WRAP_CLASS; } return basBuildAppendFile(ps, text + codePos, len - codePos, wrap, name, ctrlList, &entry->startLine, &entry->lineCount); } // appendDroppingLines -- append text to a NUL-terminated buffer of cap // bytes holding len bytes, dropping whole leading lines while it would // not fit so the newest text always survives (an oversize chunk keeps // its tail). Returns the new length. static int32_t appendDroppingLines(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen) { if (textLen > cap - 1) { text += textLen - (cap - 1); textLen = cap - 1; } int32_t drop = 0; int32_t keep = len; while (keep + textLen > cap - 1) { const char *nl = memchr(buf + drop, '\n', (size_t)keep); if (!nl) { drop += keep; keep = 0; break; } int32_t cut = (int32_t)(nl + 1 - (buf + drop)); drop += cut; keep -= cut; } memmove(buf, buf + drop, (size_t)keep); memcpy(buf + keep, text, (size_t)textLen); keep += textLen; buf[keep] = '\0'; return keep; } static void applySyntaxColors(void) { if (!sEditor) { return; } wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, TEXT_SYNTAX_MAX); } 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: ' (REM is handled with the identifiers) if (ch == '\'') { while (i < lineLen) { colors[i++] = TEXT_SYNTAX_COMMENT; } return; } // String literal if (ch == '"') { colors[i++] = TEXT_SYNTAX_STRING; while (i < lineLen && line[i] != '"') { colors[i++] = TEXT_SYNTAX_STRING; } if (i < lineLen) { colors[i++] = TEXT_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++] = TEXT_SYNTAX_NUMBER; } continue; } // Date literal #...#, colored as a number (the compiler's rule: // other '#'s are file numbers) double serial; int32_t dateLen = (ch == '#') ? basLexerDateLiteralLen(line + i, lineLen - i, &serial) : 0; if (dateLen > 0) { memset(colors + i, TEXT_SYNTAX_NUMBER, (size_t)dateLen); i += dateLen; continue; } // Identifier or keyword if (isalpha((unsigned char)ch) || ch == '_') { int32_t start = i; while (i < lineLen && basIsIdentChar(line[i])) { i++; } // A type suffix (Dir$, Len%) is colored with its word but is // not part of the keyword lookup. int32_t wordLen = i - start; if (i < lineLen && basIsTypeSuffixChar(line[i])) { i++; } if (isRemAt(line, start, lineLen)) { for (int32_t j = start; j < lineLen; j++) { colors[j] = TEXT_SYNTAX_COMMENT; } return; } uint8_t c = classifyWord(line + start, wordLen, start > 0 && line[start - 1] == '.'); 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++] = TEXT_SYNTAX_OPERATOR; continue; } // Default (whitespace, parens, etc.) colors[i++] = TEXT_SYNTAX_DEFAULT; } } // buildSnippetVm -- compile a one-line snippet and return a VM ready to // run it. When paused in the debugger, the paused VM's variables are // added to the snippet's symbol table (as globals) and their current // values copied in, so expressions can reference them by name. When // captureName is given, *outCaptureIdx receives the global slot of that // snippet variable (-1 when it does not exist). On failure returns NULL // and copies the error text into errBuf. static BasVmT *buildSnippetVm(const char *src, BasModuleT **outMod, char *errBuf, int32_t errBufSize, const char *captureName, int32_t *outCaptureIdx) { *outMod = NULL; errBuf[0] = '\0'; BasCompilerT *compiler = basCompilerCreate(src, (int32_t)strlen(src)); if (!compiler) { snprintf(errBuf, errBufSize, "Out of memory"); return NULL; } // 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; bool paused = (sDbgState == DbgPausedE && sVm && sDbgModule && sDbgModule->debugVars); if (paused) { int32_t curProcIdx = vmCurProcIdx(); for (int32_t i = 0; i < sDbgModule->debugVarCount && varMapCount < BAS_VM_MAX_GLOBALS; i++) { const BasDebugVarT *dv = &sDbgModule->debugVars[i]; if (!debugVarVisible(dv, curProcIdx) || isMangledDebugName(dv->name)) { continue; } // The snippet's globals, in order: slot varMapCount if (basCompilerDeclareGlobal(compiler, dv->name, dv->dataType)) { varMap[varMapCount] = i; varMapCount++; } } } if (!basCompile(compiler)) { snprintf(errBuf, errBufSize, "%s", basCompilerError(compiler)); basCompilerDestroy(compiler); return NULL; } if (captureName) { *outCaptureIdx = basCompilerGlobalSlot(compiler, captureName); } BasModuleT *mod = basCompilerBuildModule(compiler); basCompilerDestroy(compiler); if (!mod) { snprintf(errBuf, errBufSize, "Failed to build module"); return NULL; } BasVmT *vm = basVmCreate(); if (!vm) { basModuleFree(mod); snprintf(errBuf, errBufSize, "Out of memory"); return NULL; } basVmLoadModule(vm, mod); // Copy values from the paused VM into the temp VM's globals for (int32_t g = 0; g < varMapCount; g++) { BasValueT *slot = getDebugVarSlot(&sDbgModule->debugVars[varMap[g]]); if (slot) { vm->globals[g] = basValCopy(*slot); } } *outMod = mod; return vm; } // ============================================================ // 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 < (int32_t)arrlen(sBreakpoints); i++) { int32_t concatLine = prjMapFileLine(&sProject, sBreakpoints[i].fileIdx, sBreakpoints[i].codeLine); if (concatLine > 0) { arrput(sVmBreakpoints, concatLine); } } } static void buildWindow(void) { // ---- Main toolbar window (top of screen) ---- sWin = dvxCreateWindow(sAc, IDE_MAIN_TITLE, 0, 0, sAc->display.width, IDE_TOOLBAR_INIT_H, 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, "&New Class Module", CMD_PRJ_NEW_CLASS); 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, "R&ename File...", CMD_PRJ_RENAME); 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, KEY_ESCAPE, 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; // Button opens the project; Ctrl+O is bound to Add File, so the tooltip // must not advertise a shortcut the button doesn't perform. wgtSetTooltip(tbOpen, "Open Project..."); WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save"); tbSave->onClick = onTbSave; wgtSetTooltip(tbSave, "Save (Ctrl+S)"); WidgetT *sep1 = wgtVSeparator(tb); sep1->minW = wgtPixels(IDE_TOOLBAR_SEP_W); // 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(IDE_TOOLBAR_SEP_W); // 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(IDE_TOOLBAR_SEP_W); // 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 = WGT_WEIGHT_FILL; // 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(); dvxRaiseWindow(sAc, sWin); } // classEventsOf -- the Events class module className declares (none when // the project has no such class), appended to *events static void classEventsOf(const char *className, IdeClassEventT **events) { for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].kind != PrjKindClassE) { continue; } bool missing = false; char *owned = NULL; const char *text = (i == sEditorFileIdx) ? getFullSource() : projectFileText(i, &owned, &missing); char name[BAS_MAX_IDENT]; bool match = text && prjClassName(&sProject, i, text, name, (int32_t)sizeof(name)) && strcasecmp(name, className) == 0; if (match) { scanClassEvents(text, events); } free(owned); if (match) { return; } } } // classFollowsFile -- whether class module fileIdx (whose text is given) // is named after its file: it has no Attribute VB_Name, or one equal to // the file's base name (as File > New Class Module writes it) static bool classFollowsFile(int32_t fileIdx, const char *text) { char attrName[BAS_MAX_IDENT]; char fileName[BAS_MAX_IDENT]; basExtractClassName("", 0, sProject.files[fileIdx].path, fileName, (int32_t)sizeof(fileName)); return !basExtractClassName(text, (int32_t)strlen(text), NULL, attrName, (int32_t)sizeof(attrName)) || strcasecmp(attrName, fileName) == 0; } // classifyWord -- the syntax color of a word. afterDot: it names a // member (obj.Width), where only reserved words keep their color. static uint8_t classifyWord(const char *word, int32_t wordLen, bool afterDot) { char upper[IDE_WORD_BUF]; if (wordLen <= 0 || wordLen >= IDE_WORD_BUF) { return TEXT_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 || (afterDot && (sSyntaxMap[idx].value & IDE_SYNTAX_UNRESERVED))) { return TEXT_SYNTAX_DEFAULT; } return sSyntaxMap[idx].value & (uint8_t)~IDE_SYNTAX_UNRESERVED; } // classNameUsed -- true when a project file is named fileName or a form // or class module of the project is called name static bool classNameUsed(const char *name, const char *fileName) { bool used = false; for (int32_t i = 0; i < prjFileCount(&sProject) && !used; i++) { PrjFileT *file = &sProject.files[i]; used = strcasecmp(file->path, fileName) == 0 || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0); if (!used && file->kind == PrjKindClassE) { bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); char other[BAS_MAX_IDENT]; used = prjClassName(&sProject, i, text, other, (int32_t)sizeof(other)) && strcasecmp(other, name) == 0; free(owned); } } return used; } // 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; } } static void clearAllBreakpoints(void) { arrsetlen(sBreakpoints, 0); // A live VM still points at sVmBreakpoints; detach before freeing. if (sVm) { basVmSetBreakpoints(sVm, NULL, 0); } arrfree(sVmBreakpoints); sVmBreakpoints = NULL; updateBreakpointWindow(); } static void clearOutput(void) { sOutputBuf[0] = '\0'; sOutputLen = 0; sOutputPending = false; setOutputText(""); } // clearVmStepState -- cancel any pending step/run-to-cursor request static void clearVmStepState(void) { if (!sVm) { return; } sVm->debugBreak = false; sVm->stepOverDepth = -1; sVm->stepOutDepth = -1; sVm->runToCursorLine = -1; } 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; } } // closeProject -- callers prompt for unsaved edits first (promptAndSave), // which saves on Yes. Nothing is written here, so Don't Save discards // changes to the .dbp as well as to the files. static void closeProject(void) { if (!hasProject()) { return; } closeFindDialog(); closeProjectUi(); invalidateCompiled(); prjClose(&sProject); updateMainTitle(); setStatus("Project closed."); updateProjectMenuState(); } // closeProjectUi -- destroy every per-project window and buffer. Safe to // call when nothing is open (every step checks its own state). static void closeProjectUi(void) { designerRelease(); // Toolbox/properties can exist without a form window (Window menu) if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; } if (sPropsWin) { prpDestroy(sAc, sPropsWin); sPropsWin = NULL; } if (sCodeWin) { dvxDestroyWindow(sAc, sCodeWin); sCodeWin = NULL; sEditor = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; } freeProcBufs(); clearAllBreakpoints(); if (sProjectWin) { prjDestroyWindow(sAc, sProjectWin); sProjectWin = NULL; } } // 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) { // A stopped program that has not unwound yet still runs sCachedModule if (sVm) { return; } 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)) { saveAllModified(); } // Stash editor/designer state into the project buffers (the compile // source) before going busy: a stash failure shows a dialog, and no // dialog can be answered while the busy state swallows input. if (hasProject() && !stashCurrentFile()) { setStatus("Compilation cancelled."); return false; } clearOutput(); setStatus("Compiling..."); dvxSetBusy(sAc, true); dvxUpdate(sAc); // Build source: either concatenate project files or use editor contents BasProjectSourceT project; IdeValidatorCtxT validatorCtx; const char *src = NULL; int32_t srcLen = 0; memset(&project, 0, sizeof(project)); memset(&validatorCtx, 0, sizeof(validatorCtx)); if (hasProject() && prjFileCount(&sProject) > 0) { // Concatenate all files (each normalized to the canonical proc // layout) with the same BEGINFORM/ENDFORM wrapping bascomp and // basrun use, recording where each file lands for the source map. // The one parse of each .frm also builds the name -> widget-type // map the validator below checks CtrlName.Member against. arrfree(sProject.sourceMap); sProject.sourceMap = NULL; // One pass per file kind in compile order: .bas modules first (so // CONST declarations are available), then .cls class modules, // then .frm code sections (see basBuildConcatProject). for (int32_t kind = 0; kind < PrjKindCountE; kind++) { for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].kind != kind) { continue; } char *ctrlList = NULL; char *fileSrc = loadCompileSource(i, &validatorCtx, &ctrlList); if (!fileSrc) { sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[i].path, (int)IDE_MAX_SOURCE); showCompileError("Compilation failed: cannot read file."); basBuildFreeProject(&project); validatorFree(&validatorCtx); return false; } int32_t fileLen = (int32_t)strlen(fileSrc); // BEGINFORM/ENDFORM directives plus a trailing newline if (project.sourceLen + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) { sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[i].path); showCompileError("Compilation failed: source too large."); free(fileSrc); basBuildFreeProject(&project); validatorFree(&validatorCtx); return false; } // startLine is recorded AFTER the injected BEGINFORM / // BEGINCLASS so the source map lines match what the editor // shows. BasSourceMapT mapEntry; memset(&mapEntry, 0, sizeof(mapEntry)); snprintf(mapEntry.path, sizeof(mapEntry.path), "%s", sProject.files[i].path); bool ok = appendCompileFile(&project, i, fileSrc, fileLen, ctrlList, &mapEntry); free(fileSrc); free(ctrlList); if (!ok) { setStatus(project.error); dvxSetBusy(sAc, false); basBuildFreeProject(&project); validatorFree(&validatorCtx); return false; } arrput(sProject.sourceMap, mapEntry); dvxUpdate(sAc); } } src = project.source; srcLen = project.sourceLen; } 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); } // Reject unknown widget types up front -- the .frm parse records // any `Begin ` whose type isn't registered by a .wgt // DXE. Those would silently drop the control at runtime and // produce a misleading "control not found" error on first access. if (arrlen(validatorCtx.badTypes) > 0) { int32_t n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n"); for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - IDE_OUTPUT_LINE_RESERVE; i++) { IdeBadTypeT *b = &validatorCtx.badTypes[i]; n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, " Form '%s' control '%s': type '%s' is not registered.\n", b->formName, b->ctrlName, b->typeName); } n = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, "\nDVX uses VB6-style widget names (e.g. SpinButton, OptionButton, HScrollBar, CheckBox, DropDown).\n"); sOutputLen = n; showCompileError("Compilation failed: unknown widget type."); basBuildFreeProject(&project); validatorFree(&validatorCtx); return false; } BasCompilerT *compiler = basCompilerCreate(src, srcLen); if (!compiler) { basBuildFreeProject(&project); validatorFree(&validatorCtx); setStatus("Out of memory."); dvxSetBusy(sAc, false); return false; } basCompilerSetOptionExplicit(compiler, sProject.optionExplicit); // The validator lets the compiler reject CtrlName.Member typos at compile // time; it falls back silently for dynamically-created controls (not // in the map). BasCtrlValidatorT validator; validator.lookupCtrlType = validatorLookupCtrlType; validator.isMethodValid = validatorIsMethodValid; validator.isPropValid = validatorIsPropValid; validator.ctx = &validatorCtx; validatorCtx.compiler = compiler; basCompilerSetValidator(compiler, &validator); if (!basCompile(compiler)) { // Navigation may raise dialogs (a form that cannot be stashed) dvxSetBusy(sAc, false); // Translate global error line to local file/line for display int32_t errFileIdx = -1; int32_t errLocalLine = basCompilerErrorLine(compiler); const char *errFile = ""; if (basCompilerErrorLine(compiler) > 0 && prjFileCount(&sProject) > 0) { prjMapLine(&sProject, basCompilerErrorLine(compiler), &errFileIdx, &errLocalLine); if (errFileIdx >= 0 && errFileIdx < prjFileCount(&sProject)) { errFile = sProject.files[errFileIdx].path; } } // Navigate to the error location; the proc it lands in names it char procName[IDE_FULL_NAME_BUF] = {0}; int32_t procLine = errLocalLine; if (basCompilerErrorLine(compiler) > 0 && errFileIdx >= 0) { int32_t procIdx = navigateToCodeLine(errFileIdx, errLocalLine, false); // The caret goes to the column the error is at, when known if (basCompilerErrorCol(compiler) > 0 && sEditor) { wgtTextAreaSetCursor(sEditor, wgtTextAreaGetCursorLine(sEditor), basCompilerErrorCol(compiler)); } if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) { snprintf(procName, sizeof(procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName); procLine = wgtTextAreaGetCursorLine(sEditor); } } // Strip the "Line N, Col M: " prefix from the compiler's error const char *msg = basCompilerError(compiler); if (kwMatch(msg, KW_LINE_PREFIX)) { while (*msg && *msg != ':') { msg++; } if (*msg == ':') { msg++; } msg = dvxSkipWs(msg); } // Show the error with procedure name and proc-relative line if (procName[0]) { sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); } else { sOutputLen = emitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); } showCompileError("Compilation failed."); basCompilerDestroy(compiler); basBuildFreeProject(&project); validatorFree(&validatorCtx); return false; } basBuildFreeProject(&project); validatorFree(&validatorCtx); BasModuleT *mod = basCompilerBuildModule(compiler); basCompilerDestroy(compiler); 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; } // createEventSubSkeleton -- append an empty "Sub Obj_Evt ... End Sub" to // the proc buffers and show it in the editor. Not marked dirty yet; // if the user adds no code it is discarded once the editor leaves it. static void createEventSubSkeleton(const char *objName, const char *evtName) { char subName[IDE_SKELETON_BUF]; char skeleton[IDE_SKELETON_BUF]; char classParams[IDE_EVENT_PARAMS_BUF]; const char *params = getEventParams(evtName); const IdeWithEventsT *withEvents = findWithEvents(objName); // A WithEvents variable's handler takes its class Event's parameters if (withEvents) { IdeClassEventT *events = NULL; classEventsOf(withEvents->className, &events); classParams[0] = '\0'; for (int32_t i = 0; i < (int32_t)arrlen(events); i++) { if (strcasecmp(events[i].name, evtName) == 0) { snprintf(classParams, sizeof(classParams), "%s", events[i].params); } } arrfree(events); params = classParams; } snprintf(subName, sizeof(subName), "%s" BAS_EVENT_HANDLER_SEP "%s", objName, evtName); // Control-array handlers take a leading Index argument if (isCtrlArrayInDesigner(objName)) { snprintf(skeleton, sizeof(skeleton), "Sub %s (Index As Integer%s%s)\n\nEnd Sub\n", subName, params[0] ? ", " : "", params); } else { snprintf(skeleton, sizeof(skeleton), "Sub %s (%s)\n\nEnd Sub\n", subName, params); } char *buf = strdup(skeleton); if (!buf) { return; } arrput(sProcBufs, buf); // Show the new procedure (it's the last one); showProc stashes the // outgoing editor text and adjusts the index if that discarded an // earlier empty skeleton. Then refresh sProcTable so breakpoints // and line translation know about the new proc. showProc((int32_t)arrlen(sProcBufs) - 1); joinProcBufs(); snprintf(sSkeletonName, sizeof(sSkeletonName), "%s", subName); } // currentObjName -- the object picked in the code window's Object // dropdown ("" when none) static void currentObjName(char *out, int32_t outSize) { int32_t objIdx = sObjDropdown ? wgtDropdownGetSelected(sObjDropdown) : -1; snprintf(out, (size_t)outSize, "%s", (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) ? sObjItems[objIdx] : ""); } 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 = editorLineToCodeLine(lineNum); int32_t fileIdx = sEditorFileIdx; // Breakpoint: red gutter dot for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { *gutterColor = packColor(&ac->display, IDE_BP_GUTTER_R, IDE_BP_GUTTER_G, IDE_BP_GUTTER_B); break; } } // Current debug line: yellow background (sDbgCurrentLine is editor-local) if (sDbgState == DbgPausedE && lineNum == sDbgCurrentLine) { return packColor(&ac->display, IDE_DBG_LINE_R, IDE_DBG_LINE_G, IDE_DBG_LINE_B); } return 0; } // debugNavigateToLine -- show the paused (or failing) statement. The // file and line come from the source map; the procedure is picked by // line range in that file once it is active. static void debugNavigateToLine(int32_t concatLine) { if (concatLine <= 0) { return; } int32_t fileIdx = -1; int32_t localLine = concatLine; // prjMapLine returns a line relative to the code section (startLine in // the source map is recorded AFTER the injected BEGINFORM directive). prjMapLine(&sProject, concatLine, &fileIdx, &localLine); navigateToCodeLine(fileIdx, localLine, true); } // debugResume -- common tail of every continue/step command: the caller // has already configured the VM's step state. static void debugResume(const char *status) { sDbgCurrentLine = -1; sDbgState = DbgRunningE; debugSetBreakTitles(false); if (sVm) { sVm->debugPaused = false; sVm->running = true; } if (sEditor) { wgtInvalidatePaint(sEditor); } updateProjectMenuState(); setStatus(status); } // debugSetBreakTitles -- tag every running form's caption with " [break]" // while paused, and put the saved captions back on resume. static void debugSetBreakTitles(bool paused) { if (!paused) { for (int32_t t = 0; t < (int32_t)arrlen(sBreakTitles); t++) { for (int32_t i = 0; sDbgFormRt && i < (int32_t)arrlen(sDbgFormRt->forms); i++) { BasFormT *form = sDbgFormRt->forms[i]; if (form == sBreakTitles[t].form && form->window) { dvxSetTitle(sAc, form->window, sBreakTitles[t].title); } } } arrsetlen(sBreakTitles, 0); return; } if (!sDbgFormRt || arrlen(sBreakTitles) > 0) { return; } for (int32_t i = 0; i < (int32_t)arrlen(sDbgFormRt->forms); i++) { BasFormT *form = sDbgFormRt->forms[i]; if (!form->window) { continue; } IdeBreakTitleT saved; char tagged[MAX_TITLE_LEN]; saved.form = form; snprintf(saved.title, sizeof(saved.title), "%s", form->window->title); arrput(sBreakTitles, saved); snprintf(tagged, sizeof(tagged), "%s%s", saved.title, IDE_BREAK_TAG); dvxSetTitle(sAc, form->window, tagged); } } static void debugStartOrResume(int32_t cmd) { if (sDbgState == DbgPausedE && sVm) { // Already paused -- apply the appropriate step command and resume 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; default: break; } debugResume("Running..."); return; } if (sDbgState == DbgIdleE) { // Not running -- compile and start in debug mode. sDbgEnabled = true; sDbgBreakOnStart = true; compileAndRun(); sDbgBreakOnStart = false; } } // debugStop -- request the running program to stop. runModule's loops and // doEventsCallback observe sStopRequested and unwind; until runModule has // returned the state stays DbgStoppingE, so nothing can start a new run // or free the module the unwinding VM is still executing. static void debugStop(void) { sStopRequested = true; // Nothing may pause the VM again while it unwinds clearVmStepState(); if (sVm) { sVm->running = false; sVm->debugPaused = false; basVmSetBreakpoints(sVm, NULL, 0); } debugSetBreakTitles(false); sDbgState = sVm ? DbgStoppingE : DbgIdleE; sDbgCurrentLine = -1; sDbgEnabled = false; if (sEditor) { wgtInvalidatePaint(sEditor); } updateProjectMenuState(); } 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(); } // debugVarVisible -- whether a debug variable is in scope where the VM is // paused: locals of the current proc, module variables, the variables of the // form whose code is running (form variables of different forms share // slot numbers, so the form must match) and the member variables of the // object a class procedure runs for. static bool debugVarVisible(const BasDebugVarT *dv, int32_t curProcIdx) { if (dv->scope == SCOPE_MEMBER) { return memberVarSlot(dv) != NULL; } // Locals and a procedure's SHARED / STATIC names belong to that // procedure; inside a procedure a module variable shows only when it // is DIM SHARED (QBasic scope). if (dv->procIndex >= 0) { if (dv->procIndex != curProcIdx) { return false; } } else if (curProcIdx >= 0 && !dv->shared) { return false; } if (dv->scope != SCOPE_FORM) { return true; } if (!sVm->currentFormVars) { return false; } if (!dv->formName[0]) { return true; } return sVm->currentForm && strcasecmp(dv->formName, ((const BasFormT *)sVm->currentForm)->name) == 0; } // dedupeBreakpoints -- drop a breakpoint that lands on the same line of // the same file as an earlier one (edits can join lines). Returns true // if any was dropped. static bool dedupeBreakpoints(void) { bool changed = false; for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i > 0; i--) { for (int32_t j = 0; j < i; j++) { if (sBreakpoints[j].fileIdx == sBreakpoints[i].fileIdx && sBreakpoints[j].codeLine == sBreakpoints[i].codeLine) { arrdel(sBreakpoints, i); changed = true; break; } } } return changed; } // deleteSelectedControl -- remove the designer's selected control and // refresh the properties panel / form window when something changed. static void deleteSelectedControl(void) { if (!sDesigner.form || sDesigner.selectedIdx < 0) { return; } 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) { sDesigner.form->dirty = true; prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } updateDirtyIndicators(); } } // designerEditCmd -- an Edit menu command applied to the designer's // selected control static void designerEditCmd(int32_t cmd) { switch (cmd) { case CMD_CUT: dsgnCopySelected(); deleteSelectedControl(); break; case CMD_COPY: dsgnCopySelected(); break; case CMD_PASTE: dsgnPasteControl(); break; case CMD_DELETE: deleteSelectedControl(); break; default: break; } } // designerRelease -- close the designer window and unload its form static void designerRelease(void) { teardownFormWin(); dsgnFree(&sDesigner); sDesignerFileIdx = -1; } static bool doEventsCallback(void *ctx) { (void)ctx; // Stop if IDE window was closed, DVX is shutting down, or the user // pressed Stop (the VM's own pause spin only exits on false here). if (!sWin || !sAc->running || sStopRequested) { return false; } idePump(); return sWin != NULL && sAc->running && !sStopRequested; } 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 with the same writer the // .frm saver uses, into a heap buffer sized for all of its lines. const DsgnControlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; int32_t bufSize = dsgnEmitControlSize(ctrl); char *buf = (char *)malloc((size_t)bufSize); if (!buf) { return; } int32_t pos = dsgnEmitControl(ctrl, buf, bufSize, 0, 0); if (pos >= 0) { dvxClipboardCopy(buf, pos); } else { setStatus("A property value of this control is too long to copy."); } free(buf); } // dsgnPasteControl -- paste a control copied with dsgnCopySelected. The // clipboard block is read by the designer's own .frm loader, so every // property (position, Visible, Enabled, HelpTopic, quoted text) comes // back exactly as a saved form would. static void dsgnPasteControl(void) { if (!sDesigner.form || !sDesigner.form->contentBox) { return; } int32_t clipLen = 0; const char *clip = dvxClipboardGet(&clipLen); if (!clip || clipLen <= 0 || !kwMatch(dvxSkipWs(clip), FRM_BEGIN_TAG)) { return; } DsgnControlT *ctrl = pasteParseControl(clip, clipLen); if (!ctrl) { return; } // A control with the same name exists: continue its control array, // offer to start one, or rename the paste char newName[DSGN_MAX_NAME]; int32_t newIndex = DSGN_NO_INDEX; bool nameExists = false; int32_t highIdx = DSGN_NO_INDEX; int32_t existCount = (int32_t)arrlen(sDesigner.form->controls); for (int32_t i = 0; i < existCount; i++) { if (strcasecmp(sDesigner.form->controls[i]->name, ctrl->name) == 0) { nameExists = true; if (sDesigner.form->controls[i]->index > highIdx) { highIdx = sDesigner.form->controls[i]->index; } } } if (nameExists && highIdx != DSGN_NO_INDEX) { snprintf(newName, sizeof(newName), "%s", ctrl->name); newIndex = highIdx + 1; } else if (nameExists && dvxMessageBox(sAc, "Paste", "A control with this name already exists.\nCreate a control array?", MB_YESNO | MB_ICONQUESTION) == ID_YES) { // Convert existing control (and its children's parent link) to index 0 for (int32_t i = 0; i < existCount; i++) { if (strcasecmp(sDesigner.form->controls[i]->name, ctrl->name) == 0) { dsgnMakeArrayMember(sDesigner.form, sDesigner.form->controls[i], 0); break; } } snprintf(newName, sizeof(newName), "%s", ctrl->name); newIndex = 1; } else { dsgnAutoName(&sDesigner, ctrl->typeName, newName, sizeof(newName)); } snprintf(ctrl->name, sizeof(ctrl->name), "%s", newName); ctrl->index = newIndex; ctrl->parentName[0] = '\0'; ctrl->parentIndex = DSGN_NO_INDEX; arrput(sDesigner.form->controls, ctrl); dsgnOptionSelected(sDesigner.form, ctrl); dsgnCreateControlWidget(sDesigner.form, ctrl, sDesigner.form->contentBox); sDesigner.selectedIdx = (int32_t)arrlen(sDesigner.form->controls) - 1; sDesigner.form->dirty = true; prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); if (sFormWin) { dvxInvalidateWindow(sAc, sFormWin); } updateDirtyIndicators(); } // editCmdKey -- the Ctrl key a text window handles for an Edit command static int32_t editCmdKey(int32_t cmd) { switch (cmd) { case CMD_CUT: return KEY_CTRL_X; case CMD_COPY: return KEY_CTRL_C; case CMD_PASTE: return KEY_CTRL_V; case CMD_SELECT_ALL: return KEY_CTRL_A; default: return 0; } } // Converts a 1-based editor line (within the current proc's buffer) to its // 1-based line in the file's full code. For (General)/no-proc context // (sCurProcIdx out of range) the editor line is the code line. static int32_t editorLineToCodeLine(int32_t editorLine) { if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { return sProcTable[sCurProcIdx].lineNum + editorLine - 1; } return editorLine; } // editorSetText -- replace the editor text as a load, not a user edit, and // resync everything derived from it. A section longer than the editor // holds is shown read-only (and never stored back) instead of being // silently truncated. caretLine: a 1-based line to put the caret at // (start of line, nothing selected), IDE_CARET_KEEP for its current line // number, or IDE_CARET_LOAD. static void editorSetText(const char *text, int32_t caretLine) { if (!sEditor) { return; } if (caretLine == IDE_CARET_KEEP) { caretLine = wgtTextAreaGetCursorLine(sEditor); } void (*savedOnChange)(WidgetT *) = sEditor->onChange; sEditor->onChange = NULL; wgtSetText(sEditor, text); sEditor->onChange = savedOnChange; sEditorTruncated = strlen(text) >= IDE_MAX_SOURCE; sEditor->readOnly = sEditorTruncated; if (sEditorTruncated) { setStatus("This section is too large to edit and is shown read-only."); } editorSnapshot(countLines(wgtGetText(sEditor))); if (caretLine > IDE_CARET_LOAD) { wgtTextAreaSetCursor(sEditor, caretLine, 1); } } // editorSnapshot -- remember the editor's line count (lineCount, the // caller has it) and, while the editor's file has breakpoints, its text: // the base the next edit's breakpoint shift is computed against. With // no breakpoints no per-keystroke copy is made; adding one snapshots. static void editorSnapshot(int32_t lineCount) { sEditorLineCount = lineCount; bool hasBreakpoints = false; for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints) && !hasBreakpoints; i++) { hasBreakpoints = sBreakpoints[i].fileIdx == sEditorFileIdx; } if (!hasBreakpoints) { free(sEditorPrevText); sEditorPrevText = NULL; return; } const char *text = wgtGetText(sEditor); size_t len = text ? strlen(text) : 0; char *copy = (char *)realloc(sEditorPrevText, len + 1); if (!copy) { free(sEditorPrevText); sEditorPrevText = NULL; return; } memcpy(copy, text ? text : "", len); copy[len] = '\0'; sEditorPrevText = copy; } // 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 (hasProject()) { 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); PrjFileKindE kind = prjKindForPath(filePath); prjAddFile(&sProject, fileName, kind); // If it's a .bas, check for a matching .frm and add it too. Only the // file-name part's extension is replaced (a directory may contain dots). if (kind == PrjKindModuleE) { char frmPath[DVX_MAX_PATH]; int32_t baseOff = (int32_t)(platformPathBaseName(filePath) - filePath); snprintf(frmPath, sizeof(frmPath), "%s", filePath); char *frmDot = strrchr(frmPath + baseOff, '.'); int32_t stemLen = frmDot ? (int32_t)(frmDot - frmPath) : (int32_t)strlen(frmPath); if (stemLen + (int32_t)sizeof(PRJ_FRM_EXT) <= (int32_t)sizeof(frmPath)) { snprintf(frmPath + stemLen, sizeof(frmPath) - stemLen, "%s", PRJ_FRM_EXT); if (prjPathExists(frmPath)) { prjAddFile(&sProject, platformPathBaseName(frmPath), PrjKindFormE); } } } sProject.dirty = false; sProject.activeFileIdx = -1; prjLoadAllFiles(&sProject, sAc); updateMainTitle(); setStatus("Project created."); updateProjectMenuState(); } // ensureProjectWindow -- create the Project Explorer if needed (wired to // the shared menu/accelerator handlers), otherwise refresh its tree. static void ensureProjectWindow(bool raise) { if (sProjectWin) { prjRebuildTree(&sProject); } else { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState); if (!sProjectWin) { return; } sProjectWin->y = toolbarBottom() + IDE_PROJECT_WIN_OFFSET; sProjectWin->onClose = onProjectWinClose; sProjectWin->onMenu = onMenu; sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } if (raise) { dvxRaiseWindow(sAc, sProjectWin); } } // evaluateImmediate -- compile and execute a single line from the // Immediate window. If the line doesn't start with a statement keyword, // wrap it in PRINT so expressions produce visible output. static void evaluateImmediate(const char *expr) { if (!expr || *expr == '\0') { return; } // Try assignment first when paused if (immTryAssign(expr)) { return; } char wrapped[IDE_IMM_WRAP_BUF]; // If it already starts with a statement keyword, use as-is if (kwMatch(expr, KW_PRINT) || kwMatch(expr, KW_DIM) || kwMatch(expr, KW_LET)) { snprintf(wrapped, sizeof(wrapped), "%s", expr); } else { snprintf(wrapped, sizeof(wrapped), "%s %s", KW_PRINT, expr); } BasModuleT *mod = NULL; char err[IDE_VALUE_BUF]; BasVmT *vm = buildSnippetVm(wrapped, &mod, err, sizeof(err), NULL, NULL); if (!vm) { immPrintCallback(NULL, "Error: ", false); immPrintCallback(NULL, err, true); return; } basVmSetPrintCallback(vm, immPrintCallback, NULL); immReportSnippetResult(vm, runSnippetVm(vm)); basVmDestroy(vm); basModuleFree(mod); } // 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 bool evalWatchExpr(const char *expr, char *outBuf, int32_t outBufSize) { if (sDbgState != DbgPausedE || !sVm || !sDbgModule || !sDbgModule->debugVars) { return false; } char wrapped[IDE_IMM_WRAP_BUF]; snprintf(wrapped, sizeof(wrapped), "%s %s", KW_PRINT, expr); BasModuleT *mod = NULL; char err[IDE_VALUE_BUF]; BasVmT *tvm = buildSnippetVm(wrapped, &mod, err, sizeof(err), NULL, NULL); if (!tvm) { return false; } // Set up capture callback sWatchPrintBuf[0] = '\0'; sWatchPrintLen = 0; basVmSetPrintCallback(tvm, watchPrintCallback, NULL); // Run runSnippetVm(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; } // evtItemAdd -- append an owned Event dropdown label; bracketed marks an // event that has no code yet static void evtItemAdd(const char *name, bool bracketed) { char label[BAS_MAX_IDENT + sizeof("[]") - 1]; snprintf(label, sizeof(label), bracketed ? "[%s]" : "%s", name); char *copy = strdup(label); if (copy) { arrput(sEvtItems, copy); } } // evtItemsReset -- empty the Event dropdown labels (they are owned) static void evtItemsReset(void) { for (int32_t i = 0; i < (int32_t)arrlen(sEvtItems); i++) { free((void *)sEvtItems[i]); } arrsetlen(sEvtItems, 0); } // evtLabelMatches -- compare an Event dropdown label against an event // name, ignoring the [] brackets that mark unimplemented events. static bool evtLabelMatches(const char *label, const char *evtName) { if (label[0] == '[') { size_t nameLen = strlen(evtName); return strncasecmp(label + 1, evtName, nameLen) == 0 && label[nameLen + 1] == ']' && label[nameLen + 2] == '\0'; } return strcasecmp(label, evtName) == 0; } // fileCodeContains -- true if the code of project file fileIdx (a .frm's // code section, not its layout) contains needle static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needleLen, bool caseSensitive) { bool missing = false; char *owned = NULL; const char *text = projectFileText(fileIdx, &owned, &missing); IdeFrmScanT scan; scan.validator = NULL; scan.code = NULL; if (text && sProject.files[fileIdx].kind == PrjKindFormE) { frmScan(text, &scan); text = scan.code; } bool found = procBufContains(text, needle, needleLen, caseSensitive); free(scan.code); free(owned); return found; } // findDebugVar -- find a debug variable by name, respecting scope static const BasDebugVarT *findDebugVar(const char *name) { if (!sVm || !sDbgModule || !sDbgModule->debugVars) { return NULL; } int32_t curProcIdx = vmCurProcIdx(); for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) { const BasDebugVarT *dv = &sDbgModule->debugVars[i]; if (strcasecmp(dv->name, name) == 0 && debugVarVisible(dv, curProcIdx)) { return dv; } } return NULL; } // findDialogBuild -- build the Find / Replace widgets into sFindWin. // Returns false if any widget could not be made. static bool findDialogBuild(bool showReplace) { // Radio order is the FindScopeE / FindDirE order static const char *scopeNames[ScopeCountE] = { [ScopeFuncE] = "Function", [ScopeObjE] = "Object", [ScopeFileE] = "File", [ScopeProjE] = "Project" }; static const char *dirNames[FindDirCountE] = { [FindDirFwdE] = "Forward", [FindDirBackE] = "Backward" }; WidgetT *root = wgtInitWindow(sAc, sFindWin); WidgetT *findRow = root ? wgtHBox(root) : NULL; sFindInput = (findRow && wgtLabel(findRow, "Find:")) ? wgtTextInput(findRow, IDE_FIND_TEXT_BUF) : NULL; WidgetT *replRow = sFindInput ? wgtHBox(root) : NULL; sReplCheck = replRow ? wgtCheckbox(replRow, "Replace:") : NULL; sReplInput = sReplCheck ? wgtTextInput(replRow, IDE_FIND_TEXT_BUF) : NULL; // Options row: scope + direction + case WidgetT *optRow = sReplInput ? wgtHBox(root) : NULL; WidgetT *scopeFrame = optRow ? wgtFrame(optRow, "Scope") : NULL; WidgetT *scopeBox = scopeFrame ? wgtVBox(scopeFrame) : NULL; sScopeGroup = scopeBox ? wgtRadioGroup(scopeBox) : NULL; WidgetT *dirFrame = sScopeGroup ? wgtFrame(optRow, "Direction") : NULL; WidgetT *dirBox = dirFrame ? wgtVBox(dirFrame) : NULL; sDirGroup = dirBox ? wgtRadioGroup(dirBox) : NULL; WidgetT *caseBox = sDirGroup ? wgtVBox(optRow) : NULL; sCaseCheck = caseBox ? wgtCheckbox(caseBox, "Match Case") : NULL; // Buttons WidgetT *btnRow = sCaseCheck ? wgtHBox(root) : NULL; WidgetT *btnFind = btnRow ? wgtButton(btnRow, "Find Next") : NULL; sBtnReplace = btnFind ? wgtButton(btnRow, "Replace") : NULL; sBtnReplAll = sBtnReplace ? wgtButton(btnRow, "Replace All") : NULL; WidgetT *btnClose = sBtnReplAll ? wgtButton(btnRow, "Close") : NULL; if (!btnClose) { return false; } for (int32_t i = 0; i < ScopeCountE; i++) { if (!wgtRadio(sScopeGroup, scopeNames[i])) { return false; } } for (int32_t i = 0; i < FindDirCountE; i++) { if (!wgtRadio(sDirGroup, dirNames[i])) { return false; } } root->spacing = wgtPixels(IDE_FIND_ROOT_SPACING); findRow->spacing = wgtPixels(IDE_WIDGET_SPACING); sFindInput->weight = WGT_WEIGHT_FILL; replRow->spacing = wgtPixels(IDE_WIDGET_SPACING); sReplCheck->onChange = onReplCheckChange; sReplInput->weight = WGT_WEIGHT_FILL; optRow->spacing = wgtPixels(IDE_WIDGET_SPACING_LG); btnRow->spacing = wgtPixels(IDE_WIDGET_SPACING); btnRow->align = AlignEndE; btnFind->onClick = onFindNext; sBtnReplace->onClick = onReplace; sBtnReplAll->onClick = onReplaceAll; btnClose->onClick = onFindCloseBtn; wgtSetText(sFindInput, sFindText); wgtCheckboxSetChecked(sReplCheck, showReplace); wgtSetText(sReplInput, sReplaceText); wgtRadioGroupSetSelected(sScopeGroup, ScopeProjE); wgtRadioGroupSetSelected(sDirGroup, FindDirFwdE); return true; } // findInEditorSections -- search the editor file's sections (General plus // every proc) other than fromIdx, in scan order away from it: forward // visits the procs after it, General, then the procs before it; backward // mirrors that. fromIdx IDE_SECTION_NONE scans every section from the start (forward) // or the end (backward). objFilter limits the scan to one object's // sections (NULL = all). The first match is shown and selected. static bool findInEditorSections(int32_t fromIdx, const char *needle, bool caseSensitive, bool forward, const char *objFilter) { int32_t needleLen = (int32_t)strlen(needle); int32_t total = (int32_t)arrlen(sProcBufs) + 1; // General plus every proc int32_t curPos = fromIdx + 1; // scan position of fromIdx int32_t count = total - 1; if (fromIdx < IDE_SECTION_GENERAL) { curPos = forward ? -1 : total; count = total; } for (int32_t k = 1; k <= count; k++) { int32_t pos = forward ? curPos + k : curPos - k; pos = (pos % total + total) % total; int32_t p = pos - 1; // -1 = General const char *text = (p < 0) ? sGeneralBuf : sProcBufs[p]; if (sectionInScope(p, objFilter) && procBufContains(text, needle, needleLen, caseSensitive)) { return showProcAndFind(p, needle, caseSensitive, forward); } } return false; } // findInProject -- Find Next within scope, starting at the editor's // selection: the rest of the shown section, the file's other sections // (Function scope skips those; Object scope keeps the selected object's), // then for Project scope the other files in order. Only a file whose // code really matches is activated. Finally the shown section is // searched again from its far end, so the search wraps to its origin. static bool findInProject(const char *needle, bool caseSensitive, bool forward, FindScopeE scope) { if (!needle || !needle[0] || !sEditor) { return false; } int32_t needleLen = (int32_t)strlen(needle); char objName[BAS_MAX_IDENT]; // The proc buffers and sProcTable must describe the current editor text getFullSource(); currentObjName(objName, sizeof(objName)); if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward)) { return true; } if (scope != ScopeFuncE && findInEditorSections(sCurProcIdx, needle, caseSensitive, forward, scope == ScopeObjE ? objName : NULL)) { return true; } int32_t fileCount = prjFileCount(&sProject); // Leaving the file: the other files are searched in their project // buffers, so the editor's and the designer's state go there first. if (scope == ScopeProjE && fileCount > 1) { int32_t step = forward ? 1 : -1; int32_t startFile = sEditorFileIdx >= 0 ? sEditorFileIdx : sProject.activeFileIdx; if (!stashCurrentFile()) { return false; } for (int32_t k = 1; startFile >= 0 && k < fileCount; k++) { int32_t fileIdx = ((startFile + step * k) % fileCount + fileCount) % fileCount; if (!fileCodeContains(fileIdx, needle, needleLen, caseSensitive)) { continue; } if (!activateFile(fileIdx, ViewCodeE)) { return false; } return findInEditorSections(IDE_SECTION_NONE, needle, caseSensitive, forward, NULL); } } // Wrap around within the shown section if (!forward) { wgtTextAreaCursorToEnd(sEditor); return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward); } // Caret to the start without a selection: a forward search then finds // a match that begins right there. wgtTextAreaSetCursor(sEditor, 1, 1); return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward); } // findMemberVarIdx -- member variable slot of class instance obj called // name, from the debug variable table (-1 when there is none) static int32_t findMemberVarIdx(const BasObjectT *obj, const char *name) { const char *cls = objectClassName(obj); for (int32_t i = 0; cls && i < sDbgModule->debugVarCount; i++) { const BasDebugVarT *dv = &sDbgModule->debugVars[i]; if (dv->scope == SCOPE_MEMBER && strcasecmp(dv->formName, cls) == 0 && strcasecmp(dv->name, name) == 0 && dv->index < obj->fieldCount) { return dv->index; } } return -1; } // 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; } // findUdtFieldIdx -- field index within a UDT from the debug type table static int32_t findUdtFieldIdx(int32_t typeId, const char *fieldName) { for (int32_t t = 0; t < sDbgModule->debugUdtDefCount; t++) { if (sDbgModule->debugUdtDefs[t].typeId != typeId) { continue; } for (int32_t f = 0; f < sDbgModule->debugUdtDefs[t].fieldCount; f++) { if (strcasecmp(sDbgModule->debugUdtDefs[t].fields[f].name, fieldName) == 0) { return f; } } return -1; } return -1; } // findWithEvents -- the WithEvents variable of the code window's file // called name, NULL when there is none static const IdeWithEventsT *findWithEvents(const char *name) { for (int32_t i = 0; i < (int32_t)arrlen(sWithEvents); i++) { if (strcasecmp(sWithEvents[i].name, name) == 0) { return &sWithEvents[i]; } } return NULL; } // flushOutput -- show buffered program output in the Output pane, // scrolled to the newest line static void flushOutput(void) { if (!sOutputPending) { return; } sOutputPending = false; setOutputText(sOutputBuf); if (sOutput) { wgtTextAreaGoToLine(sOutput, countLines(sOutputBuf)); } } static void formatValue(const BasValueT *v, char *buf, int32_t bufSize) { switch (v->type) { case BAS_TYPE_INTEGER: case BAS_TYPE_BYTE: snprintf(buf, bufSize, "%d", (int)v->intVal); break; case BAS_TYPE_CURRENCY: { BasStringT *text = basValFormatString(*v); snprintf(buf, bufSize, "%s", text->data); basStringUnref(text); 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_DATE: { BasStringT *text = basValFormatString(*v); snprintf(buf, bufSize, "#%s#", text->data); basStringUnref(text); break; } case BAS_TYPE_STRING: if (v->strVal) { snprintf(buf, bufSize, "\"%.*s\"", (int)(bufSize - IDE_QUOTE_OVERHEAD), v->strVal->data); } else { snprintf(buf, bufSize, "\"\""); } break; case BAS_TYPE_EMPTY: snprintf(buf, bufSize, "Empty"); break; case BAS_TYPE_NULL: snprintf(buf, bufSize, "Null"); break; case BAS_TYPE_MISSING: snprintf(buf, bufSize, "Missing"); break; case BAS_TYPE_OBJECT: snprintf(buf, bufSize, "%s", v->objVal ? "<" IDE_OBJECT_TYPE ">" : "Nothing"); break; case BAS_TYPE_INSTANCE: if (v->instVal->classIdx == BAS_OBJ_COLLECTION) { snprintf(buf, bufSize, IDE_COLLECTION_TYPE " (%d items)", (int)v->instVal->itemCount); } else { snprintf(buf, bufSize, "<%s>", instanceTypeName(v->instVal)); } break; case BAS_TYPE_ARRAY: { BasArrayT *arr = v->arrVal; if (!arr) { snprintf(buf, bufSize, "(uninitialized)"); break; } // emitClamped clamps pos to bufSize-1 on truncation, so a // multi-dim array can't push pos past bufSize and make a later // 'bufSize - pos' go negative (huge size_t -> OOB write). int32_t pos = emitClamped(buf, bufSize, 0, "%s(", typeNameStr(arr->elementType)); for (int32_t d = 0; d < arr->dims && pos < bufSize - IDE_ARRAY_DIM_RESERVE; d++) { if (d > 0) { pos = emitClamped(buf, bufSize, pos, ", "); } pos = emitClamped(buf, bufSize, pos, "%d To %d", (int)arr->lbound[d], (int)arr->ubound[d]); } emitClamped(buf, bufSize, pos, ") [%d]", (int)arr->totalElements); break; } default: snprintf(buf, bufSize, "..."); break; } } // 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 = IDE_SECTION_NONE; sEditorFileIdx = -1; sSkeletonName[0] = '\0'; arrsetlen(sProcTable, 0); } // frmScan -- one parse of .frm text: collects the code section into // scan->code (malloc'd) and, when scan->validator is set, the form's // controls for the compile-time member validator. static void frmScan(const char *frmText, IdeFrmScanT *scan) { FrmParserCbsT cbs; memset(&cbs, 0, sizeof(cbs)); cbs.userData = scan; cbs.onFormBegin = validatorOnFormBegin; cbs.onCtrlBegin = validatorOnCtrlBegin; cbs.onMenuBegin = validatorOnMenuBegin; cbs.onFormEnd = frmScanOnFormEnd; frmParse(frmText, (int32_t)strlen(frmText), &cbs); } // frmScanOnFormEnd -- capture the code after the form's End exactly as // the designer loads it (dsgnCodeSection), so compiled line numbers // match what the editor shows. static void frmScanOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) { IdeFrmScanT *scan = (IdeFrmScanT *)userData; int32_t codeLen = 0; const char *code = dsgnCodeSection(trailingSrc, trailingLen, &codeLen); if (!code) { return; } free(scan->code); scan->code = (char *)malloc((size_t)codeLen + 1); if (scan->code) { memcpy(scan->code, code, (size_t)codeLen); scan->code[codeLen] = '\0'; } } // generalAppend -- (General) text followed by text on new lines, as a // malloc'd string (NULL when out of memory) static char *generalAppend(const char *general, const char *text) { int32_t genLen = general ? (int32_t)strlen(general) : 0; int32_t addLen = (int32_t)strlen(text); char *merged = (char *)malloc((size_t)genLen + (size_t)addLen + sizeof("\n")); if (!merged) { return NULL; } int32_t pos = 0; if (genLen > 0) { memcpy(merged, general, (size_t)genLen); pos = genLen; if (merged[pos - 1] != '\n') { merged[pos++] = '\n'; } } memcpy(merged + pos, text, (size_t)addLen + 1); return merged; } // 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]; // A frame only initializes and releases its own procedure's // localCount slots; anything past them is stale. A ByRef // parameter shows (and sets) the caller's variable. if (dv->index >= 0 && dv->index < frame->localCount) { return basValDeref(&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]; } } else if (dv->scope == SCOPE_MEMBER) { return memberVarSlot(dv); } return NULL; } // Parameter list the runtime passes to a handler of evtName ("" when // none, or for events not in the master table). static const char *getEventParams(const char *evtName) { for (int32_t i = 0; i < IDE_EVENT_COUNT; i++) { if (strcasecmp(sEventTable[i].suffix, evtName) == 0) { return sEventTable[i].params; } } return ""; } static bool getFindForward(void) { return !sDirGroup || wgtRadioGroupGetSelected(sDirGroup) != FindDirBackE; } static bool getFindMatchCase(void) { return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck); } static FindScopeE getFindScope(void) { int32_t idx = sScopeGroup ? wgtRadioGroupGetSelected(sScopeGroup) : ScopeProjE; return (idx >= 0 && idx < ScopeCountE) ? (FindScopeE)idx : ScopeProjE; } // getFullSource -- save the editor and reassemble all buffers into one // source string. Returns a pointer into an internal cache that is owned // by joinProcBufs and freed on the next call. Callers must NOT free it; // strdup it if the result must outlive the next call. static const char *getFullSource(void) { saveCurProc(false); return joinProcBufs(); } static WindowT *getLastFocusWin(void) { if (sLastFocusWin && (sLastFocusWin == sCodeWin || sLastFocusWin == sOutWin || sLastFocusWin == sImmWin || sLastFocusWin == sFormWin)) { return sLastFocusWin; } sLastFocusWin = NULL; return NULL; } // handleEditCmd -- Edit menu. Cut/Copy/Paste/Select All/Delete act on the // last focused content window: the designer's selected control, or the // text of the code, output or immediate window. static void handleEditCmd(int32_t cmd) { switch (cmd) { case CMD_CUT: case CMD_COPY: case CMD_PASTE: case CMD_SELECT_ALL: case CMD_DELETE: { WindowT *target = getLastFocusWin(); if (target && target == sFormWin) { designerEditCmd(cmd); } else if (target && target->onKey) { if (cmd == CMD_DELETE) { target->onKey(target, KEY_DELETE, 0); } else { target->onKey(target, editCmdKey(cmd), ACCEL_CTRL); } } 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; default: break; } } // Accelerators bypass the menu enabled state, so anything that changes // the project or the compiled module is gated on the program being idle. static void handleFileCmd(int32_t cmd) { bool isIdle = (sDbgState == DbgIdleE); switch (cmd) { case CMD_OPEN: if (isIdle) { loadFile(); } break; case CMD_SAVE: saveFile(); break; case CMD_SAVE_ALL: if (saveAllModified()) { setStatus("All files saved."); } break; case CMD_MAKE_EXE: if (isIdle) { makeExecutable(); } break; case CMD_EXIT: if (sWin) { onClose(sWin); } break; default: // Recent files if (isIdle && cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) { recentOpen(cmd - CMD_RECENT_BASE); } break; } } static void handleProjectCmd(int32_t cmd) { bool isIdle = (sDbgState == DbgIdleE); switch (cmd) { case CMD_PRJ_NEW: if (isIdle) { newProject(); } break; case CMD_PRJ_OPEN: if (isIdle) { openProject(); } break; case CMD_PRJ_SAVE: if (hasProject() && saveProjectFile()) { setStatus("Project saved."); } break; case CMD_PRJ_CLOSE: if (isIdle && promptAndSave()) { closeProject(); } break; case CMD_PRJ_PROPS: if (hasProject()) { if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) { updateMainTitle(); if (sProjectWin) { prjRebuildTree(&sProject); } } } break; case CMD_PRJ_NEW_CLASS: if (isIdle && hasProject()) { newClassModule(); } break; case CMD_PRJ_RENAME: { int32_t renIdx = prjGetSelectedFileIdx(); if (isIdle && renIdx >= 0 && renIdx < prjFileCount(&sProject)) { renameProjectFile(renIdx); } break; } case CMD_PRJ_REMOVE: { int32_t rmIdx = prjGetSelectedFileIdx(); if (!isIdle || rmIdx < 0 || rmIdx >= prjFileCount(&sProject)) { break; } PrjFileT *rmFile = &sProject.files[rmIdx]; char rmMsg[IDE_MSG_BUF]; 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 ((rmIdx == sEditorFileIdx || rmIdx == sDesignerFileIdx) && !stashCurrentFile()) { break; } if (rmFile->modified) { int32_t result = dvxPromptSave(sAc, IDE_MAIN_TITLE); if (result == DVX_SAVE_CANCEL) { break; } if (result == DVX_SAVE_YES && !writeProjectFile(rmIdx)) { break; } } // Detach the editor/designer from the file before it goes away if (rmIdx == sEditorFileIdx) { freeProcBufs(); editorSetText("", IDE_CARET_LOAD); } else if (sEditorFileIdx > rmIdx) { sEditorFileIdx--; } if (rmIdx == sDesignerFileIdx) { designerRelease(); } else if (sDesignerFileIdx > rmIdx) { sDesignerFileIdx--; } removeBreakpointsForFile(rmIdx); prjRemoveFile(&sProject, rmIdx); // adjusts activeFileIdx invalidateCompiled(); updateDropdowns(); prjRebuildTree(&sProject); updateProjectMenuState(); updateDirtyIndicators(); break; } default: break; } } static void handleRunCmd(int32_t cmd) { switch (cmd) { case CMD_RUN: if (sDbgState == DbgPausedE) { // Resume from breakpoint -- clear debug mode so it runs free sDbgEnabled = false; clearVmStepState(); if (sVm) { basVmSetBreakpoints(sVm, NULL, 0); } debugResume("Running..."); } else if (sDbgState == DbgIdleE) { sDbgEnabled = false; compileAndRun(); } break; case CMD_DEBUG: if (sDbgState == DbgPausedE) { // Already debugging -- resume, run to next breakpoint clearVmStepState(); debugResume("Debugging..."); } else if (sDbgState == DbgIdleE) { // Start in debug mode with breakpoints sDbgEnabled = true; compileAndRun(); } break; case CMD_RUN_NOCMP: if (sDbgState == DbgIdleE) { runCached(); } break; case CMD_STOP: if (sDbgState == DbgRunningE || sDbgState == DbgPausedE) { debugStop(); setStatus("Program stopped."); } break; case CMD_OUTPUT_TO_LOG: if (sWin && sWin->menuBar) { sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG); prefsSetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_OUTPUT_TO_LOG, sOutputToLog); prefsSave(sPrefs); } 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, PREF_SEC_RUN, PREF_KEY_SAVE_ON_RUN, save); prefsSave(sPrefs); } break; default: break; } } static void handleViewCmd(int32_t cmd) { switch (cmd) { case CMD_VIEW_CODE: { int32_t selFileIdx = prjGetSelectedFileIdx(); // The selected file, else the active one (reopening a closed // code window with its code reloaded) if (selFileIdx < 0 || selFileIdx >= prjFileCount(&sProject)) { selFileIdx = sProject.activeFileIdx; } activateFile(selFileIdx, ViewCodeE); break; } case CMD_VIEW_DESIGN: { int32_t selFileIdx = prjGetSelectedFileIdx(); // The Shift+F7 accelerator bypasses the menu state: only a form // opens in the designer. if (selFileIdx >= 0 && selFileIdx < prjFileCount(&sProject) && sProject.files[selFileIdx].kind == PrjKindFormE) { activateFile(selFileIdx, ViewDesignE); } else if (activeIsForm()) { switchToDesign(); } else { setStatus("The active file is not a form."); } 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, PREF_SEC_VIEW, PREF_KEY_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, PREF_SEC_VIEW, PREF_KEY_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 < prjFileCount(&sProject) && sProject.files[selMenuIdx].kind == PrjKindFormE) { activateFile(selMenuIdx, ViewDesignE); } if (!activeIsForm() || !sDesigner.form) { setStatus("The active file is not a form."); break; } // 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; renameMenuItemsInCode(oldNames, oldCount); // Rebuild menu bar preview if (sFormWin) { wmDestroyMenuBar(sFormWin, &sAc->display); dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form); dvxInvalidateWindow(sAc, sFormWin); } // Rebuild Object dropdown to reflect added/removed/renamed items updateDropdowns(); updateDirtyIndicators(); } for (int32_t mi = 0; mi < oldCount; mi++) { free(oldNames[mi]); } arrfree(oldNames); break; } default: break; } } static void handleWindowCmd(int32_t cmd) { switch (cmd) { case CMD_WIN_CODE: // An active file reloads into a reopened window; otherwise an // empty scratch editor. if (sProject.activeFileIdx >= 0) { showCodeView(sProject.activeFileIdx); } else { showCodeWindow(); if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } } 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, toolbarBottom()); if (sToolboxWin) { 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: ensureProjectWindow(true); break; default: break; } } static bool hasProject(void) { return sProject.projectPath[0] != '\0'; } static bool hasUnsavedData(void) { return anyFileModified() || sProject.dirty; } // Build control help topic from type name. // Topic IDs in .bhs files follow the pattern ctrl.. // 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 < 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 with a help file: its forms use the program's // help (same rules as a compiled program) if (sDbgFormRt && sDbgFormRt->helpFile[0] && basFormRtHelpTopic(sDbgFormRt, sCtx->helpTopic, sizeof(sCtx->helpTopic))) { snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sDbgFormRt->helpFile); return; } // Code editor / Immediate window: look up the word under the cursor if ((focusWin == sCodeWin && sEditor) || (focusWin == sImmWin && sImmediate)) { WidgetT *area = (focusWin == sCodeWin) ? sEditor : sImmediate; char word[IDE_FULL_NAME_BUF]; if (wgtTextAreaGetWordAtCursor(area, word, sizeof(word)) > 0) { const char *topic = helpLookupKeyword(word); if (topic) { snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); return; } } // No keyword match -- open the window's own topic snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", (focusWin == sCodeWin) ? "ide.editor" : "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; } // Remaining windows map straight to a topic const char *topic = "ide.overview"; if (focusWin == sOutWin) { topic = "ide.output"; } else if (focusWin == sProjectWin) { topic = "ide.project"; } else if (focusWin == sLocalsWin) { topic = "ide.debug.locals"; } else if (focusWin == sCallStackWin) { topic = "ide.debug.callstack"; } else if (focusWin == sWatchWin) { topic = "ide.debug.watch"; } else if (focusWin == sBreakpointWin) { topic = "ide.debug.breakpoints"; } snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic); } static void helpSetCtrlTopic(const char *typeName) { helpBuildCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic)); } // ideErrorBox -- modal error message. The busy state swallows all input, // so it is cleared first or the box could never be dismissed. static void ideErrorBox(const char *msg) { dvxSetBusy(sAc, false); dvxErrorBox(sAc, NULL, msg); } // idePump -- one pass of the GUI event loop while a program runs, with // its buffered output shown first static void idePump(void) { flushOutput(); dvxUpdate(sAc); } // ============================================================ // 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 void ideRenameInCode(const char *oldName, const char *newName) { if (!oldName || !newName || strcasecmp(oldName, newName) == 0) { return; } // The editor may hold edits that are not in the proc buffers yet saveCurProc(false); // Rename in the per-procedure buffers (the code bound to the editor) bool editorChanged = false; bool shownChanged = false; if (sGeneralBuf) { char *replaced = renameInBuffer(sGeneralBuf, oldName, newName); if (replaced) { free(sGeneralBuf); sGeneralBuf = replaced; editorChanged = true; shownChanged = sCurProcIdx == IDE_SECTION_GENERAL; } } for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) { char *replaced = renameInBuffer(sProcBufs[i], oldName, newName); if (replaced) { free(sProcBufs[i]); sProcBufs[i] = replaced; editorChanged = true; shownChanged = shownChanged || i == sCurProcIdx; } } // Reload the editor only when the section it shows was renamed in if (shownChanged) { editorSetText(sCurProcIdx == IDE_SECTION_GENERAL ? (sGeneralBuf ? sGeneralBuf : "") : sProcBufs[sCurProcIdx], IDE_CARET_KEEP); } if (editorChanged && sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject)) { sProject.files[sEditorFileIdx].modified = true; } // Update form->code: from the renamed buffers when the editor holds // the designer's code, otherwise directly -- with the code window // closed the designer's copy of the code is the only one loaded. if (sDesigner.form) { if (sEditorFileIdx >= 0 && sEditorFileIdx == sDesignerFileIdx) { stashFormCode(); sDesigner.form->dirty = true; } else if (sDesigner.form->code) { char *replaced = renameInBuffer(sDesigner.form->code, oldName, newName); if (replaced) { free(sDesigner.form->code); sDesigner.form->code = replaced; sDesigner.form->dirty = true; } } // A renamed form: its project entry follows the designer if (sDesignerFileIdx >= 0 && sDesignerFileIdx < prjFileCount(&sProject)) { snprintf(sProject.files[sDesignerFileIdx].formName, sizeof(sProject.files[sDesignerFileIdx].formName), "%s", sDesigner.form->name); } } // Rename in every other file's buffer (loading it first if needed) for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (i == sEditorFileIdx || i == sDesignerFileIdx) { continue; } bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); char *replaced = renameInBuffer(text, oldName, newName); free(owned); if (replaced) { free(sProject.files[i].buffer); sProject.files[i].buffer = replaced; sProject.files[i].modified = true; } } // Refresh dropdowns to reflect renamed procedures updateDropdowns(); } // immEvalRhs -- evaluate the right-hand side of an Immediate-window // assignment as a BASIC expression (it may use the paused program's // variables) converted to the target's type. Reports any error in the // Immediate window and returns false. static bool immEvalRhs(const char *rhs, uint8_t targetType, BasValueT *outVal) { char src[IDE_IMM_WRAP_BUF + BAS_MAX_IDENT * 2]; snprintf(src, sizeof(src), "%s %s AS %s: %s = %s", KW_DIM, IDE_IMM_RHS_VAR, typeNameStr(targetType), IDE_IMM_RHS_VAR, rhs); BasModuleT *mod = NULL; char err[IDE_VALUE_BUF]; int32_t idx = -1; BasVmT *vm = buildSnippetVm(src, &mod, err, sizeof(err), IDE_IMM_RHS_VAR, &idx); if (!vm) { immPrintCallback(NULL, "Error: ", false); immPrintCallback(NULL, err, true); return false; } BasVmResultE result = runSnippetVm(vm); bool ok = (result == BAS_VM_HALTED || result == BAS_VM_OK) && idx >= 0 && idx < BAS_VM_MAX_GLOBALS; if (ok) { *outVal = basValCopy(vm->globals[idx]); } else { immReportSnippetResult(vm, result); } basVmDestroy(vm); basModuleFree(mod); return ok; } // immPrintCallback -- append program output to the Immediate window. // The window has a fixed capacity, so the oldest lines scroll off the // top once output would take the room kept for typing (IDE_IMM_HEADROOM). static void immPrintCallback(void *ctx, const char *text, bool newline) { (void)ctx; if (!sImmediate) { return; } static char immBuf[IDE_MAX_IMM]; const char *cur = wgtGetText(sImmediate); int32_t len = cur ? (int32_t)strlen(cur) : 0; if (len > IDE_MAX_IMM - 1) { len = IDE_MAX_IMM - 1; } memcpy(immBuf, cur ? cur : "", (size_t)len); immBuf[len] = '\0'; if (text) { len = appendDroppingLines(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, text, (int32_t)strlen(text)); } if (newline) { appendDroppingLines(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, "\n", 1); } wgtSetText(sImmediate, immBuf); // Move cursor to end so user can keep typing wgtTextAreaGoToLine(sImmediate, countLines(immBuf)); } // immReportSnippetResult -- report a failed Immediate/Watch snippet run static void immReportSnippetResult(BasVmT *vm, BasVmResultE result) { if (result == BAS_VM_HALTED || result == BAS_VM_OK || result == BAS_VM_BREAKPOINT) { return; } immPrintCallback(NULL, "Error: ", false); immPrintCallback(NULL, result == BAS_VM_STEP_LIMIT ? "Statement did not finish; stopped." : basVmGetError(vm), true); } // immTryAssign -- handle "varName = expr" when paused at a breakpoint. // Looks up the variable, evaluates the RHS expression, and writes the // value back to the running VM. Returns true if handled as an assignment. static bool immTryAssign(const char *expr) { if (sDbgState != DbgPausedE || !sVm || !sDbgModule) { return false; } // Skip leading whitespace const char *p = dvxSkipWs(expr); // Skip optional LET keyword if (kwMatch(p, KW_LET)) { p = dvxSkipWs(p + KW_LEN(KW_LET)); } // Resolve the LHS to a live slot in the VM const char *afterLhs = NULL; BasValueT *slot = resolveVarPath(p, &afterLhs); if (!slot || !afterLhs) { return false; } // Build display name from LHS char lhsName[IDE_VALUE_BUF]; 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; } // Only scalars can be assigned; the RHS is evaluated as an expression BasValueT newVal; bool scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN || slot->type == BAS_TYPE_DATE || slot->type == BAS_TYPE_BYTE || slot->type == BAS_TYPE_CURRENCY; if (!scalar) { immPrintCallback(NULL, "Cannot assign to this variable type", true); return true; } if (!immEvalRhs(p, slot->type, &newVal)) { return true; } // Write the value directly to the slot basValRelease(slot); *slot = newVal; // transfer ownership -- don't release newVal // Show confirmation char confirm[IDE_VALUE_BUF]; 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; } // Hash-based keyword/type lookup using stb_ds. // Key = uppercase word, value = TEXT_SYNTAX_KEYWORD or TEXT_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); uint8_t color = (cls == BAS_KW_CLASS_TYPE || cls == BAS_KW_CLASS_LITERAL) ? TEXT_SYNTAX_TYPE : TEXT_SYNTAX_KEYWORD; shput(sSyntaxMap, kw, basLexerKeywordReserved(i) ? color : (uint8_t)(color | IDE_SYNTAX_UNRESERVED)); } } static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) { (void)ctx; // The prompt, and everything printed before it, is visible while asking if (prompt) { outputAppend(prompt, (int32_t)strlen(prompt)); } flushOutput(); return dvxInputBox(sAc, IDE_MAIN_TITLE, prompt ? prompt : "Enter value:", NULL, buf, bufSize); } // instanceTypeName -- what a class instance or Collection is, for the // debugger: its class name, or "Collection" static const char *instanceTypeName(const BasObjectT *obj) { const char *cls = objectClassName(obj); if (cls) { return cls; } return obj->classIdx == BAS_OBJ_COLLECTION ? IDE_COLLECTION_TYPE : IDE_OBJECT_TYPE; } // invalidateCompiled -- the compiled module and source map describe the // project's previous file list; drop them (a running program keeps the // module it executes). static void invalidateCompiled(void) { if (!sVm && sCachedModule) { basModuleFree(sCachedModule); sCachedModule = NULL; } arrfree(sProject.sourceMap); sProject.sourceMap = NULL; updateProjectMenuState(); } 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; } // Check if position i is inside a string literal or comment (' or REM). // 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--; } const char *line = src + lineStart; int32_t lineLen = (int32_t)strcspn(line, "\n"); bool inString = false; for (int32_t j = 0; j < i - lineStart; j++) { if (line[j] == '"') { inString = !inString; } else if (!inString && (line[j] == '\'' || isRemAt(line, j, lineLen))) { // Rest of line is a comment return true; } } return inString; } // Internal mangled names (e.g. "DoCount$Count" for Static vars) have a $ // in the middle; string variable names end with $ and are fine. static bool isMangledDebugName(const char *name) { const char *dollar = strchr(name, '$'); return dollar && dollar[1] != '\0'; } // isRemAt -- true if the word REM (a comment to end of line) starts at // line[pos]: a whole word in any case. Shared by the colorizer, the // rename scanner and breakpoint validation. static bool isRemAt(const char *line, int32_t pos, int32_t lineLen) { int32_t end = pos + KW_LEN(KW_REM); if (end > lineLen || strncasecmp(line + pos, KW_REM, KW_LEN(KW_REM)) != 0) { return false; } if (pos > 0 && basIsIdentChar(line[pos - 1])) { return false; } return end == lineLen || !basIsIdentChar(line[end]); } static bool isReplaceEnabled(void) { return sReplCheck && wgtCheckboxIsChecked(sReplCheck); } // joinProcArrays -- build the canonical file text from a (General) // buffer and a proc array: General, one blank line, then each proc // separated by one blank line. Returns a malloc'd string or NULL. static char *joinProcArrays(const char *general, char **procs) { int32_t totalLen = 0; int32_t procCount = (int32_t)arrlen(procs); if (general && general[0]) { totalLen += (int32_t)strlen(general); totalLen += 2; // newline + blank line separator } for (int32_t i = 0; i < procCount; i++) { if (procs[i]) { totalLen += (int32_t)strlen(procs[i]); totalLen += 2; // newline + blank line between procedures } } char *out = (char *)malloc(totalLen + 1); if (!out) { return NULL; } int32_t pos = 0; if (general && general[0]) { int32_t len = (int32_t)strlen(general); memcpy(out + pos, general, len); pos += len; if (out[pos - 1] != '\n') { out[pos++] = '\n'; } out[pos++] = '\n'; } for (int32_t i = 0; i < procCount; i++) { if (procs[i]) { int32_t len = (int32_t)strlen(procs[i]); memcpy(out + pos, procs[i], len); pos += len; if (pos > 0 && out[pos - 1] != '\n') { out[pos++] = '\n'; } // Blank line between procedures if (i < procCount - 1) { out[pos++] = '\n'; } } } out[pos] = '\0'; return out; } // joinProcBufs -- reassemble the editor's buffers into sFullSourceCache // and rebuild sProcTable from the result so both always describe the // same text. Does not save the editor first (see getFullSource). static const char *joinProcBufs(void) { free(sFullSourceCache); sFullSourceCache = joinProcArrays(sGeneralBuf, sProcBufs); rebuildProcTable(); return sFullSourceCache ? sFullSourceCache : ""; } // kwMatch -- case-insensitive prefix test against a keyword tag static bool kwMatch(const char *p, const char *kw) { return strncasecmp(p, kw, strlen(kw)) == 0; } // lineCopy -- copy 1-based line `line` of text (without its newline) into // out, truncated to fit. Returns the copied length. static int32_t lineCopy(const char *text, int32_t line, char *out, int32_t outSize) { const char *start = text + lineStartOffset(text, line); int32_t len = (int32_t)strcspn(start, "\n"); if (len > outSize - 1) { len = outSize - 1; } memcpy(out, start, (size_t)len); out[len] = '\0'; return len; } // lineStartOffset -- byte offset where 1-based line `line` of text starts // (the text's end when it has fewer lines) static int32_t lineStartOffset(const char *text, int32_t line) { const char *p = text; for (int32_t ln = 1; *p && ln < line; ln++) { p = skipLine(p); } return (int32_t)(p - text); } static void listRowsAdd(IdeListRowsT *rows, const char *text) { char *copy = strdup(text); if (copy) { arrput(rows->strs, copy); } arrput(rows->cells, copy ? copy : ""); } static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount) { wgtListViewSetData(list, rowCount > 0 ? rows->cells : NULL, rowCount); } static void listRowsFree(IdeListRowsT *rows) { listRowsReset(rows); arrfree(rows->strs); arrfree(rows->cells); rows->strs = NULL; rows->cells = NULL; } static void listRowsReset(IdeListRowsT *rows) { for (int32_t i = 0; i < (int32_t)arrlen(rows->strs); i++) { free(rows->strs[i]); } arrsetlen(rows->strs, 0); arrsetlen(rows->cells, 0); } // loadCompileSource -- the code text of project file fileIdx, normalized // to the canonical proc layout (malloc'd). Loaded buffers are // authoritative; disk is only read for files never loaded. For a .frm // the result is just its code section, *outCtrlList gets its controls for // the BEGINFORM line (basFrmControlList, malloc'd; NULL for other files) // and the same single parse adds them to validator (when non-NULL). // Returns NULL when the file cannot be read. static char *loadCompileSource(int32_t fileIdx, IdeValidatorCtxT *validator, char **outCtrlList) { bool missing = false; char *owned = NULL; const char *text = projectFileText(fileIdx, &owned, &missing); if (!text) { return NULL; } IdeFrmScanT scan; const char *code = text; scan.validator = validator; scan.code = NULL; *outCtrlList = NULL; if (sProject.files[fileIdx].kind == PrjKindFormE) { int32_t textLen = (int32_t)strlen(text); frmScan(text, &scan); // The controls the form declares (the list never outgrows the text) *outCtrlList = (char *)malloc((size_t)textLen + 1); if (!*outCtrlList || basFrmControlList(text, textLen, *outCtrlList, textLen + 1) < 0) { free(*outCtrlList); free(scan.code); free(owned); *outCtrlList = NULL; return NULL; } // The designer's form: its code is exactly what the editor numbers if (fileIdx == sDesignerFileIdx && sDesigner.form) { code = sDesigner.form->code ? sDesigner.form->code : ""; } else { code = scan.code ? scan.code : ""; } } char *packed = packProcLayout(code); free(scan.code); free(owned); return packed; } // loadDesignerFile -- load form file fileIdx into the designer (a new, // blank form named after the file when it does not exist yet). Returns // false, keeping the current designer, when it exists but cannot be read. static bool loadDesignerFile(int32_t fileIdx) { PrjFileT *target = &sProject.files[fileIdx]; bool missing = false; char *owned = NULL; const char *frmSrc = projectFileText(fileIdx, &owned, &missing); if (!frmSrc && !missing) { reportUnreadable(target->path); return false; } designerRelease(); if (frmSrc && frmSrc[0]) { dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc)); } else { // New blank form -- derive name from filename char formName[BAS_MAX_IDENT]; const char *base = platformPathBaseName(target->path); snprintf(formName, sizeof(formName), "%.*s", (int)strcspn(base, "."), base); dsgnNewForm(&sDesigner, formName); target->modified = true; } free(owned); sDesignerFileIdx = fileIdx; if (sDesigner.form) { snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name); } return true; } static void loadFile(void) { FileFilterT filters[] = { { "BASIC Files (*.bas)" }, { "Form Files (*.frm)" }, { "Class Modules (*.cls)" }, { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) { return; } openSinglePath(path); } // loadFormCodeIntoEditor -- bind the editor to the designer form's code // (whatever the editor held goes back to its own file first). static void loadFormCodeIntoEditor(void) { if (!sDesigner.form) { return; } saveCurProc(true); stashEditor(); parseProcs(sDesigner.form->code ? sDesigner.form->code : ""); sEditorFileIdx = sDesignerFileIdx; if (!sCodeWin) { showCodeWindow(); } bool saved = sDropdownNavSuppressed; sDropdownNavSuppressed = true; updateDropdowns(); sDropdownNavSuppressed = saved; showProc(IDE_SECTION_GENERAL); } // loadFrmFiles -- register every .frm listed in the current project with // the form runtime. The text is the same the compile used (loaded // buffers, disk only for never-loaded files). Registration only caches // the text; the caller's basFormRtLoadAllForms then instantiates them, // so a Form_Load that does "Load Form2" finds Form2's .frm regardless of // project file order. static void loadFrmFiles(BasFormRtT *rt) { for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].kind != PrjKindFormE) { continue; } bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); char formName[BAS_MAX_IDENT]; if (text && basExtractFormName(text, formName, sizeof(formName))) { basFormRtRegisterFrm(rt, formName, text, (int32_t)strlen(text)); } free(owned); } } // loadModuleIntoEditor -- bind the editor to .bas file fileIdx holding source static void loadModuleIntoEditor(int32_t fileIdx, const char *source) { parseProcs(source); sEditorFileIdx = fileIdx; updateDropdowns(); showProc(IDE_SECTION_GENERAL); } 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); } // Convert an editor-local line number to a full-source line number, or // IDE_NO_LINE when that line was not compiled (e.g. a class module's // header). The editor shows one procedure at a time (sCurProcIdx), so // the procedure's starting line within the file's code is added, then // the source map places the file's line in the concatenated source (its // startLine already accounts for the injected BEGINFORM / BEGINCLASS). static int32_t localToConcatLine(int32_t editorLine) { int32_t fileLine = editorLineToCodeLine(editorLine); // Editor text bound to no project file is compiled on its own if (sEditorFileIdx < 0) { return fileLine; } int32_t concatLine = prjMapFileLine(&sProject, sEditorFileIdx, fileLine); return concatLine > 0 ? concatLine : IDE_NO_LINE; } // logFlushLine -- write the pending PRINT line to the log (Output to Log) static void logFlushLine(void) { sLogLine[sLogLineLen] = '\0'; dvxLog("BASIC: %s", sLogLine); sLogLineLen = 0; } // lookupWatchVar -- evaluate a simple watch expression by reading the // paused VM directly. Supports varName, varName(idx, ...), varName.field // and varName(idx).field; anything else falls back to evalWatchExpr. static bool lookupWatchVar(const char *expr, BasValueT *outVal) { const char *end = NULL; BasValueT *slot = resolveVarPath(dvxSkipWs(expr), &end); if (!slot || !end || *dvxSkipWs(end) != '\0') { return false; } *outVal = *slot; return true; } static void makeExecutable(void) { if (!hasProject()) { 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, DVX_ARRAY_LEN(filters), outPath, sizeof(outPath))) { return; } // Ask debug or release const char *modeItems[] = { "Debug (include error info)", "Release (smaller, no debug info)" }; int32_t modeChoice = 0; if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, DVX_ARRAY_LEN(modeItems), 0, &modeChoice)) { return; } bool release = (modeChoice == 1); setStatus(release ? "Building release executable..." : "Building debug executable..."); dvxSetBusy(sAc, true); dvxUpdate(sAc); // Gather every .frm text (unsaved editor buffers win over disk). char **frmSources = NULL; // stb_ds: owned form text for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].kind != PrjKindFormE) { continue; } bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); char *frmSrc = owned ? owned : (text ? strdup(text) : NULL); if (frmSrc) { arrput(frmSources, frmSrc); } } 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.projectDir = sProject.projectDir; spec.iconPath = sProject.iconPath; spec.helpFile = sProject.helpFile; spec.selfPath = sCtx->appPath; spec.module = sCachedModule; spec.frmSources = (const char *const *)frmSources; spec.frmCount = (int32_t)arrlen(frmSources); spec.release = release; const char *failure = basBuildApp(outPath, &spec); for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) { free(frmSources[i]); } arrfree(frmSources); dvxSetBusy(sAc, false); if (failure) { setStatus(failure); return; } char msg[IDE_MSG_BUF]; snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug"); setStatus(msg); } // memberVarSlot -- member variable dv (SCOPE_MEMBER) of the object the // innermost procedure runs for, or NULL when that procedure is not a // procedure of dv's class static BasValueT *memberVarSlot(const BasDebugVarT *dv) { if (sVm->callDepth <= 0) { return NULL; } const BasValueT *me = &sVm->callStack[sVm->callDepth - 1].me; if (me->type != BAS_TYPE_INSTANCE) { return NULL; } BasObjectT *obj = me->instVal; const char *cls = objectClassName(obj); if (!cls || strcasecmp(cls, dv->formName) != 0 || dv->index < 0 || dv->index >= obj->fieldCount) { return NULL; } return &obj->fields[dv->index]; } static void navigateToBreakpoint(int32_t bpIdx) { if (bpIdx < 0 || bpIdx >= (int32_t)arrlen(sBreakpoints)) { return; } navigateToCodeLine(sBreakpoints[bpIdx].fileIdx, sBreakpoints[bpIdx].codeLine, false); } // ============================================================ // navigateToCodeLine -- show a specific file/line in the code editor // ============================================================ // // fileIdx: project file index (-1 = the editor's current code) // codeLine: 1-based line within the file's code section // setDbgLine: if true, update sDbgCurrentLine for the debug decorator // // The procedure is the one whose line range holds codeLine (else the // General section), found after the file is active. Returns the shown // proc index (IDE_SECTION_GENERAL for General), or IDE_SECTION_NONE when // the file could not be activated. static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbgLine) { if (fileIdx >= 0 && !activateFile(fileIdx, ViewCodeE)) { return IDE_SECTION_NONE; } if (!sCodeWin) { showCodeWindow(); } if (sCodeWin && !sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } if (!sEditor) { return IDE_SECTION_NONE; } // Line numbers refer to the current buffers; the editor-local line is // taken before showProc, which may drop an untouched skeleton. getFullSource(); int32_t procIdx = procIndexForCodeLine(codeLine); int32_t editorLine = (procIdx >= 0) ? codeLine - sProcTable[procIdx].lineNum + 1 : codeLine; if (procIdx != sCurProcIdx) { showProc(procIdx); } // Update dropdowns to match if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) { selectDropdowns(sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName); } else if (sObjDropdown) { wgtDropdownSetSelected(sObjDropdown, 0); } if (setDbgLine) { sDbgCurrentLine = editorLine; } wgtTextAreaGoToLine(sEditor, editorLine); wgtInvalidatePaint(sEditor); return sCurProcIdx; } // navigateToEventSub -- open code editor at the default event sub for the // selected control (or form). 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(IDE_FORM_TYPE_NAME); } navigateToNamedEventSub(ctrlName, eventName); } // navigateToNamedEventSub -- open the code editor at Ctrl_Event, creating // 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; } // The designer may show a form while another file is active if (sDesignerFileIdx >= 0 && sDesignerFileIdx != sProject.activeFileIdx && !activateFile(sDesignerFileIdx, ViewCodeE)) { return; } // 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); int32_t procIdx = -1; for (int32_t i = 0; i < procCount; i++) { if (strcasecmp(sProcTable[i].objName, ctrlName) == 0 && strcasecmp(sProcTable[i].evtName, eventName) == 0) { procIdx = i; break; } } if (procIdx >= 0) { showProc(procIdx); } else { createEventSubSkeleton(ctrlName, eventName); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } setStatus("Code view."); selectDropdowns(ctrlName, eventName); } // newClassModule -- add class module ClassN (the first N whose name no // form or class of the project uses and whose file does not exist yet), // holding the standard header, to the project and open it in the editor. static void newClassModule(void) { char name[BAS_MAX_IDENT]; char fileName[DVX_MAX_PATH]; char path[DVX_MAX_PATH * 2]; for (int32_t n = 1; ; n++) { snprintf(name, sizeof(name), IDE_NEW_CLASS_FMT, (int)n); snprintf(fileName, sizeof(fileName), "%s%s", name, BAS_CLS_EXT); snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", sProject.projectDir, fileName); if (!classNameUsed(name, fileName) && !prjPathExists(path)) { break; } } char text[sizeof(IDE_NEW_CLASS_TEXT) + BAS_MAX_IDENT]; snprintf(text, sizeof(text), IDE_NEW_CLASS_TEXT, name); bool wasDirty = sProject.dirty; int32_t idx = prjAddFile(&sProject, fileName, PrjKindClassE); sProject.files[idx].buffer = strdup(text); sProject.files[idx].modified = true; if (!sProject.files[idx].buffer || !activateFile(idx, ViewCodeE)) { prjRemoveFile(&sProject, idx); sProject.dirty = wasDirty; return; } invalidateCompiled(); prjRebuildTree(&sProject); updateDirtyIndicators(); setStatus("Class module added."); } static void newProject(void) { if (!promptAndSave()) { return; } 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%s", name, IDE_DBP_EXT); 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); saveProjectFile(); ensureProjectWindow(false); updateMainTitle(); setStatus("New project created."); updateProjectMenuState(); } // objectClassName -- the class module name of class instance obj in the // running program, or NULL (a Collection) static const char *objectClassName(const BasObjectT *obj) { const BasModuleT *mod = sVm->module; return (mod && obj->classIdx >= 0 && obj->classIdx < mod->classCount) ? mod->classes[obj->classIdx].name : NULL; } static void onBreakpointHit(void *ctx, int32_t line) { (void)ctx; // The VM fires this with line == -1 at end-of-sub after a breakpointed // handler finishes; that is not a real pause point (every genuine pause -- // breakpoint or Step Into/Over/Out -- arrives with a positive line), so // ignore it. Otherwise Continue would re-pause and require a second press. // After Stop, a handler still run while runModule unwinds never pauses: // the state stays Stopping and no debugger UI is driven. if (line < 0 || sStopRequested || sDbgState == DbgStoppingE) { return; } sDbgState = DbgPausedE; 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 != KEY_DELETE && keyCode != KEY_ASCII_DEL) { return; } if (!sBreakpointList || arrlen(sBreakpoints) == 0) { return; } // Remove selected breakpoints in reverse order to preserve indices for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) { if (wgtListViewIsItemSelected(sBreakpointList, i)) { arrdel(sBreakpoints, i); } } syncVmBreakpoints(); updateBreakpointWindow(); // Repaint editor to remove breakpoint dots if (sEditor) { wgtInvalidatePaint(sEditor); } } 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 debugStop(); // 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 the project (designer, code, project windows and buffers), // then whatever can exist without one (toolbox/properties). closeProject(); closeProjectUi(); if (sOutWin) { dvxDestroyWindow(sAc, sOutWin); sOutWin = NULL; } if (sImmWin) { dvxDestroyWindow(sAc, sImmWin); sImmWin = NULL; } if (sLocalsWin) { dvxDestroyWindow(sAc, sLocalsWin); sLocalsWin = NULL; sLocalsList = NULL; } if (sCallStackWin) { dvxDestroyWindow(sAc, sCallStackWin); sCallStackWin = NULL; sCallStackList = NULL; } if (sWatchWin) { dvxDestroyWindow(sAc, sWatchWin); sWatchWin = NULL; sWatchList = NULL; sWatchInput = NULL; } if (sBreakpointWin) { dvxDestroyWindow(sAc, sBreakpointWin); sBreakpointWin = NULL; sBreakpointList = NULL; } // win itself is destroyed last, below, once nothing refers to it sWin = NULL; // A running program is still executing sCachedModule (this close // arrived through its event pump); runModule frees it on exit. if (sCachedModule && !sVm) { basModuleFree(sCachedModule); sCachedModule = NULL; } arrfree(sProcTable); sProcTable = NULL; for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) { free((void *)sObjItems[i]); } evtItemsReset(); arrfree(sObjItems); arrfree(sEvtItems); arrfree(sWithEvents); sObjItems = NULL; sEvtItems = NULL; sWithEvents = NULL; listRowsFree(&sBpRows); listRowsFree(&sLocalsRows); listRowsFree(&sCallRows); listRowsFree(&sWatchRows); for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprs); i++) { free(sWatchExprs[i]); } arrfree(sWatchExprs); sWatchExprs = NULL; for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) { free(sRecentFiles[i]); } arrfree(sRecentFiles); sRecentFiles = NULL; arrfree(sBreakpoints); sBreakpoints = NULL; arrfree(sVmBreakpoints); sVmBreakpoints = NULL; arrfree(sBreakTitles); sBreakTitles = NULL; free(sFullSourceCache); sFullSourceCache = NULL; free(sEditorPrevText); sEditorPrevText = NULL; shfree(sSyntaxMap); sSyntaxMap = NULL; prefsClose(sPrefs); sPrefs = NULL; // The accelerator table is shared by every IDE window; all of them // are gone by now, so release it before the main window goes. dvxFreeAccelTable(win->accelTable); win->accelTable = NULL; dvxDestroyWindow(sAc, win); } static void onCodeWinClose(WindowT *win) { // Stash code back before the window is destroyed: a module's text to // its project buffer, a form's to its code. The proc buffers stay, // but no section is shown any more (a reopened editor starts empty). saveCurProc(true); stashEditor(); dvxDestroyWindow(sAc, win); sCodeWin = NULL; sEditor = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; sCurProcIdx = IDE_SECTION_NONE; if (sLastFocusWin == win) { sLastFocusWin = NULL; } updateProjectMenuState(); } // Preferences Colors tab: Change... (or a double-click on the list) picks // a new color for the selected entry in the color dialog. static void onColorChange(WidgetT *w) { (void)w; int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList); if (idx < 0 || idx >= TEXT_SYNTAX_MAX) { return; } if (dvxColorDialog(sAc, sSyntaxColorNames[idx], &sPrefsDlg.syntaxColors[idx])) { prefsUpdateColorSwatch(); } } static void onColorListChange(WidgetT *w) { (void)w; prefsUpdateColorSwatch(); } static void onContentFocus(WindowT *win) { sLastFocusWin = win; } static void onEditorChange(WidgetT *w) { (void)w; if (!sEditor) { return; } // Breakpoints follow lines that moved; the snapshot is kept current on // every edit so each diff covers exactly one change. const char *text = wgtGetText(sEditor); int32_t lines = countLines(text); int32_t delta = lines - sEditorLineCount; if (delta != 0 && sEditorPrevText) { shiftBreakpointsForEdit(sEditorPrevText, text, delta); } editorSnapshot(lines); // Mark the file the editor is bound to as modified; the indicators // only need a refresh when that is news. if (sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject)) { PrjFileT *file = &sProject.files[sEditorFileIdx]; bool wasModified = file->modified; file->modified = true; // Only mark form dirty when editing the form's code, not a .bas file if (sEditorFileIdx == sDesignerFileIdx && sDesigner.form) { sDesigner.form->dirty = true; } if (!wasModified) { 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, IDE_GLOBAL_SECTION) == 0) { showProc(IDE_SECTION_GENERAL); return; } // Strip brackets if present (unimplemented event) char evtName[BAS_MAX_IDENT]; 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; } } // (General) lists existing procedures only; a stale entry has no // event to generate a handler for. if (strcasecmp(selObj, IDE_GENERAL_SECTION) == 0) { showProc(IDE_SECTION_GENERAL); return; } createEventSubSkeleton(selObj, evtName); } static void onFindClose(WindowT *win) { (void)win; closeFindDialog(); } static void onFindCloseBtn(WidgetT *w) { (void)w; closeFindDialog(); } static void onFindNext(WidgetT *w) { (void)w; // The dialog's text wins; with the dialog closed (F3), the last // search is repeated. if (sFindInput) { const char *needle = wgtGetText(sFindInput); if (!needle || !needle[0]) { return; } snprintf(sFindText, sizeof(sFindText), "%s", needle); } if (sFindText[0] && !findInProject(sFindText, getFindMatchCase(), getFindForward(), getFindScope())) { 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+X/C/V and Del act on the selected control, like the Edit menu if ((mod & ACCEL_CTRL) && (key == KEY_CTRL_X || key == KEY_CTRL_C || key == KEY_CTRL_V)) { designerEditCmd(key == KEY_CTRL_X ? CMD_CUT : (key == KEY_CTRL_C ? CMD_COPY : CMD_PASTE)); return; } if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) { designerEditCmd(CMD_DELETE); 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, IDE_MENU_EVENT); return; } } } onMenu(win, menuId); } // onFormWinMouse -- handle mouse events on the form designer window. // Coordinates are relative to the window's client area (content box origin). 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; } bool dirtyBefore = sDesigner.form->dirty; if (isDown && !wasDown) { // A click in the designer makes it the Edit menu's target sLastFocusWin = sFormWin; // 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); dvxInvalidateWindow(sAc, sFormWin); if (clicks >= 2 && sDesigner.activeTool[0] == '\0') { navigateToEventSub(); } } else if (isDown && wasDown) { // Drag dsgnOnMouse(&sDesigner, x, y, true); prpRefresh(&sDesigner); dvxInvalidateWindow(sAc, sFormWin); } else if (!isDown && wasDown) { // Release dsgnOnMouse(&sDesigner, x, y, false); prpRefresh(&sDesigner); dvxInvalidateWindow(sAc, sFormWin); } lastButtons = buttons; // Plain moves change nothing; only a change of dirty state is news if (sDesigner.form && sDesigner.form->dirty && !dirtyBefore) { 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 dsgnPaintOverlay(&sDesigner); } // onFormWinResize -- update form dimensions when the design window is // resized; only a real size change makes the form dirty. static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) { // Let the widget system handle the layout recalculation widgetOnResize(win, newW, newH); // Store the outer window dimensions (win->w/h), not the content // dimensions (newW/newH), since dvxResizeWindow takes outer dims. if (sDesigner.form && (sDesigner.form->width != win->w || sDesigner.form->height != win->h)) { bool wasDirty = sDesigner.form->dirty; sDesigner.form->width = win->w; sDesigner.form->height = win->h; sDesigner.form->dirty = true; prpRefresh(&sDesigner); if (!wasDirty) { updateDirtyIndicators(); } } } static void onGutterClick(WidgetT *w, int32_t lineNum) { (void)w; toggleBreakpointLine(lineNum); } // onImmediateKeyDown -- Enter in the Immediate window evaluates the line // it was pressed on. The textarea has already split that line at the // caret, so the statement is the line above the caret joined with the // part the split moved down (empty when Enter was pressed at the end). // A full window cannot take the newline, so nothing was split: the user // is told instead of evaluating the wrong lines. static void onImmediateKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) { (void)w; (void)shift; if (keyCode != KEY_ENTER || !sImmediate) { return; } const char *text = wgtGetText(sImmediate); int32_t caretLine = wgtTextAreaGetCursorLine(sImmediate); // The statement is on the line above the caret; there is none on line 1 if (!text || caretLine <= 1) { return; } if ((int32_t)strlen(text) >= IDE_MAX_IMM - 1) { setStatus("The Immediate window is full; delete some text and press Enter again."); return; } char expr[IDE_IMM_LINE_BUF]; int32_t len = lineCopy(text, caretLine - 1, expr, (int32_t)sizeof(expr)); lineCopy(text, caretLine, expr + len, (int32_t)sizeof(expr) - len); if (*dvxSkipWs(expr) != '\0') { evaluateImmediate(expr); } } // The IDE keeps sImmWin/sImmediate for its whole lifetime; destroying // the window here would leave them dangling. static void onImmWinClose(WindowT *win) { dvxHideWindow(sAc, win); } 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) { shellOpenHelp(sAc, sIdeHelpFile); } if (menuId == CMD_HELP_API) { shellOpenHelp(sAc, DVX_SYSTEM_HELP_FILE_LITERAL); } 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. 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]; // A WithEvents variable's events are its class's. Read first: reading // the editor's own class rebuilds sProcTable, which the list of events // with code below points into. const IdeWithEventsT *withEvents = findWithEvents(selObj); IdeClassEventT *classEvents = NULL; if (withEvents) { classEventsOf(withEvents->className, &classEvents); } // Collect which events already have code evtItemsReset(); 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); } } if (strcasecmp(selObj, IDE_GENERAL_SECTION) == 0) { // Always include (Global) to access module-level code evtItemAdd(IDE_GLOBAL_SECTION, false); for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) { evtItemAdd(existingEvts[i], false); } arrfree(existingEvts); arrfree(classEvents); 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; } // 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; } } } // The control's type and widget interface (its own events) const WgtIfaceT *iface = NULL; const char *ctrlType = 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); ctrlType = sDesigner.form->controls[i]->typeName; if (wgtName) { iface = wgtGetIface(wgtName); } break; } } } // Standard events for this kind of object, from the master table // (NULL-terminated stb_ds temp array); a WithEvents variable's are its // class's Events const char **availEvents = NULL; if (withEvents) { for (int32_t i = 0; i < (int32_t)arrlen(classEvents); i++) { arrput(availEvents, classEvents[i].name); } } else if (isMenuItem) { arrput(availEvents, IDE_MENU_EVENT); } else { bool draws = (ctrlType != NULL && strcasecmp(ctrlType, BAS_PICTUREBOX_TYPE) == 0); uint8_t scope = isForm ? BAS_EVT_SCOPE_FORM : (BAS_EVT_SCOPE_CTRL | (draws ? BAS_EVT_SCOPE_DRAW : 0)); for (int32_t i = 0; i < IDE_EVENT_COUNT; i++) { if (sEventTable[i].scope & scope) { arrput(availEvents, sEventTable[i].suffix); } } } arrput(availEvents, NULL); // Build the event list: standard events + widget-specific events // 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; } } evtItemAdd(availEvents[i], !hasCode); } // 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; } } evtItemAdd(evtName, !hasCode); } } // 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++) { if (evtLabelMatches(sEvtItems[j], existingEvts[i])) { alreadyListed = true; break; } } if (!alreadyListed) { evtItemAdd(existingEvts[i], false); } } arrfree(existingEvts); arrfree(availEvents); // The default event of a WithEvents variable: its class's first const char *withEventsDefault = NULL; char firstEvent[BAS_MAX_IDENT]; if (arrlen(classEvents) > 0) { snprintf(firstEvent, sizeof(firstEvent), "%s", classEvents[0].name); withEventsDefault = firstEvent; } arrfree(classEvents); 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 = withEventsDefault; if (isForm) { defEvt = dsgnDefaultEvent(IDE_FORM_TYPE_NAME); } else if (!withEvents && 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 = IDE_FALLBACK_EVENT; } // Find the default event in the list (it will be bracketed) for (int32_t i = 0; i < evtCount; i++) { if (evtLabelMatches(sEvtItems[i], defEvt)) { wgtDropdownSetSelected(sEvtDropdown, i); onEvtDropdownChange(sEvtDropdown); break; } } } } } // Hide (not destroy): sOutWin/sOutput stay bound for PRINT output. static void onOutWinClose(WindowT *win) { dvxHideWindow(sAc, win); } static void onPrefsCancel(WidgetT *w) { (void)w; sPrefsDlg.done = true; } // onPrefsClose -- the window's close box cancels; showPreferencesDialog // destroys the window once its loop sees done. static void onPrefsClose(WindowT *win) { (void)win; sPrefsDlg.done = true; } static void onPrefsOk(WidgetT *w) { (void)w; sPrefsDlg.accepted = true; sPrefsDlg.done = true; } static void onPrjFileDblClick(int32_t fileIdx) { 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 : ""); bool caseSens = getFindMatchCase(); bool forward = getFindForward(); // Single Replace always acts on the editor currently shown: if its // selection matches the search term, splice in the replacement and // advance to the next match; otherwise just find next. Find Next's // own scope/direction handling (which may switch procs or files) is // only reached when there is no matching selection to replace, so a // cross-file single replace stays unambiguous -- consistent with how // onReplaceAll treats each scope as a sequence of in-editor passes. if (wgtTextAreaReplaceSelection(sEditor, sFindText, sReplaceText, caseSens, forward)) { return; } 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) { int32_t needleLen = (int32_t)strlen(sFindText); // Every file is searched in its project buffer (stashCurrentFile // also saves the editor into its sections first) if (!stashCurrentFile()) { return; } for (int32_t i = 0; i < prjFileCount(&sProject); i++) { // Skip files whose code has no match at all if (!fileCodeContains(i, sFindText, needleLen, caseSens)) { continue; } if (!activateFile(i, ViewCodeE)) { break; } totalCount += replaceInSections(NULL, caseSens); } } else if (scope == ScopeFileE && sEditor) { totalCount = replaceInSections(NULL, caseSens); } else if (scope == ScopeObjE && sEditor) { char objName[BAS_MAX_IDENT]; currentObjName(objName, sizeof(objName)); if (objName[0]) { totalCount = replaceInSections(objName, caseSens); } } char statusBuf[BAS_MAX_IDENT]; 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 char *copy = strdup(text); if (!copy) { return; } arrput(sWatchExprs, copy); 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 != KEY_DELETE && keyCode != KEY_ASCII_DEL) { 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(); } // openAnyPath -- open a project (.dbp) in place of the current one, or // a .bas/.frm (see openSinglePath) static void openAnyPath(const char *path) { if (dvxHasExt(platformPathBaseName(path), IDE_DBP_EXT)) { if (promptAndSave()) { openProjectPath(path); } } else { openSinglePath(path); } } 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", IDE_FIND_WIN_W, IDE_FIND_WIN_H, false); if (!sFindWin) { return; } sFindWin->onClose = onFindClose; sFindWin->onMenu = onMenu; sFindWin->accelTable = sWin ? sWin->accelTable : NULL; if (!findDialogBuild(showReplace)) { closeFindDialog(); ideErrorBox("Not enough memory to open Find / Replace."); return; } // Set initial replace enable state onReplCheckChange(sReplCheck); dvxFitWindow(sAc, sFindWin); } static void openProject(void) { if (!promptAndSave()) { return; } FileFilterT filters[] = { { "Project Files (*.dbp)" }, { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) { return; } openProjectPath(path); } // openProjectPath -- open a .dbp in place of the current project. The // new project is loaded first; if that fails the current one stays open // and the path is dropped from the Recent list. static void openProjectPath(const char *path) { PrjStateT *loaded = (PrjStateT *)malloc(sizeof(PrjStateT)); if (!loaded) { ideErrorBox("Not enough memory to open the project."); return; } prjInit(loaded); // prjLoad fails for a missing .dbp, so a stale recent entry is // rejected here instead of opening as a phantom empty project. if (!prjLoad(loaded, path)) { prjClose(loaded); free(loaded); ideErrorBox("Could not open project file."); recentRemove(path); return; } closeProject(); sProject = *loaded; free(loaded); prjLoadAllFiles(&sProject, sAc); ensureProjectWindow(false); updateMainTitle(); setStatus("Project loaded."); updateProjectMenuState(); recentAdd(path); } // openSinglePath -- open a .bas/.cls/.frm: added to the current project, or // used to create an implicit project when none is open. static void openSinglePath(const char *path) { if (hasProject()) { if (addFileToProject(path)) { recentAdd(path); } return; } if (!promptAndSave()) { return; } ensureProject(path); ensureProjectWindow(true); updateProjectMenuState(); recentAdd(path); } // outputAppend -- add program/report text to the Output buffer, dropping // the oldest lines when it is full so the newest text always shows static void outputAppend(const char *text, int32_t len) { sOutputLen = appendDroppingLines(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, text, len); sOutputPending = true; } // outputLog -- with Output to Log on, log a compile or runtime report static void outputLog(const char *text) { if (sOutputToLog && text[0]) { dvxLog("BASIC: %s", text); } } // packProcLayout -- normalize source text to the canonical proc layout // (see joinProcArrays). Returns a malloc'd string or NULL. static char *packProcLayout(const char *source) { char *general = NULL; char **procs = NULL; if (!splitProcs(source, &general, &procs, NULL)) { return NULL; } char *packed = joinProcArrays(general, procs); free(general); for (int32_t i = 0; i < (int32_t)arrlen(procs); i++) { free(procs[i]); } arrfree(procs); return packed; } // parseIndexList -- parse "i1, i2, ...)" starting just after the '('. // Returns the position after the closing ')' (or where parsing stopped). static const char *parseIndexList(const char *p, int32_t *indices, int32_t *outCount) { int32_t n = 0; while (*p && *p != ')' && n < BAS_ARRAY_MAX_DIMS) { const char *start = p; p = dvxSkipWs(p); indices[n++] = atoi(p); // Skip past the number if (*p == '-') { p++; } while (*p >= '0' && *p <= '9') { p++; } p = dvxSkipWs(p); if (*p == ',') { p++; } // Anything unparseable would otherwise spin forever if (p == start) { break; } } if (*p == ')') { p++; } *outCount = n; return p; } // parseProcs -- split source into (General) + per-procedure buffers static void parseProcs(const char *source) { freeProcBufs(); // Out of memory: keep the text whole in (General) rather than lose it if (!splitProcs(source ? source : "", &sGeneralBuf, &sProcBufs, NULL)) { sGeneralBuf = strdup(source ? source : ""); } } // pasteParseControl -- parse one "Begin ... End" block with // the designer's .frm loader by wrapping it in a scratch form. Returns // the loaded control record (the caller owns it), or NULL. static DsgnControlT *pasteParseControl(const char *clip, int32_t clipLen) { static const char head[] = FRM_BEGIN_TAG IDE_FORM_TYPE_NAME " " IDE_PASTE_FORM "\n"; static const char tail[] = "\n" FRM_END_TAG "\n"; int32_t len = (int32_t)(sizeof(head) - 1) + clipLen + (int32_t)(sizeof(tail) - 1); char *text = (char *)malloc((size_t)len + 1); if (!text) { return NULL; } memcpy(text, head, sizeof(head) - 1); memcpy(text + sizeof(head) - 1, clip, (size_t)clipLen); memcpy(text + sizeof(head) - 1 + clipLen, tail, sizeof(tail)); DsgnStateT tmp; DsgnControlT *ctrl = NULL; memset(&tmp, 0, sizeof(tmp)); if (dsgnLoadFrm(&tmp, text, len) && tmp.form && arrlen(tmp.form->controls) > 0) { ctrl = tmp.form->controls[0]; arrdel(tmp.form->controls, 0); } dsgnFree(&tmp); free(text); return ctrl; } // pathExists -- true when a file can be opened for reading // prefsBuild -- build the Preferences dialog's widgets into win and load // the current settings. Returns false if any widget could not be made. static bool prefsBuild(WindowT *win) { WidgetT *root = wgtInitWindow(sAc, win); WidgetT *tabs = root ? wgtTabControl(root) : NULL; if (!tabs) { return false; } root->spacing = wgtPixels(IDE_WIDGET_SPACING); tabs->weight = WGT_WEIGHT_FILL; // ======== General tab ======== WidgetT *generalPage = wgtTabPage(tabs, "General"); WidgetT *edFrame = generalPage ? wgtFrame(generalPage, "Editor") : NULL; if (!edFrame) { return false; } generalPage->spacing = wgtPixels(IDE_WIDGET_SPACING); edFrame->spacing = wgtPixels(IDE_WIDGET_SPACING_SM); sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming"); sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "OPTION EXPLICIT default for new projects"); WidgetT *tabRow = wgtHBox(edFrame); if (!sPrefsDlg.renameSkipComments || !sPrefsDlg.optionExplicit || !tabRow || !wgtLabel(tabRow, "Tab width:")) { return false; } wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true)); wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, false)); tabRow->spacing = wgtPixels(IDE_WIDGET_SPACING); sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, IDE_PREFS_TAB_INPUT_MAX); sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs"); if (!sPrefsDlg.tabWidthInput || !sPrefsDlg.useSpaces) { return false; } char tabBuf[IDE_TAB_WIDTH_BUF]; snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH)); sPrefsDlg.tabWidthInput->maxW = wgtPixels(IDE_PREFS_TAB_INPUT_W); wgtSetText(sPrefsDlg.tabWidthInput, tabBuf); wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, true)); // Project Defaults section WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults"); if (!prjFrame) { return false; } prjFrame->spacing = wgtPixels(IDE_WIDGET_SPACING_SM); prjFrame->weight = WGT_WEIGHT_FILL; sPrefsDlg.defAuthor = prefsTextRow(prjFrame, "Author:", IDE_PREFS_TEXT_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, "")); sPrefsDlg.defPublisher = prefsTextRow(prjFrame, "Publisher:", IDE_PREFS_TEXT_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, "")); sPrefsDlg.defVersion = prefsTextRow(prjFrame, "Version:", IDE_PREFS_VERSION_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, IDE_PREF_DEFAULT_VERSION)); sPrefsDlg.defCopyright = prefsTextRow(prjFrame, "Copyright:", IDE_PREFS_TEXT_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, "")); // One line, as long as a project's [Project] Description can be: an // INI value is a single line (see prjSingleLine). sPrefsDlg.defDescription = prefsTextRow(prjFrame, "Description:", PRJ_MAX_DESC - 1, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, "")); if (!sPrefsDlg.defAuthor || !sPrefsDlg.defPublisher || !sPrefsDlg.defVersion || !sPrefsDlg.defCopyright || !sPrefsDlg.defDescription) { return false; } // ======== Colors tab ======== // Load current colors from prefs (or defaults) for (int32_t i = 0; i < TEXT_SYNTAX_MAX; i++) { char key[IDE_PREF_KEY_BUF]; snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i); sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)textEditDefaultSyntaxRgb((uint8_t)i)); } WidgetT *colorsPage = wgtTabPage(tabs, "Colors"); WidgetT *colorsHBox = colorsPage ? wgtHBox(colorsPage) : NULL; // Left: color list. Right: swatch, values and Change... sPrefsDlg.colorList = colorsHBox ? wgtListBox(colorsHBox) : NULL; WidgetT *colorBox = sPrefsDlg.colorList ? wgtVBox(colorsHBox) : NULL; WidgetT *caption = colorBox ? wgtLabel(colorBox, "Color:") : NULL; sPrefsDlg.colorSwatch = caption ? wgtCanvas(colorBox, IDE_SWATCH_W, IDE_SWATCH_H) : NULL; sPrefsDlg.lblR = sPrefsDlg.colorSwatch ? wgtLabel(colorBox, "") : NULL; sPrefsDlg.lblG = sPrefsDlg.lblR ? wgtLabel(colorBox, "") : NULL; sPrefsDlg.lblB = sPrefsDlg.lblG ? wgtLabel(colorBox, "") : NULL; WidgetT *changeBtn = sPrefsDlg.lblB ? wgtButton(colorBox, "Change...") : NULL; if (!changeBtn) { return false; } colorsPage->spacing = wgtPixels(IDE_WIDGET_SPACING); colorsHBox->spacing = wgtPixels(IDE_WIDGET_SPACING_LG); colorsHBox->weight = WGT_WEIGHT_FILL; sPrefsDlg.colorList->weight = WGT_WEIGHT_FILL; sPrefsDlg.colorList->onChange = onColorListChange; sPrefsDlg.colorList->onDblClick = onColorChange; colorBox->spacing = wgtPixels(IDE_WIDGET_SPACING_SM); colorBox->weight = WGT_WEIGHT_FILL; changeBtn->onClick = onColorChange; wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, TEXT_SYNTAX_MAX); // Start on the Keywords entry (the most commonly customized) wgtListBoxSetSelected(sPrefsDlg.colorList, TEXT_SYNTAX_KEYWORD); prefsUpdateColorSwatch(); wgtTabControlSetActive(tabs, 0); // ---- OK / Cancel ---- WidgetT *btnRow = wgtHBox(root); WidgetT *btnOk = btnRow ? wgtButton(btnRow, "OK") : NULL; WidgetT *btnCancel = btnOk ? wgtButton(btnRow, "Cancel") : NULL; if (!btnCancel) { return false; } btnRow->spacing = wgtPixels(IDE_WIDGET_SPACING_LG); btnRow->align = AlignEndE; btnOk->minW = wgtPixels(IDE_PREFS_BUTTON_W); btnOk->onClick = onPrefsOk; btnCancel->minW = wgtPixels(IDE_PREFS_BUTTON_W); btnCancel->onClick = onPrefsCancel; return true; } // prefsSetChannelLabel -- "Channel value" under the Colors tab swatch static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value) { char buf[IDE_RGB_LABEL_BUF]; snprintf(buf, sizeof(buf), "%s %d", channel, (int)value); wgtSetText(label, buf); } // prefsTextRow -- "Caption: [input]" row holding value. Returns the input, // NULL if a widget could not be made. static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLen, const char *value) { WidgetT *row = wgtHBox(parent); WidgetT *label = row ? wgtLabel(row, caption) : NULL; WidgetT *input = label ? wgtTextInput(row, maxLen) : NULL; if (!input) { return NULL; } row->spacing = wgtPixels(IDE_WIDGET_SPACING); label->minW = wgtPixels(IDE_PREFS_LABEL_W); input->weight = WGT_WEIGHT_FILL; wgtSetText(input, value); return input; } // Shows the selected syntax color in the swatch and as numbers. static void prefsUpdateColorSwatch(void) { int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList); if (idx < 0 || idx >= TEXT_SYNTAX_MAX) { return; } uint32_t rgb = sPrefsDlg.syntaxColors[idx]; uint8_t r = DVX_RGB_RED(rgb); uint8_t g = DVX_RGB_GREEN(rgb); uint8_t b = DVX_RGB_BLUE(rgb); prefsSetChannelLabel(sPrefsDlg.lblR, "Red", r); prefsSetChannelLabel(sPrefsDlg.lblG, "Green", g); prefsSetChannelLabel(sPrefsDlg.lblB, "Blue", b); wgtCanvasClear(sPrefsDlg.colorSwatch, packColor(&sAc->display, r, g, b)); } // printCallback -- PRINT from the running program. The text is buffered // (the pane is refreshed once per event-loop pass, see flushOutput) and, // with Output to Log on, logged one line at a time. static void printCallback(void *ctx, const char *text, bool newline) { (void)ctx; if (!text) { return; } int32_t textLen = (int32_t)strlen(text); outputAppend(text, textLen); if (newline) { outputAppend("\n", 1); } if (!sOutputToLog) { return; } for (int32_t i = 0; i < textLen; i++) { if (sLogLineLen >= (int32_t)sizeof(sLogLine) - 1) { logFlushLine(); } sLogLine[sLogLineLen++] = text[i]; } if (newline) { logFlushLine(); } } // procBodyEmpty -- true when a proc's text has nothing but blank lines // between its declaration and its End Sub / End Function static bool procBodyEmpty(const char *procText) { bool isSub = false; const char *p = dvxSkipWs(procText); const char *afterKw = procDeclAt(p, &isSub); const char *endTag = (afterKw && !isSub) ? KW_END_FUNCTION : KW_END_SUB; for (p = skipLine(p); *p && !kwMatch(dvxSkipWs(p), endTag); p = skipLine(p)) { const char *q = dvxSkipWs(p); if (*q != '\n' && *q != '\r' && *q != '\0') { return false; } } return true; } // procBufContains -- true if needle occurs anywhere in a proc buffer. static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive) { if (!hay) { return false; } if (caseSensitive) { return strstr(hay, needle) != NULL; } return findSubstrNoCase(hay, needle, needleLen) != NULL; } // procDeclAt -- if p (already whitespace-skipped) starts a SUB or // FUNCTION declaration (Public or Private in front allowed), returns the // text after the keyword; else NULL. static const char *procDeclAt(const char *p, bool *outIsSub) { if (kwMatch(p, KW_PUBLIC)) { p = dvxSkipWs(p + KW_LEN(KW_PUBLIC)); } else if (kwMatch(p, KW_PRIVATE)) { p = dvxSkipWs(p + KW_LEN(KW_PRIVATE)); } if (kwMatch(p, KW_SUB)) { *outIsSub = true; return p + KW_LEN(KW_SUB); } if (kwMatch(p, KW_FUNCTION)) { *outIsSub = false; return p + KW_LEN(KW_FUNCTION); } return NULL; } // procIndexByName -- sProcTable index of the proc objName/evtName, or -1 static int32_t procIndexByName(const char *objName, const char *evtName) { for (int32_t i = 0; i < (int32_t)arrlen(sProcTable); i++) { if (strcasecmp(sProcTable[i].objName, objName) == 0 && strcasecmp(sProcTable[i].evtName, evtName) == 0) { return i; } } return -1; } // procIndexForCodeLine -- sProcTable index of the proc whose declaration // through End line range holds codeLine, or -1 for the (General) section static int32_t procIndexForCodeLine(int32_t codeLine) { for (int32_t i = 0; i < (int32_t)arrlen(sProcTable); i++) { if (codeLine >= sProcTable[i].lineNum && codeLine <= sProcTable[i].endLineNum) { return i; } } return -1; } // procNameFromDecl -- copy the procedure name that follows a SUB/FUNCTION // keyword (stops at '(' or whitespace). static void procNameFromDecl(const char *afterKw, char *out, int32_t outSize) { const char *p = dvxSkipWs(afterKw); int32_t n = 0; while (*p && *p != '(' && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && n < outSize - 1) { out[n++] = *p++; } out[n] = '\0'; } // procNameOf -- the name a proc's text declares (its first line) static void procNameOf(const char *procText, char *out, int32_t outSize) { bool isSub = false; const char *afterKw = procDeclAt(dvxSkipWs(procText), &isSub); procNameFromDecl(afterKw ? afterKw : procText, out, outSize); } // procStartLines -- first whole-file line of each proc in the canonical // layout of general + procs (see joinProcArrays), as an stb_ds array the // caller frees static int32_t *procStartLines(const char *general, char **procs) { int32_t *starts = NULL; int32_t genLines = sectionLineCount(general); int32_t line = genLines > 0 ? genLines + IDE_SECTION_GAP + 1 : 1; for (int32_t p = 0; p < (int32_t)arrlen(procs); p++) { arrput(starts, line); line += sectionLineCount(procs[p]) + IDE_SECTION_GAP; } return starts; } // projectFileText -- the current text of project file fileIdx: its loaded // buffer, else the file on disk read by prjReadFileText (returned in // *outOwned too, for the caller to free). Returns NULL when the file // cannot be used; *outMissing then tells whether it simply does not // exist yet (a new file) rather than being unreadable or too large. static const char *projectFileText(int32_t fileIdx, char **outOwned, bool *outMissing) { PrjFileT *file = &sProject.files[fileIdx]; *outOwned = NULL; *outMissing = false; if (file->buffer) { return file->buffer; } *outOwned = prjReadFileText(&sProject, fileIdx, outMissing); return *outOwned; } // 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 or a save failed. static bool promptAndSave(void) { if (!hasUnsavedData()) { return true; } int32_t result = dvxPromptSave(sAc, IDE_MAIN_TITLE); if (result == DVX_SAVE_YES) { return saveAllModified(); } return result == DVX_SAVE_NO; } // rebuildProcTable -- one sProcTable entry per proc buffer (so table and // buffer indices always agree) with its object/event split and its line // range in the canonical layout. Procedure names are split on '_' into // ObjectName and EventName (e.g. "Command1_Click") using the designer's // known object names. static void rebuildProcTable(void) { arrsetlen(sProcTable, 0); // Collect all known object names once; they only depend on sDesigner.form const char **objNames = NULL; // stb_ds temp array if (sDesigner.form) { arrput(objNames, sDesigner.form->name); for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls); ci++) { arrput(objNames, sDesigner.form->controls[ci]->name); } for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems); mi++) { arrput(objNames, sDesigner.form->menuItems[mi].name); } } int32_t *starts = procStartLines(sGeneralBuf, sProcBufs); for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) { const char *text = sProcBufs[p]; bool isSub = false; int32_t endOffset = DVX_MAX(sectionLineCount(text) - 1, 0); char procName[BAS_MAX_IDENT]; procNameOf(text, procName, sizeof(procName)); if (procDeclAt(dvxSkipWs(text), &isSub)) { skipToEndProc(text, isSub, &endOffset); } IdeProcEntryT entry; memset(&entry, 0, sizeof(entry)); entry.lineNum = starts[p]; entry.endLineNum = starts[p] + endOffset; // 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; for (int32_t oi = 0; oi < (int32_t)arrlen(objNames); oi++) { int32_t objLen = (int32_t)strlen(objNames[oi]); if (objLen > 0 && strncasecmp(procName, objNames[oi], objLen) == 0 && procName[objLen] == '_') { snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]); snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + objLen + 1); isEvent = true; break; } } if (!isEvent) { snprintf(entry.objName, sizeof(entry.objName), "%s", IDE_GENERAL_SECTION); snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName); } arrput(sProcTable, entry); } arrfree(starts); arrfree(objNames); } 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 char *copy = strdup(path); if (!copy) { return; } arrins(sRecentFiles, 0, copy); 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[IDE_PREF_KEY_BUF]; snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i); const char *val = prefsGetString(sPrefs, PREF_SEC_RECENT, key, ""); if (val[0]) { char *copy = strdup(val); if (copy) { arrput(sRecentFiles, copy); } } } } static void recentOpen(int32_t index) { if (index < 0 || index >= (int32_t)arrlen(sRecentFiles)) { return; } // recentAdd reorders the list, so work from a private copy of the path char path[DVX_MAX_PATH]; snprintf(path, sizeof(path), "%s", sRecentFiles[index]); openAnyPath(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); } } // recentRemove -- drop path from the Recent list (it no longer opens) static void recentRemove(const char *path) { for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) { if (strcasecmp(sRecentFiles[i], path) == 0) { free(sRecentFiles[i]); arrdel(sRecentFiles, i); recentSave(); recentRebuildMenu(); return; } } } 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[IDE_PREF_KEY_BUF]; snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i); prefsSetString(sPrefs, PREF_SEC_RECENT, key, i < count ? sRecentFiles[i] : ""); } prefsSave(sPrefs); } // remapBreakpoints -- move the editor file's breakpoints to their anchors' // lines in the current layout (starts: procStartLines of the current // buffers; anchors parallel sBreakpoints). A breakpoint whose line is // gone is removed. sProcTable must describe the current buffers. static void remapBreakpoints(const IdeLineAnchorT *anchors, const int32_t *starts) { bool changed = false; for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) { IdeBreakpointT *bp = &sBreakpoints[i]; if (bp->fileIdx != sEditorFileIdx) { continue; } int32_t line = anchorLine(&anchors[i], starts); if (line == IDE_NO_LINE) { arrdel(sBreakpoints, i); changed = true; continue; } changed = changed || line != bp->codeLine; bp->codeLine = line; setBreakpointProcName(bp, procIndexForCodeLine(line)); } changed = dedupeBreakpoints() || changed; if (changed) { syncVmBreakpoints(); } updateBreakpointWindow(); } 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 = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) { if (sBreakpoints[i].fileIdx == fileIdx) { arrdel(sBreakpoints, i); } else if (sBreakpoints[i].fileIdx > fileIdx) { sBreakpoints[i].fileIdx--; } } syncVmBreakpoints(); updateBreakpointWindow(); } // renameInBuffer -- word-boundary rename of OldName followed by '.' or // '_'. The character before the match must be a non-identifier // character to avoid replacing "Command1" inside "MyCommand1". Returns // the new text (malloc'd), or NULL when nothing matched. 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); bool skipComments = prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true); // First pass: count replacements to compute output size int32_t count = 0; for (int32_t i = 0; i <= srcLen - oldLen; i++) { if (renameMatchAt(src, i, oldName, oldLen, skipComments)) { 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 && renameMatchAt(src, i, oldName, oldLen, skipComments)) { memcpy(out + op, newName, newLen); op += newLen; i += oldLen; continue; } out[op++] = src[i++]; } out[op] = '\0'; return out; } // renameMatchAt -- whether oldName at src[i] is a reference to rename: a // whole identifier start followed by '.' or '_', outside comments and // strings when skipComments. static bool renameMatchAt(const char *src, int32_t i, const char *oldName, int32_t oldLen, bool skipComments) { if (strncasecmp(&src[i], oldName, oldLen) != 0) { return false; } char after = src[i + oldLen]; if (after != '.' && after != '_') { return false; } if (i > 0 && basIsIdentChar(src[i - 1])) { return false; } return !skipComments || !isInStringOrComment(src, i); } // renameMenuItemsInCode -- after the Menu Editor, rename the handlers of // menu items whose name changed. Items are matched by the identity the // editor keeps (DsgnMenuItemT.id, the item's position + 1 on entry), so // inserting, deleting and moving items never looks like a rename. All // old names go to unique placeholders first, so swapped names cannot // collapse two handlers into one. static void renameMenuItemsInCode(char **oldNames, int32_t oldCount) { char **fromNames = NULL; // stb_ds: borrowed old names char **toNames = NULL; // stb_ds: borrowed new names int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems); for (int32_t mi = 0; mi < newCount; mi++) { DsgnMenuItemT *item = &sDesigner.form->menuItems[mi]; int32_t was = item->id - 1; if (item->id == DSGN_NEW_MENU_ID || was < 0 || was >= oldCount || !oldNames[was] || !oldNames[was][0] || !item->name[0]) { continue; } if (strcasecmp(oldNames[was], item->name) != 0) { arrput(fromNames, oldNames[was]); arrput(toNames, item->name); } } int32_t renames = (int32_t)arrlen(fromNames); bool viaTemporary = renames > 1; // a single rename cannot collide if (viaTemporary) { for (int32_t r = 0; r < renames; r++) { char temp[BAS_MAX_IDENT]; snprintf(temp, sizeof(temp), IDE_RENAME_TEMP_FMT, (int)r); ideRenameInCode(fromNames[r], temp); } } for (int32_t r = 0; r < renames; r++) { char temp[BAS_MAX_IDENT]; snprintf(temp, sizeof(temp), IDE_RENAME_TEMP_FMT, (int)r); ideRenameInCode(viaTemporary ? temp : fromNames[r], toNames[r]); } arrfree(fromNames); arrfree(toNames); } // renameProjectFile -- File > Rename File: ask for a new name for // project file fileIdx, then renameProjectFileTo static void renameProjectFile(int32_t fileIdx) { char prompt[IDE_MSG_BUF]; char newName[DVX_MAX_PATH]; snprintf(prompt, sizeof(prompt), "New name for %s:", sProject.files[fileIdx].path); if (dvxInputBox(sAc, IDE_RENAME_TITLE, prompt, sProject.files[fileIdx].path, newName, sizeof(newName)) && newName[0] != '\0') { renameProjectFileTo(fileIdx, newName); } } // renameProjectFileTo -- rename project file fileIdx to newName on disk // and in the project (prjRenameFile, which puts everything back on // failure; errors are shown). A class module named after its file // (classFollowsFile) takes the new name as its class name; that name must // then be free. A form keeps its Name. static bool renameProjectFileTo(int32_t fileIdx, const char *newName) { char err[IDE_MSG_BUF]; // The editor or designer may hold newer text than the project buffer if ((fileIdx == sEditorFileIdx || fileIdx == sDesignerFileIdx) && !stashCurrentFile()) { return false; } PrjFileT *file = &sProject.files[fileIdx]; bool nameClass = false; char oldClass[BAS_MAX_IDENT]; char newClass[BAS_MAX_IDENT]; if (file->kind == PrjKindClassE) { bool missing = false; char *owned = NULL; const char *text = projectFileText(fileIdx, &owned, &missing); nameClass = text && classFollowsFile(fileIdx, text) && prjClassName(&sProject, fileIdx, text, oldClass, (int32_t)sizeof(oldClass)); free(owned); basExtractClassName("", 0, newName, newClass, (int32_t)sizeof(newClass)); } if (nameClass && !basIsValidIdent(newClass)) { snprintf(err, sizeof(err), "%s is not a valid class name: the class module is named after its file.", newClass); ideErrorBox(err); return false; } if (nameClass && strcasecmp(newClass, oldClass) != 0 && classNameUsed(newClass, "")) { snprintf(err, sizeof(err), "The project already has a form or class module named %s.", newClass); ideErrorBox(err); return false; } if (!prjRenameFile(&sProject, fileIdx, newName, err, (int32_t)sizeof(err))) { ideErrorBox(err); return false; } if (nameClass && !setClassNameAttr(fileIdx, newClass)) { ideErrorBox("The file was renamed, but its class name could not be changed."); } invalidateCompiled(); updateProjectMenuState(); updateDirtyIndicators(); setStatus("File renamed."); return true; } // replaceInSections -- Replace All over the editor file's sections: every // proc (and General) holding the search text, or only the procs of object // objFilter. The targets are collected by name first and looked up again // before each visit, since showing a proc can drop an empty skeleton and // shift the indices. Returns the number of replacements. static int32_t replaceInSections(const char *objFilter, bool caseSensitive) { int32_t needleLen = (int32_t)strlen(sFindText); IdeProcEntryT *targets = NULL; // stb_ds: procs to visit, by name int32_t count = 0; if (!sEditor) { return 0; } getFullSource(); bool inGeneral = sectionInScope(-1, objFilter) && procBufContains(sGeneralBuf, sFindText, needleLen, caseSensitive); for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs) && p < (int32_t)arrlen(sProcTable); p++) { if (sectionInScope(p, objFilter) && procBufContains(sProcBufs[p], sFindText, needleLen, caseSensitive)) { arrput(targets, sProcTable[p]); } } if (inGeneral) { showProc(IDE_SECTION_GENERAL); count += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSensitive); } for (int32_t t = 0; t < (int32_t)arrlen(targets); t++) { int32_t idx = procIndexByName(targets[t].objName, targets[t].evtName); if (idx < 0) { continue; } showProc(idx); count += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSensitive); } arrfree(targets); return count; } // reportUnreadable -- a project file exists but cannot be loaded static void reportUnreadable(const char *path) { char msg[IDE_MSG_BUF]; snprintf(msg, sizeof(msg), "Cannot load %s: unreadable or larger than %d bytes.", path, (int)(IDE_MAX_SOURCE - 1)); ideErrorBox(msg); } // resolveVarPath -- parse a variable reference 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 it can't be resolved. *endPtr is set past the path. static BasValueT *resolveVarPath(const char *path, const char **endPtr) { const char *p = path; // Extract variable name (identifier plus optional type suffix) const char *nameStart = p; while (basIsIdentChar(*p)) { p++; } if (p == nameStart) { return NULL; } if (basIsTypeSuffixChar(*p)) { p++; } char varName[IDE_FULL_NAME_BUF]; 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 == '(') { if (slot->type != BAS_TYPE_ARRAY || !slot->arrVal) { return NULL; } int32_t indices[BAS_ARRAY_MAX_DIMS]; int32_t numIndices = 0; p = parseIndexList(p + 1, indices, &numIndices); int32_t flatIdx = basArrayIndex(slot->arrVal, indices, numIndices); if (flatIdx < 0 || flatIdx >= slot->arrVal->totalElements) { return NULL; } slot = basValDeref(&slot->arrVal->elements[flatIdx]); } // Parse optional UDT field or class member variable: .fieldName if (*p == '.') { p++; // skip '.' bool isInstance = slot->type == BAS_TYPE_INSTANCE; if (!isInstance && (slot->type != BAS_TYPE_UDT || !slot->udtVal)) { return NULL; } const char *fieldStart = p; while (basIsIdentChar(*p)) { p++; } char fieldName[IDE_FULL_NAME_BUF]; int32_t fieldLen = (int32_t)(p - fieldStart); if (fieldLen <= 0 || fieldLen >= (int32_t)sizeof(fieldName)) { return NULL; } memcpy(fieldName, fieldStart, fieldLen); fieldName[fieldLen] = '\0'; if (isInstance) { int32_t memberIdx = findMemberVarIdx(slot->instVal, fieldName); if (memberIdx < 0) { return NULL; } slot = &slot->instVal->fields[memberIdx]; } else { int32_t fieldIdx = findUdtFieldIdx(slot->udtVal->typeId, fieldName); if (fieldIdx < 0 || fieldIdx >= slot->udtVal->fieldCount) { return NULL; } slot = &slot->udtVal->fields[fieldIdx]; } } if (endPtr) { *endPtr = p; } return slot; } static void runCached(void) { if (!sCachedModule) { setStatus("No compiled program. Press F5 to compile first."); return; } // The forms are registered from the project buffers if (!stashCurrentFile()) { return; } clearOutput(); runModule(sCachedModule); } static void runModule(BasModuleT *mod) { // Never nest a run: a stopped program may still be unwinding if (sVm) { return; } // Create VM BasVmT *vm = basVmCreate(); if (!vm) { setStatus("Out of memory creating VM."); return; } // Create form runtime (bridges UI opcodes to DVX widgets) BasFormRtT *formRt = basFormRtCreate(sAc, vm, mod); if (!formRt) { basVmDestroy(vm); setStatus("Out of memory creating form runtime."); return; } setStatus("Running..."); closeFindDialog(); // Hide the designer, code and project windows while the program runs; // they are restored (where they were visible) when it ends. 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); } 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" DVX_PATH_SEP "CONFIG", sProject.projectDir); snprintf(vm->appData, sizeof(vm->appData), "%s" DVX_PATH_SEP "DATA", sProject.projectDir); basVmSetCommand(vm, sProject.commandArgs); platformMkdirRecursive(vm->appConfig); platformMkdirRecursive(vm->appData); // 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); // The IDE surfaces runtime errors via the Output pane and jumps // the editor to the faulting line, so suppress the form runtime's // modal error dialog. Keeping the dialog on top of an already- // modal-heavy IDE session was producing phantom "press OK twice" // UX as the dialog's nested event pump raced with other queued // events coming out of the user's program. formRt->suppressErrorDialog = true; // The program's screen window (console / graphics) carries its name snprintf(formRt->title, sizeof(formRt->title), "%s", sProject.name); sVm = vm; sDbgFormRt = formRt; sDbgModule = mod; sDbgState = DbgRunningE; sStopRequested = false; // 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) { syncVmBreakpoints(); 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 = runProgramSlices(vm); // 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). // Breakpoints inside event handlers pause inside the VM's own // spin (basVmCallSub -> doEventsCallback), so nothing here needs // to watch for DbgPausedE. if (result == BAS_VM_HALTED && basFormRtKeepsRunning(formRt)) { setStatus("Running (event loop)..."); // vm->errorMsg outliving an event dispatch means an unhandled // runtime error escaped a handler (ON ERROR clears the message // when it takes over); without this test the program kept // running after the error and the report below never fired // until the user pressed Stop. while (sWin && sAc->running && basFormRtKeepsRunning(formRt) && !sStopRequested && !vm->ended && vm->errorMsg[0] == '\0') { idePump(); } } // A normal end releases the module variables: their objects' // Class_Terminates run (VB) if (result == BAS_VM_HALTED && sWin && sAc->running && !sStopRequested && !formRt->terminated && basVmFinish(vm)) { runProgramSlices(vm); } // A runtime error fired from a host callback (e.g. setting a // property on an unknown control) halts execution by writing // vm->errorMsg and setting vm->ended. basVmRun surfaces that as // BAS_VM_ERROR for module-level code, but errors raised inside an // event handler just break the loop above. Either way, report // the message and navigate the editor to the offending line so // the user can fix it. debugNavigateToLine depends on sVm and // sDbgModule, so it has to run BEFORE the cleanup block below. // Use vm->errorLine (snapshot at error time) rather than // vm->currentLine: the dialog/event pump can fire another handler // whose OP_LINE overwrites currentLine before we read it here. bool hadRuntimeError = (vm->errorMsg[0] != '\0') && sWin; int32_t errorLine = hadRuntimeError ? vm->errorLine : 0; if (hadRuntimeError) { // Map the concatenated line to a file / proc-local line the // same way compile errors do, so the Output message matches // what the editor is actually showing. int32_t errFileIdx = -1; int32_t errLocalLine = errorLine; const char *errFile = ""; if (errorLine > 0) { prjMapLine(&sProject, errorLine, &errFileIdx, &errLocalLine); if (errFileIdx >= 0 && errFileIdx < prjFileCount(&sProject)) { errFile = sProject.files[errFileIdx].path; } } // Navigate to the failing line; the proc whose line range holds it // (none for module-level code between procs) names the location. char procName[IDE_FULL_NAME_BUF] = {0}; int32_t procLine = errLocalLine; if (errFileIdx >= 0) { int32_t procIdx = navigateToCodeLine(errFileIdx, errLocalLine, false); if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) { snprintf(procName, sizeof(procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName); procLine = wgtTextAreaGetCursorLine(sEditor); } } char report[IDE_REPORT_BUF]; if (procName[0]) { snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", procName, (int)procLine, vm->errorMsg); } else if (errFile[0]) { snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, vm->errorMsg); } else { snprintf(report, sizeof(report), "\nRUNTIME ERROR:\nline %d: %s\n", (int)errLocalLine, vm->errorMsg); } outputAppend(report, (int32_t)strlen(report)); outputLog(report); } if (sLogLineLen > 0) { logFlushLine(); } arrsetlen(sBreakTitles, 0); sVm = NULL; sDbgFormRt = NULL; sDbgModule = NULL; sDbgState = DbgIdleE; 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 -- and release the // module onClose left for us. if (!sWin) { if (sCachedModule) { basModuleFree(sCachedModule); sCachedModule = NULL; } return; } updateProjectMenuState(); sOutputPending = true; flushOutput(); setStatus(hadRuntimeError ? "Runtime error." : "Done."); // Restore IDE windows. On a runtime error we want the code // window on top so the user lands on the offending line; don't // pop the form designer / toolbox / project back up, since that // would cover the editor (dvxShowWindow raises). Output window // stays up too so the error text is visible. if (hadRuntimeError) { if (sCodeWin && !sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } if (sCodeWin) { dvxRaiseWindow(sAc, sCodeWin); } } else { 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); } // runProgramSlices -- run the program in step slices, yielding to DVX // between them, pausing at breakpoints, until it halts, fails (its message // left in vm->errorMsg) or is stopped. Returns the last result. static BasVmResultE runProgramSlices(BasVmT *vm) { BasVmResultE result = BAS_VM_HALTED; for (;;) { if (!sWin || !sAc->running || sStopRequested) { break; } if (sDbgState == DbgPausedE) { // Paused at breakpoint/step -- spin on GUI events until user acts idePump(); // User may have pressed F5 (continue), F8 (step), or Esc (stop) if (sDbgState == DbgRunningE) { vm->running = true; } continue; } result = basVmRun(vm); // Module-level pause: the same state as a pause inside a handler if (result == BAS_VM_BREAKPOINT) { onBreakpointHit(NULL, vm->currentLine > 0 ? vm->currentLine : 0); continue; } if (result == BAS_VM_STEP_LIMIT) { idePump(); continue; } if (result == BAS_VM_HALTED) { break; } // Module-level runtime error. For VM-internal errors (e.g. // division by zero) there's no vm->errorLine set, so fall // back to vm->currentLine -- but copy it into errorLine so // the shared post-loop handler below can format and navigate // uniformly with the event-handler error path. if (vm->errorMsg[0] == '\0') { const char *msg = basVmGetError(vm); snprintf(vm->errorMsg, sizeof(vm->errorMsg), "%s", msg ? msg : "Runtime error"); } if (vm->errorLine == 0 && vm->currentLine > 0) { vm->errorLine = vm->currentLine; } break; } return result; } // runSnippetVm -- run an Immediate/Watch snippet in bounded slices so an // endless loop typed into the Immediate window cannot hang the IDE. // BAS_VM_STEP_LIMIT means it was abandoned. static BasVmResultE runSnippetVm(BasVmT *vm) { BasVmResultE result = BAS_VM_STEP_LIMIT; basVmSetStepLimit(vm, IDE_SNIPPET_SLICE); for (int32_t slice = 0; slice < IDE_SNIPPET_MAX_SLICES && result == BAS_VM_STEP_LIMIT; slice++) { result = basVmRun(vm); } return result; } // saveActiveFile -- write the active file (editor/designer state first // stashed into its buffer). Returns false when nothing was written. static bool saveActiveFile(void) { if (!hasProject()) { return false; } int32_t idx = sProject.activeFileIdx; if (idx < 0 || idx >= prjFileCount(&sProject)) { return false; } // Ensure buffer is up-to-date with editor/designer state stashCurrentFile(); if (!writeProjectFile(idx)) { return false; } setStatus("Saved."); updateDirtyIndicators(); return true; } // saveAllModified -- write every modified file plus the .dbp when dirty. // Returns false if any write failed (the user has already been told). static bool saveAllModified(void) { if (!hasProject()) { return true; } stashCurrentFile(); syncFormDirty(); bool ok = true; for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].modified && !writeProjectFile(i)) { ok = false; } } if (sProject.dirty && !saveProjectFile()) { ok = false; } updateDirtyIndicators(); return ok; } // saveCurProc -- store the editor text back into the buffer it shows. // Nothing typed is ever dropped: lines outside the shown procedure move // to (General), extra Sub/Function blocks become procedures of their // own (a duplicate name too, so the compiler reports it), and a // procedure whose declaration was deleted becomes (General) text. With // discardSkeleton, an untouched generated event skeleton is removed // instead -- only when the editor is leaving it. Breakpoints and the // caret follow their lines to wherever they were stored. When memory // runs out nothing changes and the text stays in the editor. // Returns true if a buffer was removed (later sProcBufs indices shifted). static bool saveCurProc(bool discardSkeleton) { if (!sEditor || sEditorTruncated) { return false; } const char *edText = wgtGetText(sEditor); int32_t cur = sCurProcIdx; if (!edText || cur < IDE_SECTION_GENERAL || cur >= (int32_t)arrlen(sProcBufs)) { return false; } const char *stored = cur == IDE_SECTION_GENERAL ? sGeneralBuf : sProcBufs[cur]; if (!stored) { stored = ""; } // Stored text is already in canonical form: unchanged text needs // nothing unless an untouched skeleton is being left. bool leavingSkeleton = discardSkeleton && cur >= 0 && sSkeletonName[0]; if (!leavingSkeleton && strcmp(edText, stored) == 0) { return false; } char *general = NULL; char **procs = NULL; IdeLineDestT *map = NULL; if (!splitProcs(edText, &general, &procs, &map)) { setStatus("Not enough memory to store the edit; it is kept in the editor."); return false; } int32_t procCount = (int32_t)arrlen(procs); char *newGeneral = NULL; if (cur == IDE_SECTION_GENERAL && procCount == 0) { // (General) with no procedure in it is kept exactly as typed newGeneral = strdup(edText); } else if (cur == IDE_SECTION_GENERAL) { newGeneral = general; general = NULL; } else if (general[0]) { newGeneral = generalAppend(sGeneralBuf, general); } if (!newGeneral && (cur == IDE_SECTION_GENERAL || general[0])) { free(general); for (int32_t i = 0; i < procCount; i++) { free(procs[i]); } arrfree(procs); arrfree(map); setStatus("Not enough memory to store the edit; it is kept in the editor."); return false; } // Pin the editor file's breakpoints and the caret to their sections in // the layout as edited (see anchorCodeLine), before the buffers move. int32_t edLines = countLines(edText); int32_t delta = edLines - countLines(stored); int32_t genBase = cur == IDE_SECTION_GENERAL ? 0 : sectionLineCount(sGeneralBuf); int32_t *starts = procStartLines(sGeneralBuf, sProcBufs); IdeLineAnchorT *anchors = NULL; IdeLineAnchorT caret; for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { IdeLineAnchorT a; anchorCodeLine(&a, sBreakpoints[i].codeLine, cur, edLines, delta, starts); arrput(anchors, a); } caret.edLine = wgtTextAreaGetCursorLine(sEditor) - 1; // Store: the shown proc keeps its slot (the block with its name, or the // first block when it was renamed); every other block is appended. int32_t ownIdx = IDE_NO_LINE; bool removed = false; if (cur >= 0 && procCount == 0) { // No declaration left: the text is module-level code free(sProcBufs[cur]); arrdel(sProcBufs, cur); sCurProcIdx = IDE_SECTION_GENERAL; removed = true; } else if (cur >= 0) { char ownName[IDE_FULL_NAME_BUF]; char name[IDE_FULL_NAME_BUF]; procNameOf(sProcBufs[cur], ownName, sizeof(ownName)); ownIdx = 0; for (int32_t i = 0; i < procCount; i++) { procNameOf(procs[i], name, sizeof(name)); if (strcasecmp(name, ownName) == 0) { ownIdx = i; break; } } procNameOf(procs[ownIdx], name, sizeof(name)); bool isSkeleton = sSkeletonName[0] && strcasecmp(name, sSkeletonName) == 0; bool bodyEmpty = procBodyEmpty(procs[ownIdx]); free(sProcBufs[cur]); if (discardSkeleton && isSkeleton && bodyEmpty && procCount == 1 && !general[0]) { free(procs[ownIdx]); procs[ownIdx] = NULL; arrdel(sProcBufs, cur); sCurProcIdx = IDE_SECTION_NONE; sSkeletonName[0] = '\0'; removed = true; } else { sProcBufs[cur] = procs[ownIdx]; // Code typed into a generated skeleton makes it the user's if (isSkeleton && !bodyEmpty) { sSkeletonName[0] = '\0'; } } } for (int32_t i = 0; i < procCount; i++) { if (i != ownIdx) { arrput(sProcBufs, procs[i]); } } if (newGeneral) { free(sGeneralBuf); sGeneralBuf = newGeneral; } // Resolve the anchors in the new layout for (int32_t i = 0; i < (int32_t)arrlen(anchors); i++) { if (anchors[i].edLine != IDE_NO_LINE) { anchorEditedLine(&anchors[i], map, procs, genBase); } } anchorEditedLine(&caret, map, procs, genBase); arrfree(starts); starts = procStartLines(sGeneralBuf, sProcBufs); rebuildProcTable(); remapBreakpoints(anchors, starts); arrfree(starts); arrfree(anchors); arrfree(map); arrfree(procs); free(general); // The editor shows exactly what was stored for the shown section, the // caret on the line it was on if (sCurProcIdx >= IDE_SECTION_GENERAL) { const char *shownBuf = sCurProcIdx == IDE_SECTION_GENERAL ? NULL : sProcBufs[sCurProcIdx]; const char *shown = sCurProcIdx == IDE_SECTION_GENERAL ? sGeneralBuf : shownBuf; if (strcmp(edText, shown ? shown : "") != 0) { int32_t line = (caret.offset != IDE_NO_LINE && caret.section == shownBuf) ? caret.offset + 1 : IDE_CARET_KEEP; editorSetText(shown ? shown : "", line); } } return removed; } // saveFile -- Ctrl+S: the active file plus any other modified forms static void saveFile(void) { if (!hasProject() || sProject.activeFileIdx < 0) { return; } if (!saveActiveFile()) { return; } syncFormDirty(); for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (i != sProject.activeFileIdx && sProject.files[i].kind == PrjKindFormE && sProject.files[i].modified) { writeProjectFile(i); } } updateDirtyIndicators(); } // saveProjectFile -- write the .dbp; dirty is only cleared on success static bool saveProjectFile(void) { if (!prjSave(&sProject)) { dvxErrorBox(sAc, NULL, "Could not write the project file."); return false; } sProject.dirty = false; return true; } // scanClassEvents -- append the [Public] Event Name[(params)] statements // of a class module's text to *events (the compiler's words, read with // its lexer so strings and comments never match) static void scanClassEvents(const char *text, IdeClassEventT **events) { BasLexerT lex; bool stmtStart = true; basLexerInit(&lex, text, (int32_t)strlen(text)); while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR) { if (stmtStart && lex.token.type == TOK_PUBLIC) { basLexerNext(&lex); } BasTokenTypeE tt = lex.token.type; bool isEvent = stmtStart && tt == TOK_IDENT && strcasecmp(lex.token.text, BAS_KW_EVENT) == 0; if (isEvent && basLexerNext(&lex) == TOK_IDENT) { IdeClassEventT evt; memset(&evt, 0, sizeof(evt)); snprintf(evt.name, sizeof(evt.name), "%s", lex.token.text); if (basLexerNext(&lex) == TOK_LPAREN) { int32_t start = lex.token.pos + 1; int32_t depth = 1; while (depth > 0 && basLexerNext(&lex) != TOK_EOF && lex.token.type != TOK_NEWLINE && lex.token.type != TOK_ERROR) { depth += (lex.token.type == TOK_LPAREN) - (lex.token.type == TOK_RPAREN); } int32_t len = lex.token.pos - start; snprintf(evt.params, sizeof(evt.params), "%.*s", (int)DVX_MIN(len, (int32_t)sizeof(evt.params) - 1), text + start); } arrput(*events, evt); tt = lex.token.type; } stmtStart = tt == TOK_NEWLINE || tt == TOK_COLON; basLexerNext(&lex); } } // scanWithEvents -- collect the WithEvents variables src declares (Dim / // Private / Public WithEvents name As Class, several declarators allowed) // into sWithEvents, read with the compiler's lexer static void scanWithEvents(const char *src) { BasLexerT lex; bool stmtStart = true; arrsetlen(sWithEvents, 0); basLexerInit(&lex, src, (int32_t)strlen(src)); while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR) { BasTokenTypeE tt = lex.token.type; if (!stmtStart || (tt != TOK_DIM && tt != TOK_PRIVATE && tt != TOK_PUBLIC)) { stmtStart = tt == TOK_NEWLINE || tt == TOK_COLON; basLexerNext(&lex); continue; } // One declarator per pass: [WithEvents] name [As type] do { bool withEvents = basLexerNext(&lex) == TOK_IDENT && strcasecmp(lex.token.text, BAS_KW_WITHEVENTS) == 0; if (withEvents && basLexerNext(&lex) == TOK_IDENT) { IdeWithEventsT we; snprintf(we.name, sizeof(we.name), "%s", lex.token.text); if (basLexerNext(&lex) == TOK_AS && basLexerNext(&lex) == TOK_IDENT) { snprintf(we.className, sizeof(we.className), "%s", lex.token.text); arrput(sWithEvents, we); } } // On to the next declarator (commas inside bounds do not count) int32_t depth = 0; while (lex.token.type != TOK_EOF && lex.token.type != TOK_ERROR && lex.token.type != TOK_NEWLINE && lex.token.type != TOK_COLON && (depth > 0 || lex.token.type != TOK_COMMA)) { depth += (lex.token.type == TOK_LPAREN) - (lex.token.type == TOK_RPAREN); basLexerNext(&lex); } } while (lex.token.type == TOK_COMMA); stmtStart = false; } } // sectionInScope -- whether section p (-1 = General) of the editor file // belongs to object objFilter (NULL = every section) static bool sectionInScope(int32_t p, const char *objFilter) { if (!objFilter) { return true; } if (p < 0) { return strcasecmp(objFilter, IDE_GENERAL_SECTION) == 0; } return p < (int32_t)arrlen(sProcTable) && strcasecmp(sProcTable[p].objName, objFilter) == 0; } // sectionLineCount -- lines a section's text takes in the canonical // layout (joinProcArrays ends every non-empty section with a newline) static int32_t sectionLineCount(const char *text) { if (!text || !text[0]) { return 0; } int32_t n = 0; const char *p = text; for (; *p; p++) { if (*p == '\n') { n++; } } return p[-1] == '\n' ? n : n + 1; } // 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++) { if (evtLabelMatches(sEvtItems[i], evtName)) { wgtDropdownSetSelected(sEvtDropdown, i); break; } } } // setBreakpointProcName -- label a breakpoint with the proc it is in // (procIdx into sProcTable, out of range = (General)) static void setBreakpointProcName(IdeBreakpointT *bp, int32_t procIdx) { if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) { snprintf(bp->procName, sizeof(bp->procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName); } else { snprintf(bp->procName, sizeof(bp->procName), "%s", IDE_GENERAL_SECTION); } } // setClassNameAttr -- give class module fileIdx the class name name in // its Attribute VB_Name (when it has one; else the name already comes // from the file name). The text becomes an unsaved edit, and the editor // shows it when the file is open there. False when out of memory or the // file cannot be read. static bool setClassNameAttr(int32_t fileIdx, const char *name) { PrjFileT *file = &sProject.files[fileIdx]; if (!file->buffer) { file->buffer = prjReadFileText(&sProject, fileIdx, NULL); } if (!file->buffer) { return false; } int32_t len = (int32_t)strlen(file->buffer); int32_t start = 0; int32_t oldLen = 0; int32_t nameLen = (int32_t)strlen(name); if (!basClassNameAttr(file->buffer, len, &start, &oldLen)) { return true; } char *text = (char *)malloc((size_t)(len - oldLen + nameLen + 1)); if (!text) { return false; } memcpy(text, file->buffer, (size_t)start); memcpy(text + start, name, (size_t)nameLen); memcpy(text + start + nameLen, file->buffer + start + oldLen, (size_t)(len - start - oldLen + 1)); free(file->buffer); file->buffer = text; file->modified = true; if (fileIdx == sEditorFileIdx) { loadModuleIntoEditor(fileIdx, text); } return true; } static void setOutputText(const char *text) { if (!sOutWin) { showOutputWindow(); } if (sOutput) { wgtSetText(sOutput, text); } } static void setStatus(const char *text) { if (sStatus) { wgtSetText(sStatus, text); } } // shiftBreakpointsForEdit -- move the editor file's breakpoints to follow // an edit that changed the line count by delta. The edited region comes // from diffing the old and new text (longest common suffix first, then // prefix): lines that start before it stay, lines that start after it // move by delta, and lines whose start was deleted or replaced lose // their breakpoint. Joined lines can leave two breakpoints on one line; // the duplicate goes. static void shiftBreakpointsForEdit(const char *oldText, const char *newText, int32_t delta) { int32_t oldLen = (int32_t)strlen(oldText); int32_t newLen = (int32_t)strlen(newText); int32_t suffix = 0; int32_t prefix = 0; while (suffix < oldLen && suffix < newLen && oldText[oldLen - 1 - suffix] == newText[newLen - 1 - suffix]) { suffix++; } while (prefix < oldLen - suffix && prefix < newLen - suffix && oldText[prefix] == newText[prefix]) { prefix++; } int32_t oldEnd = oldLen - suffix; // end of the replaced old text int32_t base = editorLineToCodeLine(1) - 1; // code line before editor line 1 int32_t oldLines = countLines(oldText); bool changed = false; for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) { if (sBreakpoints[i].fileIdx != sEditorFileIdx) { continue; } int32_t edLine = sBreakpoints[i].codeLine - base; // Sections before the editor's are untouched, sections after it move if (edLine < 1) { continue; } if (edLine > oldLines) { sBreakpoints[i].codeLine += delta; changed = true; continue; } int32_t start = lineStartOffset(oldText, edLine); if (start < prefix) { continue; } if (start >= oldEnd) { sBreakpoints[i].codeLine += delta; changed = true; } else if (start != prefix) { arrdel(sBreakpoints, i); changed = true; } } changed = dedupeBreakpoints() || changed; if (changed) { syncVmBreakpoints(); updateBreakpointWindow(); } } static void showBreakpointWindow(void) { if (sBreakpointWin) { dvxShowWindow(sAc, sBreakpointWin); dvxRaiseWindow(sAc, sBreakpointWin); updateBreakpointWindow(); return; } int32_t winW = IDE_BP_WIN_W; int32_t winH = IDE_DEBUG_WIN_H; int32_t winX = sAc->display.width - winW - IDE_WIN_MARGIN; int32_t winY = sAc->display.height - winH - IDE_WIN_MARGIN; 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 = WGT_WEIGHT_FILL; sBreakpointList->onKeyDown = onBreakpointListKeyDown; sBreakpointList->onDblClick = onBreakpointListDblClick; wgtListViewSetMultiSelect(sBreakpointList, true); static const ListViewColT cols[] = { { "File", wgtChars(IDE_COL_W_FILE), ListViewAlignLeftE }, { "Procedure", wgtChars(IDE_COL_W_PROC), ListViewAlignLeftE }, { "Line", wgtChars(IDE_COL_W_LINE), ListViewAlignRightE }, }; wgtListViewSetColumns(sBreakpointList, cols, DVX_ARRAY_LEN(cols)); } } } updateBreakpointWindow(); } static void showCallStackWindow(void) { if (sCallStackWin) { dvxShowWindow(sAc, sCallStackWin); dvxRaiseWindow(sAc, sCallStackWin); updateCallStackWindow(); return; } int32_t winW = IDE_CALLSTACK_WIN_W; int32_t winH = IDE_DEBUG_WIN_H; int32_t winX = sAc->display.width - winW; int32_t winY = toolbarBottom() + IDE_CALLSTACK_WIN_Y_OFF; 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 = WGT_WEIGHT_FILL; static const ListViewColT cols[] = { { "Procedure", wgtChars(IDE_COL_W_PROC), ListViewAlignLeftE }, { "Line", wgtChars(IDE_COL_W_LINE), ListViewAlignRightE }, }; wgtListViewSetColumns(sCallStackList, cols, DVX_ARRAY_LEN(cols)); } } } updateCallStackWindow(); } // showCodeView -- show the code of fileIdx (the active file): reopen the // code window with the file's code loaded when it was closed, reload it // when the editor holds another file, show the window if hidden, raise. static void showCodeView(int32_t fileIdx) { bool reopened = !sCodeWin; if (reopened) { showCodeWindow(); } if (!sEditor) { return; } if (reopened || sEditorFileIdx != fileIdx) { if (sProject.files[fileIdx].kind == PrjKindFormE) { loadFormCodeIntoEditor(); } else { // Stash first: it may replace this very file's buffer saveCurProc(true); stashEditor(); bool missing = false; char *owned = NULL; const char *source = projectFileText(fileIdx, &owned, &missing); loadModuleIntoEditor(fileIdx, source ? source : ""); free(owned); } } if (!sCodeWin->visible) { dvxShowWindow(sAc, sCodeWin); } dvxRaiseWindow(sAc, sCodeWin); setStatus("Code view."); } static void showCodeWindow(void) { if (sCodeWin) { return; // already open } int32_t codeY = toolbarBottom(); int32_t codeH = sAc->display.height - codeY - (IDE_OUTPUT_WIN_H + IDE_OUTPUT_WIN_GAP); sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true); if (!sCodeWin) { return; } // Ensure position is below the toolbar (dvxCreateWindow may adjust) sCodeWin->y = codeY; sCodeWin->onMenu = onMenu; sCodeWin->onFocus = onContentFocus; sCodeWin->onClose = onCodeWinClose; sCodeWin->accelTable = sWin ? sWin->accelTable : NULL; sLastFocusWin = sCodeWin; WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin); WidgetT *dropdownRow = codeRoot ? wgtHBox(codeRoot) : NULL; sObjDropdown = (dropdownRow && wgtLabel(dropdownRow, "Object:")) ? wgtDropdown(dropdownRow) : NULL; sEvtDropdown = (sObjDropdown && wgtLabel(dropdownRow, "Function:")) ? wgtDropdown(dropdownRow) : NULL; sEditor = sEvtDropdown ? wgtTextArea(codeRoot, IDE_MAX_SOURCE) : NULL; if (!sEditor) { dvxDestroyWindow(sAc, sCodeWin); sCodeWin = NULL; sObjDropdown = NULL; sEvtDropdown = NULL; sLastFocusWin = NULL; ideErrorBox("Not enough memory to open the code window."); return; } // A new editor shows no section yet sCurProcIdx = IDE_SECTION_NONE; dropdownRow->spacing = wgtPixels(IDE_WIDGET_SPACING); sObjDropdown->weight = WGT_WEIGHT_FILL; sObjDropdown->onChange = onObjDropdownChange; wgtDropdownSetItems(sObjDropdown, NULL, 0); sEvtDropdown->weight = WGT_WEIGHT_FILL; sEvtDropdown->onChange = onEvtDropdownChange; wgtDropdownSetItems(sEvtDropdown, NULL, 0); sEditor->weight = WGT_WEIGHT_FILL; wgtTextAreaSetColorize(sEditor, basicColorize, NULL); // Apply saved syntax colors uint32_t initColors[TEXT_SYNTAX_MAX]; for (int32_t i = 0; i < TEXT_SYNTAX_MAX; i++) { char key[IDE_PREF_KEY_BUF]; snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i); initColors[i] = (uint32_t)prefsGetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)textEditDefaultSyntaxRgb((uint8_t)i)); } wgtTextAreaSetSyntaxColors(sEditor, initColors, TEXT_SYNTAX_MAX); wgtTextAreaSetLineDecorator(sEditor, debugLineDecorator, sAc); wgtTextAreaSetGutterClick(sEditor, onGutterClick); wgtTextAreaSetShowLineNumbers(sEditor, true); wgtTextAreaSetAutoIndent(sEditor, true); wgtTextAreaSetCaptureTabs(sEditor, true); wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH)); wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, true)); // Programmatic loads go through editorSetText, which suppresses // onChange, so the handler can be attached right away without false // dirty marking. sEditor->onChange = onEditorChange; editorSnapshot(countLines(wgtGetText(sEditor))); updateProjectMenuState(); updateDirtyIndicators(); } // showCompileError -- present sOutputBuf (already filled) as the // compile result and leave the busy state. static void showCompileError(const char *status) { outputLog(sOutputBuf); setOutputText(sOutputBuf); showOutputWindow(); if (sOutWin) { dvxRaiseWindow(sAc, sOutWin); } setStatus(status); dvxSetBusy(sAc, false); } static void showImmediateWindow(void) { if (sImmWin) { dvxShowWindow(sAc, sImmWin); dvxRaiseWindow(sAc, sImmWin); return; } int32_t outH = IDE_OUTPUT_WIN_H; 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->onClose = onImmWinClose; 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 = WGT_WEIGHT_FILL; sImmediate->readOnly = false; sImmediate->onKeyDown = onImmediateKeyDown; } 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); updateLocalsWindow(); return; } int32_t winW = IDE_LOCALS_WIN_W; int32_t winH = IDE_LOCALS_WIN_H; 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 = WGT_WEIGHT_FILL; static const ListViewColT cols[] = { { "Name", wgtChars(IDE_COL_W_NAME), ListViewAlignLeftE }, { "Type", wgtChars(IDE_COL_W_TYPE), ListViewAlignLeftE }, { "Value", wgtChars(IDE_COL_W_VALUE), ListViewAlignLeftE }, }; wgtListViewSetColumns(sLocalsList, cols, DVX_ARRAY_LEN(cols)); } } } updateLocalsWindow(); } static void showOutputWindow(void) { if (sOutWin) { dvxShowWindow(sAc, sOutWin); return; } int32_t outH = IDE_OUTPUT_WIN_H; int32_t outY = sAc->display.height - outH; sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true); if (sOutWin) { sOutWin->onClose = onOutWinClose; sOutWin->onFocus = onContentFocus; sOutWin->onMenu = onMenu; sOutWin->accelTable = sWin ? sWin->accelTable : NULL; sLastFocusWin = sOutWin; WidgetT *outRoot = wgtInitWindow(sAc, sOutWin); sOutput = outRoot ? wgtTextArea(outRoot, IDE_MAX_OUTPUT) : NULL; if (!sOutput) { dvxLog("IDE: failed to create output TextArea"); return; } sOutput->weight = WGT_WEIGHT_FILL; sOutput->readOnly = true; if (sOutputLen > 0) { setOutputText(sOutputBuf); } } } static void showPreferencesDialog(void) { // One dialog at a time: its handlers share sPrefsDlg if (sPrefsDlg.win) { dvxRaiseWindow(sAc, sPrefsDlg.win); return; } memset(&sPrefsDlg, 0, sizeof(sPrefsDlg)); WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", IDE_PREFS_WIN_W, IDE_PREFS_WIN_H, false); if (!win) { return; } win->maxW = win->w; win->maxH = win->h; if (!prefsBuild(win)) { dvxDestroyWindow(sAc, win); memset(&sPrefsDlg, 0, sizeof(sPrefsDlg)); ideErrorBox("Not enough memory to open Preferences."); return; } // Closing the window (X, Alt+F4) cancels like the Cancel button sPrefsDlg.win = win; win->onClose = onPrefsClose; win->modal = true; dvxFitWindow(sAc, win); WindowT *prevModal = sAc->modalWindow; sAc->modalWindow = win; while (!sPrefsDlg.done && sAc->running) { dvxUpdate(sAc); } if (sPrefsDlg.accepted) { // General tab prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments)); prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, wgtCheckboxIsChecked(sPrefsDlg.optionExplicit)); prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, wgtCheckboxIsChecked(sPrefsDlg.useSpaces)); const char *tw = wgtGetText(sPrefsDlg.tabWidthInput); int32_t tabW = tw ? atoi(tw) : IDE_DEFAULT_TAB_WIDTH; if (tabW < IDE_MIN_TAB_WIDTH) { tabW = IDE_MIN_TAB_WIDTH; } if (tabW > IDE_MAX_TAB_WIDTH) { tabW = IDE_MAX_TAB_WIDTH; } prefsSetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, tabW); if (sEditor) { wgtTextAreaSetTabWidth(sEditor, tabW); wgtTextAreaSetUseTabChar(sEditor, !wgtCheckboxIsChecked(sPrefsDlg.useSpaces)); } const char *val; val = wgtGetText(sPrefsDlg.defAuthor); prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, val ? val : ""); val = wgtGetText(sPrefsDlg.defPublisher); prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, val ? val : ""); val = wgtGetText(sPrefsDlg.defVersion); prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, val ? val : IDE_PREF_DEFAULT_VERSION); val = wgtGetText(sPrefsDlg.defCopyright); prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, val ? val : ""); char desc[PRJ_MAX_DESC]; val = wgtGetText(sPrefsDlg.defDescription); prjSingleLine(desc, sizeof(desc), val ? val : ""); prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, desc); // Colors tab for (int32_t i = 0; i < TEXT_SYNTAX_MAX; i++) { char key[IDE_PREF_KEY_BUF]; snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i); prefsSetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)sPrefsDlg.syntaxColors[i]); } applySyntaxColors(); prefsSave(sPrefs); } sAc->modalWindow = prevModal; sPrefsDlg.win = NULL; dvxDestroyWindow(sAc, win); } // showProc -- display a procedure buffer (-1 = General) in the editor static void showProc(int32_t procIdx) { if (!sEditor) { return; } // Save whatever is currently in the editor. Leaving an untouched // event skeleton discards it; since arrdel shifts everything after // it, the target index follows. if (sCurProcIdx >= IDE_SECTION_GENERAL) { int32_t leftIdx = sCurProcIdx; bool removed = saveCurProc(procIdx != sCurProcIdx); // Every later procedure's line number depends on the buffer just // saved (lines added or removed above it shift them), so the proc // table is rebuilt after any save -- not only when the list // changed -- or breakpoints toggled in the next proc land on the // wrong compile line. joinProcBufs(); if (removed && leftIdx >= 0 && procIdx > leftIdx) { procIdx--; } } if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcBufs)) { editorSetText(sProcBufs[procIdx] ? sProcBufs[procIdx] : "", IDE_CARET_LOAD); sCurProcIdx = procIdx; } else { editorSetText(sGeneralBuf ? sGeneralBuf : "", IDE_CARET_LOAD); sCurProcIdx = IDE_SECTION_GENERAL; } } // 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, bool forward) { 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) { // showProc leaves the cursor at the top of the proc, which is the // wrong anchor for a backward scan (it would find nothing). When // searching backward, move the cursor to the true end of the buffer // (offset == len) so the scan walks upward through every match, // including any on the final line of a proc with no trailing newline. if (!forward) { wgtTextAreaCursorToEnd(sEditor); } return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward); } return false; } static void showWatchWindow(void) { if (sWatchWin) { dvxShowWindow(sAc, sWatchWin); dvxRaiseWindow(sAc, sWatchWin); updateWatchWindow(); return; } int32_t winW = IDE_WATCH_WIN_W; int32_t winH = IDE_DEBUG_WIN_H; int32_t winX = sAc->display.width - winW - IDE_WATCH_WIN_X_GAP; 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, IDE_VALUE_BUF); if (sWatchInput) { sWatchInput->onKeyDown = onWatchInputKeyDown; } // Results list below sWatchList = wgtListView(root); if (sWatchList) { sWatchList->weight = WGT_WEIGHT_FILL; sWatchList->onKeyDown = onWatchListKeyDown; sWatchList->onDblClick = onWatchListDblClick; static const ListViewColT cols[] = { { "Expression", wgtChars(IDE_COL_W_EXPR), ListViewAlignLeftE }, { "Value", wgtChars(IDE_COL_W_WATCH_VALUE), ListViewAlignLeftE }, }; wgtListViewSetColumns(sWatchList, cols, DVX_ARRAY_LEN(cols)); } } } updateWatchWindow(); } // skipLine -- advance past the end of the current line (or to the NUL) static const char *skipLine(const char *p) { while (*p && *p != '\n') { p++; } if (*p == '\n') { p++; } return p; } // skipToEndProc -- from the start of a SUB/FUNCTION declaration line, // return the position just past the matching END SUB/END FUNCTION line // (or the end of the text). *outEndLineOffset receives the END line's // distance in lines from the declaration (the last line's when missing). static const char *skipToEndProc(const char *declLine, bool isSub, int32_t *outEndLineOffset) { const char *endTag = isSub ? KW_END_SUB : KW_END_FUNCTION; const char *scan = skipLine(declLine); int32_t line = 1; bool found = false; while (*scan) { if (kwMatch(dvxSkipWs(scan), endTag)) { found = true; scan = skipLine(scan); break; } scan = skipLine(scan); line++; } *outEndLineOffset = found ? line : line - 1; return scan; } // splitProcs -- split source into a (General) buffer plus one buffer per // SUB/FUNCTION block. Every line outside a procedure (including any // between or after procedures) belongs to General, so nothing is lost // when the file is reassembled; General's trailing blank lines are // trimmed. When outMap is not NULL it receives, per source line, the // piece and line it went to (a trimmed line maps past General's end). // Returns false, with nothing allocated, when memory runs out. Caller // frees the results. static bool splitProcs(const char *source, char **outGeneral, char ***outProcs, IdeLineDestT **outMap) { int32_t srcLen = (int32_t)strlen(source); char *general = (char *)malloc(srcLen + 2); // room for '\n' + NUL int32_t genLen = 0; int32_t genLine = 0; char **procs = NULL; IdeLineDestT *map = NULL; const char *pos = source; bool ok = general != NULL; while (ok && *pos) { bool isSub = false; if (procDeclAt(dvxSkipWs(pos), &isSub)) { int32_t endOffset = 0; const char *end = skipToEndProc(pos, isSub, &endOffset); int32_t procLen = (int32_t)(end - pos); char *procBuf = (char *)malloc(procLen + 1); ok = procBuf != NULL; if (ok) { memcpy(procBuf, pos, procLen); procBuf[procLen] = '\0'; arrput(procs, procBuf); int32_t line = 0; for (const char *q = pos; outMap && q < end; q = skipLine(q)) { IdeLineDestT dest = { (int32_t)arrlen(procs) - 1, line++ }; arrput(map, dest); } } pos = end; continue; } const char *next = skipLine(pos); memcpy(general + genLen, pos, next - pos); genLen += (int32_t)(next - pos); if (outMap) { IdeLineDestT dest = { IDE_SECTION_GENERAL, genLine }; arrput(map, dest); } genLine++; pos = next; } if (!ok) { free(general); for (int32_t i = 0; i < (int32_t)arrlen(procs); i++) { free(procs[i]); } arrfree(procs); arrfree(map); return false; } // Trim trailing blank lines; a non-empty section ends in one newline general[genLen] = '\0'; genLen = dvxTrimRight(general); if (genLen > 0) { general[genLen++] = '\n'; } general[genLen] = '\0'; *outGeneral = general; *outProcs = procs; if (outMap) { *outMap = map; } return true; } // stashCurrentFile -- stash the editor's and the designer's state back // into the project buffers of the files they belong to (the designer may // hold a form while a module is active). Caching only: nothing is // marked modified beyond mirroring the designer's dirty flag. Returns // false, after telling the user, when the form cannot be serialized; its // previous buffer is kept. static bool stashCurrentFile(void) { stashEditor(); if (!sDesigner.form || sDesignerFileIdx < 0 || sDesignerFileIdx >= prjFileCount(&sProject)) { return true; } PrjFileT *file = &sProject.files[sDesignerFileIdx]; syncFormDirty(); // Serialize form designer state to .frm text char *frmBuf = (char *)malloc(IDE_MAX_SOURCE); if (!frmBuf) { ideErrorBox("Not enough memory to save the form."); return false; } int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE); // On failure keep the previous buffer rather than a truncated one if (frmLen < 0) { free(frmBuf); ideErrorBox(frmLen == DSGN_SAVE_LINE_TOO_LONG ? "A property value is too long to save at its position in the form. Shorten it and save again." : "Form is too large to save; its code section does not fit."); return false; } frmBuf[frmLen] = '\0'; // Keep only what the text needs, not the whole scratch size char *fitted = (char *)realloc(frmBuf, (size_t)frmLen + 1); free(file->buffer); file->buffer = fitted ? fitted : frmBuf; snprintf(file->formName, sizeof(file->formName), "%s", sDesigner.form->name); return true; } // stashEditor -- write the editor's text back to the file it is bound // to: a module's project buffer, or the designer form's code. static void stashEditor(void) { if (sEditorFileIdx < 0 || sEditorFileIdx >= prjFileCount(&sProject)) { return; } PrjFileT *file = &sProject.files[sEditorFileIdx]; if (file->kind == PrjKindFormE) { stashFormCode(); return; } char *copy = strdup(getFullSource()); if (copy) { free(file->buffer); file->buffer = copy; } } // stashFormCode -- if the proc buffers belong to the designer's form, // save them back to form->code. static void stashFormCode(void) { if (!sDesigner.form || sEditorFileIdx < 0 || sEditorFileIdx != sDesignerFileIdx) { return; } char *code = strdup(getFullSource()); if (code) { free(sDesigner.form->code); sDesigner.form->code = code; } } static void switchToDesign(void) { stashFormCode(); // If already open, just bring to front if (sFormWin) { dvxRaiseWindow(sAc, sFormWin); return; } // A form file the designer could not parse opens as a blank form if (!sDesigner.form) { dsgnNewForm(&sDesigner, IDE_DEFAULT_FORM); sDesignerFileIdx = sProject.activeFileIdx; } if (!sDesigner.form) { return; } DsgnFormT *form = sDesigner.form; // Create the form designer window using the shared form window builder char title[IDE_FORM_TITLE_BUF]; snprintf(title, sizeof(title), "%s [Design]", form->name); WidgetT *root; WidgetT *contentBox; sFormWin = basFormRtCreateFormWindow(sAc, title, form->layout, form->resizable, false, false, form->width, form->height, 0, 0, &root, &contentBox); if (!sFormWin) { return; } sFormWin->visible = true; sFormWin->onClose = onFormWinClose; sFormWin->onMenu = onFormWinMenu; sFormWin->onFocus = onContentFocus; sFormWin->accelTable = sWin ? sWin->accelTable : NULL; sDesigner.formWin = sFormWin; // Build preview menu bar from form's menu items dsgnBuildPreviewMenuBar(sFormWin, form); // Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint). // onResize is attached after the initial sizing below: sizing the // window to the form is not an edit. sFormWin->onPaint = onFormWinPaint; sFormWin->onMouse = onFormWinMouse; sFormWin->onKey = onFormWinKey; sFormWin->onCursorQuery = onFormWinCursorQuery; // Create live widgets for each control dsgnCreateWidgets(&sDesigner, contentBox); // Set form caption as window title if (form->caption[0]) { snprintf(title, sizeof(title), "%s [Design]", form->caption); dvxSetTitle(sAc, sFormWin, title); } // Size the form window if (form->autoSize) { dvxFitWindow(sAc, sFormWin); form->width = sFormWin->w; form->height = sFormWin->h; } else { dvxResizeWindow(sAc, sFormWin, form->width, form->height); } sFormWin->onResize = onFormWinResize; // Create toolbox and properties windows if (!sToolboxWin) { sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom()); if (sToolboxWin) { 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."); } // syncFormDirty -- mirror form->dirty onto the designer file's project // entry so the tree, title bar and save logic all see one consistent flag. static void syncFormDirty(void) { if (sDesigner.form && sDesigner.form->dirty && sDesignerFileIdx >= 0 && sDesignerFileIdx < prjFileCount(&sProject)) { sProject.files[sDesignerFileIdx].modified = true; } } // syncVmBreakpoints -- push the IDE breakpoint list to the live VM (no-op // when nothing is running or the program was started without debugging). static void syncVmBreakpoints(void) { if (!sVm || !sDbgEnabled) { return; } buildVmBreakpoints(); basVmSetBreakpoints(sVm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints)); } 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 = sEditorFileIdx; if (fileIdx < 0) { return; } // Convert editor line to file code line by adding proc offset int32_t codeLine = editorLineToCodeLine(editorLine); // Check if this breakpoint already exists -- remove it for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) { arrdel(sBreakpoints, i); syncVmBreakpoints(); if (sEditor) { wgtInvalidatePaint(sEditor); } updateBreakpointWindow(); return; } } // Validate that this line is breakable (not blank, comment, SUB/FUNCTION // decl/end or a class module's header) if (sEditor) { const char *text = wgtGetText(sEditor); if (text) { // A class module's header opens its (General) section if (sProject.files[fileIdx].kind == PrjKindClassE && sCurProcIdx == IDE_SECTION_GENERAL) { int32_t firstCodeLine = 1; prjClassCodePos(text, (int32_t)strlen(text), &firstCodeLine); if (editorLine < firstCodeLine) { return; } } // Find the start of editorLine (1-based) const char *p = text; for (int32_t ln = 1; *p && ln < editorLine; ln++) { p = skipLine(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 (isRemAt(p, 0, (int32_t)strcspn(p, "\n"))) { return; } // SUB/FUNCTION declaration or END SUB/FUNCTION bool isSub; if (procDeclAt(p, &isSub) || kwMatch(p, KW_END_SUB) || kwMatch(p, KW_END_FUNCTION)) { return; } } } // Add new breakpoint IdeBreakpointT bp; memset(&bp, 0, sizeof(bp)); bp.fileIdx = fileIdx; bp.codeLine = codeLine; setBreakpointProcName(&bp, sCurProcIdx); arrput(sBreakpoints, bp); syncVmBreakpoints(); // The first breakpoint of the file starts the edit snapshots if (sEditor) { editorSnapshot(sEditorLineCount); wgtInvalidatePaint(sEditor); } updateBreakpointWindow(); } static int32_t toolbarBottom(void) { return sWin ? sWin->y + sWin->h + IDE_TOOLBAR_GAP : IDE_TOOLBAR_FALLBACK_Y; } static const char *typeNameStr(uint8_t dt) { switch (dt) { case BAS_TYPE_INTEGER: return "Integer"; case BAS_TYPE_BYTE: return "Byte"; case BAS_TYPE_CURRENCY: return "Currency"; 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_DATE: return "Date"; case BAS_TYPE_ARRAY: return "Array"; case BAS_TYPE_UDT: return "UDT"; case BAS_TYPE_OBJECT: case BAS_TYPE_INSTANCE: return IDE_OBJECT_TYPE; case BAS_TYPE_EMPTY: return "Empty"; case BAS_TYPE_NULL: return "Null"; case BAS_TYPE_VARIANT: return "Variant"; case BAS_TYPE_MISSING: return "Missing"; default: return "?"; } } static void updateBreakpointWindow(void) { if (!sBreakpointList || !sBreakpointWin || !sBreakpointWin->visible) { return; } listRowsReset(&sBpRows); int32_t count = (int32_t)arrlen(sBreakpoints); for (int32_t i = 0; i < count; i++) { char lineBuf[IDE_LINE_NUM_BUF]; // File name if (sBreakpoints[i].fileIdx >= 0 && sBreakpoints[i].fileIdx < prjFileCount(&sProject)) { listRowsAdd(&sBpRows, sProject.files[sBreakpoints[i].fileIdx].path); } else { listRowsAdd(&sBpRows, "?"); } // Procedure name listRowsAdd(&sBpRows, sBreakpoints[i].procName); // Line number snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sBreakpoints[i].codeLine); listRowsAdd(&sBpRows, lineBuf); } listRowsCommit(&sBpRows, sBreakpointList, count); } static void updateCallStackWindow(void) { if (!sCallStackList || !sCallStackWin || !sCallStackWin->visible) { return; } listRowsReset(&sCallRows); if (sDbgState != DbgPausedE || !sVm || !sDbgModule) { listRowsCommit(&sCallRows, sCallStackList, 0); return; } int32_t rowCount = 0; // Current location first if (sDbgCurrentLine > 0) { char lineBuf[IDE_LINE_NUM_BUF]; int32_t curProcIdx = vmCurProcIdx(); snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sDbgCurrentLine); listRowsAdd(&sCallRows, curProcIdx >= 0 ? sDbgModule->procs[curProcIdx].name : IDE_MODULE_FRAME); listRowsAdd(&sCallRows, lineBuf); rowCount++; } // Walk the callers, innermost first. Frame d is suspended at the // return address of frame d + 1; frame 0 is the implicit module frame. for (int32_t d = sVm->callDepth - 2; d >= 0; d--) { listRowsAdd(&sCallRows, d > 0 ? vmProcNameForPc(sVm->callStack[d + 1].returnPc) : IDE_MODULE_FRAME); listRowsAdd(&sCallRows, ""); rowCount++; } listRowsCommit(&sCallRows, sCallStackList, rowCount); } static void updateDirtyIndicators(void) { syncFormDirty(); updateMainTitle(); // Code window title -- only shows * when code has been edited if (sCodeWin) { bool codeDirty = false; const char *codeFile = ""; if (sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject)) { codeDirty = sProject.files[sEditorFileIdx].modified; codeFile = sProject.files[sEditorFileIdx].path; } else if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject)) { codeDirty = sProject.files[sProject.activeFileIdx].modified; codeFile = sProject.files[sProject.activeFileIdx].path; } char codeTitle[IDE_MSG_BUF]; snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : ""); dvxSetTitle(sAc, sCodeWin, codeTitle); } // Design window title if (sFormWin && sDesigner.form) { char title[IDE_FORM_TITLE_BUF]; 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); } } // updateDropdowns -- refresh sProcTable from the editor's buffers and // repopulate the Object dropdown (which cascades into the Event dropdown). static void updateDropdowns(void) { // Rebuilds sProcTable as a side effect; the file's WithEvents // variables are objects too scanWithEvents(getFullSource()); // 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. for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) { free((void *)sObjItems[i]); } arrsetlen(sObjItems, 0); evtItemsReset(); if (!sObjDropdown || !sEvtDropdown) { return; } // 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(IDE_GENERAL_SECTION)); 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)); } } } for (int32_t i = 0; i < (int32_t)arrlen(sWithEvents); i++) { arrput(sObjItems, strdup(sWithEvents[i].name)); } // A failed strdup would put a NULL name in the dropdown for (int32_t i = (int32_t)arrlen(sObjItems) - 1; i >= 0; i--) { if (!sObjItems[i]) { arrdel(sObjItems, i); } } // 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; } listRowsReset(&sLocalsRows); if (sDbgState != DbgPausedE || !sVm || !sDbgModule) { listRowsCommit(&sLocalsRows, sLocalsList, 0); return; } // Locals of the current proc, globals, and the current form's variables int32_t curProcIdx = vmCurProcIdx(); int32_t rowCount = 0; if (sDbgModule->debugVars) { for (int32_t i = 0; i < sDbgModule->debugVarCount; i++) { BasDebugVarT *dv = &sDbgModule->debugVars[i]; if (!debugVarVisible(dv, curProcIdx) || isMangledDebugName(dv->name)) { continue; } char typeBuf[IDE_TYPE_NAME_BUF]; char valueBuf[BAS_MAX_IDENT]; // Read the value first so we can use it for the type column BasValueT val; BasValueT *slot = getDebugVarSlot(dv); if (slot) { val = *slot; } else { memset(&val, 0, sizeof(val)); } // Type column -- arrays show "type()" with the element type, an // object its class, a Variant "Variant/" and what it holds const char *valType = (val.type == BAS_TYPE_INSTANCE) ? instanceTypeName(val.instVal) : typeNameStr(val.type); if (dv->dataType == BAS_TYPE_ARRAY && val.arrVal) { snprintf(typeBuf, sizeof(typeBuf), "%s()", typeNameStr(val.arrVal->elementType)); } else if (dv->dataType == BAS_TYPE_VARIANT) { snprintf(typeBuf, sizeof(typeBuf), "%s/%s", typeNameStr(BAS_TYPE_VARIANT), valType); } else if (val.type == BAS_TYPE_INSTANCE) { snprintf(typeBuf, sizeof(typeBuf), "%s", valType); } else { snprintf(typeBuf, sizeof(typeBuf), "%s", typeNameStr(dv->dataType)); } formatValue(&val, valueBuf, sizeof(valueBuf)); listRowsAdd(&sLocalsRows, dv->name); listRowsAdd(&sLocalsRows, typeBuf); listRowsAdd(&sLocalsRows, valueBuf); rowCount++; } } listRowsCommit(&sLocalsRows, sLocalsList, rowCount); } // updateMainTitle -- "DVX BASIC - [ProjectName] *" (the * when anything // is unsaved), or just "DVX BASIC" with no project open. static void updateMainTitle(void) { if (!sWin) { return; } char title[IDE_TITLE_BUF]; if (hasProject()) { snprintf(title, sizeof(title), "%s - [%s]%s", IDE_MAIN_TITLE, sProject.name, hasUnsavedData() ? " *" : ""); } else { snprintf(title, sizeof(title), "%s", IDE_MAIN_TITLE); } dvxSetTitle(sAc, sWin, title); } static void updateProjectMenuState(void) { if (!sWin || !sWin->menuBar) { return; } bool project = hasProject(); bool hasFile = (project && sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject)); bool hasForm = activeIsForm(); bool isIdle = (sDbgState == DbgIdleE); bool isPaused = (sDbgState == DbgPausedE); bool isRunning = (sDbgState == DbgRunningE); bool canRun = project && (isIdle || isPaused); bool canStop = isRunning || isPaused; // Project menu wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, project && sProject.dirty); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, project && isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW, isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_OPEN, isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_OPEN, isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW_CLASS, project && isIdle); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, project); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_RENAME, isIdle && prjGetSelectedFileIdx() >= 0); wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, isIdle && 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 wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyFileModified()); wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, project && isIdle); // Edit menu wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, project); wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, project); wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, project); // View menu -- consider both active file and project tree selection int32_t selIdx = prjGetSelectedFileIdx(); bool selIsFile = (project && selIdx >= 0 && selIdx < prjFileCount(&sProject)); bool selIsForm = (selIsFile && sProject.files[selIdx].kind == PrjKindFormE); 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, project && isIdle && 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); } } // updateWatchWindow -- evaluate watch expressions static void updateWatchWindow(void) { if (!sWatchList || !sWatchWin || !sWatchWin->visible) { return; } listRowsReset(&sWatchRows); int32_t count = (int32_t)arrlen(sWatchExprs); for (int32_t i = 0; i < count; i++) { char valBuf[IDE_VALUE_BUF]; valBuf[0] = '\0'; if (sDbgState == DbgPausedE && 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))) { snprintf(valBuf, sizeof(valBuf), ""); } } listRowsAdd(&sWatchRows, sWatchExprs[i]); listRowsAdd(&sWatchRows, valBuf); } listRowsCommit(&sWatchRows, sWatchList, count); } // ============================================================ // Compile-time CtrlName.Member validator // ============================================================ // // The IDE (unlike bascomp) has widget DXEs loaded, so wgtGetIface // returns live interface metadata. Combined with a static scan of // the project's .frm files (control name -> widget type), we can // reject typos like GfxCanvas.Boggle or LblStatus.Caphtion at // compile time instead of letting them surface as runtime errors // at event-click time. Dynamically-created controls (via // CreateControl at runtime) aren't in the map; lookupCtrlType // returns NULL for them and validation is skipped. // validatorFree -- release a validator context filled during a compile static void validatorFree(IdeValidatorCtxT *ctx) { arrfree(ctx->entries); arrfree(ctx->badTypes); ctx->entries = NULL; ctx->badTypes = NULL; } static bool validatorIsMethodValid(void *ctx, const char *wgtType, const char *methodName) { (void)ctx; if (!wgtType || !methodName) { return true; // be permissive on malformed input } if (basFormRtCommonMethodId(methodName) != BAS_CM_NONE) { return true; } // Drawing methods of forms and PictureBoxes if ((strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0) && basFormRtIsDrawMember(methodName, false)) { return true; } // Form-level methods if (strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0) { return strcasecmp(methodName, "Show") == 0 || strcasecmp(methodName, "Hide") == 0; } // Menu items have no methods beyond common if (strcasecmp(wgtType, IDE_MENU_TYPE_NAME) == 0) { return false; } // Widget-specific methods from the live iface const char *wgtName = wgtFindByBasName(wgtType); if (!wgtName) { return true; // unknown type -- skip validation } const WgtIfaceT *iface = wgtGetIface(wgtName); if (!iface) { return true; } // A known type with no interface methods accepts only the common // set (already checked above); being permissive here let typos // like btnGo.Boggle compile. for (int32_t i = 0; i < iface->methodCount; i++) { if (strcasecmp(iface->methods[i].name, methodName) == 0) { return true; } } return false; } static bool validatorIsPropValid(void *ctx, const char *wgtType, const char *propName) { (void)ctx; if (!wgtType || !propName) { return true; } // Drawing properties of forms and PictureBoxes if ((strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(wgtType, BAS_PICTUREBOX_TYPE) == 0) && basFormRtIsDrawMember(propName, true)) { return true; } // Form-level properties (the runtime's own table) if (strcasecmp(wgtType, IDE_FORM_TYPE_NAME) == 0) { return basFormRtFindFormProp(propName) != NULL; } // Menu items (the runtime's own table) if (strcasecmp(wgtType, IDE_MENU_TYPE_NAME) == 0) { return basFormRtFindMenuProp(propName) != NULL; } // Properties every non-menu control accepts (the runtime's own table) if (basFormRtFindCommonProp(propName)) { return true; } // Widget-specific props from the live iface const char *wgtName = wgtFindByBasName(wgtType); if (!wgtName) { return true; } const WgtIfaceT *iface = wgtGetIface(wgtName); if (!iface) { return true; } // Same reasoning as validatorIsMethodValid: an empty interface // property table must not turn validation off for the whole type. return wgtIfaceFindProp(iface, propName) != NULL; } // validatorLookupCtrlType -- widget type of ctrlName for the parser's // member checks, or NULL to skip validation. Resolved the way the // runtime finds controls: a name declared on the form whose code is being // compiled (basParserFormScope) wins; any other name is looked up across // every form and skipped when it has different types on different forms // (Text1 as TextBox and ListBox) rather than checked against the wrong one. static const char *validatorLookupCtrlType(void *ctx, const char *ctrlName) { IdeValidatorCtxT *v = (IdeValidatorCtxT *)ctx; const char *found = NULL; bool ambiguous = false; if (!v || !ctrlName) { return NULL; } const char *scope = v->compiler ? basCompilerFormScope(v->compiler) : ""; for (int32_t i = 0; i < (int32_t)arrlen(v->entries); i++) { const IdeCtrlMapEntryT *e = &v->entries[i]; if (strcasecmp(e->name, ctrlName) != 0) { continue; } if (scope[0] && strcasecmp(e->formName, scope) == 0) { return e->wgtType; } ambiguous = ambiguous || (found && strcasecmp(found, e->wgtType) != 0); found = e->wgtType; } return ambiguous ? NULL : found; } static void validatorOnCtrlBegin(void *ud, const char *typeName, const char *name) { IdeValidatorCtxT *v = ((IdeFrmScanT *)ud)->validator; if (!v) { return; } IdeCtrlMapEntryT e; memset(&e, 0, sizeof(e)); snprintf(e.name, BAS_MAX_IDENT, "%s", name ? name : ""); snprintf(e.wgtType, BAS_MAX_IDENT, "%s", typeName ? typeName : ""); snprintf(e.formName, BAS_MAX_IDENT, "%s", v->currentForm); arrput(v->entries, e); // Also flag unknown widget types. wgtFindByBasName returns NULL // for anything not registered by a .wgt DXE loaded by the IDE. // Skip internal structural types (they're handled by layout code, // not widget DXEs). if (typeName && typeName[0]) { bool structural = (strcasecmp(typeName, IDE_FORM_TYPE_NAME) == 0 || strcasecmp(typeName, IDE_MENU_TYPE_NAME) == 0); if (!structural && !wgtFindByBasName(typeName)) { IdeBadTypeT bad; memset(&bad, 0, sizeof(bad)); snprintf(bad.ctrlName, BAS_MAX_IDENT, "%s", name ? name : "?"); snprintf(bad.typeName, BAS_MAX_IDENT, "%s", typeName); snprintf(bad.formName, BAS_MAX_IDENT, "%s", v->currentForm); arrput(v->badTypes, bad); } } } static bool validatorOnFormBegin(void *ud, const char *name) { IdeValidatorCtxT *v = ((IdeFrmScanT *)ud)->validator; if (!v) { return true; } IdeCtrlMapEntryT e; memset(&e, 0, sizeof(e)); snprintf(v->currentForm, BAS_MAX_IDENT, "%s", name ? name : ""); snprintf(e.name, BAS_MAX_IDENT, "%s", v->currentForm); snprintf(e.wgtType, BAS_MAX_IDENT, "%s", IDE_FORM_TYPE_NAME); snprintf(e.formName, BAS_MAX_IDENT, "%s", v->currentForm); arrput(v->entries, e); return true; } static void validatorOnMenuBegin(void *ud, const char *name, int32_t level) { (void)level; IdeValidatorCtxT *v = ((IdeFrmScanT *)ud)->validator; if (!v) { return; } IdeCtrlMapEntryT e; memset(&e, 0, sizeof(e)); snprintf(e.name, BAS_MAX_IDENT, "%s", name ? name : ""); snprintf(e.wgtType, BAS_MAX_IDENT, "%s", IDE_MENU_TYPE_NAME); snprintf(e.formName, BAS_MAX_IDENT, "%s", v->currentForm); arrput(v->entries, e); } // vmCurProcIdx -- compiled proc the paused VM is in, or -1 at module // level. Proc bodies carry no end address and module-level code can sit // after a proc (a later file, form init code), so the frame depth -- not // the pc -- says whether a proc is executing. static int32_t vmCurProcIdx(void) { return sVm->callDepth > 1 ? vmProcIndexForPc(sVm->pc) : -1; } // vmProcIndexForPc -- index of the compiled proc whose codeAddr is the // greatest one <= pc (-1 before every proc). Only meaningful for a pc // known to be inside a proc (see vmCurProcIdx). Requires sDbgModule. static int32_t vmProcIndexForPc(int32_t pc) { int32_t best = -1; int32_t bestAddr = -1; for (int32_t i = 0; i < sDbgModule->procCount; i++) { int32_t addr = sDbgModule->procs[i].codeAddr; if (addr <= pc && addr > bestAddr) { bestAddr = addr; best = i; } } return best; } static const char *vmProcNameForPc(int32_t pc) { int32_t idx = vmProcIndexForPc(pc); return idx >= 0 ? sDbgModule->procs[idx].name : IDE_MODULE_FRAME; } 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); } 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'; } // writeProjectFile -- write files[idx].buffer to disk (a NULL buffer is // an empty file), byte for byte. The text goes to a temporary sibling // file that platformReplaceFile swaps in only once every write // succeeded, so a failed save never destroys the copy on disk. Clears // the modified flags only on success; reports the failure otherwise. static bool writeProjectFile(int32_t idx) { PrjFileT *file = &sProject.files[idx]; const char *text = file->buffer ? file->buffer : ""; char fullPath[DVX_MAX_PATH]; char tempPath[DVX_MAX_PATH]; char bakPath[DVX_MAX_PATH]; prjFullPath(&sProject, idx, fullPath, sizeof(fullPath)); bool ok = platformPathSibling(tempPath, sizeof(tempPath), fullPath, PLATFORM_TMP_EXT) && platformPathSibling(bakPath, sizeof(bakPath), fullPath, PLATFORM_BAK_EXT); if (ok) { size_t len = strlen(text); FILE *f = fopen(tempPath, "wb"); ok = (f != NULL); if (f) { ok = (fwrite(text, 1, len, f) == len); ok = (fclose(f) == 0) && ok; } // The swap keeps the original on failure (and removes tempPath) if (ok) { ok = platformReplaceFile(tempPath, fullPath, bakPath); } else { remove(tempPath); } } if (!ok) { char msg[IDE_MSG_BUF]; snprintf(msg, sizeof(msg), "Could not write %s.", file->path); ideErrorBox(msg); return false; } file->modified = false; if (idx == sDesignerFileIdx && sDesigner.form) { sDesigner.form->dirty = false; } return true; } 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, IDE_HELP_FILE); snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile); ctx->onHelpQuery = helpQueryHandler; ctx->helpQueryCtx = NULL; prjInit(&sProject); buildWindow(); // Load persisted settings shellEnsureConfigDir(sCtx); char prefsPath[DVX_MAX_PATH]; shellConfigPath(sCtx, IDE_PREFS_FILE, prefsPath, sizeof(prefsPath)); sPrefs = prefsLoad(prefsPath); if (!sPrefs) { sPrefs = prefsCreate(); prefsSaveAs(sPrefs, prefsPath); } if (sWin && sWin->menuBar) { if (sToolbar) { bool showTb = prefsGetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_TOOLBAR, true); sToolbar->visible = showTb; wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb); } if (sStatusBar) { bool showSb = prefsGetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_STATUSBAR, true); sStatusBar->visible = showSb; wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb); } bool saveOnRun = prefsGetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_SAVE_ON_RUN, true); wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun); sOutputToLog = prefsGetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_OUTPUT_TO_LOG, false); wmMenuItemSetChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG, sOutputToLog); } // Load recent files list and populate menu recentLoad(); recentRebuildMenu(); if (sWin) { dvxFitWindowH(sAc, sWin); } updateProjectMenuState(); setStatus("Ready."); // A project or source file on the command line (a document opened // through its file type) opens at once. char arg[DVX_MAX_PATH]; bool quoted = false; if (dvxNextArg(ctx->args, arg, sizeof(arg), "ed)) { openAnyPath(arg); } return 0; }