DVX_GUI/src/apps/kpunch/dvxbasic/ide/ideMain.c
2026-10-05 20:01:04 -05:00

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