From 63bb0430c3ea2531bcf75c9c2c813658185dc9d9 Mon Sep 17 00:00:00 2001 From: "METANEOCORTEX\\Kotti" Date: Sat, 16 May 2026 15:19:18 +0200 Subject: [PATCH] fix: incr/decr number handler, support bin, keep hex cases --- src/Edit.c | 248 ++++++++++++++++++++++++++++++++++++++++----------- todo/TODO.md | 8 +- 2 files changed, 201 insertions(+), 55 deletions(-) diff --git a/src/Edit.c b/src/Edit.c index ad0762ac3..10db53dcf 100644 --- a/src/Edit.c +++ b/src/Edit.c @@ -20,10 +20,14 @@ #include #include #include +#include #include #include #include #include +#include +#include +#include #include "Styles.h" #include "Dialogs.h" @@ -2796,76 +2800,216 @@ void EditSelectToMatchingBrace() } +//============================================================================= +// +// EditModifyNumber() — shared helpers +// + +static unsigned int _BumpUInt(unsigned int n, bool bInc) +{ + if (bInc && n < UINT_MAX) { + return n + 1; + } + if (!bInc && n > 0) { + return n - 1; + } + return n; +} + +// Parse [s, end) as base-N unsigned. Rejects partial consumption and any +// value that doesn't fit in unsigned int. +static bool _ParseUInt32(const char *s, const char *end, int base, unsigned int *out) +{ + errno = 0; + char *e = NULL; + unsigned long long const ull = strtoull(s, &e, base); + if (e != end || errno == ERANGE || ull > UINT_MAX) { + return false; + } + *out = (unsigned int)ull; + return true; +} + + +// Per-radix helpers. Each receives a NUL-terminated, whitespace-trimmed +// body and a bInc flag (true = +1, false = -1). On a clean format- +// preserving transform it writes the new body into out and returns true; +// on rejection (wrong format, > UINT_MAX, etc.) it returns false and +// EditModifyNumber tries the next radix or the TinyExpr fallback. + +static bool _ModifyDec(const char *s, bool bInc, char *out, size_t outSz) +{ + const char *p = s; + while (*p >= '0' && *p <= '9') { + ++p; + } + if (p == s || *p != '\0' || (p - s) > 64) { + return false; + } + unsigned int n = 0; + if (!_ParseUInt32(s, p, 10, &n)) { + return false; + } + StringCchPrintfA(out, outSz, "%0*u", (int)(p - s), _BumpUInt(n, bInc)); + return true; +} + +static bool _ModifyHex(const char *s, bool bInc, char *out, size_t outSz) +{ + if (s[0] != '0' || (s[1] != 'x' && s[1] != 'X')) { + return false; + } + bool const bUppercasePrefix = (s[1] == 'X'); + const char *const pDig = s + 2; + const char *p = pDig; + while ((*p >= '0' && *p <= '9') || (*p >= 'a' && *p <= 'f') || (*p >= 'A' && *p <= 'F')) { + ++p; + } + if (p == pDig || *p != '\0' || (p - pDig) > 64) { + return false; + } + unsigned int n = 0; + if (!_ParseUInt32(pDig, p, 16, &n)) { + return false; + } + + // Digit case: any letter in the digit run wins; if none (e.g. "0x9", + // "0x42"), fall back to the prefix case. + bool bUppercaseDigits = bUppercasePrefix; + for (const char *q = p - 1; q >= pDig; --q) { + if (*q >= 'a' && *q <= 'f') { bUppercaseDigits = false; break; } + if (*q >= 'A' && *q <= 'F') { bUppercaseDigits = true; break; } + } + + char digits[80]; + StringCchPrintfA(digits, COUNTOF(digits), bUppercaseDigits ? "%0*X" : "%0*x", (int)(p - pDig), _BumpUInt(n, bInc)); + StringCchPrintfA(out, outSz, bUppercasePrefix ? "0X%s" : "0x%s", digits); + return true; +} + +static bool _ModifyBin(const char *s, bool bInc, char *out, size_t outSz) +{ + if (s[0] != '0' || (s[1] != 'b' && s[1] != 'B')) { + return false; + } + bool const bUppercasePrefix = (s[1] == 'B'); + const char *const pBits = s + 2; + const char *p = pBits; + while (*p == '0' || *p == '1') { + ++p; + } + if (p == pBits || *p != '\0' || (p - pBits) > 32) { + return false; + } + + unsigned int n = 0; + for (const char *q = pBits; q < p; ++q) { + n = (n << 1) | (unsigned int)(*q - '0'); + } + n = _BumpUInt(n, bInc); + + int const iOrigBits = (int)(p - pBits); + int iMinBits = 1; + for (unsigned int tmp = n; tmp > 1u; tmp >>= 1) { + ++iMinBits; + } + int const iOutBits = (iMinBits > iOrigBits) ? iMinBits : iOrigBits; + if ((size_t)(iOutBits + 3) > outSz) { + return false; + } + out[0] = '0'; + out[1] = bUppercasePrefix ? 'B' : 'b'; + for (int i = 0; i < iOutBits; ++i) { + out[2 + i] = ((n >> (iOutBits - 1 - i)) & 1u) ? '1' : '0'; + } + out[2 + iOutBits] = '\0'; + return true; +} + + //============================================================================= // // EditModifyNumber() // -void EditModifyNumber(HWND hwnd,bool bIncrease) +// Ctrl+Alt+[+/-] on a selection. Tries each format-preserving path +// (decimal / hex / binary), falls back to TinyExpr-wrapped arithmetic +// for expressions, floats and negatives. Outer whitespace from the +// original selection is re-emitted around the new value. Re-selects +// the result so the user can press the hotkey again to keep changing. +// +void EditModifyNumber(HWND hwnd, bool bIncrease) { + UNREFERENCED_PARAMETER(hwnd); if (Sci_IsMultiOrRectangleSelection()) { InfoBoxLng(MB_ICONWARNING, NULL, IDS_MUI_SELRECTORMULTI); return; } + if (SciCall_IsSelectionEmpty()) { + return; + } + if ((DocPos)SciCall_GetSelText(NULL) > 4096) { + return; + } - const DocPos iSelStart = SciCall_GetSelectionStart(); - const DocPos iSelEnd = SciCall_GetSelectionEnd(); + char szSel[4096] = { '\0' }; + SciCall_GetSelText(szSel); + size_t const iSelLen = strlen(szSel); - if ((iSelEnd - iSelStart) > 0) { - char chNumber[32] = { '\0' }; - if (SciCall_GetSelText(NULL) < COUNTOF(chNumber)) { + // Trim outer whitespace (preserved around the new value). + size_t iLeadWS = 0; + while (szSel[iLeadWS] == ' ' || szSel[iLeadWS] == '\t') { + ++iLeadWS; + } + size_t iBodyEnd = iSelLen; + while (iBodyEnd > iLeadWS && (szSel[iBodyEnd - 1] == ' ' || szSel[iBodyEnd - 1] == '\t')) { + --iBodyEnd; + } + if (iBodyEnd == iLeadWS) { + return; // whitespace-only selection + } + size_t const iTrailWS = iSelLen - iBodyEnd; - SciCall_GetSelText(chNumber); + // NUL-poke szSel so helpers see a zero-terminated trimmed view. + char const chSaved = szSel[iBodyEnd]; + szSel[iBodyEnd] = '\0'; + const char *const pTrimmed = szSel + iLeadWS; - if (StrChrIA(chNumber, '-')) { - return; - } + char chBody[256] = { '\0' }; + bool bHandled = _ModifyDec(pTrimmed, bIncrease, chBody, COUNTOF(chBody)) + || _ModifyHex(pTrimmed, bIncrease, chBody, COUNTOF(chBody)) + || _ModifyBin(pTrimmed, bIncrease, chBody, COUNTOF(chBody)); - unsigned int iNumber; - int iWidth; - char chFormat[32] = { '\0' }; - if (!StrChrIA(chNumber, 'x') && sscanf_s(chNumber, "%ui", &iNumber) == 1) { - iWidth = (int)StringCchLenA(chNumber, COUNTOF(chNumber)); - if (bIncrease && (iNumber < UINT_MAX)) { - iNumber++; - } - if (!bIncrease && (iNumber > 0)) { - iNumber--; - } + if (!bHandled) { + char szExpr[4128]; + StringCchPrintfA(szExpr, COUNTOF(szExpr), "(%s)%s", pTrimmed, bIncrease ? "+1" : "-1"); - StringCchPrintfA(chFormat, COUNTOF(chFormat), "%%0%ii", iWidth); - StringCchPrintfA(chNumber, COUNTOF(chNumber), chFormat, iNumber); - EditReplaceSelection(chNumber, false); - } else if (sscanf_s(chNumber, "%x", &iNumber) == 1) { - iWidth = (int)StringCchLenA(chNumber, COUNTOF(chNumber)) - 2; - if (bIncrease && iNumber < UINT_MAX) { - iNumber++; - } - if (!bIncrease && iNumber > 0) { - iNumber--; - } - bool bUppercase = false; - for (int i = (int)StringCchLenA(chNumber, COUNTOF(chNumber)) - 1; i >= 0; i--) { - if (IsCharLowerA(chNumber[i])) { - break; - } - if (IsCharUpperA(chNumber[i])) { - bUppercase = true; - break; - } - } - if (bUppercase) { - StringCchPrintfA(chFormat, COUNTOF(chFormat), "%%#0%iX", iWidth); - } else { - StringCchPrintfA(chFormat, COUNTOF(chFormat), "%%#0%ix", iWidth); - } - - StringCchPrintfA(chNumber, COUNTOF(chNumber), chFormat, iNumber); - EditReplaceSelection(chNumber, false); - } + te_int_t iErr = 0; + double const dResult = te_interp(szExpr, &iErr); + if (iErr != 0 || !isfinite(dResult)) { + return; // silent no-op + } + if (fabs(dResult) <= 9.2233720368547758e18 && dResult == (double)(long long)dResult) { + StringCchPrintfA(chBody, COUNTOF(chBody), "%lld", (long long)dResult); + } else { + StringCchPrintfA(chBody, COUNTOF(chBody), "%.15g", dResult); } } - UNREFERENCED_PARAMETER(hwnd); + szSel[iBodyEnd] = chSaved; // restore trailing-ws char for the compose step + + // Compose: . + size_t const iBodyOutLen = strlen(chBody); + char szFinal[4256]; + if (iLeadWS + iBodyOutLen + iTrailWS >= COUNTOF(szFinal)) { + EditReplaceSelection(chBody, false); // pathological size: drop ws + return; + } + memcpy(szFinal, szSel, iLeadWS); + memcpy(szFinal + iLeadWS, chBody, iBodyOutLen); + memcpy(szFinal + iLeadWS + iBodyOutLen, szSel + iBodyEnd, iTrailWS); + szFinal[iLeadWS + iBodyOutLen + iTrailWS] = '\0'; + + EditReplaceSelection(szFinal, false); } diff --git a/todo/TODO.md b/todo/TODO.md index 44b60be32..34d21bbee 100644 --- a/todo/TODO.md +++ b/todo/TODO.md @@ -313,7 +313,7 @@ - Issue: [#4946](https://github.com/rizonesoft/Notepad3/issues/4946) ### Copy/Clipboard -- [x] **(Q2) Copy as RTF** - Rich text copy with syntax highlighting +- [x] **(Q2) Copy as RTF** - Rich text copy with syntax highlighting - ✅ IMPLEMENTED - Issue: [#5052](https://github.com/rizonesoft/Notepad3/issues/5052) - **Reference**: [Notepad4](https://github.com/zufuliu/notepad4) implements via `IDM_EDIT_COPYRTF` - [ ] **(Q2) Copy/Cut/Paste Binary** - Binary data handling @@ -325,8 +325,10 @@ - `IDM_EDIT_INSERT_PATHNAME` - Copy full path ### Additional Micro Features -- [ ] **(Q1) Increment/Decrement Number** - Modify number at cursor (Ctrl+Alt++/-) -- [ ] **(Q2) Show Hex View** - Display hex representation of selection +- [x] **(Q1) Increment/Decrement Number** - Modify number at cursor (Ctrl+Alt++/-) + - implemented via TinyExpr enhancement +- [x] **(Q2) Show Hex View** - Display hex representation of selection + - implemented via TinyExpr output format switch - [ ] **(Q1) CSV Options Dialog** - Configure CSV delimiter/qualifier - [x] CSV Rainbow Lexer (home-brew) has an auto-detect-delimiter - [ ] **(Q3) Large File Mode** - Optimized mode for files >2GB