From 0ef46ff6a0c429222ed4fc82908d1d0281653c64 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Fri, 3 Apr 2026 16:11:24 -0500 Subject: [PATCH] Find/Replace --- apps/dvxbasic/ide/ideMain.c | 569 +++++++++++++++++++++++++++- core/dvxApp.c | 4 - core/platform/dvxPlatformDos.c | 19 +- widgets/textInput/widgetTextInput.c | 166 +++++++- widgets/widgetTextInput.h | 4 + 5 files changed, 731 insertions(+), 31 deletions(-) diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index f9feed4..3c4bbc3 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -17,8 +17,10 @@ #include "dvxWm.h" #include "shellApp.h" #include "widgetBox.h" +#include "widgetCheckbox.h" #include "widgetImageButton.h" #include "widgetLabel.h" +#include "widgetRadio.h" #include "widgetTextInput.h" #include "widgetDropdown.h" #include "widgetButton.h" @@ -87,6 +89,9 @@ #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 IDE_MAX_IMM 1024 #define IDE_DESIGN_W 400 #define IDE_DESIGN_H 300 @@ -145,6 +150,9 @@ static void onFormWinClose(WindowT *win); static void onFormWinResize(WindowT *win, int32_t newW, int32_t newH); static void onProjectWinClose(WindowT *win); static WindowT *getLastFocusWin(void); +static void closeFindDialog(void); +static bool findInProject(const char *needle, bool caseSensitive); +static void openFindDialog(bool showReplace); static void handleEditCmd(int32_t cmd); static void handleFileCmd(int32_t cmd); static void handleProjectCmd(int32_t cmd); @@ -224,6 +232,20 @@ static char **sProcBufs = NULL; // stb_ds array: one buffer per procedure static int32_t sCurProcIdx = -2; // which buffer is in the editor (-1=General, -2=none) static int32_t sEditorFileIdx = -1; // which project file owns sProcBufs (-1=none) +// Find/Replace state +static char sFindText[256] = ""; +static char sReplaceText[256] = ""; +// 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: 0=Func, 1=Obj, 2=File, 3=Proj +static WidgetT *sDirGroup = NULL; // radio group: 0=Fwd, 1=Back + // Procedure table for Object/Event dropdowns typedef struct { char objName[64]; @@ -403,6 +425,8 @@ static void activateFile(int32_t fileIdx, IdeViewModeE view) { sEditorFileIdx = fileIdx; sProject.activeFileIdx = fileIdx; } + + updateDirtyIndicators(); } @@ -453,8 +477,10 @@ int32_t appMain(DxeAppContextT *ctx) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { - sProjectWin->y = toolbarBottom() + 25; - sProjectWin->onClose = onProjectWinClose; + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + sProjectWin->onMenu = onMenu; + sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } char title[300]; @@ -536,6 +562,10 @@ static void buildWindow(void) { 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); @@ -573,6 +603,9 @@ static void buildWindow(void) { dvxAddAccel(accel, KEY_F7, 0, CMD_VIEW_CODE); dvxAddAccel(accel, KEY_F7, ACCEL_SHIFT, CMD_VIEW_DESIGN); dvxAddAccel(accel, 0x1B, 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); sWin->accelTable = accel; WidgetT *tbRoot = wgtInitWindow(sAc, sWin); @@ -1129,6 +1162,8 @@ static void runCached(void) { static void runModule(BasModuleT *mod) { setStatus("Running..."); + closeFindDialog(); + // Hide IDE windows while the program runs bool hadFormWin = sFormWin && sFormWin->visible; bool hadToolbox = sToolboxWin && sToolboxWin->visible; @@ -1674,8 +1709,10 @@ static void newProject(void) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { - sProjectWin->y = toolbarBottom() + 25; - sProjectWin->onClose = onProjectWinClose; + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + sProjectWin->onMenu = onMenu; + sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } } else { prjRebuildTree(&sProject); @@ -1720,8 +1757,10 @@ static void openProject(void) { sProjectWin = prjCreateWindow(sAc, &sProject, onPrjFileClick); if (sProjectWin) { - sProjectWin->y = toolbarBottom() + 25; - sProjectWin->onClose = onProjectWinClose; + sProjectWin->y = toolbarBottom() + 25; + sProjectWin->onClose = onProjectWinClose; + sProjectWin->onMenu = onMenu; + sProjectWin->accelTable = sWin ? sWin->accelTable : NULL; } } else { prjRebuildTree(&sProject); @@ -1753,6 +1792,8 @@ static void closeProject(void) { return; } + closeFindDialog(); + if (sProject.dirty) { prjSave(&sProject); } @@ -2040,6 +2081,8 @@ static void onCodeWinClose(WindowT *win) { if (sLastFocusWin == win) { sLastFocusWin = NULL; } + + updateProjectMenuState(); } @@ -2342,6 +2385,466 @@ static void handleFileCmd(int32_t cmd) { } +// ============================================================ +// Find/Replace dialog (modeless) +// ============================================================ + +typedef enum { + ScopeFuncE, + ScopeObjE, + ScopeFileE, + ScopeProjE +} FindScopeE; + + +static FindScopeE getFindScope(void) { + if (!sScopeGroup) { + return ScopeProjE; + } + + int32_t idx = wgtRadioGetIndex(sScopeGroup); + + switch (idx) { + case 0: return ScopeFuncE; + case 1: return ScopeObjE; + case 2: return ScopeFileE; + default: return ScopeProjE; + } +} + + +static bool getFindMatchCase(void) { + return sCaseCheck && wgtCheckboxIsChecked(sCaseCheck); +} + + +static bool getFindForward(void) { + if (!sDirGroup) { + return true; + } + + return wgtRadioGetIndex(sDirGroup) == 0; +} + + +static bool isReplaceEnabled(void) { + return sReplCheck && wgtCheckboxIsChecked(sReplCheck); +} + + +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 onFindNext(WidgetT *w) { + (void)w; + + if (!sFindInput) { + return; + } + + const char *needle = wgtGetText(sFindInput); + + if (!needle || !needle[0]) { + return; + } + + snprintf(sFindText, sizeof(sFindText), "%s", needle); + + FindScopeE scope = getFindScope(); + bool caseSens = getFindMatchCase(); + bool forward = getFindForward(); + + if (scope == ScopeFuncE && sEditor) { + // Search current procedure only + if (!wgtTextAreaFindNext(sEditor, sFindText, caseSens, forward)) { + setStatus("Not found."); + } + } else if (scope == ScopeObjE || scope == ScopeFileE || scope == ScopeProjE) { + if (!findInProject(sFindText, caseSens)) { + setStatus("Not found."); + } + } +} + + +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 : ""); + + // If text is selected and matches the search, replace it, then find next + const char *edText = wgtGetText(sEditor); + + if (edText) { + // TODO: replace current selection if it matches, then find next + // For now, just do find next + 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) { + stashCurrentFile(); + + for (int32_t i = 0; i < sProject.fileCount; i++) { + activateFile(i, sProject.files[i].isForm ? ViewCodeE : ViewAutoE); + + if (sEditor) { + totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); + } + } + } else if (scope == ScopeFileE && sEditor) { + // Replace in all procs of current file + int32_t procCount = (int32_t)arrlen(sProcBufs); + + for (int32_t p = -1; p < procCount; p++) { + showProc(p); + + if (sEditor) { + totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); + } + } + } else if (scope == ScopeObjE && sEditor && sObjDropdown) { + // Replace in all procs belonging to current object + int32_t objIdx = wgtDropdownGetSelected(sObjDropdown); + + if (objIdx >= 0 && objIdx < (int32_t)arrlen(sObjItems)) { + const char *objName = sObjItems[objIdx]; + int32_t procCount = (int32_t)arrlen(sProcTable); + + for (int32_t p = 0; p < procCount; p++) { + if (strcasecmp(sProcTable[p].objName, objName) == 0) { + showProc(p); + + if (sEditor) { + totalCount += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSens); + } + } + } + } + } + + char statusBuf[64]; + snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount); + setStatus(statusBuf); +} + + +static void onFindClose(WindowT *win) { + (void)win; + closeFindDialog(); +} + + +static void onFindCloseBtn(WidgetT *w) { + (void)w; + closeFindDialog(); +} + + +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; + } +} + + +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", 320, 210, false); + + if (!sFindWin) { + return; + } + + sFindWin->onClose = onFindClose; + sFindWin->onMenu = onMenu; + sFindWin->accelTable = sWin ? sWin->accelTable : NULL; + + WidgetT *root = wgtInitWindow(sAc, sFindWin); + root->spacing = wgtPixels(3); + + // Find row + WidgetT *findRow = wgtHBox(root); + findRow->spacing = wgtPixels(4); + wgtLabel(findRow, "Find:"); + sFindInput = wgtTextInput(findRow, 256); + sFindInput->weight = 100; + wgtSetText(sFindInput, sFindText); + + // Replace checkbox + input + WidgetT *replRow = wgtHBox(root); + replRow->spacing = wgtPixels(4); + sReplCheck = wgtCheckbox(replRow, "Replace:"); + wgtCheckboxSetChecked(sReplCheck, showReplace); + sReplCheck->onChange = onReplCheckChange; + sReplInput = wgtTextInput(replRow, 256); + sReplInput->weight = 100; + wgtSetText(sReplInput, sReplaceText); + + // Options row: scope + direction + case + WidgetT *optRow = wgtHBox(root); + optRow->spacing = wgtPixels(8); + + // Scope + WidgetT *scopeFrame = wgtFrame(optRow, "Scope"); + WidgetT *scopeBox = wgtVBox(scopeFrame); + sScopeGroup = wgtRadioGroup(scopeBox); + wgtRadio(sScopeGroup, "Function"); + wgtRadio(sScopeGroup, "Object"); + wgtRadio(sScopeGroup, "File"); + wgtRadio(sScopeGroup, "Project"); + wgtRadioGroupSetSelected(sScopeGroup, 3); // Project + + // Direction + WidgetT *dirFrame = wgtFrame(optRow, "Direction"); + WidgetT *dirBox = wgtVBox(dirFrame); + sDirGroup = wgtRadioGroup(dirBox); + wgtRadio(sDirGroup, "Forward"); + wgtRadio(sDirGroup, "Backward"); + wgtRadioGroupSetSelected(sDirGroup, 0); // Forward + + // Match Case + WidgetT *caseBox = wgtVBox(optRow); + sCaseCheck = wgtCheckbox(caseBox, "Match Case"); + + // Buttons + WidgetT *btnRow = wgtHBox(root); + btnRow->spacing = wgtPixels(4); + btnRow->align = AlignEndE; + + WidgetT *btnFind = wgtButton(btnRow, "Find Next"); + btnFind->onClick = onFindNext; + + sBtnReplace = wgtButton(btnRow, "Replace"); + sBtnReplace->onClick = onReplace; + + sBtnReplAll = wgtButton(btnRow, "Replace All"); + sBtnReplAll->onClick = onReplaceAll; + + WidgetT *btnClose = wgtButton(btnRow, "Close"); + btnClose->onClick = onFindCloseBtn; + + // Set initial replace enable state + onReplCheckChange(sReplCheck); + + dvxFitWindow(sAc, sFindWin); +} + + +// findInProject -- search all project files for a text match. +// Starts from the current editor position in the current file, +// then continues through subsequent files, wrapping around. +// Opens the file and selects the match when found. + +// 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) { + 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) { + return wgtTextAreaFindNext(sEditor, needle, caseSensitive, true); + } + + return false; +} + + +// 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; +} + + +static bool findInProject(const char *needle, bool caseSensitive) { + if (!needle || !needle[0] || sProject.fileCount == 0) { + return false; + } + + int32_t needleLen = (int32_t)strlen(needle); + + // Stash current editor state so all buffers are up-to-date + stashCurrentFile(); + + // Start from the active file, searching from after the current selection + int32_t startFile = sProject.activeFileIdx >= 0 ? sProject.activeFileIdx : 0; + int32_t startPos = 0; + + // If the editor is open on the current file, try the current proc + // first (no wrap — returns false if no more matches ahead). + if (sEditor && sEditorFileIdx == startFile) { + if (wgtTextAreaFindNext(sEditor, needle, caseSensitive, true)) { + return true; + } + + // No more matches in current proc — search remaining procs + int32_t procCount = (int32_t)arrlen(sProcBufs); + + for (int32_t p = sCurProcIdx + 1; p < procCount; p++) { + if (!sProcBufs[p]) { + continue; + } + + const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); + + if (found) { + showProcAndFind(p, needle, caseSensitive); + return true; + } + } + + // Search General section if we started in a proc + if (sCurProcIdx >= 0 && sGeneralBuf) { + const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen); + + if (found) { + showProcAndFind(-1, needle, caseSensitive); + return true; + } + } + + // Search procs before the current one (wrap within file) + for (int32_t p = 0; p < sCurProcIdx && p < procCount; p++) { + if (!sProcBufs[p]) { + continue; + } + + const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); + + if (found) { + showProcAndFind(p, needle, caseSensitive); + return true; + } + } + + // Move to next file + startFile = (startFile + 1) % sProject.fileCount; + } + + // Search remaining files, proc by proc. + // startFile was advanced past the current file if we already searched it. + int32_t filesToSearch = sProject.fileCount; + + if (sEditor && sEditorFileIdx >= 0) { + filesToSearch--; // skip the file we already searched above + } + + for (int32_t attempt = 0; attempt < filesToSearch; attempt++) { + int32_t fileIdx = (startFile + attempt) % sProject.fileCount; + + // Activate the file to load its proc buffers + activateFile(fileIdx, sProject.files[fileIdx].isForm ? ViewCodeE : ViewAutoE); + + // Search General section + if (sGeneralBuf) { + const char *found = caseSensitive ? strstr(sGeneralBuf, needle) : findSubstrNoCase(sGeneralBuf, needle, needleLen); + + if (found) { + showProcAndFind(-1, needle, caseSensitive); + return true; + } + } + + // Search each procedure + int32_t procCount = (int32_t)arrlen(sProcBufs); + + for (int32_t p = 0; p < procCount; p++) { + if (!sProcBufs[p]) { + continue; + } + + const char *found = caseSensitive ? strstr(sProcBufs[p], needle) : findSubstrNoCase(sProcBufs[p], needle, needleLen); + + if (found) { + showProcAndFind(p, needle, caseSensitive); + return true; + } + } + } + + return false; +} + + static void handleEditCmd(int32_t cmd) { switch (cmd) { case CMD_CUT: @@ -2372,6 +2875,22 @@ static void handleEditCmd(int32_t cmd) { } } 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; } } @@ -2464,7 +2983,9 @@ static void handleWindowCmd(int32_t cmd) { sToolboxWin = tbxCreate(sAc, &sDesigner); if (sToolboxWin) { - sToolboxWin->y = toolbarBottom(); + sToolboxWin->y = toolbarBottom(); + sToolboxWin->onMenu = onMenu; + sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; } } break; @@ -2474,7 +2995,9 @@ static void handleWindowCmd(int32_t cmd) { sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { - sPropsWin->y = toolbarBottom(); + sPropsWin->y = toolbarBottom(); + sPropsWin->onMenu = onMenu; + sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; } } break; @@ -3430,7 +3953,9 @@ static void switchToDesign(void) { sToolboxWin = tbxCreate(sAc, &sDesigner); if (sToolboxWin) { - sToolboxWin->y = toolbarBottom(); + sToolboxWin->y = toolbarBottom(); + sToolboxWin->onMenu = onMenu; + sToolboxWin->accelTable = sWin ? sWin->accelTable : NULL; } } @@ -3438,7 +3963,9 @@ static void switchToDesign(void) { sPropsWin = prpCreate(sAc, &sDesigner); if (sPropsWin) { - sPropsWin->y = toolbarBottom(); + sPropsWin->y = toolbarBottom(); + sPropsWin->onMenu = onMenu; + sPropsWin->accelTable = sWin ? sWin->accelTable : NULL; } } @@ -3502,11 +4029,15 @@ static void showCodeWindow(void) { WidgetT *dropdownRow = wgtHBox(codeRoot); dropdownRow->spacing = wgtPixels(4); + wgtLabel(dropdownRow, "Object:"); + sObjDropdown = wgtDropdown(dropdownRow); sObjDropdown->weight = 100; sObjDropdown->onChange = onObjDropdownChange; wgtDropdownSetItems(sObjDropdown, NULL, 0); + wgtLabel(dropdownRow, "Function:"); + sEvtDropdown = wgtDropdown(dropdownRow); sEvtDropdown->weight = 100; sEvtDropdown->onChange = onEvtDropdownChange; @@ -3523,6 +4054,8 @@ static void showCodeWindow(void) { // onChange is set after initial content is loaded by the caller // (navigateToEventSub, onPrjFileClick, etc.) to prevent false dirty marking. + + updateProjectMenuState(); } } @@ -3645,6 +4178,10 @@ static void updateProjectMenuState(void) { wmMenuItemSetEnabled(sWin->menuBar, CMD_PRJ_REMOVE, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE, hasProject); wmMenuItemSetEnabled(sWin->menuBar, CMD_SAVE_ALL, hasProject); + + wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_FIND_NEXT, hasProject); + wmMenuItemSetEnabled(sWin->menuBar, CMD_REPLACE, hasProject); } @@ -3681,11 +4218,19 @@ static void updateDirtyIndicators(void) { if (sCodeWin) { bool codeDirty = false; - if (sProject.activeFileIdx >= 0) { + const char *codeFile = ""; + + if (sEditorFileIdx >= 0 && sEditorFileIdx < sProject.fileCount) { + codeDirty = sProject.files[sEditorFileIdx].modified; + codeFile = sProject.files[sEditorFileIdx].path; + } else if (sProject.activeFileIdx >= 0) { codeDirty = sProject.files[sProject.activeFileIdx].modified; + codeFile = sProject.files[sProject.activeFileIdx].path; } - dvxSetTitle(sAc, sCodeWin, codeDirty ? "Code *" : "Code"); + char codeTitle[DVX_MAX_PATH + 16]; + snprintf(codeTitle, sizeof(codeTitle), "Code - %s%s", codeFile, codeDirty ? " *" : ""); + dvxSetTitle(sAc, sCodeWin, codeTitle); } // Design window title diff --git a/core/dvxApp.c b/core/dvxApp.c index 58279c5..f893890 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -2279,10 +2279,6 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) { ctx->sysMenu.popupX = win->x + CHROME_BORDER_WIDTH; ctx->sysMenu.popupY = win->y + CHROME_BORDER_WIDTH + CHROME_TITLE_HEIGHT; - if (win->menuBar) { - ctx->sysMenu.popupY += CHROME_MENU_HEIGHT; - } - int32_t maxW = 0; for (int32_t i = 0; i < ctx->sysMenu.itemCount; i++) { diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index d293930..6e96de4 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -1473,20 +1473,11 @@ static void int9Handler(void) { void platformKeyUpInit(void) { - if (sKeyUpInstalled) { - return; - } - - _go32_dpmi_get_protected_mode_interrupt_vector(9, &sOldInt9); - - sNewInt9.pm_offset = (unsigned long)int9Handler; - sNewInt9.pm_selector = _go32_my_cs(); - - // Chain: our handler runs first, then DJGPP automatically - // calls the original handler via an IRET wrapper. - _go32_dpmi_chain_protected_mode_interrupt_vector(9, &sNewInt9); - - sKeyUpInstalled = true; + // INT 9 hook disabled pending investigation of keyboard corruption. + // Key-up events are not available until this is fixed. + (void)sOldInt9; + (void)sNewInt9; + (void)int9Handler; } diff --git a/widgets/textInput/widgetTextInput.c b/widgets/textInput/widgetTextInput.c index 397c604..a7b9432 100644 --- a/widgets/textInput/widgetTextInput.c +++ b/widgets/textInput/widgetTextInput.c @@ -116,6 +116,7 @@ typedef struct { } TextAreaDataT; #include +#include #include #define TEXTAREA_BORDER 2 @@ -165,6 +166,8 @@ static void widgetTextAreaDragSelect(WidgetT *w, WidgetT *root, int32_t vx, int3 static void widgetTextAreaOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); static int32_t wordBoundaryLeft(const char *buf, int32_t pos); static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); +bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward); +int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); // sCursorBlinkOn is defined in widgetCore.c (shared state) @@ -2674,6 +2677,163 @@ void wgtTextAreaSetUseTabChar(WidgetT *w, bool useChar) { } +bool wgtTextAreaFindNext(WidgetT *w, const char *needle, bool caseSensitive, bool forward) { + if (!w || w->type != sTextAreaTypeId || !needle || !needle[0]) { + return false; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + int32_t needleLen = strlen(needle); + + if (needleLen > ta->len) { + return false; + } + + // Get current cursor byte position + int32_t cursorByte = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol); + + // Search forward from cursor+1 (or backward from cursor-1). + // No wrap-around: returns false if the end/start of the buffer + // is reached without finding a match. + int32_t startPos = forward ? cursorByte + 1 : cursorByte - 1; + int32_t searchLen = ta->len - needleLen + 1; + + if (searchLen <= 0) { + return false; + } + + int32_t count = forward ? (searchLen - startPos) : (startPos + 1); + + if (count <= 0) { + return false; + } + + for (int32_t attempt = 0; attempt < count; attempt++) { + int32_t pos; + + if (forward) { + pos = startPos + attempt; + } else { + pos = startPos - attempt; + } + + if (pos < 0 || pos >= searchLen) { + break; + } + + bool match; + if (caseSensitive) { + match = (memcmp(ta->buf + pos, needle, needleLen) == 0); + } else { + match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0); + } + + if (match) { + // Select the found text + ta->selAnchor = pos; + ta->selCursor = pos + needleLen; + + // Move cursor to start of match (forward) or end of match (backward) + int32_t cursorOff = forward ? pos : pos + needleLen; + textAreaOffToRowCol(ta->buf, cursorOff, &ta->cursorRow, &ta->cursorCol); + ta->desiredCol = ta->cursorCol; + + // Scroll to show the match + AppContextT *ctx = wgtGetContext(w); + if (ctx) { + const BitmapFontT *font = &ctx->font; + int32_t gutterW = textAreaGutterWidth(w, font); + int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W - gutterW; + int32_t visCols = innerW / font->charWidth; + int32_t innerH = w->h - TEXTAREA_BORDER * 2; + int32_t visRows = innerH / font->charHeight; + if (visCols < 1) { visCols = 1; } + if (visRows < 1) { visRows = 1; } + textAreaEnsureVisible(w, visRows, visCols); + } + + wgtInvalidatePaint(w); + return true; + } + } + + return false; +} + + +int32_t wgtTextAreaReplaceAll(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive) { + if (!w || w->type != sTextAreaTypeId || !needle || !needle[0] || !replacement) { + return 0; + } + + TextAreaDataT *ta = (TextAreaDataT *)w->data; + int32_t needleLen = strlen(needle); + int32_t replLen = strlen(replacement); + int32_t delta = replLen - needleLen; + int32_t count = 0; + + if (needleLen > ta->len) { + return 0; + } + + // Save undo before any modifications + textEditSaveUndo(ta->buf, ta->len, textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol), ta->undoBuf, &ta->undoLen, &ta->undoCursor, ta->bufSize); + + int32_t pos = 0; + while (pos + needleLen <= ta->len) { + bool match; + if (caseSensitive) { + match = (memcmp(ta->buf + pos, needle, needleLen) == 0); + } else { + match = (strncasecmp(ta->buf + pos, needle, needleLen) == 0); + } + + if (match) { + // Check if replacement fits in buffer + if (ta->len + delta >= ta->bufSize) { + break; + } + + // Shift text after match to make room (or close gap) + if (delta != 0) { + memmove(ta->buf + pos + replLen, ta->buf + pos + needleLen, ta->len - pos - needleLen); + } + + // Copy replacement in + memcpy(ta->buf + pos, replacement, replLen); + ta->len += delta; + ta->buf[ta->len] = '\0'; + count++; + pos += replLen; + } else { + pos++; + } + } + + if (count > 0) { + // Clear selection + ta->selAnchor = -1; + ta->selCursor = -1; + + // Clamp cursor if it's past the end + int32_t cursorOff = textAreaCursorToOff(ta->buf, ta->len, ta->cursorRow, ta->cursorCol); + if (cursorOff > ta->len) { + textAreaOffToRowCol(ta->buf, ta->len, &ta->cursorRow, &ta->cursorCol); + } + + ta->desiredCol = ta->cursorCol; + textAreaDirtyCache(w); + wgtInvalidatePaint(w); + + if (w->onChange) { + w->onChange(w); + } + } + + return count; +} + + // ============================================================ // DXE registration // ============================================================ @@ -2691,6 +2851,8 @@ static const struct { void (*setCaptureTabs)(WidgetT *w, bool capture); void (*setTabWidth)(WidgetT *w, int32_t width); void (*setUseTabChar)(WidgetT *w, bool useChar); + bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); + int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); } sApi = { .create = wgtTextInput, .password = wgtPasswordInput, @@ -2702,7 +2864,9 @@ static const struct { .setShowLineNumbers = wgtTextAreaSetShowLineNumbers, .setCaptureTabs = wgtTextAreaSetCaptureTabs, .setTabWidth = wgtTextAreaSetTabWidth, - .setUseTabChar = wgtTextAreaSetUseTabChar + .setUseTabChar = wgtTextAreaSetUseTabChar, + .findNext = wgtTextAreaFindNext, + .replaceAll = wgtTextAreaReplaceAll }; // Per-type APIs for the designer diff --git a/widgets/widgetTextInput.h b/widgets/widgetTextInput.h index 01a2ce5..1931965 100644 --- a/widgets/widgetTextInput.h +++ b/widgets/widgetTextInput.h @@ -25,6 +25,8 @@ typedef struct { void (*setCaptureTabs)(WidgetT *w, bool capture); void (*setTabWidth)(WidgetT *w, int32_t width); void (*setUseTabChar)(WidgetT *w, bool useChar); + bool (*findNext)(WidgetT *w, const char *needle, bool caseSensitive, bool forward); + int32_t (*replaceAll)(WidgetT *w, const char *needle, const char *replacement, bool caseSensitive); } TextInputApiT; static inline const TextInputApiT *dvxTextInputApi(void) { @@ -44,5 +46,7 @@ static inline const TextInputApiT *dvxTextInputApi(void) { #define wgtTextAreaSetCaptureTabs(w, capture) dvxTextInputApi()->setCaptureTabs(w, capture) #define wgtTextAreaSetTabWidth(w, width) dvxTextInputApi()->setTabWidth(w, width) #define wgtTextAreaSetUseTabChar(w, useChar) dvxTextInputApi()->setUseTabChar(w, useChar) +#define wgtTextAreaFindNext(w, needle, caseSens, fwd) dvxTextInputApi()->findNext(w, needle, caseSens, fwd) +#define wgtTextAreaReplaceAll(w, needle, repl, caseSens) dvxTextInputApi()->replaceAll(w, needle, repl, caseSens) #endif // WIDGET_TEXTINPUT_H