DVX_GUI/src/apps/kpunch/dvxbasic/ide/ideMain.c
2026-10-20 17:24:15 -05:00

9266 lines
290 KiB
C

// 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 "idePackage.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 <dlfcn.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// ============================================================
// Constants
// ============================================================
#define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file
#define IDE_MAX_OUTPUT 32768
#define IDE_RUN_TASK_NAME "DVX BASIC program" // the task a run's program runs on (runModule)
#define IDE_BUSY_PUMP_MS 50 // a compile, a debugger snippet, or a Replace All lets DVX go on this often (ideBusyPump)
#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_MAKE_PKG_LABEL "Make Pac&kage..." // File menu: the build as an add-on package
#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_MAKE_PKG 178
#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; runBody has not unwound yet
} IdeDebugStateE;
// shellAppTaskStart, found at run time (the host tests have no shell)
typedef __typeof__(shellAppTaskStart) *IdeTaskStartFnT;
// The run in progress (runStart, runBody): the IDE windows it hid, and
// the owner of the fonts it added
typedef struct {
bool hadFormWin;
bool hadToolbox;
bool hadProps;
bool hadCodeWin;
bool hadPrjWin;
int32_t fontOwner;
} IdeRunT;
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 ideBusyPump(void *ctx);
static void ideBusyPumpEnd(void);
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 const char *makeOutput(const char *outPath, bool release);
static void makePackage(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 runBody(void);
static void runCached(void);
static void runFontsAdd(int32_t owner);
static void runModule(BasModuleT *mod);
static BasVmResultE runProgramSlices(BasFormRtT *rt);
static bool runStart(BasModuleT *mod);
static void runTaskEntry(void *arg);
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;
static PlatformTicksT sBusyPumpAt = 0; // when ideBusyPump last let DVX go on
static bool sBusyPumpBusy = false; // ideBusyPump set the busy state (ideBusyPumpEnd clears it)
static const BasDbgPumpT sBusyPump = { ideBusyPump, NULL }; // the debugger's snippets pump through it
static IdeRunT sRun;
// 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);
wmAddMenuItem(fileMenu, IDE_MAKE_PKG_LABEL, CMD_MAKE_PKG);
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 = dvxStrAppendfCut(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 = dvxStrAppendfCut(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 <Type> <Name>` 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 = dvxStrAppendfCut(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 = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName);
}
n = dvxStrAppendfCut(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, ideBusyPump, NULL);
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 = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg);
} else {
sOutputLen = dvxStrAppendfCut(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. runBody's loops and
// doEventsCallback observe sStopRequested and unwind; until runBody 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, NULL, NULL);
}
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 -- 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) {
bool assigned = basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL, &sBusyPump);
ideBusyPumpEnd();
if (assigned) {
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_MAKE_PKG:
if (isIdle) {
makePackage();
}
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));
}
// ideBusyPump -- basBuildCompile's unit callback, the debugger's snippet
// pump (sBusyPump), and Replace All's between procedures: a frame every
// IDE_BUSY_PUMP_MS, so DVX goes on through a long compile, snippet, or
// replace. The busy state keeps the user's input out meanwhile; set here
// when the caller had not (ideBusyPumpEnd clears it).
static void ideBusyPump(void *ctx) {
(void)ctx;
if (platformClock() - sBusyPumpAt < PLATFORM_MS_TO_TICKS(IDE_BUSY_PUMP_MS)) {
return;
}
if (!sAc->busy) {
dvxSetBusy(sAc, true);
sBusyPumpBusy = true;
}
dvxUpdate(sAc);
sBusyPumpAt = platformClock();
}
// ideBusyPumpEnd -- after a step ideBusyPump may have pumped: the busy
// state it set goes.
static void ideBusyPumpEnd(void) {
if (sBusyPumpBusy) {
dvxSetBusy(sAc, false);
}
sBusyPumpBusy = false;
}
// 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 (and the events waiting for it run)
static void idePump(void) {
flushOutput();
if (sDbgFormRt) {
basFormRtUpdate(sDbgFormRt);
} else {
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)
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..."));
const char *failure = makeOutput(outPath, release);
if (failure) {
setStatus(failure);
return;
}
char msg[IDE_MSG_BUF];
snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug");
setStatus(msg);
}
// makeOutput -- build the compiled project (compileProject first) at
// outPath: the program, or a control project's control file, through the
// build code bascomp uses. NULL when built, else why not (IdeBuildFnT).
static const char *makeOutput(const char *outPath, bool release) {
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 = prjIsControlProject(&sProject) ? basBuildControl(outPath, &spec) : basBuildApp(outPath, &spec);
freeTexts(frmSources);
freeTexts(ctlSources);
dvxSetBusy(sAc, false);
return failure;
}
// makePackage -- File > Make Package: the release build and its help as
// an add-on package (idePackage.c)
static void makePackage(void) {
if (!hasProject()) {
setStatus("Save the project first.");
return;
}
if (!compileProject()) {
return;
}
const char *result = idePackageMake(sAc, &sProject, makeOutput, setStatus);
if (result) {
setStatus(result);
}
}
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 runBody 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 (on its task, or
// this close arrived through its event pump); runBody 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);
}
}
// A long replace let DVX go on (replaceInSections pumps per procedure)
ideBusyPumpEnd();
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);
ideBusyPump(NULL);
}
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);
}
// runBody -- the rest of a run (runModule), on the task the program runs
// on: its forms loaded and shown, its code run, then its event loop until
// it ends; the error it ended on reported, the IDE's windows restored.
static void runBody(void) {
BasFormRtT *formRt = sDbgFormRt;
BasVmT *vm = sVm;
// 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(sRun.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 (sRun.hadFormWin && sFormWin) {
dvxShowWindow(sAc, sFormWin);
}
if (sRun.hadToolbox && sToolboxWin) {
dvxShowWindow(sAc, sToolboxWin);
}
if (sRun.hadProps && sPropsWin) {
dvxShowWindow(sAc, sPropsWin);
}
if (sRun.hadCodeWin && sCodeWin) {
dvxShowWindow(sAc, sCodeWin);
}
if (sRun.hadPrjWin && sProjectWin) {
dvxShowWindow(sAc, sProjectWin);
}
}
// Repaint to clear destroyed runtime forms and restore designer
dvxUpdate(sAc);
}
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));
}
}
}
// runModule -- run mod: set up here (runStart), then run (runBody) on a
// task of the IDE's own (shellAppTaskStart), so the shell's main loop
// goes on while the program does -- programs that end are reaped, Exit
// and DOS requests carried out, the screen saver started. Without one
// (the host tests have no shell) the program runs here, to its end.
static void runModule(BasModuleT *mod) {
if (!runStart(mod)) {
return;
}
IdeTaskStartFnT taskStart = (IdeTaskStartFnT)dlsym(NULL, DVX_SYM("shellAppTaskStart"));
if (!taskStart || taskStart(sAc, IDE_RUN_TASK_NAME, BAS_RUN_STACK_SIZE, runTaskEntry, NULL) < 0) {
runBody();
}
}
// 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;
}
// runStart -- the first part of a run (runModule): the program's runtime
// made, the IDE's windows hidden, breakpoints set (and stepping when the
// run breaks on start), the project's fonts added. False when it cannot
// start.
static bool runStart(BasModuleT *mod) {
// Never nest a run: a stopped program may still be unwinding
if (sVm) {
return false;
}
if (prjIsControlProject(&sProject)) {
setStatus(IDE_CTL_NO_RUN);
return false;
}
// 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 false;
}
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.
sRun.hadFormWin = sFormWin && sFormWin->visible;
sRun.hadToolbox = sToolboxWin && sToolboxWin->visible;
sRun.hadProps = sPropsWin && sPropsWin->visible;
sRun.hadCodeWin = sCodeWin && sCodeWin->visible;
sRun.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)
sRun.fontOwner = sAc->currentAppId;
runFontsAdd(sRun.fontOwner);
return true;
}
// runTaskEntry -- runModule's task: the program's runtime moves to it,
// then the run goes on (runBody).
static void runTaskEntry(void *arg) {
(void)arg;
basFormRtAdopt(sDbgFormRt);
runBody();
}
// 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);
wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_PKG, 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), &sBusyPump)) {
snprintf(valBuf, sizeof(valBuf), "%s", BAS_DBG_EVAL_ERROR);
}
listRowsAdd(&sWatchRows, sWatchExprs[i]);
listRowsAdd(&sWatchRows, valBuf);
}
ideBusyPumpEnd();
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, all or nothing (platformWriteFile), 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];
prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
if (!platformWriteFile(fullPath, text, (int32_t)strlen(text))) {
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), &quoted)) {
openAnyPath(arg);
}
return 0;
}