// 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 "dvxText.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 "../basdev/basDebug.h" #include "../basdev/basHelpMap.h" #include "../basdev/basProcs.h" #include "../basdev/basSource.h" #include "../basdev/basValidate.h" #include "../basRes.h" #include "../compiler/basEvents.h" #include "../compiler/compiler.h" #include "../formrt/basRunHost.h" #include "../formrt/formrt.h" #include "../formrt/frmParser.h" #include "dvxRes.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_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 (FRM_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_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_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_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log #define IDE_MAIN_TITLE "DVX BASIC" #define IDE_GENERAL_SECTION "(General)" #define IDE_GLOBAL_SECTION "(Global)" #define IDE_DEFAULT_FORM "Form1" #define IDE_HELP_FILE "dvxbasic.hlp" #define IDE_PREFS_FILE "dvxbasic.ini" #define IDE_FORM_TYPE_NAME FRM_KW_FORM // designer type name of a form #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_RENAME_TEMP_FMT "IDE_MNU_REN%d" // placeholder during menu renames #define IDE_NEW_CLASS_FMT "Class%d" // name of a new class module #define IDE_NEW_CTL_FMT "UserControl%d" // name of a new UserControl #define IDE_MAKE_EXE_LABEL "&Make Executable..." // File menu: a program project's build #define IDE_MAKE_CTL_LABEL "&Make Control..." // File menu: a control project's build #define IDE_UC_DEFAULT_EVENT BAS_UC_EVT_INIT // the UserControl object's event when none has code yet #define IDE_CTL_NO_RUN "A control project is not run: make its control file (File > Make Control), or add its .ctl files to a program project." // 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_REM "REM" #define FRM_BEGIN_TAG "Begin " #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 // Code editor: the built-in colorizer, and the line markers (0xRRGGBB) // of a breakpoint (gutter dot) and the paused line (background) #define IDE_SYNTAX_MODE "bas" #define IDE_BP_GUTTER_RGB 0xC80000 #define IDE_DBG_LINE_RGB 0xFFFF80 // 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_PRJ_NEW_CTL 177 #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; // 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; // How concatProject ended typedef enum { ConcatOkE, ConcatUnreadableE, // a file cannot be read (missing or too large) ConcatTooLargeE, // the source outgrows the editor's limit ConcatFailedE // the build refused a file (ps->error says why) } IdeConcatE; // One .frm parse during compile: the validator map plus the code section typedef struct { BasValidatorT *validator; // NULL = do not collect controls char *code; // malloc'd code section (NULL = none) } IdeFrmScanT; // 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 void addClassEvent(void *ctx, const char *name, const char *params); static bool addFileToProject(const char *path); static void addWithEvents(void *ctx, const char *name, const char *className); 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 void applySyntaxColors(void); 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 bool classNameUsed(const char *name, const char *fileName); static void cleanupFormWin(void); static void clearAllBreakpoints(void); static void clearOutput(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 IdeConcatE concatProject(BasProjectSourceT *ps, BasValidatorT *validator, bool withForms, BasSourceMapT **map, int32_t *outFileIdx); static int32_t countLines(const char *text); static void createEventSubSkeleton(const char *objName, const char *evtName); static char **ctlHeaders(void); static void currentObjName(char *out, int32_t outSize); static void debugNavigateToLine(int32_t concatLine); static void debugResume(const char *status); static void debugStartOrResume(int32_t cmd); static void debugStop(void); static void debugUpdateWindows(void); static bool dedupeBreakpoints(void); static void deleteSelectedControl(void); static void designCtlRefresh(void); static void designCtlRelease(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 bool editorIsControl(void); 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 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 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 const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen); static const IdeWithEventsT *findWithEvents(const char *name); static void flushOutput(void); static void freeProcBufs(void); static void freeTexts(char **texts); 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 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 helpQueryHandler(void *ctx); static void helpSetCtrlTopic(const char *typeName); static void ideErrorBox(const char *msg); static void idePump(void); static void immPrintCallback(void *ctx, const char *text, bool newline); static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize); static void invalidateCompiled(void); static bool isCtrlArrayInDesigner(const char *ctrlName); 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 listRowsAddRow(void *ctx, const char *const *cells, int32_t cellCount); 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, BasValidatorT *validator, char **outCtrlList); static bool loadDesignerFile(int32_t fileIdx); static void loadFile(void); static void loadFormCodeIntoEditor(void); static void loadModuleIntoEditor(int32_t fileIdx, const char *source); static void loadRunFiles(BasFormRtT *rt); static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText); static int32_t localToConcatLine(int32_t editorLine); static void logFlushLine(void); static void makeExecutable(void); 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 newClassFile(PrjFileKindE kind); static void newProject(void); static const char *objectType(const char *objName); 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 onCtlHeaderSet(const char *key, const char *value); 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 onUcPropChanged(WidgetT *w, const char *propName); 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 void parseProcs(const char *source); 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 int32_t procIndexByName(const char *objName, const char *evtName); static int32_t procIndexForCodeLine(int32_t codeLine); 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 propsFollowActive(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 refreshEditorMarkers(void); static void remapBreakpoints(const IdeLineAnchorT *anchors, const int32_t *starts); static void removeBreakpointsForFile(int32_t fileIdx); 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 void runCached(void); static void runFontsAdd(int32_t owner); static void runModule(BasModuleT *mod); static BasVmResultE runProgramSlices(BasFormRtT *rt); static bool saveActiveFile(void); static bool saveAllModified(void); static bool saveCurProc(bool discardSkeleton); static void saveFile(void); static bool saveProjectFile(void); 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 setFileClassName(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 showProperties(void); static void showToolbox(void); static void showWatchWindow(void); static const char *skipLine(const char *p); 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 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 watchEditSelected(void); static bool writeProjectFile(int32_t idx); int32_t appMain(DxeAppContextT *ctx); // ============================================================ // 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 BasModuleT *sDesignModule = NULL; // the project's UserControls as the designer runs them (basCtlLoadDesign) 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 BasBreakTitleT *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) // ============================================================ // 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; // ============================================================ // 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); } propsFollowActive(); 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(); propsFollowActive(); 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; } // addClassEvent -- basSourceClassEvents callback: one Event of a class // module into the IdeClassEventT array ctx points at static void addClassEvent(void *ctx, const char *name, const char *params) { IdeClassEventT **events = (IdeClassEventT **)ctx; IdeClassEventT evt; memset(&evt, 0, sizeof(evt)); snprintf(evt.name, sizeof(evt.name), "%s", name); snprintf(evt.params, sizeof(evt.params), "%s", params); arrput(*events, evt); } // 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]; platformPathDirName(path, dir, sizeof(dir)); if (!platformPathSame(dir, sProject.projectDir)) { ideErrorBox("Files must be located in the project directory."); return false; } for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (platformPathSame(sProject.files[i].path, fileName)) { 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); if (sProject.files[prjFileCount(&sProject) - 1].kind == PrjKindControlE) { designCtlRefresh(); } return true; } // addWithEvents -- basSourceWithEvents callback: one WithEvents variable // of the editor's file into sWithEvents static void addWithEvents(void *ctx, const char *name, const char *className) { IdeWithEventsT we; (void)ctx; snprintf(we.name, sizeof(we.name), "%s", name); snprintf(we.className, sizeof(we.className), "%s", className); arrput(sWithEvents, we); } // 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 or // UserControl's code after its header wrapped in BEGINCLASS (a // UserControl's with its mark), 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 || file->kind == PrjKindControlE) { 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 = prjKindWrap((PrjFileKindE)file->kind); } return basBuildAppendFile(ps, text + codePos, len - codePos, wrap, name, ctrlList, &entry->startLine, &entry->lineCount); } static void applySyntaxColors(void) { if (!sEditor) { return; } wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, TEXT_SYNTAX_MAX); } // ============================================================ // 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, "New &UserControl", CMD_PRJ_NEW_CTL); wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE); wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL); wmAddMenuSeparator(fileMenu); wmAddMenuItem(fileMenu, IDE_MAKE_EXE_LABEL, 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) { basSourceClassEvents(text, addClassEvent, events); } free(owned); if (match) { return; } } } // classFollowsFile -- whether class module or UserControl fileIdx (whose // text is given) is named after its file: a class module with no // Attribute VB_Name or one equal to the file's base name, a UserControl // whose Begin UserControl name is (as File > New Class Module / New // UserControl write them) static bool classFollowsFile(int32_t fileIdx, const char *text) { char ownName[BAS_MAX_IDENT]; char fileName[BAS_MAX_IDENT]; basExtractClassName("", 0, sProject.files[fileIdx].path, fileName, (int32_t)sizeof(fileName)); return !prjClassName(&sProject, fileIdx, text, ownName, (int32_t)sizeof(ownName)) || strcasecmp(ownName, fileName) == 0; } // classNameUsed -- true when a project file is named fileName or a form, // class module or UserControl 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 = platformPathSame(file->path, fileName) || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0); if (!used && (file->kind == PrjKindClassE || file->kind == PrjKindControlE)) { 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) { basDsgnSetDesignWindow(sFormWin, false); 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]->userData = NULL; } } sDesigner.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(""); } 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(); designCtlRelease(); 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; BasValidatorT 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; int32_t failIdx = -1; IdeConcatE concat = concatProject(&project, &validatorCtx, true, &sProject.sourceMap, &failIdx); if (concat == ConcatUnreadableE) { sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE); showCompileError("Compilation failed: cannot read file."); } else if (concat == ConcatTooLargeE) { sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path); showCompileError("Compilation failed: source too large."); } else if (concat == ConcatFailedE) { setStatus(project.error); dvxSetBusy(sAc, false); } if (concat != ConcatOkE) { basBuildFreeProject(&project); basValidateFree(&validatorCtx); return false; } 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 = frmEmitClamped(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++) { BasBadTypeT *b = &validatorCtx.badTypes[i]; n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName); } n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n"); sOutputLen = n; showCompileError("Compilation failed: unknown widget type."); basBuildFreeProject(&project); basValidateFree(&validatorCtx); return false; } // 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; BasCompileErrorT err; basValidateCallbacks(&validatorCtx, &validator); BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err); basBuildFreeProject(&project); basValidateFree(&validatorCtx); if (!mod && err.result != BAS_COMPILE_ERROR) { setStatus(err.message); dvxSetBusy(sAc, false); return false; } if (!mod) { // 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 = err.line; const char *errFile = ""; if (err.line > 0 && prjFileCount(&sProject) > 0) { prjMapLine(&sProject, err.line, &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 (err.line > 0 && errFileIdx >= 0) { int32_t procIdx = navigateToCodeLine(errFileIdx, errLocalLine, false); // The caret goes to the column the error is at, when known if (err.col > 0 && sEditor) { wgtTextAreaSetCursor(sEditor, wgtTextAreaGetCursorLine(sEditor), err.col); } if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) { snprintf(procName, sizeof(procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName); procLine = wgtTextAreaGetCursorLine(sEditor); } } // Show the error (without its "Line N, Col M: " prefix) with the // procedure name and proc-relative line const char *msg = basBuildErrorText(err.message); if (procName[0]) { sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg); } else { sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg); } showCompileError("Compilation failed."); 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; } // concatProject -- the compile source of the project into ps: one pass per // file kind in compile order -- .bas modules first (so CONST declarations // are available), then .cls class modules, then .ctl UserControls, then // (withForms) the .frm code sections (see basBuildConcatProject) -- each // file's source map entry appended to *map and the forms' controls added // to validator (may be NULL). On failure *outFileIdx names the file. static IdeConcatE concatProject(BasProjectSourceT *ps, BasValidatorT *validator, bool withForms, BasSourceMapT **map, int32_t *outFileIdx) { for (int32_t kind = 0; kind < PrjKindCountE; kind++) { for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].kind != kind || (kind == PrjKindFormE && !withForms)) { continue; } char *ctrlList = NULL; char *fileSrc = loadCompileSource(i, validator, &ctrlList); *outFileIdx = i; if (!fileSrc) { return ConcatUnreadableE; } int32_t fileLen = (int32_t)strlen(fileSrc); // BEGINFORM/ENDFORM directives plus a trailing newline if (ps->sourceLen + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) { free(fileSrc); free(ctrlList); return ConcatTooLargeE; } // 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(ps, i, fileSrc, fileLen, ctrlList, &mapEntry); free(fileSrc); free(ctrlList); if (!ok) { return ConcatFailedE; } arrput(*map, mapEntry); dvxUpdate(sAc); } } return ConcatOkE; } 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, with the parameters of the // object's event: a control's (its type's, basCtlEventParams), a // WithEvents variable's class Event's, or in a .ctl the UserControl // object's own (Private: a Public one would be a method of the control). // 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]; char arrayParams[IDE_EVENT_PARAMS_BUF]; const char *objType = objectType(objName); bool ucObject = strcmp(objType, BAS_UC_OBJECT) == 0; const char *params = basCtlEventParams(objType[0] ? objType : NULL, 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 (!ucObject && isCtrlArrayInDesigner(objName)) { snprintf(arrayParams, sizeof(arrayParams), "Index As Integer%s%s", params[0] ? ", " : "", params); params = arrayParams; } snprintf(skeleton, sizeof(skeleton), PRJ_HANDLER_FMT, ucObject ? PRJ_PRIVATE : "", 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); } // ctlHeaders -- the header of every UserControl of the project (its text // up to the class code: VERSION and the Begin UserControl ... End block), // in project order, the way CTLn resources and the designer take them. // The text is the same the compile used (loaded buffers, disk only for // never-loaded files). stb_ds array of malloc'd texts (freeTexts). static char **ctlHeaders(void) { char **headers = NULL; for (int32_t i = 0; i < prjFileCount(&sProject); i++) { if (sProject.files[i].kind != PrjKindControlE) { continue; } bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); int32_t len = text ? basClassCodePos(text, (int32_t)strlen(text)) : 0; char *header = text ? (char *)malloc((size_t)len + 1) : NULL; if (header) { memcpy(header, text, (size_t)len); header[len] = '\0'; arrput(headers, header); } free(owned); } return headers; } // 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] : ""); } // 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; basRunHostTagBreak(sDbgFormRt, &sBreakTitles, false); if (sVm) { basVmResume(sVm); } refreshEditorMarkers(); updateProjectMenuState(); setStatus(status); } 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 if (sVm) { basVmStop(sVm); } basRunHostTagBreak(sDbgFormRt, &sBreakTitles, false); sDbgState = sVm ? DbgStoppingE : DbgIdleE; sDbgCurrentLine = -1; sDbgEnabled = false; refreshEditorMarkers(); 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(); } // 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(); } } // designCtlRefresh -- the project's UserControls for the designer: their // code (with the modules and class modules it may use, not the forms) // compiled and registered (basCtlLoadDesign) in place of the set before, // so the toolbox offers them and the forms being designed hold live // instances that run the code with UserMode False. The designer's // widgets, the toolbox and the properties window follow. A project whose // controls do not compile has none in the designer until they do (the // status line says so); while code of the old set runs nothing changes. static void designCtlRefresh(void) { char **headers = ctlHeaders(); int32_t count = (int32_t)arrlen(headers); BasModuleT *module = NULL; BasProjectSourceT ps; BasSourceMapT *map = NULL; int32_t failIdx = -1; memset(&ps, 0, sizeof(ps)); if (count > 0 && concatProject(&ps, NULL, false, &map, &failIdx) == ConcatOkE) { BasCompileErrorT err; module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err); } basBuildFreeProject(&ps); arrfree(map); if (!basCtlUnloadDesign(NULL)) { basModuleFree(module); freeTexts(headers); return; } basModuleFree(sDesignModule); sDesignModule = module; int32_t registered = module ? basCtlLoadDesign(NULL, module, (const char *const *)headers, count, sProject.projectPath) : 0; freeTexts(headers); if (count > 0 && !module) { setStatus("The project's UserControls do not compile; the designer cannot show them until they do (compile with F5 to see why)."); } else if (registered < count) { setStatus("A UserControl of the project has the name of an installed control; the designer cannot show it."); } if (sFormWin && sDesigner.form) { dsgnRebuildWidgets(&sDesigner); dvxInvalidateWindow(sAc, sFormWin); } if (sToolboxWin) { tbxDestroy(sAc, sToolboxWin); sToolboxWin = NULL; showToolbox(); } if (sPropsWin) { prpRebuildTree(&sDesigner); prpRefresh(&sDesigner); } } // designCtlRelease -- the designer lets the project's UserControls go // (the project closes). While their code runs (a message box of theirs // is up) they stay, and so does their module. static void designCtlRelease(void) { if (basCtlUnloadDesign(NULL)) { basModuleFree(sDesignModule); sDesignModule = NULL; } } // 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; } // The selected control and its children as .frm text, by the writer // the .frm saver uses int32_t len = 0; char *buf = frmModelCtrlText(sDesigner.form, sDesigner.form->controls[sDesigner.selectedIdx], &len); if (buf) { dvxClipboardCopy(buf, len); } else { setStatus("A property value of this control is too long to copy."); } free(buf); } // dsgnPasteControl -- paste a control (and its children) copied with // dsgnCopySelected. The clipboard blocks are read by the .frm model's // loader, so every property comes back exactly as a saved form would. static void dsgnPasteControl(void) { if (!sDesigner.form || !sDesigner.contentBox) { return; } int32_t clipLen = 0; const char *clip = dvxClipboardGet(&clipLen); if (!clip || clipLen <= 0 || !kwMatch(dvxSkipWs(clip), FRM_BEGIN_TAG)) { return; } FrmModelT *pasted = frmModelCtrlsFromText(clip, clipLen); if (!pasted) { return; } // A control with the same name exists: continue its control array, // offer to start one, or rename the paste FrmCtrlT *src = pasted->controls[0]; char newName[FRM_MAX_NAME]; int32_t newIndex = FRM_NO_INDEX; bool nameExists = false; int32_t highIdx = FRM_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, src->name) == 0) { nameExists = true; if (sDesigner.form->controls[i]->index > highIdx) { highIdx = sDesigner.form->controls[i]->index; } } } if (nameExists && highIdx != FRM_NO_INDEX) { snprintf(newName, sizeof(newName), "%s", src->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) { // The existing control (and its children's parent link) becomes index 0 frmModelMakeArrayMember(sDesigner.form, sDesigner.form->controls[frmModelFindCtrl(sDesigner.form, src->name, FRM_NO_INDEX)], 0); snprintf(newName, sizeof(newName), "%s", src->name); newIndex = 1; } else { frmModelAutoName(sDesigner.form, src->typeName, newName, sizeof(newName)); } FrmCtrlT *ctrl = frmModelInsertTree(sDesigner.form, pasted, src, NULL, newName, newIndex); frmModelFree(pasted); if (!ctrl) { return; } // Widgets for the pasted controls (the others have theirs) dsgnCreateWidgets(&sDesigner, sDesigner.contentBox); sDesigner.selectedIdx = frmModelCtrlPos(sDesigner.form, ctrl); 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; } } // editorIsControl -- true when the editor holds a UserControl (.ctl): // its object is UserControl, with the UserControl_* events static bool editorIsControl(void) { return sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject) && sProject.files[sEditorFileIdx].kind == PrjKindControlE; } // 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))); refreshEditorMarkers(); 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); prjApplyDefaults(&sProject, 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."); designCtlRefresh(); 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. // evaluateImmediate -- run a line typed into the Immediate window (an // assignment to the paused program's variable, a statement, or an // expression to print); an assignment refreshes the debug windows. static void evaluateImmediate(const char *expr) { if (basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL)) { updateLocalsWindow(); updateWatchWindow(); } } // 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; } // 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); } // 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; } // 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)); } } // 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); } // freeTexts -- free an stb_ds array of malloc'd texts and the array static void freeTexts(char **texts) { for (int32_t i = 0; i < (int32_t)arrlen(texts); i++) { free(texts[i]); } arrfree(texts); } // 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) { basValidateScanFrm(scan->validator, frmText, (int32_t)strlen(frmText), frmScanOnFormEnd, scan); } // frmScanOnFormEnd -- capture the code after the form's End exactly as // the designer loads it (frmCodeSection), 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 = frmCodeSection(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; } 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(); updateProjectMenuState(); if (sProjectWin) { prjRebuildTree(&sProject); } } } break; case CMD_PRJ_NEW_CLASS: if (isIdle && hasProject()) { newClassFile(PrjKindClassE); } break; case CMD_PRJ_NEW_CTL: if (isIdle && hasProject()) { newClassFile(PrjKindControlE); } 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--; } bool rmControl = rmFile->kind == PrjKindControlE; removeBreakpointsForFile(rmIdx); prjRemoveFile(&sProject, rmIdx); // adjusts activeFileIdx invalidateCompiled(); updateDropdowns(); prjRebuildTree(&sProject); updateProjectMenuState(); updateDirtyIndicators(); if (rmControl) { designCtlRefresh(); } 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; if (sVm) { basVmClearStepState(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 if (sVm) { basVmClearStepState(sVm); } 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: showToolbox(); break; case CMD_WIN_PROPS: showProperties(); 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; } 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 = basHelpKeywordTopic(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) { basHelpCtrlTopic(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); bool codeOnly = prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true); // Rename in the per-procedure buffers (the code bound to the editor) bool editorChanged = false; bool shownChanged = false; if (sGeneralBuf) { char *replaced = basSourceRename(sGeneralBuf, oldName, newName, codeOnly); 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 = basSourceRename(sProcBufs[i], oldName, newName, codeOnly); 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 = basSourceRename(sDesigner.form->code, oldName, newName, codeOnly); 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 = basSourceRename(text, oldName, newName, codeOnly); 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(); } // 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 = basDbgAppendDropping(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, text, (int32_t)strlen(text)); } if (newline) { basDbgAppendDropping(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)); } 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); } // 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; } // isRemAt -- true if the word REM (a comment to end of line) starts at // line[pos]: a whole word in any case (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 : ""); } // listRowsAddRow -- a BasDbgRowFnT: one row of cells into ctx (IdeListRowsT) static void listRowsAddRow(void *ctx, const char *const *cells, int32_t cellCount) { for (int32_t i = 0; i < cellCount; i++) { listRowsAdd((IdeListRowsT *)ctx, cells[i]); } } 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 (and no control list) when the file cannot be read or // memory runs out. static char *loadCompileSource(int32_t fileIdx, BasValidatorT *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); // Out of memory: the caller frees nothing when the source is NULL if (!packed) { free(*outCtrlList); *outCtrlList = NULL; } 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)" }, { "UserControls (*.ctl)" }, { "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); } // 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); } // loadRunFiles -- register the project's UserControls and forms with the // form runtime of a run, as the built program does: each .ctl's header // (its control class, before any form can use it) and each .frm's text. // The text is the same the compile used (loaded buffers, disk only for // never-loaded files). A form's registration only caches the text; the // caller's basRunHostLoadForms then instantiates them, so a Load handler // that does "Load Form2" finds Form2's .frm regardless of project file // order. static void loadRunFiles(BasFormRtT *rt) { for (int32_t i = 0; i < prjFileCount(&sProject); i++) { PrjFileT *file = &sProject.files[i]; if (file->kind != PrjKindFormE && file->kind != PrjKindControlE) { continue; } bool missing = false; char *owned = NULL; const char *text = projectFileText(i, &owned, &missing); if (text && file->kind == PrjKindControlE) { basFormRtRegisterCtl(rt, text, (int32_t)strlen(text)); } else if (text) { basRunHostRegisterFrm(rt, text, (int32_t)strlen(text)); } free(owned); } } 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; } // makeExecutable -- File > Make Executable: the program (.app), or for a // control project (File > Make Control) its control file (.wgt, named // after the project), through the build code bascomp uses 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 bool control = prjIsControlProject(&sProject); FileFilterT appFilters[] = { { "DVX Applications (*.app)" }, { "All Files (*.*)" } }; FileFilterT ctlFilters[] = { { "Control Files (*.wgt)" }, { "All Files (*.*)" } }; char outPath[DVX_MAX_PATH]; outPath[0] = '\0'; if (control) { snprintf(outPath, sizeof(outPath), "%s" BAS_WGT_EXT, sProject.name); } if (!dvxFileDialog(sAc, control ? "Make Control" : "Make Executable", FD_SAVE, NULL, control ? ctlFilters : appFilters, control ? DVX_ARRAY_LEN(ctlFilters) : DVX_ARRAY_LEN(appFilters), 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(control ? "Building control file..." : (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; char **ctlSources = ctlHeaders(); spec.module = sCachedModule; spec.frmSources = (const char *const *)frmSources; spec.frmCount = (int32_t)arrlen(frmSources); spec.ctlSources = (const char *const *)ctlSources; spec.ctlCount = (int32_t)arrlen(ctlSources); spec.release = release; spec.multiInstance = sProject.multiInstance; spec.fontCount = prjFontCount(&sProject); for (int32_t i = 0; i < spec.fontCount && i < BAS_BUILD_FONTS_MAX; i++) { spec.fonts[i] = prjFontPath(&sProject, i); } const char *failure = control ? basBuildControl(outPath, &spec) : basBuildApp(outPath, &spec); freeTexts(frmSources); freeTexts(ctlSources); 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); } 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); refreshEditorMarkers(); 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)) { FrmCtrlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx]; ctrlName = ctrl->name; eventName = basCtlDefaultEvent(ctrl->typeName); } else { ctrlName = sDesigner.form->name; eventName = basCtlDefaultEvent(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); } // newClassFile -- add class module ClassN or UserControl UserControlN // (kind; the first N whose name no form, class or control of the project // uses and whose file does not exist yet), holding the standard header // (a UserControl's: prjCtlNew), to the project and open it in the editor. static void newClassFile(PrjFileKindE kind) { bool control = (kind == PrjKindControlE); const char *ext = control ? BAS_CTL_EXT : BAS_CLS_EXT; 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), control ? IDE_NEW_CTL_FMT : IDE_NEW_CLASS_FMT, (int)n); snprintf(fileName, sizeof(fileName), "%s%s", name, ext); platformPathJoin(path, sizeof(path), sProject.projectDir, fileName); if (!classNameUsed(name, fileName) && !prjPathExists(path)) { break; } } char classText[sizeof(IDE_NEW_CLASS_TEXT) + BAS_MAX_IDENT]; char *text = NULL; if (control) { text = prjCtlNew(name); } else { snprintf(classText, sizeof(classText), IDE_NEW_CLASS_TEXT, name); text = strdup(classText); } bool wasDirty = sProject.dirty; int32_t idx = prjAddFile(&sProject, fileName, kind); sProject.files[idx].buffer = text; sProject.files[idx].modified = true; if (!text || !activateFile(idx, ViewCodeE)) { prjRemoveFile(&sProject, idx); sProject.dirty = wasDirty; return; } invalidateCompiled(); prjRebuildTree(&sProject); updateDirtyIndicators(); setStatus(control ? "UserControl added." : "Class module added."); if (control) { designCtlRefresh(); } } 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, BAS_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); prjApplyDefaults(&sProject, sPrefs); snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath); saveProjectFile(); ensureProjectWindow(false); updateMainTitle(); setStatus("New project created."); updateProjectMenuState(); } // objectType -- the designer type of an Object dropdown entry: the form // (IDE_FORM_TYPE_NAME), a menu item (FRM_KW_MENU), a control's type, in a // .ctl the UserControl object (BAS_UC_OBJECT), else "" (a WithEvents // variable, or a name that is none of these) static const char *objectType(const char *objName) { if (editorIsControl()) { return strcasecmp(objName, BAS_UC_OBJECT) == 0 ? BAS_UC_OBJECT : ""; } if (!sDesigner.form) { return ""; } if (strcasecmp(objName, sDesigner.form->name) == 0) { return IDE_FORM_TYPE_NAME; } for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) { if (strcasecmp(sDesigner.form->menuItems[i].name, objName) == 0) { return FRM_KW_MENU; } } for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) { if (strcasecmp(sDesigner.form->controls[i]->name, objName) == 0) { return sDesigner.form->controls[i]->typeName; } } return ""; } 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) { basVmSetDebugPaused(sVm, true); } if (line > 0) { sDbgCurrentLine = line; debugNavigateToLine(line); } basRunHostTagBreak(sDbgFormRt, &sBreakTitles, 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(); } 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(); basUcSetPropChangedHook(NULL); 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; 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; } // onCtlHeaderSet -- the Properties window changed header key key of the // active UserControl: its text rewritten (prjCtlHeaderSet: that line, or // the name on the Begin line; the code untouched) as an unsaved edit, the // editor and the file's breakpoints following (a line added or removed // moves every code line), the designer's controls refreshed. A new name // must be free in the project. static void onCtlHeaderSet(const char *key, const char *value) { int32_t idx = sProject.activeFileIdx; if (idx < 0 || idx >= prjFileCount(&sProject) || sProject.files[idx].kind != PrjKindControlE) { return; } if (idx == sEditorFileIdx) { stashEditor(); } bool missing = false; char *owned = NULL; const char *text = projectFileText(idx, &owned, &missing); char oldName[BAS_MAX_IDENT]; bool rename = strcasecmp(key, BAS_PROP_NAME) == 0 && text && prjClassName(&sProject, idx, text, oldName, (int32_t)sizeof(oldName)) && strcasecmp(oldName, value) != 0; char *changed = (text && !(rename && classNameUsed(value, ""))) ? prjCtlHeaderSet(text, key, value) : NULL; if (!changed) { char msg[IDE_MSG_BUF]; snprintf(msg, sizeof(msg), rename ? "The project already has a form, class module, or UserControl named %s, or it is not a valid name." : "%s cannot be set to that value.", rename ? value : key); ideErrorBox(msg); free(owned); return; } int32_t delta = countLines(changed) - countLines(text); PrjFileT *file = &sProject.files[idx]; free(owned); free(file->buffer); file->buffer = changed; file->modified = true; for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { if (sBreakpoints[i].fileIdx == idx) { sBreakpoints[i].codeLine += delta; } } if (delta != 0) { syncVmBreakpoints(); updateBreakpointWindow(); } if (idx == sEditorFileIdx) { loadModuleIntoEditor(idx, changed); } invalidateCompiled(); updateDirtyIndicators(); designCtlRefresh(); propsFollowActive(); } 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, else the resize cursors over // the selection's handles if (sDesigner.activeTool[0] != '\0') { return CURSOR_CROSSHAIR; } return basDsgnCursorAt(dsgnSelectedWidget(&sDesigner), x, y); } 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" "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; } // What the object is: the form, a menu item (only event is Click), a // control, the UserControl object of a .ctl const char *objType = objectType(selObj); bool ucObject = strcmp(objType, BAS_UC_OBJECT) == 0; // The events of this kind of object (the master table's, then the // widget's own: basCtlEventAt, as designer.bas lists them), a // WithEvents variable's class's Events, or the UserControl events the // compiler knows (NULL-terminated stb_ds temp array) const char **availEvents = NULL; if (withEvents) { for (int32_t i = 0; i < (int32_t)arrlen(classEvents); i++) { arrput(availEvents, classEvents[i].name); } } else if (ucObject) { for (int32_t i = 0; basUcEventInfo(i); i++) { arrput(availEvents, basUcEventInfo(i)->name); } } else { for (int32_t i = 0; basCtlEventAt(objType, i); i++) { arrput(availEvents, basCtlEventAt(objType, i)); } } arrput(availEvents, NULL); // Bracketed: no handler written yet 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 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 kind of object, which will create its skeleton const char *defEvt = withEventsDefault; if (ucObject) { defEvt = IDE_UC_DEFAULT_EVENT; } else if (!withEvents) { defEvt = basCtlDefaultEvent(objType); } 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); } // onUcPropChanged -- PropertyChanged in the code of a UserControl on the // designer: the properties window reads the values again static void onUcPropChanged(WidgetT *w, const char *propName) { (void)w; (void)propName; if (sPropsWin && sDesigner.form) { prpRefresh(&sDesigner); } } 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), BAS_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."); designCtlRefresh(); 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 = basDbgAppendDropping(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; } // 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 : ""); } } // 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 = basProcDeclAt(p, &isSub); const char *endTag = (afterKw && !isSub) ? BAS_PROC_KW_END_FUNCTION : BAS_PROC_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; } // 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; } // 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 = basProcDeclAt(dvxSkipWs(procText), &isSub); basProcNameFromDecl(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; } // propsFollowActive -- the Properties window shows the header of the // active file when it is a UserControl (its newest text: the editor's // when it holds the file), else the designer's selection static void propsFollowActive(void) { int32_t idx = sProject.activeFileIdx; if (idx < 0 || idx >= prjFileCount(&sProject) || sProject.files[idx].kind != PrjKindControlE) { prpShowCtl(NULL, NULL); return; } if (idx == sEditorFileIdx) { stashEditor(); } bool missing = false; char *owned = NULL; const char *text = projectFileText(idx, &owned, &missing); prpShowCtl(text, onCtlHeaderSet); free(owned); } // 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: the designer form's, or in a // .ctl the UserControl object const char **objNames = NULL; // stb_ds temp array if (editorIsControl()) { arrput(objNames, BAS_UC_OBJECT); } else 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 (basProcDeclAt(dvxSkipWs(text), &isSub)) { basProcSkipToEnd(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 (platformPathSame(sRecentFiles[i], path)) { 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 (platformPathSame(sRecentFiles[i], path)) { 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); } // refreshEditorMarkers -- the code editor's line markers: a gutter dot on // each breakpoint of the shown section, the paused line's background. // Called whenever the breakpoints, the paused line or the shown section // change (updateBreakpointWindow, editorSetText, the debugger). static void refreshEditorMarkers(void) { if (!sEditor) { return; } int32_t base = editorLineToCodeLine(1) - 1; // code line before editor line 1 bool pausedHasBp = false; wgtTextAreaClearLineMarkers(sEditor); for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) { int32_t edLine = sBreakpoints[i].codeLine - base; if (sBreakpoints[i].fileIdx == sEditorFileIdx && edLine >= 1) { wgtTextAreaSetLineMarker(sEditor, edLine, IDE_BP_GUTTER_RGB, TEXTAREA_MARKER_NONE); pausedHasBp = pausedHasBp || edLine == sDbgCurrentLine; } } if (sDbgState == DbgPausedE && sDbgCurrentLine > 0) { wgtTextAreaSetLineMarker(sEditor, sDbgCurrentLine, pausedHasBp ? IDE_BP_GUTTER_RGB : TEXTAREA_MARKER_NONE, IDE_DBG_LINE_RGB); } } // 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(); } // renameMenuItemsInCode -- after the Menu Editor, rename the handlers of // menu items whose name changed. Items are matched by the identity the // editor keeps (FrmMenuItemT.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++) { FrmMenuItemT *item = &sDesigner.form->menuItems[mi]; int32_t was = item->id - 1; if (item->id == FRM_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 or UserControl 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 || file->kind == PrjKindControlE) { 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 or UserControl 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, class module, or UserControl named %s.", newClass); ideErrorBox(err); return false; } if (!prjRenameFile(&sProject, fileIdx, newName, err, (int32_t)sizeof(err))) { ideErrorBox(err); return false; } if (nameClass && !setFileClassName(fileIdx, newClass)) { ideErrorBox("The file was renamed, but its class name could not be changed."); } if (nameClass && file->kind == PrjKindControlE) { designCtlRefresh(); } 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, IDE_MAX_SOURCE - 1); ideErrorBox(msg); } 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); } // The project's fonts added for owner (the IDE) while a program runs; one // that cannot be added is reported in the Output window static void runFontsAdd(int32_t owner) { for (int32_t i = 0; i < prjFontCount(&sProject); i++) { char path[DVX_MAX_PATH]; platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i)); if (!dvxFontAddFile(owner, path)) { char msg[DVX_MAX_PATH + IDE_MSG_BUF]; snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path); outputAppend(msg, (int32_t)strlen(msg)); } } } static void runModule(BasModuleT *mod) { // Never nest a run: a stopped program may still be unwinding if (sVm) { return; } if (prjIsControlProject(&sProject)) { setStatus(IDE_CTL_NO_RUN); return; } // The program's VM and form runtime. In the IDE, App.Path is the // project's folder, App.Data its DATA folder (what the program will // ship), and App.User the user folder the project's program gets // once installed beside it; the program's screen window carries // the project's name and F1 opens the project's help file. The IDE // reports runtime errors through the Output pane and jumps the editor // to the faulting line, so the form runtime's modal error box is // suppressed (its nested event pump raced with events still queued // by the program: "press OK twice"). char helpPath[DVX_MAX_PATH] = ""; const char *hostError = NULL; BasRunSpecT spec; if (sProject.helpFile[0]) { platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile); } memset(&spec, 0, sizeof(spec)); spec.appDir = sProject.projectDir; spec.appFile = sProject.projectPath; spec.args = sProject.commandArgs; spec.title = sProject.name; spec.helpFile = helpPath[0] ? helpPath : NULL; spec.printFn = printCallback; spec.inputFn = inputCallback; spec.doEventsFn = doEventsCallback; spec.suppressErrorDialog = true; BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError); if (!formRt) { setStatus(hostError); return; } BasVmT *vm = formRt->vm; 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); } basVmSetBreakpointCallback(vm, onBreakpointHit, NULL); sVm = vm; sDbgFormRt = formRt; sDbgModule = mod; sDbgState = DbgRunningE; sStopRequested = false; 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); } } // The project's fonts, for the program run here as for the built one // (added for the IDE until the run ends) int32_t fontOwner = sAc->currentAppId; runFontsAdd(fontOwner); // Load forms from project files (AFTER breakpoints are set so // init code breakpoints fire), then show the startup form. loadRunFiles(formRt); basRunHostLoadForms(formRt, sProject.startupForm); // Run in slices of 10000 steps, yielding to DVX between slices basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE); BasVmResultE result = runProgramSlices(formRt); // 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)..."); // An error message 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 && !sStopRequested && basRunHostAlive(formRt)) { 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(formRt); } // 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 = basVmGetError(vm)[0] != '\0' && sWin; int32_t errorLine = hadRuntimeError ? basVmGetErrorLine(vm) : 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, basVmGetError(vm)); } else if (errFile[0]) { snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, basVmGetError(vm)); } else { snprintf(report, sizeof(report), "\nRUNTIME ERROR:\nline %d: %s\n", (int)errLocalLine, basVmGetError(vm)); } 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; basRunHostDestroy(formRt); // The fonts the run added (the project's and any the program added): // the IDE carries none of its own dvxFontRemoveOwner(fontOwner); // 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 and line left in the VM, see basRunHostSlice) or is stopped. // Returns the last result. static BasVmResultE runProgramSlices(BasFormRtT *rt) { BasVmResultE result = BAS_VM_HALTED; for (;;) { if (!sWin || !sAc->running || sStopRequested) { break; } if (sDbgState == DbgPausedE) { // Paused at breakpoint/step -- spin on GUI events until the // user continues, steps or stops (debugResume resumes the VM) idePump(); continue; } result = basRunHostSlice(rt); // Module-level pause: the same state as a pause inside a handler if (result == BAS_VM_BREAKPOINT) { int32_t line = basVmGetCurrentLine(rt->vm); onBreakpointHit(NULL, line > 0 ? line : 0); continue; } if (result == BAS_VM_STEP_LIMIT) { idePump(); continue; } break; } 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; } // 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); } } // setFileClassName -- 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), or UserControl fileIdx the name on its Begin // UserControl line. 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 setFileClassName(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); char *text = NULL; if (file->kind == PrjKindControlE) { text = prjCtlHeaderSet(file->buffer, BAS_PROP_NAME, name); } else if (basClassNameAttr(file->buffer, len, &start, &oldLen)) { text = (char *)malloc((size_t)(len - oldLen + nameLen + 1)); if (text) { 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)); } } else { return true; } if (!text) { return false; } 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; wgtTextAreaSetSyntaxMode(sEditor, IDE_SYNTAX_MODE); // 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); 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; } // showProperties -- the Properties window (made when there is none) static void showProperties(void) { if (sPropsWin) { return; } sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { sPropsWin->y = toolbarBottom(); sPropsWin->onMenu = onMenu; sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; propsFollowActive(); } } // showToolbox -- the Toolbox window (made when there is none): every // control type a form can hold, the project's own UserControls included static void showToolbox(void) { if (sToolboxWin) { return; } sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom()); if (sToolboxWin) { sToolboxWin->onMenu = onMenu; sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; } } 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; } // 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 (basProcDeclAt(dvxSkipWs(pos), &isSub)) { int32_t endOffset = 0; const char *end = basProcSkipToEnd(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 == FRM_WRITE_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; } FrmModelT *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; // UserControls placed here see UserMode False (their code draws, the // input goes to the designer) from their Initialize on basDsgnSetDesignWindow(sFormWin, true); // 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); } dsgnFitFormWin(&sDesigner); sFormWin->onResize = onFormWinResize; // Create toolbox and properties windows showToolbox(); showProperties(); 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(); 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 (basProcDeclAt(p, &isSub) || kwMatch(p, BAS_PROC_KW_END_SUB) || kwMatch(p, BAS_PROC_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); } updateBreakpointWindow(); } static int32_t toolbarBottom(void) { return sWin ? sWin->y + sWin->h + IDE_TOOLBAR_GAP : IDE_TOOLBAR_FALLBACK_Y; } static void updateBreakpointWindow(void) { refreshEditorMarkers(); 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); int32_t rowCount = (sDbgState == DbgPausedE && sVm && sDbgModule) ? basDbgCallStack(sVm, sDbgCurrentLine, listRowsAddRow, &sCallRows) : 0; 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 arrsetlen(sWithEvents, 0); basSourceWithEvents(getFullSource(), addWithEvents, NULL); // 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 (editorIsControl()) { arrput(sObjItems, strdup(BAS_UC_OBJECT)); } else 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++) { FrmMenuItemT *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; } // Locals of the current proc, globals, and the current form's variables listRowsReset(&sLocalsRows); int32_t rowCount = (sDbgState == DbgPausedE && sVm && sDbgModule) ? basDbgLocals(sVm, listRowsAddRow, &sLocalsRows) : 0; 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_NEW_CTL, 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); // A control project makes a control file MenuItemT *makeItem = sFileMenu ? wmMenuFindItemInMenu(sFileMenu, CMD_MAKE_EXE) : NULL; if (makeItem) { wmMenuItemSetLabel(makeItem, prjIsControlProject(&sProject) ? IDE_MAKE_CTL_LABEL : IDE_MAKE_EXE_LABEL); } // 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 && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf))) { snprintf(valBuf, sizeof(valBuf), "%s", BAS_DBG_EVAL_ERROR); } listRowsAdd(&sWatchRows, sWatchExprs[i]); listRowsAdd(&sWatchRows, valBuf); } listRowsCommit(&sWatchRows, sWatchList, count); } static void watchEditSelected(void) { if (!sWatchList || !sWatchInput) { return; } int32_t sel = wgtListViewGetSelected(sWatchList); if (sel < 0 || sel >= (int32_t)arrlen(sWatchExprs)) { return; } // Put expression text into the input box wgtSetText(sWatchInput, sWatchExprs[sel]); // Remove from list free(sWatchExprs[sel]); arrdel(sWatchExprs, sel); updateWatchWindow(); // Focus the input box wgtSetFocused(sWatchInput); } // 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; } if (file->kind == PrjKindControlE) { designCtlRefresh(); } return true; } int32_t appMain(DxeAppContextT *ctx) { sCtx = ctx; sAc = ctx->shellCtx; // Set help file and context-sensitive F1 handler platformPathJoin(ctx->helpFile, sizeof(ctx->helpFile), ctx->appDir, IDE_HELP_FILE); snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile); ctx->onHelpQuery = helpQueryHandler; ctx->helpQueryCtx = NULL; prjInit(&sProject); buildWindow(); basUcSetPropChangedHook(onUcPropChanged); // Load persisted settings shellEnsureUserDir(sCtx); char prefsPath[DVX_MAX_PATH]; shellUserPath(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; }