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