From b1465e7116898a403c2c36c8fccd00ec3717e5c0 Mon Sep 17 00:00:00 2001 From: Rainer Kottenhoff Date: Thu, 7 May 2026 10:26:47 +0200 Subject: [PATCH 1/3] chore: polish "Page Setup..." dialog --- src/Print.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/Print.cpp b/src/Print.cpp index 07c3c0052..e98240f37 100644 --- a/src/Print.cpp +++ b/src/Print.cpp @@ -487,6 +487,9 @@ static UINT_PTR CALLBACK _LPSetupHookProc(HWND hwnd, UINT uiMsg, WPARAM wParam, SetExplorerTheme(GetDlgItem(hwnd, IDCANCEL)); SetExplorerTheme(GetDlgItem(hwnd, IDC_PRINTER)); int const ctl[] = { 30, 31, 32, 33, 34, cmb2, cmb3, 1037, 1038, 1056, 1057, 1072, 1073, 1074, 1075, 1076, 1089, 1090, + 1102, 1103, 1104, 1105, // margin labels + 1155, 1156, 1157, 1158, // margin edits + 1080, 1081, 1082, // preview rects IDC_STATIC, IDC_STATIC2, IDC_STATIC3, IDC_STATIC4, IDC_STATIC5, IDC_STATIC6 }; for (int i : ctl) { @@ -496,7 +499,7 @@ static UINT_PTR CALLBACK _LPSetupHookProc(HWND hwnd, UINT uiMsg, WPARAM wParam, #endif UDACCEL const acc[1] = { { 0, 10 } }; - SendDlgItemMessage(hwnd, 30, EM_LIMITTEXT, 32, 0); + SendDlgItemMessage(hwnd, 30, EM_LIMITTEXT, 4, 0); SendDlgItemMessage(hwnd, 31, UDM_SETACCEL, 1, (WPARAM)acc); SendDlgItemMessage(hwnd, 31, UDM_SETRANGE32, NP3_MIN_ZOOM_PERCENT, NP3_MAX_ZOOM_PERCENT); SendDlgItemMessage(hwnd, 31, UDM_SETPOS32, 0, Settings.PrintZoom); @@ -579,11 +582,15 @@ static UINT_PTR CALLBACK _LPSetupHookProc(HWND hwnd, UINT uiMsg, WPARAM wParam, AllowDarkModeForWindowEx(hwnd, darkModeEnabled); RefreshTitleBarThemeColor(hwnd); - int const buttons[] = { IDOK, IDCANCEL, IDC_PRINTER }; - for (int button : buttons) { - HWND const hBtn = GetDlgItem(hwnd, button); - AllowDarkModeForWindowEx(hBtn, darkModeEnabled); - SendMessage(hBtn, WM_THEMECHANGED, 0, 0); + int const ctl[] = { IDOK, IDCANCEL, IDC_PRINTER, + 30, 31, 32, 33, 34, // custom: zoom, spin, header, footer, color + 1137, 1138, // standard: paper size, source combos + 1155, 1156, 1157, 1158 // standard: margin edits + }; + for (int id : ctl) { + HWND const hCtl = GetDlgItem(hwnd, id); + AllowDarkModeForWindowEx(hCtl, darkModeEnabled); + SendMessage(hCtl, WM_THEMECHANGED, 0, 0); } UpdateWindowEx(hwnd); } @@ -595,7 +602,6 @@ static UINT_PTR CALLBACK _LPSetupHookProc(HWND hwnd, UINT uiMsg, WPARAM wParam, BOOL bError = FALSE; auto const iZoom = static_cast(SendDlgItemMessage(hwnd, 31, UDM_GETPOS32, 0, (LPARAM)&bError)); Settings.PrintZoom = bError ? Defaults.PrintZoom : iZoom; - /*int const iFontSize = (int)*/ SendDlgItemMessage(hwnd, 41, UDM_GETPOS32, 0, (LPARAM)&bError); Settings.PrintHeader = static_cast(SendDlgItemMessage(hwnd, 32, CB_GETCURSEL, 0, 0)); Settings.PrintFooter = static_cast(SendDlgItemMessage(hwnd, 33, CB_GETCURSEL, 0, 0)); Settings.PrintColorMode = static_cast(SendDlgItemMessage(hwnd, 34, CB_GETCURSEL, 0, 0)); From 9bcee30cf6a799084eb4586c18d701cca35ae203 Mon Sep 17 00:00:00 2001 From: Rainer Kottenhoff Date: Thu, 7 May 2026 18:02:12 +0200 Subject: [PATCH 2/3] chore: extend navigation documentation, minor fixes for change-history navigation, housekeeping TODO.md --- readme/KeyboardShortcuts.md | 95 +++++++++++++++++++++++++++++++++++++ src/Edit.c | 70 ++++++++++++++++++++------- src/Edit.h | 2 + src/Notepad3.c | 3 ++ todo/TODO.md | 34 ++++++------- 5 files changed, 172 insertions(+), 32 deletions(-) diff --git a/readme/KeyboardShortcuts.md b/readme/KeyboardShortcuts.md index 624804b61..8e088bd4b 100644 --- a/readme/KeyboardShortcuts.md +++ b/readme/KeyboardShortcuts.md @@ -423,6 +423,101 @@ A compact summary of the function-key family (most frequent collisions): --- +## Cheatsheet — caret & view navigation + +A single-stop reference for moving the caret, scrolling the view, and jumping between document landmarks. Repeats keys already listed in the topical sections above so the cheatsheet stands alone. Notepad3 inherits Scintilla's default keymap unchanged (no `SCI_ASSIGNCMDKEY` overrides), so the standard caret bindings below all work even though they are not part of Notepad3's accelerator table. + +### Caret movement (basics) + +| Shortcut | Action | +|---|---| +| `←` / `→` | Char left / right | +| `↑` / `↓` | Line up / down | +| `Home` / `End` | Line start / end | +| `Ctrl+Home` / `Ctrl+End` | Document start / end | +| `PageUp` / `PageDown` | Page up / down | +| `Shift+`*movement* | Extend selection in that direction | +| `Ctrl+Shift+`*movement* | Extend selection by word / document chunk | + +### Word navigation + +| Shortcut | Action | +|---|---| +| `Ctrl+←` / `Ctrl+→` | Caret one word left / right | +| `Ctrl+Backspace` | Delete word before caret | +| `Ctrl+Del` | Delete word after caret | +| `Ctrl+Alt+A` | Toggle **Accelerated Word Navigation** (see below) | +| `Ctrl+Space` | Select word at caret (full line if word already selected) | +| `Ctrl+Shift+Space` | Multi-select all matches of word at caret | + +**Accelerated Word Navigation** (`Ctrl+Alt+A`) merges punctuation into the word-character set, so word-jump keys (`Ctrl+←`, `Ctrl+→`, `Ctrl+Backspace`, `Ctrl+Del`) skip whole code tokens at once. + +Example — caret stops in `foo->bar.baz` walking with `Ctrl+→`: + +- off: `foo`  |  `->`  |  `bar`  |  `.`  |  `baz` (5 stops) +- on: `foo->bar.baz` (1 stop) + +Fine-tune which characters still break words via `[Settings2] ExtendedWhiteSpaceChars=` — see [`config/Configuration.md`](config/Configuration.md). The toggle is persisted in `[Settings] AccelWordNavigation=`. + +### View / scroll + +| Shortcut | Action | +|---|---| +| `Ctrl+↑` / `Ctrl+↓` | Scroll one line up / down (caret stays put) | +| `Ctrl+PgUp` / `Ctrl+PgDn` | Goto previous / next block (paragraph or fold) | +| `Ctrl+Shift+PgUp` / `Ctrl+Shift+PgDn` | Select to previous / next block | +| `Alt+PageUp` / `Alt+PageDown` | Paragraph navigation up / down | +| `Ctrl+Alt+V` | Focused View — show only lines matching word/selection | +| `Ctrl+G` | Goto line / column dialog | + +### Bookmarks & change history + +| Shortcut | Action | +|---|---| +| `F2` | Goto next bookmark — falls through to next change-history marker if no further bookmark is found (sticky) | +| `Shift+F2` | Goto previous bookmark — same fallback | +| `Ctrl+F2` | Toggle bookmark on current line | +| `Alt+F2` | Clear all bookmarks | + +If the document has no bookmarks (or none ahead of the caret in the search direction), `F2` / `Shift+F2` walk **change-history markers** instead — modified, saved, and reverted lines shown in the change-history margin (see *View → Change History*). Once a press has landed the caret on a change-history marker, repeated presses keep walking change-history markers until you reach a bookmarked line. + +### Find navigation + +| Shortcut | Action | +|---|---| +| `F3` | Find next | +| `Shift+F3` | Find previous | +| `Ctrl+F3` | Find next occurrence of current selection | +| `Ctrl+Shift+F3` | Find previous occurrence of current selection | +| `F4` | Replace next | +| `Ctrl+Alt+F2` | Expand selection to next match | +| `Ctrl+Alt+Shift+F2` | Expand selection to previous match | +| `Alt+A` | Toggle "mark all occurrences" of current word | + +### Brace navigation + +| Shortcut | Action | +|---|---| +| `Ctrl+B` | Find matching brace (or jump to nearest enclosing opening brace) | +| `Ctrl+Shift+B` | Select to matching brace; repeated presses expand the selection outward one nesting level at a time | + +### Folding + +| Shortcut | Action | +|---|---| +| `Alt+←` / `Alt+→` | Collapse / expand current fold | +| `Alt+-` / `Alt++` | Jump to previous / next fold point | +| `Ctrl+Alt+F` | Toggle all folds | + +### Selection-boundary jumps + +| Shortcut | Action | +|---|---| +| `Ctrl+,` | Jump caret to selection start | +| `Ctrl+.` | Jump caret to selection end | + +--- + ## See also - [`faq/FAQ.md`](faq/FAQ.md) — *"Migrating from Notepad2"* lists every shortcut whose meaning was reassigned, plus the **why**. diff --git a/src/Edit.c b/src/Edit.c index c44bb0f36..ad0762ac3 100644 --- a/src/Edit.c +++ b/src/Edit.c @@ -10072,27 +10072,61 @@ void EditSetBookmarkList(HWND hwnd, LPCWSTR pszBookMarks) #define NOT_FOUND_LN ((DocLn)-1) -static int _RespectLastSearch(const int bitmask, const DocLn iLine) +// Sticky-state for F2/Shift+F2 dual bookmark/change-history navigation. +// Module-scope so EditBookmarkResetNavigation can clear them on document load +// and EditBookmarkAdjustNavigation can shift the line index across edits. +static int s_LastSearchBitmask = BOOKMARK_BITMASK(); +static DocLn s_LastSearchStart = NOT_FOUND_LN; + + +void EditBookmarkResetNavigation(void) { - static int _LastSearchBitmask = BOOKMARK_BITMASK(); - static DocLn _LastSearchStart = NOT_FOUND_LN; + s_LastSearchBitmask = BOOKMARK_BITMASK(); + s_LastSearchStart = NOT_FOUND_LN; +} + + +void EditBookmarkAdjustNavigation(const DocLn modLine, const DocLn linesAdded) +{ + if (s_LastSearchStart == NOT_FOUND_LN) { + return; + } + if (modLine <= s_LastSearchStart) { + DocLn const shifted = s_LastSearchStart + linesAdded; + if (shifted < 0) { + // Sticky line was removed by the deletion: drop sticky state. + s_LastSearchStart = NOT_FOUND_LN; + } + else { + s_LastSearchStart = shifted; + } + } +} - if (iLine == _LastSearchStart) { - if (bitmask & _LastSearchBitmask) { - return _LastSearchBitmask; // respect last found bitmask + +static int _RespectLastSearch(const int bitmask, const DocLn iLine) +{ + if (iLine == s_LastSearchStart) { + if (bitmask & s_LastSearchBitmask) { + return s_LastSearchBitmask; // respect last found bitmask } } if (bitmask & BOOKMARK_BITMASK()) { - _LastSearchBitmask = BOOKMARK_BITMASK(); // BOOKMARKS got prio + s_LastSearchBitmask = BOOKMARK_BITMASK(); // BOOKMARKS got prio } else { - _LastSearchBitmask = bitmask ? bitmask : BOOKMARK_BITMASK(); + s_LastSearchBitmask = bitmask ? bitmask : BOOKMARK_BITMASK(); } - _LastSearchStart = iLine; - return _LastSearchBitmask; + s_LastSearchStart = iLine; + return s_LastSearchBitmask; } +// Linear-scan fallback for change-history markers. +// Scintilla's SCI_MARKERNEXT walks LineMarkers only (scintilla/src/PerLine.cxx). +// Change-history markers (IDs 21-24) live in the separate change-history machinery +// and are reported via SCI_MARKERGET per-line but are NOT in LineMarkers, so we +// must linear-scan when the bitmask requests them. static inline DocLn _MarkerNext(const DocLn iLine, const int bitmask) { if (bitmask & CHANGE_HISTORY_MARKER_BITMASK()) { @@ -10112,6 +10146,8 @@ void EditBookmarkNext(HWND hwnd, DocLn iLine) { UNREFERENCED_PARAMETER(hwnd); + DocLn const iStartLn = iLine; // immune to consecutive-marker-skip mutation below + int bitmask = _RespectLastSearch(SciCall_MarkerGet(iLine), iLine); DocLn iNextLine = _MarkerNext(iLine + 1, bitmask); @@ -10122,14 +10158,14 @@ void EditBookmarkNext(HWND hwnd, DocLn iLine) } } - if ((iNextLine == NOT_FOUND_LN) && (iLine > 0)) { + if ((iNextLine == NOT_FOUND_LN) && (iStartLn > 0)) { iNextLine = _MarkerNext(0, bitmask); // wrap around } if (iNextLine == NOT_FOUND_LN) { // find any bookmark bitmask = ALL_MARKERS_BITMASK(); iNextLine = _MarkerNext(iLine + 1, bitmask); - if ((iNextLine == NOT_FOUND_LN) && (iLine > 0)) { + if ((iNextLine == NOT_FOUND_LN) && (iStartLn > 0)) { iNextLine = _MarkerNext(0, bitmask); // wrap around } } @@ -10137,7 +10173,7 @@ void EditBookmarkNext(HWND hwnd, DocLn iLine) if (iNextLine == NOT_FOUND_LN) { // find change history marker bitmask = CHANGE_HISTORY_MARKER_BITMASK(); iNextLine = _MarkerNext(iLine + 1, bitmask); - if ((iNextLine == NOT_FOUND_LN) && (iLine > 0)) { + if ((iNextLine == NOT_FOUND_LN) && (iStartLn > 0)) { iNextLine = _MarkerNext(0, bitmask); // wrap around } } @@ -10152,6 +10188,7 @@ void EditBookmarkNext(HWND hwnd, DocLn iLine) // // EditBookmarkPrevious() // +// Linear-scan fallback for change-history markers; see _MarkerNext above. static inline DocLn _MarkerPrevious(const DocLn iLine, const int bitmask) { if (bitmask & CHANGE_HISTORY_MARKER_BITMASK()) { @@ -10170,6 +10207,7 @@ void EditBookmarkPrevious(HWND hwnd, DocLn iLine) UNREFERENCED_PARAMETER(hwnd); DocLn const docLnCount = SciCall_GetLineCount(); + DocLn const iStartLn = iLine; // immune to consecutive-marker-skip mutation below int bitmask = _RespectLastSearch(SciCall_MarkerGet(iLine), iLine); iLine = (iLine <= 0) ? docLnCount + 1 : iLine; @@ -10182,14 +10220,14 @@ void EditBookmarkPrevious(HWND hwnd, DocLn iLine) } } - if ((iPrevLine == NOT_FOUND_LN) && (iLine < docLnCount)) { + if ((iPrevLine == NOT_FOUND_LN) && (iStartLn < docLnCount)) { iPrevLine = _MarkerPrevious(docLnCount, bitmask); // wrap around } if (iPrevLine == NOT_FOUND_LN) { // find any bookmark bitmask = ALL_MARKERS_BITMASK(); iPrevLine = _MarkerPrevious(iLine - 1, bitmask); - if ((iPrevLine == NOT_FOUND_LN) && (iLine < docLnCount)) { + if ((iPrevLine == NOT_FOUND_LN) && (iStartLn < docLnCount)) { iPrevLine = _MarkerPrevious(docLnCount, bitmask); // wrap around } } @@ -10197,7 +10235,7 @@ void EditBookmarkPrevious(HWND hwnd, DocLn iLine) if (iPrevLine == NOT_FOUND_LN) { // find change history marker bitmask = CHANGE_HISTORY_MARKER_BITMASK(); iPrevLine = _MarkerPrevious(iLine - 1, bitmask); - if ((iPrevLine == NOT_FOUND_LN) && (iLine < docLnCount)) { + if ((iPrevLine == NOT_FOUND_LN) && (iStartLn < docLnCount)) { iPrevLine = _MarkerPrevious(docLnCount, bitmask); // wrap around } } diff --git a/src/Edit.h b/src/Edit.h index 0f040b37e..ea06130fa 100644 --- a/src/Edit.h +++ b/src/Edit.h @@ -132,6 +132,8 @@ void EditSetBookmarkList(HWND hwnd,LPCWSTR pszBookMarks); void EditBookmarkNext(HWND hwnd, const DocLn iLine); void EditBookmarkPrevious(HWND hwnd, const DocLn iLine); void EditBookmarkToggle(HWND hwnd, const DocLn ln, const int modifiers); +void EditBookmarkResetNavigation(void); +void EditBookmarkAdjustNavigation(const DocLn modLine, const DocLn linesAdded); void EditMarkAllOccurrences(HWND hwnd, bool bForceClear); void EditFoldMarkedLineRange(HWND hwnd, bool bHideLines); void EditBookMarkLineRange(HWND hwnd); diff --git a/src/Notepad3.c b/src/Notepad3.c index 1e8cb860a..78e6da944 100644 --- a/src/Notepad3.c +++ b/src/Notepad3.c @@ -8943,6 +8943,7 @@ static LRESULT _MsgNotifyFromEdit(HWND hwnd, const SCNotification* const scn) } EditUpdateVisibleIndicators(); if (scn->linesAdded != 0) { + EditBookmarkAdjustNavigation(SciCall_LineFromPosition(scn->position), scn->linesAdded); if (Settings.SplitUndoTypingSeqOnLnBreak && (scn->linesAdded > 0)) { if (!(iModType & (SC_PERFORMED_UNDO | SC_PERFORMED_REDO))) { _SplitUndoTransaction(); @@ -11124,6 +11125,8 @@ bool FileLoad(const HPATHL hfile_pth, const FileLoadFlags fLoadFlags, const DocP } } + EditBookmarkResetNavigation(); + if (!bReloadFile) { ResetEncryption(); } diff --git a/todo/TODO.md b/todo/TODO.md index 6f34787c0..b21c2ce6e 100644 --- a/todo/TODO.md +++ b/todo/TODO.md @@ -27,19 +27,19 @@ - Issue: [#5060](https://github.com/rizonesoft/Notepad3/issues/5060) - Fix: When `/m` is used without 'R' flag, explicitly clear SCFIND_REGEXP to force text mode - [x] **(Q3) Replace GetOpenFileNameW with IFileOpenDialog** - Modern file dialog API - - [ ] **Test on Windows Server 2022 and higer** - ⚠ Validation❗ - Issues: [#5066](https://github.com/rizonesoft/Notepad3/issues/5066), [#5080](https://github.com/rizonesoft/Notepad3/issues/5080) - Fixes crash on Windows Server 2022 (STATUS_STACK_BUFFER_OVERRUN in ntdll.dll) - See [research/server2022-file-dialog-crash.md](research/server2022-file-dialog-crash.md) + - [ ] **Test on Windows Server 2022 and higer** - ⚠ Validation❗ - [x] **(Q1) BUG: Cannot save settings without folder** - ✅ FIXED - Issue: [#5075](https://github.com/rizonesoft/Notepad3/issues/5075) - Fix: Changed `CreateDirectoryW` to `SHCreateDirectoryExW` to create all intermediate directories - [ ] **(Q2) BUG: Replace dialog full-width caching** - Second replace uses wrong character - Issue: [#4268](https://github.com/rizonesoft/Notepad3/issues/4268) - CJK full-width replacement cached incorrectly -- [x] **(Q2) BUG: Initial window position not working** - Position settings ignored - - [x] **To be analyzed - works as designed ???** - ⚠ Validation ❗ +- [x] **(Q2) BUG: Initial window position not working** - Position settings ignored - ✅ FIXED - Issue: [#4725](https://github.com/rizonesoft/Notepad3/issues/4725) + - [ ] **To be analyzed - works as designed ???** - ⚠ Validation ❗ - [ ] **(Q2) BUG: Minipath options don't save** - FullRowSelect/TrackSelect broken - Issue: [#4116](https://github.com/rizonesoft/Notepad3/issues/4116) - [x] **(Q1) BUG: Monitoring log not saved** - ✅ FIXED @@ -49,23 +49,21 @@ - Issue: [#5050](https://github.com/rizonesoft/Notepad3/issues/5050) - [x] **(Q1) BUG: Find/Replace patterns not updating** - Dropdown not refreshed immediately - ✅ FIXED - Issue: [#5134](https://github.com/rizonesoft/Notepad3/issues/5134) -- [ ] **(Q3) BUG: Multiple file positions not saved** - Only last file's bookmarks/caret preserved +- [x] **(Q3) BUG: Multiple file positions not saved** - Only last file's bookmarks/caret preserved - ✅ FIXED - Issue: [#5151](https://github.com/rizonesoft/Notepad3/issues/5151) - [ ] **(Q3) BUG: grepWinNP3 crash** - Right-click search results crashes - Issue: [#5158](https://github.com/rizonesoft/Notepad3/issues/5158) - [x] **(Q2) BUG: PHP comment toggle** - Ctrl+Q not working in Web Source Code - ✅ FIXED - Issue: [#5163](https://github.com/rizonesoft/Notepad3/issues/5163) -- [ ] **(Q2) BUG: AltGr shortcut conflict** - Can't type `}` `@` on non-US keyboards +- [/] **(Q2) CHR: Ctrl+LAlt+V is used by NP3 - AltGr+V works** - Can't type `}` `@` on non-US keyboards - ❌ WON'T CHANGE - Issue: [#5220](https://github.com/rizonesoft/Notepad3/issues/5220) - [x] **(Q1) BUG: Mouse scroll settings not updated** - ✅ FIXED - Issue: [#5223](https://github.com/rizonesoft/Notepad3/issues/5223) - Fix: Forward `WM_SETTINGCHANGE` to Scintilla to refresh cached scroll parameters - [x] **(Q2) BUG: File lock held too long on save** - Blocks FileSystemWatcher - ✅ FIXED - - [ ] **Needs validation** - - Issue: [#5301](https://github.com/rizonesoft/Notepad3/issues/5301) + - Issue: [#5301](https://github.com/rizonesoft/Notepad3/issues/5301) - ⚠ Validation❗ - [x] **(Q2) BUG: Folder handle leak** - Can't rename/delete folders with opened files - - [ ] **Needs testing** - ⚠ Validation❗ - - Issue: [#5342](https://github.com/rizonesoft/Notepad3/issues/5342) + - Issue: [#5342](https://github.com/rizonesoft/Notepad3/issues/5342) - ⚠ Validation❗ - [x] **(Q1) BUG: Black line in Language menu** - ✅ FIXED - Issue: [#5361](https://github.com/rizonesoft/Notepad3/issues/5361) - Fix: Removed `WM_UAHNCPAINTMENUPOPUP` from message interception - was using wrong window handle @@ -132,8 +130,7 @@ - [ ] (Q1) Documentation updates - [x] **(Q2) Long Path Support** - Support paths >260 characters (Win10+) - ✅ FIXED - - Issue: [#3580](https://github.com/rizonesoft/Notepad3/issues/3580) - - [ ] **Should be fixed** - ⚠ Validation ❗ + - Issue: [#3580](https://github.com/rizonesoft/Notepad3/issues/3580) - ⚠ Validation ❗ - [ ] **(Q3) Additional Syntax Highlighting** - New language lexers - Haskell: [#3035](https://github.com/rizonesoft/Notepad3/issues/3035) - Lexilla `LexHaskell.cxx` @@ -154,7 +151,7 @@ - [ ] **(Q2) Custom Hyperlink Schemes** - User-defined URL protocol recognition - ed2k:// links: [#5405](https://github.com/rizonesoft/Notepad3/issues/5405) - Customizable via INI settings -- [ ] **(Q2) Smarter URL Recognition** - Improve URL boundary detection +- [x] **(Q2) Smarter URL Recognition** - Improve URL boundary detection - ✅ FIXED - Issue: [#5464](https://github.com/rizonesoft/Notepad3/issues/5464) - Don't include trailing `'` when URL is quoted - [ ] **(Q3) Display Hidden Characters** - Show invisible/control characters @@ -173,13 +170,14 @@ - this will be out of scope, if not supported by lexers itself - [ ] **(Q3) Custom Keyboard Shortcuts** - User-configurable shortcut keys - Issue: [#595](https://github.com/rizonesoft/Notepad3/issues/595) + - [x] Workaround: Use Microsoft PowerToys's Keyboard-Manager to redirect -## Open Discussions +## Open Discussions - [ ] **(Q2) BUG: Highlight current line broken** - Settings not respected (regression) - Issue: [#5270](https://github.com/rizonesoft/Notepad3/issues/5270) - **This is a discussion, about limited line highlite rule language in schema definition ** -- [x] **(Q3) BUG: Regex replace issue** - Verify if still present - ✅ FIXED +- [x] **(Q3) QUE: Regex replace issue** - Verify if still present - ✅ FIXED - Issue: [#3531](https://github.com/rizonesoft/Notepad3/issues/3531) - Was no Bug but bad RegEx pattern design (expectation vs. what regex really does) @@ -205,9 +203,12 @@ ### Navigation - [ ] **(Q3) Navigate Backward/Forward** - VS Code-like history navigation -- [ ] **(Q2) Go to Block Start/End** - Jump to enclosing block + - Remark: maybe description of change-history navigation is a start. - [x] **(Q2) Brace Find Enhancement** - Search backward for nearest brace when not at one - ✅ IMPLEMENTED - - Issue: [#4863](https://github.com/rizonesoft/Notepad3/issues/4863) + - Issue: [#4863](https://github.com/rizonesoft/Notepad3/issues/4863) - ⚠ Validation ❗ +- [ ] **(Q2) Go to Block Start/End** - Jump to enclosing block + - [x] Fixed by Bracket navigation, incl. Selection + - [ ] No solution for Python-like "Block by Indent" navigation - [ ] **(Q2) Go to Sibling Block** - Navigate between sibling code blocks - [ ] **(Q2) Touchpad Horizontal Scroll** - Direct touchpad left/right scrolling - Issue: [#3468](https://github.com/rizonesoft/Notepad3/issues/3468) @@ -246,6 +247,7 @@ - [ ] **(Q1) Increment/Decrement Number** - Modify number at cursor (Ctrl+Alt++/-) - [ ] **(Q2) Show Hex View** - Display hex representation of selection - [ ] **(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 - Issue: [#4699](https://github.com/rizonesoft/Notepad3/issues/4699) - Disable mark occurrences for large files - Issue: [#4954](https://github.com/rizonesoft/Notepad3/issues/4954) - Can't open files >4GB From 554cbee1800fbab13b6d16c58f4d0a5bb7a7655c Mon Sep 17 00:00:00 2001 From: Rainer Kottenhoff Date: Thu, 7 May 2026 19:32:07 +0200 Subject: [PATCH 3/3] feat(tinyexpr): add hex/binary status-bar output modes with click-to-copy and binary literal input --- readme/tinyexprcpp/TinyExprPP.md | 43 ++++++++ src/Notepad3.c | 182 ++++++++++++++++++++++++++++--- src/Notepad3.h | 1 + src/tinyexprcpp/tinyexpr_cif.cpp | 77 ++++++++++++- src/tinyexprcpp/tinyexpr_cif.h | 6 + todo/TODO.md | 4 +- 6 files changed, 291 insertions(+), 22 deletions(-) diff --git a/readme/tinyexprcpp/TinyExprPP.md b/readme/tinyexprcpp/TinyExprPP.md index 6baec6c1e..4326eb808 100644 --- a/readme/tinyexprcpp/TinyExprPP.md +++ b/readme/tinyexprcpp/TinyExprPP.md @@ -24,6 +24,44 @@ When *Evaluate TinyExpr on Selection* is enabled, selecting any text containing **Multi-selection / rectangular selection** is supported — selected values are concatenated and evaluated as a single expression. +#### Output modes — decimal · hexadecimal · binary + +The TinyExpr status field can display the result in three integer-result formats. The active mode applies to every subsequent evaluation, and is **process-local** — it resets to *decimal* on the next Notepad3 launch. + +| Mode | Example output | Notes | +|------|----------------|-------| +| Decimal *(default)* | `15`, `3.14`, `Inf` | Existing format; integers use up to 21 significant digits, floats `%.8G`. | +| Hexadecimal | `0xF`, `0xFF`, `0xFFFFFFFF` | Uppercase, no padding. | +| Binary | `0b1111`, `0b11111111` | Lowercase `b` prefix, no padding. | + +In hex / binary, fractional results are rounded to the nearest integer (round-half-away-from-zero). Negative values are shown in **two's-complement** (`-1` → `0xFFFFFFFF` / 32 ones). Values outside the supported integer range, `NaN`, and `Inf` fall back to the decimal `%.8G` representation for that single evaluation. + +**Effective bit width** is chosen at runtime via `te_supports_64bit()` — currently 32 bits on the standard MSVC build (where the parser's `double` has a 53-bit mantissa). The display widens automatically to 64 bits if the parser is ever rebuilt with a 64-bit-precise numeric type. + +**Empty selection placeholder** also reflects the active mode: `--`, `0x--`, or `0b--`. + +**Parse error indicator** carries the same prefix: a syntax error at column N is shown as `^[N]` (decimal), `0x^[N]` (hex), or `0b^[N]` (binary). + +#### Status-bar gestures on the TinyExpr field + +| Gesture | Action | +|---------|--------| +| **Single left-click** | Copies the currently displayed result (in the active mode) to the clipboard. The copy is debounced by the system double-click interval, so a follow-up double-click cancels the copy. | +| **Double-click** | Cycles output mode: *Decimal → Hexadecimal → Binary → Decimal …* The status field refreshes immediately. | +| **Right-click** | Standard status-bar context menu (unchanged). | + +#### Binary input + +Expressions may also **use** binary literals `0b…` (or `0B…`), in addition to the always-supported decimal, scientific, and hex (`0x…`) literals. They round-trip the binary output mode: + +``` +0b101010=? → 42 +0xFF + 0b1=? → 256 +BITAND(0b1100, 0b1010)=? → 8 +``` + +Binary literals are recognized only at token-start positions, so identifiers like `var0b1` are not affected. + ### 3. Go to Line / Column Dialog The **Go to Line** dialog (Ctrl+G) accepts expressions, not just numbers: @@ -76,9 +114,12 @@ The Dark Mode highlight contrast value in the Customize Schemes dialog accepts e | Decimal | `3.14` | 3.14 | | Leading dot | `.5` | 0.5 | | Hexadecimal | `0x1F` | 31 | +| Binary | `0b101010` | 42 | | Scientific | `1e3` | 1000 | | Negative scientific | `2.5e-2` | 0.025 | +> Binary literals (`0b…` / `0B…`) are a Notepad3 extension on top of TinyExpr++ — they are rewritten to decimal before the parser sees them. + ### Operators (by Precedence) From highest to lowest precedence: @@ -265,9 +306,11 @@ SQRT(3^2 + 4^2)=? → 5 ``` 0xFF=? → 255 +0b101010=? → 42 2^16 - 1=? → 65535 BITLSHIFT(1, 20)=? → 1048576 (1 MB in bytes) BITAND(0xF0, 0x3C)=? → 48 +BITAND(0b1100, 0b1010)=? → 8 ``` ### Line Numbering (Modify Lines Dialog) diff --git a/src/Notepad3.c b/src/Notepad3.c index 78e6da944..ad5fb5718 100644 --- a/src/Notepad3.c +++ b/src/Notepad3.c @@ -212,6 +212,19 @@ static bool s_bUndoRedoScroll = false; static double s_dExpression = 0.0; static te_int_t s_iExprError = -1; +// TinyExpr++ output mode (process-local, cycled by double-click on STATUS_TINYEXPR) +typedef enum TE_OUT_MODE_T { + TE_OUT_DEC = 0, + TE_OUT_HEX = 1, + TE_OUT_BIN = 2, + TE_OUT_MODE_COUNT = 3, +} TE_OUT_MODE_T; + +static TE_OUT_MODE_T s_iTinyExprOutMode = TE_OUT_DEC; + +static LPCWSTR const s_pszTinyExprModePrefixW[TE_OUT_MODE_COUNT] = { L"", L"0x", L"0b" }; +static LPCSTR const s_pszTinyExprModePrefixA[TE_OUT_MODE_COUNT] = { "", "0x", "0b" }; + static char* s_SelectionBuffer = NULL; //~static CONST WCHAR *const s_ToolbarWndClassName = L"NP3_TOOLBAR_CLASS"; @@ -598,8 +611,102 @@ static inline void ResetFileObservationData(const bool bResetEvt) { #define TE_FMTA "%.8G" #define TE_FMTW L"%.8G" +// Bounds for safe double -> signed-integer cast. (double)INT_MAX/INT64_MAX may +// round UP to the next power of two, so use literals strictly below 2^31 / 2^63. +#define TE_INT64_DBL_MAX (9.2233720368547758E+18) +#define TE_INT32_DBL_MAX (2.147483648E+9) + +// Returns 32 or 64 — the bit width used for HEX/BIN two's-complement display. +// Tracks te_parser::supports_64bit() (constexpr in C++): with the default +// `double` te_type this is 32; if Notepad3 ever switches to long double or +// another 64-bit-precise type, it widens to 64 automatically. +static int _TinyExprBitWidth(void) +{ + static int s_width = 0; + if (s_width == 0) { + s_width = te_supports_64bit() ? 64 : 32; + } + return s_width; +} + +static double _TinyExprIntBound(void) +{ + return (_TinyExprBitWidth() >= 64) ? TE_INT64_DBL_MAX : TE_INT32_DBL_MAX; +} + +static void _FormatBinA(LPSTR pszDest, size_t cchDest, unsigned __int64 u, int width) +{ + // "0b" prefix + up to `width` bits + NUL. + if (cchDest < 4) { + if (cchDest > 0) { + pszDest[0] = '\0'; + } + return; + } + pszDest[0] = '0'; + pszDest[1] = 'b'; + if (u == 0) { + pszDest[2] = '0'; + pszDest[3] = '\0'; + return; + } + int hi = width - 1; + while (hi > 0 && ((u >> hi) & 1ULL) == 0ULL) { + --hi; + } + size_t pos = 2; + for (int b = hi; b >= 0 && pos + 1 < cchDest; --b) { + pszDest[pos++] = (char)('0' + (int)((u >> b) & 1ULL)); + } + pszDest[pos] = '\0'; +} + +static void _FormatBinW(LPWSTR pszDest, size_t cchDest, unsigned __int64 u, int width) +{ + if (cchDest < 4) { + if (cchDest > 0) { + pszDest[0] = L'\0'; + } + return; + } + pszDest[0] = L'0'; + pszDest[1] = L'b'; + if (u == 0) { + pszDest[2] = L'0'; + pszDest[3] = L'\0'; + return; + } + int hi = width - 1; + while (hi > 0 && ((u >> hi) & 1ULL) == 0ULL) { + --hi; + } + size_t pos = 2; + for (int b = hi; b >= 0 && pos + 1 < cchDest; --b) { + pszDest[pos++] = (WCHAR)(L'0' + (int)((u >> b) & 1ULL)); + } + pszDest[pos] = L'\0'; +} + void TinyExprToStringA(LPSTR pszDest, size_t cchDest, const double dExprEval) { + if ((s_iTinyExprOutMode != TE_OUT_DEC) && isfinite(dExprEval) && (fabs(dExprEval) < _TinyExprIntBound())) { + int const width = _TinyExprBitWidth(); + __int64 const i64 = (__int64)llround(dExprEval); + unsigned __int64 u = (unsigned __int64)i64; + if (width < 64) { + u &= ((1ULL << width) - 1ULL); // truncate to the supported width + } + if (s_iTinyExprOutMode == TE_OUT_HEX) { + if (width >= 64) { + StringCchPrintfA(pszDest, cchDest, "0x%llX", u); + } else { + StringCchPrintfA(pszDest, cchDest, "0x%X", (unsigned int)u); + } + } else { // TE_OUT_BIN + _FormatBinA(pszDest, cchDest, u, width); + } + return; + } double intpart = 0.0; double const fracpart = modf(dExprEval, &intpart); if ((fabs(fracpart) < TE_ZERO) && (fabs(intpart) < 1.0E+21)) { @@ -613,6 +720,24 @@ void TinyExprToStringA(LPSTR pszDest, size_t cchDest, const double dExprEval) void TinyExprToString(LPWSTR pszDest, size_t cchDest, const double dExprEval) { + if ((s_iTinyExprOutMode != TE_OUT_DEC) && isfinite(dExprEval) && (fabs(dExprEval) < _TinyExprIntBound())) { + int const width = _TinyExprBitWidth(); + __int64 const i64 = (__int64)llround(dExprEval); + unsigned __int64 u = (unsigned __int64)i64; + if (width < 64) { + u &= ((1ULL << width) - 1ULL); + } + if (s_iTinyExprOutMode == TE_OUT_HEX) { + if (width >= 64) { + StringCchPrintf(pszDest, cchDest, L"0x%llX", u); + } else { + StringCchPrintf(pszDest, cchDest, L"0x%X", (unsigned int)u); + } + } else { // TE_OUT_BIN + _FormatBinW(pszDest, cchDest, u, width); + } + return; + } double intpart = 0.0; double const fracpart = modf(dExprEval, &intpart); if ((fabs(fracpart) < TE_ZERO) && (fabs(intpart) < 1.0E+21)) { @@ -624,6 +749,28 @@ void TinyExprToString(LPWSTR pszDest, size_t cchDest, const double dExprEval) } // ---------------------------------------------------------------------------- +// One-shot debounce timer: NM_CLICK on STATUS_TINYEXPR arms this; if NM_DBLCLK fires +// within GetDoubleClickTime() ms, the dblclk handler kills the timer so we don't +// copy on a double-click. Otherwise the timer fires once and copies the formatted result. +static VOID CALLBACK TinyExprCopyTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime) +{ + UNREFERENCED_PARAMETER(uMsg); + UNREFERENCED_PARAMETER(dwTime); + KillTimer(hwnd, idEvent); // one-shot + + char chExpr[80] = { '\0' }; + if (s_iExprError == 0) { + TinyExprToStringA(chExpr, COUNTOF(chExpr), s_dExpression); + } else if (s_iExprError > 0) { + StringCchPrintfA(chExpr, COUNTOF(chExpr), "%s^[" TE_INT_FMT "]", + s_pszTinyExprModePrefixA[s_iTinyExprOutMode], s_iExprError); + } else { + return; // nothing meaningful to copy + } + SciCall_CopyText((DocPos)StringCchLenA(chExpr, COUNTOF(chExpr)), chExpr); +} +// ---------------------------------------------------------------------------- + //============================================================================= // @@ -877,6 +1024,7 @@ static void _CleanUpResources(const HWND hwnd, bool bIsInitialized) KillTimer(hwnd, ID_LOGROTATETIMER); KillTimer(hwnd, ID_AUTOSAVETIMER); KillTimer(hwnd, ID_PASTEBOARDTIMER); + KillTimer(hwnd, ID_TINYEXPRCOPYTIMER); } if (Globals.pStdDarkModeIniStyles) { @@ -9358,9 +9506,15 @@ LRESULT MsgNotify(HWND hwnd, WPARAM wParam, LPARAM lParam) PostWMCommand(hwnd, eol_cmd); } } - } + } break; + case STATUS_TINYEXPR: + // Defer the copy by one double-click interval so a follow-up dblclk + // (which cycles output mode) cancels it instead of copying first. + SetTimer(hwnd, ID_TINYEXPRCOPYTIMER, GetDoubleClickTime(), TinyExprCopyTimerProc); + break; + default: result = FALSE; break; @@ -9406,17 +9560,11 @@ LRESULT MsgNotify(HWND hwnd, WPARAM wParam, LPARAM lParam) PostWMCommand(hwnd, IDM_VIEW_SCHEME); break; - case STATUS_TINYEXPR: { - char chExpr[80] = { '\0' }; - if (s_iExprError == 0) { - TinyExprToStringA(chExpr, COUNTOF(chExpr), s_dExpression); - } else if (s_iExprError > 0) { - StringCchPrintfA(chExpr, COUNTOF(chExpr), "^[" TE_INT_FMT "]", s_iExprError); - SciCall_CopyText((DocPos)StringCchLenA(chExpr, COUNTOF(chExpr)), chExpr); - } - SciCall_CopyText((DocPos)StringCchLenA(chExpr, COUNTOF(chExpr)), chExpr); - } - break; + case STATUS_TINYEXPR: + KillTimer(hwnd, ID_TINYEXPRCOPYTIMER); // cancel pending single-click copy + s_iTinyExprOutMode = (TE_OUT_MODE_T)((s_iTinyExprOutMode + 1) % TE_OUT_MODE_COUNT); + UpdateStatusbar(true); + break; default: result = FALSE; @@ -10582,12 +10730,11 @@ static void _UpdateStatusbarDelayed(bool bForceRedraw) // try calculate expression of selection if (g_iStatusbarVisible[STATUS_TINYEXPR]) { - static WCHAR tchExpression[32] = { L'\0' }; + static WCHAR tchExpression[80] = { L'\0' }; // fits "0b" + 64 bits + NUL with headroom static te_int_t s_iExErr = -3; s_dExpression = 0.0; - tchExpression[0] = L'-'; - tchExpression[1] = L'-'; - tchExpression[2] = L'\0'; + StringCchPrintf(tchExpression, COUNTOF(tchExpression), L"%s--", + s_pszTinyExprModePrefixW[s_iTinyExprOutMode]); if (Settings.EvalTinyExprOnSelection) { if (bIsSelCharCountable) { @@ -10621,7 +10768,8 @@ static void _UpdateStatusbarDelayed(bool bForceRedraw) if (!s_iExprError) { TinyExprToString(tchExpression, COUNTOF(tchExpression), s_dExpression); } else if (s_iExprError > 0) { - StringCchPrintf(tchExpression, COUNTOF(tchExpression), L"^[" _W(TE_INT_FMT) L"]", s_iExprError); + StringCchPrintf(tchExpression, COUNTOF(tchExpression), L"%s^[" _W(TE_INT_FMT) L"]", + s_pszTinyExprModePrefixW[s_iTinyExprOutMode], s_iExprError); } if (bForceRedraw || (!s_iExprError || (s_iExErr != s_iExprError))) { diff --git a/src/Notepad3.h b/src/Notepad3.h index 2dc0364b5..0d04407eb 100644 --- a/src/Notepad3.h +++ b/src/Notepad3.h @@ -71,6 +71,7 @@ np3params, *LPnp3params; #define ID_AUTOSCROLLTIMER (0xA004) // Middle-Click Auto-Scroll #define ID_ATOMICSAVETIMER (0xA005) // Atomic Save Detection #define ID_DEFERMINIMIZETIMER (0xA006) // Deferred minimize on /B + /I startup +#define ID_TINYEXPRCOPYTIMER (0xA007) // TinyExpr status-bar single-click copy debounce //==== Reuse Window Lock Timeout ============================================== diff --git a/src/tinyexprcpp/tinyexpr_cif.cpp b/src/tinyexprcpp/tinyexpr_cif.cpp index 0a80bfc2d..0f91f13a6 100644 --- a/src/tinyexprcpp/tinyexpr_cif.cpp +++ b/src/tinyexprcpp/tinyexpr_cif.cpp @@ -17,6 +17,8 @@ #include #include #include +#include +#include #include #include #include @@ -72,6 +74,66 @@ static void te_cif_configure_separators(te_parser &parser) // else: keep defaults ('.' and ',') } +// --------------------------------------------------------------------------- +// Pre-pass: rewrite C-style binary literals `0b` (or `0B`) to their +// decimal equivalent, since TinyExpr++ tokenizes via std::strtod which only +// accepts `0x` (hex) and decimal/scientific. Hex is left alone (parser handles +// it). Octal is not rewritten — TinyExpr++ has no concept of octal literals +// either, but binary is the only base we currently emit on output, so input +// parity here matters most. +// +// Detection rule: `0b` is recognized as a binary literal only when it begins +// at a token-start position (start-of-string, after whitespace, an operator, +// `(`, `,`, `;`, etc.). Inside a variable name (e.g. `var0b1`) it is left as-is. +// --------------------------------------------------------------------------- +static std::string te_cif_rewrite_binary_literals(const char *expression) +{ + std::string out; + if (!expression) { + return out; + } + out.reserve(std::strlen(expression)); + + bool numStart = true; // expression start counts as a token boundary + const unsigned char *p = reinterpret_cast(expression); + while (*p) { + if (numStart && p[0] == '0' && (p[1] == 'b' || p[1] == 'B') && + (p[2] == '0' || p[2] == '1')) { + const unsigned char *digits = p + 2; + const unsigned char *end = digits; + unsigned long long value = 0; + while (*end == '0' || *end == '1') { + if (end - digits < 64) { // silently clamp at 64 bits + value = (value << 1) | static_cast(*end - '0'); + } + ++end; + } + char buf[32]; + std::snprintf(buf, sizeof(buf), "%llu", value); + out.append(buf); + p = end; + numStart = false; + continue; + } + unsigned char const c = *p; + out.push_back(static_cast(c)); + switch (c) { + case '+': case '-': case '*': case '/': case '%': case '^': + case '(': case ',': case ';': + case '<': case '>': case '=': case '!': + case '|': case '&': case ':': + case ' ': case '\t': case '\n': case '\r': + numStart = true; + break; + default: + numStart = false; + break; + } + ++p; + } + return out; +} + // --------------------------------------------------------------------------- // Helper: map TinyExpr++ error state to old 1-based error position // Old convention: 0 = success, >= 1 = 1-based error position @@ -107,9 +169,10 @@ double te_interp(const char *expression, te_int_t *error) te_parser parser; te_cif_add_compat_functions(parser); te_cif_configure_separators(parser); + std::string const rewritten = te_cif_rewrite_binary_literals(expression); double result; try { - result = parser.evaluate(expression); + result = parser.evaluate(rewritten); } catch (...) { if (error) { @@ -161,8 +224,9 @@ void *te_compile(const char *expression, const void *variables, int var_count, t ctx->parser.set_variables_and_functions(std::move(cppVars)); } - // Compile and evaluate once to check for errors - (void)ctx->parser.evaluate(expression); + // Compile and evaluate once to check for errors. Rewrite 0b… literals first. + std::string const rewritten = te_cif_rewrite_binary_literals(expression); + (void)ctx->parser.evaluate(rewritten); if (!ctx->parser.success()) { if (error) { @@ -209,4 +273,11 @@ void te_free(void *n) } } +// Reports whether the parser's te_type can represent uint64_t losslessly. +// te_parser::supports_64bit() is constexpr; the optimizer will inline the constant. +int te_supports_64bit(void) +{ + return te_parser::supports_64bit() ? 1 : 0; +} + } // extern "C" diff --git a/src/tinyexprcpp/tinyexpr_cif.h b/src/tinyexprcpp/tinyexpr_cif.h index 1286c7103..a2c931e5c 100644 --- a/src/tinyexprcpp/tinyexpr_cif.h +++ b/src/tinyexprcpp/tinyexpr_cif.h @@ -54,6 +54,12 @@ double te_eval(te_expr *n); * Safe to call on NULL pointers. */ void te_free(te_expr *n); +/* Returns 1 if the parser's internal numeric type can represent uint64_t + * without loss of precision, 0 otherwise. + * (For the default `double` te_type the answer is 0; bitwise ops are + * therefore limited to ~52 bits in practice.) */ +int te_supports_64bit(void); + /* ---- NP3-specific utility functions (inline) ---- */ diff --git a/todo/TODO.md b/todo/TODO.md index b21c2ce6e..295aae2f9 100644 --- a/todo/TODO.md +++ b/todo/TODO.md @@ -188,8 +188,8 @@ - [ ] **(Q2) LaTeX Input Method** - LaTeX character insertion (`\alpha` → α) - [x] **(Q2) Base64 Encode/Decode** - ✅ IMPLEMENTED via `IDM_EDIT_BASE64ENCODE/DECODE` - [ ] **(Q1) Insert Unicode Control Characters** - LRM, RLM, ZWJ, ZWNJ, etc. -- [ ] **(Q2) Number Base Conversion** - Binary/Decimal/Octal/Hex - - Issue: [#5059](https://github.com/rizonesoft/Notepad3/issues/5059) - TinyExpr output in hex/binary +- [x] **(Q2) Number Base Conversion** - Binary/Decimal/Octal/Hex - ✅ IMPLEMENTED + - Issue: [#5059](https://github.com/rizonesoft/Notepad3/issues/5059) - TinyExpr output in hex/binary - ⚠ Validation ❗ - [ ] **(Q2) Character Map Conversions** - Fullwidth↔Halfwidth, CJK transforms (`LCMapStringEx`) - [ ] **(Q3) Code Compress/Pretty** - Minify/beautify code - Issue: [#5515](https://github.com/rizonesoft/Notepad3/issues/5515) - JSON format, compress, escape/unescape