9266 lines
290 KiB
C
9266 lines
290 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
// ideMain.c -- DVX BASIC IDE application
|
|
//
|
|
// A DVX app that edits, compiles, runs and debugs BASIC projects.
|
|
// PRINT output goes to a scrollable TextArea widget. Compile and
|
|
// runtime errors are displayed with file/procedure line numbers.
|
|
|
|
#include "dvxApp.h"
|
|
#include "dvxCur.h"
|
|
#include "dvxPlat.h"
|
|
#include "dvxDlg.h"
|
|
#include "dvxPrefs.h"
|
|
#include "dvxText.h"
|
|
#include "dvxWgt.h"
|
|
#include "dvxWgtP.h"
|
|
#include "dvxWm.h"
|
|
#include "shellApp.h"
|
|
#include "box/box.h"
|
|
#include "checkbox/checkbox.h"
|
|
#include "imageButton/imgBtn.h"
|
|
#include "label/label.h"
|
|
#include "radio/radio.h"
|
|
#include "textInput/textInpt.h"
|
|
#include "dropdown/dropdown.h"
|
|
#include "canvas/canvas.h"
|
|
#include "listBox/listBox.h"
|
|
#include "tabControl/tabCtrl.h"
|
|
#include "button/button.h"
|
|
#include "splitter/splitter.h"
|
|
#include "statusBar/statBar.h"
|
|
#include "listView/listView.h"
|
|
#include "separator/separatr.h"
|
|
#include "toolbar/toolbar.h"
|
|
|
|
#include "ideDesigner.h"
|
|
#include "ideProject.h"
|
|
#include "ideMenuEditor.h"
|
|
#include "ideToolbox.h"
|
|
#include "ideProperties.h"
|
|
#include "idePackage.h"
|
|
|
|
#include "../basBuild.h"
|
|
#include "../basdev/basDebug.h"
|
|
#include "../basdev/basHelpMap.h"
|
|
#include "../basdev/basProcs.h"
|
|
#include "../basdev/basSource.h"
|
|
#include "../basdev/basValidate.h"
|
|
#include "../basRes.h"
|
|
#include "../compiler/basEvents.h"
|
|
#include "../compiler/compiler.h"
|
|
#include "../formrt/basRunHost.h"
|
|
#include "../formrt/formrt.h"
|
|
#include "../formrt/frmParser.h"
|
|
#include "dvxRes.h"
|
|
#include "../../../../libs/kpunch/texthelp/textHelp.h"
|
|
#include "../runtime/vm.h"
|
|
#include "../runtime/values.h"
|
|
|
|
#include "stb_ds_wrap.h"
|
|
|
|
#include <dlfcn.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
// ============================================================
|
|
// Constants
|
|
// ============================================================
|
|
|
|
#define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file
|
|
#define IDE_MAX_OUTPUT 32768
|
|
#define IDE_RUN_TASK_NAME "DVX BASIC program" // the task a run's program runs on (runModule)
|
|
#define IDE_BUSY_PUMP_MS 50 // a compile, a debugger snippet, or a Replace All lets DVX go on this often (ideBusyPump)
|
|
#define IDE_SOURCE_MARGIN 64
|
|
#define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names
|
|
#define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *"
|
|
#define IDE_FORM_TITLE_BUF (FRM_MAX_TEXT + 32) // "caption [Design] *"
|
|
#define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message
|
|
#define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report
|
|
#define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys
|
|
#define IDE_IMM_LINE_BUF 512 // one Immediate-window line
|
|
#define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton
|
|
#define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text
|
|
#define IDE_RENAME_TITLE "Rename File"
|
|
#define IDE_VALUE_BUF 256 // formatted variable value / watch text
|
|
#define IDE_FIND_TEXT_BUF 256 // find / replace text
|
|
#define IDE_LINE_NUM_BUF 16 // decimal line number text
|
|
#define IDE_RGB_LABEL_BUF 16 // "Green 255"
|
|
#define IDE_TAB_WIDTH_BUF 8 // tab width text input
|
|
#define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line
|
|
#define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log
|
|
|
|
#define IDE_MAIN_TITLE "DVX BASIC"
|
|
#define IDE_GENERAL_SECTION "(General)"
|
|
#define IDE_GLOBAL_SECTION "(Global)"
|
|
#define IDE_DEFAULT_FORM "Form1"
|
|
#define IDE_HELP_FILE "dvxbasic.hlp"
|
|
#define IDE_PREFS_FILE "dvxbasic.ini"
|
|
#define IDE_FORM_TYPE_NAME FRM_KW_FORM // designer type name of a form
|
|
#define IDE_MENU_EVENT BAS_EVT_CLICK // the only event a menu item fires
|
|
#define IDE_FALLBACK_EVENT BAS_EVT_CLICK // default event when a type names none
|
|
#define IDE_RENAME_TEMP_FMT "IDE_MNU_REN%d" // placeholder during menu renames
|
|
#define IDE_NEW_CLASS_FMT "Class%d" // name of a new class module
|
|
#define IDE_NEW_CTL_FMT "UserControl%d" // name of a new UserControl
|
|
#define IDE_MAKE_EXE_LABEL "&Make Executable..." // File menu: a program project's build
|
|
#define IDE_MAKE_CTL_LABEL "&Make Control..." // File menu: a control project's build
|
|
#define IDE_MAKE_PKG_LABEL "Make Pac&kage..." // File menu: the build as an add-on package
|
|
#define IDE_UC_DEFAULT_EVENT BAS_UC_EVT_INIT // the UserControl object's event when none has code yet
|
|
#define IDE_CTL_NO_RUN "A control project is not run: make its control file (File > Make Control), or add its .ctl files to a program project."
|
|
// Text of a new class module (%s = its name)
|
|
#define IDE_NEW_CLASS_TEXT BAS_CLS_KW_VERSION " DVX 1.00 CLASS\n" BAS_CLS_KW_ATTRIBUTE " " BAS_CLS_ATTR_NAME " = \"%s\"\n\nOption Explicit\n"
|
|
|
|
// Keyword tags for procedure scanning (case-insensitive prefix match)
|
|
#define KW_REM "REM"
|
|
#define FRM_BEGIN_TAG "Begin "
|
|
#define KW_LEN(s) ((int32_t)sizeof(s) - 1)
|
|
|
|
// Preference sections/keys used from more than one place
|
|
#define IDE_PREF_KEY_TAB_WIDTH "tabWidth"
|
|
#define IDE_PREF_KEY_USE_SPACES "useSpaces"
|
|
#define IDE_PREF_KEY_RENAME_SKIP "renameSkipComments"
|
|
#define PREF_SEC_SYNTAX "syntax"
|
|
#define PREF_KEY_COLOR_FMT "color%d"
|
|
#define PREF_SEC_RUN "run"
|
|
#define PREF_KEY_SAVE_ON_RUN "saveOnRun"
|
|
#define PREF_KEY_OUTPUT_TO_LOG "outputToLog"
|
|
#define PREF_SEC_VIEW "view"
|
|
#define PREF_KEY_TOOLBAR "toolbar"
|
|
#define PREF_KEY_STATUSBAR "statusbar"
|
|
#define PREF_SEC_RECENT "recent"
|
|
#define PREF_KEY_RECENT_FMT "file%ld"
|
|
#define IDE_DEFAULT_TAB_WIDTH 3
|
|
#define IDE_MIN_TAB_WIDTH 1
|
|
#define IDE_MAX_TAB_WIDTH 8
|
|
|
|
// Code editor: the built-in colorizer, and the line markers (0xRRGGBB)
|
|
// of a breakpoint (gutter dot) and the paused line (background)
|
|
#define IDE_SYNTAX_MODE "bas"
|
|
#define IDE_BP_GUTTER_RGB 0xC80000
|
|
#define IDE_DBG_LINE_RGB 0xFFFF80
|
|
|
|
// Window geometry
|
|
#define IDE_WIN_MARGIN 10 // gap from screen edge
|
|
#define IDE_TOOLBAR_GAP 2 // gap below the toolbar window
|
|
#define IDE_TOOLBAR_FALLBACK_Y 60 // toolbar bottom when no toolbar window
|
|
#define IDE_TOOLBAR_SEP_W 10 // toolbar group separator width
|
|
#define IDE_TOOLBAR_INIT_H 200 // toolbar window height before fit
|
|
#define IDE_PROJECT_WIN_OFFSET 25 // project window y below toolbar bottom
|
|
#define IDE_BP_WIN_W 320
|
|
#define IDE_DEBUG_WIN_H 180 // breakpoints / call stack / watch height
|
|
#define IDE_CALLSTACK_WIN_W 220
|
|
#define IDE_CALLSTACK_WIN_Y_OFF 210 // below toolbar bottom
|
|
#define IDE_LOCALS_WIN_W 250
|
|
#define IDE_LOCALS_WIN_H 200
|
|
#define IDE_WATCH_WIN_W 280
|
|
#define IDE_WATCH_WIN_X_GAP 260 // watch window sits left of locals
|
|
#define IDE_FIND_WIN_W 320
|
|
#define IDE_FIND_WIN_H 210
|
|
#define IDE_FIND_ROOT_SPACING 3
|
|
#define IDE_PREFS_WIN_W 420
|
|
#define IDE_PREFS_WIN_H 440
|
|
#define IDE_PREFS_LABEL_W 80
|
|
#define IDE_PREFS_TAB_INPUT_W 40
|
|
#define IDE_PREFS_TAB_INPUT_MAX 4
|
|
#define IDE_PREFS_TEXT_MAX 64
|
|
#define IDE_PREFS_VERSION_MAX 16
|
|
#define IDE_PREFS_BUTTON_W 60
|
|
#define IDE_SWATCH_W 64
|
|
#define IDE_SWATCH_H 24
|
|
#define IDE_WIDGET_SPACING 4
|
|
#define IDE_WIDGET_SPACING_SM 2
|
|
#define IDE_WIDGET_SPACING_LG 8
|
|
|
|
// Debug ListView column widths (characters)
|
|
#define IDE_COL_W_FILE 12
|
|
#define IDE_COL_W_PROC 16
|
|
#define IDE_COL_W_LINE 6
|
|
#define IDE_COL_W_NAME 12
|
|
#define IDE_COL_W_TYPE 8
|
|
#define IDE_COL_W_VALUE 16
|
|
#define IDE_COL_W_EXPR 14
|
|
#define IDE_COL_W_WATCH_VALUE 20
|
|
|
|
// Menu command IDs
|
|
#define CMD_OPEN 100
|
|
#define CMD_RUN 101
|
|
#define CMD_STOP 102
|
|
#define CMD_CLEAR 103
|
|
#define CMD_EXIT 104
|
|
#define CMD_RUN_NOCMP 105
|
|
#define CMD_VIEW_CODE 106
|
|
#define CMD_VIEW_DESIGN 107
|
|
#define CMD_SAVE 108
|
|
#define CMD_WIN_CODE 109
|
|
#define CMD_WIN_OUTPUT 110
|
|
#define CMD_WIN_IMM 111
|
|
#define CMD_WIN_TOOLBOX 112
|
|
#define CMD_WIN_PROPS 113
|
|
#define CMD_DELETE 114
|
|
#define CMD_CUT 115
|
|
#define CMD_COPY 116
|
|
#define CMD_PASTE 117
|
|
#define CMD_SELECT_ALL 118
|
|
#define CMD_VIEW_TOOLBAR 119
|
|
#define CMD_VIEW_STATUS 120
|
|
#define CMD_SAVE_ALL 129
|
|
#define CMD_SAVE_ON_RUN 139
|
|
#define CMD_PRJ_NEW 130
|
|
#define CMD_PRJ_OPEN 131
|
|
#define CMD_PRJ_SAVE 132
|
|
#define CMD_PRJ_CLOSE 133
|
|
#define CMD_PRJ_NEW_CLASS 134
|
|
#define CMD_PRJ_RENAME 135
|
|
#define CMD_PRJ_REMOVE 136
|
|
#define CMD_PRJ_PROPS 138
|
|
#define CMD_WIN_PROJECT 137
|
|
#define CMD_HELP_ABOUT 140
|
|
#define CMD_FIND 141
|
|
#define CMD_REPLACE 142
|
|
#define CMD_FIND_NEXT 143
|
|
#define CMD_MENU_EDITOR 144
|
|
#define CMD_PREFERENCES 145
|
|
#define CMD_STEP_INTO 146
|
|
#define CMD_STEP_OVER 147
|
|
#define CMD_STEP_OUT 148
|
|
#define CMD_TOGGLE_BP 149
|
|
#define CMD_RUN_TO_CURSOR 150
|
|
#define CMD_WIN_LOCALS 151
|
|
#define CMD_DEBUG 152
|
|
#define CMD_WIN_CALLSTACK 153
|
|
#define CMD_WIN_WATCH 154
|
|
#define CMD_WIN_BREAKPOINTS 155
|
|
#define CMD_DEBUG_LAYOUT 156
|
|
#define CMD_OUTPUT_TO_LOG 159
|
|
#define CMD_RECENT_BASE 160 // 160-175 reserved for recent files
|
|
#define CMD_RECENT_MAX 16 // Keep cap: menu command-ID range is a fixed slice
|
|
#define CMD_MAKE_EXE 176
|
|
#define CMD_PRJ_NEW_CTL 177
|
|
#define CMD_MAKE_PKG 178
|
|
#define CMD_HELP_CONTENTS 157
|
|
#define CMD_HELP_API 158
|
|
#define IDE_MAX_IMM 1024
|
|
#define IDE_IMM_HEADROOM IDE_IMM_LINE_BUF // Immediate space output never takes, left for typing
|
|
#define IDE_SECTION_GENERAL (-1) // section index of (General) (sCurProcIdx, find, navigation)
|
|
#define IDE_SECTION_NONE (-2) // editor bound to no section / scan every section / not activated
|
|
#define IDE_SECTION_GAP 1 // blank lines between sections in the canonical layout
|
|
#define IDE_NO_LINE (-1) // no line (dropped anchor, unmapped line)
|
|
#define IDE_CARET_LOAD 0 // editorSetText: leave the caret where loading puts it
|
|
#define IDE_CARET_KEEP (-1) // editorSetText: keep the caret on its current line number
|
|
#define IDE_OUTPUT_WIN_H 120 // Output/Immediate window height
|
|
#define IDE_OUTPUT_WIN_GAP 2 // gap between code window and output strip
|
|
|
|
// View mode for activateFile
|
|
typedef enum {
|
|
ViewAutoE, // .frm -> design, .bas -> code
|
|
ViewCodeE, // force code view
|
|
ViewDesignE // force design view
|
|
} IdeViewModeE;
|
|
|
|
// Find scope: also the radio index in the Find dialog's Scope group
|
|
typedef enum {
|
|
ScopeFuncE,
|
|
ScopeObjE,
|
|
ScopeFileE,
|
|
ScopeProjE,
|
|
ScopeCountE
|
|
} FindScopeE;
|
|
|
|
// Find direction: also the radio index in the Direction group
|
|
typedef enum {
|
|
FindDirFwdE,
|
|
FindDirBackE,
|
|
FindDirCountE
|
|
} FindDirE;
|
|
|
|
// Debug state
|
|
typedef enum {
|
|
DbgIdleE, // no program loaded
|
|
DbgRunningE, // program executing
|
|
DbgPausedE, // stopped at breakpoint/step
|
|
DbgStoppingE // Stop requested; runBody has not unwound yet
|
|
} IdeDebugStateE;
|
|
|
|
// shellAppTaskStart, found at run time (the host tests have no shell)
|
|
typedef __typeof__(shellAppTaskStart) *IdeTaskStartFnT;
|
|
|
|
// The run in progress (runStart, runBody): the IDE windows it hid, and
|
|
// the owner of the fonts it added
|
|
typedef struct {
|
|
bool hadFormWin;
|
|
bool hadToolbox;
|
|
bool hadProps;
|
|
bool hadCodeWin;
|
|
bool hadPrjWin;
|
|
int32_t fontOwner;
|
|
} IdeRunT;
|
|
|
|
typedef struct {
|
|
int32_t fileIdx; // project file index
|
|
int32_t codeLine; // line within file's code section (1-based)
|
|
char procName[IDE_FULL_NAME_BUF]; // "obj.evt" combined name
|
|
} IdeBreakpointT;
|
|
|
|
// Procedure table for Object/Event dropdowns
|
|
typedef struct {
|
|
char objName[BAS_MAX_IDENT];
|
|
char evtName[BAS_MAX_IDENT];
|
|
int32_t lineNum; // line of the SUB / FUNCTION declaration
|
|
int32_t endLineNum; // line of the END SUB / END FUNCTION
|
|
} IdeProcEntryT;
|
|
|
|
// A WithEvents variable declared in the file in the code window: an
|
|
// entry of the Object dropdown whose events are its class's
|
|
typedef struct {
|
|
char name[BAS_MAX_IDENT];
|
|
char className[BAS_MAX_IDENT];
|
|
} IdeWithEventsT;
|
|
|
|
// An Event a class module declares: its name and parameter list text
|
|
// (the handler skeleton takes the same parameters)
|
|
typedef struct {
|
|
char name[BAS_MAX_IDENT];
|
|
char params[IDE_EVENT_PARAMS_BUF];
|
|
} IdeClassEventT;
|
|
|
|
// Where one line of split source went (see splitProcs)
|
|
typedef struct {
|
|
int32_t piece; // IDE_SECTION_GENERAL or an index into the proc array
|
|
int32_t line; // 0-based line within that piece
|
|
} IdeLineDestT;
|
|
|
|
// A line pinned to its section while saveCurProc rearranges the buffers:
|
|
// a line of the edited text (edLine), or a line of a stored section.
|
|
typedef struct {
|
|
int32_t edLine; // 0-based line of the edited text, or IDE_NO_LINE
|
|
const char *section; // owning proc buffer; NULL = (General)
|
|
int32_t offset; // 0-based line within section, or IDE_NO_LINE (gone)
|
|
} IdeLineAnchorT;
|
|
|
|
// Row storage for the debug ListViews. Cell text is strdup'd row-major
|
|
// into strs; cells holds the same pointers in the layout
|
|
// wgtListViewSetData expects.
|
|
typedef struct {
|
|
char **strs; // stb_ds: owned cell text
|
|
const char **cells; // stb_ds: borrowed pointers into strs
|
|
} IdeListRowsT;
|
|
|
|
// How concatProject ended
|
|
typedef enum {
|
|
ConcatOkE,
|
|
ConcatUnreadableE, // a file cannot be read (missing or too large)
|
|
ConcatTooLargeE, // the source outgrows the editor's limit
|
|
ConcatFailedE // the build refused a file (ps->error says why)
|
|
} IdeConcatE;
|
|
|
|
// One .frm parse during compile: the validator map plus the code section
|
|
typedef struct {
|
|
BasValidatorT *validator; // NULL = do not collect controls
|
|
char *code; // malloc'd code section (NULL = none)
|
|
} IdeFrmScanT;
|
|
|
|
// Preferences dialog state (used by several onPrefs* handlers)
|
|
typedef struct {
|
|
WindowT *win; // non-NULL while the dialog is open
|
|
bool done;
|
|
bool accepted;
|
|
// General tab
|
|
WidgetT *renameSkipComments;
|
|
WidgetT *optionExplicit;
|
|
WidgetT *tabWidthInput;
|
|
WidgetT *useSpaces;
|
|
WidgetT *defAuthor;
|
|
WidgetT *defPublisher;
|
|
WidgetT *defVersion;
|
|
WidgetT *defCopyright;
|
|
WidgetT *defDescription;
|
|
// Colors tab
|
|
WidgetT *colorList;
|
|
WidgetT *lblR;
|
|
WidgetT *lblG;
|
|
WidgetT *lblB;
|
|
WidgetT *colorSwatch;
|
|
uint32_t syntaxColors[TEXT_SYNTAX_MAX];
|
|
} IdePrefsDlgT;
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static bool activateFailed(void);
|
|
static bool activateFile(int32_t fileIdx, IdeViewModeE view);
|
|
static bool activeIsForm(void);
|
|
static void addClassEvent(void *ctx, const char *name, const char *params);
|
|
static bool addFileToProject(const char *path);
|
|
static void addWithEvents(void *ctx, const char *name, const char *className);
|
|
static void anchorCodeLine(IdeLineAnchorT *a, int32_t codeLine, int32_t cur, int32_t edLines, int32_t delta, const int32_t *starts);
|
|
static void anchorEditedLine(IdeLineAnchorT *a, const IdeLineDestT *map, char **procs, int32_t genBase);
|
|
static int32_t anchorLine(const IdeLineAnchorT *a, const int32_t *starts);
|
|
static bool anyFileModified(void);
|
|
static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, const char *ctrlList, BasSourceMapT *entry);
|
|
static void applySyntaxColors(void);
|
|
static void buildVmBreakpoints(void);
|
|
static void buildWindow(void);
|
|
static void classEventsOf(const char *className, IdeClassEventT **events);
|
|
static bool classFollowsFile(int32_t fileIdx, const char *text);
|
|
static bool classNameUsed(const char *name, const char *fileName);
|
|
static void cleanupFormWin(void);
|
|
static void clearAllBreakpoints(void);
|
|
static void clearOutput(void);
|
|
static void closeFindDialog(void);
|
|
static void closeProject(void);
|
|
static void closeProjectUi(void);
|
|
static int cmpEvtPtrs(const void *a, const void *b);
|
|
static int cmpStrPtrs(const void *a, const void *b);
|
|
static void compileAndRun(void);
|
|
static bool compileProject(void);
|
|
static IdeConcatE concatProject(BasProjectSourceT *ps, BasValidatorT *validator, bool withForms, BasSourceMapT **map, int32_t *outFileIdx);
|
|
static int32_t countLines(const char *text);
|
|
static void createEventSubSkeleton(const char *objName, const char *evtName);
|
|
static char **ctlHeaders(void);
|
|
static void currentObjName(char *out, int32_t outSize);
|
|
static void debugNavigateToLine(int32_t concatLine);
|
|
static void debugResume(const char *status);
|
|
static void debugStartOrResume(int32_t cmd);
|
|
static void debugStop(void);
|
|
static void debugUpdateWindows(void);
|
|
static bool dedupeBreakpoints(void);
|
|
static void deleteSelectedControl(void);
|
|
static void designCtlRefresh(void);
|
|
static void designCtlRelease(void);
|
|
static void designerEditCmd(int32_t cmd);
|
|
static void designerRelease(void);
|
|
static bool doEventsCallback(void *ctx);
|
|
static void dsgnCopySelected(void);
|
|
static void dsgnPasteControl(void);
|
|
static int32_t editCmdKey(int32_t cmd);
|
|
static bool editorIsControl(void);
|
|
static int32_t editorLineToCodeLine(int32_t editorLine);
|
|
static void editorSetText(const char *text, int32_t caretLine);
|
|
static void editorSnapshot(int32_t lineCount);
|
|
static void ensureProject(const char *filePath);
|
|
static void ensureProjectWindow(bool raise);
|
|
static void evaluateImmediate(const char *expr);
|
|
static void evtItemAdd(const char *name, bool bracketed);
|
|
static void evtItemsReset(void);
|
|
static bool evtLabelMatches(const char *label, const char *evtName);
|
|
static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needleLen, bool caseSensitive);
|
|
static bool findDialogBuild(bool showReplace);
|
|
static bool findInEditorSections(int32_t fromIdx, const char *needle, bool caseSensitive, bool forward, const char *objFilter);
|
|
static bool findInProject(const char *needle, bool caseSensitive, bool forward, FindScopeE scope);
|
|
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen);
|
|
static const IdeWithEventsT *findWithEvents(const char *name);
|
|
static void flushOutput(void);
|
|
static void freeProcBufs(void);
|
|
static void freeTexts(char **texts);
|
|
static void frmScan(const char *frmText, IdeFrmScanT *scan);
|
|
static void frmScanOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen);
|
|
static char *generalAppend(const char *general, const char *text);
|
|
static bool getFindForward(void);
|
|
static bool getFindMatchCase(void);
|
|
static FindScopeE getFindScope(void);
|
|
static const char *getFullSource(void);
|
|
static WindowT *getLastFocusWin(void);
|
|
static void handleEditCmd(int32_t cmd);
|
|
static void handleFileCmd(int32_t cmd);
|
|
static void handleProjectCmd(int32_t cmd);
|
|
static void handleRunCmd(int32_t cmd);
|
|
static void handleViewCmd(int32_t cmd);
|
|
static void handleWindowCmd(int32_t cmd);
|
|
static bool hasProject(void);
|
|
static bool hasUnsavedData(void);
|
|
static void helpQueryHandler(void *ctx);
|
|
static void helpSetCtrlTopic(const char *typeName);
|
|
static void ideBusyPump(void *ctx);
|
|
static void ideBusyPumpEnd(void);
|
|
static void ideErrorBox(const char *msg);
|
|
static void idePump(void);
|
|
static void immPrintCallback(void *ctx, const char *text, bool newline);
|
|
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize);
|
|
static void invalidateCompiled(void);
|
|
static bool isCtrlArrayInDesigner(const char *ctrlName);
|
|
static bool isRemAt(const char *line, int32_t pos, int32_t lineLen);
|
|
static bool isReplaceEnabled(void);
|
|
static char *joinProcArrays(const char *general, char **procs);
|
|
static const char *joinProcBufs(void);
|
|
static bool kwMatch(const char *p, const char *kw);
|
|
static int32_t lineCopy(const char *text, int32_t line, char *out, int32_t outSize);
|
|
static int32_t lineStartOffset(const char *text, int32_t line);
|
|
static void listRowsAdd(IdeListRowsT *rows, const char *text);
|
|
static void listRowsAddRow(void *ctx, const char *const *cells, int32_t cellCount);
|
|
static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount);
|
|
static void listRowsFree(IdeListRowsT *rows);
|
|
static void listRowsReset(IdeListRowsT *rows);
|
|
static char *loadCompileSource(int32_t fileIdx, BasValidatorT *validator, char **outCtrlList);
|
|
static bool loadDesignerFile(int32_t fileIdx);
|
|
static void loadFile(void);
|
|
static void loadFormCodeIntoEditor(void);
|
|
static void loadModuleIntoEditor(int32_t fileIdx, const char *source);
|
|
static void loadRunFiles(BasFormRtT *rt);
|
|
static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText);
|
|
static int32_t localToConcatLine(int32_t editorLine);
|
|
static void logFlushLine(void);
|
|
static void makeExecutable(void);
|
|
static const char *makeOutput(const char *outPath, bool release);
|
|
static void makePackage(void);
|
|
static void navigateToBreakpoint(int32_t bpIdx);
|
|
static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbgLine);
|
|
static void navigateToEventSub(void);
|
|
static void navigateToNamedEventSub(const char *ctrlName, const char *eventName);
|
|
static void newClassFile(PrjFileKindE kind);
|
|
static void newProject(void);
|
|
static const char *objectType(const char *objName);
|
|
static void onBreakpointHit(void *ctx, int32_t line);
|
|
static void onBreakpointListDblClick(WidgetT *w);
|
|
static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
|
|
static void onBreakpointWinClose(WindowT *win);
|
|
static void onCallStackClose(WindowT *win);
|
|
static void onClose(WindowT *win);
|
|
static void onCodeWinClose(WindowT *win);
|
|
static void onColorChange(WidgetT *w);
|
|
static void onColorListChange(WidgetT *w);
|
|
static void onContentFocus(WindowT *win);
|
|
static void onCtlHeaderSet(const char *key, const char *value);
|
|
static void onEditorChange(WidgetT *w);
|
|
static void onEvtDropdownChange(WidgetT *w);
|
|
static void onFindClose(WindowT *win);
|
|
static void onFindCloseBtn(WidgetT *w);
|
|
static void onFindNext(WidgetT *w);
|
|
static void onFormWinClose(WindowT *win);
|
|
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y);
|
|
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod);
|
|
static void onFormWinMenu(WindowT *win, int32_t menuId);
|
|
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
|
|
static void onFormWinPaint(WindowT *win, RectT *dirtyArea);
|
|
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH);
|
|
static void onGutterClick(WidgetT *w, int32_t lineNum);
|
|
static void onImmediateKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
|
|
static void onImmWinClose(WindowT *win);
|
|
static void onLocalsClose(WindowT *win);
|
|
static void onMenu(WindowT *win, int32_t menuId);
|
|
static void onObjDropdownChange(WidgetT *w);
|
|
static void onOutWinClose(WindowT *win);
|
|
static void onPrefsCancel(WidgetT *w);
|
|
static void onPrefsClose(WindowT *win);
|
|
static void onPrefsOk(WidgetT *w);
|
|
static void onPrjFileDblClick(int32_t fileIdx);
|
|
static void onProjectWinClose(WindowT *win);
|
|
static void onReplace(WidgetT *w);
|
|
static void onReplaceAll(WidgetT *w);
|
|
static void onReplCheckChange(WidgetT *w);
|
|
static void onTbCode(WidgetT *w);
|
|
static void onTbDebug(WidgetT *w);
|
|
static void onTbDesign(WidgetT *w);
|
|
static void onTbOpen(WidgetT *w);
|
|
static void onTbRun(WidgetT *w);
|
|
static void onTbRunToCur(WidgetT *w);
|
|
static void onTbSave(WidgetT *w);
|
|
static void onTbStepInto(WidgetT *w);
|
|
static void onTbStepOut(WidgetT *w);
|
|
static void onTbStepOver(WidgetT *w);
|
|
static void onTbStop(WidgetT *w);
|
|
static void onUcPropChanged(WidgetT *w, const char *propName);
|
|
static void onWatchClose(WindowT *win);
|
|
static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
|
|
static void onWatchListDblClick(WidgetT *w);
|
|
static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
|
|
static void openAnyPath(const char *path);
|
|
static void openFindDialog(bool showReplace);
|
|
static void openProject(void);
|
|
static void openProjectPath(const char *path);
|
|
static void openSinglePath(const char *path);
|
|
static void outputAppend(const char *text, int32_t len);
|
|
static void outputLog(const char *text);
|
|
static char *packProcLayout(const char *source);
|
|
static void parseProcs(const char *source);
|
|
static bool prefsBuild(WindowT *win);
|
|
static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value);
|
|
static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLen, const char *value);
|
|
static void prefsUpdateColorSwatch(void);
|
|
static void printCallback(void *ctx, const char *text, bool newline);
|
|
static bool procBodyEmpty(const char *procText);
|
|
static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive);
|
|
static int32_t procIndexByName(const char *objName, const char *evtName);
|
|
static int32_t procIndexForCodeLine(int32_t codeLine);
|
|
static void procNameOf(const char *procText, char *out, int32_t outSize);
|
|
static int32_t *procStartLines(const char *general, char **procs);
|
|
static const char *projectFileText(int32_t fileIdx, char **outOwned, bool *outMissing);
|
|
static bool promptAndSave(void);
|
|
static void propsFollowActive(void);
|
|
static void rebuildProcTable(void);
|
|
static void recentAdd(const char *path);
|
|
static void recentLoad(void);
|
|
static void recentOpen(int32_t index);
|
|
static void recentRebuildMenu(void);
|
|
static void recentRemove(const char *path);
|
|
static void recentSave(void);
|
|
static void refreshEditorMarkers(void);
|
|
static void remapBreakpoints(const IdeLineAnchorT *anchors, const int32_t *starts);
|
|
static void removeBreakpointsForFile(int32_t fileIdx);
|
|
static void renameMenuItemsInCode(char **oldNames, int32_t oldCount);
|
|
static void renameProjectFile(int32_t fileIdx);
|
|
static bool renameProjectFileTo(int32_t fileIdx, const char *newName);
|
|
static int32_t replaceInSections(const char *objFilter, bool caseSensitive);
|
|
static void reportUnreadable(const char *path);
|
|
static void runBody(void);
|
|
static void runCached(void);
|
|
static void runFontsAdd(int32_t owner);
|
|
static void runModule(BasModuleT *mod);
|
|
static BasVmResultE runProgramSlices(BasFormRtT *rt);
|
|
static bool runStart(BasModuleT *mod);
|
|
static void runTaskEntry(void *arg);
|
|
static bool saveActiveFile(void);
|
|
static bool saveAllModified(void);
|
|
static bool saveCurProc(bool discardSkeleton);
|
|
static void saveFile(void);
|
|
static bool saveProjectFile(void);
|
|
static bool sectionInScope(int32_t p, const char *objFilter);
|
|
static int32_t sectionLineCount(const char *text);
|
|
static void selectDropdowns(const char *objName, const char *evtName);
|
|
static void setBreakpointProcName(IdeBreakpointT *bp, int32_t procIdx);
|
|
static bool setFileClassName(int32_t fileIdx, const char *name);
|
|
static void setOutputText(const char *text);
|
|
static void setStatus(const char *text);
|
|
static void shiftBreakpointsForEdit(const char *oldText, const char *newText, int32_t delta);
|
|
static void showBreakpointWindow(void);
|
|
static void showCallStackWindow(void);
|
|
static void showCodeView(int32_t fileIdx);
|
|
static void showCodeWindow(void);
|
|
static void showCompileError(const char *status);
|
|
static void showImmediateWindow(void);
|
|
static void showLocalsWindow(void);
|
|
static void showOutputWindow(void);
|
|
static void showPreferencesDialog(void);
|
|
static void showProc(int32_t procIdx);
|
|
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive, bool forward);
|
|
static void showProperties(void);
|
|
static void showToolbox(void);
|
|
static void showWatchWindow(void);
|
|
static const char *skipLine(const char *p);
|
|
static bool splitProcs(const char *source, char **outGeneral, char ***outProcs, IdeLineDestT **outMap);
|
|
static bool stashCurrentFile(void);
|
|
static void stashEditor(void);
|
|
static void stashFormCode(void);
|
|
static void switchToDesign(void);
|
|
static void syncFormDirty(void);
|
|
static void syncVmBreakpoints(void);
|
|
static void teardownFormWin(void);
|
|
static void toggleBreakpoint(void);
|
|
static void toggleBreakpointLine(int32_t editorLine);
|
|
static int32_t toolbarBottom(void);
|
|
static void updateBreakpointWindow(void);
|
|
static void updateCallStackWindow(void);
|
|
static void updateDirtyIndicators(void);
|
|
static void updateDropdowns(void);
|
|
static void updateLocalsWindow(void);
|
|
static void updateMainTitle(void);
|
|
static void updateProjectMenuState(void);
|
|
static void updateWatchWindow(void);
|
|
static void watchEditSelected(void);
|
|
static bool writeProjectFile(int32_t idx);
|
|
int32_t appMain(DxeAppContextT *ctx);
|
|
|
|
// ============================================================
|
|
// Module state
|
|
// ============================================================
|
|
|
|
static DxeAppContextT *sCtx = NULL;
|
|
static char sIdeHelpFile[DVX_MAX_PATH]; // IDE help file (restored after program run)
|
|
static AppContextT *sAc = NULL;
|
|
static PrefsHandleT *sPrefs = NULL;
|
|
static WindowT *sWin = NULL; // Main toolbar window
|
|
static WindowT *sCodeWin = NULL; // Code editor window
|
|
static WindowT *sOutWin = NULL; // Output window
|
|
static WindowT *sImmWin = NULL; // Immediate window
|
|
static WidgetT *sEditor = NULL;
|
|
static WidgetT *sOutput = NULL;
|
|
static WidgetT *sImmediate = NULL;
|
|
static WidgetT *sObjDropdown = NULL;
|
|
static WidgetT *sEvtDropdown = NULL;
|
|
static WidgetT *sToolbar = NULL;
|
|
static WidgetT *sStatusBar = NULL;
|
|
static WidgetT *sStatus = NULL;
|
|
static WidgetT *sTbRun = NULL;
|
|
static WidgetT *sTbStop = NULL;
|
|
static WidgetT *sTbDebug = NULL;
|
|
static WidgetT *sTbStepInto = NULL;
|
|
static WidgetT *sTbStepOver = NULL;
|
|
static WidgetT *sTbStepOut = NULL;
|
|
static WidgetT *sTbRunToCur = NULL;
|
|
static WidgetT *sTbCode = NULL;
|
|
static WidgetT *sTbDesign = NULL;
|
|
static BasVmT *sVm = NULL; // VM instance (non-NULL while running)
|
|
static BasModuleT *sCachedModule = NULL; // Last compiled module (for Ctrl+F5)
|
|
static BasModuleT *sDesignModule = NULL; // the project's UserControls as the designer runs them (basCtlLoadDesign)
|
|
static DsgnStateT sDesigner;
|
|
static WindowT *sFormWin = NULL; // Form designer window (separate)
|
|
static WindowT *sToolboxWin = NULL;
|
|
static WindowT *sPropsWin = NULL;
|
|
static WindowT *sProjectWin = NULL;
|
|
static PrjStateT sProject;
|
|
static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window
|
|
|
|
static char sOutputBuf[IDE_MAX_OUTPUT];
|
|
static int32_t sOutputLen = 0;
|
|
|
|
static PlatformTicksT sBusyPumpAt = 0; // when ideBusyPump last let DVX go on
|
|
static bool sBusyPumpBusy = false; // ideBusyPump set the busy state (ideBusyPumpEnd clears it)
|
|
static const BasDbgPumpT sBusyPump = { ideBusyPump, NULL }; // the debugger's snippets pump through it
|
|
|
|
static IdeRunT sRun;
|
|
|
|
// Procedure view state -- the editor shows one procedure at a time.
|
|
// Each procedure is stored in its own malloc'd buffer. The editor
|
|
// swaps directly between buffers with no splicing needed.
|
|
//
|
|
// The canonical text of a file is joinProcArrays(sGeneralBuf, sProcBufs):
|
|
// the (General) section, a blank line, then each procedure separated by
|
|
// one blank line. Everything that reads line numbers (sProcTable,
|
|
// breakpoints, the compiler's source map) works in that layout, and
|
|
// compileProject normalizes every file into it, so there is exactly one
|
|
// line-number space.
|
|
static char *sGeneralBuf = NULL; // (General) section: module-level code
|
|
static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure
|
|
static char *sFullSourceCache = NULL; // last joinProcBufs() result (owned here)
|
|
static int32_t sCurProcIdx = IDE_SECTION_NONE; // which buffer is in the editor (or IDE_SECTION_GENERAL / _NONE)
|
|
static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none)
|
|
static int32_t sDesignerFileIdx = -1; // which project file owns sDesigner.form (-1=none)
|
|
static int32_t sEditorLineCount = 0; // line count for breakpoint adjustment on edit
|
|
static char *sEditorPrevText = NULL; // editor text before the latest edit (malloc'd)
|
|
static bool sEditorTruncated = false; // section exceeds the editor: shown read-only
|
|
static char sSkeletonName[IDE_FULL_NAME_BUF] = ""; // untouched generated event Sub
|
|
|
|
// Find/Replace state
|
|
static char sFindText[IDE_FIND_TEXT_BUF] = "";
|
|
static char sReplaceText[IDE_FIND_TEXT_BUF] = "";
|
|
// Find/Replace dialog state (modeless)
|
|
static WindowT *sFindWin = NULL;
|
|
static WidgetT *sFindInput = NULL;
|
|
static WidgetT *sReplInput = NULL;
|
|
static WidgetT *sReplCheck = NULL;
|
|
static WidgetT *sBtnReplace = NULL;
|
|
static WidgetT *sBtnReplAll = NULL;
|
|
static WidgetT *sCaseCheck = NULL;
|
|
static WidgetT *sScopeGroup = NULL; // radio group indexed by FindScopeE
|
|
static WidgetT *sDirGroup = NULL; // radio group indexed by FindDirE
|
|
|
|
static IdeProcEntryT *sProcTable = NULL; // stb_ds dynamic array (rebuilt by joinProcBufs)
|
|
static const char **sObjItems = NULL; // stb_ds dynamic array of owned (strdup'd) names
|
|
static const char **sEvtItems = NULL; // stb_ds dynamic array of owned (strdup'd) labels
|
|
static IdeWithEventsT *sWithEvents = NULL; // stb_ds: the editor file's WithEvents variables (updateDropdowns)
|
|
static bool sDropdownNavSuppressed = false;
|
|
static bool sStopRequested = false;
|
|
static bool sOutputToLog = false;
|
|
static bool sOutputPending = false; // sOutputBuf not yet shown
|
|
static char sLogLine[IDE_LOG_LINE_BUF]; // PRINT text not yet logged
|
|
static int32_t sLogLineLen = 0;
|
|
|
|
// Recent files list (stored in dvxbasic.ini [recent] section)
|
|
// stb_ds dynamic array of strdup'd paths. Capped at CMD_RECENT_MAX by the menu
|
|
// command-ID range.
|
|
static char **sRecentFiles = NULL;
|
|
static MenuT *sFileMenu = NULL;
|
|
static int32_t sFileMenuBase = 0; // item count before recent files
|
|
|
|
static IdeDebugStateE sDbgState = DbgIdleE;
|
|
static BasBreakTitleT *sBreakTitles = NULL; // stb_ds: captions tagged " [break]"
|
|
static IdeBreakpointT *sBreakpoints = NULL; // stb_ds array
|
|
static int32_t *sVmBreakpoints = NULL; // stb_ds array of concat line numbers (built at compile time)
|
|
static int32_t sDbgCurrentLine = -1; // line where paused (-1 = none)
|
|
static BasFormRtT *sDbgFormRt = NULL; // form runtime for debug session
|
|
static BasModuleT *sDbgModule = NULL; // module for debug session
|
|
static bool sDbgBreakOnStart = false; // break at first statement
|
|
static bool sDbgEnabled = false; // true = debug mode (breakpoints active)
|
|
static WindowT *sLocalsWin = NULL; // Locals window
|
|
static WidgetT *sLocalsList = NULL; // Locals ListView widget
|
|
static WindowT *sCallStackWin = NULL; // Call stack window
|
|
static WidgetT *sCallStackList = NULL; // Call stack ListView widget
|
|
static WindowT *sWatchWin = NULL; // Watch window
|
|
static WidgetT *sWatchList = NULL; // Watch ListView widget
|
|
static WidgetT *sWatchInput = NULL; // Watch expression input
|
|
static char **sWatchExprs = NULL; // stb_ds dynamic array of strdup'd watch expressions
|
|
static WindowT *sBreakpointWin = NULL; // Breakpoints window
|
|
static WidgetT *sBreakpointList = NULL; // Breakpoints ListView widget
|
|
|
|
// Per-refresh display rows for the debug ListViews
|
|
static IdeListRowsT sBpRows;
|
|
static IdeListRowsT sLocalsRows;
|
|
static IdeListRowsT sCallRows;
|
|
static IdeListRowsT sWatchRows;
|
|
|
|
// ============================================================
|
|
// Syntax highlighting tables (shared by several functions)
|
|
// ============================================================
|
|
|
|
// Color entry names for the Colors tab (matches TEXT_SYNTAX_* indices)
|
|
static const char *sSyntaxColorNames[TEXT_SYNTAX_MAX] = {
|
|
[TEXT_SYNTAX_DEFAULT] = "Default Text",
|
|
[TEXT_SYNTAX_KEYWORD] = "Keywords",
|
|
[TEXT_SYNTAX_STRING] = "Strings",
|
|
[TEXT_SYNTAX_COMMENT] = "Comments",
|
|
[TEXT_SYNTAX_NUMBER] = "Numbers",
|
|
[TEXT_SYNTAX_OPERATOR] = "Operators",
|
|
[TEXT_SYNTAX_TYPE] = "Types",
|
|
};
|
|
|
|
static IdePrefsDlgT sPrefsDlg;
|
|
|
|
// ============================================================
|
|
// App descriptor
|
|
// ============================================================
|
|
|
|
SHELL_APP_DESCRIPTOR(
|
|
.name = "DVX BASIC",
|
|
.hasMainLoop = false,
|
|
.multiInstance = false,
|
|
.stackSize = SHELL_STACK_DEFAULT,
|
|
.priority = 0
|
|
);
|
|
|
|
|
|
// activateFailed -- the switch did not happen: when the skeleton the
|
|
// editor showed was dropped on the way out, show the outgoing file's
|
|
// (General) so the editor is never left bound to no section. Returns
|
|
// false for activateFile to pass on.
|
|
static bool activateFailed(void) {
|
|
if (sCurProcIdx == IDE_SECTION_NONE && sEditorFileIdx >= 0) {
|
|
updateDropdowns();
|
|
showProc(IDE_SECTION_GENERAL);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// activateFile -- make fileIdx the active file and show it in the given
|
|
// view. Returns false (leaving the current file active) when the
|
|
// outgoing file cannot be stashed or the incoming one cannot be loaded.
|
|
static bool activateFile(int32_t fileIdx, IdeViewModeE view) {
|
|
if (fileIdx < 0 || fileIdx >= prjFileCount(&sProject)) {
|
|
return false;
|
|
}
|
|
|
|
PrjFileT *target = &sProject.files[fileIdx];
|
|
|
|
// Resolve ViewAutoE
|
|
if (view == ViewAutoE) {
|
|
view = target->kind == PrjKindFormE ? ViewDesignE : ViewCodeE;
|
|
}
|
|
|
|
// If already active, just ensure the right view is showing
|
|
if (fileIdx == sProject.activeFileIdx) {
|
|
if (view == ViewDesignE) {
|
|
switchToDesign();
|
|
} else {
|
|
showCodeView(fileIdx);
|
|
}
|
|
|
|
propsFollowActive();
|
|
return true;
|
|
}
|
|
|
|
// PHASE 1: Stash the outgoing file. The editor is being rebound, so an
|
|
// untouched event skeleton is dropped first. A failed stash keeps the
|
|
// current file active rather than throwing its edits away.
|
|
saveCurProc(true);
|
|
|
|
if (!stashCurrentFile()) {
|
|
return activateFailed();
|
|
}
|
|
|
|
// PHASE 2: Load the incoming file
|
|
if (target->kind == PrjKindFormE) {
|
|
// A form already in the designer keeps its live (possibly unsaved) state
|
|
if ((fileIdx != sDesignerFileIdx || !sDesigner.form) && !loadDesignerFile(fileIdx)) {
|
|
return activateFailed();
|
|
}
|
|
|
|
sProject.activeFileIdx = fileIdx;
|
|
|
|
if (view == ViewDesignE) {
|
|
switchToDesign();
|
|
|
|
// The editor must never stay bound to the previous file: an open
|
|
// code window now shows this form's code, otherwise the stale
|
|
// proc buffers are released so nothing can be edited into limbo.
|
|
if (sCodeWin) {
|
|
loadFormCodeIntoEditor();
|
|
} else {
|
|
freeProcBufs();
|
|
}
|
|
} else {
|
|
loadFormCodeIntoEditor();
|
|
}
|
|
} else {
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *source = projectFileText(fileIdx, &owned, &missing);
|
|
|
|
if (!source && !missing) {
|
|
reportUnreadable(target->path);
|
|
return activateFailed();
|
|
}
|
|
|
|
if (!sCodeWin) {
|
|
showCodeWindow();
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
// Only a file that does not exist yet starts out empty (and unsaved)
|
|
if (!source) {
|
|
target->modified = true;
|
|
}
|
|
|
|
sProject.activeFileIdx = fileIdx;
|
|
loadModuleIntoEditor(fileIdx, source ? source : "");
|
|
free(owned);
|
|
}
|
|
|
|
updateProjectMenuState();
|
|
updateDirtyIndicators();
|
|
propsFollowActive();
|
|
return true;
|
|
}
|
|
|
|
|
|
// activeIsForm -- true when the active project file is a .frm
|
|
static bool activeIsForm(void) {
|
|
return sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject) && sProject.files[sProject.activeFileIdx].kind == PrjKindFormE;
|
|
}
|
|
|
|
|
|
// addClassEvent -- basSourceClassEvents callback: one Event of a class
|
|
// module into the IdeClassEventT array ctx points at
|
|
static void addClassEvent(void *ctx, const char *name, const char *params) {
|
|
IdeClassEventT **events = (IdeClassEventT **)ctx;
|
|
IdeClassEventT evt;
|
|
|
|
memset(&evt, 0, sizeof(evt));
|
|
snprintf(evt.name, sizeof(evt.name), "%s", name);
|
|
snprintf(evt.params, sizeof(evt.params), "%s", params);
|
|
arrput(*events, evt);
|
|
}
|
|
|
|
|
|
// Adds path to the open project (or activates it when already listed).
|
|
// Returns false when the file lives outside the project directory --
|
|
// PrjFileT.path is project-relative, so such a file could never be
|
|
// read back and the next save would silently create an empty copy --
|
|
// or when it exists but cannot be loaded.
|
|
static bool addFileToProject(const char *path) {
|
|
const char *fileName = platformPathBaseName(path);
|
|
char dir[DVX_MAX_PATH];
|
|
|
|
platformPathDirName(path, dir, sizeof(dir));
|
|
|
|
if (!platformPathSame(dir, sProject.projectDir)) {
|
|
ideErrorBox("Files must be located in the project directory.");
|
|
return false;
|
|
}
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (platformPathSame(sProject.files[i].path, fileName)) {
|
|
return activateFile(i, ViewAutoE);
|
|
}
|
|
}
|
|
|
|
bool wasDirty = sProject.dirty;
|
|
|
|
prjAddFile(&sProject, fileName, prjKindForPath(path));
|
|
|
|
// A file that exists but cannot be loaded is not kept: its empty
|
|
// stand-in would overwrite the real file on the next save.
|
|
if (!activateFile(prjFileCount(&sProject) - 1, ViewAutoE)) {
|
|
prjRemoveFile(&sProject, prjFileCount(&sProject) - 1);
|
|
sProject.dirty = wasDirty;
|
|
return false;
|
|
}
|
|
|
|
invalidateCompiled();
|
|
prjRebuildTree(&sProject);
|
|
|
|
if (sProject.files[prjFileCount(&sProject) - 1].kind == PrjKindControlE) {
|
|
designCtlRefresh();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// addWithEvents -- basSourceWithEvents callback: one WithEvents variable
|
|
// of the editor's file into sWithEvents
|
|
static void addWithEvents(void *ctx, const char *name, const char *className) {
|
|
IdeWithEventsT we;
|
|
|
|
(void)ctx;
|
|
snprintf(we.name, sizeof(we.name), "%s", name);
|
|
snprintf(we.className, sizeof(we.className), "%s", className);
|
|
arrput(sWithEvents, we);
|
|
}
|
|
|
|
|
|
// anchorCodeLine -- pin whole-file line codeLine of the editor's file to
|
|
// the section holding it. The layout is the stored buffers with section
|
|
// cur replaced by the edited text: its edLines lines start at cur's
|
|
// start, and every later section was moved by delta while it was edited
|
|
// (starts: procStartLines of the stored buffers).
|
|
static void anchorCodeLine(IdeLineAnchorT *a, int32_t codeLine, int32_t cur, int32_t edLines, int32_t delta, const int32_t *starts) {
|
|
int32_t curStart = cur == IDE_SECTION_GENERAL ? 1 : starts[cur];
|
|
|
|
a->edLine = IDE_NO_LINE;
|
|
a->section = NULL;
|
|
a->offset = IDE_NO_LINE;
|
|
|
|
if (codeLine >= curStart && codeLine < curStart + edLines) {
|
|
a->edLine = codeLine - curStart;
|
|
return;
|
|
}
|
|
|
|
if (codeLine >= curStart + edLines) {
|
|
codeLine -= delta;
|
|
}
|
|
|
|
if (cur != IDE_SECTION_GENERAL && codeLine >= 1 && codeLine <= sectionLineCount(sGeneralBuf)) {
|
|
a->offset = codeLine - 1;
|
|
return;
|
|
}
|
|
|
|
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
|
|
if (p != cur && codeLine >= starts[p] && codeLine < starts[p] + sectionLineCount(sProcBufs[p])) {
|
|
a->section = sProcBufs[p];
|
|
a->offset = codeLine - starts[p];
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// anchorEditedLine -- pin a line of the edited text (a->edLine) to where
|
|
// splitProcs sent it: a proc buffer, or (General) at genBase plus its
|
|
// line in the General piece. A line that was not stored (a discarded
|
|
// skeleton's, whose procs[] entry is NULL) is gone.
|
|
static void anchorEditedLine(IdeLineAnchorT *a, const IdeLineDestT *map, char **procs, int32_t genBase) {
|
|
a->section = NULL;
|
|
a->offset = IDE_NO_LINE;
|
|
|
|
if (a->edLine < 0 || a->edLine >= (int32_t)arrlen(map)) {
|
|
return;
|
|
}
|
|
|
|
const IdeLineDestT *d = &map[a->edLine];
|
|
|
|
if (d->piece == IDE_SECTION_GENERAL) {
|
|
a->offset = genBase + d->line;
|
|
} else if (procs[d->piece]) {
|
|
a->section = procs[d->piece];
|
|
a->offset = d->line;
|
|
}
|
|
}
|
|
|
|
|
|
// anchorLine -- whole-file line of an anchor in the current layout
|
|
// (starts: procStartLines of the current buffers), or IDE_NO_LINE when
|
|
// its line no longer exists
|
|
static int32_t anchorLine(const IdeLineAnchorT *a, const int32_t *starts) {
|
|
if (a->offset < 0) {
|
|
return IDE_NO_LINE;
|
|
}
|
|
|
|
if (!a->section) {
|
|
return a->offset < sectionLineCount(sGeneralBuf) ? a->offset + 1 : IDE_NO_LINE;
|
|
}
|
|
|
|
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
|
|
if (sProcBufs[p] == a->section) {
|
|
return a->offset < sectionLineCount(a->section) ? starts[p] + a->offset : IDE_NO_LINE;
|
|
}
|
|
}
|
|
|
|
return IDE_NO_LINE;
|
|
}
|
|
|
|
|
|
// anyFileModified -- whether any project file (or the designer's form)
|
|
// has unsaved edits; the one rule for Save All, the title and the exit
|
|
// prompt (which also counts the .dbp itself)
|
|
static bool anyFileModified(void) {
|
|
bool modified = sDesigner.form && sDesigner.form->dirty;
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject) && !modified; i++) {
|
|
modified = sProject.files[i].modified;
|
|
}
|
|
|
|
return modified;
|
|
}
|
|
|
|
|
|
// appendCompileFile -- append project file fileIdx's compile text (text,
|
|
// len bytes: its canonical layout, a form's code section only) to ps the
|
|
// way basBuildConcatProject does: a form's code wrapped in BEGINFORM with
|
|
// its controls (ctrlList, see basFrmControlList), a class module's or
|
|
// UserControl's code after its header wrapped in BEGINCLASS (a
|
|
// UserControl's with its mark), a module in BEGINMODULE. Fills entry's
|
|
// line fields; false with ps->error set on failure.
|
|
static bool appendCompileFile(BasProjectSourceT *ps, int32_t fileIdx, const char *text, int32_t len, const char *ctrlList, BasSourceMapT *entry) {
|
|
PrjFileT *file = &sProject.files[fileIdx];
|
|
const char *name = "";
|
|
BasBuildWrapE wrap = BAS_BUILD_WRAP_MODULE;
|
|
int32_t codePos = 0;
|
|
char className[BAS_MAX_IDENT];
|
|
|
|
entry->firstFileLine = 1;
|
|
|
|
if (file->kind == PrjKindFormE) {
|
|
name = file->formName;
|
|
wrap = BAS_BUILD_WRAP_FORM;
|
|
} else if (file->kind == PrjKindClassE || file->kind == PrjKindControlE) {
|
|
if (!prjClassName(&sProject, fileIdx, text, className, (int32_t)sizeof(className))) {
|
|
snprintf(ps->error, sizeof(ps->error), "No class name for %s", file->path);
|
|
return false;
|
|
}
|
|
|
|
codePos = prjClassCodePos(text, len, &entry->firstFileLine);
|
|
name = className;
|
|
wrap = prjKindWrap((PrjFileKindE)file->kind);
|
|
}
|
|
|
|
return basBuildAppendFile(ps, text + codePos, len - codePos, wrap, name, ctrlList, &entry->startLine, &entry->lineCount);
|
|
}
|
|
|
|
|
|
static void applySyntaxColors(void) {
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
wgtTextAreaSetSyntaxColors(sEditor, sPrefsDlg.syntaxColors, TEXT_SYNTAX_MAX);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// buildVmBreakpoints -- convert IDE breakpoints to VM concat line numbers
|
|
// ============================================================
|
|
//
|
|
// Called after compilation when the source map is fresh. Converts
|
|
// each (fileIdx, codeLine) pair to a concatenated source line number
|
|
// that matches OP_LINE values in the compiled bytecode.
|
|
|
|
static void buildVmBreakpoints(void) {
|
|
arrfree(sVmBreakpoints);
|
|
sVmBreakpoints = NULL;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
|
|
int32_t concatLine = prjMapFileLine(&sProject, sBreakpoints[i].fileIdx, sBreakpoints[i].codeLine);
|
|
|
|
if (concatLine > 0) {
|
|
arrput(sVmBreakpoints, concatLine);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void buildWindow(void) {
|
|
// ---- Main toolbar window (top of screen) ----
|
|
sWin = dvxCreateWindow(sAc, IDE_MAIN_TITLE, 0, 0, sAc->display.width, IDE_TOOLBAR_INIT_H, true);
|
|
|
|
if (!sWin) {
|
|
return;
|
|
}
|
|
|
|
sWin->onClose = onClose;
|
|
sWin->onMenu = onMenu;
|
|
|
|
// Menu bar
|
|
MenuBarT *menuBar = wmAddMenuBar(sWin);
|
|
sFileMenu = wmAddMenu(menuBar, "&File");
|
|
MenuT *fileMenu = sFileMenu;
|
|
wmAddMenuItem(fileMenu, "New &Project...", CMD_PRJ_NEW);
|
|
wmAddMenuItem(fileMenu, "&Open Project...", CMD_PRJ_OPEN);
|
|
wmAddMenuItem(fileMenu, "Save Projec&t", CMD_PRJ_SAVE);
|
|
wmAddMenuItem(fileMenu, "&Close Project", CMD_PRJ_CLOSE);
|
|
wmAddMenuSeparator(fileMenu);
|
|
wmAddMenuItem(fileMenu, "Project Propert&ies...", CMD_PRJ_PROPS);
|
|
wmAddMenuSeparator(fileMenu);
|
|
wmAddMenuItem(fileMenu, "&Add File...\tCtrl+O", CMD_OPEN);
|
|
wmAddMenuItem(fileMenu, "&New Class Module", CMD_PRJ_NEW_CLASS);
|
|
wmAddMenuItem(fileMenu, "New &UserControl", CMD_PRJ_NEW_CTL);
|
|
wmAddMenuItem(fileMenu, "&Save File\tCtrl+S", CMD_SAVE);
|
|
wmAddMenuItem(fileMenu, "Save A&ll", CMD_SAVE_ALL);
|
|
wmAddMenuSeparator(fileMenu);
|
|
wmAddMenuItem(fileMenu, IDE_MAKE_EXE_LABEL, CMD_MAKE_EXE);
|
|
wmAddMenuItem(fileMenu, IDE_MAKE_PKG_LABEL, CMD_MAKE_PKG);
|
|
wmAddMenuSeparator(fileMenu);
|
|
wmAddMenuItem(fileMenu, "R&ename File...", CMD_PRJ_RENAME);
|
|
wmAddMenuItem(fileMenu, "&Remove File", CMD_PRJ_REMOVE);
|
|
wmAddMenuSeparator(fileMenu);
|
|
wmAddMenuItem(fileMenu, "E&xit", CMD_EXIT);
|
|
sFileMenuBase = sFileMenu->itemCount;
|
|
|
|
MenuT *editMenu = wmAddMenu(menuBar, "&Edit");
|
|
wmAddMenuItem(editMenu, "Cu&t\tCtrl+X", CMD_CUT);
|
|
wmAddMenuItem(editMenu, "&Copy\tCtrl+C", CMD_COPY);
|
|
wmAddMenuItem(editMenu, "&Paste\tCtrl+V", CMD_PASTE);
|
|
wmAddMenuSeparator(editMenu);
|
|
wmAddMenuItem(editMenu, "Select &All\tCtrl+A", CMD_SELECT_ALL);
|
|
wmAddMenuSeparator(editMenu);
|
|
wmAddMenuItem(editMenu, "&Delete\tDel", CMD_DELETE);
|
|
wmAddMenuSeparator(editMenu);
|
|
wmAddMenuItem(editMenu, "&Find...\tCtrl+F", CMD_FIND);
|
|
wmAddMenuItem(editMenu, "Find &Next\tF3", CMD_FIND_NEXT);
|
|
wmAddMenuItem(editMenu, "&Replace...\tCtrl+H", CMD_REPLACE);
|
|
|
|
MenuT *runMenu = wmAddMenu(menuBar, "&Run");
|
|
wmAddMenuItem(runMenu, "&Run\tF5", CMD_RUN);
|
|
wmAddMenuItem(runMenu, "&Debug\tShift+F5", CMD_DEBUG);
|
|
wmAddMenuItem(runMenu, "Run &Without Recompile\tCtrl+F5", CMD_RUN_NOCMP);
|
|
wmAddMenuItem(runMenu, "&Stop\tEsc", CMD_STOP);
|
|
wmAddMenuSeparator(runMenu);
|
|
wmAddMenuItem(runMenu, "Step &Into\tF8", CMD_STEP_INTO);
|
|
wmAddMenuItem(runMenu, "Step &Over\tShift+F8", CMD_STEP_OVER);
|
|
wmAddMenuItem(runMenu, "Step Ou&t\tCtrl+Shift+F8", CMD_STEP_OUT);
|
|
wmAddMenuItem(runMenu, "Run to &Cursor\tCtrl+F8", CMD_RUN_TO_CURSOR);
|
|
wmAddMenuSeparator(runMenu);
|
|
wmAddMenuCheckItem(runMenu, "Output Window to &Log", CMD_OUTPUT_TO_LOG, false);
|
|
wmAddMenuSeparator(runMenu);
|
|
wmAddMenuItem(runMenu, "Toggle &Breakpoint\tF9", CMD_TOGGLE_BP);
|
|
wmAddMenuSeparator(runMenu);
|
|
wmAddMenuItem(runMenu, "Cl&ear Output", CMD_CLEAR);
|
|
wmAddMenuSeparator(runMenu);
|
|
wmAddMenuCheckItem(runMenu, "S&ave on Run", CMD_SAVE_ON_RUN, true);
|
|
|
|
MenuT *viewMenu = wmAddMenu(menuBar, "&View");
|
|
wmAddMenuItem(viewMenu, "&Code\tF7", CMD_VIEW_CODE);
|
|
wmAddMenuItem(viewMenu, "&Designer\tShift+F7", CMD_VIEW_DESIGN);
|
|
wmAddMenuSeparator(viewMenu);
|
|
wmAddMenuCheckItem(viewMenu, "&Toolbar", CMD_VIEW_TOOLBAR, true);
|
|
wmAddMenuCheckItem(viewMenu, "&Status Bar", CMD_VIEW_STATUS, true);
|
|
wmAddMenuSeparator(viewMenu);
|
|
wmAddMenuItem(viewMenu, "&Menu Editor...\tCtrl+E", CMD_MENU_EDITOR);
|
|
|
|
MenuT *winMenu = wmAddMenu(menuBar, "&Window");
|
|
wmAddMenuItem(winMenu, "&Code Editor", CMD_WIN_CODE);
|
|
wmAddMenuItem(winMenu, "&Output", CMD_WIN_OUTPUT);
|
|
wmAddMenuItem(winMenu, "&Immediate", CMD_WIN_IMM);
|
|
wmAddMenuItem(winMenu, "&Locals", CMD_WIN_LOCALS);
|
|
wmAddMenuItem(winMenu, "Call &Stack", CMD_WIN_CALLSTACK);
|
|
wmAddMenuItem(winMenu, "&Watch", CMD_WIN_WATCH);
|
|
wmAddMenuItem(winMenu, "&Breakpoints", CMD_WIN_BREAKPOINTS);
|
|
wmAddMenuSeparator(winMenu);
|
|
wmAddMenuItem(winMenu, "&Project Explorer", CMD_WIN_PROJECT);
|
|
wmAddMenuItem(winMenu, "&Toolbox", CMD_WIN_TOOLBOX);
|
|
wmAddMenuItem(winMenu, "P&roperties", CMD_WIN_PROPS);
|
|
|
|
MenuT *toolsMenu = wmAddMenu(menuBar, "&Tools");
|
|
wmAddMenuItem(toolsMenu, "&Preferences...", CMD_PREFERENCES);
|
|
wmAddMenuSeparator(toolsMenu);
|
|
wmAddMenuCheckItem(toolsMenu, "Debug &Layout", CMD_DEBUG_LAYOUT, false);
|
|
|
|
MenuT *helpMenu = wmAddMenu(menuBar, "&Help");
|
|
wmAddMenuItem(helpMenu, "&DVX BASIC Help\tF1", CMD_HELP_CONTENTS);
|
|
wmAddMenuItem(helpMenu, "DVX &API Reference", CMD_HELP_API);
|
|
wmAddMenuSeparator(helpMenu);
|
|
wmAddMenuItem(helpMenu, "A&bout DVX BASIC...", CMD_HELP_ABOUT);
|
|
|
|
AccelTableT *accel = dvxCreateAccelTable();
|
|
dvxAddAccel(accel, 'O', ACCEL_CTRL, CMD_OPEN);
|
|
dvxAddAccel(accel, 'S', ACCEL_CTRL, CMD_SAVE);
|
|
dvxAddAccel(accel, KEY_F5, 0, CMD_RUN);
|
|
dvxAddAccel(accel, KEY_F5, ACCEL_SHIFT, CMD_DEBUG);
|
|
dvxAddAccel(accel, KEY_F5, ACCEL_CTRL, CMD_RUN_NOCMP);
|
|
dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE);
|
|
dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN);
|
|
dvxAddAccel(accel, KEY_ESCAPE, 0, CMD_STOP);
|
|
dvxAddAccel(accel, 'F', ACCEL_CTRL, CMD_FIND);
|
|
dvxAddAccel(accel, 'H', ACCEL_CTRL, CMD_REPLACE);
|
|
dvxAddAccel(accel, KEY_F3, 0, CMD_FIND_NEXT);
|
|
dvxAddAccel(accel, 'E', ACCEL_CTRL, CMD_MENU_EDITOR);
|
|
dvxAddAccel(accel, KEY_F8, 0, CMD_STEP_INTO);
|
|
dvxAddAccel(accel, KEY_F8, ACCEL_SHIFT, CMD_STEP_OVER);
|
|
dvxAddAccel(accel, KEY_F8, ACCEL_CTRL | ACCEL_SHIFT, CMD_STEP_OUT);
|
|
dvxAddAccel(accel, KEY_F8, ACCEL_CTRL, CMD_RUN_TO_CURSOR);
|
|
dvxAddAccel(accel, KEY_F9, 0, CMD_TOGGLE_BP);
|
|
sWin->accelTable = accel;
|
|
|
|
WidgetT *tbRoot = wgtInitWindow(sAc, sWin);
|
|
sToolbar = wgtToolbar(tbRoot);
|
|
WidgetT *tb = sToolbar;
|
|
|
|
// File group
|
|
WidgetT *tbOpen = loadTbIcon(tb, "tb_open", "Open");
|
|
tbOpen->onClick = onTbOpen;
|
|
// Button opens the project; Ctrl+O is bound to Add File, so the tooltip
|
|
// must not advertise a shortcut the button doesn't perform.
|
|
wgtSetTooltip(tbOpen, "Open Project...");
|
|
|
|
WidgetT *tbSave = loadTbIcon(tb, "tb_save", "Save");
|
|
tbSave->onClick = onTbSave;
|
|
wgtSetTooltip(tbSave, "Save (Ctrl+S)");
|
|
|
|
WidgetT *sep1 = wgtVSeparator(tb);
|
|
sep1->minW = wgtPixels(IDE_TOOLBAR_SEP_W);
|
|
|
|
// Run group
|
|
sTbRun = loadTbIcon(tb, "tb_run", "Run");
|
|
sTbRun->onClick = onTbRun;
|
|
wgtSetTooltip(sTbRun, "Run (F5)");
|
|
|
|
sTbStop = loadTbIcon(tb, "tb_stop", "Stop");
|
|
sTbStop->onClick = onTbStop;
|
|
wgtSetTooltip(sTbStop, "Stop (Esc)");
|
|
|
|
WidgetT *sep2 = wgtVSeparator(tb);
|
|
sep2->minW = wgtPixels(IDE_TOOLBAR_SEP_W);
|
|
|
|
// Debug group
|
|
sTbDebug = loadTbIcon(tb, "tb_debug", "Debug");
|
|
sTbDebug->onClick = onTbDebug;
|
|
wgtSetTooltip(sTbDebug, "Debug (Shift+F5)");
|
|
|
|
sTbStepInto = loadTbIcon(tb, "tb_stepin", "Into");
|
|
sTbStepInto->onClick = onTbStepInto;
|
|
wgtSetTooltip(sTbStepInto, "Step Into (F8)");
|
|
|
|
sTbStepOver = loadTbIcon(tb, "tb_stepov", "Over");
|
|
sTbStepOver->onClick = onTbStepOver;
|
|
wgtSetTooltip(sTbStepOver, "Step Over (Shift+F8)");
|
|
|
|
sTbStepOut = loadTbIcon(tb, "tb_stepou", "Out");
|
|
sTbStepOut->onClick = onTbStepOut;
|
|
wgtSetTooltip(sTbStepOut, "Step Out (Ctrl+Shift+F8)");
|
|
|
|
sTbRunToCur = loadTbIcon(tb, "tb_runtoc", "Cursor");
|
|
sTbRunToCur->onClick = onTbRunToCur;
|
|
wgtSetTooltip(sTbRunToCur, "Run to Cursor (Ctrl+F8)");
|
|
|
|
WidgetT *sep3 = wgtVSeparator(tb);
|
|
sep3->minW = wgtPixels(IDE_TOOLBAR_SEP_W);
|
|
|
|
// View group
|
|
sTbCode = loadTbIcon(tb, "tb_code", "Code");
|
|
sTbCode->onClick = onTbCode;
|
|
wgtSetTooltip(sTbCode, "Code View (F7)");
|
|
|
|
sTbDesign = loadTbIcon(tb, "tb_design", "Design");
|
|
sTbDesign->onClick = onTbDesign;
|
|
wgtSetTooltip(sTbDesign, "Design View (Shift+F7)");
|
|
|
|
sStatusBar = wgtStatusBar(tbRoot);
|
|
WidgetT *statusBar = sStatusBar;
|
|
sStatus = wgtLabel(statusBar, "");
|
|
sStatus->weight = WGT_WEIGHT_FILL;
|
|
|
|
// Fit height to content, keeping full screen width
|
|
dvxFitWindowH(sAc, sWin);
|
|
|
|
// Initialize designer (form window created on demand)
|
|
dsgnInit(&sDesigner, sAc);
|
|
sDesigner.projectDir = sProject.projectDir;
|
|
|
|
showOutputWindow();
|
|
showImmediateWindow();
|
|
dvxRaiseWindow(sAc, sWin);
|
|
}
|
|
|
|
|
|
// classEventsOf -- the Events class module className declares (none when
|
|
// the project has no such class), appended to *events
|
|
static void classEventsOf(const char *className, IdeClassEventT **events) {
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (sProject.files[i].kind != PrjKindClassE) {
|
|
continue;
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = (i == sEditorFileIdx) ? getFullSource() : projectFileText(i, &owned, &missing);
|
|
char name[BAS_MAX_IDENT];
|
|
bool match = text && prjClassName(&sProject, i, text, name, (int32_t)sizeof(name)) && strcasecmp(name, className) == 0;
|
|
|
|
if (match) {
|
|
basSourceClassEvents(text, addClassEvent, events);
|
|
}
|
|
|
|
free(owned);
|
|
|
|
if (match) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// classFollowsFile -- whether class module or UserControl fileIdx (whose
|
|
// text is given) is named after its file: a class module with no
|
|
// Attribute VB_Name or one equal to the file's base name, a UserControl
|
|
// whose Begin UserControl name is (as File > New Class Module / New
|
|
// UserControl write them)
|
|
static bool classFollowsFile(int32_t fileIdx, const char *text) {
|
|
char ownName[BAS_MAX_IDENT];
|
|
char fileName[BAS_MAX_IDENT];
|
|
|
|
basExtractClassName("", 0, sProject.files[fileIdx].path, fileName, (int32_t)sizeof(fileName));
|
|
return !prjClassName(&sProject, fileIdx, text, ownName, (int32_t)sizeof(ownName)) || strcasecmp(ownName, fileName) == 0;
|
|
}
|
|
|
|
|
|
// classNameUsed -- true when a project file is named fileName or a form,
|
|
// class module or UserControl of the project is called name
|
|
static bool classNameUsed(const char *name, const char *fileName) {
|
|
bool used = false;
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject) && !used; i++) {
|
|
PrjFileT *file = &sProject.files[i];
|
|
|
|
used = platformPathSame(file->path, fileName) || (file->kind == PrjKindFormE && strcasecmp(file->formName, name) == 0);
|
|
|
|
if (!used && (file->kind == PrjKindClassE || file->kind == PrjKindControlE)) {
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(i, &owned, &missing);
|
|
char other[BAS_MAX_IDENT];
|
|
|
|
used = prjClassName(&sProject, i, text, other, (int32_t)sizeof(other)) && strcasecmp(other, name) == 0;
|
|
free(owned);
|
|
}
|
|
}
|
|
|
|
return used;
|
|
}
|
|
|
|
|
|
// cleanupFormWin -- release designer-related state without destroying
|
|
// the form window itself (the caller handles that).
|
|
static void cleanupFormWin(void) {
|
|
basDsgnSetDesignWindow(sFormWin, false);
|
|
sFormWin = NULL;
|
|
sDesigner.formWin = NULL;
|
|
|
|
// Null out widget pointers -- the widgets were destroyed with the window.
|
|
// Without this, dsgnCreateWidgets skips controls that have non-NULL
|
|
// widget pointers, resulting in an empty form on the second open.
|
|
if (sDesigner.form) {
|
|
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
sDesigner.form->controls[i]->userData = NULL;
|
|
}
|
|
}
|
|
|
|
sDesigner.contentBox = NULL;
|
|
|
|
if (sToolboxWin) {
|
|
tbxDestroy(sAc, sToolboxWin);
|
|
sToolboxWin = NULL;
|
|
}
|
|
|
|
if (sPropsWin) {
|
|
prpDestroy(sAc, sPropsWin);
|
|
sPropsWin = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
static void clearAllBreakpoints(void) {
|
|
arrsetlen(sBreakpoints, 0);
|
|
|
|
// A live VM still points at sVmBreakpoints; detach before freeing.
|
|
if (sVm) {
|
|
basVmSetBreakpoints(sVm, NULL, 0);
|
|
}
|
|
|
|
arrfree(sVmBreakpoints);
|
|
sVmBreakpoints = NULL;
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
|
|
static void clearOutput(void) {
|
|
sOutputBuf[0] = '\0';
|
|
sOutputLen = 0;
|
|
sOutputPending = false;
|
|
setOutputText("");
|
|
}
|
|
|
|
|
|
static void closeFindDialog(void) {
|
|
if (sFindWin) {
|
|
dvxDestroyWindow(sAc, sFindWin);
|
|
sFindWin = NULL;
|
|
sFindInput = NULL;
|
|
sReplInput = NULL;
|
|
sReplCheck = NULL;
|
|
sBtnReplace = NULL;
|
|
sBtnReplAll = NULL;
|
|
sCaseCheck = NULL;
|
|
sScopeGroup = NULL;
|
|
sDirGroup = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// closeProject -- callers prompt for unsaved edits first (promptAndSave),
|
|
// which saves on Yes. Nothing is written here, so Don't Save discards
|
|
// changes to the .dbp as well as to the files.
|
|
static void closeProject(void) {
|
|
if (!hasProject()) {
|
|
return;
|
|
}
|
|
|
|
closeFindDialog();
|
|
closeProjectUi();
|
|
designCtlRelease();
|
|
invalidateCompiled();
|
|
prjClose(&sProject);
|
|
updateMainTitle();
|
|
setStatus("Project closed.");
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// closeProjectUi -- destroy every per-project window and buffer. Safe to
|
|
// call when nothing is open (every step checks its own state).
|
|
static void closeProjectUi(void) {
|
|
designerRelease();
|
|
|
|
// Toolbox/properties can exist without a form window (Window menu)
|
|
if (sToolboxWin) {
|
|
tbxDestroy(sAc, sToolboxWin);
|
|
sToolboxWin = NULL;
|
|
}
|
|
|
|
if (sPropsWin) {
|
|
prpDestroy(sAc, sPropsWin);
|
|
sPropsWin = NULL;
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxDestroyWindow(sAc, sCodeWin);
|
|
sCodeWin = NULL;
|
|
sEditor = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
}
|
|
|
|
freeProcBufs();
|
|
clearAllBreakpoints();
|
|
|
|
if (sProjectWin) {
|
|
prjDestroyWindow(sAc, sProjectWin);
|
|
sProjectWin = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// Sort event names: implemented (no brackets) first, then unimplemented
|
|
// ([brackets]), alphabetically within each group.
|
|
|
|
static int cmpEvtPtrs(const void *a, const void *b) {
|
|
const char *sa = *(const char **)a;
|
|
const char *sb = *(const char **)b;
|
|
bool aImpl = (sa[0] != '[');
|
|
bool bImpl = (sb[0] != '[');
|
|
|
|
if (aImpl != bImpl) {
|
|
return aImpl ? -1 : 1;
|
|
}
|
|
|
|
// Skip brackets for alphabetical comparison
|
|
if (sa[0] == '[') {
|
|
sa++;
|
|
}
|
|
|
|
if (sb[0] == '[') {
|
|
sb++;
|
|
}
|
|
|
|
return strcasecmp(sa, sb);
|
|
}
|
|
|
|
|
|
static int cmpStrPtrs(const void *a, const void *b) {
|
|
const char *sa = *(const char **)a;
|
|
const char *sb = *(const char **)b;
|
|
return strcasecmp(sa, sb);
|
|
}
|
|
|
|
|
|
static void compileAndRun(void) {
|
|
// A stopped program that has not unwound yet still runs sCachedModule
|
|
if (sVm) {
|
|
return;
|
|
}
|
|
|
|
if (compileProject()) {
|
|
runModule(sCachedModule);
|
|
}
|
|
}
|
|
|
|
|
|
static bool compileProject(void) {
|
|
// Save all dirty files before compiling if Save on Run is enabled
|
|
if (sWin && sWin->menuBar && wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN)) {
|
|
saveAllModified();
|
|
}
|
|
|
|
// Stash editor/designer state into the project buffers (the compile
|
|
// source) before going busy: a stash failure shows a dialog, and no
|
|
// dialog can be answered while the busy state swallows input.
|
|
if (hasProject() && !stashCurrentFile()) {
|
|
setStatus("Compilation cancelled.");
|
|
return false;
|
|
}
|
|
|
|
clearOutput();
|
|
setStatus("Compiling...");
|
|
dvxSetBusy(sAc, true);
|
|
dvxUpdate(sAc);
|
|
|
|
// Build source: either concatenate project files or use editor contents
|
|
BasProjectSourceT project;
|
|
BasValidatorT validatorCtx;
|
|
const char *src = NULL;
|
|
int32_t srcLen = 0;
|
|
|
|
memset(&project, 0, sizeof(project));
|
|
memset(&validatorCtx, 0, sizeof(validatorCtx));
|
|
|
|
if (hasProject() && prjFileCount(&sProject) > 0) {
|
|
// Concatenate all files (each normalized to the canonical proc
|
|
// layout) with the same BEGINFORM/ENDFORM wrapping bascomp and
|
|
// basrun use, recording where each file lands for the source map.
|
|
// The one parse of each .frm also builds the name -> widget-type
|
|
// map the validator below checks CtrlName.Member against.
|
|
arrfree(sProject.sourceMap);
|
|
sProject.sourceMap = NULL;
|
|
|
|
int32_t failIdx = -1;
|
|
IdeConcatE concat = concatProject(&project, &validatorCtx, true, &sProject.sourceMap, &failIdx);
|
|
|
|
if (concat == ConcatUnreadableE) {
|
|
sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE);
|
|
showCompileError("Compilation failed: cannot read file.");
|
|
} else if (concat == ConcatTooLargeE) {
|
|
sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path);
|
|
showCompileError("Compilation failed: source too large.");
|
|
} else if (concat == ConcatFailedE) {
|
|
setStatus(project.error);
|
|
dvxSetBusy(sAc, false);
|
|
}
|
|
|
|
if (concat != ConcatOkE) {
|
|
basBuildFreeProject(&project);
|
|
basValidateFree(&validatorCtx);
|
|
return false;
|
|
}
|
|
|
|
src = project.source;
|
|
srcLen = project.sourceLen;
|
|
} else {
|
|
// No project files -- compile the full source
|
|
src = getFullSource();
|
|
|
|
if (!src || *src == '\0') {
|
|
setStatus("No source code to compile.");
|
|
dvxSetBusy(sAc, false);
|
|
return false;
|
|
}
|
|
|
|
srcLen = (int32_t)strlen(src);
|
|
}
|
|
|
|
// Reject unknown widget types up front -- the .frm parse records
|
|
// any `Begin <Type> <Name>` whose type isn't registered by a .wgt
|
|
// DXE. Those would silently drop the control at runtime and
|
|
// produce a misleading "control not found" error on first access.
|
|
if (arrlen(validatorCtx.badTypes) > 0) {
|
|
int32_t n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n");
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - IDE_OUTPUT_LINE_RESERVE; i++) {
|
|
BasBadTypeT *b = &validatorCtx.badTypes[i];
|
|
n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName);
|
|
}
|
|
|
|
n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n");
|
|
sOutputLen = n;
|
|
showCompileError("Compilation failed: unknown widget type.");
|
|
basBuildFreeProject(&project);
|
|
basValidateFree(&validatorCtx);
|
|
return false;
|
|
}
|
|
|
|
// The validator lets the compiler reject CtrlName.Member typos at compile
|
|
// time; it falls back silently for dynamically-created controls (not
|
|
// in the map).
|
|
BasCtrlValidatorT validator;
|
|
BasCompileErrorT err;
|
|
|
|
basValidateCallbacks(&validatorCtx, &validator);
|
|
|
|
BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err, ideBusyPump, NULL);
|
|
|
|
basBuildFreeProject(&project);
|
|
basValidateFree(&validatorCtx);
|
|
|
|
if (!mod && err.result != BAS_COMPILE_ERROR) {
|
|
setStatus(err.message);
|
|
dvxSetBusy(sAc, false);
|
|
return false;
|
|
}
|
|
|
|
if (!mod) {
|
|
// Navigation may raise dialogs (a form that cannot be stashed)
|
|
dvxSetBusy(sAc, false);
|
|
|
|
// Translate global error line to local file/line for display
|
|
int32_t errFileIdx = -1;
|
|
int32_t errLocalLine = err.line;
|
|
const char *errFile = "";
|
|
|
|
if (err.line > 0 && prjFileCount(&sProject) > 0) {
|
|
prjMapLine(&sProject, err.line, &errFileIdx, &errLocalLine);
|
|
|
|
if (errFileIdx >= 0 && errFileIdx < prjFileCount(&sProject)) {
|
|
errFile = sProject.files[errFileIdx].path;
|
|
}
|
|
}
|
|
|
|
// Navigate to the error location; the proc it lands in names it
|
|
char procName[IDE_FULL_NAME_BUF] = {0};
|
|
int32_t procLine = errLocalLine;
|
|
|
|
if (err.line > 0 && errFileIdx >= 0) {
|
|
int32_t procIdx = navigateToCodeLine(errFileIdx, errLocalLine, false);
|
|
|
|
// The caret goes to the column the error is at, when known
|
|
if (err.col > 0 && sEditor) {
|
|
wgtTextAreaSetCursor(sEditor, wgtTextAreaGetCursorLine(sEditor), err.col);
|
|
}
|
|
|
|
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
|
|
snprintf(procName, sizeof(procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
|
|
procLine = wgtTextAreaGetCursorLine(sEditor);
|
|
}
|
|
}
|
|
|
|
// Show the error (without its "Line N, Col M: " prefix) with the
|
|
// procedure name and proc-relative line
|
|
const char *msg = basBuildErrorText(err.message);
|
|
|
|
if (procName[0]) {
|
|
sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg);
|
|
} else {
|
|
sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg);
|
|
}
|
|
|
|
showCompileError("Compilation failed.");
|
|
return false;
|
|
}
|
|
|
|
dvxSetBusy(sAc, false);
|
|
|
|
// Cache the compiled module for Ctrl+F5
|
|
if (sCachedModule) {
|
|
basModuleFree(sCachedModule);
|
|
}
|
|
|
|
sCachedModule = mod;
|
|
|
|
// Update Object/Event dropdowns
|
|
updateDropdowns();
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// concatProject -- the compile source of the project into ps: one pass per
|
|
// file kind in compile order -- .bas modules first (so CONST declarations
|
|
// are available), then .cls class modules, then .ctl UserControls, then
|
|
// (withForms) the .frm code sections (see basBuildConcatProject) -- each
|
|
// file's source map entry appended to *map and the forms' controls added
|
|
// to validator (may be NULL). On failure *outFileIdx names the file.
|
|
static IdeConcatE concatProject(BasProjectSourceT *ps, BasValidatorT *validator, bool withForms, BasSourceMapT **map, int32_t *outFileIdx) {
|
|
for (int32_t kind = 0; kind < PrjKindCountE; kind++) {
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (sProject.files[i].kind != kind || (kind == PrjKindFormE && !withForms)) {
|
|
continue;
|
|
}
|
|
|
|
char *ctrlList = NULL;
|
|
char *fileSrc = loadCompileSource(i, validator, &ctrlList);
|
|
|
|
*outFileIdx = i;
|
|
|
|
if (!fileSrc) {
|
|
return ConcatUnreadableE;
|
|
}
|
|
|
|
int32_t fileLen = (int32_t)strlen(fileSrc);
|
|
|
|
// BEGINFORM/ENDFORM directives plus a trailing newline
|
|
if (ps->sourceLen + fileLen + IDE_SOURCE_MARGIN >= IDE_MAX_SOURCE) {
|
|
free(fileSrc);
|
|
free(ctrlList);
|
|
return ConcatTooLargeE;
|
|
}
|
|
|
|
// startLine is recorded AFTER the injected BEGINFORM /
|
|
// BEGINCLASS so the source map lines match what the editor
|
|
// shows.
|
|
BasSourceMapT mapEntry;
|
|
|
|
memset(&mapEntry, 0, sizeof(mapEntry));
|
|
snprintf(mapEntry.path, sizeof(mapEntry.path), "%s", sProject.files[i].path);
|
|
|
|
bool ok = appendCompileFile(ps, i, fileSrc, fileLen, ctrlList, &mapEntry);
|
|
|
|
free(fileSrc);
|
|
free(ctrlList);
|
|
|
|
if (!ok) {
|
|
return ConcatFailedE;
|
|
}
|
|
|
|
arrput(*map, mapEntry);
|
|
dvxUpdate(sAc);
|
|
}
|
|
}
|
|
|
|
return ConcatOkE;
|
|
}
|
|
|
|
|
|
static int32_t countLines(const char *text) {
|
|
if (!text || !text[0]) {
|
|
return 1;
|
|
}
|
|
|
|
int32_t n = 1;
|
|
|
|
for (const char *p = text; *p; p++) {
|
|
if (*p == '\n') {
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return n;
|
|
}
|
|
|
|
|
|
// createEventSubSkeleton -- append an empty "Sub Obj_Evt ... End Sub" to
|
|
// the proc buffers and show it in the editor, with the parameters of the
|
|
// object's event: a control's (its type's, basCtlEventParams), a
|
|
// WithEvents variable's class Event's, or in a .ctl the UserControl
|
|
// object's own (Private: a Public one would be a method of the control).
|
|
// Not marked dirty yet; if the user adds no code it is discarded once the
|
|
// editor leaves it.
|
|
static void createEventSubSkeleton(const char *objName, const char *evtName) {
|
|
char subName[IDE_SKELETON_BUF];
|
|
char skeleton[IDE_SKELETON_BUF];
|
|
char classParams[IDE_EVENT_PARAMS_BUF];
|
|
char arrayParams[IDE_EVENT_PARAMS_BUF];
|
|
const char *objType = objectType(objName);
|
|
bool ucObject = strcmp(objType, BAS_UC_OBJECT) == 0;
|
|
const char *params = basCtlEventParams(objType[0] ? objType : NULL, evtName);
|
|
const IdeWithEventsT *withEvents = findWithEvents(objName);
|
|
|
|
// A WithEvents variable's handler takes its class Event's parameters
|
|
if (withEvents) {
|
|
IdeClassEventT *events = NULL;
|
|
|
|
classEventsOf(withEvents->className, &events);
|
|
classParams[0] = '\0';
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(events); i++) {
|
|
if (strcasecmp(events[i].name, evtName) == 0) {
|
|
snprintf(classParams, sizeof(classParams), "%s", events[i].params);
|
|
}
|
|
}
|
|
|
|
arrfree(events);
|
|
params = classParams;
|
|
}
|
|
|
|
snprintf(subName, sizeof(subName), "%s" BAS_EVENT_HANDLER_SEP "%s", objName, evtName);
|
|
|
|
// Control-array handlers take a leading Index argument
|
|
if (!ucObject && isCtrlArrayInDesigner(objName)) {
|
|
snprintf(arrayParams, sizeof(arrayParams), "Index As Integer%s%s", params[0] ? ", " : "", params);
|
|
params = arrayParams;
|
|
}
|
|
|
|
snprintf(skeleton, sizeof(skeleton), PRJ_HANDLER_FMT, ucObject ? PRJ_PRIVATE : "", subName, params);
|
|
|
|
char *buf = strdup(skeleton);
|
|
|
|
if (!buf) {
|
|
return;
|
|
}
|
|
|
|
arrput(sProcBufs, buf);
|
|
|
|
// Show the new procedure (it's the last one); showProc stashes the
|
|
// outgoing editor text and adjusts the index if that discarded an
|
|
// earlier empty skeleton. Then refresh sProcTable so breakpoints
|
|
// and line translation know about the new proc.
|
|
showProc((int32_t)arrlen(sProcBufs) - 1);
|
|
joinProcBufs();
|
|
snprintf(sSkeletonName, sizeof(sSkeletonName), "%s", subName);
|
|
}
|
|
|
|
|
|
// ctlHeaders -- the header of every UserControl of the project (its text
|
|
// up to the class code: VERSION and the Begin UserControl ... End block),
|
|
// in project order, the way CTLn resources and the designer take them.
|
|
// The text is the same the compile used (loaded buffers, disk only for
|
|
// never-loaded files). stb_ds array of malloc'd texts (freeTexts).
|
|
static char **ctlHeaders(void) {
|
|
char **headers = NULL;
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (sProject.files[i].kind != PrjKindControlE) {
|
|
continue;
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(i, &owned, &missing);
|
|
int32_t len = text ? basClassCodePos(text, (int32_t)strlen(text)) : 0;
|
|
char *header = text ? (char *)malloc((size_t)len + 1) : NULL;
|
|
|
|
if (header) {
|
|
memcpy(header, text, (size_t)len);
|
|
header[len] = '\0';
|
|
arrput(headers, header);
|
|
}
|
|
|
|
free(owned);
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
|
|
// currentObjName -- the object picked in the code window's Object
|
|
// dropdown ("" when none)
|
|
static void currentObjName(char *out, int32_t outSize) {
|
|
int32_t objIdx = sObjDropdown ? wgtDropdownGetSelected(sObjDropdown) : -1;
|
|
|
|
snprintf(out, (size_t)outSize, "%s", (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) ? sObjItems[objIdx] : "");
|
|
}
|
|
|
|
|
|
// debugNavigateToLine -- show the paused (or failing) statement. The
|
|
// file and line come from the source map; the procedure is picked by
|
|
// line range in that file once it is active.
|
|
static void debugNavigateToLine(int32_t concatLine) {
|
|
if (concatLine <= 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t fileIdx = -1;
|
|
int32_t localLine = concatLine;
|
|
|
|
// prjMapLine returns a line relative to the code section (startLine in
|
|
// the source map is recorded AFTER the injected BEGINFORM directive).
|
|
prjMapLine(&sProject, concatLine, &fileIdx, &localLine);
|
|
|
|
navigateToCodeLine(fileIdx, localLine, true);
|
|
}
|
|
|
|
|
|
// debugResume -- common tail of every continue/step command: the caller
|
|
// has already configured the VM's step state.
|
|
static void debugResume(const char *status) {
|
|
sDbgCurrentLine = -1;
|
|
sDbgState = DbgRunningE;
|
|
basRunHostTagBreak(sDbgFormRt, &sBreakTitles, false);
|
|
|
|
if (sVm) {
|
|
basVmResume(sVm);
|
|
}
|
|
|
|
refreshEditorMarkers();
|
|
|
|
updateProjectMenuState();
|
|
setStatus(status);
|
|
}
|
|
|
|
|
|
static void debugStartOrResume(int32_t cmd) {
|
|
if (sDbgState == DbgPausedE && sVm) {
|
|
// Already paused -- apply the appropriate step command and resume
|
|
switch (cmd) {
|
|
case CMD_STEP_INTO:
|
|
basVmStepInto(sVm);
|
|
break;
|
|
|
|
case CMD_STEP_OVER:
|
|
basVmStepOver(sVm);
|
|
break;
|
|
|
|
case CMD_STEP_OUT:
|
|
basVmStepOut(sVm);
|
|
break;
|
|
|
|
case CMD_RUN_TO_CURSOR:
|
|
if (sEditor) {
|
|
basVmRunToCursor(sVm, localToConcatLine(wgtTextAreaGetCursorLine(sEditor)));
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
debugResume("Running...");
|
|
return;
|
|
}
|
|
|
|
if (sDbgState == DbgIdleE) {
|
|
// Not running -- compile and start in debug mode.
|
|
sDbgEnabled = true;
|
|
sDbgBreakOnStart = true;
|
|
compileAndRun();
|
|
sDbgBreakOnStart = false;
|
|
}
|
|
}
|
|
|
|
|
|
// debugStop -- request the running program to stop. runBody's loops and
|
|
// doEventsCallback observe sStopRequested and unwind; until runBody has
|
|
// returned the state stays DbgStoppingE, so nothing can start a new run
|
|
// or free the module the unwinding VM is still executing.
|
|
static void debugStop(void) {
|
|
sStopRequested = true;
|
|
|
|
// Nothing may pause the VM again while it unwinds
|
|
if (sVm) {
|
|
basVmStop(sVm);
|
|
}
|
|
|
|
basRunHostTagBreak(sDbgFormRt, &sBreakTitles, false);
|
|
sDbgState = sVm ? DbgStoppingE : DbgIdleE;
|
|
sDbgCurrentLine = -1;
|
|
sDbgEnabled = false;
|
|
|
|
refreshEditorMarkers();
|
|
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
static void debugUpdateWindows(void) {
|
|
// Auto-show debug windows if not already open
|
|
if (!sLocalsWin) {
|
|
showLocalsWindow();
|
|
} else if (!sLocalsWin->visible) {
|
|
dvxShowWindow(sAc, sLocalsWin);
|
|
}
|
|
|
|
if (!sCallStackWin) {
|
|
showCallStackWindow();
|
|
} else if (!sCallStackWin->visible) {
|
|
dvxShowWindow(sAc, sCallStackWin);
|
|
}
|
|
|
|
updateLocalsWindow();
|
|
updateCallStackWindow();
|
|
updateWatchWindow();
|
|
}
|
|
|
|
|
|
// dedupeBreakpoints -- drop a breakpoint that lands on the same line of
|
|
// the same file as an earlier one (edits can join lines). Returns true
|
|
// if any was dropped.
|
|
static bool dedupeBreakpoints(void) {
|
|
bool changed = false;
|
|
|
|
for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i > 0; i--) {
|
|
for (int32_t j = 0; j < i; j++) {
|
|
if (sBreakpoints[j].fileIdx == sBreakpoints[i].fileIdx && sBreakpoints[j].codeLine == sBreakpoints[i].codeLine) {
|
|
arrdel(sBreakpoints, i);
|
|
changed = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
|
|
// deleteSelectedControl -- remove the designer's selected control and
|
|
// refresh the properties panel / form window when something changed.
|
|
static void deleteSelectedControl(void) {
|
|
if (!sDesigner.form || sDesigner.selectedIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
dsgnOnKey(&sDesigner, KEY_DELETE);
|
|
int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
if (newCount != prevCount) {
|
|
sDesigner.form->dirty = true;
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
|
|
updateDirtyIndicators();
|
|
}
|
|
}
|
|
|
|
|
|
// designCtlRefresh -- the project's UserControls for the designer: their
|
|
// code (with the modules and class modules it may use, not the forms)
|
|
// compiled and registered (basCtlLoadDesign) in place of the set before,
|
|
// so the toolbox offers them and the forms being designed hold live
|
|
// instances that run the code with UserMode False. The designer's
|
|
// widgets, the toolbox and the properties window follow. A project whose
|
|
// controls do not compile has none in the designer until they do (the
|
|
// status line says so); while code of the old set runs nothing changes.
|
|
static void designCtlRefresh(void) {
|
|
char **headers = ctlHeaders();
|
|
int32_t count = (int32_t)arrlen(headers);
|
|
BasModuleT *module = NULL;
|
|
BasProjectSourceT ps;
|
|
BasSourceMapT *map = NULL;
|
|
int32_t failIdx = -1;
|
|
|
|
memset(&ps, 0, sizeof(ps));
|
|
|
|
if (count > 0 && concatProject(&ps, NULL, false, &map, &failIdx) == ConcatOkE) {
|
|
BasCompileErrorT err;
|
|
|
|
module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err, NULL, NULL);
|
|
}
|
|
|
|
basBuildFreeProject(&ps);
|
|
arrfree(map);
|
|
|
|
if (!basCtlUnloadDesign(NULL)) {
|
|
basModuleFree(module);
|
|
freeTexts(headers);
|
|
return;
|
|
}
|
|
|
|
basModuleFree(sDesignModule);
|
|
sDesignModule = module;
|
|
|
|
int32_t registered = module ? basCtlLoadDesign(NULL, module, (const char *const *)headers, count, sProject.projectPath) : 0;
|
|
|
|
freeTexts(headers);
|
|
|
|
if (count > 0 && !module) {
|
|
setStatus("The project's UserControls do not compile; the designer cannot show them until they do (compile with F5 to see why).");
|
|
} else if (registered < count) {
|
|
setStatus("A UserControl of the project has the name of an installed control; the designer cannot show it.");
|
|
}
|
|
|
|
if (sFormWin && sDesigner.form) {
|
|
dsgnRebuildWidgets(&sDesigner);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
|
|
if (sToolboxWin) {
|
|
tbxDestroy(sAc, sToolboxWin);
|
|
sToolboxWin = NULL;
|
|
showToolbox();
|
|
}
|
|
|
|
if (sPropsWin) {
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
}
|
|
}
|
|
|
|
|
|
// designCtlRelease -- the designer lets the project's UserControls go
|
|
// (the project closes). While their code runs (a message box of theirs
|
|
// is up) they stay, and so does their module.
|
|
static void designCtlRelease(void) {
|
|
if (basCtlUnloadDesign(NULL)) {
|
|
basModuleFree(sDesignModule);
|
|
sDesignModule = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// designerEditCmd -- an Edit menu command applied to the designer's
|
|
// selected control
|
|
static void designerEditCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_CUT:
|
|
dsgnCopySelected();
|
|
deleteSelectedControl();
|
|
break;
|
|
|
|
case CMD_COPY:
|
|
dsgnCopySelected();
|
|
break;
|
|
|
|
case CMD_PASTE:
|
|
dsgnPasteControl();
|
|
break;
|
|
|
|
case CMD_DELETE:
|
|
deleteSelectedControl();
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// designerRelease -- close the designer window and unload its form
|
|
static void designerRelease(void) {
|
|
teardownFormWin();
|
|
dsgnFree(&sDesigner);
|
|
sDesignerFileIdx = -1;
|
|
}
|
|
|
|
|
|
static bool doEventsCallback(void *ctx) {
|
|
(void)ctx;
|
|
|
|
// Stop if IDE window was closed, DVX is shutting down, or the user
|
|
// pressed Stop (the VM's own pause spin only exits on false here).
|
|
if (!sWin || !sAc->running || sStopRequested) {
|
|
return false;
|
|
}
|
|
|
|
idePump();
|
|
|
|
return sWin != NULL && sAc->running && !sStopRequested;
|
|
}
|
|
|
|
|
|
static void dsgnCopySelected(void) {
|
|
if (!sDesigner.form || sDesigner.selectedIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
if (sDesigner.selectedIdx >= count) {
|
|
return;
|
|
}
|
|
|
|
// The selected control and its children as .frm text, by the writer
|
|
// the .frm saver uses
|
|
int32_t len = 0;
|
|
char *buf = frmModelCtrlText(sDesigner.form, sDesigner.form->controls[sDesigner.selectedIdx], &len);
|
|
|
|
if (buf) {
|
|
dvxClipboardCopy(buf, len);
|
|
} else {
|
|
setStatus("A property value of this control is too long to copy.");
|
|
}
|
|
|
|
free(buf);
|
|
}
|
|
|
|
|
|
// dsgnPasteControl -- paste a control (and its children) copied with
|
|
// dsgnCopySelected. The clipboard blocks are read by the .frm model's
|
|
// loader, so every property comes back exactly as a saved form would.
|
|
static void dsgnPasteControl(void) {
|
|
if (!sDesigner.form || !sDesigner.contentBox) {
|
|
return;
|
|
}
|
|
|
|
int32_t clipLen = 0;
|
|
const char *clip = dvxClipboardGet(&clipLen);
|
|
|
|
if (!clip || clipLen <= 0 || !kwMatch(dvxSkipWs(clip), FRM_BEGIN_TAG)) {
|
|
return;
|
|
}
|
|
|
|
FrmModelT *pasted = frmModelCtrlsFromText(clip, clipLen);
|
|
|
|
if (!pasted) {
|
|
return;
|
|
}
|
|
|
|
// A control with the same name exists: continue its control array,
|
|
// offer to start one, or rename the paste
|
|
FrmCtrlT *src = pasted->controls[0];
|
|
char newName[FRM_MAX_NAME];
|
|
int32_t newIndex = FRM_NO_INDEX;
|
|
bool nameExists = false;
|
|
int32_t highIdx = FRM_NO_INDEX;
|
|
int32_t existCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
for (int32_t i = 0; i < existCount; i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i]->name, src->name) == 0) {
|
|
nameExists = true;
|
|
|
|
if (sDesigner.form->controls[i]->index > highIdx) {
|
|
highIdx = sDesigner.form->controls[i]->index;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nameExists && highIdx != FRM_NO_INDEX) {
|
|
snprintf(newName, sizeof(newName), "%s", src->name);
|
|
newIndex = highIdx + 1;
|
|
} else if (nameExists && dvxMessageBox(sAc, "Paste", "A control with this name already exists.\nCreate a control array?", MB_YESNO | MB_ICONQUESTION) == ID_YES) {
|
|
// The existing control (and its children's parent link) becomes index 0
|
|
frmModelMakeArrayMember(sDesigner.form, sDesigner.form->controls[frmModelFindCtrl(sDesigner.form, src->name, FRM_NO_INDEX)], 0);
|
|
snprintf(newName, sizeof(newName), "%s", src->name);
|
|
newIndex = 1;
|
|
} else {
|
|
frmModelAutoName(sDesigner.form, src->typeName, newName, sizeof(newName));
|
|
}
|
|
|
|
FrmCtrlT *ctrl = frmModelInsertTree(sDesigner.form, pasted, src, NULL, newName, newIndex);
|
|
|
|
frmModelFree(pasted);
|
|
|
|
if (!ctrl) {
|
|
return;
|
|
}
|
|
|
|
// Widgets for the pasted controls (the others have theirs)
|
|
dsgnCreateWidgets(&sDesigner, sDesigner.contentBox);
|
|
|
|
sDesigner.selectedIdx = frmModelCtrlPos(sDesigner.form, ctrl);
|
|
|
|
prpRebuildTree(&sDesigner);
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (sFormWin) {
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
|
|
// editCmdKey -- the Ctrl key a text window handles for an Edit command
|
|
static int32_t editCmdKey(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_CUT:
|
|
return KEY_CTRL_X;
|
|
|
|
case CMD_COPY:
|
|
return KEY_CTRL_C;
|
|
|
|
case CMD_PASTE:
|
|
return KEY_CTRL_V;
|
|
|
|
case CMD_SELECT_ALL:
|
|
return KEY_CTRL_A;
|
|
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
// editorIsControl -- true when the editor holds a UserControl (.ctl):
|
|
// its object is UserControl, with the UserControl_* events
|
|
static bool editorIsControl(void) {
|
|
return sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject) && sProject.files[sEditorFileIdx].kind == PrjKindControlE;
|
|
}
|
|
|
|
|
|
// Converts a 1-based editor line (within the current proc's buffer) to its
|
|
// 1-based line in the file's full code. For (General)/no-proc context
|
|
// (sCurProcIdx out of range) the editor line is the code line.
|
|
static int32_t editorLineToCodeLine(int32_t editorLine) {
|
|
if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
|
|
return sProcTable[sCurProcIdx].lineNum + editorLine - 1;
|
|
}
|
|
|
|
return editorLine;
|
|
}
|
|
|
|
|
|
// editorSetText -- replace the editor text as a load, not a user edit, and
|
|
// resync everything derived from it. A section longer than the editor
|
|
// holds is shown read-only (and never stored back) instead of being
|
|
// silently truncated. caretLine: a 1-based line to put the caret at
|
|
// (start of line, nothing selected), IDE_CARET_KEEP for its current line
|
|
// number, or IDE_CARET_LOAD.
|
|
static void editorSetText(const char *text, int32_t caretLine) {
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
if (caretLine == IDE_CARET_KEEP) {
|
|
caretLine = wgtTextAreaGetCursorLine(sEditor);
|
|
}
|
|
|
|
void (*savedOnChange)(WidgetT *) = sEditor->onChange;
|
|
sEditor->onChange = NULL;
|
|
wgtSetText(sEditor, text);
|
|
sEditor->onChange = savedOnChange;
|
|
|
|
sEditorTruncated = strlen(text) >= IDE_MAX_SOURCE;
|
|
sEditor->readOnly = sEditorTruncated;
|
|
|
|
if (sEditorTruncated) {
|
|
setStatus("This section is too large to edit and is shown read-only.");
|
|
}
|
|
|
|
editorSnapshot(countLines(wgtGetText(sEditor)));
|
|
refreshEditorMarkers();
|
|
|
|
if (caretLine > IDE_CARET_LOAD) {
|
|
wgtTextAreaSetCursor(sEditor, caretLine, 1);
|
|
}
|
|
}
|
|
|
|
|
|
// editorSnapshot -- remember the editor's line count (lineCount, the
|
|
// caller has it) and, while the editor's file has breakpoints, its text:
|
|
// the base the next edit's breakpoint shift is computed against. With
|
|
// no breakpoints no per-keystroke copy is made; adding one snapshots.
|
|
static void editorSnapshot(int32_t lineCount) {
|
|
sEditorLineCount = lineCount;
|
|
|
|
bool hasBreakpoints = false;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints) && !hasBreakpoints; i++) {
|
|
hasBreakpoints = sBreakpoints[i].fileIdx == sEditorFileIdx;
|
|
}
|
|
|
|
if (!hasBreakpoints) {
|
|
free(sEditorPrevText);
|
|
sEditorPrevText = NULL;
|
|
return;
|
|
}
|
|
|
|
const char *text = wgtGetText(sEditor);
|
|
size_t len = text ? strlen(text) : 0;
|
|
char *copy = (char *)realloc(sEditorPrevText, len + 1);
|
|
|
|
if (!copy) {
|
|
free(sEditorPrevText);
|
|
sEditorPrevText = NULL;
|
|
return;
|
|
}
|
|
|
|
memcpy(copy, text ? text : "", len);
|
|
copy[len] = '\0';
|
|
sEditorPrevText = copy;
|
|
}
|
|
|
|
|
|
// Auto-create an implicit project when opening a file without one.
|
|
// Derives project name and directory from the file path. Also adds
|
|
// a matching .frm file if one exists alongside the .bas.
|
|
|
|
static void ensureProject(const char *filePath) {
|
|
if (hasProject()) {
|
|
return;
|
|
}
|
|
|
|
// Derive directory and base name from path
|
|
char dir[DVX_MAX_PATH];
|
|
char baseName[PRJ_MAX_NAME];
|
|
snprintf(dir, sizeof(dir), "%s", filePath);
|
|
|
|
char *sep = platformPathDirEnd(dir);
|
|
|
|
const char *fileName = filePath;
|
|
|
|
if (sep) {
|
|
fileName = sep + 1;
|
|
*sep = '\0';
|
|
} else {
|
|
dir[0] = '.';
|
|
dir[1] = '\0';
|
|
}
|
|
|
|
// Strip extension for project name
|
|
// Length-clamped memcpy instead of strncpy/snprintf because
|
|
// GCC warns about both when source (DVX_MAX_PATH) exceeds
|
|
// the buffer (PRJ_MAX_NAME), even though truncation is safe.
|
|
int32_t nl = (int32_t)strlen(fileName);
|
|
|
|
if (nl >= PRJ_MAX_NAME) {
|
|
nl = PRJ_MAX_NAME - 1;
|
|
}
|
|
|
|
memcpy(baseName, fileName, nl);
|
|
baseName[nl] = '\0';
|
|
|
|
char *dot = strrchr(baseName, '.');
|
|
|
|
if (dot) {
|
|
*dot = '\0';
|
|
}
|
|
|
|
prjNew(&sProject, baseName, dir);
|
|
prjApplyDefaults(&sProject, sPrefs);
|
|
|
|
PrjFileKindE kind = prjKindForPath(filePath);
|
|
|
|
prjAddFile(&sProject, fileName, kind);
|
|
|
|
// If it's a .bas, check for a matching .frm and add it too. Only the
|
|
// file-name part's extension is replaced (a directory may contain dots).
|
|
if (kind == PrjKindModuleE) {
|
|
char frmPath[DVX_MAX_PATH];
|
|
int32_t baseOff = (int32_t)(platformPathBaseName(filePath) - filePath);
|
|
|
|
snprintf(frmPath, sizeof(frmPath), "%s", filePath);
|
|
|
|
char *frmDot = strrchr(frmPath + baseOff, '.');
|
|
int32_t stemLen = frmDot ? (int32_t)(frmDot - frmPath) : (int32_t)strlen(frmPath);
|
|
|
|
if (stemLen + (int32_t)sizeof(PRJ_FRM_EXT) <= (int32_t)sizeof(frmPath)) {
|
|
snprintf(frmPath + stemLen, sizeof(frmPath) - stemLen, "%s", PRJ_FRM_EXT);
|
|
|
|
if (prjPathExists(frmPath)) {
|
|
prjAddFile(&sProject, platformPathBaseName(frmPath), PrjKindFormE);
|
|
}
|
|
}
|
|
}
|
|
|
|
sProject.dirty = false;
|
|
sProject.activeFileIdx = -1;
|
|
|
|
prjLoadAllFiles(&sProject, sAc);
|
|
updateMainTitle();
|
|
setStatus("Project created.");
|
|
designCtlRefresh();
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// ensureProjectWindow -- create the Project Explorer if needed (wired to
|
|
// the shared menu/accelerator handlers), otherwise refresh its tree.
|
|
static void ensureProjectWindow(bool raise) {
|
|
if (sProjectWin) {
|
|
prjRebuildTree(&sProject);
|
|
} else {
|
|
sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileDblClick, updateProjectMenuState);
|
|
|
|
if (!sProjectWin) {
|
|
return;
|
|
}
|
|
|
|
sProjectWin->y = toolbarBottom() + IDE_PROJECT_WIN_OFFSET;
|
|
sProjectWin->onClose = onProjectWinClose;
|
|
sProjectWin->onMenu = onMenu;
|
|
sProjectWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
|
|
if (raise) {
|
|
dvxRaiseWindow(sAc, sProjectWin);
|
|
}
|
|
}
|
|
|
|
|
|
// evaluateImmediate -- run a line typed into the Immediate window (an
|
|
// assignment to the paused program's variable, a statement, or an
|
|
// expression to print); an assignment refreshes the debug windows.
|
|
static void evaluateImmediate(const char *expr) {
|
|
bool assigned = basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL, &sBusyPump);
|
|
|
|
ideBusyPumpEnd();
|
|
|
|
if (assigned) {
|
|
updateLocalsWindow();
|
|
updateWatchWindow();
|
|
}
|
|
}
|
|
|
|
|
|
// evtItemAdd -- append an owned Event dropdown label; bracketed marks an
|
|
// event that has no code yet
|
|
static void evtItemAdd(const char *name, bool bracketed) {
|
|
char label[BAS_MAX_IDENT + sizeof("[]") - 1];
|
|
|
|
snprintf(label, sizeof(label), bracketed ? "[%s]" : "%s", name);
|
|
|
|
char *copy = strdup(label);
|
|
|
|
if (copy) {
|
|
arrput(sEvtItems, copy);
|
|
}
|
|
}
|
|
|
|
|
|
// evtItemsReset -- empty the Event dropdown labels (they are owned)
|
|
static void evtItemsReset(void) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sEvtItems); i++) {
|
|
free((void *)sEvtItems[i]);
|
|
}
|
|
|
|
arrsetlen(sEvtItems, 0);
|
|
}
|
|
|
|
|
|
// evtLabelMatches -- compare an Event dropdown label against an event
|
|
// name, ignoring the [] brackets that mark unimplemented events.
|
|
|
|
static bool evtLabelMatches(const char *label, const char *evtName) {
|
|
if (label[0] == '[') {
|
|
size_t nameLen = strlen(evtName);
|
|
|
|
return strncasecmp(label + 1, evtName, nameLen) == 0 && label[nameLen + 1] == ']' && label[nameLen + 2] == '\0';
|
|
}
|
|
|
|
return strcasecmp(label, evtName) == 0;
|
|
}
|
|
|
|
|
|
// fileCodeContains -- true if the code of project file fileIdx (a .frm's
|
|
// code section, not its layout) contains needle
|
|
static bool fileCodeContains(int32_t fileIdx, const char *needle, int32_t needleLen, bool caseSensitive) {
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(fileIdx, &owned, &missing);
|
|
IdeFrmScanT scan;
|
|
|
|
scan.validator = NULL;
|
|
scan.code = NULL;
|
|
|
|
if (text && sProject.files[fileIdx].kind == PrjKindFormE) {
|
|
frmScan(text, &scan);
|
|
text = scan.code;
|
|
}
|
|
|
|
bool found = procBufContains(text, needle, needleLen, caseSensitive);
|
|
|
|
free(scan.code);
|
|
free(owned);
|
|
return found;
|
|
}
|
|
|
|
|
|
// findDialogBuild -- build the Find / Replace widgets into sFindWin.
|
|
// Returns false if any widget could not be made.
|
|
static bool findDialogBuild(bool showReplace) {
|
|
// Radio order is the FindScopeE / FindDirE order
|
|
static const char *scopeNames[ScopeCountE] = {
|
|
[ScopeFuncE] = "Function",
|
|
[ScopeObjE] = "Object",
|
|
[ScopeFileE] = "File",
|
|
[ScopeProjE] = "Project"
|
|
};
|
|
static const char *dirNames[FindDirCountE] = {
|
|
[FindDirFwdE] = "Forward",
|
|
[FindDirBackE] = "Backward"
|
|
};
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sFindWin);
|
|
WidgetT *findRow = root ? wgtHBox(root) : NULL;
|
|
|
|
sFindInput = (findRow && wgtLabel(findRow, "Find:")) ? wgtTextInput(findRow, IDE_FIND_TEXT_BUF) : NULL;
|
|
|
|
WidgetT *replRow = sFindInput ? wgtHBox(root) : NULL;
|
|
|
|
sReplCheck = replRow ? wgtCheckbox(replRow, "Replace:") : NULL;
|
|
sReplInput = sReplCheck ? wgtTextInput(replRow, IDE_FIND_TEXT_BUF) : NULL;
|
|
|
|
// Options row: scope + direction + case
|
|
WidgetT *optRow = sReplInput ? wgtHBox(root) : NULL;
|
|
WidgetT *scopeFrame = optRow ? wgtFrame(optRow, "Scope") : NULL;
|
|
WidgetT *scopeBox = scopeFrame ? wgtVBox(scopeFrame) : NULL;
|
|
|
|
sScopeGroup = scopeBox ? wgtRadioGroup(scopeBox) : NULL;
|
|
|
|
WidgetT *dirFrame = sScopeGroup ? wgtFrame(optRow, "Direction") : NULL;
|
|
WidgetT *dirBox = dirFrame ? wgtVBox(dirFrame) : NULL;
|
|
|
|
sDirGroup = dirBox ? wgtRadioGroup(dirBox) : NULL;
|
|
|
|
WidgetT *caseBox = sDirGroup ? wgtVBox(optRow) : NULL;
|
|
|
|
sCaseCheck = caseBox ? wgtCheckbox(caseBox, "Match Case") : NULL;
|
|
|
|
// Buttons
|
|
WidgetT *btnRow = sCaseCheck ? wgtHBox(root) : NULL;
|
|
WidgetT *btnFind = btnRow ? wgtButton(btnRow, "Find Next") : NULL;
|
|
|
|
sBtnReplace = btnFind ? wgtButton(btnRow, "Replace") : NULL;
|
|
sBtnReplAll = sBtnReplace ? wgtButton(btnRow, "Replace All") : NULL;
|
|
|
|
WidgetT *btnClose = sBtnReplAll ? wgtButton(btnRow, "Close") : NULL;
|
|
|
|
if (!btnClose) {
|
|
return false;
|
|
}
|
|
|
|
for (int32_t i = 0; i < ScopeCountE; i++) {
|
|
if (!wgtRadio(sScopeGroup, scopeNames[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < FindDirCountE; i++) {
|
|
if (!wgtRadio(sDirGroup, dirNames[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
root->spacing = wgtPixels(IDE_FIND_ROOT_SPACING);
|
|
findRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
sFindInput->weight = WGT_WEIGHT_FILL;
|
|
replRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
sReplCheck->onChange = onReplCheckChange;
|
|
sReplInput->weight = WGT_WEIGHT_FILL;
|
|
optRow->spacing = wgtPixels(IDE_WIDGET_SPACING_LG);
|
|
btnRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
btnRow->align = AlignEndE;
|
|
btnFind->onClick = onFindNext;
|
|
sBtnReplace->onClick = onReplace;
|
|
sBtnReplAll->onClick = onReplaceAll;
|
|
btnClose->onClick = onFindCloseBtn;
|
|
|
|
wgtSetText(sFindInput, sFindText);
|
|
wgtCheckboxSetChecked(sReplCheck, showReplace);
|
|
wgtSetText(sReplInput, sReplaceText);
|
|
|
|
wgtRadioGroupSetSelected(sScopeGroup, ScopeProjE);
|
|
wgtRadioGroupSetSelected(sDirGroup, FindDirFwdE);
|
|
return true;
|
|
}
|
|
|
|
|
|
// findInEditorSections -- search the editor file's sections (General plus
|
|
// every proc) other than fromIdx, in scan order away from it: forward
|
|
// visits the procs after it, General, then the procs before it; backward
|
|
// mirrors that. fromIdx IDE_SECTION_NONE scans every section from the start (forward)
|
|
// or the end (backward). objFilter limits the scan to one object's
|
|
// sections (NULL = all). The first match is shown and selected.
|
|
static bool findInEditorSections(int32_t fromIdx, const char *needle, bool caseSensitive, bool forward, const char *objFilter) {
|
|
int32_t needleLen = (int32_t)strlen(needle);
|
|
int32_t total = (int32_t)arrlen(sProcBufs) + 1; // General plus every proc
|
|
int32_t curPos = fromIdx + 1; // scan position of fromIdx
|
|
int32_t count = total - 1;
|
|
|
|
if (fromIdx < IDE_SECTION_GENERAL) {
|
|
curPos = forward ? -1 : total;
|
|
count = total;
|
|
}
|
|
|
|
for (int32_t k = 1; k <= count; k++) {
|
|
int32_t pos = forward ? curPos + k : curPos - k;
|
|
|
|
pos = (pos % total + total) % total;
|
|
|
|
int32_t p = pos - 1; // -1 = General
|
|
const char *text = (p < 0) ? sGeneralBuf : sProcBufs[p];
|
|
|
|
if (sectionInScope(p, objFilter) && procBufContains(text, needle, needleLen, caseSensitive)) {
|
|
return showProcAndFind(p, needle, caseSensitive, forward);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// findInProject -- Find Next within scope, starting at the editor's
|
|
// selection: the rest of the shown section, the file's other sections
|
|
// (Function scope skips those; Object scope keeps the selected object's),
|
|
// then for Project scope the other files in order. Only a file whose
|
|
// code really matches is activated. Finally the shown section is
|
|
// searched again from its far end, so the search wraps to its origin.
|
|
static bool findInProject(const char *needle, bool caseSensitive, bool forward, FindScopeE scope) {
|
|
if (!needle || !needle[0] || !sEditor) {
|
|
return false;
|
|
}
|
|
|
|
int32_t needleLen = (int32_t)strlen(needle);
|
|
char objName[BAS_MAX_IDENT];
|
|
|
|
// The proc buffers and sProcTable must describe the current editor text
|
|
getFullSource();
|
|
currentObjName(objName, sizeof(objName));
|
|
|
|
if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward)) {
|
|
return true;
|
|
}
|
|
|
|
if (scope != ScopeFuncE && findInEditorSections(sCurProcIdx, needle, caseSensitive, forward, scope == ScopeObjE ? objName : NULL)) {
|
|
return true;
|
|
}
|
|
|
|
int32_t fileCount = prjFileCount(&sProject);
|
|
|
|
// Leaving the file: the other files are searched in their project
|
|
// buffers, so the editor's and the designer's state go there first.
|
|
if (scope == ScopeProjE && fileCount > 1) {
|
|
int32_t step = forward ? 1 : -1;
|
|
int32_t startFile = sEditorFileIdx >= 0 ? sEditorFileIdx : sProject.activeFileIdx;
|
|
|
|
if (!stashCurrentFile()) {
|
|
return false;
|
|
}
|
|
|
|
for (int32_t k = 1; startFile >= 0 && k < fileCount; k++) {
|
|
int32_t fileIdx = ((startFile + step * k) % fileCount + fileCount) % fileCount;
|
|
|
|
if (!fileCodeContains(fileIdx, needle, needleLen, caseSensitive)) {
|
|
continue;
|
|
}
|
|
|
|
if (!activateFile(fileIdx, ViewCodeE)) {
|
|
return false;
|
|
}
|
|
|
|
return findInEditorSections(IDE_SECTION_NONE, needle, caseSensitive, forward, NULL);
|
|
}
|
|
}
|
|
|
|
// Wrap around within the shown section
|
|
if (!forward) {
|
|
wgtTextAreaCursorToEnd(sEditor);
|
|
return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward);
|
|
}
|
|
|
|
// Caret to the start without a selection: a forward search then finds
|
|
// a match that begins right there.
|
|
wgtTextAreaSetCursor(sEditor, 1, 1);
|
|
return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward);
|
|
}
|
|
|
|
|
|
// Case-insensitive strstr replacement (strcasestr is a GNU extension)
|
|
static const char *findSubstrNoCase(const char *haystack, const char *needle, int32_t needleLen) {
|
|
for (; *haystack; haystack++) {
|
|
if (strncasecmp(haystack, needle, needleLen) == 0) {
|
|
return haystack;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// findWithEvents -- the WithEvents variable of the code window's file
|
|
// called name, NULL when there is none
|
|
static const IdeWithEventsT *findWithEvents(const char *name) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sWithEvents); i++) {
|
|
if (strcasecmp(sWithEvents[i].name, name) == 0) {
|
|
return &sWithEvents[i];
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// flushOutput -- show buffered program output in the Output pane,
|
|
// scrolled to the newest line
|
|
static void flushOutput(void) {
|
|
if (!sOutputPending) {
|
|
return;
|
|
}
|
|
|
|
sOutputPending = false;
|
|
setOutputText(sOutputBuf);
|
|
|
|
if (sOutput) {
|
|
wgtTextAreaGoToLine(sOutput, countLines(sOutputBuf));
|
|
}
|
|
}
|
|
|
|
|
|
// freeProcBufs -- release all procedure buffers
|
|
|
|
static void freeProcBufs(void) {
|
|
free(sGeneralBuf);
|
|
sGeneralBuf = NULL;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
|
|
free(sProcBufs[i]);
|
|
}
|
|
|
|
arrfree(sProcBufs);
|
|
sProcBufs = NULL;
|
|
sCurProcIdx = IDE_SECTION_NONE;
|
|
sEditorFileIdx = -1;
|
|
sSkeletonName[0] = '\0';
|
|
arrsetlen(sProcTable, 0);
|
|
}
|
|
|
|
|
|
// freeTexts -- free an stb_ds array of malloc'd texts and the array
|
|
static void freeTexts(char **texts) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(texts); i++) {
|
|
free(texts[i]);
|
|
}
|
|
|
|
arrfree(texts);
|
|
}
|
|
|
|
|
|
// frmScan -- one parse of .frm text: collects the code section into
|
|
// scan->code (malloc'd) and, when scan->validator is set, the form's
|
|
// controls for the compile-time member validator.
|
|
static void frmScan(const char *frmText, IdeFrmScanT *scan) {
|
|
basValidateScanFrm(scan->validator, frmText, (int32_t)strlen(frmText), frmScanOnFormEnd, scan);
|
|
}
|
|
|
|
|
|
// frmScanOnFormEnd -- capture the code after the form's End exactly as
|
|
// the designer loads it (frmCodeSection), so compiled line numbers
|
|
// match what the editor shows.
|
|
static void frmScanOnFormEnd(void *userData, const char *trailingSrc, int32_t trailingLen) {
|
|
IdeFrmScanT *scan = (IdeFrmScanT *)userData;
|
|
int32_t codeLen = 0;
|
|
const char *code = frmCodeSection(trailingSrc, trailingLen, &codeLen);
|
|
|
|
if (!code) {
|
|
return;
|
|
}
|
|
|
|
free(scan->code);
|
|
scan->code = (char *)malloc((size_t)codeLen + 1);
|
|
|
|
if (scan->code) {
|
|
memcpy(scan->code, code, (size_t)codeLen);
|
|
scan->code[codeLen] = '\0';
|
|
}
|
|
}
|
|
|
|
|
|
// generalAppend -- (General) text followed by text on new lines, as a
|
|
// malloc'd string (NULL when out of memory)
|
|
static char *generalAppend(const char *general, const char *text) {
|
|
int32_t genLen = general ? (int32_t)strlen(general) : 0;
|
|
int32_t addLen = (int32_t)strlen(text);
|
|
char *merged = (char *)malloc((size_t)genLen + (size_t)addLen + sizeof("\n"));
|
|
|
|
if (!merged) {
|
|
return NULL;
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
if (genLen > 0) {
|
|
memcpy(merged, general, (size_t)genLen);
|
|
pos = genLen;
|
|
|
|
if (merged[pos - 1] != '\n') {
|
|
merged[pos++] = '\n';
|
|
}
|
|
}
|
|
|
|
memcpy(merged + pos, text, (size_t)addLen + 1);
|
|
return merged;
|
|
}
|
|
|
|
|
|
static bool getFindForward(void) {
|
|
return !sDirGroup || wgtRadioGroupGetSelected(sDirGroup) != FindDirBackE;
|
|
}
|
|
|
|
|
|
static bool getFindMatchCase(void) {
|
|
return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck);
|
|
}
|
|
|
|
|
|
static FindScopeE getFindScope(void) {
|
|
int32_t idx = sScopeGroup ? wgtRadioGroupGetSelected(sScopeGroup) : ScopeProjE;
|
|
|
|
return (idx >= 0 && idx < ScopeCountE) ? (FindScopeE)idx : ScopeProjE;
|
|
}
|
|
|
|
|
|
// getFullSource -- save the editor and reassemble all buffers into one
|
|
// source string. Returns a pointer into an internal cache that is owned
|
|
// by joinProcBufs and freed on the next call. Callers must NOT free it;
|
|
// strdup it if the result must outlive the next call.
|
|
|
|
static const char *getFullSource(void) {
|
|
saveCurProc(false);
|
|
return joinProcBufs();
|
|
}
|
|
|
|
|
|
static WindowT *getLastFocusWin(void) {
|
|
if (sLastFocusWin && (sLastFocusWin == sCodeWin || sLastFocusWin == sOutWin || sLastFocusWin == sImmWin || sLastFocusWin == sFormWin)) {
|
|
return sLastFocusWin;
|
|
}
|
|
|
|
sLastFocusWin = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
|
|
// handleEditCmd -- Edit menu. Cut/Copy/Paste/Select All/Delete act on the
|
|
// last focused content window: the designer's selected control, or the
|
|
// text of the code, output or immediate window.
|
|
static void handleEditCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_CUT:
|
|
case CMD_COPY:
|
|
case CMD_PASTE:
|
|
case CMD_SELECT_ALL:
|
|
case CMD_DELETE: {
|
|
WindowT *target = getLastFocusWin();
|
|
|
|
if (target && target == sFormWin) {
|
|
designerEditCmd(cmd);
|
|
} else if (target && target->onKey) {
|
|
if (cmd == CMD_DELETE) {
|
|
target->onKey(target, KEY_DELETE, 0);
|
|
} else {
|
|
target->onKey(target, editCmdKey(cmd), ACCEL_CTRL);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case CMD_FIND:
|
|
openFindDialog(false);
|
|
break;
|
|
|
|
case CMD_FIND_NEXT:
|
|
if (sFindText[0]) {
|
|
onFindNext(NULL);
|
|
} else {
|
|
openFindDialog(false);
|
|
}
|
|
break;
|
|
|
|
case CMD_REPLACE:
|
|
openFindDialog(true);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Accelerators bypass the menu enabled state, so anything that changes
|
|
// the project or the compiled module is gated on the program being idle.
|
|
static void handleFileCmd(int32_t cmd) {
|
|
bool isIdle = (sDbgState == DbgIdleE);
|
|
|
|
switch (cmd) {
|
|
case CMD_OPEN:
|
|
if (isIdle) {
|
|
loadFile();
|
|
}
|
|
break;
|
|
|
|
case CMD_SAVE:
|
|
saveFile();
|
|
break;
|
|
|
|
case CMD_SAVE_ALL:
|
|
if (saveAllModified()) {
|
|
setStatus("All files saved.");
|
|
}
|
|
break;
|
|
|
|
case CMD_MAKE_EXE:
|
|
if (isIdle) {
|
|
makeExecutable();
|
|
}
|
|
break;
|
|
|
|
case CMD_MAKE_PKG:
|
|
if (isIdle) {
|
|
makePackage();
|
|
}
|
|
break;
|
|
|
|
case CMD_EXIT:
|
|
if (sWin) {
|
|
onClose(sWin);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Recent files
|
|
if (isIdle && cmd >= CMD_RECENT_BASE && cmd < CMD_RECENT_BASE + CMD_RECENT_MAX) {
|
|
recentOpen(cmd - CMD_RECENT_BASE);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleProjectCmd(int32_t cmd) {
|
|
bool isIdle = (sDbgState == DbgIdleE);
|
|
|
|
switch (cmd) {
|
|
case CMD_PRJ_NEW:
|
|
if (isIdle) {
|
|
newProject();
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_OPEN:
|
|
if (isIdle) {
|
|
openProject();
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_SAVE:
|
|
if (hasProject() && saveProjectFile()) {
|
|
setStatus("Project saved.");
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_CLOSE:
|
|
if (isIdle && promptAndSave()) {
|
|
closeProject();
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_PROPS:
|
|
if (hasProject()) {
|
|
if (prjPropertiesDialog(sAc, &sProject, sCtx->appPath)) {
|
|
updateMainTitle();
|
|
updateProjectMenuState();
|
|
|
|
if (sProjectWin) {
|
|
prjRebuildTree(&sProject);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_NEW_CLASS:
|
|
if (isIdle && hasProject()) {
|
|
newClassFile(PrjKindClassE);
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_NEW_CTL:
|
|
if (isIdle && hasProject()) {
|
|
newClassFile(PrjKindControlE);
|
|
}
|
|
break;
|
|
|
|
case CMD_PRJ_RENAME: {
|
|
int32_t renIdx = prjGetSelectedFileIdx();
|
|
|
|
if (isIdle && renIdx >= 0 && renIdx < prjFileCount(&sProject)) {
|
|
renameProjectFile(renIdx);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case CMD_PRJ_REMOVE: {
|
|
int32_t rmIdx = prjGetSelectedFileIdx();
|
|
|
|
if (!isIdle || rmIdx < 0 || rmIdx >= prjFileCount(&sProject)) {
|
|
break;
|
|
}
|
|
|
|
PrjFileT *rmFile = &sProject.files[rmIdx];
|
|
char rmMsg[IDE_MSG_BUF];
|
|
snprintf(rmMsg, sizeof(rmMsg), "Remove %s from the project?", rmFile->path);
|
|
|
|
if (dvxMessageBox(sAc, "Remove File", rmMsg, MB_YESNO | MB_ICONQUESTION) != ID_YES) {
|
|
break;
|
|
}
|
|
|
|
if ((rmIdx == sEditorFileIdx || rmIdx == sDesignerFileIdx) && !stashCurrentFile()) {
|
|
break;
|
|
}
|
|
|
|
if (rmFile->modified) {
|
|
int32_t result = dvxPromptSave(sAc, IDE_MAIN_TITLE);
|
|
|
|
if (result == DVX_SAVE_CANCEL) {
|
|
break;
|
|
}
|
|
|
|
if (result == DVX_SAVE_YES && !writeProjectFile(rmIdx)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Detach the editor/designer from the file before it goes away
|
|
if (rmIdx == sEditorFileIdx) {
|
|
freeProcBufs();
|
|
editorSetText("", IDE_CARET_LOAD);
|
|
} else if (sEditorFileIdx > rmIdx) {
|
|
sEditorFileIdx--;
|
|
}
|
|
|
|
if (rmIdx == sDesignerFileIdx) {
|
|
designerRelease();
|
|
} else if (sDesignerFileIdx > rmIdx) {
|
|
sDesignerFileIdx--;
|
|
}
|
|
|
|
bool rmControl = rmFile->kind == PrjKindControlE;
|
|
|
|
removeBreakpointsForFile(rmIdx);
|
|
prjRemoveFile(&sProject, rmIdx); // adjusts activeFileIdx
|
|
invalidateCompiled();
|
|
updateDropdowns();
|
|
prjRebuildTree(&sProject);
|
|
updateProjectMenuState();
|
|
updateDirtyIndicators();
|
|
|
|
if (rmControl) {
|
|
designCtlRefresh();
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleRunCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_RUN:
|
|
if (sDbgState == DbgPausedE) {
|
|
// Resume from breakpoint -- clear debug mode so it runs free
|
|
sDbgEnabled = false;
|
|
|
|
if (sVm) {
|
|
basVmClearStepState(sVm);
|
|
basVmSetBreakpoints(sVm, NULL, 0);
|
|
}
|
|
|
|
debugResume("Running...");
|
|
} else if (sDbgState == DbgIdleE) {
|
|
sDbgEnabled = false;
|
|
compileAndRun();
|
|
}
|
|
break;
|
|
|
|
case CMD_DEBUG:
|
|
if (sDbgState == DbgPausedE) {
|
|
// Already debugging -- resume, run to next breakpoint
|
|
if (sVm) {
|
|
basVmClearStepState(sVm);
|
|
}
|
|
|
|
debugResume("Debugging...");
|
|
} else if (sDbgState == DbgIdleE) {
|
|
// Start in debug mode with breakpoints
|
|
sDbgEnabled = true;
|
|
compileAndRun();
|
|
}
|
|
break;
|
|
|
|
case CMD_RUN_NOCMP:
|
|
if (sDbgState == DbgIdleE) {
|
|
runCached();
|
|
}
|
|
break;
|
|
|
|
case CMD_STOP:
|
|
if (sDbgState == DbgRunningE || sDbgState == DbgPausedE) {
|
|
debugStop();
|
|
setStatus("Program stopped.");
|
|
}
|
|
break;
|
|
|
|
case CMD_OUTPUT_TO_LOG:
|
|
if (sWin && sWin->menuBar) {
|
|
sOutputToLog = wmMenuItemIsChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG);
|
|
prefsSetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_OUTPUT_TO_LOG, sOutputToLog);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
|
|
case CMD_STEP_INTO:
|
|
case CMD_STEP_OVER:
|
|
case CMD_STEP_OUT:
|
|
case CMD_RUN_TO_CURSOR:
|
|
debugStartOrResume(cmd);
|
|
break;
|
|
|
|
case CMD_TOGGLE_BP:
|
|
toggleBreakpoint();
|
|
break;
|
|
|
|
case CMD_CLEAR:
|
|
clearOutput();
|
|
break;
|
|
|
|
case CMD_SAVE_ON_RUN:
|
|
if (sWin && sWin->menuBar) {
|
|
bool save = wmMenuItemIsChecked(sWin->menuBar, CMD_SAVE_ON_RUN);
|
|
prefsSetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_SAVE_ON_RUN, save);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleViewCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_VIEW_CODE: {
|
|
int32_t selFileIdx = prjGetSelectedFileIdx();
|
|
|
|
// The selected file, else the active one (reopening a closed
|
|
// code window with its code reloaded)
|
|
if (selFileIdx < 0 || selFileIdx >= prjFileCount(&sProject)) {
|
|
selFileIdx = sProject.activeFileIdx;
|
|
}
|
|
|
|
activateFile(selFileIdx, ViewCodeE);
|
|
break;
|
|
}
|
|
|
|
case CMD_VIEW_DESIGN: {
|
|
int32_t selFileIdx = prjGetSelectedFileIdx();
|
|
|
|
// The Shift+F7 accelerator bypasses the menu state: only a form
|
|
// opens in the designer.
|
|
if (selFileIdx >= 0 && selFileIdx < prjFileCount(&sProject) && sProject.files[selFileIdx].kind == PrjKindFormE) {
|
|
activateFile(selFileIdx, ViewDesignE);
|
|
} else if (activeIsForm()) {
|
|
switchToDesign();
|
|
} else {
|
|
setStatus("The active file is not a form.");
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
case CMD_VIEW_TOOLBAR:
|
|
if (sToolbar && sWin->menuBar) {
|
|
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_TOOLBAR);
|
|
sToolbar->visible = show;
|
|
dvxFitWindowH(sAc, sWin);
|
|
prefsSetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_TOOLBAR, show);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
|
|
case CMD_VIEW_STATUS:
|
|
if (sStatusBar && sWin->menuBar) {
|
|
bool show = wmMenuItemIsChecked(sWin->menuBar, CMD_VIEW_STATUS);
|
|
sStatusBar->visible = show;
|
|
dvxFitWindowH(sAc, sWin);
|
|
prefsSetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_STATUSBAR, show);
|
|
prefsSave(sPrefs);
|
|
}
|
|
break;
|
|
|
|
case CMD_MENU_EDITOR: {
|
|
// Activate selected form if not already active
|
|
int32_t selMenuIdx = prjGetSelectedFileIdx();
|
|
|
|
if (selMenuIdx >= 0 && selMenuIdx != sProject.activeFileIdx && selMenuIdx < prjFileCount(&sProject) && sProject.files[selMenuIdx].kind == PrjKindFormE) {
|
|
activateFile(selMenuIdx, ViewDesignE);
|
|
}
|
|
|
|
if (!activeIsForm() || !sDesigner.form) {
|
|
setStatus("The active file is not a form.");
|
|
break;
|
|
}
|
|
|
|
// Snapshot old menu names for rename detection
|
|
char **oldNames = NULL;
|
|
int32_t oldCount = (int32_t)arrlen(sDesigner.form->menuItems);
|
|
|
|
for (int32_t mi = 0; mi < oldCount; mi++) {
|
|
arrput(oldNames, strdup(sDesigner.form->menuItems[mi].name));
|
|
}
|
|
|
|
if (mnuEditorDialog(sAc, sDesigner.form)) {
|
|
sDesigner.form->dirty = true;
|
|
renameMenuItemsInCode(oldNames, oldCount);
|
|
|
|
// Rebuild menu bar preview
|
|
if (sFormWin) {
|
|
wmDestroyMenuBar(sFormWin, &sAc->display);
|
|
dsgnBuildPreviewMenuBar(sFormWin, sDesigner.form);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
|
|
// Rebuild Object dropdown to reflect added/removed/renamed items
|
|
updateDropdowns();
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
for (int32_t mi = 0; mi < oldCount; mi++) {
|
|
free(oldNames[mi]);
|
|
}
|
|
|
|
arrfree(oldNames);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static void handleWindowCmd(int32_t cmd) {
|
|
switch (cmd) {
|
|
case CMD_WIN_CODE:
|
|
// An active file reloads into a reopened window; otherwise an
|
|
// empty scratch editor.
|
|
if (sProject.activeFileIdx >= 0) {
|
|
showCodeView(sProject.activeFileIdx);
|
|
} else {
|
|
showCodeWindow();
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CMD_WIN_OUTPUT:
|
|
showOutputWindow();
|
|
break;
|
|
|
|
case CMD_WIN_IMM:
|
|
showImmediateWindow();
|
|
break;
|
|
|
|
case CMD_WIN_LOCALS:
|
|
showLocalsWindow();
|
|
break;
|
|
|
|
case CMD_WIN_CALLSTACK:
|
|
showCallStackWindow();
|
|
break;
|
|
|
|
case CMD_WIN_WATCH:
|
|
showWatchWindow();
|
|
break;
|
|
|
|
case CMD_WIN_BREAKPOINTS:
|
|
showBreakpointWindow();
|
|
break;
|
|
|
|
case CMD_WIN_TOOLBOX:
|
|
showToolbox();
|
|
break;
|
|
|
|
case CMD_WIN_PROPS:
|
|
showProperties();
|
|
break;
|
|
|
|
case CMD_WIN_PROJECT:
|
|
ensureProjectWindow(true);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
static bool hasProject(void) {
|
|
return sProject.projectPath[0] != '\0';
|
|
}
|
|
|
|
|
|
static bool hasUnsavedData(void) {
|
|
return anyFileModified() || sProject.dirty;
|
|
}
|
|
|
|
|
|
static void helpQueryHandler(void *ctx) {
|
|
(void)ctx;
|
|
|
|
sCtx->helpTopic[0] = '\0';
|
|
|
|
// Restore IDE help file (may have been swapped for a BASIC program's)
|
|
snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sIdeHelpFile);
|
|
|
|
// Determine which window is focused
|
|
WindowT *focusWin = NULL;
|
|
|
|
if (sAc->stack.focusedIdx >= 0 && sAc->stack.focusedIdx < sAc->stack.count) {
|
|
focusWin = sAc->stack.windows[sAc->stack.focusedIdx];
|
|
}
|
|
|
|
// Running BASIC program with a help file: its forms use the program's
|
|
// help (same rules as a compiled program)
|
|
if (sDbgFormRt && sDbgFormRt->helpFile[0] && basFormRtHelpTopic(sDbgFormRt, sCtx->helpTopic, sizeof(sCtx->helpTopic))) {
|
|
snprintf(sCtx->helpFile, sizeof(sCtx->helpFile), "%s", sDbgFormRt->helpFile);
|
|
return;
|
|
}
|
|
|
|
// Code editor / Immediate window: look up the word under the cursor
|
|
if ((focusWin == sCodeWin && sEditor) || (focusWin == sImmWin && sImmediate)) {
|
|
WidgetT *area = (focusWin == sCodeWin) ? sEditor : sImmediate;
|
|
char word[IDE_FULL_NAME_BUF];
|
|
|
|
if (wgtTextAreaGetWordAtCursor(area, word, sizeof(word)) > 0) {
|
|
const char *topic = basHelpKeywordTopic(word);
|
|
|
|
if (topic) {
|
|
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No keyword match -- open the window's own topic
|
|
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", (focusWin == sCodeWin) ? "ide.editor" : "ide.immediate");
|
|
return;
|
|
}
|
|
|
|
// Form designer: help for the selected control type
|
|
if (focusWin == sFormWin && sDesigner.form) {
|
|
if (sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) {
|
|
helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName);
|
|
} else {
|
|
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.form");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Toolbox: help for the active tool
|
|
if (focusWin == sToolboxWin) {
|
|
if (sDesigner.activeTool[0]) {
|
|
helpSetCtrlTopic(sDesigner.activeTool);
|
|
} else {
|
|
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ide.toolbox");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Properties panel
|
|
if (focusWin == sPropsWin) {
|
|
if (sDesigner.form && sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < arrlen(sDesigner.form->controls)) {
|
|
helpSetCtrlTopic(sDesigner.form->controls[sDesigner.selectedIdx]->typeName);
|
|
} else {
|
|
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "ctrl.common.props");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Remaining windows map straight to a topic
|
|
const char *topic = "ide.overview";
|
|
|
|
if (focusWin == sOutWin) {
|
|
topic = "ide.output";
|
|
} else if (focusWin == sProjectWin) {
|
|
topic = "ide.project";
|
|
} else if (focusWin == sLocalsWin) {
|
|
topic = "ide.debug.locals";
|
|
} else if (focusWin == sCallStackWin) {
|
|
topic = "ide.debug.callstack";
|
|
} else if (focusWin == sWatchWin) {
|
|
topic = "ide.debug.watch";
|
|
} else if (focusWin == sBreakpointWin) {
|
|
topic = "ide.debug.breakpoints";
|
|
}
|
|
|
|
snprintf(sCtx->helpTopic, sizeof(sCtx->helpTopic), "%s", topic);
|
|
}
|
|
|
|
|
|
static void helpSetCtrlTopic(const char *typeName) {
|
|
basHelpCtrlTopic(typeName, sCtx->helpTopic, sizeof(sCtx->helpTopic));
|
|
}
|
|
|
|
|
|
// ideBusyPump -- basBuildCompile's unit callback, the debugger's snippet
|
|
// pump (sBusyPump), and Replace All's between procedures: a frame every
|
|
// IDE_BUSY_PUMP_MS, so DVX goes on through a long compile, snippet, or
|
|
// replace. The busy state keeps the user's input out meanwhile; set here
|
|
// when the caller had not (ideBusyPumpEnd clears it).
|
|
static void ideBusyPump(void *ctx) {
|
|
(void)ctx;
|
|
|
|
if (platformClock() - sBusyPumpAt < PLATFORM_MS_TO_TICKS(IDE_BUSY_PUMP_MS)) {
|
|
return;
|
|
}
|
|
|
|
if (!sAc->busy) {
|
|
dvxSetBusy(sAc, true);
|
|
sBusyPumpBusy = true;
|
|
}
|
|
|
|
dvxUpdate(sAc);
|
|
sBusyPumpAt = platformClock();
|
|
}
|
|
|
|
|
|
// ideBusyPumpEnd -- after a step ideBusyPump may have pumped: the busy
|
|
// state it set goes.
|
|
static void ideBusyPumpEnd(void) {
|
|
if (sBusyPumpBusy) {
|
|
dvxSetBusy(sAc, false);
|
|
}
|
|
|
|
sBusyPumpBusy = false;
|
|
}
|
|
|
|
|
|
// ideErrorBox -- modal error message. The busy state swallows all input,
|
|
// so it is cleared first or the box could never be dismissed.
|
|
static void ideErrorBox(const char *msg) {
|
|
dvxSetBusy(sAc, false);
|
|
dvxErrorBox(sAc, NULL, msg);
|
|
}
|
|
|
|
|
|
// idePump -- one pass of the GUI event loop while a program runs, with
|
|
// its buffered output shown first (and the events waiting for it run)
|
|
static void idePump(void) {
|
|
flushOutput();
|
|
|
|
if (sDbgFormRt) {
|
|
basFormRtUpdate(sDbgFormRt);
|
|
} else {
|
|
dvxUpdate(sAc);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// ideRenameInCode -- rename form/control references in all .bas files
|
|
// ============================================================
|
|
//
|
|
// Case-insensitive replacement of OldName followed by '.' or '_' with
|
|
// NewName followed by the same delimiter. This handles:
|
|
// ControlName.Property -> NewName.Property
|
|
// ControlName_Click -> NewName_Click (event handlers)
|
|
// FormName.ControlName.X -> NewFormName.ControlName.X
|
|
// Sub FormName_Load -> Sub NewFormName_Load
|
|
|
|
void ideRenameInCode(const char *oldName, const char *newName) {
|
|
if (!oldName || !newName || strcasecmp(oldName, newName) == 0) {
|
|
return;
|
|
}
|
|
|
|
// The editor may hold edits that are not in the proc buffers yet
|
|
saveCurProc(false);
|
|
|
|
bool codeOnly = prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true);
|
|
|
|
// Rename in the per-procedure buffers (the code bound to the editor)
|
|
bool editorChanged = false;
|
|
bool shownChanged = false;
|
|
|
|
if (sGeneralBuf) {
|
|
char *replaced = basSourceRename(sGeneralBuf, oldName, newName, codeOnly);
|
|
|
|
if (replaced) {
|
|
free(sGeneralBuf);
|
|
sGeneralBuf = replaced;
|
|
editorChanged = true;
|
|
shownChanged = sCurProcIdx == IDE_SECTION_GENERAL;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sProcBufs); i++) {
|
|
char *replaced = basSourceRename(sProcBufs[i], oldName, newName, codeOnly);
|
|
|
|
if (replaced) {
|
|
free(sProcBufs[i]);
|
|
sProcBufs[i] = replaced;
|
|
editorChanged = true;
|
|
shownChanged = shownChanged || i == sCurProcIdx;
|
|
}
|
|
}
|
|
|
|
// Reload the editor only when the section it shows was renamed in
|
|
if (shownChanged) {
|
|
editorSetText(sCurProcIdx == IDE_SECTION_GENERAL ? (sGeneralBuf ? sGeneralBuf : "") : sProcBufs[sCurProcIdx], IDE_CARET_KEEP);
|
|
}
|
|
|
|
if (editorChanged && sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject)) {
|
|
sProject.files[sEditorFileIdx].modified = true;
|
|
}
|
|
|
|
// Update form->code: from the renamed buffers when the editor holds
|
|
// the designer's code, otherwise directly -- with the code window
|
|
// closed the designer's copy of the code is the only one loaded.
|
|
if (sDesigner.form) {
|
|
if (sEditorFileIdx >= 0 && sEditorFileIdx == sDesignerFileIdx) {
|
|
stashFormCode();
|
|
sDesigner.form->dirty = true;
|
|
} else if (sDesigner.form->code) {
|
|
char *replaced = basSourceRename(sDesigner.form->code, oldName, newName, codeOnly);
|
|
|
|
if (replaced) {
|
|
free(sDesigner.form->code);
|
|
sDesigner.form->code = replaced;
|
|
sDesigner.form->dirty = true;
|
|
}
|
|
}
|
|
|
|
// A renamed form: its project entry follows the designer
|
|
if (sDesignerFileIdx >= 0 && sDesignerFileIdx < prjFileCount(&sProject)) {
|
|
snprintf(sProject.files[sDesignerFileIdx].formName, sizeof(sProject.files[sDesignerFileIdx].formName), "%s", sDesigner.form->name);
|
|
}
|
|
}
|
|
|
|
// Rename in every other file's buffer (loading it first if needed)
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (i == sEditorFileIdx || i == sDesignerFileIdx) {
|
|
continue;
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(i, &owned, &missing);
|
|
char *replaced = basSourceRename(text, oldName, newName, codeOnly);
|
|
|
|
free(owned);
|
|
|
|
if (replaced) {
|
|
free(sProject.files[i].buffer);
|
|
sProject.files[i].buffer = replaced;
|
|
sProject.files[i].modified = true;
|
|
}
|
|
}
|
|
|
|
// Refresh dropdowns to reflect renamed procedures
|
|
updateDropdowns();
|
|
}
|
|
|
|
|
|
// immPrintCallback -- append program output to the Immediate window.
|
|
// The window has a fixed capacity, so the oldest lines scroll off the
|
|
// top once output would take the room kept for typing (IDE_IMM_HEADROOM).
|
|
|
|
static void immPrintCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!sImmediate) {
|
|
return;
|
|
}
|
|
|
|
static char immBuf[IDE_MAX_IMM];
|
|
|
|
const char *cur = wgtGetText(sImmediate);
|
|
int32_t len = cur ? (int32_t)strlen(cur) : 0;
|
|
|
|
if (len > IDE_MAX_IMM - 1) {
|
|
len = IDE_MAX_IMM - 1;
|
|
}
|
|
|
|
memcpy(immBuf, cur ? cur : "", (size_t)len);
|
|
immBuf[len] = '\0';
|
|
|
|
if (text) {
|
|
len = basDbgAppendDropping(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, text, (int32_t)strlen(text));
|
|
}
|
|
|
|
if (newline) {
|
|
basDbgAppendDropping(immBuf, IDE_MAX_IMM - IDE_IMM_HEADROOM, len, "\n", 1);
|
|
}
|
|
|
|
wgtSetText(sImmediate, immBuf);
|
|
|
|
// Move cursor to end so user can keep typing
|
|
wgtTextAreaGoToLine(sImmediate, countLines(immBuf));
|
|
}
|
|
|
|
|
|
static bool inputCallback(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
|
|
(void)ctx;
|
|
|
|
// The prompt, and everything printed before it, is visible while asking
|
|
if (prompt) {
|
|
outputAppend(prompt, (int32_t)strlen(prompt));
|
|
}
|
|
|
|
flushOutput();
|
|
return dvxInputBox(sAc, IDE_MAIN_TITLE, prompt ? prompt : "Enter value:", NULL, buf, bufSize);
|
|
}
|
|
|
|
|
|
// invalidateCompiled -- the compiled module and source map describe the
|
|
// project's previous file list; drop them (a running program keeps the
|
|
// module it executes).
|
|
static void invalidateCompiled(void) {
|
|
if (!sVm && sCachedModule) {
|
|
basModuleFree(sCachedModule);
|
|
sCachedModule = NULL;
|
|
}
|
|
|
|
arrfree(sProject.sourceMap);
|
|
sProject.sourceMap = NULL;
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
static bool isCtrlArrayInDesigner(const char *ctrlName) {
|
|
if (!sDesigner.form) {
|
|
return false;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(sDesigner.form->controls);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i]->name, ctrlName) == 0 && sDesigner.form->controls[i]->index >= 0) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// isRemAt -- true if the word REM (a comment to end of line) starts at
|
|
// line[pos]: a whole word in any case (breakpoint validation).
|
|
static bool isRemAt(const char *line, int32_t pos, int32_t lineLen) {
|
|
int32_t end = pos + KW_LEN(KW_REM);
|
|
|
|
if (end > lineLen || strncasecmp(line + pos, KW_REM, KW_LEN(KW_REM)) != 0) {
|
|
return false;
|
|
}
|
|
|
|
if (pos > 0 && basIsIdentChar(line[pos - 1])) {
|
|
return false;
|
|
}
|
|
|
|
return end == lineLen || !basIsIdentChar(line[end]);
|
|
}
|
|
|
|
|
|
static bool isReplaceEnabled(void) {
|
|
return sReplCheck && wgtCheckboxIsChecked(sReplCheck);
|
|
}
|
|
|
|
|
|
// joinProcArrays -- build the canonical file text from a (General)
|
|
// buffer and a proc array: General, one blank line, then each proc
|
|
// separated by one blank line. Returns a malloc'd string or NULL.
|
|
static char *joinProcArrays(const char *general, char **procs) {
|
|
int32_t totalLen = 0;
|
|
int32_t procCount = (int32_t)arrlen(procs);
|
|
|
|
if (general && general[0]) {
|
|
totalLen += (int32_t)strlen(general);
|
|
totalLen += 2; // newline + blank line separator
|
|
}
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (procs[i]) {
|
|
totalLen += (int32_t)strlen(procs[i]);
|
|
totalLen += 2; // newline + blank line between procedures
|
|
}
|
|
}
|
|
|
|
char *out = (char *)malloc(totalLen + 1);
|
|
|
|
if (!out) {
|
|
return NULL;
|
|
}
|
|
|
|
int32_t pos = 0;
|
|
|
|
if (general && general[0]) {
|
|
int32_t len = (int32_t)strlen(general);
|
|
memcpy(out + pos, general, len);
|
|
pos += len;
|
|
|
|
if (out[pos - 1] != '\n') {
|
|
out[pos++] = '\n';
|
|
}
|
|
|
|
out[pos++] = '\n';
|
|
}
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (procs[i]) {
|
|
int32_t len = (int32_t)strlen(procs[i]);
|
|
memcpy(out + pos, procs[i], len);
|
|
pos += len;
|
|
|
|
if (pos > 0 && out[pos - 1] != '\n') {
|
|
out[pos++] = '\n';
|
|
}
|
|
|
|
// Blank line between procedures
|
|
if (i < procCount - 1) {
|
|
out[pos++] = '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
out[pos] = '\0';
|
|
return out;
|
|
}
|
|
|
|
|
|
// joinProcBufs -- reassemble the editor's buffers into sFullSourceCache
|
|
// and rebuild sProcTable from the result so both always describe the
|
|
// same text. Does not save the editor first (see getFullSource).
|
|
static const char *joinProcBufs(void) {
|
|
free(sFullSourceCache);
|
|
sFullSourceCache = joinProcArrays(sGeneralBuf, sProcBufs);
|
|
|
|
rebuildProcTable();
|
|
return sFullSourceCache ? sFullSourceCache : "";
|
|
}
|
|
|
|
|
|
// kwMatch -- case-insensitive prefix test against a keyword tag
|
|
static bool kwMatch(const char *p, const char *kw) {
|
|
return strncasecmp(p, kw, strlen(kw)) == 0;
|
|
}
|
|
|
|
|
|
// lineCopy -- copy 1-based line `line` of text (without its newline) into
|
|
// out, truncated to fit. Returns the copied length.
|
|
static int32_t lineCopy(const char *text, int32_t line, char *out, int32_t outSize) {
|
|
const char *start = text + lineStartOffset(text, line);
|
|
int32_t len = (int32_t)strcspn(start, "\n");
|
|
|
|
if (len > outSize - 1) {
|
|
len = outSize - 1;
|
|
}
|
|
|
|
memcpy(out, start, (size_t)len);
|
|
out[len] = '\0';
|
|
return len;
|
|
}
|
|
|
|
|
|
// lineStartOffset -- byte offset where 1-based line `line` of text starts
|
|
// (the text's end when it has fewer lines)
|
|
static int32_t lineStartOffset(const char *text, int32_t line) {
|
|
const char *p = text;
|
|
|
|
for (int32_t ln = 1; *p && ln < line; ln++) {
|
|
p = skipLine(p);
|
|
}
|
|
|
|
return (int32_t)(p - text);
|
|
}
|
|
|
|
|
|
static void listRowsAdd(IdeListRowsT *rows, const char *text) {
|
|
char *copy = strdup(text);
|
|
|
|
if (copy) {
|
|
arrput(rows->strs, copy);
|
|
}
|
|
|
|
arrput(rows->cells, copy ? copy : "");
|
|
}
|
|
|
|
|
|
// listRowsAddRow -- a BasDbgRowFnT: one row of cells into ctx (IdeListRowsT)
|
|
static void listRowsAddRow(void *ctx, const char *const *cells, int32_t cellCount) {
|
|
for (int32_t i = 0; i < cellCount; i++) {
|
|
listRowsAdd((IdeListRowsT *)ctx, cells[i]);
|
|
}
|
|
}
|
|
|
|
|
|
static void listRowsCommit(const IdeListRowsT *rows, WidgetT *list, int32_t rowCount) {
|
|
wgtListViewSetData(list, rowCount > 0 ? rows->cells : NULL, rowCount);
|
|
}
|
|
|
|
|
|
static void listRowsFree(IdeListRowsT *rows) {
|
|
listRowsReset(rows);
|
|
arrfree(rows->strs);
|
|
arrfree(rows->cells);
|
|
rows->strs = NULL;
|
|
rows->cells = NULL;
|
|
}
|
|
|
|
|
|
static void listRowsReset(IdeListRowsT *rows) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(rows->strs); i++) {
|
|
free(rows->strs[i]);
|
|
}
|
|
|
|
arrsetlen(rows->strs, 0);
|
|
arrsetlen(rows->cells, 0);
|
|
}
|
|
|
|
|
|
// loadCompileSource -- the code text of project file fileIdx, normalized
|
|
// to the canonical proc layout (malloc'd). Loaded buffers are
|
|
// authoritative; disk is only read for files never loaded. For a .frm
|
|
// the result is just its code section, *outCtrlList gets its controls for
|
|
// the BEGINFORM line (basFrmControlList, malloc'd; NULL for other files)
|
|
// and the same single parse adds them to validator (when non-NULL).
|
|
// Returns NULL (and no control list) when the file cannot be read or
|
|
// memory runs out.
|
|
static char *loadCompileSource(int32_t fileIdx, BasValidatorT *validator, char **outCtrlList) {
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(fileIdx, &owned, &missing);
|
|
|
|
if (!text) {
|
|
return NULL;
|
|
}
|
|
|
|
IdeFrmScanT scan;
|
|
const char *code = text;
|
|
|
|
scan.validator = validator;
|
|
scan.code = NULL;
|
|
|
|
*outCtrlList = NULL;
|
|
|
|
if (sProject.files[fileIdx].kind == PrjKindFormE) {
|
|
int32_t textLen = (int32_t)strlen(text);
|
|
|
|
frmScan(text, &scan);
|
|
|
|
// The controls the form declares (the list never outgrows the text)
|
|
*outCtrlList = (char *)malloc((size_t)textLen + 1);
|
|
|
|
if (!*outCtrlList || basFrmControlList(text, textLen, *outCtrlList, textLen + 1) < 0) {
|
|
free(*outCtrlList);
|
|
free(scan.code);
|
|
free(owned);
|
|
*outCtrlList = NULL;
|
|
return NULL;
|
|
}
|
|
|
|
// The designer's form: its code is exactly what the editor numbers
|
|
if (fileIdx == sDesignerFileIdx && sDesigner.form) {
|
|
code = sDesigner.form->code ? sDesigner.form->code : "";
|
|
} else {
|
|
code = scan.code ? scan.code : "";
|
|
}
|
|
}
|
|
|
|
char *packed = packProcLayout(code);
|
|
|
|
// Out of memory: the caller frees nothing when the source is NULL
|
|
if (!packed) {
|
|
free(*outCtrlList);
|
|
*outCtrlList = NULL;
|
|
}
|
|
|
|
free(scan.code);
|
|
free(owned);
|
|
return packed;
|
|
}
|
|
|
|
|
|
// loadDesignerFile -- load form file fileIdx into the designer (a new,
|
|
// blank form named after the file when it does not exist yet). Returns
|
|
// false, keeping the current designer, when it exists but cannot be read.
|
|
static bool loadDesignerFile(int32_t fileIdx) {
|
|
PrjFileT *target = &sProject.files[fileIdx];
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *frmSrc = projectFileText(fileIdx, &owned, &missing);
|
|
|
|
if (!frmSrc && !missing) {
|
|
reportUnreadable(target->path);
|
|
return false;
|
|
}
|
|
|
|
designerRelease();
|
|
|
|
if (frmSrc && frmSrc[0]) {
|
|
dsgnLoadFrm(&sDesigner, frmSrc, (int32_t)strlen(frmSrc));
|
|
} else {
|
|
// New blank form -- derive name from filename
|
|
char formName[BAS_MAX_IDENT];
|
|
const char *base = platformPathBaseName(target->path);
|
|
|
|
snprintf(formName, sizeof(formName), "%.*s", (int)strcspn(base, "."), base);
|
|
dsgnNewForm(&sDesigner, formName);
|
|
target->modified = true;
|
|
}
|
|
|
|
free(owned);
|
|
sDesignerFileIdx = fileIdx;
|
|
|
|
if (sDesigner.form) {
|
|
snprintf(target->formName, sizeof(target->formName), "%s", sDesigner.form->name);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static void loadFile(void) {
|
|
FileFilterT filters[] = {
|
|
{ "BASIC Files (*.bas)" },
|
|
{ "Form Files (*.frm)" },
|
|
{ "Class Modules (*.cls)" },
|
|
{ "UserControls (*.ctl)" },
|
|
{ "All Files (*.*)" }
|
|
};
|
|
|
|
char path[DVX_MAX_PATH];
|
|
|
|
if (!dvxFileDialog(sAc, "Add File", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
|
|
return;
|
|
}
|
|
|
|
openSinglePath(path);
|
|
}
|
|
|
|
|
|
// loadFormCodeIntoEditor -- bind the editor to the designer form's code
|
|
// (whatever the editor held goes back to its own file first).
|
|
static void loadFormCodeIntoEditor(void) {
|
|
if (!sDesigner.form) {
|
|
return;
|
|
}
|
|
|
|
saveCurProc(true);
|
|
stashEditor();
|
|
parseProcs(sDesigner.form->code ? sDesigner.form->code : "");
|
|
sEditorFileIdx = sDesignerFileIdx;
|
|
|
|
if (!sCodeWin) {
|
|
showCodeWindow();
|
|
}
|
|
|
|
bool saved = sDropdownNavSuppressed;
|
|
sDropdownNavSuppressed = true;
|
|
updateDropdowns();
|
|
sDropdownNavSuppressed = saved;
|
|
|
|
showProc(IDE_SECTION_GENERAL);
|
|
}
|
|
|
|
|
|
// loadModuleIntoEditor -- bind the editor to .bas file fileIdx holding source
|
|
static void loadModuleIntoEditor(int32_t fileIdx, const char *source) {
|
|
parseProcs(source);
|
|
sEditorFileIdx = fileIdx;
|
|
updateDropdowns();
|
|
showProc(IDE_SECTION_GENERAL);
|
|
}
|
|
|
|
|
|
// loadRunFiles -- register the project's UserControls and forms with the
|
|
// form runtime of a run, as the built program does: each .ctl's header
|
|
// (its control class, before any form can use it) and each .frm's text.
|
|
// The text is the same the compile used (loaded buffers, disk only for
|
|
// never-loaded files). A form's registration only caches the text; the
|
|
// caller's basRunHostLoadForms then instantiates them, so a Load handler
|
|
// that does "Load Form2" finds Form2's .frm regardless of project file
|
|
// order.
|
|
static void loadRunFiles(BasFormRtT *rt) {
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
PrjFileT *file = &sProject.files[i];
|
|
|
|
if (file->kind != PrjKindFormE && file->kind != PrjKindControlE) {
|
|
continue;
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(i, &owned, &missing);
|
|
|
|
if (text && file->kind == PrjKindControlE) {
|
|
basFormRtRegisterCtl(rt, text, (int32_t)strlen(text));
|
|
} else if (text) {
|
|
basRunHostRegisterFrm(rt, text, (int32_t)strlen(text));
|
|
}
|
|
|
|
free(owned);
|
|
}
|
|
}
|
|
|
|
|
|
static WidgetT *loadTbIcon(WidgetT *parent, const char *resName, const char *fallbackText) {
|
|
int32_t iconW = 0;
|
|
int32_t iconH = 0;
|
|
int32_t iconPitch = 0;
|
|
uint8_t *data = dvxResLoadIcon(sAc, sCtx->appPath, resName, &iconW, &iconH, &iconPitch);
|
|
|
|
if (data) {
|
|
return wgtImageButton(parent, data, iconW, iconH, iconPitch);
|
|
}
|
|
|
|
// Fallback to text button if icon not found
|
|
return wgtButton(parent, fallbackText);
|
|
}
|
|
|
|
|
|
// Convert an editor-local line number to a full-source line number, or
|
|
// IDE_NO_LINE when that line was not compiled (e.g. a class module's
|
|
// header). The editor shows one procedure at a time (sCurProcIdx), so
|
|
// the procedure's starting line within the file's code is added, then
|
|
// the source map places the file's line in the concatenated source (its
|
|
// startLine already accounts for the injected BEGINFORM / BEGINCLASS).
|
|
static int32_t localToConcatLine(int32_t editorLine) {
|
|
int32_t fileLine = editorLineToCodeLine(editorLine);
|
|
|
|
// Editor text bound to no project file is compiled on its own
|
|
if (sEditorFileIdx < 0) {
|
|
return fileLine;
|
|
}
|
|
|
|
int32_t concatLine = prjMapFileLine(&sProject, sEditorFileIdx, fileLine);
|
|
|
|
return concatLine > 0 ? concatLine : IDE_NO_LINE;
|
|
}
|
|
|
|
|
|
// logFlushLine -- write the pending PRINT line to the log (Output to Log)
|
|
static void logFlushLine(void) {
|
|
sLogLine[sLogLineLen] = '\0';
|
|
dvxLog("BASIC: %s", sLogLine);
|
|
sLogLineLen = 0;
|
|
}
|
|
|
|
|
|
// makeExecutable -- File > Make Executable: the program (.app), or for a
|
|
// control project (File > Make Control) its control file (.wgt, named
|
|
// after the project)
|
|
static void makeExecutable(void) {
|
|
if (!hasProject()) {
|
|
setStatus("Save the project first.");
|
|
return;
|
|
}
|
|
|
|
// Compile (always recompile to ensure latest code)
|
|
if (!compileProject()) {
|
|
return;
|
|
}
|
|
|
|
// Ask for output path
|
|
bool control = prjIsControlProject(&sProject);
|
|
FileFilterT appFilters[] = {
|
|
{ "DVX Applications (*.app)" },
|
|
{ "All Files (*.*)" }
|
|
};
|
|
FileFilterT ctlFilters[] = {
|
|
{ "Control Files (*.wgt)" },
|
|
{ "All Files (*.*)" }
|
|
};
|
|
char outPath[DVX_MAX_PATH];
|
|
outPath[0] = '\0';
|
|
|
|
if (control) {
|
|
snprintf(outPath, sizeof(outPath), "%s" BAS_WGT_EXT, sProject.name);
|
|
}
|
|
|
|
if (!dvxFileDialog(sAc, control ? "Make Control" : "Make Executable", FD_SAVE, NULL, control ? ctlFilters : appFilters, control ? DVX_ARRAY_LEN(ctlFilters) : DVX_ARRAY_LEN(appFilters), outPath, sizeof(outPath))) {
|
|
return;
|
|
}
|
|
|
|
// Ask debug or release
|
|
const char *modeItems[] = {
|
|
"Debug (include error info)",
|
|
"Release (smaller, no debug info)"
|
|
};
|
|
int32_t modeChoice = 0;
|
|
|
|
if (!dvxChoiceDialog(sAc, "Build Mode", "Select build mode:", modeItems, DVX_ARRAY_LEN(modeItems), 0, &modeChoice)) {
|
|
return;
|
|
}
|
|
|
|
bool release = (modeChoice == 1);
|
|
|
|
setStatus(control ? "Building control file..." : (release ? "Building release executable..." : "Building debug executable..."));
|
|
|
|
const char *failure = makeOutput(outPath, release);
|
|
|
|
if (failure) {
|
|
setStatus(failure);
|
|
return;
|
|
}
|
|
|
|
char msg[IDE_MSG_BUF];
|
|
snprintf(msg, sizeof(msg), "Created %s (%s)", outPath, release ? "release" : "debug");
|
|
setStatus(msg);
|
|
}
|
|
|
|
|
|
// makeOutput -- build the compiled project (compileProject first) at
|
|
// outPath: the program, or a control project's control file, through the
|
|
// build code bascomp uses. NULL when built, else why not (IdeBuildFnT).
|
|
static const char *makeOutput(const char *outPath, bool release) {
|
|
dvxSetBusy(sAc, true);
|
|
dvxUpdate(sAc);
|
|
|
|
// Gather every .frm text (unsaved editor buffers win over disk).
|
|
char **frmSources = NULL; // stb_ds: owned form text
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (sProject.files[i].kind != PrjKindFormE) {
|
|
continue;
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(i, &owned, &missing);
|
|
char *frmSrc = owned ? owned : (text ? strdup(text) : NULL);
|
|
|
|
if (frmSrc) {
|
|
arrput(frmSources, frmSrc);
|
|
}
|
|
}
|
|
|
|
BasBuildSpecT spec;
|
|
memset(&spec, 0, sizeof(spec));
|
|
spec.projName = sProject.name;
|
|
spec.author = sProject.author;
|
|
spec.publisher = sProject.publisher;
|
|
spec.version = sProject.version;
|
|
spec.copyright = sProject.copyright;
|
|
spec.description = sProject.description;
|
|
spec.projectDir = sProject.projectDir;
|
|
spec.iconPath = sProject.iconPath;
|
|
spec.helpFile = sProject.helpFile;
|
|
char **ctlSources = ctlHeaders();
|
|
|
|
spec.module = sCachedModule;
|
|
spec.frmSources = (const char *const *)frmSources;
|
|
spec.frmCount = (int32_t)arrlen(frmSources);
|
|
spec.ctlSources = (const char *const *)ctlSources;
|
|
spec.ctlCount = (int32_t)arrlen(ctlSources);
|
|
spec.release = release;
|
|
spec.multiInstance = sProject.multiInstance;
|
|
spec.fontCount = prjFontCount(&sProject);
|
|
|
|
for (int32_t i = 0; i < spec.fontCount && i < BAS_BUILD_FONTS_MAX; i++) {
|
|
spec.fonts[i] = prjFontPath(&sProject, i);
|
|
}
|
|
|
|
const char *failure = prjIsControlProject(&sProject) ? basBuildControl(outPath, &spec) : basBuildApp(outPath, &spec);
|
|
|
|
freeTexts(frmSources);
|
|
freeTexts(ctlSources);
|
|
dvxSetBusy(sAc, false);
|
|
return failure;
|
|
}
|
|
|
|
|
|
// makePackage -- File > Make Package: the release build and its help as
|
|
// an add-on package (idePackage.c)
|
|
static void makePackage(void) {
|
|
if (!hasProject()) {
|
|
setStatus("Save the project first.");
|
|
return;
|
|
}
|
|
|
|
if (!compileProject()) {
|
|
return;
|
|
}
|
|
|
|
const char *result = idePackageMake(sAc, &sProject, makeOutput, setStatus);
|
|
|
|
if (result) {
|
|
setStatus(result);
|
|
}
|
|
}
|
|
|
|
|
|
static void navigateToBreakpoint(int32_t bpIdx) {
|
|
if (bpIdx < 0 || bpIdx >= (int32_t)arrlen(sBreakpoints)) {
|
|
return;
|
|
}
|
|
|
|
navigateToCodeLine(sBreakpoints[bpIdx].fileIdx, sBreakpoints[bpIdx].codeLine, false);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// navigateToCodeLine -- show a specific file/line in the code editor
|
|
// ============================================================
|
|
//
|
|
// fileIdx: project file index (-1 = the editor's current code)
|
|
// codeLine: 1-based line within the file's code section
|
|
// setDbgLine: if true, update sDbgCurrentLine for the debug decorator
|
|
//
|
|
// The procedure is the one whose line range holds codeLine (else the
|
|
// General section), found after the file is active. Returns the shown
|
|
// proc index (IDE_SECTION_GENERAL for General), or IDE_SECTION_NONE when
|
|
// the file could not be activated.
|
|
|
|
static int32_t navigateToCodeLine(int32_t fileIdx, int32_t codeLine, bool setDbgLine) {
|
|
if (fileIdx >= 0 && !activateFile(fileIdx, ViewCodeE)) {
|
|
return IDE_SECTION_NONE;
|
|
}
|
|
|
|
if (!sCodeWin) {
|
|
showCodeWindow();
|
|
}
|
|
|
|
if (sCodeWin && !sCodeWin->visible) {
|
|
dvxShowWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
if (!sEditor) {
|
|
return IDE_SECTION_NONE;
|
|
}
|
|
|
|
// Line numbers refer to the current buffers; the editor-local line is
|
|
// taken before showProc, which may drop an untouched skeleton.
|
|
getFullSource();
|
|
|
|
int32_t procIdx = procIndexForCodeLine(codeLine);
|
|
int32_t editorLine = (procIdx >= 0) ? codeLine - sProcTable[procIdx].lineNum + 1 : codeLine;
|
|
|
|
if (procIdx != sCurProcIdx) {
|
|
showProc(procIdx);
|
|
}
|
|
|
|
// Update dropdowns to match
|
|
if (sCurProcIdx >= 0 && sCurProcIdx < (int32_t)arrlen(sProcTable)) {
|
|
selectDropdowns(sProcTable[sCurProcIdx].objName, sProcTable[sCurProcIdx].evtName);
|
|
} else if (sObjDropdown) {
|
|
wgtDropdownSetSelected(sObjDropdown, 0);
|
|
}
|
|
|
|
if (setDbgLine) {
|
|
sDbgCurrentLine = editorLine;
|
|
}
|
|
|
|
wgtTextAreaGoToLine(sEditor, editorLine);
|
|
refreshEditorMarkers();
|
|
return sCurProcIdx;
|
|
}
|
|
|
|
|
|
// navigateToEventSub -- open code editor at the default event sub for the
|
|
// selected control (or form).
|
|
|
|
static void navigateToEventSub(void) {
|
|
if (!sDesigner.form) {
|
|
return;
|
|
}
|
|
|
|
// Determine control name and default event
|
|
const char *ctrlName = NULL;
|
|
const char *eventName = NULL;
|
|
|
|
if (sDesigner.selectedIdx >= 0 && sDesigner.selectedIdx < (int32_t)arrlen(sDesigner.form->controls)) {
|
|
FrmCtrlT *ctrl = sDesigner.form->controls[sDesigner.selectedIdx];
|
|
ctrlName = ctrl->name;
|
|
eventName = basCtlDefaultEvent(ctrl->typeName);
|
|
} else {
|
|
ctrlName = sDesigner.form->name;
|
|
eventName = basCtlDefaultEvent(IDE_FORM_TYPE_NAME);
|
|
}
|
|
|
|
navigateToNamedEventSub(ctrlName, eventName);
|
|
}
|
|
|
|
|
|
// navigateToNamedEventSub -- open the code editor at Ctrl_Event, creating
|
|
// the sub skeleton if it doesn't exist. Code is stored in the .frm
|
|
// file's code section (sDesigner.form->code).
|
|
|
|
static void navigateToNamedEventSub(const char *ctrlName, const char *eventName) {
|
|
if (!sDesigner.form || !ctrlName || !eventName) {
|
|
return;
|
|
}
|
|
|
|
// The designer may show a form while another file is active
|
|
if (sDesignerFileIdx >= 0 && sDesignerFileIdx != sProject.activeFileIdx && !activateFile(sDesignerFileIdx, ViewCodeE)) {
|
|
return;
|
|
}
|
|
|
|
// Load form code into editor (stashes existing code, parses procs,
|
|
// populates dropdowns without triggering navigation)
|
|
loadFormCodeIntoEditor();
|
|
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
// Search for existing procedure
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
int32_t procIdx = -1;
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, ctrlName) == 0 && strcasecmp(sProcTable[i].evtName, eventName) == 0) {
|
|
procIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (procIdx >= 0) {
|
|
showProc(procIdx);
|
|
} else {
|
|
createEventSubSkeleton(ctrlName, eventName);
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
setStatus("Code view.");
|
|
selectDropdowns(ctrlName, eventName);
|
|
}
|
|
|
|
|
|
// newClassFile -- add class module ClassN or UserControl UserControlN
|
|
// (kind; the first N whose name no form, class or control of the project
|
|
// uses and whose file does not exist yet), holding the standard header
|
|
// (a UserControl's: prjCtlNew), to the project and open it in the editor.
|
|
static void newClassFile(PrjFileKindE kind) {
|
|
bool control = (kind == PrjKindControlE);
|
|
const char *ext = control ? BAS_CTL_EXT : BAS_CLS_EXT;
|
|
char name[BAS_MAX_IDENT];
|
|
char fileName[DVX_MAX_PATH];
|
|
char path[DVX_MAX_PATH * 2];
|
|
|
|
for (int32_t n = 1; ; n++) {
|
|
snprintf(name, sizeof(name), control ? IDE_NEW_CTL_FMT : IDE_NEW_CLASS_FMT, (int)n);
|
|
snprintf(fileName, sizeof(fileName), "%s%s", name, ext);
|
|
platformPathJoin(path, sizeof(path), sProject.projectDir, fileName);
|
|
|
|
if (!classNameUsed(name, fileName) && !prjPathExists(path)) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
char classText[sizeof(IDE_NEW_CLASS_TEXT) + BAS_MAX_IDENT];
|
|
char *text = NULL;
|
|
|
|
if (control) {
|
|
text = prjCtlNew(name);
|
|
} else {
|
|
snprintf(classText, sizeof(classText), IDE_NEW_CLASS_TEXT, name);
|
|
text = strdup(classText);
|
|
}
|
|
|
|
bool wasDirty = sProject.dirty;
|
|
int32_t idx = prjAddFile(&sProject, fileName, kind);
|
|
|
|
sProject.files[idx].buffer = text;
|
|
sProject.files[idx].modified = true;
|
|
|
|
if (!text || !activateFile(idx, ViewCodeE)) {
|
|
prjRemoveFile(&sProject, idx);
|
|
sProject.dirty = wasDirty;
|
|
return;
|
|
}
|
|
|
|
invalidateCompiled();
|
|
prjRebuildTree(&sProject);
|
|
updateDirtyIndicators();
|
|
setStatus(control ? "UserControl added." : "Class module added.");
|
|
|
|
if (control) {
|
|
designCtlRefresh();
|
|
}
|
|
}
|
|
|
|
|
|
static void newProject(void) {
|
|
if (!promptAndSave()) {
|
|
return;
|
|
}
|
|
|
|
char name[PRJ_MAX_NAME];
|
|
|
|
if (!dvxInputBox(sAc, "New Project", "Project name:", "", name, sizeof(name))) {
|
|
return;
|
|
}
|
|
|
|
if (name[0] == '\0') {
|
|
return;
|
|
}
|
|
|
|
// Ask for directory via save dialog (file = name.dbp)
|
|
FileFilterT filters[] = {
|
|
{ "Project Files (*.dbp)" }
|
|
};
|
|
|
|
char dbpPath[DVX_MAX_PATH];
|
|
snprintf(dbpPath, sizeof(dbpPath), "%s%s", name, BAS_DBP_EXT);
|
|
|
|
if (!dvxFileDialog(sAc, "Save New Project", FD_SAVE, NULL, filters, 1, dbpPath, sizeof(dbpPath))) {
|
|
return;
|
|
}
|
|
|
|
closeProject();
|
|
|
|
// Derive directory from chosen path
|
|
char dir[DVX_MAX_PATH];
|
|
snprintf(dir, sizeof(dir), "%s", dbpPath);
|
|
char *sep = platformPathDirEnd(dir);
|
|
|
|
if (sep) {
|
|
*sep = '\0';
|
|
} else {
|
|
dir[0] = '.';
|
|
dir[1] = '\0';
|
|
}
|
|
|
|
prjNew(&sProject, name, dir);
|
|
prjApplyDefaults(&sProject, sPrefs);
|
|
snprintf(sProject.projectPath, sizeof(sProject.projectPath), "%s", dbpPath);
|
|
saveProjectFile();
|
|
|
|
ensureProjectWindow(false);
|
|
updateMainTitle();
|
|
setStatus("New project created.");
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// objectType -- the designer type of an Object dropdown entry: the form
|
|
// (IDE_FORM_TYPE_NAME), a menu item (FRM_KW_MENU), a control's type, in a
|
|
// .ctl the UserControl object (BAS_UC_OBJECT), else "" (a WithEvents
|
|
// variable, or a name that is none of these)
|
|
static const char *objectType(const char *objName) {
|
|
if (editorIsControl()) {
|
|
return strcasecmp(objName, BAS_UC_OBJECT) == 0 ? BAS_UC_OBJECT : "";
|
|
}
|
|
|
|
if (!sDesigner.form) {
|
|
return "";
|
|
}
|
|
|
|
if (strcasecmp(objName, sDesigner.form->name) == 0) {
|
|
return IDE_FORM_TYPE_NAME;
|
|
}
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
|
|
if (strcasecmp(sDesigner.form->menuItems[i].name, objName) == 0) {
|
|
return FRM_KW_MENU;
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
|
|
if (strcasecmp(sDesigner.form->controls[i]->name, objName) == 0) {
|
|
return sDesigner.form->controls[i]->typeName;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
|
|
static void onBreakpointHit(void *ctx, int32_t line) {
|
|
(void)ctx;
|
|
|
|
// The VM fires this with line == -1 at end-of-sub after a breakpointed
|
|
// handler finishes; that is not a real pause point (every genuine pause --
|
|
// breakpoint or Step Into/Over/Out -- arrives with a positive line), so
|
|
// ignore it. Otherwise Continue would re-pause and require a second press.
|
|
// After Stop, a handler still run while runBody unwinds never pauses:
|
|
// the state stays Stopping and no debugger UI is driven.
|
|
if (line < 0 || sStopRequested || sDbgState == DbgStoppingE) {
|
|
return;
|
|
}
|
|
|
|
sDbgState = DbgPausedE;
|
|
|
|
if (sVm) {
|
|
basVmSetDebugPaused(sVm, true);
|
|
}
|
|
|
|
if (line > 0) {
|
|
sDbgCurrentLine = line;
|
|
debugNavigateToLine(line);
|
|
}
|
|
|
|
basRunHostTagBreak(sDbgFormRt, &sBreakTitles, true);
|
|
debugUpdateWindows();
|
|
updateProjectMenuState();
|
|
setStatus("Paused.");
|
|
}
|
|
|
|
|
|
static void onBreakpointListDblClick(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sBreakpointList) {
|
|
return;
|
|
}
|
|
|
|
int32_t sel = wgtListViewGetSelected(sBreakpointList);
|
|
navigateToBreakpoint(sel);
|
|
}
|
|
|
|
|
|
static void onBreakpointListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
|
|
(void)w;
|
|
(void)shift;
|
|
|
|
// Delete key
|
|
if (keyCode != KEY_DELETE && keyCode != KEY_ASCII_DEL) {
|
|
return;
|
|
}
|
|
|
|
if (!sBreakpointList || arrlen(sBreakpoints) == 0) {
|
|
return;
|
|
}
|
|
|
|
// Remove selected breakpoints in reverse order to preserve indices
|
|
for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
|
|
if (wgtListViewIsItemSelected(sBreakpointList, i)) {
|
|
arrdel(sBreakpoints, i);
|
|
}
|
|
}
|
|
|
|
syncVmBreakpoints();
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
|
|
static void onBreakpointWinClose(WindowT *win) {
|
|
dvxHideWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onCallStackClose(WindowT *win) {
|
|
dvxHideWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onClose(WindowT *win) {
|
|
if (!promptAndSave()) {
|
|
return;
|
|
}
|
|
|
|
// Stop any running program
|
|
debugStop();
|
|
|
|
// Prevent stale focus tracking during shutdown
|
|
sLastFocusWin = NULL;
|
|
|
|
// Null widget pointers first so nothing references destroyed widgets
|
|
sEditor = NULL;
|
|
sOutput = NULL;
|
|
sImmediate = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
sStatus = NULL;
|
|
sToolbar = NULL;
|
|
sStatusBar = NULL;
|
|
|
|
// Close the project (designer, code, project windows and buffers),
|
|
// then whatever can exist without one (toolbox/properties).
|
|
closeProject();
|
|
closeProjectUi();
|
|
basUcSetPropChangedHook(NULL);
|
|
|
|
if (sOutWin) {
|
|
dvxDestroyWindow(sAc, sOutWin);
|
|
sOutWin = NULL;
|
|
}
|
|
|
|
if (sImmWin) {
|
|
dvxDestroyWindow(sAc, sImmWin);
|
|
sImmWin = NULL;
|
|
}
|
|
|
|
if (sLocalsWin) {
|
|
dvxDestroyWindow(sAc, sLocalsWin);
|
|
sLocalsWin = NULL;
|
|
sLocalsList = NULL;
|
|
}
|
|
|
|
if (sCallStackWin) {
|
|
dvxDestroyWindow(sAc, sCallStackWin);
|
|
sCallStackWin = NULL;
|
|
sCallStackList = NULL;
|
|
}
|
|
|
|
if (sWatchWin) {
|
|
dvxDestroyWindow(sAc, sWatchWin);
|
|
sWatchWin = NULL;
|
|
sWatchList = NULL;
|
|
sWatchInput = NULL;
|
|
}
|
|
|
|
if (sBreakpointWin) {
|
|
dvxDestroyWindow(sAc, sBreakpointWin);
|
|
sBreakpointWin = NULL;
|
|
sBreakpointList = NULL;
|
|
}
|
|
|
|
// win itself is destroyed last, below, once nothing refers to it
|
|
sWin = NULL;
|
|
|
|
// A running program is still executing sCachedModule (on its task, or
|
|
// this close arrived through its event pump); runBody frees it on exit.
|
|
if (sCachedModule && !sVm) {
|
|
basModuleFree(sCachedModule);
|
|
sCachedModule = NULL;
|
|
}
|
|
|
|
arrfree(sProcTable);
|
|
sProcTable = NULL;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) {
|
|
free((void *)sObjItems[i]);
|
|
}
|
|
|
|
evtItemsReset();
|
|
arrfree(sObjItems);
|
|
arrfree(sEvtItems);
|
|
arrfree(sWithEvents);
|
|
sObjItems = NULL;
|
|
sEvtItems = NULL;
|
|
sWithEvents = NULL;
|
|
|
|
listRowsFree(&sBpRows);
|
|
listRowsFree(&sLocalsRows);
|
|
listRowsFree(&sCallRows);
|
|
listRowsFree(&sWatchRows);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sWatchExprs); i++) {
|
|
free(sWatchExprs[i]);
|
|
}
|
|
|
|
arrfree(sWatchExprs);
|
|
sWatchExprs = NULL;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
|
|
free(sRecentFiles[i]);
|
|
}
|
|
|
|
arrfree(sRecentFiles);
|
|
sRecentFiles = NULL;
|
|
|
|
arrfree(sBreakpoints);
|
|
sBreakpoints = NULL;
|
|
arrfree(sVmBreakpoints);
|
|
sVmBreakpoints = NULL;
|
|
arrfree(sBreakTitles);
|
|
sBreakTitles = NULL;
|
|
|
|
free(sFullSourceCache);
|
|
sFullSourceCache = NULL;
|
|
free(sEditorPrevText);
|
|
sEditorPrevText = NULL;
|
|
|
|
prefsClose(sPrefs);
|
|
sPrefs = NULL;
|
|
|
|
// The accelerator table is shared by every IDE window; all of them
|
|
// are gone by now, so release it before the main window goes.
|
|
dvxFreeAccelTable(win->accelTable);
|
|
win->accelTable = NULL;
|
|
|
|
dvxDestroyWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onCodeWinClose(WindowT *win) {
|
|
// Stash code back before the window is destroyed: a module's text to
|
|
// its project buffer, a form's to its code. The proc buffers stay,
|
|
// but no section is shown any more (a reopened editor starts empty).
|
|
saveCurProc(true);
|
|
stashEditor();
|
|
|
|
dvxDestroyWindow(sAc, win);
|
|
sCodeWin = NULL;
|
|
sEditor = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
sCurProcIdx = IDE_SECTION_NONE;
|
|
|
|
if (sLastFocusWin == win) {
|
|
sLastFocusWin = NULL;
|
|
}
|
|
|
|
updateProjectMenuState();
|
|
}
|
|
|
|
|
|
// Preferences Colors tab: Change... (or a double-click on the list) picks
|
|
// a new color for the selected entry in the color dialog.
|
|
static void onColorChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);
|
|
|
|
if (idx < 0 || idx >= TEXT_SYNTAX_MAX) {
|
|
return;
|
|
}
|
|
|
|
if (dvxColorDialog(sAc, sSyntaxColorNames[idx], &sPrefsDlg.syntaxColors[idx])) {
|
|
prefsUpdateColorSwatch();
|
|
}
|
|
}
|
|
|
|
|
|
static void onColorListChange(WidgetT *w) {
|
|
(void)w;
|
|
prefsUpdateColorSwatch();
|
|
}
|
|
|
|
|
|
static void onContentFocus(WindowT *win) {
|
|
sLastFocusWin = win;
|
|
}
|
|
|
|
|
|
// onCtlHeaderSet -- the Properties window changed header key key of the
|
|
// active UserControl: its text rewritten (prjCtlHeaderSet: that line, or
|
|
// the name on the Begin line; the code untouched) as an unsaved edit, the
|
|
// editor and the file's breakpoints following (a line added or removed
|
|
// moves every code line), the designer's controls refreshed. A new name
|
|
// must be free in the project.
|
|
static void onCtlHeaderSet(const char *key, const char *value) {
|
|
int32_t idx = sProject.activeFileIdx;
|
|
|
|
if (idx < 0 || idx >= prjFileCount(&sProject) || sProject.files[idx].kind != PrjKindControlE) {
|
|
return;
|
|
}
|
|
|
|
if (idx == sEditorFileIdx) {
|
|
stashEditor();
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(idx, &owned, &missing);
|
|
char oldName[BAS_MAX_IDENT];
|
|
bool rename = strcasecmp(key, BAS_PROP_NAME) == 0 && text && prjClassName(&sProject, idx, text, oldName, (int32_t)sizeof(oldName)) && strcasecmp(oldName, value) != 0;
|
|
char *changed = (text && !(rename && classNameUsed(value, ""))) ? prjCtlHeaderSet(text, key, value) : NULL;
|
|
|
|
if (!changed) {
|
|
char msg[IDE_MSG_BUF];
|
|
|
|
snprintf(msg, sizeof(msg), rename ? "The project already has a form, class module, or UserControl named %s, or it is not a valid name." : "%s cannot be set to that value.", rename ? value : key);
|
|
ideErrorBox(msg);
|
|
free(owned);
|
|
return;
|
|
}
|
|
|
|
int32_t delta = countLines(changed) - countLines(text);
|
|
PrjFileT *file = &sProject.files[idx];
|
|
|
|
free(owned);
|
|
free(file->buffer);
|
|
file->buffer = changed;
|
|
file->modified = true;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
|
|
if (sBreakpoints[i].fileIdx == idx) {
|
|
sBreakpoints[i].codeLine += delta;
|
|
}
|
|
}
|
|
|
|
if (delta != 0) {
|
|
syncVmBreakpoints();
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
if (idx == sEditorFileIdx) {
|
|
loadModuleIntoEditor(idx, changed);
|
|
}
|
|
|
|
invalidateCompiled();
|
|
updateDirtyIndicators();
|
|
designCtlRefresh();
|
|
propsFollowActive();
|
|
}
|
|
|
|
|
|
static void onEditorChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
// Breakpoints follow lines that moved; the snapshot is kept current on
|
|
// every edit so each diff covers exactly one change.
|
|
const char *text = wgtGetText(sEditor);
|
|
int32_t lines = countLines(text);
|
|
int32_t delta = lines - sEditorLineCount;
|
|
|
|
if (delta != 0 && sEditorPrevText) {
|
|
shiftBreakpointsForEdit(sEditorPrevText, text, delta);
|
|
}
|
|
|
|
editorSnapshot(lines);
|
|
|
|
// Mark the file the editor is bound to as modified; the indicators
|
|
// only need a refresh when that is news.
|
|
if (sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject)) {
|
|
PrjFileT *file = &sProject.files[sEditorFileIdx];
|
|
bool wasModified = file->modified;
|
|
|
|
file->modified = true;
|
|
|
|
// Only mark form dirty when editing the form's code, not a .bas file
|
|
if (sEditorFileIdx == sDesignerFileIdx && sDesigner.form) {
|
|
sDesigner.form->dirty = true;
|
|
}
|
|
|
|
if (!wasModified) {
|
|
updateDirtyIndicators();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// onEvtDropdownChange -- navigate to the selected procedure when the
|
|
// event dropdown changes.
|
|
|
|
static void onEvtDropdownChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (sDropdownNavSuppressed) {
|
|
return;
|
|
}
|
|
|
|
if (!sObjDropdown || !sEvtDropdown || !sEditor) {
|
|
return;
|
|
}
|
|
|
|
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
|
int32_t evtIdx = wgtDropdownGetSelected(sEvtDropdown);
|
|
|
|
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems) || evtIdx < 0 || evtIdx >= (int32_t)arrlen(sEvtItems)) {
|
|
return;
|
|
}
|
|
|
|
const char *selObj = sObjItems[objIdx];
|
|
const char *selEvt = sEvtItems[evtIdx];
|
|
|
|
// (Global) shows the General module-level section
|
|
if (strcasecmp(selEvt, IDE_GLOBAL_SECTION) == 0) {
|
|
showProc(IDE_SECTION_GENERAL);
|
|
return;
|
|
}
|
|
|
|
// Strip brackets if present (unimplemented event)
|
|
char evtName[BAS_MAX_IDENT];
|
|
|
|
if (selEvt[0] == '[') {
|
|
snprintf(evtName, sizeof(evtName), "%s", selEvt + 1);
|
|
int32_t len = (int32_t)strlen(evtName);
|
|
|
|
if (len > 0 && evtName[len - 1] == ']') {
|
|
evtName[len - 1] = '\0';
|
|
}
|
|
} else {
|
|
snprintf(evtName, sizeof(evtName), "%s", selEvt);
|
|
}
|
|
|
|
// Search for an existing proc matching object + event
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, selObj) == 0 && strcasecmp(sProcTable[i].evtName, evtName) == 0) {
|
|
showProc(i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// (General) lists existing procedures only; a stale entry has no
|
|
// event to generate a handler for.
|
|
if (strcasecmp(selObj, IDE_GENERAL_SECTION) == 0) {
|
|
showProc(IDE_SECTION_GENERAL);
|
|
return;
|
|
}
|
|
|
|
createEventSubSkeleton(selObj, evtName);
|
|
}
|
|
|
|
|
|
static void onFindClose(WindowT *win) {
|
|
(void)win;
|
|
closeFindDialog();
|
|
}
|
|
|
|
|
|
static void onFindCloseBtn(WidgetT *w) {
|
|
(void)w;
|
|
closeFindDialog();
|
|
}
|
|
|
|
|
|
static void onFindNext(WidgetT *w) {
|
|
(void)w;
|
|
|
|
// The dialog's text wins; with the dialog closed (F3), the last
|
|
// search is repeated.
|
|
if (sFindInput) {
|
|
const char *needle = wgtGetText(sFindInput);
|
|
|
|
if (!needle || !needle[0]) {
|
|
return;
|
|
}
|
|
|
|
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
|
}
|
|
|
|
if (sFindText[0] && !findInProject(sFindText, getFindMatchCase(), getFindForward(), getFindScope())) {
|
|
setStatus("Not found.");
|
|
}
|
|
}
|
|
|
|
|
|
// onFormWinClose -- shell callback when user clicks X on the form window.
|
|
static void onFormWinClose(WindowT *win) {
|
|
dvxDestroyWindow(sAc, win);
|
|
cleanupFormWin();
|
|
}
|
|
|
|
|
|
static int32_t onFormWinCursorQuery(WindowT *win, int32_t x, int32_t y) {
|
|
(void)win;
|
|
|
|
if (!sDesigner.form) {
|
|
return 0;
|
|
}
|
|
|
|
// Crosshair when placing a new control, else the resize cursors over
|
|
// the selection's handles
|
|
if (sDesigner.activeTool[0] != '\0') {
|
|
return CURSOR_CROSSHAIR;
|
|
}
|
|
|
|
return basDsgnCursorAt(dsgnSelectedWidget(&sDesigner), x, y);
|
|
}
|
|
|
|
|
|
static void onFormWinKey(WindowT *win, int32_t key, int32_t mod) {
|
|
// Ctrl+X/C/V and Del act on the selected control, like the Edit menu
|
|
if ((mod & ACCEL_CTRL) && (key == KEY_CTRL_X || key == KEY_CTRL_C || key == KEY_CTRL_V)) {
|
|
designerEditCmd(key == KEY_CTRL_X ? CMD_CUT : (key == KEY_CTRL_C ? CMD_COPY : CMD_PASTE));
|
|
return;
|
|
}
|
|
|
|
if (key == KEY_DELETE && sDesigner.selectedIdx >= 0) {
|
|
designerEditCmd(CMD_DELETE);
|
|
return;
|
|
}
|
|
|
|
// Forward unhandled keys to the widget system
|
|
widgetOnKey(win, key, mod);
|
|
}
|
|
|
|
|
|
// onFormWinMenu -- menu click on the designer form preview.
|
|
// Designer menu items use IDs starting at DSGN_MENU_ID_BASE.
|
|
// Clicking one navigates to the menu item's Click event code.
|
|
// Other IDs (from shared accel table) fall through to onMenu.
|
|
static void onFormWinMenu(WindowT *win, int32_t menuId) {
|
|
if (sDesigner.form && menuId >= DSGN_MENU_ID_BASE) {
|
|
int32_t idx = menuId - DSGN_MENU_ID_BASE;
|
|
int32_t menuCount = (int32_t)arrlen(sDesigner.form->menuItems);
|
|
|
|
if (idx >= 0 && idx < menuCount) {
|
|
const char *name = sDesigner.form->menuItems[idx].name;
|
|
|
|
if (name[0]) {
|
|
navigateToNamedEventSub(name, IDE_MENU_EVENT);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
onMenu(win, menuId);
|
|
}
|
|
|
|
|
|
// onFormWinMouse -- handle mouse events on the form designer window.
|
|
// Coordinates are relative to the window's client area (content box origin).
|
|
|
|
static void onFormWinMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
|
|
(void)win;
|
|
static int32_t lastButtons = 0;
|
|
|
|
bool wasDown = (lastButtons & MOUSE_LEFT) != 0;
|
|
bool isDown = (buttons & MOUSE_LEFT) != 0;
|
|
|
|
if (!sDesigner.form || !sFormWin) {
|
|
lastButtons = buttons;
|
|
return;
|
|
}
|
|
|
|
bool dirtyBefore = sDesigner.form->dirty;
|
|
|
|
if (isDown && !wasDown) {
|
|
// A click in the designer makes it the Edit menu's target
|
|
sLastFocusWin = sFormWin;
|
|
|
|
// Detect double-click using the system-wide setting
|
|
int32_t clicks = multiClickDetect(x, y);
|
|
|
|
int32_t prevCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
bool wasDirty = sDesigner.form->dirty;
|
|
dsgnOnMouse(&sDesigner, x, y, false);
|
|
int32_t newCount = (int32_t)arrlen(sDesigner.form->controls);
|
|
bool nowDirty = sDesigner.form->dirty;
|
|
|
|
if (newCount != prevCount || (nowDirty && !wasDirty)) {
|
|
prpRebuildTree(&sDesigner);
|
|
}
|
|
|
|
prpRefresh(&sDesigner);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
|
|
if (clicks >= 2 && sDesigner.activeTool[0] == '\0') {
|
|
navigateToEventSub();
|
|
}
|
|
} else if (isDown && wasDown) {
|
|
// Drag
|
|
dsgnOnMouse(&sDesigner, x, y, true);
|
|
prpRefresh(&sDesigner);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
} else if (!isDown && wasDown) {
|
|
// Release
|
|
dsgnOnMouse(&sDesigner, x, y, false);
|
|
prpRefresh(&sDesigner);
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
}
|
|
|
|
lastButtons = buttons;
|
|
|
|
// Plain moves change nothing; only a change of dirty state is news
|
|
if (sDesigner.form && sDesigner.form->dirty && !dirtyBefore) {
|
|
updateDirtyIndicators();
|
|
}
|
|
}
|
|
|
|
|
|
// onFormWinPaint -- draw selection handles after widgets have painted.
|
|
|
|
static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
|
|
if (!win) {
|
|
return;
|
|
}
|
|
|
|
// Force measure + relayout only on structural changes (control
|
|
// add/remove/resize). Selection clicks just need repaint + overlay.
|
|
if (win->paintNeeded >= PAINT_FULL && win->widgetRoot) {
|
|
widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
|
|
win->widgetRoot->w = 0; // force layout pass
|
|
}
|
|
|
|
// Designer always needs full repaint (handles must be erased/redrawn)
|
|
win->paintNeeded = PAINT_FULL;
|
|
widgetOnPaint(win, dirtyArea);
|
|
|
|
// Then draw selection handles on top
|
|
dsgnPaintOverlay(&sDesigner);
|
|
}
|
|
|
|
|
|
// onFormWinResize -- update form dimensions when the design window is
|
|
// resized; only a real size change makes the form dirty.
|
|
static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH) {
|
|
// Let the widget system handle the layout recalculation
|
|
widgetOnResize(win, newW, newH);
|
|
|
|
// Store the outer window dimensions (win->w/h), not the content
|
|
// dimensions (newW/newH), since dvxResizeWindow takes outer dims.
|
|
if (sDesigner.form && (sDesigner.form->width != win->w || sDesigner.form->height != win->h)) {
|
|
bool wasDirty = sDesigner.form->dirty;
|
|
|
|
sDesigner.form->width = win->w;
|
|
sDesigner.form->height = win->h;
|
|
sDesigner.form->dirty = true;
|
|
prpRefresh(&sDesigner);
|
|
|
|
if (!wasDirty) {
|
|
updateDirtyIndicators();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void onGutterClick(WidgetT *w, int32_t lineNum) {
|
|
(void)w;
|
|
toggleBreakpointLine(lineNum);
|
|
}
|
|
|
|
|
|
// onImmediateKeyDown -- Enter in the Immediate window evaluates the line
|
|
// it was pressed on. The textarea has already split that line at the
|
|
// caret, so the statement is the line above the caret joined with the
|
|
// part the split moved down (empty when Enter was pressed at the end).
|
|
// A full window cannot take the newline, so nothing was split: the user
|
|
// is told instead of evaluating the wrong lines.
|
|
static void onImmediateKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
|
|
(void)w;
|
|
(void)shift;
|
|
|
|
if (keyCode != KEY_ENTER || !sImmediate) {
|
|
return;
|
|
}
|
|
|
|
const char *text = wgtGetText(sImmediate);
|
|
int32_t caretLine = wgtTextAreaGetCursorLine(sImmediate);
|
|
|
|
// The statement is on the line above the caret; there is none on line 1
|
|
if (!text || caretLine <= 1) {
|
|
return;
|
|
}
|
|
|
|
if ((int32_t)strlen(text) >= IDE_MAX_IMM - 1) {
|
|
setStatus("The Immediate window is full; delete some text and press Enter again.");
|
|
return;
|
|
}
|
|
|
|
char expr[IDE_IMM_LINE_BUF];
|
|
int32_t len = lineCopy(text, caretLine - 1, expr, (int32_t)sizeof(expr));
|
|
|
|
lineCopy(text, caretLine, expr + len, (int32_t)sizeof(expr) - len);
|
|
|
|
if (*dvxSkipWs(expr) != '\0') {
|
|
evaluateImmediate(expr);
|
|
}
|
|
}
|
|
|
|
|
|
// The IDE keeps sImmWin/sImmediate for its whole lifetime; destroying
|
|
// the window here would leave them dangling.
|
|
static void onImmWinClose(WindowT *win) {
|
|
dvxHideWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onLocalsClose(WindowT *win) {
|
|
dvxHideWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onMenu(WindowT *win, int32_t menuId) {
|
|
(void)win;
|
|
|
|
handleFileCmd(menuId);
|
|
handleEditCmd(menuId);
|
|
handleRunCmd(menuId);
|
|
handleViewCmd(menuId);
|
|
handleWindowCmd(menuId);
|
|
handleProjectCmd(menuId);
|
|
|
|
if (menuId == CMD_PREFERENCES) {
|
|
showPreferencesDialog();
|
|
}
|
|
|
|
if (menuId == CMD_DEBUG_LAYOUT && sWin && sWin->menuBar) {
|
|
wgtSetDebugLayout(sAc, wmMenuItemIsChecked(sWin->menuBar, CMD_DEBUG_LAYOUT));
|
|
}
|
|
|
|
if (menuId == CMD_HELP_CONTENTS) {
|
|
shellOpenHelp(sAc, sIdeHelpFile);
|
|
}
|
|
|
|
if (menuId == CMD_HELP_API) {
|
|
shellOpenHelp(sAc, DVX_SYSTEM_HELP_FILE_LITERAL);
|
|
}
|
|
|
|
if (menuId == CMD_HELP_ABOUT) {
|
|
dvxMessageBox(sAc, "About DVX BASIC",
|
|
"DVX BASIC 1.0\n"
|
|
"BASIC Development Environment\n"
|
|
"for the DVX GUI System\n"
|
|
"\n"
|
|
"Copyright 2026 Scott Duensing",
|
|
MB_OK | MB_ICONINFO);
|
|
}
|
|
}
|
|
|
|
|
|
// onObjDropdownChange -- update the Event dropdown when the Object
|
|
// selection changes.
|
|
|
|
static void onObjDropdownChange(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
int32_t objIdx = wgtDropdownGetSelected(sObjDropdown);
|
|
|
|
if (objIdx < 0 || objIdx >= (int32_t)arrlen(sObjItems)) {
|
|
return;
|
|
}
|
|
|
|
const char *selObj = sObjItems[objIdx];
|
|
|
|
// A WithEvents variable's events are its class's. Read first: reading
|
|
// the editor's own class rebuilds sProcTable, which the list of events
|
|
// with code below points into.
|
|
const IdeWithEventsT *withEvents = findWithEvents(selObj);
|
|
IdeClassEventT *classEvents = NULL;
|
|
|
|
if (withEvents) {
|
|
classEventsOf(withEvents->className, &classEvents);
|
|
}
|
|
|
|
// Collect which events already have code
|
|
evtItemsReset();
|
|
|
|
int32_t procCount = (int32_t)arrlen(sProcTable);
|
|
const char **existingEvts = NULL; // stb_ds temp array
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (strcasecmp(sProcTable[i].objName, selObj) == 0) {
|
|
arrput(existingEvts, sProcTable[i].evtName);
|
|
}
|
|
}
|
|
|
|
if (strcasecmp(selObj, IDE_GENERAL_SECTION) == 0) {
|
|
// Always include (Global) to access module-level code
|
|
evtItemAdd(IDE_GLOBAL_SECTION, false);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
|
|
evtItemAdd(existingEvts[i], false);
|
|
}
|
|
|
|
arrfree(existingEvts);
|
|
arrfree(classEvents);
|
|
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
// Sort procs after (Global)
|
|
if (evtCount > 2) {
|
|
qsort(sEvtItems + 1, evtCount - 1, sizeof(const char *), cmpStrPtrs);
|
|
}
|
|
|
|
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
|
|
wgtDropdownSetSelected(sEvtDropdown, 0);
|
|
onEvtDropdownChange(sEvtDropdown);
|
|
|
|
return;
|
|
}
|
|
|
|
// What the object is: the form, a menu item (only event is Click), a
|
|
// control, the UserControl object of a .ctl
|
|
const char *objType = objectType(selObj);
|
|
bool ucObject = strcmp(objType, BAS_UC_OBJECT) == 0;
|
|
|
|
// The events of this kind of object (the master table's, then the
|
|
// widget's own: basCtlEventAt, as designer.bas lists them), a
|
|
// WithEvents variable's class's Events, or the UserControl events the
|
|
// compiler knows (NULL-terminated stb_ds temp array)
|
|
const char **availEvents = NULL;
|
|
|
|
if (withEvents) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(classEvents); i++) {
|
|
arrput(availEvents, classEvents[i].name);
|
|
}
|
|
} else if (ucObject) {
|
|
for (int32_t i = 0; basUcEventInfo(i); i++) {
|
|
arrput(availEvents, basUcEventInfo(i)->name);
|
|
}
|
|
} else {
|
|
for (int32_t i = 0; basCtlEventAt(objType, i); i++) {
|
|
arrput(availEvents, basCtlEventAt(objType, i));
|
|
}
|
|
}
|
|
|
|
arrput(availEvents, NULL);
|
|
|
|
// Bracketed: no handler written yet
|
|
for (int32_t i = 0; availEvents[i]; i++) {
|
|
bool hasCode = false;
|
|
|
|
for (int32_t j = 0; j < (int32_t)arrlen(existingEvts); j++) {
|
|
if (strcasecmp(existingEvts[j], availEvents[i]) == 0) {
|
|
hasCode = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
evtItemAdd(availEvents[i], !hasCode);
|
|
}
|
|
|
|
// Add any existing events not in the standard/widget list (custom subs)
|
|
for (int32_t i = 0; i < (int32_t)arrlen(existingEvts); i++) {
|
|
bool alreadyListed = false;
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
for (int32_t j = 0; j < evtCount; j++) {
|
|
if (evtLabelMatches(sEvtItems[j], existingEvts[i])) {
|
|
alreadyListed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!alreadyListed) {
|
|
evtItemAdd(existingEvts[i], false);
|
|
}
|
|
}
|
|
|
|
arrfree(existingEvts);
|
|
arrfree(availEvents);
|
|
|
|
// The default event of a WithEvents variable: its class's first
|
|
const char *withEventsDefault = NULL;
|
|
char firstEvent[BAS_MAX_IDENT];
|
|
|
|
if (arrlen(classEvents) > 0) {
|
|
snprintf(firstEvent, sizeof(firstEvent), "%s", classEvents[0].name);
|
|
withEventsDefault = firstEvent;
|
|
}
|
|
|
|
arrfree(classEvents);
|
|
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
// Sort: implemented events first, then unimplemented, alphabetical within each
|
|
if (evtCount > 1) {
|
|
qsort(sEvtItems, evtCount, sizeof(const char *), cmpEvtPtrs);
|
|
}
|
|
|
|
wgtDropdownSetItems(sEvtDropdown, sEvtItems, evtCount);
|
|
|
|
if (evtCount > 0) {
|
|
// Find the first implemented event (no brackets)
|
|
int32_t selectIdx = -1;
|
|
|
|
for (int32_t i = 0; i < evtCount; i++) {
|
|
if (sEvtItems[i][0] != '[') {
|
|
selectIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (selectIdx >= 0) {
|
|
// Navigate to the first implemented event
|
|
wgtDropdownSetSelected(sEvtDropdown, selectIdx);
|
|
onEvtDropdownChange(sEvtDropdown);
|
|
} else {
|
|
// No implemented events -- find and select the default event
|
|
// for this kind of object, which will create its skeleton
|
|
const char *defEvt = withEventsDefault;
|
|
|
|
if (ucObject) {
|
|
defEvt = IDE_UC_DEFAULT_EVENT;
|
|
} else if (!withEvents) {
|
|
defEvt = basCtlDefaultEvent(objType);
|
|
}
|
|
|
|
if (!defEvt) {
|
|
defEvt = IDE_FALLBACK_EVENT;
|
|
}
|
|
|
|
// Find the default event in the list (it will be bracketed)
|
|
for (int32_t i = 0; i < evtCount; i++) {
|
|
if (evtLabelMatches(sEvtItems[i], defEvt)) {
|
|
wgtDropdownSetSelected(sEvtDropdown, i);
|
|
onEvtDropdownChange(sEvtDropdown);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Hide (not destroy): sOutWin/sOutput stay bound for PRINT output.
|
|
static void onOutWinClose(WindowT *win) {
|
|
dvxHideWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onPrefsCancel(WidgetT *w) {
|
|
(void)w;
|
|
sPrefsDlg.done = true;
|
|
}
|
|
|
|
|
|
// onPrefsClose -- the window's close box cancels; showPreferencesDialog
|
|
// destroys the window once its loop sees done.
|
|
static void onPrefsClose(WindowT *win) {
|
|
(void)win;
|
|
sPrefsDlg.done = true;
|
|
}
|
|
|
|
|
|
static void onPrefsOk(WidgetT *w) {
|
|
(void)w;
|
|
sPrefsDlg.accepted = true;
|
|
sPrefsDlg.done = true;
|
|
}
|
|
|
|
|
|
static void onPrjFileDblClick(int32_t fileIdx) {
|
|
activateFile(fileIdx, ViewAutoE);
|
|
}
|
|
|
|
|
|
static void onProjectWinClose(WindowT *win) {
|
|
prjDestroyWindow(sAc, win);
|
|
sProjectWin = NULL;
|
|
}
|
|
|
|
|
|
static void onReplace(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sEditor || !sFindInput || !sReplInput || !isReplaceEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const char *needle = wgtGetText(sFindInput);
|
|
const char *repl = wgtGetText(sReplInput);
|
|
|
|
if (!needle || !needle[0]) {
|
|
return;
|
|
}
|
|
|
|
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
|
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
|
|
|
|
bool caseSens = getFindMatchCase();
|
|
bool forward = getFindForward();
|
|
|
|
// Single Replace always acts on the editor currently shown: if its
|
|
// selection matches the search term, splice in the replacement and
|
|
// advance to the next match; otherwise just find next. Find Next's
|
|
// own scope/direction handling (which may switch procs or files) is
|
|
// only reached when there is no matching selection to replace, so a
|
|
// cross-file single replace stays unambiguous -- consistent with how
|
|
// onReplaceAll treats each scope as a sequence of in-editor passes.
|
|
if (wgtTextAreaReplaceSelection(sEditor, sFindText, sReplaceText, caseSens, forward)) {
|
|
return;
|
|
}
|
|
|
|
onFindNext(w);
|
|
}
|
|
|
|
|
|
static void onReplaceAll(WidgetT *w) {
|
|
(void)w;
|
|
|
|
if (!sFindInput || !sReplInput || !isReplaceEnabled()) {
|
|
return;
|
|
}
|
|
|
|
const char *needle = wgtGetText(sFindInput);
|
|
const char *repl = wgtGetText(sReplInput);
|
|
|
|
if (!needle || !needle[0]) {
|
|
return;
|
|
}
|
|
|
|
snprintf(sFindText, sizeof(sFindText), "%s", needle);
|
|
snprintf(sReplaceText, sizeof(sReplaceText), "%s", repl ? repl : "");
|
|
|
|
bool caseSens = getFindMatchCase();
|
|
FindScopeE scope = getFindScope();
|
|
int32_t totalCount = 0;
|
|
|
|
if (scope == ScopeFuncE && sEditor) {
|
|
totalCount = wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens);
|
|
} else if (scope == ScopeProjE) {
|
|
int32_t needleLen = (int32_t)strlen(sFindText);
|
|
|
|
// Every file is searched in its project buffer (stashCurrentFile
|
|
// also saves the editor into its sections first)
|
|
if (!stashCurrentFile()) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
// Skip files whose code has no match at all
|
|
if (!fileCodeContains(i, sFindText, needleLen, caseSens)) {
|
|
continue;
|
|
}
|
|
|
|
if (!activateFile(i, ViewCodeE)) {
|
|
break;
|
|
}
|
|
|
|
totalCount += replaceInSections(NULL, caseSens);
|
|
}
|
|
} else if (scope == ScopeFileE && sEditor) {
|
|
totalCount = replaceInSections(NULL, caseSens);
|
|
} else if (scope == ScopeObjE && sEditor) {
|
|
char objName[BAS_MAX_IDENT];
|
|
|
|
currentObjName(objName, sizeof(objName));
|
|
|
|
if (objName[0]) {
|
|
totalCount = replaceInSections(objName, caseSens);
|
|
}
|
|
}
|
|
|
|
// A long replace let DVX go on (replaceInSections pumps per procedure)
|
|
ideBusyPumpEnd();
|
|
|
|
char statusBuf[BAS_MAX_IDENT];
|
|
snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
|
|
setStatus(statusBuf);
|
|
}
|
|
|
|
|
|
static void onReplCheckChange(WidgetT *w) {
|
|
(void)w;
|
|
bool show = isReplaceEnabled();
|
|
|
|
if (sReplInput) {
|
|
sReplInput->enabled = show;
|
|
}
|
|
|
|
if (sBtnReplace) {
|
|
sBtnReplace->enabled = show;
|
|
}
|
|
|
|
if (sBtnReplAll) {
|
|
sBtnReplAll->enabled = show;
|
|
}
|
|
|
|
if (sFindWin) {
|
|
dvxInvalidateWindow(sAc, sFindWin);
|
|
}
|
|
}
|
|
|
|
|
|
static void onTbCode(WidgetT *w) {
|
|
(void)w;
|
|
handleViewCmd(CMD_VIEW_CODE);
|
|
}
|
|
|
|
|
|
static void onTbDebug(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_DEBUG);
|
|
}
|
|
|
|
|
|
static void onTbDesign(WidgetT *w) {
|
|
(void)w;
|
|
handleViewCmd(CMD_VIEW_DESIGN);
|
|
}
|
|
|
|
|
|
static void onTbOpen(WidgetT *w) {
|
|
(void)w;
|
|
handleProjectCmd(CMD_PRJ_OPEN);
|
|
}
|
|
|
|
|
|
static void onTbRun(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_RUN);
|
|
}
|
|
|
|
|
|
static void onTbRunToCur(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_RUN_TO_CURSOR);
|
|
}
|
|
|
|
|
|
static void onTbSave(WidgetT *w) {
|
|
(void)w;
|
|
handleFileCmd(CMD_SAVE);
|
|
}
|
|
|
|
|
|
static void onTbStepInto(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_STEP_INTO);
|
|
}
|
|
|
|
|
|
static void onTbStepOut(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_STEP_OUT);
|
|
}
|
|
|
|
|
|
static void onTbStepOver(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_STEP_OVER);
|
|
}
|
|
|
|
|
|
static void onTbStop(WidgetT *w) {
|
|
(void)w;
|
|
handleRunCmd(CMD_STOP);
|
|
}
|
|
|
|
|
|
// onUcPropChanged -- PropertyChanged in the code of a UserControl on the
|
|
// designer: the properties window reads the values again
|
|
static void onUcPropChanged(WidgetT *w, const char *propName) {
|
|
(void)w;
|
|
(void)propName;
|
|
|
|
if (sPropsWin && sDesigner.form) {
|
|
prpRefresh(&sDesigner);
|
|
}
|
|
}
|
|
|
|
|
|
static void onWatchClose(WindowT *win) {
|
|
dvxHideWindow(sAc, win);
|
|
}
|
|
|
|
|
|
static void onWatchInputKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
|
|
(void)w;
|
|
(void)shift;
|
|
|
|
if (keyCode != '\r' && keyCode != '\n') {
|
|
return;
|
|
}
|
|
|
|
if (!sWatchInput) {
|
|
return;
|
|
}
|
|
|
|
const char *text = wgtGetText(sWatchInput);
|
|
|
|
if (!text || !text[0]) {
|
|
return;
|
|
}
|
|
|
|
// Add to watch list
|
|
char *copy = strdup(text);
|
|
|
|
if (!copy) {
|
|
return;
|
|
}
|
|
|
|
arrput(sWatchExprs, copy);
|
|
updateWatchWindow();
|
|
|
|
// Clear input
|
|
wgtSetText(sWatchInput, "");
|
|
}
|
|
|
|
|
|
static void onWatchListDblClick(WidgetT *w) {
|
|
(void)w;
|
|
watchEditSelected();
|
|
}
|
|
|
|
|
|
static void onWatchListKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
|
|
(void)w;
|
|
(void)shift;
|
|
|
|
// Enter -- edit selected item
|
|
if (keyCode == '\r' || keyCode == '\n') {
|
|
watchEditSelected();
|
|
return;
|
|
}
|
|
|
|
// Delete key (scancode 0x53 with extended flag)
|
|
if (keyCode != KEY_DELETE && keyCode != KEY_ASCII_DEL) {
|
|
return;
|
|
}
|
|
|
|
if (!sWatchList) {
|
|
return;
|
|
}
|
|
|
|
int32_t sel = wgtListViewGetSelected(sWatchList);
|
|
|
|
if (sel < 0 || sel >= (int32_t)arrlen(sWatchExprs)) {
|
|
return;
|
|
}
|
|
|
|
free(sWatchExprs[sel]);
|
|
arrdel(sWatchExprs, sel);
|
|
|
|
updateWatchWindow();
|
|
}
|
|
|
|
|
|
// openAnyPath -- open a project (.dbp) in place of the current one, or
|
|
// a .bas/.frm (see openSinglePath)
|
|
static void openAnyPath(const char *path) {
|
|
if (dvxHasExt(platformPathBaseName(path), BAS_DBP_EXT)) {
|
|
if (promptAndSave()) {
|
|
openProjectPath(path);
|
|
}
|
|
} else {
|
|
openSinglePath(path);
|
|
}
|
|
}
|
|
|
|
|
|
static void openFindDialog(bool showReplace) {
|
|
if (sFindWin) {
|
|
// Already open -- just toggle replace mode and raise
|
|
if (sReplCheck) {
|
|
wgtCheckboxSetChecked(sReplCheck, showReplace);
|
|
onReplCheckChange(sReplCheck);
|
|
}
|
|
|
|
dvxRaiseWindow(sAc, sFindWin);
|
|
return;
|
|
}
|
|
|
|
sFindWin = dvxCreateWindowCentered(sAc, "Find / Replace", IDE_FIND_WIN_W, IDE_FIND_WIN_H, false);
|
|
|
|
if (!sFindWin) {
|
|
return;
|
|
}
|
|
|
|
sFindWin->onClose = onFindClose;
|
|
sFindWin->onMenu = onMenu;
|
|
sFindWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
|
|
if (!findDialogBuild(showReplace)) {
|
|
closeFindDialog();
|
|
ideErrorBox("Not enough memory to open Find / Replace.");
|
|
return;
|
|
}
|
|
|
|
// Set initial replace enable state
|
|
onReplCheckChange(sReplCheck);
|
|
dvxFitWindow(sAc, sFindWin);
|
|
}
|
|
|
|
|
|
static void openProject(void) {
|
|
if (!promptAndSave()) {
|
|
return;
|
|
}
|
|
|
|
FileFilterT filters[] = {
|
|
{ "Project Files (*.dbp)" },
|
|
{ "All Files (*.*)" }
|
|
};
|
|
|
|
char path[DVX_MAX_PATH];
|
|
|
|
if (!dvxFileDialog(sAc, "Open Project", FD_OPEN, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
|
|
return;
|
|
}
|
|
|
|
openProjectPath(path);
|
|
}
|
|
|
|
|
|
// openProjectPath -- open a .dbp in place of the current project. The
|
|
// new project is loaded first; if that fails the current one stays open
|
|
// and the path is dropped from the Recent list.
|
|
static void openProjectPath(const char *path) {
|
|
PrjStateT *loaded = (PrjStateT *)malloc(sizeof(PrjStateT));
|
|
|
|
if (!loaded) {
|
|
ideErrorBox("Not enough memory to open the project.");
|
|
return;
|
|
}
|
|
|
|
prjInit(loaded);
|
|
|
|
// prjLoad fails for a missing .dbp, so a stale recent entry is
|
|
// rejected here instead of opening as a phantom empty project.
|
|
if (!prjLoad(loaded, path)) {
|
|
prjClose(loaded);
|
|
free(loaded);
|
|
ideErrorBox("Could not open project file.");
|
|
recentRemove(path);
|
|
return;
|
|
}
|
|
|
|
closeProject();
|
|
sProject = *loaded;
|
|
free(loaded);
|
|
|
|
prjLoadAllFiles(&sProject, sAc);
|
|
ensureProjectWindow(false);
|
|
updateMainTitle();
|
|
setStatus("Project loaded.");
|
|
designCtlRefresh();
|
|
updateProjectMenuState();
|
|
recentAdd(path);
|
|
}
|
|
|
|
|
|
// openSinglePath -- open a .bas/.cls/.frm: added to the current project, or
|
|
// used to create an implicit project when none is open.
|
|
static void openSinglePath(const char *path) {
|
|
if (hasProject()) {
|
|
if (addFileToProject(path)) {
|
|
recentAdd(path);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!promptAndSave()) {
|
|
return;
|
|
}
|
|
|
|
ensureProject(path);
|
|
ensureProjectWindow(true);
|
|
updateProjectMenuState();
|
|
recentAdd(path);
|
|
}
|
|
|
|
|
|
// outputAppend -- add program/report text to the Output buffer, dropping
|
|
// the oldest lines when it is full so the newest text always shows
|
|
static void outputAppend(const char *text, int32_t len) {
|
|
sOutputLen = basDbgAppendDropping(sOutputBuf, IDE_MAX_OUTPUT, sOutputLen, text, len);
|
|
sOutputPending = true;
|
|
}
|
|
|
|
|
|
// outputLog -- with Output to Log on, log a compile or runtime report
|
|
static void outputLog(const char *text) {
|
|
if (sOutputToLog && text[0]) {
|
|
dvxLog("BASIC: %s", text);
|
|
}
|
|
}
|
|
|
|
|
|
// packProcLayout -- normalize source text to the canonical proc layout
|
|
// (see joinProcArrays). Returns a malloc'd string or NULL.
|
|
static char *packProcLayout(const char *source) {
|
|
char *general = NULL;
|
|
char **procs = NULL;
|
|
|
|
if (!splitProcs(source, &general, &procs, NULL)) {
|
|
return NULL;
|
|
}
|
|
|
|
char *packed = joinProcArrays(general, procs);
|
|
|
|
free(general);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(procs); i++) {
|
|
free(procs[i]);
|
|
}
|
|
|
|
arrfree(procs);
|
|
return packed;
|
|
}
|
|
|
|
|
|
// parseProcs -- split source into (General) + per-procedure buffers
|
|
|
|
static void parseProcs(const char *source) {
|
|
freeProcBufs();
|
|
|
|
// Out of memory: keep the text whole in (General) rather than lose it
|
|
if (!splitProcs(source ? source : "", &sGeneralBuf, &sProcBufs, NULL)) {
|
|
sGeneralBuf = strdup(source ? source : "");
|
|
}
|
|
}
|
|
|
|
|
|
// pathExists -- true when a file can be opened for reading
|
|
// prefsBuild -- build the Preferences dialog's widgets into win and load
|
|
// the current settings. Returns false if any widget could not be made.
|
|
static bool prefsBuild(WindowT *win) {
|
|
WidgetT *root = wgtInitWindow(sAc, win);
|
|
WidgetT *tabs = root ? wgtTabControl(root) : NULL;
|
|
|
|
if (!tabs) {
|
|
return false;
|
|
}
|
|
|
|
root->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
tabs->weight = WGT_WEIGHT_FILL;
|
|
|
|
// ======== General tab ========
|
|
WidgetT *generalPage = wgtTabPage(tabs, "General");
|
|
WidgetT *edFrame = generalPage ? wgtFrame(generalPage, "Editor") : NULL;
|
|
|
|
if (!edFrame) {
|
|
return false;
|
|
}
|
|
|
|
generalPage->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
edFrame->spacing = wgtPixels(IDE_WIDGET_SPACING_SM);
|
|
|
|
sPrefsDlg.renameSkipComments = wgtCheckbox(edFrame, "Skip comments/strings when renaming");
|
|
sPrefsDlg.optionExplicit = wgtCheckbox(edFrame, "OPTION EXPLICIT default for new projects");
|
|
|
|
WidgetT *tabRow = wgtHBox(edFrame);
|
|
|
|
if (!sPrefsDlg.renameSkipComments || !sPrefsDlg.optionExplicit || !tabRow || !wgtLabel(tabRow, "Tab width:")) {
|
|
return false;
|
|
}
|
|
|
|
wgtCheckboxSetChecked(sPrefsDlg.renameSkipComments, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, true));
|
|
wgtCheckboxSetChecked(sPrefsDlg.optionExplicit, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, false));
|
|
tabRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
|
|
sPrefsDlg.tabWidthInput = wgtTextInput(tabRow, IDE_PREFS_TAB_INPUT_MAX);
|
|
sPrefsDlg.useSpaces = wgtCheckbox(edFrame, "Insert spaces instead of tabs");
|
|
|
|
if (!sPrefsDlg.tabWidthInput || !sPrefsDlg.useSpaces) {
|
|
return false;
|
|
}
|
|
|
|
char tabBuf[IDE_TAB_WIDTH_BUF];
|
|
|
|
snprintf(tabBuf, sizeof(tabBuf), "%d", (int)prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH));
|
|
sPrefsDlg.tabWidthInput->maxW = wgtPixels(IDE_PREFS_TAB_INPUT_W);
|
|
wgtSetText(sPrefsDlg.tabWidthInput, tabBuf);
|
|
wgtCheckboxSetChecked(sPrefsDlg.useSpaces, prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, true));
|
|
|
|
// Project Defaults section
|
|
WidgetT *prjFrame = wgtFrame(generalPage, "New Project Defaults");
|
|
|
|
if (!prjFrame) {
|
|
return false;
|
|
}
|
|
|
|
prjFrame->spacing = wgtPixels(IDE_WIDGET_SPACING_SM);
|
|
prjFrame->weight = WGT_WEIGHT_FILL;
|
|
|
|
sPrefsDlg.defAuthor = prefsTextRow(prjFrame, "Author:", IDE_PREFS_TEXT_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, ""));
|
|
sPrefsDlg.defPublisher = prefsTextRow(prjFrame, "Publisher:", IDE_PREFS_TEXT_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, ""));
|
|
sPrefsDlg.defVersion = prefsTextRow(prjFrame, "Version:", IDE_PREFS_VERSION_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, IDE_PREF_DEFAULT_VERSION));
|
|
sPrefsDlg.defCopyright = prefsTextRow(prjFrame, "Copyright:", IDE_PREFS_TEXT_MAX, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, ""));
|
|
|
|
// One line, as long as a project's [Project] Description can be: an
|
|
// INI value is a single line (see prjSingleLine).
|
|
sPrefsDlg.defDescription = prefsTextRow(prjFrame, "Description:", PRJ_MAX_DESC - 1, prefsGetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, ""));
|
|
|
|
if (!sPrefsDlg.defAuthor || !sPrefsDlg.defPublisher || !sPrefsDlg.defVersion || !sPrefsDlg.defCopyright || !sPrefsDlg.defDescription) {
|
|
return false;
|
|
}
|
|
|
|
// ======== Colors tab ========
|
|
// Load current colors from prefs (or defaults)
|
|
for (int32_t i = 0; i < TEXT_SYNTAX_MAX; i++) {
|
|
char key[IDE_PREF_KEY_BUF];
|
|
snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i);
|
|
sPrefsDlg.syntaxColors[i] = (uint32_t)prefsGetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)textEditDefaultSyntaxRgb((uint8_t)i));
|
|
}
|
|
|
|
WidgetT *colorsPage = wgtTabPage(tabs, "Colors");
|
|
WidgetT *colorsHBox = colorsPage ? wgtHBox(colorsPage) : NULL;
|
|
|
|
// Left: color list. Right: swatch, values and Change...
|
|
sPrefsDlg.colorList = colorsHBox ? wgtListBox(colorsHBox) : NULL;
|
|
|
|
WidgetT *colorBox = sPrefsDlg.colorList ? wgtVBox(colorsHBox) : NULL;
|
|
WidgetT *caption = colorBox ? wgtLabel(colorBox, "Color:") : NULL;
|
|
|
|
sPrefsDlg.colorSwatch = caption ? wgtCanvas(colorBox, IDE_SWATCH_W, IDE_SWATCH_H) : NULL;
|
|
sPrefsDlg.lblR = sPrefsDlg.colorSwatch ? wgtLabel(colorBox, "") : NULL;
|
|
sPrefsDlg.lblG = sPrefsDlg.lblR ? wgtLabel(colorBox, "") : NULL;
|
|
sPrefsDlg.lblB = sPrefsDlg.lblG ? wgtLabel(colorBox, "") : NULL;
|
|
|
|
WidgetT *changeBtn = sPrefsDlg.lblB ? wgtButton(colorBox, "Change...") : NULL;
|
|
|
|
if (!changeBtn) {
|
|
return false;
|
|
}
|
|
|
|
colorsPage->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
colorsHBox->spacing = wgtPixels(IDE_WIDGET_SPACING_LG);
|
|
colorsHBox->weight = WGT_WEIGHT_FILL;
|
|
sPrefsDlg.colorList->weight = WGT_WEIGHT_FILL;
|
|
sPrefsDlg.colorList->onChange = onColorListChange;
|
|
sPrefsDlg.colorList->onDblClick = onColorChange;
|
|
colorBox->spacing = wgtPixels(IDE_WIDGET_SPACING_SM);
|
|
colorBox->weight = WGT_WEIGHT_FILL;
|
|
changeBtn->onClick = onColorChange;
|
|
wgtListBoxSetItems(sPrefsDlg.colorList, sSyntaxColorNames, TEXT_SYNTAX_MAX);
|
|
|
|
// Start on the Keywords entry (the most commonly customized)
|
|
wgtListBoxSetSelected(sPrefsDlg.colorList, TEXT_SYNTAX_KEYWORD);
|
|
prefsUpdateColorSwatch();
|
|
wgtTabControlSetActive(tabs, 0);
|
|
|
|
// ---- OK / Cancel ----
|
|
WidgetT *btnRow = wgtHBox(root);
|
|
WidgetT *btnOk = btnRow ? wgtButton(btnRow, "OK") : NULL;
|
|
WidgetT *btnCancel = btnOk ? wgtButton(btnRow, "Cancel") : NULL;
|
|
|
|
if (!btnCancel) {
|
|
return false;
|
|
}
|
|
|
|
btnRow->spacing = wgtPixels(IDE_WIDGET_SPACING_LG);
|
|
btnRow->align = AlignEndE;
|
|
btnOk->minW = wgtPixels(IDE_PREFS_BUTTON_W);
|
|
btnOk->onClick = onPrefsOk;
|
|
btnCancel->minW = wgtPixels(IDE_PREFS_BUTTON_W);
|
|
btnCancel->onClick = onPrefsCancel;
|
|
return true;
|
|
}
|
|
|
|
|
|
// prefsSetChannelLabel -- "Channel value" under the Colors tab swatch
|
|
static void prefsSetChannelLabel(WidgetT *label, const char *channel, uint8_t value) {
|
|
char buf[IDE_RGB_LABEL_BUF];
|
|
|
|
snprintf(buf, sizeof(buf), "%s %d", channel, (int)value);
|
|
wgtSetText(label, buf);
|
|
}
|
|
|
|
|
|
// prefsTextRow -- "Caption: [input]" row holding value. Returns the input,
|
|
// NULL if a widget could not be made.
|
|
static WidgetT *prefsTextRow(WidgetT *parent, const char *caption, int32_t maxLen, const char *value) {
|
|
WidgetT *row = wgtHBox(parent);
|
|
WidgetT *label = row ? wgtLabel(row, caption) : NULL;
|
|
WidgetT *input = label ? wgtTextInput(row, maxLen) : NULL;
|
|
|
|
if (!input) {
|
|
return NULL;
|
|
}
|
|
|
|
row->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
label->minW = wgtPixels(IDE_PREFS_LABEL_W);
|
|
input->weight = WGT_WEIGHT_FILL;
|
|
wgtSetText(input, value);
|
|
return input;
|
|
}
|
|
|
|
|
|
// Shows the selected syntax color in the swatch and as numbers.
|
|
static void prefsUpdateColorSwatch(void) {
|
|
int32_t idx = wgtListBoxGetSelected(sPrefsDlg.colorList);
|
|
|
|
if (idx < 0 || idx >= TEXT_SYNTAX_MAX) {
|
|
return;
|
|
}
|
|
|
|
uint32_t rgb = sPrefsDlg.syntaxColors[idx];
|
|
uint8_t r = DVX_RGB_RED(rgb);
|
|
uint8_t g = DVX_RGB_GREEN(rgb);
|
|
uint8_t b = DVX_RGB_BLUE(rgb);
|
|
|
|
prefsSetChannelLabel(sPrefsDlg.lblR, "Red", r);
|
|
prefsSetChannelLabel(sPrefsDlg.lblG, "Green", g);
|
|
prefsSetChannelLabel(sPrefsDlg.lblB, "Blue", b);
|
|
wgtCanvasClear(sPrefsDlg.colorSwatch, packColor(&sAc->display, r, g, b));
|
|
}
|
|
|
|
|
|
// printCallback -- PRINT from the running program. The text is buffered
|
|
// (the pane is refreshed once per event-loop pass, see flushOutput) and,
|
|
// with Output to Log on, logged one line at a time.
|
|
static void printCallback(void *ctx, const char *text, bool newline) {
|
|
(void)ctx;
|
|
|
|
if (!text) {
|
|
return;
|
|
}
|
|
|
|
int32_t textLen = (int32_t)strlen(text);
|
|
|
|
outputAppend(text, textLen);
|
|
|
|
if (newline) {
|
|
outputAppend("\n", 1);
|
|
}
|
|
|
|
if (!sOutputToLog) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = 0; i < textLen; i++) {
|
|
if (sLogLineLen >= (int32_t)sizeof(sLogLine) - 1) {
|
|
logFlushLine();
|
|
}
|
|
|
|
sLogLine[sLogLineLen++] = text[i];
|
|
}
|
|
|
|
if (newline) {
|
|
logFlushLine();
|
|
}
|
|
}
|
|
|
|
|
|
// procBodyEmpty -- true when a proc's text has nothing but blank lines
|
|
// between its declaration and its End Sub / End Function
|
|
static bool procBodyEmpty(const char *procText) {
|
|
bool isSub = false;
|
|
const char *p = dvxSkipWs(procText);
|
|
const char *afterKw = basProcDeclAt(p, &isSub);
|
|
const char *endTag = (afterKw && !isSub) ? BAS_PROC_KW_END_FUNCTION : BAS_PROC_KW_END_SUB;
|
|
|
|
for (p = skipLine(p); *p && !kwMatch(dvxSkipWs(p), endTag); p = skipLine(p)) {
|
|
const char *q = dvxSkipWs(p);
|
|
|
|
if (*q != '\n' && *q != '\r' && *q != '\0') {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// procBufContains -- true if needle occurs anywhere in a proc buffer.
|
|
static bool procBufContains(const char *hay, const char *needle, int32_t needleLen, bool caseSensitive) {
|
|
if (!hay) {
|
|
return false;
|
|
}
|
|
|
|
if (caseSensitive) {
|
|
return strstr(hay, needle) != NULL;
|
|
}
|
|
|
|
return findSubstrNoCase(hay, needle, needleLen) != NULL;
|
|
}
|
|
|
|
|
|
// procIndexByName -- sProcTable index of the proc objName/evtName, or -1
|
|
static int32_t procIndexByName(const char *objName, const char *evtName) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sProcTable); i++) {
|
|
if (strcasecmp(sProcTable[i].objName, objName) == 0 && strcasecmp(sProcTable[i].evtName, evtName) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// procIndexForCodeLine -- sProcTable index of the proc whose declaration
|
|
// through End line range holds codeLine, or -1 for the (General) section
|
|
static int32_t procIndexForCodeLine(int32_t codeLine) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sProcTable); i++) {
|
|
if (codeLine >= sProcTable[i].lineNum && codeLine <= sProcTable[i].endLineNum) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// procNameOf -- the name a proc's text declares (its first line)
|
|
static void procNameOf(const char *procText, char *out, int32_t outSize) {
|
|
bool isSub = false;
|
|
const char *afterKw = basProcDeclAt(dvxSkipWs(procText), &isSub);
|
|
|
|
basProcNameFromDecl(afterKw ? afterKw : procText, out, outSize);
|
|
}
|
|
|
|
|
|
// procStartLines -- first whole-file line of each proc in the canonical
|
|
// layout of general + procs (see joinProcArrays), as an stb_ds array the
|
|
// caller frees
|
|
static int32_t *procStartLines(const char *general, char **procs) {
|
|
int32_t *starts = NULL;
|
|
int32_t genLines = sectionLineCount(general);
|
|
int32_t line = genLines > 0 ? genLines + IDE_SECTION_GAP + 1 : 1;
|
|
|
|
for (int32_t p = 0; p < (int32_t)arrlen(procs); p++) {
|
|
arrput(starts, line);
|
|
line += sectionLineCount(procs[p]) + IDE_SECTION_GAP;
|
|
}
|
|
|
|
return starts;
|
|
}
|
|
|
|
|
|
// projectFileText -- the current text of project file fileIdx: its loaded
|
|
// buffer, else the file on disk read by prjReadFileText (returned in
|
|
// *outOwned too, for the caller to free). Returns NULL when the file
|
|
// cannot be used; *outMissing then tells whether it simply does not
|
|
// exist yet (a new file) rather than being unreadable or too large.
|
|
static const char *projectFileText(int32_t fileIdx, char **outOwned, bool *outMissing) {
|
|
PrjFileT *file = &sProject.files[fileIdx];
|
|
|
|
*outOwned = NULL;
|
|
*outMissing = false;
|
|
|
|
if (file->buffer) {
|
|
return file->buffer;
|
|
}
|
|
|
|
*outOwned = prjReadFileText(&sProject, fileIdx, outMissing);
|
|
return *outOwned;
|
|
}
|
|
|
|
|
|
// promptAndSave -- ask user to save, discard, or cancel.
|
|
// Returns true if the caller should proceed (user saved or discarded).
|
|
// Returns false if the user cancelled or a save failed.
|
|
|
|
static bool promptAndSave(void) {
|
|
if (!hasUnsavedData()) {
|
|
return true;
|
|
}
|
|
|
|
int32_t result = dvxPromptSave(sAc, IDE_MAIN_TITLE);
|
|
|
|
if (result == DVX_SAVE_YES) {
|
|
return saveAllModified();
|
|
}
|
|
|
|
return result == DVX_SAVE_NO;
|
|
}
|
|
|
|
|
|
// propsFollowActive -- the Properties window shows the header of the
|
|
// active file when it is a UserControl (its newest text: the editor's
|
|
// when it holds the file), else the designer's selection
|
|
static void propsFollowActive(void) {
|
|
int32_t idx = sProject.activeFileIdx;
|
|
|
|
if (idx < 0 || idx >= prjFileCount(&sProject) || sProject.files[idx].kind != PrjKindControlE) {
|
|
prpShowCtl(NULL, NULL);
|
|
return;
|
|
}
|
|
|
|
if (idx == sEditorFileIdx) {
|
|
stashEditor();
|
|
}
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(idx, &owned, &missing);
|
|
|
|
prpShowCtl(text, onCtlHeaderSet);
|
|
free(owned);
|
|
}
|
|
|
|
|
|
// rebuildProcTable -- one sProcTable entry per proc buffer (so table and
|
|
// buffer indices always agree) with its object/event split and its line
|
|
// range in the canonical layout. Procedure names are split on '_' into
|
|
// ObjectName and EventName (e.g. "Command1_Click") using the designer's
|
|
// known object names.
|
|
|
|
static void rebuildProcTable(void) {
|
|
arrsetlen(sProcTable, 0);
|
|
|
|
// Collect all known object names once: the designer form's, or in a
|
|
// .ctl the UserControl object
|
|
const char **objNames = NULL; // stb_ds temp array
|
|
|
|
if (editorIsControl()) {
|
|
arrput(objNames, BAS_UC_OBJECT);
|
|
} else if (sDesigner.form) {
|
|
arrput(objNames, sDesigner.form->name);
|
|
|
|
for (int32_t ci = 0; ci < (int32_t)arrlen(sDesigner.form->controls); ci++) {
|
|
arrput(objNames, sDesigner.form->controls[ci]->name);
|
|
}
|
|
|
|
for (int32_t mi = 0; mi < (int32_t)arrlen(sDesigner.form->menuItems); mi++) {
|
|
arrput(objNames, sDesigner.form->menuItems[mi].name);
|
|
}
|
|
}
|
|
|
|
int32_t *starts = procStartLines(sGeneralBuf, sProcBufs);
|
|
|
|
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs); p++) {
|
|
const char *text = sProcBufs[p];
|
|
bool isSub = false;
|
|
int32_t endOffset = DVX_MAX(sectionLineCount(text) - 1, 0);
|
|
char procName[BAS_MAX_IDENT];
|
|
|
|
procNameOf(text, procName, sizeof(procName));
|
|
|
|
if (basProcDeclAt(dvxSkipWs(text), &isSub)) {
|
|
basProcSkipToEnd(text, isSub, &endOffset);
|
|
}
|
|
|
|
IdeProcEntryT entry;
|
|
memset(&entry, 0, sizeof(entry));
|
|
entry.lineNum = starts[p];
|
|
entry.endLineNum = starts[p] + endOffset;
|
|
|
|
// Match proc name against known objects: form name, controls,
|
|
// menu items. Try each as a prefix followed by "_". This handles
|
|
// names with multiple underscores correctly (e.g., "cmdOK_Click"
|
|
// matches "cmdOK", not "This_Is_A_Dumb_Name" matching a control
|
|
// named "This").
|
|
bool isEvent = false;
|
|
|
|
for (int32_t oi = 0; oi < (int32_t)arrlen(objNames); oi++) {
|
|
int32_t objLen = (int32_t)strlen(objNames[oi]);
|
|
|
|
if (objLen > 0 && strncasecmp(procName, objNames[oi], objLen) == 0 && procName[objLen] == '_') {
|
|
snprintf(entry.objName, sizeof(entry.objName), "%s", objNames[oi]);
|
|
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName + objLen + 1);
|
|
isEvent = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!isEvent) {
|
|
snprintf(entry.objName, sizeof(entry.objName), "%s", IDE_GENERAL_SECTION);
|
|
snprintf(entry.evtName, sizeof(entry.evtName), "%s", procName);
|
|
}
|
|
|
|
arrput(sProcTable, entry);
|
|
}
|
|
|
|
arrfree(starts);
|
|
arrfree(objNames);
|
|
}
|
|
|
|
|
|
static void recentAdd(const char *path) {
|
|
if (!path || !path[0]) {
|
|
return;
|
|
}
|
|
|
|
// If already in the list, move it to the top
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
|
|
if (platformPathSame(sRecentFiles[i], path)) {
|
|
char *existing = sRecentFiles[i];
|
|
arrdel(sRecentFiles, i);
|
|
arrins(sRecentFiles, 0, existing);
|
|
recentSave();
|
|
recentRebuildMenu();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If at cap, drop the oldest entry (last)
|
|
while ((int32_t)arrlen(sRecentFiles) >= CMD_RECENT_MAX) {
|
|
int32_t last = (int32_t)arrlen(sRecentFiles) - 1;
|
|
free(sRecentFiles[last]);
|
|
arrdel(sRecentFiles, last);
|
|
}
|
|
|
|
// Insert new entry at the top
|
|
char *copy = strdup(path);
|
|
|
|
if (!copy) {
|
|
return;
|
|
}
|
|
|
|
arrins(sRecentFiles, 0, copy);
|
|
recentSave();
|
|
recentRebuildMenu();
|
|
}
|
|
|
|
|
|
static void recentLoad(void) {
|
|
// Clear any prior entries
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
|
|
free(sRecentFiles[i]);
|
|
}
|
|
|
|
arrsetlen(sRecentFiles, 0);
|
|
|
|
if (!sPrefs) {
|
|
return;
|
|
}
|
|
|
|
for (int32_t i = 0; i < CMD_RECENT_MAX; i++) {
|
|
char key[IDE_PREF_KEY_BUF];
|
|
snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i);
|
|
const char *val = prefsGetString(sPrefs, PREF_SEC_RECENT, key, "");
|
|
|
|
if (val[0]) {
|
|
char *copy = strdup(val);
|
|
|
|
if (copy) {
|
|
arrput(sRecentFiles, copy);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void recentOpen(int32_t index) {
|
|
if (index < 0 || index >= (int32_t)arrlen(sRecentFiles)) {
|
|
return;
|
|
}
|
|
|
|
// recentAdd reorders the list, so work from a private copy of the path
|
|
char path[DVX_MAX_PATH];
|
|
snprintf(path, sizeof(path), "%s", sRecentFiles[index]);
|
|
|
|
openAnyPath(path);
|
|
}
|
|
|
|
|
|
static void recentRebuildMenu(void) {
|
|
if (!sFileMenu) {
|
|
return;
|
|
}
|
|
|
|
// Truncate menu back to the base items (everything through Exit)
|
|
sFileMenu->itemCount = sFileMenuBase;
|
|
|
|
int32_t count = (int32_t)arrlen(sRecentFiles);
|
|
|
|
if (count == 0) {
|
|
return;
|
|
}
|
|
|
|
// Append separator + recent file items after Exit
|
|
wmAddMenuSeparator(sFileMenu);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
// Show just the filename for shorter labels
|
|
const char *name = platformPathBaseName(sRecentFiles[i]);
|
|
|
|
char label[MAX_MENU_LABEL];
|
|
snprintf(label, sizeof(label), "&%ld %s", (long)i + 1, name);
|
|
wmAddMenuItem(sFileMenu, label, CMD_RECENT_BASE + i);
|
|
}
|
|
}
|
|
|
|
|
|
// recentRemove -- drop path from the Recent list (it no longer opens)
|
|
static void recentRemove(const char *path) {
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sRecentFiles); i++) {
|
|
if (platformPathSame(sRecentFiles[i], path)) {
|
|
free(sRecentFiles[i]);
|
|
arrdel(sRecentFiles, i);
|
|
recentSave();
|
|
recentRebuildMenu();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void recentSave(void) {
|
|
if (!sPrefs) {
|
|
return;
|
|
}
|
|
|
|
int32_t count = (int32_t)arrlen(sRecentFiles);
|
|
|
|
for (int32_t i = 0; i < CMD_RECENT_MAX; i++) {
|
|
char key[IDE_PREF_KEY_BUF];
|
|
snprintf(key, sizeof(key), PREF_KEY_RECENT_FMT, (long)i);
|
|
prefsSetString(sPrefs, PREF_SEC_RECENT, key, i < count ? sRecentFiles[i] : "");
|
|
}
|
|
|
|
prefsSave(sPrefs);
|
|
}
|
|
|
|
|
|
// refreshEditorMarkers -- the code editor's line markers: a gutter dot on
|
|
// each breakpoint of the shown section, the paused line's background.
|
|
// Called whenever the breakpoints, the paused line or the shown section
|
|
// change (updateBreakpointWindow, editorSetText, the debugger).
|
|
static void refreshEditorMarkers(void) {
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
int32_t base = editorLineToCodeLine(1) - 1; // code line before editor line 1
|
|
bool pausedHasBp = false;
|
|
|
|
wgtTextAreaClearLineMarkers(sEditor);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
|
|
int32_t edLine = sBreakpoints[i].codeLine - base;
|
|
|
|
if (sBreakpoints[i].fileIdx == sEditorFileIdx && edLine >= 1) {
|
|
wgtTextAreaSetLineMarker(sEditor, edLine, IDE_BP_GUTTER_RGB, TEXTAREA_MARKER_NONE);
|
|
pausedHasBp = pausedHasBp || edLine == sDbgCurrentLine;
|
|
}
|
|
}
|
|
|
|
if (sDbgState == DbgPausedE && sDbgCurrentLine > 0) {
|
|
wgtTextAreaSetLineMarker(sEditor, sDbgCurrentLine, pausedHasBp ? IDE_BP_GUTTER_RGB : TEXTAREA_MARKER_NONE, IDE_DBG_LINE_RGB);
|
|
}
|
|
}
|
|
|
|
|
|
// remapBreakpoints -- move the editor file's breakpoints to their anchors'
|
|
// lines in the current layout (starts: procStartLines of the current
|
|
// buffers; anchors parallel sBreakpoints). A breakpoint whose line is
|
|
// gone is removed. sProcTable must describe the current buffers.
|
|
static void remapBreakpoints(const IdeLineAnchorT *anchors, const int32_t *starts) {
|
|
bool changed = false;
|
|
|
|
for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
|
|
IdeBreakpointT *bp = &sBreakpoints[i];
|
|
|
|
if (bp->fileIdx != sEditorFileIdx) {
|
|
continue;
|
|
}
|
|
|
|
int32_t line = anchorLine(&anchors[i], starts);
|
|
|
|
if (line == IDE_NO_LINE) {
|
|
arrdel(sBreakpoints, i);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
changed = changed || line != bp->codeLine;
|
|
bp->codeLine = line;
|
|
setBreakpointProcName(bp, procIndexForCodeLine(line));
|
|
}
|
|
|
|
changed = dedupeBreakpoints() || changed;
|
|
|
|
if (changed) {
|
|
syncVmBreakpoints();
|
|
}
|
|
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
|
|
static void removeBreakpointsForFile(int32_t fileIdx) {
|
|
// Remove breakpoints for the given file and adjust indices for
|
|
// files above the removed one (since file indices shift down).
|
|
for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
|
|
if (sBreakpoints[i].fileIdx == fileIdx) {
|
|
arrdel(sBreakpoints, i);
|
|
} else if (sBreakpoints[i].fileIdx > fileIdx) {
|
|
sBreakpoints[i].fileIdx--;
|
|
}
|
|
}
|
|
|
|
syncVmBreakpoints();
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
|
|
// renameMenuItemsInCode -- after the Menu Editor, rename the handlers of
|
|
// menu items whose name changed. Items are matched by the identity the
|
|
// editor keeps (FrmMenuItemT.id, the item's position + 1 on entry), so
|
|
// inserting, deleting and moving items never looks like a rename. All
|
|
// old names go to unique placeholders first, so swapped names cannot
|
|
// collapse two handlers into one.
|
|
static void renameMenuItemsInCode(char **oldNames, int32_t oldCount) {
|
|
char **fromNames = NULL; // stb_ds: borrowed old names
|
|
char **toNames = NULL; // stb_ds: borrowed new names
|
|
int32_t newCount = (int32_t)arrlen(sDesigner.form->menuItems);
|
|
|
|
for (int32_t mi = 0; mi < newCount; mi++) {
|
|
FrmMenuItemT *item = &sDesigner.form->menuItems[mi];
|
|
int32_t was = item->id - 1;
|
|
|
|
if (item->id == FRM_NEW_MENU_ID || was < 0 || was >= oldCount || !oldNames[was] || !oldNames[was][0] || !item->name[0]) {
|
|
continue;
|
|
}
|
|
|
|
if (strcasecmp(oldNames[was], item->name) != 0) {
|
|
arrput(fromNames, oldNames[was]);
|
|
arrput(toNames, item->name);
|
|
}
|
|
}
|
|
|
|
int32_t renames = (int32_t)arrlen(fromNames);
|
|
bool viaTemporary = renames > 1; // a single rename cannot collide
|
|
|
|
if (viaTemporary) {
|
|
for (int32_t r = 0; r < renames; r++) {
|
|
char temp[BAS_MAX_IDENT];
|
|
|
|
snprintf(temp, sizeof(temp), IDE_RENAME_TEMP_FMT, (int)r);
|
|
ideRenameInCode(fromNames[r], temp);
|
|
}
|
|
}
|
|
|
|
for (int32_t r = 0; r < renames; r++) {
|
|
char temp[BAS_MAX_IDENT];
|
|
|
|
snprintf(temp, sizeof(temp), IDE_RENAME_TEMP_FMT, (int)r);
|
|
ideRenameInCode(viaTemporary ? temp : fromNames[r], toNames[r]);
|
|
}
|
|
|
|
arrfree(fromNames);
|
|
arrfree(toNames);
|
|
}
|
|
|
|
|
|
// renameProjectFile -- File > Rename File: ask for a new name for
|
|
// project file fileIdx, then renameProjectFileTo
|
|
static void renameProjectFile(int32_t fileIdx) {
|
|
char prompt[IDE_MSG_BUF];
|
|
char newName[DVX_MAX_PATH];
|
|
|
|
snprintf(prompt, sizeof(prompt), "New name for %s:", sProject.files[fileIdx].path);
|
|
|
|
if (dvxInputBox(sAc, IDE_RENAME_TITLE, prompt, sProject.files[fileIdx].path, newName, sizeof(newName)) && newName[0] != '\0') {
|
|
renameProjectFileTo(fileIdx, newName);
|
|
}
|
|
}
|
|
|
|
|
|
// renameProjectFileTo -- rename project file fileIdx to newName on disk
|
|
// and in the project (prjRenameFile, which puts everything back on
|
|
// failure; errors are shown). A class module or UserControl named after
|
|
// its file (classFollowsFile) takes the new name as its class name; that
|
|
// name must then be free. A form keeps its Name.
|
|
static bool renameProjectFileTo(int32_t fileIdx, const char *newName) {
|
|
char err[IDE_MSG_BUF];
|
|
|
|
// The editor or designer may hold newer text than the project buffer
|
|
if ((fileIdx == sEditorFileIdx || fileIdx == sDesignerFileIdx) && !stashCurrentFile()) {
|
|
return false;
|
|
}
|
|
|
|
PrjFileT *file = &sProject.files[fileIdx];
|
|
bool nameClass = false;
|
|
char oldClass[BAS_MAX_IDENT];
|
|
char newClass[BAS_MAX_IDENT];
|
|
|
|
if (file->kind == PrjKindClassE || file->kind == PrjKindControlE) {
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *text = projectFileText(fileIdx, &owned, &missing);
|
|
|
|
nameClass = text && classFollowsFile(fileIdx, text) && prjClassName(&sProject, fileIdx, text, oldClass, (int32_t)sizeof(oldClass));
|
|
free(owned);
|
|
basExtractClassName("", 0, newName, newClass, (int32_t)sizeof(newClass));
|
|
}
|
|
|
|
if (nameClass && !basIsValidIdent(newClass)) {
|
|
snprintf(err, sizeof(err), "%s is not a valid class name: the class module or UserControl is named after its file.", newClass);
|
|
ideErrorBox(err);
|
|
return false;
|
|
}
|
|
|
|
if (nameClass && strcasecmp(newClass, oldClass) != 0 && classNameUsed(newClass, "")) {
|
|
snprintf(err, sizeof(err), "The project already has a form, class module, or UserControl named %s.", newClass);
|
|
ideErrorBox(err);
|
|
return false;
|
|
}
|
|
|
|
if (!prjRenameFile(&sProject, fileIdx, newName, err, (int32_t)sizeof(err))) {
|
|
ideErrorBox(err);
|
|
return false;
|
|
}
|
|
|
|
if (nameClass && !setFileClassName(fileIdx, newClass)) {
|
|
ideErrorBox("The file was renamed, but its class name could not be changed.");
|
|
}
|
|
|
|
if (nameClass && file->kind == PrjKindControlE) {
|
|
designCtlRefresh();
|
|
}
|
|
|
|
invalidateCompiled();
|
|
updateProjectMenuState();
|
|
updateDirtyIndicators();
|
|
setStatus("File renamed.");
|
|
return true;
|
|
}
|
|
|
|
|
|
// replaceInSections -- Replace All over the editor file's sections: every
|
|
// proc (and General) holding the search text, or only the procs of object
|
|
// objFilter. The targets are collected by name first and looked up again
|
|
// before each visit, since showing a proc can drop an empty skeleton and
|
|
// shift the indices. Returns the number of replacements.
|
|
static int32_t replaceInSections(const char *objFilter, bool caseSensitive) {
|
|
int32_t needleLen = (int32_t)strlen(sFindText);
|
|
IdeProcEntryT *targets = NULL; // stb_ds: procs to visit, by name
|
|
int32_t count = 0;
|
|
|
|
if (!sEditor) {
|
|
return 0;
|
|
}
|
|
|
|
getFullSource();
|
|
|
|
bool inGeneral = sectionInScope(-1, objFilter) && procBufContains(sGeneralBuf, sFindText, needleLen, caseSensitive);
|
|
|
|
for (int32_t p = 0; p < (int32_t)arrlen(sProcBufs) && p < (int32_t)arrlen(sProcTable); p++) {
|
|
if (sectionInScope(p, objFilter) && procBufContains(sProcBufs[p], sFindText, needleLen, caseSensitive)) {
|
|
arrput(targets, sProcTable[p]);
|
|
}
|
|
}
|
|
|
|
if (inGeneral) {
|
|
showProc(IDE_SECTION_GENERAL);
|
|
count += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSensitive);
|
|
}
|
|
|
|
for (int32_t t = 0; t < (int32_t)arrlen(targets); t++) {
|
|
int32_t idx = procIndexByName(targets[t].objName, targets[t].evtName);
|
|
|
|
if (idx < 0) {
|
|
continue;
|
|
}
|
|
|
|
showProc(idx);
|
|
count += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSensitive);
|
|
ideBusyPump(NULL);
|
|
}
|
|
|
|
arrfree(targets);
|
|
return count;
|
|
}
|
|
|
|
|
|
// reportUnreadable -- a project file exists but cannot be loaded
|
|
static void reportUnreadable(const char *path) {
|
|
char msg[IDE_MSG_BUF];
|
|
|
|
snprintf(msg, sizeof(msg), "Cannot load %s: unreadable or larger than %d bytes.", path, IDE_MAX_SOURCE - 1);
|
|
ideErrorBox(msg);
|
|
}
|
|
|
|
|
|
// runBody -- the rest of a run (runModule), on the task the program runs
|
|
// on: its forms loaded and shown, its code run, then its event loop until
|
|
// it ends; the error it ended on reported, the IDE's windows restored.
|
|
static void runBody(void) {
|
|
BasFormRtT *formRt = sDbgFormRt;
|
|
BasVmT *vm = sVm;
|
|
|
|
// Load forms from project files (AFTER breakpoints are set so
|
|
// init code breakpoints fire), then show the startup form.
|
|
loadRunFiles(formRt);
|
|
basRunHostLoadForms(formRt, sProject.startupForm);
|
|
|
|
// Run in slices of 10000 steps, yielding to DVX between slices
|
|
basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);
|
|
|
|
BasVmResultE result = runProgramSlices(formRt);
|
|
|
|
// VB-style event loop: after module-level code finishes,
|
|
// keep processing events as long as any form is loaded.
|
|
// The program ends when all forms are unloaded (closed).
|
|
// Breakpoints inside event handlers pause inside the VM's own
|
|
// spin (basVmCallSub -> doEventsCallback), so nothing here needs
|
|
// to watch for DbgPausedE.
|
|
if (result == BAS_VM_HALTED && basFormRtKeepsRunning(formRt)) {
|
|
setStatus("Running (event loop)...");
|
|
|
|
// An error message outliving an event dispatch means an
|
|
// unhandled runtime error escaped a handler (ON ERROR clears the
|
|
// message when it takes over); without this test the program
|
|
// kept running after the error and the report below never fired
|
|
// until the user pressed Stop.
|
|
while (sWin && sAc->running && !sStopRequested && basRunHostAlive(formRt)) {
|
|
idePump();
|
|
}
|
|
}
|
|
|
|
// A normal end releases the module variables: their objects'
|
|
// Class_Terminates run (VB)
|
|
if (result == BAS_VM_HALTED && sWin && sAc->running && !sStopRequested && !formRt->terminated && basVmFinish(vm)) {
|
|
runProgramSlices(formRt);
|
|
}
|
|
|
|
// A runtime error fired from a host callback (e.g. setting a
|
|
// property on an unknown control) halts execution by writing
|
|
// vm->errorMsg and setting vm->ended. basVmRun surfaces that as
|
|
// BAS_VM_ERROR for module-level code, but errors raised inside an
|
|
// event handler just break the loop above. Either way, report
|
|
// the message and navigate the editor to the offending line so
|
|
// the user can fix it. debugNavigateToLine depends on sVm and
|
|
// sDbgModule, so it has to run BEFORE the cleanup block below.
|
|
// Use vm->errorLine (snapshot at error time) rather than
|
|
// vm->currentLine: the dialog/event pump can fire another handler
|
|
// whose OP_LINE overwrites currentLine before we read it here.
|
|
bool hadRuntimeError = basVmGetError(vm)[0] != '\0' && sWin;
|
|
int32_t errorLine = hadRuntimeError ? basVmGetErrorLine(vm) : 0;
|
|
|
|
if (hadRuntimeError) {
|
|
// Map the concatenated line to a file / proc-local line the
|
|
// same way compile errors do, so the Output message matches
|
|
// what the editor is actually showing.
|
|
int32_t errFileIdx = -1;
|
|
int32_t errLocalLine = errorLine;
|
|
const char *errFile = "";
|
|
|
|
if (errorLine > 0) {
|
|
prjMapLine(&sProject, errorLine, &errFileIdx, &errLocalLine);
|
|
|
|
if (errFileIdx >= 0 && errFileIdx < prjFileCount(&sProject)) {
|
|
errFile = sProject.files[errFileIdx].path;
|
|
}
|
|
}
|
|
|
|
// Navigate to the failing line; the proc whose line range holds it
|
|
// (none for module-level code between procs) names the location.
|
|
char procName[IDE_FULL_NAME_BUF] = {0};
|
|
int32_t procLine = errLocalLine;
|
|
|
|
if (errFileIdx >= 0) {
|
|
int32_t procIdx = navigateToCodeLine(errFileIdx, errLocalLine, false);
|
|
|
|
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
|
|
snprintf(procName, sizeof(procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
|
|
procLine = wgtTextAreaGetCursorLine(sEditor);
|
|
}
|
|
}
|
|
|
|
char report[IDE_REPORT_BUF];
|
|
|
|
if (procName[0]) {
|
|
snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", procName, (int)procLine, basVmGetError(vm));
|
|
} else if (errFile[0]) {
|
|
snprintf(report, sizeof(report), "\nRUNTIME ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, basVmGetError(vm));
|
|
} else {
|
|
snprintf(report, sizeof(report), "\nRUNTIME ERROR:\nline %d: %s\n", (int)errLocalLine, basVmGetError(vm));
|
|
}
|
|
|
|
outputAppend(report, (int32_t)strlen(report));
|
|
outputLog(report);
|
|
}
|
|
|
|
if (sLogLineLen > 0) {
|
|
logFlushLine();
|
|
}
|
|
|
|
arrsetlen(sBreakTitles, 0);
|
|
sVm = NULL;
|
|
sDbgFormRt = NULL;
|
|
sDbgModule = NULL;
|
|
sDbgState = DbgIdleE;
|
|
sDbgCurrentLine = -1;
|
|
sDbgEnabled = false;
|
|
|
|
basRunHostDestroy(formRt);
|
|
|
|
// The fonts the run added (the project's and any the program added):
|
|
// the IDE carries none of its own
|
|
dvxFontRemoveOwner(sRun.fontOwner);
|
|
|
|
// If the IDE was closed while the program was running, skip all UI
|
|
// updates -- the windows are already destroyed -- and release the
|
|
// module onClose left for us.
|
|
if (!sWin) {
|
|
if (sCachedModule) {
|
|
basModuleFree(sCachedModule);
|
|
sCachedModule = NULL;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
updateProjectMenuState();
|
|
sOutputPending = true;
|
|
flushOutput();
|
|
|
|
setStatus(hadRuntimeError ? "Runtime error." : "Done.");
|
|
|
|
// Restore IDE windows. On a runtime error we want the code
|
|
// window on top so the user lands on the offending line; don't
|
|
// pop the form designer / toolbox / project back up, since that
|
|
// would cover the editor (dvxShowWindow raises). Output window
|
|
// stays up too so the error text is visible.
|
|
if (hadRuntimeError) {
|
|
if (sCodeWin && !sCodeWin->visible) {
|
|
dvxShowWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
}
|
|
} else {
|
|
if (sRun.hadFormWin && sFormWin) {
|
|
dvxShowWindow(sAc, sFormWin);
|
|
}
|
|
|
|
if (sRun.hadToolbox && sToolboxWin) {
|
|
dvxShowWindow(sAc, sToolboxWin);
|
|
}
|
|
|
|
if (sRun.hadProps && sPropsWin) {
|
|
dvxShowWindow(sAc, sPropsWin);
|
|
}
|
|
|
|
if (sRun.hadCodeWin && sCodeWin) {
|
|
dvxShowWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
if (sRun.hadPrjWin && sProjectWin) {
|
|
dvxShowWindow(sAc, sProjectWin);
|
|
}
|
|
}
|
|
|
|
// Repaint to clear destroyed runtime forms and restore designer
|
|
dvxUpdate(sAc);
|
|
}
|
|
|
|
|
|
static void runCached(void) {
|
|
if (!sCachedModule) {
|
|
setStatus("No compiled program. Press F5 to compile first.");
|
|
return;
|
|
}
|
|
|
|
// The forms are registered from the project buffers
|
|
if (!stashCurrentFile()) {
|
|
return;
|
|
}
|
|
|
|
clearOutput();
|
|
runModule(sCachedModule);
|
|
}
|
|
|
|
|
|
// The project's fonts added for owner (the IDE) while a program runs; one
|
|
// that cannot be added is reported in the Output window
|
|
static void runFontsAdd(int32_t owner) {
|
|
for (int32_t i = 0; i < prjFontCount(&sProject); i++) {
|
|
char path[DVX_MAX_PATH];
|
|
|
|
platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i));
|
|
|
|
if (!dvxFontAddFile(owner, path)) {
|
|
char msg[DVX_MAX_PATH + IDE_MSG_BUF];
|
|
|
|
snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path);
|
|
outputAppend(msg, (int32_t)strlen(msg));
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// runModule -- run mod: set up here (runStart), then run (runBody) on a
|
|
// task of the IDE's own (shellAppTaskStart), so the shell's main loop
|
|
// goes on while the program does -- programs that end are reaped, Exit
|
|
// and DOS requests carried out, the screen saver started. Without one
|
|
// (the host tests have no shell) the program runs here, to its end.
|
|
static void runModule(BasModuleT *mod) {
|
|
if (!runStart(mod)) {
|
|
return;
|
|
}
|
|
|
|
IdeTaskStartFnT taskStart = (IdeTaskStartFnT)dlsym(NULL, DVX_SYM("shellAppTaskStart"));
|
|
|
|
if (!taskStart || taskStart(sAc, IDE_RUN_TASK_NAME, BAS_RUN_STACK_SIZE, runTaskEntry, NULL) < 0) {
|
|
runBody();
|
|
}
|
|
}
|
|
|
|
|
|
// runProgramSlices -- run the program in step slices, yielding to DVX
|
|
// between them, pausing at breakpoints, until it halts, fails (its
|
|
// message and line left in the VM, see basRunHostSlice) or is stopped.
|
|
// Returns the last result.
|
|
static BasVmResultE runProgramSlices(BasFormRtT *rt) {
|
|
BasVmResultE result = BAS_VM_HALTED;
|
|
|
|
for (;;) {
|
|
if (!sWin || !sAc->running || sStopRequested) {
|
|
break;
|
|
}
|
|
|
|
if (sDbgState == DbgPausedE) {
|
|
// Paused at breakpoint/step -- spin on GUI events until the
|
|
// user continues, steps or stops (debugResume resumes the VM)
|
|
idePump();
|
|
continue;
|
|
}
|
|
|
|
result = basRunHostSlice(rt);
|
|
|
|
// Module-level pause: the same state as a pause inside a handler
|
|
if (result == BAS_VM_BREAKPOINT) {
|
|
int32_t line = basVmGetCurrentLine(rt->vm);
|
|
|
|
onBreakpointHit(NULL, line > 0 ? line : 0);
|
|
continue;
|
|
}
|
|
|
|
if (result == BAS_VM_STEP_LIMIT) {
|
|
idePump();
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
// runStart -- the first part of a run (runModule): the program's runtime
|
|
// made, the IDE's windows hidden, breakpoints set (and stepping when the
|
|
// run breaks on start), the project's fonts added. False when it cannot
|
|
// start.
|
|
static bool runStart(BasModuleT *mod) {
|
|
// Never nest a run: a stopped program may still be unwinding
|
|
if (sVm) {
|
|
return false;
|
|
}
|
|
|
|
if (prjIsControlProject(&sProject)) {
|
|
setStatus(IDE_CTL_NO_RUN);
|
|
return false;
|
|
}
|
|
|
|
// The program's VM and form runtime. In the IDE, App.Path is the
|
|
// project's folder, App.Data its DATA folder (what the program will
|
|
// ship), and App.User the user folder the project's program gets
|
|
// once installed beside it; the program's screen window carries
|
|
// the project's name and F1 opens the project's help file. The IDE
|
|
// reports runtime errors through the Output pane and jumps the editor
|
|
// to the faulting line, so the form runtime's modal error box is
|
|
// suppressed (its nested event pump raced with events still queued
|
|
// by the program: "press OK twice").
|
|
char helpPath[DVX_MAX_PATH] = "";
|
|
const char *hostError = NULL;
|
|
BasRunSpecT spec;
|
|
|
|
if (sProject.helpFile[0]) {
|
|
platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile);
|
|
}
|
|
|
|
memset(&spec, 0, sizeof(spec));
|
|
spec.appDir = sProject.projectDir;
|
|
spec.appFile = sProject.projectPath;
|
|
spec.args = sProject.commandArgs;
|
|
spec.title = sProject.name;
|
|
spec.helpFile = helpPath[0] ? helpPath : NULL;
|
|
spec.printFn = printCallback;
|
|
spec.inputFn = inputCallback;
|
|
spec.doEventsFn = doEventsCallback;
|
|
spec.suppressErrorDialog = true;
|
|
|
|
BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError);
|
|
|
|
if (!formRt) {
|
|
setStatus(hostError);
|
|
return false;
|
|
}
|
|
|
|
BasVmT *vm = formRt->vm;
|
|
|
|
setStatus("Running...");
|
|
closeFindDialog();
|
|
|
|
// Hide the designer, code and project windows while the program runs;
|
|
// they are restored (where they were visible) when it ends.
|
|
sRun.hadFormWin = sFormWin && sFormWin->visible;
|
|
sRun.hadToolbox = sToolboxWin && sToolboxWin->visible;
|
|
sRun.hadProps = sPropsWin && sPropsWin->visible;
|
|
sRun.hadCodeWin = sCodeWin && sCodeWin->visible;
|
|
sRun.hadPrjWin = sProjectWin && sProjectWin->visible;
|
|
|
|
if (sFormWin) {
|
|
dvxHideWindow(sAc, sFormWin);
|
|
}
|
|
|
|
if (sToolboxWin) {
|
|
dvxHideWindow(sAc, sToolboxWin);
|
|
}
|
|
|
|
if (sPropsWin) {
|
|
dvxHideWindow(sAc, sPropsWin);
|
|
}
|
|
|
|
if (sCodeWin) {
|
|
dvxHideWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
if (sProjectWin) {
|
|
dvxHideWindow(sAc, sProjectWin);
|
|
}
|
|
|
|
basVmSetBreakpointCallback(vm, onBreakpointHit, NULL);
|
|
|
|
sVm = vm;
|
|
sDbgFormRt = formRt;
|
|
sDbgModule = mod;
|
|
sDbgState = DbgRunningE;
|
|
sStopRequested = false;
|
|
|
|
updateProjectMenuState();
|
|
|
|
// Set breakpoints BEFORE loading forms so breakpoints in form
|
|
// init code (module-level statements inside BEGINFORM) fire.
|
|
if (sDbgEnabled) {
|
|
syncVmBreakpoints();
|
|
|
|
if (sDbgBreakOnStart) {
|
|
basVmStepInto(vm);
|
|
}
|
|
}
|
|
|
|
// The project's fonts, for the program run here as for the built one
|
|
// (added for the IDE until the run ends)
|
|
sRun.fontOwner = sAc->currentAppId;
|
|
runFontsAdd(sRun.fontOwner);
|
|
return true;
|
|
}
|
|
|
|
|
|
// runTaskEntry -- runModule's task: the program's runtime moves to it,
|
|
// then the run goes on (runBody).
|
|
static void runTaskEntry(void *arg) {
|
|
(void)arg;
|
|
basFormRtAdopt(sDbgFormRt);
|
|
runBody();
|
|
}
|
|
|
|
|
|
// saveActiveFile -- write the active file (editor/designer state first
|
|
// stashed into its buffer). Returns false when nothing was written.
|
|
static bool saveActiveFile(void) {
|
|
if (!hasProject()) {
|
|
return false;
|
|
}
|
|
|
|
int32_t idx = sProject.activeFileIdx;
|
|
|
|
if (idx < 0 || idx >= prjFileCount(&sProject)) {
|
|
return false;
|
|
}
|
|
|
|
// Ensure buffer is up-to-date with editor/designer state
|
|
stashCurrentFile();
|
|
|
|
if (!writeProjectFile(idx)) {
|
|
return false;
|
|
}
|
|
|
|
setStatus("Saved.");
|
|
updateDirtyIndicators();
|
|
return true;
|
|
}
|
|
|
|
|
|
// saveAllModified -- write every modified file plus the .dbp when dirty.
|
|
// Returns false if any write failed (the user has already been told).
|
|
static bool saveAllModified(void) {
|
|
if (!hasProject()) {
|
|
return true;
|
|
}
|
|
|
|
stashCurrentFile();
|
|
syncFormDirty();
|
|
|
|
bool ok = true;
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (sProject.files[i].modified && !writeProjectFile(i)) {
|
|
ok = false;
|
|
}
|
|
}
|
|
|
|
if (sProject.dirty && !saveProjectFile()) {
|
|
ok = false;
|
|
}
|
|
|
|
updateDirtyIndicators();
|
|
return ok;
|
|
}
|
|
|
|
|
|
// saveCurProc -- store the editor text back into the buffer it shows.
|
|
// Nothing typed is ever dropped: lines outside the shown procedure move
|
|
// to (General), extra Sub/Function blocks become procedures of their
|
|
// own (a duplicate name too, so the compiler reports it), and a
|
|
// procedure whose declaration was deleted becomes (General) text. With
|
|
// discardSkeleton, an untouched generated event skeleton is removed
|
|
// instead -- only when the editor is leaving it. Breakpoints and the
|
|
// caret follow their lines to wherever they were stored. When memory
|
|
// runs out nothing changes and the text stays in the editor.
|
|
// Returns true if a buffer was removed (later sProcBufs indices shifted).
|
|
|
|
static bool saveCurProc(bool discardSkeleton) {
|
|
if (!sEditor || sEditorTruncated) {
|
|
return false;
|
|
}
|
|
|
|
const char *edText = wgtGetText(sEditor);
|
|
int32_t cur = sCurProcIdx;
|
|
|
|
if (!edText || cur < IDE_SECTION_GENERAL || cur >= (int32_t)arrlen(sProcBufs)) {
|
|
return false;
|
|
}
|
|
|
|
const char *stored = cur == IDE_SECTION_GENERAL ? sGeneralBuf : sProcBufs[cur];
|
|
|
|
if (!stored) {
|
|
stored = "";
|
|
}
|
|
|
|
// Stored text is already in canonical form: unchanged text needs
|
|
// nothing unless an untouched skeleton is being left.
|
|
bool leavingSkeleton = discardSkeleton && cur >= 0 && sSkeletonName[0];
|
|
|
|
if (!leavingSkeleton && strcmp(edText, stored) == 0) {
|
|
return false;
|
|
}
|
|
|
|
char *general = NULL;
|
|
char **procs = NULL;
|
|
IdeLineDestT *map = NULL;
|
|
|
|
if (!splitProcs(edText, &general, &procs, &map)) {
|
|
setStatus("Not enough memory to store the edit; it is kept in the editor.");
|
|
return false;
|
|
}
|
|
|
|
int32_t procCount = (int32_t)arrlen(procs);
|
|
char *newGeneral = NULL;
|
|
|
|
if (cur == IDE_SECTION_GENERAL && procCount == 0) {
|
|
// (General) with no procedure in it is kept exactly as typed
|
|
newGeneral = strdup(edText);
|
|
} else if (cur == IDE_SECTION_GENERAL) {
|
|
newGeneral = general;
|
|
general = NULL;
|
|
} else if (general[0]) {
|
|
newGeneral = generalAppend(sGeneralBuf, general);
|
|
}
|
|
|
|
if (!newGeneral && (cur == IDE_SECTION_GENERAL || general[0])) {
|
|
free(general);
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
free(procs[i]);
|
|
}
|
|
|
|
arrfree(procs);
|
|
arrfree(map);
|
|
setStatus("Not enough memory to store the edit; it is kept in the editor.");
|
|
return false;
|
|
}
|
|
|
|
// Pin the editor file's breakpoints and the caret to their sections in
|
|
// the layout as edited (see anchorCodeLine), before the buffers move.
|
|
int32_t edLines = countLines(edText);
|
|
int32_t delta = edLines - countLines(stored);
|
|
int32_t genBase = cur == IDE_SECTION_GENERAL ? 0 : sectionLineCount(sGeneralBuf);
|
|
int32_t *starts = procStartLines(sGeneralBuf, sProcBufs);
|
|
IdeLineAnchorT *anchors = NULL;
|
|
IdeLineAnchorT caret;
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
|
|
IdeLineAnchorT a;
|
|
|
|
anchorCodeLine(&a, sBreakpoints[i].codeLine, cur, edLines, delta, starts);
|
|
arrput(anchors, a);
|
|
}
|
|
|
|
caret.edLine = wgtTextAreaGetCursorLine(sEditor) - 1;
|
|
|
|
// Store: the shown proc keeps its slot (the block with its name, or the
|
|
// first block when it was renamed); every other block is appended.
|
|
int32_t ownIdx = IDE_NO_LINE;
|
|
bool removed = false;
|
|
|
|
if (cur >= 0 && procCount == 0) {
|
|
// No declaration left: the text is module-level code
|
|
free(sProcBufs[cur]);
|
|
arrdel(sProcBufs, cur);
|
|
sCurProcIdx = IDE_SECTION_GENERAL;
|
|
removed = true;
|
|
} else if (cur >= 0) {
|
|
char ownName[IDE_FULL_NAME_BUF];
|
|
char name[IDE_FULL_NAME_BUF];
|
|
|
|
procNameOf(sProcBufs[cur], ownName, sizeof(ownName));
|
|
ownIdx = 0;
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
procNameOf(procs[i], name, sizeof(name));
|
|
|
|
if (strcasecmp(name, ownName) == 0) {
|
|
ownIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
procNameOf(procs[ownIdx], name, sizeof(name));
|
|
|
|
bool isSkeleton = sSkeletonName[0] && strcasecmp(name, sSkeletonName) == 0;
|
|
bool bodyEmpty = procBodyEmpty(procs[ownIdx]);
|
|
|
|
free(sProcBufs[cur]);
|
|
|
|
if (discardSkeleton && isSkeleton && bodyEmpty && procCount == 1 && !general[0]) {
|
|
free(procs[ownIdx]);
|
|
procs[ownIdx] = NULL;
|
|
arrdel(sProcBufs, cur);
|
|
sCurProcIdx = IDE_SECTION_NONE;
|
|
sSkeletonName[0] = '\0';
|
|
removed = true;
|
|
} else {
|
|
sProcBufs[cur] = procs[ownIdx];
|
|
|
|
// Code typed into a generated skeleton makes it the user's
|
|
if (isSkeleton && !bodyEmpty) {
|
|
sSkeletonName[0] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < procCount; i++) {
|
|
if (i != ownIdx) {
|
|
arrput(sProcBufs, procs[i]);
|
|
}
|
|
}
|
|
|
|
if (newGeneral) {
|
|
free(sGeneralBuf);
|
|
sGeneralBuf = newGeneral;
|
|
}
|
|
|
|
// Resolve the anchors in the new layout
|
|
for (int32_t i = 0; i < (int32_t)arrlen(anchors); i++) {
|
|
if (anchors[i].edLine != IDE_NO_LINE) {
|
|
anchorEditedLine(&anchors[i], map, procs, genBase);
|
|
}
|
|
}
|
|
|
|
anchorEditedLine(&caret, map, procs, genBase);
|
|
arrfree(starts);
|
|
starts = procStartLines(sGeneralBuf, sProcBufs);
|
|
rebuildProcTable();
|
|
remapBreakpoints(anchors, starts);
|
|
arrfree(starts);
|
|
arrfree(anchors);
|
|
arrfree(map);
|
|
arrfree(procs);
|
|
free(general);
|
|
|
|
// The editor shows exactly what was stored for the shown section, the
|
|
// caret on the line it was on
|
|
if (sCurProcIdx >= IDE_SECTION_GENERAL) {
|
|
const char *shownBuf = sCurProcIdx == IDE_SECTION_GENERAL ? NULL : sProcBufs[sCurProcIdx];
|
|
const char *shown = sCurProcIdx == IDE_SECTION_GENERAL ? sGeneralBuf : shownBuf;
|
|
|
|
if (strcmp(edText, shown ? shown : "") != 0) {
|
|
int32_t line = (caret.offset != IDE_NO_LINE && caret.section == shownBuf) ? caret.offset + 1 : IDE_CARET_KEEP;
|
|
|
|
editorSetText(shown ? shown : "", line);
|
|
}
|
|
}
|
|
|
|
return removed;
|
|
}
|
|
|
|
|
|
// saveFile -- Ctrl+S: the active file plus any other modified forms
|
|
static void saveFile(void) {
|
|
if (!hasProject() || sProject.activeFileIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
if (!saveActiveFile()) {
|
|
return;
|
|
}
|
|
|
|
syncFormDirty();
|
|
|
|
for (int32_t i = 0; i < prjFileCount(&sProject); i++) {
|
|
if (i != sProject.activeFileIdx && sProject.files[i].kind == PrjKindFormE && sProject.files[i].modified) {
|
|
writeProjectFile(i);
|
|
}
|
|
}
|
|
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
|
|
// saveProjectFile -- write the .dbp; dirty is only cleared on success
|
|
static bool saveProjectFile(void) {
|
|
if (!prjSave(&sProject)) {
|
|
dvxErrorBox(sAc, NULL, "Could not write the project file.");
|
|
return false;
|
|
}
|
|
|
|
sProject.dirty = false;
|
|
return true;
|
|
}
|
|
|
|
|
|
// sectionInScope -- whether section p (-1 = General) of the editor file
|
|
// belongs to object objFilter (NULL = every section)
|
|
static bool sectionInScope(int32_t p, const char *objFilter) {
|
|
if (!objFilter) {
|
|
return true;
|
|
}
|
|
|
|
if (p < 0) {
|
|
return strcasecmp(objFilter, IDE_GENERAL_SECTION) == 0;
|
|
}
|
|
|
|
return p < (int32_t)arrlen(sProcTable) && strcasecmp(sProcTable[p].objName, objFilter) == 0;
|
|
}
|
|
|
|
|
|
// sectionLineCount -- lines a section's text takes in the canonical
|
|
// layout (joinProcArrays ends every non-empty section with a newline)
|
|
static int32_t sectionLineCount(const char *text) {
|
|
if (!text || !text[0]) {
|
|
return 0;
|
|
}
|
|
|
|
int32_t n = 0;
|
|
const char *p = text;
|
|
|
|
for (; *p; p++) {
|
|
if (*p == '\n') {
|
|
n++;
|
|
}
|
|
}
|
|
|
|
return p[-1] == '\n' ? n : n + 1;
|
|
}
|
|
|
|
|
|
// selectDropdowns -- set the Object and Event dropdown selections to
|
|
// match a given control/event without triggering navigation callbacks.
|
|
// The caller is responsible for having already navigated to the proc.
|
|
|
|
static void selectDropdowns(const char *objName, const char *evtName) {
|
|
if (!sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
// Select the object
|
|
int32_t objCount = (int32_t)arrlen(sObjItems);
|
|
|
|
for (int32_t i = 0; i < objCount; i++) {
|
|
if (strcasecmp(sObjItems[i], objName) == 0) {
|
|
wgtDropdownSetSelected(sObjDropdown, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Rebuild the event list for this object but suppress navigation
|
|
bool savedSuppress = sDropdownNavSuppressed;
|
|
sDropdownNavSuppressed = true;
|
|
onObjDropdownChange(sObjDropdown);
|
|
sDropdownNavSuppressed = savedSuppress;
|
|
|
|
// Now select the specific event
|
|
int32_t evtCount = (int32_t)arrlen(sEvtItems);
|
|
|
|
for (int32_t i = 0; i < evtCount; i++) {
|
|
if (evtLabelMatches(sEvtItems[i], evtName)) {
|
|
wgtDropdownSetSelected(sEvtDropdown, i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// setBreakpointProcName -- label a breakpoint with the proc it is in
|
|
// (procIdx into sProcTable, out of range = (General))
|
|
static void setBreakpointProcName(IdeBreakpointT *bp, int32_t procIdx) {
|
|
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
|
|
snprintf(bp->procName, sizeof(bp->procName), "%s.%s", sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
|
|
} else {
|
|
snprintf(bp->procName, sizeof(bp->procName), "%s", IDE_GENERAL_SECTION);
|
|
}
|
|
}
|
|
|
|
|
|
// setFileClassName -- give class module fileIdx the class name name in
|
|
// its Attribute VB_Name (when it has one; else the name already comes
|
|
// from the file name), or UserControl fileIdx the name on its Begin
|
|
// UserControl line. The text becomes an unsaved edit, and the editor
|
|
// shows it when the file is open there. False when out of memory or the
|
|
// file cannot be read.
|
|
static bool setFileClassName(int32_t fileIdx, const char *name) {
|
|
PrjFileT *file = &sProject.files[fileIdx];
|
|
|
|
if (!file->buffer) {
|
|
file->buffer = prjReadFileText(&sProject, fileIdx, NULL);
|
|
}
|
|
|
|
if (!file->buffer) {
|
|
return false;
|
|
}
|
|
|
|
int32_t len = (int32_t)strlen(file->buffer);
|
|
int32_t start = 0;
|
|
int32_t oldLen = 0;
|
|
int32_t nameLen = (int32_t)strlen(name);
|
|
char *text = NULL;
|
|
|
|
if (file->kind == PrjKindControlE) {
|
|
text = prjCtlHeaderSet(file->buffer, BAS_PROP_NAME, name);
|
|
} else if (basClassNameAttr(file->buffer, len, &start, &oldLen)) {
|
|
text = (char *)malloc((size_t)(len - oldLen + nameLen + 1));
|
|
|
|
if (text) {
|
|
memcpy(text, file->buffer, (size_t)start);
|
|
memcpy(text + start, name, (size_t)nameLen);
|
|
memcpy(text + start + nameLen, file->buffer + start + oldLen, (size_t)(len - start - oldLen + 1));
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
|
|
if (!text) {
|
|
return false;
|
|
}
|
|
|
|
free(file->buffer);
|
|
file->buffer = text;
|
|
file->modified = true;
|
|
|
|
if (fileIdx == sEditorFileIdx) {
|
|
loadModuleIntoEditor(fileIdx, text);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static void setOutputText(const char *text) {
|
|
if (!sOutWin) {
|
|
showOutputWindow();
|
|
}
|
|
|
|
if (sOutput) {
|
|
wgtSetText(sOutput, text);
|
|
}
|
|
}
|
|
|
|
|
|
static void setStatus(const char *text) {
|
|
if (sStatus) {
|
|
wgtSetText(sStatus, text);
|
|
}
|
|
}
|
|
|
|
|
|
// shiftBreakpointsForEdit -- move the editor file's breakpoints to follow
|
|
// an edit that changed the line count by delta. The edited region comes
|
|
// from diffing the old and new text (longest common suffix first, then
|
|
// prefix): lines that start before it stay, lines that start after it
|
|
// move by delta, and lines whose start was deleted or replaced lose
|
|
// their breakpoint. Joined lines can leave two breakpoints on one line;
|
|
// the duplicate goes.
|
|
static void shiftBreakpointsForEdit(const char *oldText, const char *newText, int32_t delta) {
|
|
int32_t oldLen = (int32_t)strlen(oldText);
|
|
int32_t newLen = (int32_t)strlen(newText);
|
|
int32_t suffix = 0;
|
|
int32_t prefix = 0;
|
|
|
|
while (suffix < oldLen && suffix < newLen && oldText[oldLen - 1 - suffix] == newText[newLen - 1 - suffix]) {
|
|
suffix++;
|
|
}
|
|
|
|
while (prefix < oldLen - suffix && prefix < newLen - suffix && oldText[prefix] == newText[prefix]) {
|
|
prefix++;
|
|
}
|
|
|
|
int32_t oldEnd = oldLen - suffix; // end of the replaced old text
|
|
int32_t base = editorLineToCodeLine(1) - 1; // code line before editor line 1
|
|
int32_t oldLines = countLines(oldText);
|
|
bool changed = false;
|
|
|
|
for (int32_t i = (int32_t)arrlen(sBreakpoints) - 1; i >= 0; i--) {
|
|
if (sBreakpoints[i].fileIdx != sEditorFileIdx) {
|
|
continue;
|
|
}
|
|
|
|
int32_t edLine = sBreakpoints[i].codeLine - base;
|
|
|
|
// Sections before the editor's are untouched, sections after it move
|
|
if (edLine < 1) {
|
|
continue;
|
|
}
|
|
|
|
if (edLine > oldLines) {
|
|
sBreakpoints[i].codeLine += delta;
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
int32_t start = lineStartOffset(oldText, edLine);
|
|
|
|
if (start < prefix) {
|
|
continue;
|
|
}
|
|
|
|
if (start >= oldEnd) {
|
|
sBreakpoints[i].codeLine += delta;
|
|
changed = true;
|
|
} else if (start != prefix) {
|
|
arrdel(sBreakpoints, i);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
changed = dedupeBreakpoints() || changed;
|
|
|
|
if (changed) {
|
|
syncVmBreakpoints();
|
|
updateBreakpointWindow();
|
|
}
|
|
}
|
|
|
|
|
|
static void showBreakpointWindow(void) {
|
|
if (sBreakpointWin) {
|
|
dvxShowWindow(sAc, sBreakpointWin);
|
|
dvxRaiseWindow(sAc, sBreakpointWin);
|
|
updateBreakpointWindow();
|
|
return;
|
|
}
|
|
|
|
int32_t winW = IDE_BP_WIN_W;
|
|
int32_t winH = IDE_DEBUG_WIN_H;
|
|
int32_t winX = sAc->display.width - winW - IDE_WIN_MARGIN;
|
|
int32_t winY = sAc->display.height - winH - IDE_WIN_MARGIN;
|
|
|
|
sBreakpointWin = dvxCreateWindow(sAc, "Breakpoints", winX, winY, winW, winH, true);
|
|
|
|
if (sBreakpointWin) {
|
|
sBreakpointWin->onClose = onBreakpointWinClose;
|
|
sBreakpointWin->onMenu = onMenu;
|
|
sBreakpointWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sBreakpointWin->resizable = true;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sBreakpointWin);
|
|
|
|
if (root) {
|
|
sBreakpointList = wgtListView(root);
|
|
|
|
if (sBreakpointList) {
|
|
sBreakpointList->weight = WGT_WEIGHT_FILL;
|
|
sBreakpointList->onKeyDown = onBreakpointListKeyDown;
|
|
sBreakpointList->onDblClick = onBreakpointListDblClick;
|
|
wgtListViewSetMultiSelect(sBreakpointList, true);
|
|
|
|
static const ListViewColT cols[] = {
|
|
{ "File", wgtChars(IDE_COL_W_FILE), ListViewAlignLeftE },
|
|
{ "Procedure", wgtChars(IDE_COL_W_PROC), ListViewAlignLeftE },
|
|
{ "Line", wgtChars(IDE_COL_W_LINE), ListViewAlignRightE },
|
|
};
|
|
|
|
wgtListViewSetColumns(sBreakpointList, cols, DVX_ARRAY_LEN(cols));
|
|
}
|
|
}
|
|
}
|
|
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
|
|
static void showCallStackWindow(void) {
|
|
if (sCallStackWin) {
|
|
dvxShowWindow(sAc, sCallStackWin);
|
|
dvxRaiseWindow(sAc, sCallStackWin);
|
|
updateCallStackWindow();
|
|
return;
|
|
}
|
|
|
|
int32_t winW = IDE_CALLSTACK_WIN_W;
|
|
int32_t winH = IDE_DEBUG_WIN_H;
|
|
int32_t winX = sAc->display.width - winW;
|
|
int32_t winY = toolbarBottom() + IDE_CALLSTACK_WIN_Y_OFF;
|
|
|
|
sCallStackWin = dvxCreateWindow(sAc, "Call Stack", winX, winY, winW, winH, true);
|
|
|
|
if (sCallStackWin) {
|
|
sCallStackWin->onClose = onCallStackClose;
|
|
sCallStackWin->onMenu = onMenu;
|
|
sCallStackWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sCallStackWin->resizable = true;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sCallStackWin);
|
|
|
|
if (root) {
|
|
sCallStackList = wgtListView(root);
|
|
|
|
if (sCallStackList) {
|
|
sCallStackList->weight = WGT_WEIGHT_FILL;
|
|
|
|
static const ListViewColT cols[] = {
|
|
{ "Procedure", wgtChars(IDE_COL_W_PROC), ListViewAlignLeftE },
|
|
{ "Line", wgtChars(IDE_COL_W_LINE), ListViewAlignRightE },
|
|
};
|
|
|
|
wgtListViewSetColumns(sCallStackList, cols, DVX_ARRAY_LEN(cols));
|
|
}
|
|
}
|
|
}
|
|
|
|
updateCallStackWindow();
|
|
}
|
|
|
|
|
|
// showCodeView -- show the code of fileIdx (the active file): reopen the
|
|
// code window with the file's code loaded when it was closed, reload it
|
|
// when the editor holds another file, show the window if hidden, raise.
|
|
static void showCodeView(int32_t fileIdx) {
|
|
bool reopened = !sCodeWin;
|
|
|
|
if (reopened) {
|
|
showCodeWindow();
|
|
}
|
|
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
if (reopened || sEditorFileIdx != fileIdx) {
|
|
if (sProject.files[fileIdx].kind == PrjKindFormE) {
|
|
loadFormCodeIntoEditor();
|
|
} else {
|
|
// Stash first: it may replace this very file's buffer
|
|
saveCurProc(true);
|
|
stashEditor();
|
|
|
|
bool missing = false;
|
|
char *owned = NULL;
|
|
const char *source = projectFileText(fileIdx, &owned, &missing);
|
|
|
|
loadModuleIntoEditor(fileIdx, source ? source : "");
|
|
free(owned);
|
|
}
|
|
}
|
|
|
|
if (!sCodeWin->visible) {
|
|
dvxShowWindow(sAc, sCodeWin);
|
|
}
|
|
|
|
dvxRaiseWindow(sAc, sCodeWin);
|
|
setStatus("Code view.");
|
|
}
|
|
|
|
|
|
static void showCodeWindow(void) {
|
|
if (sCodeWin) {
|
|
return; // already open
|
|
}
|
|
|
|
int32_t codeY = toolbarBottom();
|
|
int32_t codeH = sAc->display.height - codeY - (IDE_OUTPUT_WIN_H + IDE_OUTPUT_WIN_GAP);
|
|
|
|
sCodeWin = dvxCreateWindow(sAc, "Code", 0, codeY, sAc->display.width, codeH, true);
|
|
|
|
if (!sCodeWin) {
|
|
return;
|
|
}
|
|
|
|
// Ensure position is below the toolbar (dvxCreateWindow may adjust)
|
|
sCodeWin->y = codeY;
|
|
sCodeWin->onMenu = onMenu;
|
|
sCodeWin->onFocus = onContentFocus;
|
|
sCodeWin->onClose = onCodeWinClose;
|
|
sCodeWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sLastFocusWin = sCodeWin;
|
|
|
|
WidgetT *codeRoot = wgtInitWindow(sAc, sCodeWin);
|
|
WidgetT *dropdownRow = codeRoot ? wgtHBox(codeRoot) : NULL;
|
|
|
|
sObjDropdown = (dropdownRow && wgtLabel(dropdownRow, "Object:")) ? wgtDropdown(dropdownRow) : NULL;
|
|
sEvtDropdown = (sObjDropdown && wgtLabel(dropdownRow, "Function:")) ? wgtDropdown(dropdownRow) : NULL;
|
|
sEditor = sEvtDropdown ? wgtTextArea(codeRoot, IDE_MAX_SOURCE) : NULL;
|
|
|
|
if (!sEditor) {
|
|
dvxDestroyWindow(sAc, sCodeWin);
|
|
sCodeWin = NULL;
|
|
sObjDropdown = NULL;
|
|
sEvtDropdown = NULL;
|
|
sLastFocusWin = NULL;
|
|
ideErrorBox("Not enough memory to open the code window.");
|
|
return;
|
|
}
|
|
|
|
// A new editor shows no section yet
|
|
sCurProcIdx = IDE_SECTION_NONE;
|
|
|
|
dropdownRow->spacing = wgtPixels(IDE_WIDGET_SPACING);
|
|
|
|
sObjDropdown->weight = WGT_WEIGHT_FILL;
|
|
sObjDropdown->onChange = onObjDropdownChange;
|
|
wgtDropdownSetItems(sObjDropdown, NULL, 0);
|
|
|
|
sEvtDropdown->weight = WGT_WEIGHT_FILL;
|
|
sEvtDropdown->onChange = onEvtDropdownChange;
|
|
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
|
|
|
|
sEditor->weight = WGT_WEIGHT_FILL;
|
|
wgtTextAreaSetSyntaxMode(sEditor, IDE_SYNTAX_MODE);
|
|
|
|
// Apply saved syntax colors
|
|
uint32_t initColors[TEXT_SYNTAX_MAX];
|
|
|
|
for (int32_t i = 0; i < TEXT_SYNTAX_MAX; i++) {
|
|
char key[IDE_PREF_KEY_BUF];
|
|
snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i);
|
|
initColors[i] = (uint32_t)prefsGetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)textEditDefaultSyntaxRgb((uint8_t)i));
|
|
}
|
|
|
|
wgtTextAreaSetSyntaxColors(sEditor, initColors, TEXT_SYNTAX_MAX);
|
|
|
|
wgtTextAreaSetGutterClick(sEditor, onGutterClick);
|
|
wgtTextAreaSetShowLineNumbers(sEditor, true);
|
|
wgtTextAreaSetAutoIndent(sEditor, true);
|
|
wgtTextAreaSetCaptureTabs(sEditor, true);
|
|
wgtTextAreaSetTabWidth(sEditor, prefsGetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, IDE_DEFAULT_TAB_WIDTH));
|
|
wgtTextAreaSetUseTabChar(sEditor, !prefsGetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, true));
|
|
|
|
// Programmatic loads go through editorSetText, which suppresses
|
|
// onChange, so the handler can be attached right away without false
|
|
// dirty marking.
|
|
sEditor->onChange = onEditorChange;
|
|
editorSnapshot(countLines(wgtGetText(sEditor)));
|
|
|
|
updateProjectMenuState();
|
|
updateDirtyIndicators();
|
|
}
|
|
|
|
|
|
// showCompileError -- present sOutputBuf (already filled) as the
|
|
// compile result and leave the busy state.
|
|
static void showCompileError(const char *status) {
|
|
outputLog(sOutputBuf);
|
|
setOutputText(sOutputBuf);
|
|
showOutputWindow();
|
|
|
|
if (sOutWin) {
|
|
dvxRaiseWindow(sAc, sOutWin);
|
|
}
|
|
|
|
setStatus(status);
|
|
dvxSetBusy(sAc, false);
|
|
}
|
|
|
|
|
|
static void showImmediateWindow(void) {
|
|
if (sImmWin) {
|
|
dvxShowWindow(sAc, sImmWin);
|
|
dvxRaiseWindow(sAc, sImmWin);
|
|
return;
|
|
}
|
|
|
|
int32_t outH = IDE_OUTPUT_WIN_H;
|
|
int32_t outY = sAc->display.height - outH;
|
|
|
|
sImmWin = dvxCreateWindow(sAc, "Immediate", sAc->display.width / 2, outY, sAc->display.width / 2, outH, true);
|
|
|
|
if (sImmWin) {
|
|
sImmWin->onClose = onImmWinClose;
|
|
sImmWin->onFocus = onContentFocus;
|
|
sImmWin->onMenu = onMenu;
|
|
sImmWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sLastFocusWin = sImmWin;
|
|
|
|
WidgetT *immRoot = wgtInitWindow(sAc, sImmWin);
|
|
|
|
if (immRoot) {
|
|
sImmediate = wgtTextArea(immRoot, IDE_MAX_IMM);
|
|
|
|
if (sImmediate) {
|
|
sImmediate->weight = WGT_WEIGHT_FILL;
|
|
sImmediate->readOnly = false;
|
|
sImmediate->onKeyDown = onImmediateKeyDown;
|
|
} else {
|
|
dvxLog("IDE: failed to create immediate TextArea");
|
|
}
|
|
|
|
} else {
|
|
dvxLog("IDE: failed to init immediate window root");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void showLocalsWindow(void) {
|
|
if (sLocalsWin) {
|
|
dvxShowWindow(sAc, sLocalsWin);
|
|
dvxRaiseWindow(sAc, sLocalsWin);
|
|
updateLocalsWindow();
|
|
return;
|
|
}
|
|
|
|
int32_t winW = IDE_LOCALS_WIN_W;
|
|
int32_t winH = IDE_LOCALS_WIN_H;
|
|
int32_t winX = sAc->display.width - winW;
|
|
int32_t winY = toolbarBottom();
|
|
|
|
sLocalsWin = dvxCreateWindow(sAc, "Locals", winX, winY, winW, winH, true);
|
|
|
|
if (sLocalsWin) {
|
|
sLocalsWin->onClose = onLocalsClose;
|
|
sLocalsWin->onMenu = onMenu;
|
|
sLocalsWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sLocalsWin->resizable = true;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sLocalsWin);
|
|
|
|
if (root) {
|
|
sLocalsList = wgtListView(root);
|
|
|
|
if (sLocalsList) {
|
|
sLocalsList->weight = WGT_WEIGHT_FILL;
|
|
|
|
static const ListViewColT cols[] = {
|
|
{ "Name", wgtChars(IDE_COL_W_NAME), ListViewAlignLeftE },
|
|
{ "Type", wgtChars(IDE_COL_W_TYPE), ListViewAlignLeftE },
|
|
{ "Value", wgtChars(IDE_COL_W_VALUE), ListViewAlignLeftE },
|
|
};
|
|
|
|
wgtListViewSetColumns(sLocalsList, cols, DVX_ARRAY_LEN(cols));
|
|
}
|
|
}
|
|
}
|
|
|
|
updateLocalsWindow();
|
|
}
|
|
|
|
|
|
static void showOutputWindow(void) {
|
|
if (sOutWin) {
|
|
dvxShowWindow(sAc, sOutWin);
|
|
return;
|
|
}
|
|
|
|
int32_t outH = IDE_OUTPUT_WIN_H;
|
|
int32_t outY = sAc->display.height - outH;
|
|
|
|
sOutWin = dvxCreateWindow(sAc, "Output", 0, outY, sAc->display.width / 2, outH, true);
|
|
|
|
if (sOutWin) {
|
|
sOutWin->onClose = onOutWinClose;
|
|
sOutWin->onFocus = onContentFocus;
|
|
sOutWin->onMenu = onMenu;
|
|
sOutWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sLastFocusWin = sOutWin;
|
|
|
|
WidgetT *outRoot = wgtInitWindow(sAc, sOutWin);
|
|
|
|
sOutput = outRoot ? wgtTextArea(outRoot, IDE_MAX_OUTPUT) : NULL;
|
|
|
|
if (!sOutput) {
|
|
dvxLog("IDE: failed to create output TextArea");
|
|
return;
|
|
}
|
|
|
|
sOutput->weight = WGT_WEIGHT_FILL;
|
|
sOutput->readOnly = true;
|
|
|
|
if (sOutputLen > 0) {
|
|
setOutputText(sOutputBuf);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void showPreferencesDialog(void) {
|
|
// One dialog at a time: its handlers share sPrefsDlg
|
|
if (sPrefsDlg.win) {
|
|
dvxRaiseWindow(sAc, sPrefsDlg.win);
|
|
return;
|
|
}
|
|
|
|
memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));
|
|
|
|
WindowT *win = dvxCreateWindowCentered(sAc, "Preferences", IDE_PREFS_WIN_W, IDE_PREFS_WIN_H, false);
|
|
|
|
if (!win) {
|
|
return;
|
|
}
|
|
|
|
win->maxW = win->w;
|
|
win->maxH = win->h;
|
|
|
|
if (!prefsBuild(win)) {
|
|
dvxDestroyWindow(sAc, win);
|
|
memset(&sPrefsDlg, 0, sizeof(sPrefsDlg));
|
|
ideErrorBox("Not enough memory to open Preferences.");
|
|
return;
|
|
}
|
|
|
|
// Closing the window (X, Alt+F4) cancels like the Cancel button
|
|
sPrefsDlg.win = win;
|
|
win->onClose = onPrefsClose;
|
|
win->modal = true;
|
|
dvxFitWindow(sAc, win);
|
|
|
|
WindowT *prevModal = sAc->modalWindow;
|
|
sAc->modalWindow = win;
|
|
|
|
while (!sPrefsDlg.done && sAc->running) {
|
|
dvxUpdate(sAc);
|
|
}
|
|
|
|
if (sPrefsDlg.accepted) {
|
|
// General tab
|
|
prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_RENAME_SKIP, wgtCheckboxIsChecked(sPrefsDlg.renameSkipComments));
|
|
prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_OPTION_EXPLICIT, wgtCheckboxIsChecked(sPrefsDlg.optionExplicit));
|
|
prefsSetBool(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_USE_SPACES, wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
|
|
|
|
const char *tw = wgtGetText(sPrefsDlg.tabWidthInput);
|
|
int32_t tabW = tw ? atoi(tw) : IDE_DEFAULT_TAB_WIDTH;
|
|
|
|
if (tabW < IDE_MIN_TAB_WIDTH) {
|
|
tabW = IDE_MIN_TAB_WIDTH;
|
|
}
|
|
|
|
if (tabW > IDE_MAX_TAB_WIDTH) {
|
|
tabW = IDE_MAX_TAB_WIDTH;
|
|
}
|
|
|
|
prefsSetInt(sPrefs, IDE_PREF_SECTION_EDITOR, IDE_PREF_KEY_TAB_WIDTH, tabW);
|
|
|
|
if (sEditor) {
|
|
wgtTextAreaSetTabWidth(sEditor, tabW);
|
|
wgtTextAreaSetUseTabChar(sEditor, !wgtCheckboxIsChecked(sPrefsDlg.useSpaces));
|
|
}
|
|
|
|
const char *val;
|
|
|
|
val = wgtGetText(sPrefsDlg.defAuthor);
|
|
prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_AUTHOR, val ? val : "");
|
|
|
|
val = wgtGetText(sPrefsDlg.defPublisher);
|
|
prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_PUBLISHER, val ? val : "");
|
|
|
|
val = wgtGetText(sPrefsDlg.defVersion);
|
|
prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_VERSION, val ? val : IDE_PREF_DEFAULT_VERSION);
|
|
|
|
val = wgtGetText(sPrefsDlg.defCopyright);
|
|
prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_COPYRIGHT, val ? val : "");
|
|
|
|
char desc[PRJ_MAX_DESC];
|
|
|
|
val = wgtGetText(sPrefsDlg.defDescription);
|
|
prjSingleLine(desc, sizeof(desc), val ? val : "");
|
|
prefsSetString(sPrefs, IDE_PREF_SECTION_DEFAULTS, IDE_PREF_KEY_DESCRIPTION, desc);
|
|
|
|
// Colors tab
|
|
for (int32_t i = 0; i < TEXT_SYNTAX_MAX; i++) {
|
|
char key[IDE_PREF_KEY_BUF];
|
|
snprintf(key, sizeof(key), PREF_KEY_COLOR_FMT, (int)i);
|
|
prefsSetInt(sPrefs, PREF_SEC_SYNTAX, key, (int32_t)sPrefsDlg.syntaxColors[i]);
|
|
}
|
|
|
|
applySyntaxColors();
|
|
prefsSave(sPrefs);
|
|
}
|
|
|
|
sAc->modalWindow = prevModal;
|
|
sPrefsDlg.win = NULL;
|
|
dvxDestroyWindow(sAc, win);
|
|
}
|
|
|
|
|
|
// showProc -- display a procedure buffer (-1 = General) in the editor
|
|
|
|
static void showProc(int32_t procIdx) {
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
// Save whatever is currently in the editor. Leaving an untouched
|
|
// event skeleton discards it; since arrdel shifts everything after
|
|
// it, the target index follows.
|
|
if (sCurProcIdx >= IDE_SECTION_GENERAL) {
|
|
int32_t leftIdx = sCurProcIdx;
|
|
bool removed = saveCurProc(procIdx != sCurProcIdx);
|
|
|
|
// Every later procedure's line number depends on the buffer just
|
|
// saved (lines added or removed above it shift them), so the proc
|
|
// table is rebuilt after any save -- not only when the list
|
|
// changed -- or breakpoints toggled in the next proc land on the
|
|
// wrong compile line.
|
|
joinProcBufs();
|
|
|
|
if (removed && leftIdx >= 0 && procIdx > leftIdx) {
|
|
procIdx--;
|
|
}
|
|
}
|
|
|
|
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcBufs)) {
|
|
editorSetText(sProcBufs[procIdx] ? sProcBufs[procIdx] : "", IDE_CARET_LOAD);
|
|
sCurProcIdx = procIdx;
|
|
} else {
|
|
editorSetText(sGeneralBuf ? sGeneralBuf : "", IDE_CARET_LOAD);
|
|
sCurProcIdx = IDE_SECTION_GENERAL;
|
|
}
|
|
}
|
|
|
|
|
|
// showProcAndFind -- switch to a procedure, sync the dropdowns, and
|
|
// select the search match in the editor.
|
|
|
|
static bool showProcAndFind(int32_t procIdx, const char *needle, bool caseSensitive, bool forward) {
|
|
showProc(procIdx);
|
|
|
|
// Sync the Object/Event dropdowns to match
|
|
if (procIdx >= 0 && procIdx < (int32_t)arrlen(sProcTable)) {
|
|
selectDropdowns(sProcTable[procIdx].objName, sProcTable[procIdx].evtName);
|
|
} else if (procIdx == -1 && sObjDropdown) {
|
|
wgtDropdownSetSelected(sObjDropdown, 0); // (General)
|
|
}
|
|
|
|
if (sEditor) {
|
|
// showProc leaves the cursor at the top of the proc, which is the
|
|
// wrong anchor for a backward scan (it would find nothing). When
|
|
// searching backward, move the cursor to the true end of the buffer
|
|
// (offset == len) so the scan walks upward through every match,
|
|
// including any on the final line of a proc with no trailing newline.
|
|
if (!forward) {
|
|
wgtTextAreaCursorToEnd(sEditor);
|
|
}
|
|
|
|
return wgtTextAreaFindNext(sEditor, needle, caseSensitive, forward);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// showProperties -- the Properties window (made when there is none)
|
|
static void showProperties(void) {
|
|
if (sPropsWin) {
|
|
return;
|
|
}
|
|
|
|
sPropsWin = prpCreate(sAc, &sDesigner);
|
|
|
|
if (sPropsWin) {
|
|
sPropsWin->y = toolbarBottom();
|
|
sPropsWin->onMenu = onMenu;
|
|
sPropsWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
propsFollowActive();
|
|
}
|
|
}
|
|
|
|
|
|
// showToolbox -- the Toolbox window (made when there is none): every
|
|
// control type a form can hold, the project's own UserControls included
|
|
static void showToolbox(void) {
|
|
if (sToolboxWin) {
|
|
return;
|
|
}
|
|
|
|
sToolboxWin = tbxCreate(sAc, &sDesigner, toolbarBottom());
|
|
|
|
if (sToolboxWin) {
|
|
sToolboxWin->onMenu = onMenu;
|
|
sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
}
|
|
}
|
|
|
|
|
|
static void showWatchWindow(void) {
|
|
if (sWatchWin) {
|
|
dvxShowWindow(sAc, sWatchWin);
|
|
dvxRaiseWindow(sAc, sWatchWin);
|
|
updateWatchWindow();
|
|
return;
|
|
}
|
|
|
|
int32_t winW = IDE_WATCH_WIN_W;
|
|
int32_t winH = IDE_DEBUG_WIN_H;
|
|
int32_t winX = sAc->display.width - winW - IDE_WATCH_WIN_X_GAP;
|
|
int32_t winY = toolbarBottom();
|
|
|
|
sWatchWin = dvxCreateWindow(sAc, "Watch", winX, winY, winW, winH, true);
|
|
|
|
if (sWatchWin) {
|
|
sWatchWin->onClose = onWatchClose;
|
|
sWatchWin->onMenu = onMenu;
|
|
sWatchWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sWatchWin->resizable = true;
|
|
|
|
WidgetT *root = wgtInitWindow(sAc, sWatchWin);
|
|
|
|
if (root) {
|
|
// Expression input at top
|
|
sWatchInput = wgtTextInput(root, IDE_VALUE_BUF);
|
|
|
|
if (sWatchInput) {
|
|
sWatchInput->onKeyDown = onWatchInputKeyDown;
|
|
}
|
|
|
|
// Results list below
|
|
sWatchList = wgtListView(root);
|
|
|
|
if (sWatchList) {
|
|
sWatchList->weight = WGT_WEIGHT_FILL;
|
|
sWatchList->onKeyDown = onWatchListKeyDown;
|
|
sWatchList->onDblClick = onWatchListDblClick;
|
|
|
|
static const ListViewColT cols[] = {
|
|
{ "Expression", wgtChars(IDE_COL_W_EXPR), ListViewAlignLeftE },
|
|
{ "Value", wgtChars(IDE_COL_W_WATCH_VALUE), ListViewAlignLeftE },
|
|
};
|
|
|
|
wgtListViewSetColumns(sWatchList, cols, DVX_ARRAY_LEN(cols));
|
|
}
|
|
}
|
|
}
|
|
|
|
updateWatchWindow();
|
|
}
|
|
|
|
|
|
// skipLine -- advance past the end of the current line (or to the NUL)
|
|
static const char *skipLine(const char *p) {
|
|
while (*p && *p != '\n') {
|
|
p++;
|
|
}
|
|
|
|
if (*p == '\n') {
|
|
p++;
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
|
|
// splitProcs -- split source into a (General) buffer plus one buffer per
|
|
// SUB/FUNCTION block. Every line outside a procedure (including any
|
|
// between or after procedures) belongs to General, so nothing is lost
|
|
// when the file is reassembled; General's trailing blank lines are
|
|
// trimmed. When outMap is not NULL it receives, per source line, the
|
|
// piece and line it went to (a trimmed line maps past General's end).
|
|
// Returns false, with nothing allocated, when memory runs out. Caller
|
|
// frees the results.
|
|
static bool splitProcs(const char *source, char **outGeneral, char ***outProcs, IdeLineDestT **outMap) {
|
|
int32_t srcLen = (int32_t)strlen(source);
|
|
char *general = (char *)malloc(srcLen + 2); // room for '\n' + NUL
|
|
int32_t genLen = 0;
|
|
int32_t genLine = 0;
|
|
char **procs = NULL;
|
|
IdeLineDestT *map = NULL;
|
|
const char *pos = source;
|
|
bool ok = general != NULL;
|
|
|
|
while (ok && *pos) {
|
|
bool isSub = false;
|
|
|
|
if (basProcDeclAt(dvxSkipWs(pos), &isSub)) {
|
|
int32_t endOffset = 0;
|
|
const char *end = basProcSkipToEnd(pos, isSub, &endOffset);
|
|
int32_t procLen = (int32_t)(end - pos);
|
|
char *procBuf = (char *)malloc(procLen + 1);
|
|
|
|
ok = procBuf != NULL;
|
|
|
|
if (ok) {
|
|
memcpy(procBuf, pos, procLen);
|
|
procBuf[procLen] = '\0';
|
|
arrput(procs, procBuf);
|
|
|
|
int32_t line = 0;
|
|
|
|
for (const char *q = pos; outMap && q < end; q = skipLine(q)) {
|
|
IdeLineDestT dest = { (int32_t)arrlen(procs) - 1, line++ };
|
|
arrput(map, dest);
|
|
}
|
|
}
|
|
|
|
pos = end;
|
|
continue;
|
|
}
|
|
|
|
const char *next = skipLine(pos);
|
|
|
|
memcpy(general + genLen, pos, next - pos);
|
|
genLen += (int32_t)(next - pos);
|
|
|
|
if (outMap) {
|
|
IdeLineDestT dest = { IDE_SECTION_GENERAL, genLine };
|
|
arrput(map, dest);
|
|
}
|
|
|
|
genLine++;
|
|
pos = next;
|
|
}
|
|
|
|
if (!ok) {
|
|
free(general);
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(procs); i++) {
|
|
free(procs[i]);
|
|
}
|
|
|
|
arrfree(procs);
|
|
arrfree(map);
|
|
return false;
|
|
}
|
|
|
|
// Trim trailing blank lines; a non-empty section ends in one newline
|
|
general[genLen] = '\0';
|
|
genLen = dvxTrimRight(general);
|
|
|
|
if (genLen > 0) {
|
|
general[genLen++] = '\n';
|
|
}
|
|
|
|
general[genLen] = '\0';
|
|
|
|
*outGeneral = general;
|
|
*outProcs = procs;
|
|
|
|
if (outMap) {
|
|
*outMap = map;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// stashCurrentFile -- stash the editor's and the designer's state back
|
|
// into the project buffers of the files they belong to (the designer may
|
|
// hold a form while a module is active). Caching only: nothing is
|
|
// marked modified beyond mirroring the designer's dirty flag. Returns
|
|
// false, after telling the user, when the form cannot be serialized; its
|
|
// previous buffer is kept.
|
|
|
|
static bool stashCurrentFile(void) {
|
|
stashEditor();
|
|
|
|
if (!sDesigner.form || sDesignerFileIdx < 0 || sDesignerFileIdx >= prjFileCount(&sProject)) {
|
|
return true;
|
|
}
|
|
|
|
PrjFileT *file = &sProject.files[sDesignerFileIdx];
|
|
|
|
syncFormDirty();
|
|
|
|
// Serialize form designer state to .frm text
|
|
char *frmBuf = (char *)malloc(IDE_MAX_SOURCE);
|
|
|
|
if (!frmBuf) {
|
|
ideErrorBox("Not enough memory to save the form.");
|
|
return false;
|
|
}
|
|
|
|
int32_t frmLen = dsgnSaveFrm(&sDesigner, frmBuf, IDE_MAX_SOURCE);
|
|
|
|
// On failure keep the previous buffer rather than a truncated one
|
|
if (frmLen < 0) {
|
|
free(frmBuf);
|
|
ideErrorBox(frmLen == FRM_WRITE_LINE_TOO_LONG ? "A property value is too long to save at its position in the form. Shorten it and save again." : "Form is too large to save; its code section does not fit.");
|
|
return false;
|
|
}
|
|
|
|
frmBuf[frmLen] = '\0';
|
|
|
|
// Keep only what the text needs, not the whole scratch size
|
|
char *fitted = (char *)realloc(frmBuf, (size_t)frmLen + 1);
|
|
|
|
free(file->buffer);
|
|
file->buffer = fitted ? fitted : frmBuf;
|
|
snprintf(file->formName, sizeof(file->formName), "%s", sDesigner.form->name);
|
|
return true;
|
|
}
|
|
|
|
|
|
// stashEditor -- write the editor's text back to the file it is bound
|
|
// to: a module's project buffer, or the designer form's code.
|
|
static void stashEditor(void) {
|
|
if (sEditorFileIdx < 0 || sEditorFileIdx >= prjFileCount(&sProject)) {
|
|
return;
|
|
}
|
|
|
|
PrjFileT *file = &sProject.files[sEditorFileIdx];
|
|
|
|
if (file->kind == PrjKindFormE) {
|
|
stashFormCode();
|
|
return;
|
|
}
|
|
|
|
char *copy = strdup(getFullSource());
|
|
|
|
if (copy) {
|
|
free(file->buffer);
|
|
file->buffer = copy;
|
|
}
|
|
}
|
|
|
|
|
|
// stashFormCode -- if the proc buffers belong to the designer's form,
|
|
// save them back to form->code.
|
|
|
|
static void stashFormCode(void) {
|
|
if (!sDesigner.form || sEditorFileIdx < 0 || sEditorFileIdx != sDesignerFileIdx) {
|
|
return;
|
|
}
|
|
|
|
char *code = strdup(getFullSource());
|
|
|
|
if (code) {
|
|
free(sDesigner.form->code);
|
|
sDesigner.form->code = code;
|
|
}
|
|
}
|
|
|
|
|
|
static void switchToDesign(void) {
|
|
stashFormCode();
|
|
|
|
// If already open, just bring to front
|
|
if (sFormWin) {
|
|
dvxRaiseWindow(sAc, sFormWin);
|
|
return;
|
|
}
|
|
|
|
// A form file the designer could not parse opens as a blank form
|
|
if (!sDesigner.form) {
|
|
dsgnNewForm(&sDesigner, IDE_DEFAULT_FORM);
|
|
sDesignerFileIdx = sProject.activeFileIdx;
|
|
}
|
|
|
|
if (!sDesigner.form) {
|
|
return;
|
|
}
|
|
|
|
FrmModelT *form = sDesigner.form;
|
|
|
|
// Create the form designer window using the shared form window builder
|
|
char title[IDE_FORM_TITLE_BUF];
|
|
snprintf(title, sizeof(title), "%s [Design]", form->name);
|
|
|
|
WidgetT *root;
|
|
WidgetT *contentBox;
|
|
|
|
sFormWin = basFormRtCreateFormWindow(sAc, title, form->layout, form->resizable, false, false, form->width, form->height, 0, 0, &root, &contentBox);
|
|
|
|
if (!sFormWin) {
|
|
return;
|
|
}
|
|
|
|
sFormWin->visible = true;
|
|
sFormWin->onClose = onFormWinClose;
|
|
sFormWin->onMenu = onFormWinMenu;
|
|
sFormWin->onFocus = onContentFocus;
|
|
sFormWin->accelTable = sWin ? sWin->accelTable : NULL;
|
|
sDesigner.formWin = sFormWin;
|
|
|
|
// UserControls placed here see UserMode False (their code draws, the
|
|
// input goes to the designer) from their Initialize on
|
|
basDsgnSetDesignWindow(sFormWin, true);
|
|
|
|
// Build preview menu bar from form's menu items
|
|
dsgnBuildPreviewMenuBar(sFormWin, form);
|
|
|
|
// Override paint and mouse AFTER wgtInitWindow (which sets widgetOnPaint).
|
|
// onResize is attached after the initial sizing below: sizing the
|
|
// window to the form is not an edit.
|
|
sFormWin->onPaint = onFormWinPaint;
|
|
sFormWin->onMouse = onFormWinMouse;
|
|
sFormWin->onKey = onFormWinKey;
|
|
sFormWin->onCursorQuery = onFormWinCursorQuery;
|
|
|
|
// Create live widgets for each control
|
|
dsgnCreateWidgets(&sDesigner, contentBox);
|
|
|
|
// Set form caption as window title
|
|
if (form->caption[0]) {
|
|
snprintf(title, sizeof(title), "%s [Design]", form->caption);
|
|
dvxSetTitle(sAc, sFormWin, title);
|
|
}
|
|
|
|
dsgnFitFormWin(&sDesigner);
|
|
sFormWin->onResize = onFormWinResize;
|
|
|
|
// Create toolbox and properties windows
|
|
showToolbox();
|
|
showProperties();
|
|
|
|
dvxInvalidateWindow(sAc, sFormWin);
|
|
updateProjectMenuState();
|
|
setStatus("Design view open.");
|
|
}
|
|
|
|
|
|
// syncFormDirty -- mirror form->dirty onto the designer file's project
|
|
// entry so the tree, title bar and save logic all see one consistent flag.
|
|
static void syncFormDirty(void) {
|
|
if (sDesigner.form && sDesigner.form->dirty && sDesignerFileIdx >= 0 && sDesignerFileIdx < prjFileCount(&sProject)) {
|
|
sProject.files[sDesignerFileIdx].modified = true;
|
|
}
|
|
}
|
|
|
|
|
|
// syncVmBreakpoints -- push the IDE breakpoint list to the live VM (no-op
|
|
// when nothing is running or the program was started without debugging).
|
|
static void syncVmBreakpoints(void) {
|
|
if (!sVm || !sDbgEnabled) {
|
|
return;
|
|
}
|
|
|
|
buildVmBreakpoints();
|
|
basVmSetBreakpoints(sVm, sVmBreakpoints, (int32_t)arrlen(sVmBreakpoints));
|
|
}
|
|
|
|
|
|
static void teardownFormWin(void) {
|
|
if (sFormWin) {
|
|
dvxDestroyWindow(sAc, sFormWin);
|
|
cleanupFormWin();
|
|
}
|
|
}
|
|
|
|
|
|
static void toggleBreakpoint(void) {
|
|
if (!sEditor) {
|
|
return;
|
|
}
|
|
|
|
toggleBreakpointLine(wgtTextAreaGetCursorLine(sEditor));
|
|
}
|
|
|
|
|
|
static void toggleBreakpointLine(int32_t editorLine) {
|
|
int32_t fileIdx = sEditorFileIdx;
|
|
|
|
if (fileIdx < 0) {
|
|
return;
|
|
}
|
|
|
|
// Convert editor line to file code line by adding proc offset
|
|
int32_t codeLine = editorLineToCodeLine(editorLine);
|
|
|
|
// Check if this breakpoint already exists -- remove it
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sBreakpoints); i++) {
|
|
if (sBreakpoints[i].fileIdx == fileIdx && sBreakpoints[i].codeLine == codeLine) {
|
|
arrdel(sBreakpoints, i);
|
|
syncVmBreakpoints();
|
|
updateBreakpointWindow();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate that this line is breakable (not blank, comment, SUB/FUNCTION
|
|
// decl/end or a class module's header)
|
|
if (sEditor) {
|
|
const char *text = wgtGetText(sEditor);
|
|
|
|
if (text) {
|
|
// A class module's header opens its (General) section
|
|
if (sProject.files[fileIdx].kind == PrjKindClassE && sCurProcIdx == IDE_SECTION_GENERAL) {
|
|
int32_t firstCodeLine = 1;
|
|
|
|
prjClassCodePos(text, (int32_t)strlen(text), &firstCodeLine);
|
|
|
|
if (editorLine < firstCodeLine) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Find the start of editorLine (1-based)
|
|
const char *p = text;
|
|
|
|
for (int32_t ln = 1; *p && ln < editorLine; ln++) {
|
|
p = skipLine(p);
|
|
}
|
|
|
|
// Skip leading whitespace
|
|
p = dvxSkipWs(p);
|
|
|
|
// Blank line
|
|
if (*p == '\0' || *p == '\n' || *p == '\r') {
|
|
return;
|
|
}
|
|
|
|
// Comment (single quote or REM)
|
|
if (*p == '\'') {
|
|
return;
|
|
}
|
|
|
|
if (isRemAt(p, 0, (int32_t)strcspn(p, "\n"))) {
|
|
return;
|
|
}
|
|
|
|
// SUB/FUNCTION declaration or END SUB/FUNCTION
|
|
bool isSub;
|
|
|
|
if (basProcDeclAt(p, &isSub) || kwMatch(p, BAS_PROC_KW_END_SUB) || kwMatch(p, BAS_PROC_KW_END_FUNCTION)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add new breakpoint
|
|
IdeBreakpointT bp;
|
|
memset(&bp, 0, sizeof(bp));
|
|
bp.fileIdx = fileIdx;
|
|
bp.codeLine = codeLine;
|
|
|
|
setBreakpointProcName(&bp, sCurProcIdx);
|
|
|
|
arrput(sBreakpoints, bp);
|
|
syncVmBreakpoints();
|
|
|
|
// The first breakpoint of the file starts the edit snapshots
|
|
if (sEditor) {
|
|
editorSnapshot(sEditorLineCount);
|
|
}
|
|
|
|
updateBreakpointWindow();
|
|
}
|
|
|
|
|
|
static int32_t toolbarBottom(void) {
|
|
return sWin ? sWin->y + sWin->h + IDE_TOOLBAR_GAP : IDE_TOOLBAR_FALLBACK_Y;
|
|
}
|
|
|
|
|
|
static void updateBreakpointWindow(void) {
|
|
refreshEditorMarkers();
|
|
|
|
if (!sBreakpointList || !sBreakpointWin || !sBreakpointWin->visible) {
|
|
return;
|
|
}
|
|
|
|
listRowsReset(&sBpRows);
|
|
|
|
int32_t count = (int32_t)arrlen(sBreakpoints);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
char lineBuf[IDE_LINE_NUM_BUF];
|
|
|
|
// File name
|
|
if (sBreakpoints[i].fileIdx >= 0 && sBreakpoints[i].fileIdx < prjFileCount(&sProject)) {
|
|
listRowsAdd(&sBpRows, sProject.files[sBreakpoints[i].fileIdx].path);
|
|
} else {
|
|
listRowsAdd(&sBpRows, "?");
|
|
}
|
|
|
|
// Procedure name
|
|
listRowsAdd(&sBpRows, sBreakpoints[i].procName);
|
|
|
|
// Line number
|
|
snprintf(lineBuf, sizeof(lineBuf), "%d", (int)sBreakpoints[i].codeLine);
|
|
listRowsAdd(&sBpRows, lineBuf);
|
|
}
|
|
|
|
listRowsCommit(&sBpRows, sBreakpointList, count);
|
|
}
|
|
|
|
|
|
static void updateCallStackWindow(void) {
|
|
if (!sCallStackList || !sCallStackWin || !sCallStackWin->visible) {
|
|
return;
|
|
}
|
|
|
|
listRowsReset(&sCallRows);
|
|
|
|
int32_t rowCount = (sDbgState == DbgPausedE && sVm && sDbgModule) ? basDbgCallStack(sVm, sDbgCurrentLine, listRowsAddRow, &sCallRows) : 0;
|
|
|
|
listRowsCommit(&sCallRows, sCallStackList, rowCount);
|
|
}
|
|
|
|
|
|
static void updateDirtyIndicators(void) {
|
|
syncFormDirty();
|
|
updateMainTitle();
|
|
|
|
// Code window title -- only shows * when code has been edited
|
|
if (sCodeWin) {
|
|
bool codeDirty = false;
|
|
|
|
const char *codeFile = "";
|
|
|
|
if (sEditorFileIdx >= 0 && sEditorFileIdx < prjFileCount(&sProject)) {
|
|
codeDirty = sProject.files[sEditorFileIdx].modified;
|
|
codeFile = sProject.files[sEditorFileIdx].path;
|
|
} else if (sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject)) {
|
|
codeDirty = sProject.files[sProject.activeFileIdx].modified;
|
|
codeFile = sProject.files[sProject.activeFileIdx].path;
|
|
}
|
|
|
|
char codeTitle[IDE_MSG_BUF];
|
|
snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : "");
|
|
dvxSetTitle(sAc, sCodeWin, codeTitle);
|
|
}
|
|
|
|
// Design window title
|
|
if (sFormWin && sDesigner.form) {
|
|
char title[IDE_FORM_TITLE_BUF];
|
|
snprintf(title, sizeof(title), "%s [Design]%s", sDesigner.form->caption[0] ? sDesigner.form->caption : sDesigner.form->name, sDesigner.form->dirty ? " *" : "");
|
|
dvxSetTitle(sAc, sFormWin, title);
|
|
}
|
|
|
|
// Project tree: rebuild with "*" on dirty files
|
|
if (sProjectWin) {
|
|
prjRebuildTree(&sProject);
|
|
}
|
|
}
|
|
|
|
|
|
// updateDropdowns -- refresh sProcTable from the editor's buffers and
|
|
// repopulate the Object dropdown (which cascades into the Event dropdown).
|
|
|
|
static void updateDropdowns(void) {
|
|
// Rebuilds sProcTable as a side effect; the file's WithEvents
|
|
// variables are objects too
|
|
arrsetlen(sWithEvents, 0);
|
|
basSourceWithEvents(getFullSource(), addWithEvents, NULL);
|
|
|
|
// Reset dynamic arrays. sObjItems owns strdup'd copies -- free them
|
|
// first so no entry survives a subsequent controls/menuItems mutation
|
|
// that could invalidate the source pointer.
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sObjItems); i++) {
|
|
free((void *)sObjItems[i]);
|
|
}
|
|
|
|
arrsetlen(sObjItems, 0);
|
|
evtItemsReset();
|
|
|
|
if (!sObjDropdown || !sEvtDropdown) {
|
|
return;
|
|
}
|
|
|
|
// Build object names for the Object dropdown. All entries are
|
|
// strdup'd so sObjItems owns its strings -- the designer's menuItems
|
|
// array in particular is an stb_ds struct array whose backing
|
|
// storage can move on the next arrput, which would dangle any name
|
|
// pointer we borrowed from &menuItems[i].name.
|
|
arrput(sObjItems, strdup(IDE_GENERAL_SECTION));
|
|
|
|
if (editorIsControl()) {
|
|
arrput(sObjItems, strdup(BAS_UC_OBJECT));
|
|
} else if (sDesigner.form) {
|
|
arrput(sObjItems, strdup(sDesigner.form->name));
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->controls); i++) {
|
|
arrput(sObjItems, strdup(sDesigner.form->controls[i]->name));
|
|
}
|
|
|
|
// Add menu item names (non-separator, non-top-level-header-only)
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sDesigner.form->menuItems); i++) {
|
|
FrmMenuItemT *mi = &sDesigner.form->menuItems[i];
|
|
|
|
if (mi->name[0] && mi->caption[0] != '-') {
|
|
arrput(sObjItems, strdup(mi->name));
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32_t i = 0; i < (int32_t)arrlen(sWithEvents); i++) {
|
|
arrput(sObjItems, strdup(sWithEvents[i].name));
|
|
}
|
|
|
|
// A failed strdup would put a NULL name in the dropdown
|
|
for (int32_t i = (int32_t)arrlen(sObjItems) - 1; i >= 0; i--) {
|
|
if (!sObjItems[i]) {
|
|
arrdel(sObjItems, i);
|
|
}
|
|
}
|
|
|
|
// Sort object items alphabetically, keeping (General) first
|
|
int32_t objCount = (int32_t)arrlen(sObjItems);
|
|
|
|
if (objCount > 1) {
|
|
qsort(sObjItems + 1, objCount - 1, sizeof(const char *), cmpStrPtrs);
|
|
}
|
|
|
|
wgtDropdownSetItems(sObjDropdown, sObjItems, objCount);
|
|
|
|
if (objCount > 0) {
|
|
wgtDropdownSetSelected(sObjDropdown, 0);
|
|
onObjDropdownChange(sObjDropdown);
|
|
} else {
|
|
wgtDropdownSetItems(sEvtDropdown, NULL, 0);
|
|
}
|
|
}
|
|
|
|
|
|
static void updateLocalsWindow(void) {
|
|
if (!sLocalsList || !sLocalsWin || !sLocalsWin->visible) {
|
|
return;
|
|
}
|
|
|
|
// Locals of the current proc, globals, and the current form's variables
|
|
listRowsReset(&sLocalsRows);
|
|
|
|
int32_t rowCount = (sDbgState == DbgPausedE && sVm && sDbgModule) ? basDbgLocals(sVm, listRowsAddRow, &sLocalsRows) : 0;
|
|
|
|
listRowsCommit(&sLocalsRows, sLocalsList, rowCount);
|
|
}
|
|
|
|
|
|
// updateMainTitle -- "DVX BASIC - [ProjectName] *" (the * when anything
|
|
// is unsaved), or just "DVX BASIC" with no project open.
|
|
static void updateMainTitle(void) {
|
|
if (!sWin) {
|
|
return;
|
|
}
|
|
|
|
char title[IDE_TITLE_BUF];
|
|
|
|
if (hasProject()) {
|
|
snprintf(title, sizeof(title), "%s - [%s]%s", IDE_MAIN_TITLE, sProject.name, hasUnsavedData() ? " *" : "");
|
|
} else {
|
|
snprintf(title, sizeof(title), "%s", IDE_MAIN_TITLE);
|
|
}
|
|
|
|
dvxSetTitle(sAc, sWin, title);
|
|
}
|
|
|
|
|
|
static void updateProjectMenuState(void) {
|
|
if (!sWin || !sWin->menuBar) {
|
|
return;
|
|
}
|
|
|
|
bool project = hasProject();
|
|
bool hasFile = (project && sProject.activeFileIdx >= 0 && sProject.activeFileIdx < prjFileCount(&sProject));
|
|
bool hasForm = activeIsForm();
|
|
bool isIdle = (sDbgState == DbgIdleE);
|
|
bool isPaused = (sDbgState == DbgPausedE);
|
|
bool isRunning = (sDbgState == DbgRunningE);
|
|
bool canRun = project && (isIdle || isPaused);
|
|
bool canStop = isRunning || isPaused;
|
|
|
|
// Project menu
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_SAVE, project && sProject.dirty);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_CLOSE, project && isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW, isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_OPEN, isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_OPEN, isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW_CLASS, project && isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_NEW_CTL, project && isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_PROPS, project);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_RENAME, isIdle && prjGetSelectedFileIdx() >= 0);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, isIdle && prjGetSelectedFileIdx() >= 0);
|
|
|
|
// Save: only when active file is dirty
|
|
bool fileDirty = hasFile && sProject.files[sProject.activeFileIdx].modified;
|
|
bool formDirty = hasFile && sDesigner.form && sDesigner.form->dirty;
|
|
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, fileDirty || formDirty);
|
|
|
|
// Save All: only when any file is dirty
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, anyFileModified());
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_EXE, project && isIdle);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_MAKE_PKG, project && isIdle);
|
|
|
|
// A control project makes a control file
|
|
MenuItemT *makeItem = sFileMenu ? wmMenuFindItemInMenu(sFileMenu, CMD_MAKE_EXE) : NULL;
|
|
|
|
if (makeItem) {
|
|
wmMenuItemSetLabel(makeItem, prjIsControlProject(&sProject) ? IDE_MAKE_CTL_LABEL : IDE_MAKE_EXE_LABEL);
|
|
}
|
|
|
|
// Edit menu
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, project);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, project);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, project);
|
|
|
|
// View menu -- consider both active file and project tree selection
|
|
int32_t selIdx = prjGetSelectedFileIdx();
|
|
bool selIsFile = (project && selIdx >= 0 && selIdx < prjFileCount(&sProject));
|
|
bool selIsForm = (selIsFile && sProject.files[selIdx].kind == PrjKindFormE);
|
|
bool canCode = hasFile || selIsFile;
|
|
bool canDesign = hasForm || selIsForm;
|
|
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_CODE, canCode);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_VIEW_DESIGN, canDesign);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_MENU_EDITOR, canDesign);
|
|
|
|
// Run menu
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN, canRun);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_NOCMP, project && isIdle && sCachedModule != NULL);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_DEBUG, canRun);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_STOP, canStop);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_INTO, canRun);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_OVER, isPaused);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_STEP_OUT, isPaused);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_RUN_TO_CURSOR, isPaused);
|
|
wmMenuItemSetEnabled(sWin->menuBar, CMD_TOGGLE_BP, hasFile);
|
|
|
|
// Toolbar buttons
|
|
if (sTbRun) {
|
|
wgtSetEnabled(sTbRun, canRun);
|
|
}
|
|
|
|
if (sTbStop) {
|
|
wgtSetEnabled(sTbStop, canStop);
|
|
}
|
|
|
|
if (sTbDebug) {
|
|
wgtSetEnabled(sTbDebug, canRun);
|
|
}
|
|
|
|
if (sTbStepInto) {
|
|
wgtSetEnabled(sTbStepInto, canRun);
|
|
}
|
|
|
|
if (sTbStepOver) {
|
|
wgtSetEnabled(sTbStepOver, isPaused);
|
|
}
|
|
|
|
if (sTbStepOut) {
|
|
wgtSetEnabled(sTbStepOut, isPaused);
|
|
}
|
|
|
|
if (sTbRunToCur) {
|
|
wgtSetEnabled(sTbRunToCur, isPaused);
|
|
}
|
|
|
|
if (sTbCode) {
|
|
wgtSetEnabled(sTbCode, canCode);
|
|
}
|
|
|
|
if (sTbDesign) {
|
|
wgtSetEnabled(sTbDesign, canDesign);
|
|
}
|
|
}
|
|
|
|
|
|
// updateWatchWindow -- evaluate watch expressions
|
|
|
|
static void updateWatchWindow(void) {
|
|
if (!sWatchList || !sWatchWin || !sWatchWin->visible) {
|
|
return;
|
|
}
|
|
|
|
listRowsReset(&sWatchRows);
|
|
|
|
int32_t count = (int32_t)arrlen(sWatchExprs);
|
|
|
|
for (int32_t i = 0; i < count; i++) {
|
|
char valBuf[IDE_VALUE_BUF];
|
|
|
|
valBuf[0] = '\0';
|
|
|
|
if (sDbgState == DbgPausedE && sVm && sDbgModule && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf), &sBusyPump)) {
|
|
snprintf(valBuf, sizeof(valBuf), "%s", BAS_DBG_EVAL_ERROR);
|
|
}
|
|
|
|
listRowsAdd(&sWatchRows, sWatchExprs[i]);
|
|
listRowsAdd(&sWatchRows, valBuf);
|
|
}
|
|
|
|
ideBusyPumpEnd();
|
|
listRowsCommit(&sWatchRows, sWatchList, count);
|
|
}
|
|
|
|
|
|
static void watchEditSelected(void) {
|
|
if (!sWatchList || !sWatchInput) {
|
|
return;
|
|
}
|
|
|
|
int32_t sel = wgtListViewGetSelected(sWatchList);
|
|
|
|
if (sel < 0 || sel >= (int32_t)arrlen(sWatchExprs)) {
|
|
return;
|
|
}
|
|
|
|
// Put expression text into the input box
|
|
wgtSetText(sWatchInput, sWatchExprs[sel]);
|
|
|
|
// Remove from list
|
|
free(sWatchExprs[sel]);
|
|
arrdel(sWatchExprs, sel);
|
|
|
|
updateWatchWindow();
|
|
|
|
// Focus the input box
|
|
wgtSetFocused(sWatchInput);
|
|
}
|
|
|
|
|
|
// writeProjectFile -- write files[idx].buffer to disk (a NULL buffer is
|
|
// an empty file), byte for byte, all or nothing (platformWriteFile), so a
|
|
// failed save never destroys the copy on disk. Clears the modified flags
|
|
// only on success; reports the failure otherwise.
|
|
static bool writeProjectFile(int32_t idx) {
|
|
PrjFileT *file = &sProject.files[idx];
|
|
const char *text = file->buffer ? file->buffer : "";
|
|
char fullPath[DVX_MAX_PATH];
|
|
|
|
prjFullPath(&sProject, idx, fullPath, sizeof(fullPath));
|
|
|
|
if (!platformWriteFile(fullPath, text, (int32_t)strlen(text))) {
|
|
char msg[IDE_MSG_BUF];
|
|
|
|
snprintf(msg, sizeof(msg), "Could not write %s.", file->path);
|
|
ideErrorBox(msg);
|
|
return false;
|
|
}
|
|
|
|
file->modified = false;
|
|
|
|
if (idx == sDesignerFileIdx && sDesigner.form) {
|
|
sDesigner.form->dirty = false;
|
|
}
|
|
|
|
if (file->kind == PrjKindControlE) {
|
|
designCtlRefresh();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
int32_t appMain(DxeAppContextT *ctx) {
|
|
sCtx = ctx;
|
|
sAc = ctx->shellCtx;
|
|
|
|
// Set help file and context-sensitive F1 handler
|
|
platformPathJoin(ctx->helpFile, sizeof(ctx->helpFile), ctx->appDir, IDE_HELP_FILE);
|
|
snprintf(sIdeHelpFile, sizeof(sIdeHelpFile), "%s", ctx->helpFile);
|
|
ctx->onHelpQuery = helpQueryHandler;
|
|
ctx->helpQueryCtx = NULL;
|
|
|
|
prjInit(&sProject);
|
|
buildWindow();
|
|
basUcSetPropChangedHook(onUcPropChanged);
|
|
|
|
// Load persisted settings
|
|
shellEnsureUserDir(sCtx);
|
|
char prefsPath[DVX_MAX_PATH];
|
|
shellUserPath(sCtx, IDE_PREFS_FILE, prefsPath, sizeof(prefsPath));
|
|
sPrefs = prefsLoad(prefsPath);
|
|
|
|
if (!sPrefs) {
|
|
sPrefs = prefsCreate();
|
|
prefsSaveAs(sPrefs, prefsPath);
|
|
}
|
|
|
|
if (sWin && sWin->menuBar) {
|
|
if (sToolbar) {
|
|
bool showTb = prefsGetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_TOOLBAR, true);
|
|
sToolbar->visible = showTb;
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_TOOLBAR, showTb);
|
|
}
|
|
|
|
if (sStatusBar) {
|
|
bool showSb = prefsGetBool(sPrefs, PREF_SEC_VIEW, PREF_KEY_STATUSBAR, true);
|
|
sStatusBar->visible = showSb;
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_VIEW_STATUS, showSb);
|
|
}
|
|
|
|
bool saveOnRun = prefsGetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_SAVE_ON_RUN, true);
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_SAVE_ON_RUN, saveOnRun);
|
|
|
|
sOutputToLog = prefsGetBool(sPrefs, PREF_SEC_RUN, PREF_KEY_OUTPUT_TO_LOG, false);
|
|
wmMenuItemSetChecked(sWin->menuBar, CMD_OUTPUT_TO_LOG, sOutputToLog);
|
|
}
|
|
|
|
// Load recent files list and populate menu
|
|
recentLoad();
|
|
recentRebuildMenu();
|
|
|
|
if (sWin) {
|
|
dvxFitWindowH(sAc, sWin);
|
|
}
|
|
|
|
updateProjectMenuState();
|
|
setStatus("Ready.");
|
|
|
|
// A project or source file on the command line (a document opened
|
|
// through its file type) opens at once.
|
|
char arg[DVX_MAX_PATH];
|
|
bool quoted = false;
|
|
|
|
if (dvxNextArg(ctx->args, arg, sizeof(arg), "ed)) {
|
|
openAnyPath(arg);
|
|
}
|
|
|
|
return 0;
|
|
}
|