diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4f504a29a..7f8349517 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,314 +1,186 @@ # Copilot Instructions for Notepad3 +Win32 C/C++ text editor on Scintilla/Lexilla. Built with Visual Studio 2026 (toolset v145) via MSBuild. Solution: `Notepad3.sln`. Windows-only. + ## Build -Notepad3 is a Windows-only C/C++ application built with Visual Studio 2026 (toolset v145) and MSBuild. The solution file is `Notepad3.sln`. +Scripts under `Build\` (PowerShell under `Build\scripts\`): -### Build commands +- `Build\Build_x64.cmd [Release|Debug]` — single platform (also `_Win32`, `_ARM64`, `_x64_AVX2`) +- `Build\BuildAll.cmd` — all four platforms +- `msbuild Notepad3.sln /m /p:Configuration=Release /p:Platform=x64` — CI equivalent +- `Build\Clean.cmd` — clean outputs +- `nuget restore` once before first build +- `Version.ps1` regenerates `src\VersionEx.h` (`Major.YY.Mdd.Build`, build number in `Versions\build.txt`) +- Tests: `test\TestFileVersion.cmd`, `test\TestAhkNotepad3.cmd` (AutoHotkey). CI in `.github/workflows/build.yml` (windows-2022, Release × all four platforms). -```powershell -# Full build (all platforms: Win32, x64, x64_AVX2, ARM64) -Build\BuildAll.cmd [Release|Debug] +Default config is Release. -# Single platform -Build\Build_x64.cmd [Release|Debug] -Build\Build_Win32.cmd [Release|Debug] -Build\Build_ARM64.cmd [Release|Debug] -Build\Build_x64_AVX2.cmd [Release|Debug] +## Core Modules (`src\`) -# MSBuild directly (used by CI) -msbuild Notepad3.sln /m /p:Configuration=Release /p:Platform=x64 +- `Notepad3.c/h` — `wWinMain`, `MainWndProc`, global state structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`), `MsgCommand()` dispatcher with static sub-handlers (`_HandleFileCommands`, `_HandleEditBasicCommands`, `_HandleViewAndSettingsCommands`, etc.). +- `Notepad3Util.c/h` — bitmap/toolbar loaders (`NP3Util_LoadBitmapFile`, `NP3Util_CreateScaledImageListFromBitmap`), wrap config (`NP3Util_SetWrapIndentMode`, `NP3Util_SetWrapVisualFlags`), middle-click auto-scroll (`NP3Util_AutoScrollStart/Stop`). +- `Edit.c/h` — find/replace (PCRE2), encoding, clipboard, indent, sort, bookmarks, folding, autocomplete. +- `Styles.c/h` — Scintilla styling, lexer selection, themes. +- `Dialogs.c/h` — dialogs, DPI-aware UI. +- `Encoding.c/h` — character encoding detection/conversion. +- `SciCall.h` — type-safe wrappers for Scintilla direct calls. +- `DynStrg.c/h` — `HSTRINGW` dynamic wide-string handle. +- `PathLib.c/h` — `HPATHL` path handle + long-path-aware Win32 wrappers. +- `TypeDefs.h` — `DocPos`, `DocLn`, `cpi_enc_t`, OS targeting, compiler macros. +- `MuiLanguage.c/h` — MUI language DLL loading. -# Clean -Build\Clean.cmd +## Vendored Libraries -# NuGet restore (required before first build) -nuget restore -``` +`scintilla\` (5.5.8 + `np3_patches\`, docs in `scintilla\doc\`), `lexilla\` (5.4.6 + `np3_patches\`, docs in `lexilla\doc\`), `scintilla\pcre2\` (10.47, replaced archived Oniguruma), `src\uchardet\`, `src\tinyexpr\` / `src\tinyexprcpp\`, `src\uthash\`, `src\crypto\` (Rijndael/SHA-256 for AES-256). -Default configuration is Release. The build scripts delegate to PowerShell scripts in `Build\scripts\`. +## grepWin (`grepWin\`) — external tool, not built from source -### Versioning +Pre-built exes under `grepWin\portables\`; `.lang` files under `grepWin\translations\`. Runtime lookup in `src\Dialogs.c`: `Settings2.GrepWinPath` → `\grepWin\grepWin-x{64,86}_portable.exe` → `%APPDATA%\Rizonesoft\Notepad3\grepWin\`. `grepWinLangResName[]` in `MuiLanguage.c` maps Notepad3 locales to `.lang` filenames (written into `grepwin.ini` before launch). Portable build scripts (`Build\make_portable_*.cmd`) package both portable exes + LICENSE + all `.lang` files. ARM64 uses the x64 exe via emulation (`#if defined(_M_ARM64)` in `Notepad3.c`). -Run `Version.ps1` before building to generate `src\VersionEx.h` from templates in `Versions\`. Version format is `Major.YY.Mdd.Build` where the build number is persisted in `Versions\build.txt`. +## Lexers (`src\StyleLexers\`) -### Tests +Each language is one `styleLexXXX.c` defining an `EDITLEXER` struct (see existing files for template). To add: create the file, register in `Styles.c` lexer array, add localization string IDs. -Tests live in `test\` and run via GitHub Actions CI (Win32 and x64 jobs): +## Localization (`language\`) -```cmd -cd test -TestFileVersion.cmd # Verifies built binary version info -TestAhkNotepad3.cmd # AutoHotkey-based GUI tests (requires AutoHotkey) -``` +27+ locales under `np3_LANG_COUNTRY\`. Language packs build as separate DLLs. -### CI +### Adding a string resource -GitHub Actions workflow at `.github/workflows/build.yml` builds all four platforms (Win32, x64, x64_AVX2, ARM64) in Release on `windows-2022` runners, triggered on push/PR to master. +1. `#define IDS_MUI_XXX ` in `language\common_res.h` (13xxx errors/warnings, 14xxx info/prompts). +2. English in `language\np3_en_us\strings_en_us.rc`. +3. Same English as placeholder in the 25 other `strings_*.rc` files (translators update later). +4. Display via `InfoBoxLng()` / `MessageBoxLng()`; check `IsYesOkay()`. `Settings.MuteMessageBeep` controls silent vs. sound — provide both paths. -## Architecture +## Clipboard Monitoring (Pasteboard Mode) -Notepad3 is a Win32 desktop text editor built on the **Scintilla** editing component with **Lexilla** for syntax highlighting. It ships with the companion tool **MiniPath** (file browser) and integrates with the external **grepWin** tool (file search/grep) via pre-built portable executables. +Runtime-toggleable; external clipboard changes are pasted at the caret. -### Core modules (in `src\`) +- Menu: `IDM_EDIT_TOGGLE_PASTEBOARD` — "Toggle Clipboard Monitoring". Check mark reflects state. +- Helpers `PasteBoard_Start(HWND)` / `PasteBoard_Stop(HWND)` wrap `AddClipboardFormatListener` + `ID_PASTEBOARDTIMER`. Used for `/B` startup and from the toggle handler. +- **Not persisted**; always OFF at startup unless `/B`. +- **Mutex with Tail** (`IDM_VIEW_CHASING_DOCTAIL` / `FileWatching.MonitoringLog`): each mode greys the other in `MsgInitMenu`; tail toolbar button (`IDT_VIEW_CHASING_DOCTAIL`) greyed; "Monitoring Log" checkbox in `ChangeNotifyDlgProc` greyed. `IsPasteBoardActive()` in `Notepad3.h` for cross-TU use. +- Startup conflict (`/B` + persisted `MonitoringLog=true`): `/B` wins this session; `FileWatching.MonitoringLog` cleared in memory, `Settings.MonitoringLog` (INI) preserved. +- `PasteBoardTimerProc` pastes at caret. `Settings2.PasteBoardSeparator` pre-pended; suppressed on (1) first paste after enable and (2) caret at line start. `\x01` = one document EOL; `\0` = no separator. +- Status bar `STATUS_OVRMODE` shows `CBS` while active (passive indicator). -- **Notepad3.c/h** — Application entry point (`wWinMain`), window procedure, global state structs (`GLOBALS_T`, `SETTINGS_T`, `FLAGS_T`, `PATHS_T`), `MsgCommand()` dispatcher with sub-handlers -- **Notepad3Util.c/h** — Utility functions extracted from Notepad3.c: bitmap/toolbar image loading (`NP3Util_LoadBitmapFile`, `NP3Util_CreateScaledImageListFromBitmap`), word-wrap configuration (`NP3Util_SetWrapIndentMode`, `NP3Util_SetWrapVisualFlags`), auto-scroll (`NP3Util_AutoScrollStart/Stop`) -- **Edit.c/h** — Text manipulation: find/replace (PCRE2 regex), encoding conversion, clipboard, indentation, sorting, bookmarks, folding, auto-complete -- **Styles.c/h** — Scintilla styling, lexer selection, theme management -- **Dialogs.c/h** — All dialog boxes and UI interactions -- **Encoding.c/h** — Character encoding detection and conversion -- **SciCall.h** — Type-safe inline wrappers for Scintilla direct function calls (avoids `SendMessage` overhead) -- **DynStrg.c/h** — Custom dynamic wide-string type (`HSTRINGW`) with automatic buffer management -- **PathLib.c/h** — Path manipulation via opaque `HPATHL` handle -- **TypeDefs.h** — Core type definitions (`DocPos`, `DocLn`, `cpi_enc_t`), Windows version targeting, compiler macros -- **MuiLanguage.c/h** — Multi-language UI support, language DLL loading +## File I/O -### Vendored dependencies +- `FileSave()` / `FileLoad()` (`Notepad3.c`) → `FileIO()` → `EditSaveFile()` / `EditLoadFile()` (`Edit.c`). +- Atomic save (temp + `ReplaceFileW`) controlled by `Settings2.AtomicFileSave`. +- `Globals.dwLastError` holds Win32 error. `FileSave()` checks `ERROR_ACCESS_DENIED`, `ERROR_PATH_NOT_FOUND` before generic fallback. +- `InstallFileWatching()` uses `FindFirstChangeNotificationW` on the parent dir. `InstallFileWatching(false)` before save, `(true)` after. -- **`scintilla\`** — Scintilla editor component with Notepad3-specific patches in `np3_patches\` -- **`scintilla\doc\`** - Scintilla documentation offline -- **`lexilla\`** — Lexilla syntax highlighting engine with custom patches -- **`lexilla\doc\`** — Lexilla documentation offline -- **`scintilla\pcre2\`** — PCRE2 10.47 regex engine for find/replace (replaced archived Oniguruma) -- **`src\uchardet\`** — Character encoding detector -- **`src\tinyexpr\` / `src\tinyexprcpp\`** — Expression evaluator for statusbar -- **`src\uthash\`** — Hash table library (C macros) -- **`src\crypto\`** — Rijndael/SHA-256 for AES-256 encryption +## PCRE2 (`scintilla\pcre2\`) -### grepWin Integration (`grepWin\`) +`scintilla\pcre2\scintilla\PCRE2RegExEngine.cxx` compiled with `SCI_OWNREGEX` overrides Scintilla's built-in regex. Key points: `FindText`, `SubstituteByPosition`, `convertReplExpr` (normalizes `\1`-`\9`→`$1`-`$9`, processes `\n\t\xHH\uHHHH`), `translateRegExpr` (`\<`/`\>` → lookarounds, `\uHHHH` → `\x{HHHH}`). Standalone `RegExFind` used by `EditURLDecode`. -grepWin is an **external** file search/grep tool — it is **not** built from source as part of Notepad3. Pre-built portable executables and translation files are stored in the repository: +Replacement backref syntax: `$0`-`$99`, `\0`-`\9`, `${name}`, `${+name}`. -- **`grepWin\portables\`** — `grepWin-x86_portable.exe`, `grepWin-x64_portable.exe`, `LICENSE.txt` -- **`grepWin\translations\`** — `*.lang` translation files (e.g. `German.lang`, `French.lang`) +URL hotspot regex: `HYPLNK_REGEX_FULL` in `src\Edit.c` — matches `https?://`, `ftp://`, `file:///`, `file://`, `mailto:`, `www.`, `ftp.`. Trailing group excludes `.,:?!`. -At runtime (`src\Dialogs.c`), Notepad3 searches for grepWin in this order: -1. `Settings2.GrepWinPath` (user-configured INI setting) -2. `\grepWin\grepWin-x64_portable.exe` (or x86) — portable layout -3. `%APPDATA%\Rizonesoft\Notepad3\grepWin\` — installed layout +## DarkMode (`src\DarkMode\`) -Language mapping (`src\MuiLanguage.c`): `grepWinLangResName[]` maps Notepad3 locale names (e.g. `de-DE`) to grepWin `.lang` filenames (e.g. `German.lang`). The language file path is written to `grepwin.ini` before launching. +Windows 10/11 dark mode via IAT hooks on uxtheme/user32 (stub DLLs included). -Portable build scripts (`Build\make_portable_*.cmd`) package grepWin into a `grepWin\` subdirectory in the archive containing both portable executables, the license, and all `*.lang` translations. +## ARM64 -### Syntax highlighting lexers (`src\StyleLexers\`) +Supported: Win32 (x86), x64, x64_AVX2, ARM64. **ARM 32-bit is not supported** — `Release|ARM` maps to Win32. -Each language has its own `styleLexXXX.c` file (~50+ languages). All lexers follow the `EDITLEXER` struct pattern defined in `EditLexer.h`: - -```c -EDITLEXER lexXXX = { - SCLEX_XXX, // Scintilla lexer ID - "lexerName", // Lexilla lexer name (case-sensitive) - IDS_LEX_XXX_STR, // Resource string ID - L"Config Name", // INI section name - L"ext1; ext2", // Default file extensions - L"", // Extension buffer (runtime) - &KeyWords_XXX, // Keyword lists - { /* EDITSTYLE array — must be last member */ } -}; -``` - -### Localization (`language\`) - -Resource-based MUI system with 27+ locales. Each locale has a directory `np3_LANG_COUNTRY\` containing resource `.rc` files. Language DLLs are built as separate projects in the solution. - -### Adding String Resources - -1. Add `#define IDS_MUI_XXX ` to `language\common_res.h` (use next available ID in the appropriate range: 13xxx for errors/warnings, 14xxx for info/prompts) -2. Add the English string to `language\np3_en_us\strings_en_us.rc` in the matching `STRINGTABLE` block -3. Add the same English text as placeholder to all other 25 locale `strings_*.rc` files (translators update later) -4. Use `InfoBoxLng()` / `MessageBoxLng()` with `MB_YESNO`, `MB_ICONWARNING`, etc. to display; check result with `IsYesOkay()` -5. `Settings.MuteMessageBeep` controls whether to use `InfoBoxLng` (silent) or `MessageBoxLng` (with sound) — always provide both paths - -### File I/O Flow - -- **`FileSave()`** (`src\Notepad3.c`) — Main save dispatcher. Handles Save, Save As, Save Copy. - Calls `FileIO()` → `EditSaveFile()` (`src\Edit.c`) -- **`FileLoad()`** (`src\Notepad3.c`) — Main load dispatcher. - Calls `FileIO()` → `EditLoadFile()` (`src\Edit.c`) -- **`EditSaveFile()`** supports atomic save (temp file + `ReplaceFileW`) controlled by `Settings2.AtomicFileSave` -- **Error handling**: `Globals.dwLastError` holds the Win32 error code after failed I/O. - `FileSave()` checks specific codes (`ERROR_ACCESS_DENIED`, `ERROR_PATH_NOT_FOUND`) before falling back to generic error. -- **File watching**: `InstallFileWatching()` uses `FindFirstChangeNotificationW` on the parent directory. - Must be stopped before save (`InstallFileWatching(false)`) and restarted after (`InstallFileWatching(true)`). - -### PCRE2 Regex Engine (`scintilla\pcre2\`) - -PCRE2 10.47 replaced the archived Oniguruma library. The Scintilla integration lives in `scintilla\pcre2\scintilla\PCRE2RegExEngine.cxx`, compiled with `SCI_OWNREGEX` to override Scintilla's built-in regex. - -Key components: -- **`PCRE2RegExEngine::FindText`** — Scintilla regex search (pattern matching via `pcre2_match`) -- **`PCRE2RegExEngine::SubstituteByPosition`** — Regex replacement with group references -- **`PCRE2RegExEngine::convertReplExpr`** — Normalizes replacement strings: converts `\1`-`\9` to `$1`-`$9`, processes escape sequences (`\n`, `\t`, `\xHH`, `\uHHHH`) -- **`PCRE2RegExEngine::translateRegExpr`** — Translates Scintilla regex extensions: `\<`/`\>` word boundaries → lookarounds, `\uHHHH` → `\x{HHHH}` -- **`RegExFind`** (exported C function) — Standalone regex find used by `EditURLDecode` in `Edit.c`; wraps `SimplePCRE2Engine` - -Replacement string backreference syntax (both flavors supported for backward compatibility): -- `$0`-`$99` and `\0`-`\9` — numbered group references -- `${name}` / `${+name}` — named group references -- Escape sequences: `\n`, `\t`, `\r`, `\\`, `\xHH`, `\uHHHH` - -URL hotspot regex is defined at `src\Edit.c:108` (`HYPLNK_REGEX_FULL` macro). It matches `https?://`, `ftp://`, `file:///`, `file://`, `mailto:`, `www.`, `ftp.` schemes. The trailing group excludes punctuation (`.,:?!`) so URLs don't absorb sentence-ending characters. - -### DarkMode (`src\DarkMode\`) - -Windows 10/11 dark mode via IAT (Import Address Table) hooks. Includes stub DLLs for uxtheme and user32. - -### ARM64 Platform Considerations - -**Supported platforms**: Win32 (x86), x64, x64_AVX2, ARM64. ARM 32-bit is **not** supported (the `Release|ARM` solution config maps to Win32). - -#### Architecture detection - -Use `#if defined(_M_ARM64)` or the helper macro `NP3_BUILD_ARM64` (defined in `src\TypeDefs.h`) for ARM64-specific code paths. **Important**: both ARM64 and x64 define `_WIN64`, so use `_M_ARM64` when you need to distinguish ARM64 from x64. - -#### ARM64 rendering defaults - -ARM64 defaults to `SC_TECHNOLOGY_DIRECTWRITERETAIN` (value 2) instead of `SC_TECHNOLOGY_DIRECTWRITE` (value 1) to preserve the Direct2D back buffer between frames. This avoids flickering on Qualcomm Adreno GPUs and the Win11 25H2 DWM compositor. The main window also uses `WS_EX_COMPOSITED` on ARM64 for system-level double-buffering. Users can override via `RenderingTechnology` in the INI file or the View menu. - -#### ARM64 build configuration - -- `CETCompat` must be `false` for ARM64 (CET is x86/x64 only) -- `TargetMachine` must be `MachineARM64` in all ARM64 linker sections -- `_WIN64` must be defined in preprocessor definitions for all ARM64 configurations -- Build fix scripts in `Build\scripts\`: `FixARM64CETCompat.ps1`, `FixARM64CrossCompile.ps1`, `FixARM64OutDir.ps1` - -#### GrepWin on ARM64 - -No native ARM64 grepWin build exists. The ARM64 build uses `grepWin-x64_portable.exe` which runs via x64 emulation on Windows ARM64. The binary selection in `src\Notepad3.c` uses `#if defined(_M_ARM64)` to handle this explicitly. - -#### Theme change flickering prevention - -`MsgThemeChanged()` in `src\Notepad3.c` wraps all heavy operations (bar recreation, lexer reset, restyling) in `WM_SETREDRAW FALSE/TRUE` to suppress intermediate repaints and performs a single `RedrawWindow()` at the end. DarkMode `RedrawWindow()` calls in `ListViewUtil.hpp` omit `RDW_ERASE` to avoid background erase flashes during theme transitions. +- Both ARM64 and x64 define `_WIN64`. Use `_M_ARM64` (or `NP3_BUILD_ARM64` in `TypeDefs.h`) to distinguish. +- Rendering: defaults to `SC_TECHNOLOGY_DIRECTWRITERETAIN` (2) — preserves Direct2D back buffer, avoids flicker on Qualcomm Adreno + Win11 25H2 DWM. Main window gets `WS_EX_COMPOSITED`. User can override via `RenderingTechnology` / View menu. +- Build: `CETCompat=false` (CET is x86/x64 only), `TargetMachine=MachineARM64`, `_WIN64` defined. Fix scripts: `Build\scripts\FixARM64{CETCompat,CrossCompile,OutDir}.ps1`. +- grepWin: uses x64 exe via emulation. +- `MsgThemeChanged()` wraps bar recreate / lexer reset / restyle in `WM_SETREDRAW FALSE/TRUE` + single `RedrawWindow()` at end. DarkMode `RedrawWindow()` in `ListViewUtil.hpp` omits `RDW_ERASE`. ## Conventions -### Code style +### Formatting & encoding -- **Formatting**: LLVM-based `.clang-format` in `src\` — 4-space indentation, Stroustrup brace style, left-aligned pointers, no column limit, no include sorting -- **Editor config**: `.editorconfig` enforces UTF-8/CRLF for source files, 4-space indentation for C/C++; Lexilla code uses tabs (preserved from upstream) -- **File encoding rules** (must be respected when creating or editing these files): - - `language\*\*.rc` — **UTF-8 without BOM**. Never write or save these files with a UTF-8 BOM. Use `Build\rc_to_utf8.cmd` to strip accidental BOMs. - - **PowerShell pitfall**: `[System.Text.Encoding]::UTF8` writes **with** BOM. Always use `[System.Text.UTF8Encoding]::new($false)` when writing `.rc` files from PowerShell scripts. - - **Python/script pitfall**: `.rc` files use **CRLF** line endings. When writing or inserting lines from Python or other scripts, use `\r\n` — not bare `\n`. After bulk edits, normalize with: `content = content.replace('\r\n', '\n').replace('\n', '\r\n')` before writing. - - `Build\Notepad3.ini`, `Build\minipath.ini` — **UTF-8 with BOM** (BOM = `EF BB BF`). These INI reference files must retain the BOM. -- **String safety**: Uses `strsafe.h` throughout; deprecated string functions are disabled +- `.clang-format` in `src\` — 4-space indent, Stroustrup braces, left-aligned pointers, no column limit, no include sorting. +- `.editorconfig` — UTF-8/CRLF for source, 4-space indent C/C++. Lexilla uses tabs (upstream preserved). +- **`language\*\*.rc` = UTF-8 WITHOUT BOM, CRLF.** Never write with BOM. Use `Build\rc_to_utf8.cmd` to strip. PowerShell: `[System.Text.UTF8Encoding]::new($false)`. Python: write `\r\n` explicitly. +- **`Build\Notepad3.ini`, `Build\minipath.ini` = UTF-8 WITH BOM** (`EF BB BF`). Preserve it. +- `strsafe.h` throughout; deprecated string functions disabled. -### Type conventions +### Types -- Use `DocPos` / `DocPosU` / `DocLn` for Scintilla document positions and line numbers (not raw `int`) -- Use `cpi_enc_t` for encoding identifiers -- Use `HSTRINGW` and `HPATHL` (opaque handle types) instead of raw `WCHAR*` buffers for dynamic strings and paths -- `NOMINMAX` is defined globally — use `min()`/`max()` macros or typed equivalents +- `DocPos` / `DocPosU` / `DocLn` for document positions/lines (not raw `int`). +- `cpi_enc_t` for encodings. +- `HSTRINGW` / `HPATHL` instead of raw `WCHAR*` buffers. +- `NOMINMAX` is global — use `min()`/`max()` or typed equivalents. ### Scintilla interaction -Always use `SciCall.h` wrappers (e.g., `SciCall_GetTextLength()`) instead of raw `SendMessage(hwnd, SCI_XXX, ...)`. The wrappers use Scintilla's direct function pointer for performance. -Add missing wrapper calls to `SciCall.h` if needed. +Always use `SciCall.h` wrappers (direct function pointer for performance). Add missing wrappers there. Naming: `DeclareSciCall{V|R}{0|01|1|2}` — V=void, R=return; 0/1/2 = param count; `01` = lParam-only. The `msg` arg is the suffix after `SCI_`. -#### SciCall.h wrapper macros - -Wrappers are declared using macros. The naming convention is `DeclareSciCall{V|R}{0|01|1|2}`: - -- **V** = void return, **R** = has return value -- **0** = no parameters, **1** = one parameter (wParam), **2** = two parameters (wParam + lParam) -- **01** = optional second parameter only (wParam=0, lParam=var) — used when the SCI message takes lParam but not wParam +Example wrappers: ```c -// Examples: -DeclareSciCallV0(Undo, UNDO); // SciCall_Undo() -DeclareSciCallV1(SetTechnology, SETTECHNOLOGY, int, technology); // SciCall_SetTechnology(int) -DeclareSciCallV2(ScrollVertical, SCROLLVERTICAL, DocLn, docLn, int, subLn); // SciCall_ScrollVertical(DocLn, int) -DeclareSciCallR0(GetTextLength, GETTEXTLENGTH, DocPos); // DocPos SciCall_GetTextLength() -DeclareSciCallR1(SupportsFeature, SUPPORTSFEATURE, bool, int, feature); // bool SciCall_SupportsFeature(int) +DeclareSciCallV0(Undo, UNDO); // SciCall_Undo() +DeclareSciCallV1(SetTechnology, SETTECHNOLOGY, int, technology); // SciCall_SetTechnology(int) +DeclareSciCallR0(GetTextLength, GETTEXTLENGTH, DocPos); // DocPos SciCall_GetTextLength() +DeclareSciCallR1(SupportsFeature, SUPPORTSFEATURE, bool, int, feature); // bool SciCall_SupportsFeature(int) ``` -The `msg` argument is the suffix after `SCI_` (e.g., `UNDO` for `SCI_UNDO`). +### Scintilla/Lexilla versions -### Scintilla / Lexilla versions - -The vendored Scintilla (5.5.8) and Lexilla (5.4.6) have Notepad3-specific patches in `scintilla\np3_patches\` and `lexilla\np3_patches\`. Version numbers are in `scintilla\version.txt` and `lexilla\version.txt`. When evaluating new Scintilla APIs, check the offline docs in `scintilla\doc\` and `lexilla\doc\`. - -### Adding a new syntax lexer - -1. Create `src\StyleLexers\styleLexNEW.c` following existing lexer patterns -2. Define `EDITLEXER` struct with lexer ID, name, extensions, keywords, and styles -3. Register it in `Styles.c` lexer array -4. Add localization string IDs to resource files +Vendored 5.5.8 / 5.4.6 with NP3 patches under `np3_patches\`. Versions in `scintilla\version.txt` / `lexilla\version.txt`. Check offline docs under `scintilla\doc\` and `lexilla\doc\` for new APIs. ### Global state -Application state is centralized in global structs in `Notepad3.c` — `Globals`, `Settings`, `Settings2`, `Flags`, `Paths`. Prefer accessing these through their defined interfaces rather than adding new globals. +Use existing structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`). Don't add new globals. -### INI file / portable-app design +### Portable INI design -Notepad3 follows a **portable-app** design for its configuration file (`Notepad3.ini`): +- INI beside the exe. No registry. Runs on defaults if none exists (user creates via "Save Settings Now"). +- **Admin redirect**: `Notepad3.ini=` in `[Notepad3]` redirects to per-user path (up to 2 levels). Redirect targets auto-created via `CreateIniFileEx()`. +- `Paths.IniFile` = active writable INI; `Paths.IniFileDefault` = recovery fallback. +- Init flow (in `src\Config\Config.cpp`): `FindIniFile()` → `TestIniFile()` → `CreateIniFile()` → `LoadSettings()`. +- **`SettingsVersion` defaults to `CFG_VER_CURRENT`** when missing — empty/new INI gets current defaults. +- **`bIniFileFromScratch`** is set when INI is 0 bytes, cleared after `SaveAllSettings()`. While set, `MuiLanguage.c` suppresses writing `PreferredLanguageLocaleName`. +- **Empty lexer sections are pruned** in `Style_ToIniSection()` via `IniSectionGetKeyCount()` — only sections with non-default styles persist. +- MiniPath uses the same pattern (`minipath\src\Config.cpp`). +- **New `Settings2` (or other INI) params must be documented as commented entries in `Build\Notepad3.ini`.** -- **No auto-creation on first run**: If no INI file is found, the application runs with defaults. The path is stored in `Paths.IniFileDefault` (not `Paths.IniFile`) so the user can explicitly create it via "Save Settings Now". -- **Admin redirect**: An administrator can place `Notepad3.ini=` in `[Notepad3]` section of the app-directory INI to redirect to a per-user path. Up to 2 levels of redirect are supported. Redirect targets **are** auto-created (the admin intended them to exist). -- **Key paths**: `Paths.IniFile` = active writable INI (empty if none exists), `Paths.IniFileDefault` = fallback path for "Save Settings Now" recovery. -- **Configuration code**: All INI init logic lives in `src\Config\Config.cpp` — `FindIniFile()` → `TestIniFile()` → `CreateIniFile()` → `LoadSettings()`. -- **Empty INI = no INI**: An empty (0-byte) INI file must produce identical saved output to having no INI file at all. `SettingsVersion` defaults to `CFG_VER_CURRENT` when missing — a missing key does NOT imply a legacy config. Empty lexer sections are removed after style saving (`IniSectionGetKeyCount()`). -- **MiniPath** follows the same portable INI and admin-redirect pattern (`minipath\src\Config.cpp`). Redirect targets are auto-created via `CreateIniFileEx()`. -- **New parameters**: When adding new `Settings2` (or other INI) parameters, always document them as commented entries in `Build\Notepad3.ini` +### Save/Load macros (`src\Config\Config.cpp`) -### Settings Save/Load Macros (`src\Config\Config.cpp`) +- **`Encoding_MapIniSetting` is asymmetric.** CPI constants (`CPI_UTF8=6`, `CPI_OEM=1`, …) don't equal INI values (`3`, `5`, …). `Encoding_MapIniSetting(true, val)` = INI→CPI (load); `(false, val)` = CPI→INI (save). CPI with `bLoad=true` produces wrong results. Reference: `MRU_Save()` uses `false`. +- **Defaults depending on `DefaultEncoding`** (e.g. `SkipANSICodePageDetection`, `LoadASCIIasUTF8`) must be recalculated in `_SaveSettings()` before `SAVE_VALUE_IF_NOT_EQ_DEFAULT` fires. See the `bCurrentEncUTF8` block. -**`Encoding_MapIniSetting` direction** — CPI constants (`CPI_UTF8=6`, `CPI_OEM=1`, etc.) do NOT equal their INI storage values (`3`, `5`, etc.). The mapping is asymmetric: -- `Encoding_MapIniSetting(true, val)` = INI integer → CPI constant (**loading**) -- `Encoding_MapIniSetting(false, val)` = CPI constant → INI integer (**saving**) +### Creating directories -Passing a CPI value with `bLoad=true` produces wrong results. Reference: `MRU_Save()` correctly uses `false` for saving. +`Path_CreateDirectoryEx(hpth)` wraps `SHCreateDirectoryExW`. Success: `SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))`. Reference: `CreateIniFile()` in `Config.cpp`. -**Conditional defaults and `SAVE_VALUE_IF_NOT_EQ_DEFAULT`** — Some settings have defaults that depend on `DefaultEncoding` (e.g., `SkipANSICodePageDetection`, `LoadASCIIasUTF8`). If the encoding can change at runtime (via the Encoding dialog), the dependent `Defaults.*` fields must be recalculated in `_SaveSettings()` before the comparison macros fire — otherwise stale defaults cause incorrect save/delete decisions. See the `bCurrentEncUTF8` block in `_SaveSettings()`. +### Long-path / PathLib wrappers -### Creating Directories +Never call Win32 file APIs directly with `Path_Get(hpth)`. Use PathLib wrappers — they apply `\\?\` prefix conditionally (only when `RtlAreLongPathsEnabled()` is false AND path ≥ 260 chars): -Use `Path_CreateDirectoryEx(hpth)` (PathLib wrapper around `SHCreateDirectoryExW`) to recursively create directory trees. Check result: `SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))`. See `CreateIniFile()` in `src\Config\Config.cpp` for the reference pattern. +| Wrapper | Win32 | +|---------|-------| +| `Path_CreateFile` | `CreateFileW` | +| `Path_DeleteFile` | `DeleteFileW` | +| `Path_GetFileAttributes` / `Path_GetFileAttributesEx` | `GetFileAttributes[Ex]W` | +| `Path_SetFileAttributes` | `SetFileAttributesW` | +| `Path_ReplaceFile` | `ReplaceFileW` | +| `Path_MoveFileEx` | `MoveFileExW` | +| `Path_FindFirstFile` | `FindFirstFileW` | +| `Path_CreateDirectoryEx` | `SHCreateDirectoryExW` | +| `Path_IsExistingFile` / `Path_IsExistingDirectory` | `GetFileAttributesW` + check | -### Long-Path Support / Win32 File API Wrappers +### Path comparison -Notepad3 supports paths exceeding `MAX_PATH` (260 chars) on systems without the Windows 10 long-path registry opt-in by conditionally prepending the `\\?\` prefix. The static function `PrependLongPathPrefix()` in PathLib.c handles this. - -**Rule: Never call Win32 file APIs directly with `Path_Get(hpth)`.** Use the PathLib wrapper functions instead — they internally apply the copy-prefix-call-release pattern: - -| Wrapper | Win32 API | -|---------|-----------| -| `Path_CreateFile(hpth, ...)` | `CreateFileW` | -| `Path_DeleteFile(hpth)` | `DeleteFileW` | -| `Path_GetFileAttributes(hpth)` | `GetFileAttributesW` | -| `Path_GetFileAttributesEx(hpth, ...)` | `GetFileAttributesExW` | -| `Path_SetFileAttributes(hpth, ...)` | `SetFileAttributesW` | -| `Path_ReplaceFile(hpth_dest, src)` | `ReplaceFileW` | -| `Path_MoveFileEx(src, hpth_dest, ...)` | `MoveFileExW` | -| `Path_FindFirstFile(hpth, ...)` | `FindFirstFileW` | -| `Path_CreateDirectoryEx(hpth)` | `SHCreateDirectoryExW` | -| `Path_IsExistingFile(hpth)` | `GetFileAttributesW` + check | -| `Path_IsExistingDirectory(hpth)` | `GetFileAttributesW` + check | - -The prefix is only added when `RtlAreLongPathsEnabled()` returns false AND the path is ≥ 260 chars. On modern Windows 10+ with the opt-in, these wrappers are effectively pass-through. - -### Path Comparison - -Use `Path_StrgComparePath()` for comparing file paths — it supports optional normalization and uses `CompareStringOrdinal` (locale-independent, case-insensitive). For raw wide-string path comparison, use `CompareStringOrdinal(s1, -1, s2, -1, TRUE)` instead of `_wcsicmp` or `_wcsnicmp` which are locale-dependent. +Use `Path_StrgComparePath()` (normalization + `CompareStringOrdinal`, locale-independent, case-insensitive). For raw wide strings: `CompareStringOrdinal(s1, -1, s2, -1, TRUE)`. **Never** `_wcsicmp` / `_wcsnicmp` (locale-dependent). ### Undo/Redo transactions -Use `_BEGIN_UNDO_ACTION_` / `_END_UNDO_ACTION_` macros (defined in `Notepad3.h`) to group Scintilla operations into single undo steps. These also handle notification limiting during bulk edits. +Use `_BEGIN_UNDO_ACTION_` / `_END_UNDO_ACTION_` macros (`Notepad3.h`) for grouping; they also throttle notifications during bulk edits. -### WriteAccessBuf — Dangling Pointer Anti-Pattern +### WriteAccessBuf — dangling pointer anti-pattern -**NEVER use a pointer obtained from `Path_WriteAccessBuf()` / `StrgWriteAccessBuf()` after ANY operation that may reallocate or swap the underlying buffer of the SAME handle.** The pointer becomes dangling (use-after-free). +**NEVER** use a pointer from `Path_WriteAccessBuf()` / `StrgWriteAccessBuf()` after any operation that may reallocate/swap the same handle's buffer: `Path_CanonicalizeEx`, `Path_Swap` / `StrgSwap`, `Path_ExpandEnvStrings` / `ExpandEnvironmentStrgs`, `Path_Append` / `Path_Reset`, `StrgCat` / `StrgInsert` / `StrgFormat` / `StrgReset`, `Path_NormalizeEx` / `Path_AbsoluteFromApp` / `Path_RelativeToApp`. -Buffer-invalidating operations: -- `Path_CanonicalizeEx(h, ...)` — calls `Path_Swap` internally -- `Path_Swap(h, ...)` / `StrgSwap(h, ...)` -- `Path_ExpandEnvStrings(h)` / `ExpandEnvironmentStrgs(h, ...)` — may realloc -- `Path_Append(h, ...)` / `Path_Reset(h, ...)` — may realloc -- `StrgCat(h, ...)` / `StrgInsert(h, ...)` / `StrgFormat(h, ...)` / `StrgReset(h, ...)` — may realloc -- `Path_NormalizeEx(h, ...)` / `Path_AbsoluteFromApp(h, ...)` / `Path_RelativeToApp(h, ...)` — may realloc/swap - -Safe patterns after invalidation: -- **Read-only**: use `Path_Get(h)` or `StrgGet(h)` — always returns current buffer -- **Read-write**: re-obtain via `ptr = Path_WriteAccessBuf(h, 0)` / `ptr = StrgWriteAccessBuf(h, 0)` (size 0 = no resize, just returns current pointer) +After any of these: +- Read-only: `Path_Get(h)` / `StrgGet(h)`. +- Read-write: re-obtain via `Path_WriteAccessBuf(h, 0)` / `StrgWriteAccessBuf(h, 0)` (size 0 = no resize). diff --git a/Build/Notepad3.ini b/Build/Notepad3.ini index 4c01e6a1d..1e64febc7 100644 --- a/Build/Notepad3.ini +++ b/Build/Notepad3.ini @@ -28,7 +28,7 @@ SettingsVersion=5 ;NoCopyLineOnEmptySelection=0 ;NoCutLineOnEmptySelection=0 ;CopyMultiSelectionSeparator= ;(-> ) {separator between multi-selection copies; empty=no separator; supports escape sequences: \r\n=CRLF, \n=LF, \t=tab, \xHH=hex} -;PasteBoardSeparator= ;(-> ) {separator inserted verbatim between entries in pasteboard mode; empty=no separator; supports \r\n, \n, \t, \xHH; include newlines explicitly e.g. "\r\n---\r\n" for a dashed separator line} +;PasteBoardSeparator= ;(-> ) {separator pre-pended before each new clipboard entry (pasted at caret) in pasteboard mode; suppressed on first paste after enable and when caret is at a line start; empty=no separator; supports \r\n, \n, \t, \xHH; include newlines explicitly e.g. "\r\n---\r\n" for a dashed separator line} ;PasteBoardDebounceMs=200 ;(min: 0, max: 5000[msec]) {debounce interval for clipboard monitoring} ;PasteBoardAddTimestamp=0 ;(0/1) {prepend [HH:MM:SS] timestamp to each pasted entry in pasteboard mode} ;NoFadeHidden=0 diff --git a/CLAUDE.md b/CLAUDE.md index fb5cde470..81d15a1ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,354 +1,192 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +Guidance for Claude Code working in this repo. Read once per session; covers non-obvious mechanics and gotchas. Self-evident build/layout information is not duplicated here. -## Project Overview +## Project -Notepad3 is a Windows-only Win32 desktop text editor (C/C++) built on **Scintilla** (editing component) and **Lexilla** (syntax highlighting). It ships with the companion tool **MiniPath** (file browser, Ctrl+M) and integrates with the external **grepWin** tool (file search/grep, Ctrl+Shift+F) via pre-built portable executables. Licensed under BSD 3-Clause. +Notepad3 — Win32 C/C++ text editor on Scintilla/Lexilla. Ships with MiniPath (`Ctrl+M`) and integrates external grepWin (`Ctrl+Shift+F`) via pre-built portable exes. BSD-3. Windows-only. -## Build Commands +## Build -```powershell -# NuGet restore (required before first build) -nuget restore Notepad3.sln +Build scripts live in `Build\` (PowerShell under `Build\scripts\`): -# Single platform builds -Build\Build_x64.cmd [Release|Debug] -Build\Build_Win32.cmd [Release|Debug] -Build\Build_ARM64.cmd [Release|Debug] -Build\Build_x64_AVX2.cmd [Release|Debug] +- `Build\Build_x64.cmd [Release|Debug]` — single platform (also `_Win32`, `_ARM64`, `_x64_AVX2`) +- `Build\BuildAll.cmd` — all platforms +- `msbuild Notepad3.sln /m /p:Configuration=Release /p:Platform=x64` — CI equivalent +- `Build\Clean.cmd` — clean outputs +- Run `nuget restore Notepad3.sln` once before first build. +- Run `Version.ps1` before building to regenerate `src\VersionEx.h` (format `Major.YY.Mdd.Build`; build number in `Versions\build.txt`). +- Tests: `test\TestFileVersion.cmd`, `test\TestAhkNotepad3.cmd` (needs AutoHotkey). CI matrix in `.github/workflows/build.yml` (windows-2022). -# All platforms at once -Build\BuildAll.cmd [Release|Debug] +Default configuration is Release. -# MSBuild directly (used by CI) -msbuild Notepad3.sln /m /p:Configuration=Release /p:Platform=x64 - -# Clean all outputs -Build\Clean.cmd -``` - -Default configuration is Release. Build scripts delegate to PowerShell in `Build\scripts\`. - -### Versioning - -Run `Version.ps1` before building to generate `src\VersionEx.h` from templates in `Versions\`. Format: `Major.YY.Mdd.Build` (build number persisted in `Versions\build.txt`). - -### Tests - -```cmd -cd test -TestFileVersion.cmd # Verifies built binary version info -TestAhkNotepad3.cmd # AutoHotkey-based GUI tests (requires AutoHotkey) -``` - -### CI - -GitHub Actions (`.github/workflows/build.yml`) builds all four platforms (Win32, x64, x64_AVX2, ARM64) in Release on `windows-2022` runners, triggered on push/PR to master. - -## Architecture - -### Core Modules (`src\`) +## Code Map (`src\`) | File | Purpose | |------|---------| -| **Notepad3.c/h** | Entry point (`wWinMain`), window procedure (`MainWndProc`), global state structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`), `MsgCommand()` dispatcher | -| **Notepad3Util.c/h** | Utility functions extracted from Notepad3.c: bitmap/toolbar image loading, word-wrap configuration, auto-scroll (middle-click continuous scroll) | -| **Edit.c/h** | Text manipulation: find/replace (PCRE2 regex), encoding conversion, clipboard, indentation, sorting, bookmarks, folding, auto-complete | -| **Styles.c/h** | Scintilla styling, lexer selection, theme management, margin configuration | -| **Dialogs.c/h** | All dialog boxes, DPI-aware UI interactions, window placement | -| **Config/Config.cpp/h** | INI file management, settings loading/saving, MRU list | -| **Encoding.c/h** | Encoding detection and conversion (integrates uchardet) | -| **SciCall.h** | Type-safe inline wrappers for Scintilla direct function calls (avoids `SendMessage` overhead) | -| **DynStrg.c/h** | Custom dynamic wide-string type (`HSTRINGW`) with automatic buffer management | -| **PathLib.c/h** | Path manipulation via opaque `HPATHL` handle | -| **TypeDefs.h** | Core type definitions (`DocPos`, `DocLn`, `cpi_enc_t`), Windows version targeting, compiler macros | -| **MuiLanguage.c/h** | Multi-language UI support, language DLL loading | +| `Notepad3.c/h` | `wWinMain`, `MainWndProc`, global state structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`), `MsgCommand()` dispatcher | +| `Notepad3Util.c/h` | Image/toolbar helpers, word-wrap config, middle-click auto-scroll | +| `Edit.c/h` | Find/replace (PCRE2), encoding, clipboard, indent, sort, bookmarks, folding, autocomplete | +| `Styles.c/h` | Scintilla styling, lexer selection, themes, margins | +| `Dialogs.c/h` | Dialogs, DPI-aware UI, window placement | +| `Config/Config.cpp/h` | INI load/save, MRU | +| `Encoding.c/h` | Encoding detection/conversion (wraps uchardet) | +| `SciCall.h` | Type-safe wrappers for Scintilla direct calls (avoid `SendMessage`) | +| `DynStrg.c/h` | `HSTRINGW` dynamic wide-string handle | +| `PathLib.c/h` | `HPATHL` path handle + long-path-aware Win32 wrappers | +| `TypeDefs.h` | `DocPos`, `DocLn`, `cpi_enc_t`, OS targeting, compiler macros | +| `MuiLanguage.c/h` | MUI language DLL loading | +| `StyleLexers\styleLexXXX.c` | Per-language `EDITLEXER` definitions (~50 files) | -### Window Hierarchy +### Menu / Command Dispatch -``` -MainWndProc (Notepad3.c) - +-- Scintilla Edit Control (hwndEdit / IDC_EDIT) - +-- Toolbar (via Rebar control) - +-- Status Bar (16 configurable fields) -``` - -### Menu / Command Architecture - -Menu items are defined in resource files (`language/np3_*/menu_*.rc`). Command handling in `Notepad3.c` is structured as: - -- **`MsgInitMenu()`** — `WM_INITMENU` handler; enables/disables/checks menu items based on current state -- **`MsgCommand()`** — `WM_COMMAND` thin dispatcher; handles timer/notification cases inline, then delegates to static sub-handlers: +- `MsgInitMenu()` (`WM_INITMENU`) — enable/check state. +- `MsgCommand()` (`WM_COMMAND`) — thin dispatcher delegating to static sub-handlers, each returning `true` if it handled: | Handler | Scope | |---------|-------| -| `_HandleFileCommands` | File open/save/print/favorites (`IDM_FILE_*`) | -| `_HandleEncodingCommands` | Encoding & line endings (`IDM_ENCODING_*`, `IDM_LINEENDINGS_*`) | -| `_HandleEditBasicCommands` | Undo/redo/cut/copy/paste/indent (`IDM_EDIT_UNDO`..`CMD_VK_INSERT`) | -| `_HandleEditLineManipulation` | Line modify/sort/join/case (`IDM_EDIT_ENCLOSESELECTION`..`IDM_EDIT_INSERT_GUID`) | -| `_HandleEditTextTransform` | Comments/URL encode/escape/hex (`IDM_EDIT_LINECOMMENT`..`IDM_EDIT_HEX2CHAR`) | -| `_HandleEditFind` | Find/replace/bookmarks/goto (`IDM_EDIT_FINDMATCHINGBRACE`..`IDM_EDIT_GOTOLINE`) | -| `_HandleViewAndSettingsCommands` | View/settings/rendering (`IDM_VIEW_*`, `IDM_SET_*`) | -| `_HandleHelpCommands` | Help/about (`IDM_HELP_*`) | -| `_HandleCmdCommands` | Keyboard shortcuts/navigation/window positioning (`CMD_*`) | -| `_HandleToolbarCommands` | Toolbar button dispatch via `s_ToolbarDispatch[]` table (`IDT_*`) | +| `_HandleFileCommands` | `IDM_FILE_*` | +| `_HandleEncodingCommands` | `IDM_ENCODING_*`, `IDM_LINEENDINGS_*` | +| `_HandleEditBasicCommands` | `IDM_EDIT_UNDO`..`CMD_VK_INSERT` | +| `_HandleEditLineManipulation` | `IDM_EDIT_ENCLOSESELECTION`..`IDM_EDIT_INSERT_GUID` | +| `_HandleEditTextTransform` | `IDM_EDIT_LINECOMMENT`..`IDM_EDIT_HEX2CHAR` | +| `_HandleEditFind` | `IDM_EDIT_FINDMATCHINGBRACE`..`IDM_EDIT_GOTOLINE` | +| `_HandleViewAndSettingsCommands` | `IDM_VIEW_*`, `IDM_SET_*` | +| `_HandleHelpCommands` | `IDM_HELP_*` | +| `_HandleCmdCommands` | `CMD_*` | +| `_HandleToolbarCommands` | `IDT_*` via `s_ToolbarDispatch[]` | -Each handler returns `true` if it handled the command, `false` to try the next. All handlers are `static` in `Notepad3.c`. +### Clipboard Monitoring (Pasteboard Mode) -### Vendored Dependencies +Runtime-toggleable; external clipboard changes are pasted at the caret. -| Directory | Library | Purpose | -|-----------|---------|---------| -| `scintilla\` | Scintilla 5.5.8 | Editor component (NP3 patches in `np3_patches\`, docs in `doc\`) | -| `lexilla\` | Lexilla 5.4.6 | Syntax highlighting (NP3 patches in `np3_patches\`, docs in `doc\`) | -| `scintilla\pcre2\` | PCRE2 10.47 | Regex engine for find/replace (replaced archived Oniguruma) | -| `src\uchardet\` | uchardet | Mozilla encoding detection | -| `src\tinyexpr\` / `src\tinyexprcpp\` | TinyExpr | Expression evaluator (statusbar) | -| `src\uthash\` | uthash | Hash table / dynamic array macros | -| `src\crypto\` | Rijndael/SHA-256 | AES-256 encryption | +- Menu: `IDM_EDIT_TOGGLE_PASTEBOARD` — "Toggle Clipboard Monitoring". Check mark reflects state. +- Helpers `PasteBoard_Start(HWND)` / `PasteBoard_Stop(HWND)` wrap `AddClipboardFormatListener` + `ID_PASTEBOARDTIMER`. Used at startup for `/B` and from the toggle handler. +- **Not persisted** — always OFF at startup unless `/B`. +- **Mutex with Tail** (`IDM_VIEW_CHASING_DOCTAIL` / `FileWatching.MonitoringLog`): each mode greys the other in `MsgInitMenu`; tail toolbar button (`IDT_VIEW_CHASING_DOCTAIL`) also greyed; "Monitoring Log" checkbox in `ChangeNotifyDlgProc` greyed while pasteboard active. `IsPasteBoardActive()` exposed in `Notepad3.h` for cross-TU use. +- Startup conflict (`/B` + persisted `MonitoringLog=true`): `/B` wins this session; `FileWatching.MonitoringLog` cleared in memory, `Settings.MonitoringLog` (INI) preserved. +- `PasteBoardTimerProc` pastes at current caret. `Settings2.PasteBoardSeparator` pre-pended before each new entry; suppressed on (1) first paste after enable and (2) caret at line start. `\x01` = one document EOL; `\0` = no separator. +- Status bar `STATUS_OVRMODE` shows `CBS` while active (passive indicator). -### grepWin Integration (`grepWin\`) +## Vendored Libraries -grepWin is an **external** file search/grep tool — it is **not** built from source as part of Notepad3. Pre-built portable executables and translation files are stored in the repository: +`scintilla\` (5.5.8), `lexilla\` (5.4.6), `scintilla\pcre2\` (PCRE2 10.47), `src\uchardet\`, `src\tinyexpr\` / `src\tinyexprcpp\`, `src\uthash\`, `src\crypto\` (Rijndael/SHA-256). NP3 patches under each `np3_patches\`; offline docs under `scintilla\doc\` and `lexilla\doc\`. -- **`grepWin\portables\`** — `grepWin-x86_portable.exe`, `grepWin-x64_portable.exe`, `LICENSE.txt` -- **`grepWin\translations\`** — `*.lang` translation files (e.g. `German.lang`, `French.lang`) +## grepWin Integration (`grepWin\`) -At runtime (`src\Dialogs.c`), Notepad3 searches for grepWin in this order: -1. `Settings2.GrepWinPath` (user-configured INI setting) -2. `\grepWin\grepWin-x64_portable.exe` (or x86) — portable layout -3. `%APPDATA%\Rizonesoft\Notepad3\grepWin\` — installed layout +External tool, **not built from source**. Pre-built exes under `grepWin\portables\`; `.lang` files under `grepWin\translations\`. Runtime lookup order (in `src\Dialogs.c`): `Settings2.GrepWinPath` → `\grepWin\grepWin-x{64,86}_portable.exe` → `%APPDATA%\Rizonesoft\Notepad3\grepWin\`. `grepWinLangResName[]` in `MuiLanguage.c` maps Notepad3 locales → `.lang` filenames. ARM64 uses the x64 exe via emulation. -Language mapping (`src\MuiLanguage.c`): `grepWinLangResName[]` maps Notepad3 locale names (e.g. `de-DE`) to grepWin `.lang` filenames (e.g. `German.lang`). The language file path is written to `grepwin.ini` before launching. +## Adding a Lexer -Portable build scripts (`Build\make_portable_*.cmd`) package grepWin into a `grepWin\` subdirectory in the archive containing both portable executables, the license, and all `*.lang` translations. +`styleLexXXX.c` defines an `EDITLEXER` (see any existing file for the struct), register in the `Styles.c` lexer array, add localization string IDs. -### Syntax Lexers (`src\StyleLexers\`) +## Localization -50+ languages, each in a `styleLexXXX.c` file. All follow the `EDITLEXER` struct pattern from `EditLexer.h`: +- 26 locales under `language\np3_LANG_COUNTRY\`. Language DLLs are separate projects in the solution. +- **`.rc` files are UTF-8 WITHOUT BOM, CRLF line endings.** Never write with BOM. Use `Build\rc_to_utf8.cmd` to strip accidental BOMs. In PowerShell use `[System.Text.UTF8Encoding]::new($false)`; in Python write `\r\n` explicitly. +- **`Build\Notepad3.ini`, `Build\minipath.ini` are UTF-8 WITH BOM** (`EF BB BF`). Preserve it. -```c -EDITLEXER lexXXX = { - SCLEX_XXX, // Scintilla lexer ID - "lexerName", // Lexilla lexer name (case-sensitive) - IDS_LEX_XXX_STR, // Resource string ID - L"Config Name", // INI section name - L"ext1; ext2", // Default file extensions - L"", // Extension buffer (runtime) - &KeyWords_XXX, // Keyword lists - { /* EDITSTYLE array */ } -}; -``` +### Adding a string resource -To add a new lexer: create `styleLexNEW.c`, define the `EDITLEXER` struct, register in `Styles.c` lexer array, add localization string IDs to resource files. +1. `#define IDS_MUI_XXX ` in `language\common_res.h` (13xxx errors/warnings, 14xxx info/prompts). +2. Add the English string to `language\np3_en_us\strings_en_us.rc`. +3. Add the same English text as placeholder to all other 25 locale files (translators update later). +4. Display via `InfoBoxLng()` / `MessageBoxLng()`; check with `IsYesOkay()`. `Settings.MuteMessageBeep` controls silent vs. sound — provide both paths. -### Localization (`language\`) +## Portable INI Design -Resource-based MUI system with 27+ locales. Each locale has a `np3_LANG_COUNTRY\` directory with `.rc` files. Language packs are built as separate DLLs (separate projects in the solution). +- INI sits beside the exe. No registry. Runs on defaults if no INI exists (user creates via "Save Settings Now"). +- **Admin redirect**: `Notepad3.ini=` in `[Notepad3]` redirects to a per-user path (up to 2 levels). Redirect targets ARE auto-created via `CreateIniFileEx()`. +- `Paths.IniFile` = active writable INI; `Paths.IniFileDefault` = recovery fallback. +- Init flow: `FindIniFile()` → `TestIniFile()` → `CreateIniFile()` → `LoadSettings()`. +- **`SettingsVersion` defaults to `CFG_VER_CURRENT`** when missing — empty/new INI gets current defaults, not legacy treatment. +- **`bIniFileFromScratch`** is set when INI is 0 bytes, cleared after `SaveAllSettings()`. While set, `MuiLanguage.c` suppresses writing `PreferredLanguageLocaleName` to keep fresh INIs clean. +- **Empty lexer sections are pruned** in `Style_ToIniSection()` via `IniSectionGetKeyCount()` after `Style_CanonicalSectionToIniCache()` establishes order — only sections with non-default styles persist. +- MiniPath follows the same pattern (`minipath\src\Config.cpp`). +- **New `Settings2` (or other INI) params must be documented as commented entries in `Build\Notepad3.ini`.** -### Adding String Resources +### Save/Load macros (`src\Config\Config.cpp`) -1. Add `#define IDS_MUI_XXX ` to `language\common_res.h` (use next available ID in the appropriate range: 13xxx for errors/warnings, 14xxx for info/prompts) -2. Add the English string to `language\np3_en_us\strings_en_us.rc` in the matching `STRINGTABLE` block -3. Add the same English text as placeholder to all other 25 locale `strings_*.rc` files (translators update later) -4. Use `InfoBoxLng()` / `MessageBoxLng()` with `MB_YESNO`, `MB_ICONWARNING`, etc. to display; check result with `IsYesOkay()` -5. `Settings.MuteMessageBeep` controls whether to use `InfoBoxLng` (silent) or `MessageBoxLng` (with sound) — always provide both paths +- **`Encoding_MapIniSetting` is asymmetric.** CPI constants (`CPI_UTF8=6`, `CPI_OEM=1`, …) don't equal INI values (`3`, `5`, …). `Encoding_MapIniSetting(true, val)` = INI→CPI (load); `(false, val)` = CPI→INI (save). Passing CPI with `bLoad=true` produces wrong results. Reference: `MRU_Save()` uses `false`. +- **Defaults that depend on `DefaultEncoding`** (e.g. `SkipANSICodePageDetection`, `LoadASCIIasUTF8`) must be recalculated in `_SaveSettings()` before `SAVE_VALUE_IF_NOT_EQ_DEFAULT` fires, since encoding can change at runtime. See the `bCurrentEncUTF8` block. -### Configuration / Portable Design +## File I/O -- INI file alongside executable (`Notepad3.ini`), no registry usage -- **No auto-creation**: runs with defaults if no INI exists; user explicitly creates via "Save Settings Now" -- **Admin redirect**: `Notepad3.ini=` in `[Notepad3]` section redirects to per-user path (up to 2 levels) -- Key paths: `Paths.IniFile` (active writable INI), `Paths.IniFileDefault` (fallback for recovery) -- INI init flow: `FindIniFile()` -> `TestIniFile()` -> `CreateIniFile()` -> `LoadSettings()` -- **SettingsVersion default**: `SettingsVersion` defaults to `CFG_VER_CURRENT` when missing from INI — an INI file without `SettingsVersion` is NOT treated as a legacy (pre-versioning) file. This ensures empty INI files and newly created INI files both get current defaults. -- **`bIniFileFromScratch`**: Set when INI file size is 0 bytes. Cleared after `SaveAllSettings()` completes. While true, `MuiLanguage.c` suppresses writing `PreferredLanguageLocaleName` to avoid polluting fresh INI files. -- **Style section saving**: `Style_ToIniSection()` removes empty lexer sections after processing via `IniSectionGetKeyCount()`. `Style_CanonicalSectionToIniCache()` establishes canonical section order but empty sections are cleaned up — only sections with non-default style values appear in the saved INI. -- **MiniPath** follows the same portable INI and admin-redirect pattern (`minipath\src\Config.cpp`). Redirect targets are auto-created via `CreateIniFileEx()`. -- **New parameters**: When adding new `Settings2` (or other INI) parameters, always document them as commented entries in `Build\Notepad3.ini` +- `FileSave()` / `FileLoad()` (in `Notepad3.c`) → `FileIO()` → `EditSaveFile()` / `EditLoadFile()` (in `Edit.c`). +- Atomic save via temp file + `ReplaceFileW` controlled by `Settings2.AtomicFileSave`. +- Error codes land in `Globals.dwLastError`; check `ERROR_ACCESS_DENIED`, `ERROR_PATH_NOT_FOUND` before falling back to generic error. +- **File watching** (`InstallFileWatching()`) uses `FindFirstChangeNotificationW` on the parent directory. Must `InstallFileWatching(false)` before save and `InstallFileWatching(true)` after. -### Settings Save/Load Macros (`src\Config\Config.cpp`) +## Long-Path / PathLib wrappers -**`Encoding_MapIniSetting` direction** — CPI constants (`CPI_UTF8=6`, `CPI_OEM=1`, etc.) do NOT equal their INI storage values (`3`, `5`, etc.). The mapping is asymmetric: -- `Encoding_MapIniSetting(true, val)` = INI integer → CPI constant (**loading**) -- `Encoding_MapIniSetting(false, val)` = CPI constant → INI integer (**saving**) +Never call Win32 file APIs directly with `Path_Get(hpth)`. Use the PathLib wrappers — they apply the `\\?\` prefix conditionally (only when `RtlAreLongPathsEnabled()` is false AND path ≥ 260 chars): -Passing a CPI value with `bLoad=true` produces wrong results. Reference: `MRU_Save()` correctly uses `false` for saving. +| Wrapper | Win32 | +|---------|-------| +| `Path_CreateFile` | `CreateFileW` | +| `Path_DeleteFile` | `DeleteFileW` | +| `Path_GetFileAttributes` / `Path_GetFileAttributesEx` | `GetFileAttributes[Ex]W` | +| `Path_SetFileAttributes` | `SetFileAttributesW` | +| `Path_ReplaceFile` | `ReplaceFileW` | +| `Path_MoveFileEx` | `MoveFileExW` | +| `Path_FindFirstFile` | `FindFirstFileW` | +| `Path_CreateDirectoryEx` | `SHCreateDirectoryExW` | +| `Path_IsExistingFile` / `Path_IsExistingDirectory` | `GetFileAttributesW` + check | -**Conditional defaults and `SAVE_VALUE_IF_NOT_EQ_DEFAULT`** — Some settings have defaults that depend on `DefaultEncoding` (e.g., `SkipANSICodePageDetection`, `LoadASCIIasUTF8`). If the encoding can change at runtime (via the Encoding dialog), the dependent `Defaults.*` fields must be recalculated in `_SaveSettings()` before the comparison macros fire — otherwise stale defaults cause incorrect save/delete decisions. See the `bCurrentEncUTF8` block in `_SaveSettings()`. +### Path comparison -### Creating Directories +Use `Path_StrgComparePath()` (supports normalization; `CompareStringOrdinal` under the hood — locale-independent, case-insensitive). For raw wide strings use `CompareStringOrdinal(s1, -1, s2, -1, TRUE)`; **never** `_wcsicmp` / `_wcsnicmp` (locale-dependent). -Use `Path_CreateDirectoryEx(hpth)` (PathLib wrapper around `SHCreateDirectoryExW`) to recursively create directory trees. Check result: `SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))`. See `CreateIniFile()` in `src\Config\Config.cpp` for the reference pattern. +### Creating directories -### Long-Path Support / Win32 File API Wrappers +Use `Path_CreateDirectoryEx(hpth)`. Success: `SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))`. Reference pattern in `CreateIniFile()` (`Config.cpp`). -Notepad3 supports paths exceeding `MAX_PATH` (260 chars) on systems without the Windows 10 long-path registry opt-in by conditionally prepending the `\\?\` prefix. The static function `PrependLongPathPrefix()` in PathLib.c handles this. +## PCRE2 -**Rule: Never call Win32 file APIs directly with `Path_Get(hpth)`.** Use the PathLib wrapper functions instead — they internally apply the copy-prefix-call-release pattern: +`scintilla\pcre2\scintilla\PCRE2RegExEngine.cxx` compiled with `SCI_OWNREGEX` overrides Scintilla's built-in regex. Entry points: `FindText`, `SubstituteByPosition`, `convertReplExpr` (normalizes `\1`-`\9`→`$1`-`$9`, processes `\n\t\xHH\uHHHH`), `translateRegExpr` (`\<`/`\>` → lookarounds, `\uHHHH` → `\x{HHHH}`). Standalone `RegExFind` (exported C) used by `EditURLDecode`. -| Wrapper | Win32 API | -|---------|-----------| -| `Path_CreateFile(hpth, ...)` | `CreateFileW` | -| `Path_DeleteFile(hpth)` | `DeleteFileW` | -| `Path_GetFileAttributes(hpth)` | `GetFileAttributesW` | -| `Path_GetFileAttributesEx(hpth, ...)` | `GetFileAttributesExW` | -| `Path_SetFileAttributes(hpth, ...)` | `SetFileAttributesW` | -| `Path_ReplaceFile(hpth_dest, src)` | `ReplaceFileW` | -| `Path_MoveFileEx(src, hpth_dest, ...)` | `MoveFileExW` | -| `Path_FindFirstFile(hpth, ...)` | `FindFirstFileW` | -| `Path_CreateDirectoryEx(hpth)` | `SHCreateDirectoryExW` | -| `Path_IsExistingFile(hpth)` | `GetFileAttributesW` + check | -| `Path_IsExistingDirectory(hpth)` | `GetFileAttributesW` + check | +Replacement backref syntax: `$0`-`$99`, `\0`-`\9`, `${name}`, `${+name}`. -The prefix is only added when `RtlAreLongPathsEnabled()` returns false AND the path is ≥ 260 chars. On modern Windows 10+ with the opt-in, these wrappers are effectively pass-through. +URL hotspot regex: `HYPLNK_REGEX_FULL` macro in `src\Edit.c` — matches `https?://`, `ftp://`, `file:///`, `file://`, `mailto:`, `www.`, `ftp.`. Trailing group excludes `.,:?!` so URLs don't absorb sentence punctuation. -### Path Comparison +## DarkMode -Use `Path_StrgComparePath()` for comparing file paths — it supports optional normalization and uses `CompareStringOrdinal` (locale-independent, case-insensitive). For raw wide-string path comparison, use `CompareStringOrdinal(s1, -1, s2, -1, TRUE)` instead of `_wcsicmp` or `_wcsnicmp` which are locale-dependent. +`src\DarkMode\` — Windows 10/11 dark mode via IAT hooks on uxtheme/user32 (stub DLLs included). -### File I/O Flow +## ARM64 -- **`FileSave()`** (`src\Notepad3.c`) — Main save dispatcher. Handles Save, Save As, Save Copy. - Calls `FileIO()` → `EditSaveFile()` (`src\Edit.c`) -- **`FileLoad()`** (`src\Notepad3.c`) — Main load dispatcher. - Calls `FileIO()` → `EditLoadFile()` (`src\Edit.c`) -- **`EditSaveFile()`** supports atomic save (temp file + `ReplaceFileW`) controlled by `Settings2.AtomicFileSave` -- **Error handling**: `Globals.dwLastError` holds the Win32 error code after failed I/O. - `FileSave()` checks specific codes (`ERROR_ACCESS_DENIED`, `ERROR_PATH_NOT_FOUND`) before falling back to generic error. -- **File watching**: `InstallFileWatching()` uses `FindFirstChangeNotificationW` on the parent directory. - Must be stopped before save (`InstallFileWatching(false)`) and restarted after (`InstallFileWatching(true)`). +Supported: Win32 (x86), x64, x64_AVX2, ARM64. **ARM 32-bit is not supported** — the `Release|ARM` solution config maps to Win32. -### PCRE2 Regex Engine (`scintilla\pcre2\`) +- Both ARM64 and x64 define `_WIN64`. Use `_M_ARM64` (or helper `NP3_BUILD_ARM64` in `TypeDefs.h`) to distinguish. +- Rendering default: `SC_TECHNOLOGY_DIRECTWRITERETAIN` (2) instead of `SC_TECHNOLOGY_DIRECTWRITE` (1) — preserves Direct2D back buffer, avoids flicker on Qualcomm Adreno + Win11 25H2 DWM. Main window also gets `WS_EX_COMPOSITED`. User can override via `RenderingTechnology` / View menu. +- Build config: `CETCompat=false` (CET is x86/x64 only), `TargetMachine=MachineARM64`, `_WIN64` defined. Fix scripts: `Build\scripts\FixARM64{CETCompat,CrossCompile,OutDir}.ps1`. +- grepWin: no native ARM64 — uses x64 exe via emulation (`#if defined(_M_ARM64)` in `Notepad3.c`). +- `MsgThemeChanged()` wraps bar recreate / lexer reset / restyle in `WM_SETREDRAW FALSE/TRUE` and does a single `RedrawWindow()` at end. DarkMode `RedrawWindow()` in `ListViewUtil.hpp` omits `RDW_ERASE` to avoid background flash. -PCRE2 10.47 replaced the archived Oniguruma library. The Scintilla integration lives in `scintilla\pcre2\scintilla\PCRE2RegExEngine.cxx`, compiled with `SCI_OWNREGEX` to override Scintilla's built-in regex. +## Conventions -Key components: -- **`PCRE2RegExEngine::FindText`** — Scintilla regex search (pattern matching via `pcre2_match`) -- **`PCRE2RegExEngine::SubstituteByPosition`** — Regex replacement with group references -- **`PCRE2RegExEngine::convertReplExpr`** — Normalizes replacement strings: converts `\1`-`\9` to `$1`-`$9`, processes escape sequences (`\n`, `\t`, `\xHH`, `\uHHHH`) -- **`PCRE2RegExEngine::translateRegExpr`** — Translates Scintilla regex extensions: `\<`/`\>` word boundaries → lookarounds, `\uHHHH` → `\x{HHHH}` -- **`RegExFind`** (exported C function) — Standalone regex find used by `EditURLDecode` in `Edit.c`; wraps `SimplePCRE2Engine` +- **Formatting**: LLVM-based `.clang-format` in `src\` — 4-space indent, Stroustrup braces, left-aligned pointers, no column limit, no include sorting. `.editorconfig` enforces UTF-8/CRLF; Lexilla code uses tabs (upstream preserved). +- **Strings**: `strsafe.h` throughout; deprecated string functions disabled. +- **Types**: `DocPos` / `DocPosU` / `DocLn` (not raw `int`). `cpi_enc_t` for encodings. `HSTRINGW` / `HPATHL` (opaque) instead of raw `WCHAR*`. `NOMINMAX` is global — use `min()`/`max()` or typed equivalents. +- **Scintilla**: always use `SciCall.h` wrappers. Add missing wrappers there. Naming: `DeclareSciCall{V|R}{0|01|1|2}` — V=void, R=return; 0/1/2 = param count; `01` = lParam-only. The `msg` arg is the suffix after `SCI_`. +- **Global state**: use existing structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`); don't add new globals. +- **Undo/redo**: use `_BEGIN_UNDO_ACTION_` / `_END_UNDO_ACTION_` macros (`Notepad3.h`) for grouping; they also throttle notifications during bulk edits. -Replacement string backreference syntax (both flavors supported for backward compatibility): -- `$0`-`$99` and `\0`-`\9` — numbered group references -- `${name}` / `${+name}` — named group references -- Escape sequences: `\n`, `\t`, `\r`, `\\`, `\xHH`, `\uHHHH` +### WriteAccessBuf — dangling pointer anti-pattern -URL hotspot regex is defined at `src\Edit.c:108` (`HYPLNK_REGEX_FULL` macro). It matches `https?://`, `ftp://`, `file:///`, `file://`, `mailto:`, `www.`, `ftp.` schemes. The trailing group excludes punctuation (`.,:?!`) so URLs don't absorb sentence-ending characters. +**NEVER** use a pointer from `Path_WriteAccessBuf()` / `StrgWriteAccessBuf()` after any operation that may reallocate/swap the underlying buffer of the SAME handle. Buffer-invalidating ops include: `Path_CanonicalizeEx`, `Path_Swap` / `StrgSwap`, `Path_ExpandEnvStrings` / `ExpandEnvironmentStrgs`, `Path_Append` / `Path_Reset`, `StrgCat` / `StrgInsert` / `StrgFormat` / `StrgReset`, `Path_NormalizeEx` / `Path_AbsoluteFromApp` / `Path_RelativeToApp`. -### DarkMode (`src\DarkMode\`) - -Windows 10/11 dark mode via IAT (Import Address Table) hooks. Includes stub DLLs for uxtheme and user32. - -### ARM64 Platform Considerations - -**Supported platforms**: Win32 (x86), x64, x64_AVX2, ARM64. ARM 32-bit is **not** supported (the `Release|ARM` solution config maps to Win32). - -#### Architecture detection - -Use `#if defined(_M_ARM64)` or the helper macro `NP3_BUILD_ARM64` (defined in `src\TypeDefs.h`) for ARM64-specific code paths. **Important**: both ARM64 and x64 define `_WIN64`, so use `_M_ARM64` when you need to distinguish ARM64 from x64. - -#### ARM64 rendering defaults - -ARM64 defaults to `SC_TECHNOLOGY_DIRECTWRITERETAIN` (value 2) instead of `SC_TECHNOLOGY_DIRECTWRITE` (value 1) to preserve the Direct2D back buffer between frames. This avoids flickering on Qualcomm Adreno GPUs and the Win11 25H2 DWM compositor. The main window also uses `WS_EX_COMPOSITED` on ARM64 for system-level double-buffering. Users can override via `RenderingTechnology` in the INI file or the View menu. - -#### ARM64 build configuration - -- `CETCompat` must be `false` for ARM64 (CET is x86/x64 only) -- `TargetMachine` must be `MachineARM64` in all ARM64 linker sections -- `_WIN64` must be defined in preprocessor definitions for all ARM64 configurations -- Build fix scripts in `Build\scripts\`: `FixARM64CETCompat.ps1`, `FixARM64CrossCompile.ps1`, `FixARM64OutDir.ps1` - -#### GrepWin on ARM64 - -No native ARM64 grepWin build exists. The ARM64 build uses `grepWin-x64_portable.exe` which runs via x64 emulation on Windows ARM64. The binary selection in `src\Notepad3.c` uses `#if defined(_M_ARM64)` to handle this explicitly. - -#### Theme change flickering prevention - -`MsgThemeChanged()` in `src\Notepad3.c` wraps all heavy operations (bar recreation, lexer reset, restyling) in `WM_SETREDRAW FALSE/TRUE` to suppress intermediate repaints and performs a single `RedrawWindow()` at the end. DarkMode `RedrawWindow()` calls in `ListViewUtil.hpp` omit `RDW_ERASE` to avoid background erase flashes during theme transitions. - -### Formatting - -- LLVM-based `.clang-format` in `src\` — 4-space indentation, Stroustrup brace style, left-aligned pointers, no column limit, no include sorting -- `.editorconfig` enforces UTF-8/CRLF for source, 4-space indent for C/C++; Lexilla code uses tabs (preserved from upstream) -- **File encoding rules** (must be respected when creating or editing these files): - - `language\*\*.rc` — **UTF-8 without BOM**. Never write or save these files with a UTF-8 BOM. Use `Build\rc_to_utf8.cmd` to strip accidental BOMs. - - **PowerShell pitfall**: `[System.Text.Encoding]::UTF8` writes **with** BOM. Always use `[System.Text.UTF8Encoding]::new($false)` when writing `.rc` files from PowerShell scripts. - - **Python/script pitfall**: `.rc` files use **CRLF** line endings. When writing or inserting lines from Python or other scripts, use `\r\n` — not bare `\n`. After bulk edits, normalize with: `content = content.replace('\r\n', '\n').replace('\n', '\r\n')` before writing. - - `Build\Notepad3.ini`, `Build\minipath.ini` — **UTF-8 with BOM** (BOM = `EF BB BF`). These INI reference files must retain the BOM. -- String safety via `strsafe.h` throughout; deprecated string functions are disabled - -### Type Conventions - -- `DocPos` / `DocPosU` / `DocLn` for Scintilla document positions and line numbers (not raw `int`) -- `cpi_enc_t` for encoding identifiers -- `HSTRINGW` and `HPATHL` (opaque handle types) instead of raw `WCHAR*` buffers -- `NOMINMAX` is defined globally — use `min()`/`max()` macros or typed equivalents - -### Scintilla Interaction - -Always use `SciCall.h` wrappers (e.g. `SciCall_GetTextLength()`) instead of raw `SendMessage(hwnd, SCI_XXX, ...)`. Add missing wrappers to `SciCall.h` if needed. - -Wrapper macros follow the naming `DeclareSciCall{V|R}{0|01|1|2}`: -- **V** = void return, **R** = has return value -- **0** = no params, **1** = one param (wParam), **2** = two params, **01** = lParam only (wParam=0) -- The `msg` argument is the suffix after `SCI_` (e.g. `UNDO` for `SCI_UNDO`) - -```c -DeclareSciCallV0(Undo, UNDO); // SciCall_Undo() -DeclareSciCallR0(GetTextLength, GETTEXTLENGTH, DocPos); // DocPos SciCall_GetTextLength() -DeclareSciCallV1(SetTechnology, SETTECHNOLOGY, int, technology); // SciCall_SetTechnology(int) -DeclareSciCallR1(SupportsFeature, SUPPORTSFEATURE, bool, int, feature); // bool SciCall_SupportsFeature(int) -``` - -### Global State - -Application state is centralized in global structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`) defined in `Notepad3.c`. Access these through their defined interfaces rather than adding new globals. - -### Undo/Redo Transactions - -Use `_BEGIN_UNDO_ACTION_` / `_END_UNDO_ACTION_` macros (defined in `Notepad3.h`) to group Scintilla operations into single undo steps. These also handle notification limiting during bulk edits. - -### WriteAccessBuf — Dangling Pointer Anti-Pattern - -**NEVER use a pointer obtained from `Path_WriteAccessBuf()` / `StrgWriteAccessBuf()` after ANY operation that may reallocate or swap the underlying buffer of the SAME handle.** The pointer becomes dangling (use-after-free). - -Buffer-invalidating operations: -- `Path_CanonicalizeEx(h, ...)` — calls `Path_Swap` internally -- `Path_Swap(h, ...)` / `StrgSwap(h, ...)` -- `Path_ExpandEnvStrings(h)` / `ExpandEnvironmentStrgs(h, ...)` — may realloc -- `Path_Append(h, ...)` / `Path_Reset(h, ...)` — may realloc -- `StrgCat(h, ...)` / `StrgInsert(h, ...)` / `StrgFormat(h, ...)` / `StrgReset(h, ...)` — may realloc -- `Path_NormalizeEx(h, ...)` / `Path_AbsoluteFromApp(h, ...)` / `Path_RelativeToApp(h, ...)` — may realloc/swap - -Safe patterns after invalidation: -- **Read-only**: use `Path_Get(h)` or `StrgGet(h)` — always returns current buffer -- **Read-write**: re-obtain via `ptr = Path_WriteAccessBuf(h, 0)` / `ptr = StrgWriteAccessBuf(h, 0)` (size 0 = no resize, just returns current pointer) +After any of these: +- Read-only: use `Path_Get(h)` / `StrgGet(h)` (always current). +- Read-write: re-obtain via `Path_WriteAccessBuf(h, 0)` / `StrgWriteAccessBuf(h, 0)` (size 0 = no resize, returns current pointer). ## Python Environment -A Python 3.14 virtual environment is available at `.venv\` for scripting tasks (batch file manipulation, locale file updates, code generation, etc.). +`.venv\` (Python 3.14) for scripting tasks — bulk locale edits, code generation, etc. ```bash -# Run a script (from project root, in bash/Cygwin) .venv/Scripts/python.exe - -# Install packages .venv/Scripts/pip.exe install ``` -Use this venv instead of system Python (which may not be installed). Useful for bulk operations across the 26 locale `strings_*.rc` files. - -## Key File Paths (Quick Reference) - -| Pattern | Purpose | -|---------|---------| -| `language/common_res.h` | String resource ID definitions | -| `language/np3_*/strings_*.rc` | Locale string tables (26 files) | -| `Build/Notepad3.ini` | Reference INI with documented settings | -| `src/Notepad3.c:FileSave()` | Save flow entry point | -| `src/Edit.c:EditSaveFile()` | Actual file write logic | -| `src/Config/Config.cpp` | INI management, settings load/save | -| `src/Dialogs.c:SaveFileDlg()` | Save As dialog | +System Python is not installed; `python3` fails. Python beats sed/perl under Cygwin for literal string insertions (reliable `\r\n`). diff --git a/language/common_res.h b/language/common_res.h index 91c69cbba..6c0a61617 100644 --- a/language/common_res.h +++ b/language/common_res.h @@ -873,7 +873,7 @@ #define IDM_TRAY_EXIT 43701 #define IDM_SETPASS 43702 #define IDM_EDIT_INSERT_GUID 43705 -#define IDM_EDIT_STOP_PASTEBOARD 43706 +#define IDM_EDIT_TOGGLE_PASTEBOARD 43706 #define IDS_ENC_ANSI 61000 diff --git a/language/np3_af_za/menu_af_za.rc b/language/np3_af_za/menu_af_za.rc index 1980d368b..c29767b21 100644 --- a/language/np3_af_za/menu_af_za.rc +++ b/language/np3_af_za/menu_af_za.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Verwyder\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Maak &Klipbord Skoon", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "W&oorde" BEGIN diff --git a/language/np3_be_by/menu_be_by.rc b/language/np3_be_by/menu_be_by.rc index 37135ea7f..39a9ac6ac 100644 --- a/language/np3_be_by/menu_be_by.rc +++ b/language/np3_be_by/menu_be_by.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "В&ыдаліць\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "А&чысціць буфер абмену", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Словы" BEGIN diff --git a/language/np3_de_de/menu_de_de.rc b/language/np3_de_de/menu_de_de.rc index 28032d581..a34dd9819 100644 --- a/language/np3_de_de/menu_de_de.rc +++ b/language/np3_de_de/menu_de_de.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Löschen\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Lösche Zwischenablage", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Zwischenablage &Überwachung abbrechen", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Zwischenablage &Überwachung umschalten", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "&Wörter" BEGIN diff --git a/language/np3_el_gr/menu_el_gr.rc b/language/np3_el_gr/menu_el_gr.rc index 14ef49cb8..1153c3d3b 100644 --- a/language/np3_el_gr/menu_el_gr.rc +++ b/language/np3_el_gr/menu_el_gr.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Διαγραφή\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Απαλοιφή πρόχειρου", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "&Λέξεις" BEGIN diff --git a/language/np3_en_gb/menu_en_gb.rc b/language/np3_en_gb/menu_en_gb.rc index 5412634f5..e22af03e5 100644 --- a/language/np3_en_gb/menu_en_gb.rc +++ b/language/np3_en_gb/menu_en_gb.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Delete\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Cl&ear Clipboard", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "W&ords" BEGIN diff --git a/language/np3_en_us/menu_en_us.rc b/language/np3_en_us/menu_en_us.rc index 0221eb200..32414051a 100644 --- a/language/np3_en_us/menu_en_us.rc +++ b/language/np3_en_us/menu_en_us.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Delete\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Cl&ear Clipboard", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "W&ords" BEGIN diff --git a/language/np3_es_es/menu_es_es.rc b/language/np3_es_es/menu_es_es.rc index 07c81189d..d41215dd2 100644 --- a/language/np3_es_es/menu_es_es.rc +++ b/language/np3_es_es/menu_es_es.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "E&liminar\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Vaciar el portapapeles", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "P&alabras" BEGIN diff --git a/language/np3_fi_fi/menu_fi_fi.rc b/language/np3_fi_fi/menu_fi_fi.rc index 4f600d853..67548fbe1 100644 --- a/language/np3_fi_fi/menu_fi_fi.rc +++ b/language/np3_fi_fi/menu_fi_fi.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Poista\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Tyhj&ennä leikepöytä", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "S&anat" BEGIN diff --git a/language/np3_fr_fr/menu_fr_fr.rc b/language/np3_fr_fr/menu_fr_fr.rc index e2284d254..b421d87f0 100644 --- a/language/np3_fr_fr/menu_fr_fr.rc +++ b/language/np3_fr_fr/menu_fr_fr.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Supprimer\tSuppr", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Vi&der le presse-papiers", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "M&ots" BEGIN diff --git a/language/np3_hi_in/menu_hi_in.rc b/language/np3_hi_in/menu_hi_in.rc index 73af73f2f..54785d730 100644 --- a/language/np3_hi_in/menu_hi_in.rc +++ b/language/np3_hi_in/menu_hi_in.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "हटाएं (&D)\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "क्लिपबोर्ड साफ़ करें (&E)", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "शब्द (&O)" BEGIN diff --git a/language/np3_hu_hu/menu_hu_hu.rc b/language/np3_hu_hu/menu_hu_hu.rc index 4a94ca18f..1231fe251 100644 --- a/language/np3_hu_hu/menu_hu_hu.rc +++ b/language/np3_hu_hu/menu_hu_hu.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Törlés\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Vágólap t&örlése", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "&Vágólapfigyelés leállítása", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "&Vágólapfigyelés leállítása", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Sza&vak" BEGIN diff --git a/language/np3_id_id/menu_id_id.rc b/language/np3_id_id/menu_id_id.rc index 28bb10d50..5e0202edf 100644 --- a/language/np3_id_id/menu_id_id.rc +++ b/language/np3_id_id/menu_id_id.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "Hap&us\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Bersi&hkan Papan Klip", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Ka&ta" BEGIN diff --git a/language/np3_it_it/menu_it_it.rc b/language/np3_it_it/menu_it_it.rc index 0a0ecfd75..8257e69bd 100644 --- a/language/np3_it_it/menu_it_it.rc +++ b/language/np3_it_it/menu_it_it.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Elimina\tCanc", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Svuota appunti", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop &monitoraggio appunti", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Stop &monitoraggio appunti", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "&Parole" BEGIN diff --git a/language/np3_ja_jp/menu_ja_jp.rc b/language/np3_ja_jp/menu_ja_jp.rc index ef572e7c9..11e6935e3 100644 --- a/language/np3_ja_jp/menu_ja_jp.rc +++ b/language/np3_ja_jp/menu_ja_jp.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "削除(&D)\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "クリップボードをクリア(&E)", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "単語(&O)" BEGIN diff --git a/language/np3_ko_kr/menu_ko_kr.rc b/language/np3_ko_kr/menu_ko_kr.rc index a8ce1c796..e875a4b6d 100644 --- a/language/np3_ko_kr/menu_ko_kr.rc +++ b/language/np3_ko_kr/menu_ko_kr.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "삭제(&D)\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "클립보드 지우기(&E)", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "클립보드 모니터링 중지(&M)", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "클립보드 모니터링 중지(&M)", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "단어(&W)" BEGIN diff --git a/language/np3_nl_nl/menu_nl_nl.rc b/language/np3_nl_nl/menu_nl_nl.rc index c88d7bcf1..43bead908 100644 --- a/language/np3_nl_nl/menu_nl_nl.rc +++ b/language/np3_nl_nl/menu_nl_nl.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Wissen\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Klembord wissen", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "&Bewaking van klembord stoppen", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "&Bewaking van klembord omschakelen", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "W&oorden" BEGIN diff --git a/language/np3_pl_pl/menu_pl_pl.rc b/language/np3_pl_pl/menu_pl_pl.rc index 5f467b65a..e4f6b254a 100644 --- a/language/np3_pl_pl/menu_pl_pl.rc +++ b/language/np3_pl_pl/menu_pl_pl.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Usuń\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Wyczyść schowek", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "W&yrazy" BEGIN diff --git a/language/np3_pt_br/menu_pt_br.rc b/language/np3_pt_br/menu_pt_br.rc index 046106968..6aa467878 100644 --- a/language/np3_pt_br/menu_pt_br.rc +++ b/language/np3_pt_br/menu_pt_br.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Apagar\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "L&impar Área de Transferência", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Parar de &Monitorar Área de Transferência", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Parar de &Monitorar Área de Transferência", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "P&alavras" BEGIN diff --git a/language/np3_pt_pt/menu_pt_pt.rc b/language/np3_pt_pt/menu_pt_pt.rc index 789e5464f..39bc0757b 100644 --- a/language/np3_pt_pt/menu_pt_pt.rc +++ b/language/np3_pt_pt/menu_pt_pt.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Eliminar\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Limpar ár&ea de transferência", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Pala&vras" BEGIN diff --git a/language/np3_ru_ru/menu_ru_ru.rc b/language/np3_ru_ru/menu_ru_ru.rc index 6bce14a0f..a064af7a3 100644 --- a/language/np3_ru_ru/menu_ru_ru.rc +++ b/language/np3_ru_ru/menu_ru_ru.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Удалить\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "О&чистить буфер обмена", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Слова" BEGIN diff --git a/language/np3_sv_se/menu_sv_se.rc b/language/np3_sv_se/menu_sv_se.rc index cc3eb2374..a76caf5d8 100644 --- a/language/np3_sv_se/menu_sv_se.rc +++ b/language/np3_sv_se/menu_sv_se.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "Ta bort\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Rensa urklipp", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Ord" BEGIN diff --git a/language/np3_tr_tr/menu_tr_tr.rc b/language/np3_tr_tr/menu_tr_tr.rc index 1ba64cb22..b6160086a 100644 --- a/language/np3_tr_tr/menu_tr_tr.rc +++ b/language/np3_tr_tr/menu_tr_tr.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "&Sil\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Panoyu temizle", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "&Sözcükler" BEGIN diff --git a/language/np3_vi_vn/menu_vi_vn.rc b/language/np3_vi_vn/menu_vi_vn.rc index 4eb284dfa..17e55e1c5 100644 --- a/language/np3_vi_vn/menu_vi_vn.rc +++ b/language/np3_vi_vn/menu_vi_vn.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "Xóa(&D)\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "Xóa bộ nhớ tạm(&E)", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "Stop Clipboard &Monitoring", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "Toggle Clipboard &Monitoring", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "Từ(&W)" BEGIN diff --git a/language/np3_zh_cn/menu_zh_cn.rc b/language/np3_zh_cn/menu_zh_cn.rc index 1b0d29d6f..babfed599 100644 --- a/language/np3_zh_cn/menu_zh_cn.rc +++ b/language/np3_zh_cn/menu_zh_cn.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "删除(&D)\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "清空剪贴板(&E)", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "停止监视剪贴板(&M)", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "停止监视剪贴板(&M)", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "单词(&O)" BEGIN diff --git a/language/np3_zh_tw/menu_zh_tw.rc b/language/np3_zh_tw/menu_zh_tw.rc index a8b183e40..f2022d155 100644 --- a/language/np3_zh_tw/menu_zh_tw.rc +++ b/language/np3_zh_tw/menu_zh_tw.rc @@ -169,7 +169,7 @@ BEGIN MENUITEM "刪除(&D)\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "清空剪貼簿(&E)", IDM_EDIT_CLEARCLIPBOARD - MENUITEM "停止監視剪貼簿(&M)", IDM_EDIT_STOP_PASTEBOARD + MENUITEM "停止監視剪貼簿(&M)", IDM_EDIT_TOGGLE_PASTEBOARD MENUITEM SEPARATOR POPUP "單詞(&O)" BEGIN diff --git a/src/Dialogs.c b/src/Dialogs.c index c53a916a7..79a3a6d65 100644 --- a/src/Dialogs.c +++ b/src/Dialogs.c @@ -2661,6 +2661,12 @@ static INT_PTR CALLBACK ChangeNotifyDlgProc(HWND hwnd, UINT umsg, WPARAM wParam, CheckDlgButton(hwnd, IDC_CHECK_BOX_A, SetBtn(Settings.ResetFileWatching)); CheckDlgButton(hwnd, IDC_CHECK_BOX_B, SetBtn(FileWatching.MonitoringLog)); + // Mutually exclusive with Clipboard Monitoring — disable the + // "Monitoring Log" checkbox while the pasteboard is active. + if (IsPasteBoardActive()) { + EnableItem(hwnd, IDC_CHECK_BOX_B, FALSE); + } + if (FileWatching.MonitoringLog) { CheckRadioButton(hwnd, IDC_RADIO_BTN_A, IDC_RADIO_BTN_E, IDC_RADIO_BTN_C); EnableItem(hwnd, IDC_RADIO_BTN_A, FALSE); diff --git a/src/Notepad3.c b/src/Notepad3.c index 3e80b0aed..2eb83ec1e 100644 --- a/src/Notepad3.c +++ b/src/Notepad3.c @@ -626,6 +626,7 @@ static bool s_flagStartAsTrayIcon = false; static bool s_flagKeepTitleExcerpt = false; static bool s_flagNewFromClipboard = false; static bool s_flagPasteBoard = false; +static bool s_bPasteBoardFirstPaste = true; static bool s_flagJumpTo = false; static FILE_WATCHING_MODE s_flagChangeNotify = FWM_NO_INIT; static bool s_flagQuietCreate = false; @@ -796,6 +797,47 @@ static void _InitGlobals() } +//============================================================================= +// +// PasteBoard_Start() / PasteBoard_Stop() +// +// Install / remove the clipboard-format listener and the polling timer used +// by Clipboard Monitoring (a.k.a. "pasteboard mode"). State is process-local +// only; it is never persisted to the INI. Mutually exclusive with Tail. +// +static void PasteBoard_Start(HWND hwnd) +{ + if (s_flagPasteBoard) { return; } + s_bPasteBoardListening = AddClipboardFormatListener(hwnd); + if (s_bPasteBoardListening) { + s_iLastCopyTime = 0; + s_bLastPasteSeqNoValid = false; + SetTimer(hwnd, ID_PASTEBOARDTIMER, 100, PasteBoardTimerProc); + } + s_flagPasteBoard = true; + s_bPasteBoardFirstPaste = true; +} + +bool IsPasteBoardActive(void) +{ + return s_flagPasteBoard; +} + +static void PasteBoard_Stop(HWND hwnd) +{ + KillTimer(hwnd, ID_PASTEBOARDTIMER); + if (s_bPasteBoardListening) { + RemoveClipboardFormatListener(hwnd); + s_bPasteBoardListening = false; + } + s_flagPasteBoard = false; + s_iLastCopyTime = 0; + s_bLastCopyFromMe = false; + s_bLastPasteSeqNoValid = false; + s_bPasteBoardFirstPaste = true; +} + + //============================================================================= // // _CleanUpResources() @@ -1914,6 +1956,13 @@ HWND InitInstance(const HINSTANCE hInstance, int nCmdShow) // Restore saved Monitoring Log setting - fixes #5037 FileWatching.MonitoringLog = Settings.MonitoringLog; + // /B (Clipboard Monitoring) and Tail are mutually exclusive. If both would + // be active at startup, the command-line flag wins for this session only; + // Settings.MonitoringLog stays untouched so the persisted value survives. + if (s_flagPasteBoard && FileWatching.MonitoringLog) { + FileWatching.MonitoringLog = false; + } + // initial set text in front of ShowWindow() EditSetNewText(Globals.hwndEdit, "", 0, false, false); @@ -2097,12 +2146,11 @@ HWND InitInstance(const HINSTANCE hInstance, int nCmdShow) } // Check for Paste Board option -- after loading files + // /B command-line flag pre-set s_flagPasteBoard = true; the helper expects it false + // and flips it to true, so clear it first to get a clean activation. if (s_flagPasteBoard) { - s_bPasteBoardListening = AddClipboardFormatListener(Globals.hwndMain); - if (s_bPasteBoardListening) { - s_iLastCopyTime = 0; - SetTimer(Globals.hwndMain, ID_PASTEBOARDTIMER, 100, PasteBoardTimerProc); - } + s_flagPasteBoard = false; + PasteBoard_Start(Globals.hwndMain); } // check if a lexer was specified from the command line @@ -3254,13 +3302,7 @@ LRESULT MsgEndSession(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM lParam) // Terminate clipboard watching if (s_flagPasteBoard) { - s_flagPasteBoard = false; - s_iLastCopyTime = 0; - KillTimer(hwnd, ID_PASTEBOARDTIMER); - if (s_bPasteBoardListening) { - RemoveClipboardFormatListener(hwnd); - s_bPasteBoardListening = false; - } + PasteBoard_Stop(hwnd); } // close Find/Replace and CustomizeSchemes @@ -4161,7 +4203,10 @@ LRESULT MsgInitMenu(HWND hwnd, WPARAM wParam, LPARAM lParam) OpenClipboard(hwnd); EnableCmd(hmenu, IDM_EDIT_CLEARCLIPBOARD, CountClipboardFormats()); CloseClipboard(); - EnableCmd(hmenu, IDM_EDIT_STOP_PASTEBOARD, s_flagPasteBoard); + // Clipboard Monitoring is a toggle; grey it out while Tail is active + // (the two modes are mutually exclusive). + CheckCmd(hmenu, IDM_EDIT_TOGGLE_PASTEBOARD, s_flagPasteBoard); + EnableCmd(hmenu, IDM_EDIT_TOGGLE_PASTEBOARD, !FileWatching.MonitoringLog); EnableCmd(hmenu, IDM_EDIT_MOVELINEUP, !ro); EnableCmd(hmenu, IDM_EDIT_MOVELINEDOWN, !ro); @@ -4287,6 +4332,7 @@ LRESULT MsgInitMenu(HWND hwnd, WPARAM wParam, LPARAM lParam) CheckCmd(hmenu, IDM_VIEW_BOOKMARK_MARGIN, SciCall_GetMarginWidthN(MARGIN_SCI_BOOKMRK) > 0); CheckCmd(hmenu, IDM_VIEW_CHGHIST_TOGGLE_MARGIN, SciCall_GetMarginWidthN(MARGIN_SCI_CHGHIST) > 0); CheckCmd(hmenu, IDM_VIEW_CHASING_DOCTAIL, FileWatching.MonitoringLog); + EnableCmd(hmenu, IDM_VIEW_CHASING_DOCTAIL, !s_flagPasteBoard); CheckCmd(hmenu, IDM_VIEW_MARKOCCUR_ONOFF, moe); CheckCmd(hmenu, IDM_VIEW_MARKOCCUR_BOOKMARKS, Settings.MarkOccurrencesBookmark); @@ -5134,16 +5180,17 @@ static bool _HandleEditBasicCommands(HWND hwnd, UINT umsg, WPARAM wParam, LPARAM break; - case IDM_EDIT_STOP_PASTEBOARD: - KillTimer(Globals.hwndMain, ID_PASTEBOARDTIMER); - if (s_bPasteBoardListening) { - RemoveClipboardFormatListener(Globals.hwndMain); - s_bPasteBoardListening = false; + case IDM_EDIT_TOGGLE_PASTEBOARD: + // Toggle Clipboard Monitoring. Mutually exclusive with Tail/Log-File + // Monitoring: refuse if Tail is active (the menu item is also greyed). + if (FileWatching.MonitoringLog) { + break; + } + if (s_flagPasteBoard) { + PasteBoard_Stop(Globals.hwndMain); + } else { + PasteBoard_Start(Globals.hwndMain); } - s_flagPasteBoard = false; - s_iLastCopyTime = 0; - s_bLastCopyFromMe = false; - s_bLastPasteSeqNoValid = false; UpdateToolbar_Now(Globals.hwndMain); UpdateStatusbar(true); break; @@ -6407,6 +6454,14 @@ static bool _HandleViewAndSettingsCommands(HWND hwnd, UINT umsg, WPARAM wParam, case IDM_VIEW_CHASING_DOCTAIL: { + // Mutually exclusive with Clipboard Monitoring: refuse to enable Tail + // while the pasteboard is active. Disabling Tail is still allowed + // (although with the mutex in place, Tail can never be ON in that + // state — belt-and-braces). + if (s_flagPasteBoard && !FileWatching.MonitoringLog) { + break; + } + InstallFileWatching(false); static DocPos _lastCaretPos = -1; @@ -9250,7 +9305,7 @@ LRESULT MsgNotify(HWND hwnd, WPARAM wParam, LPARAM lParam) case STATUS_OVRMODE: if (s_flagPasteBoard) { - PostWMCommand(hwnd, IDM_EDIT_STOP_PASTEBOARD); + PostWMCommand(hwnd, IDM_EDIT_TOGGLE_PASTEBOARD); } else { PostWMCommand(hwnd, CMD_VK_INSERT); } @@ -9978,6 +10033,7 @@ static void _UpdateToolbarDelayed() CheckTool(Globals.hwndToolbar, IDT_VIEW_WORDWRAP, Globals.fvCurFile.bWordWrap); CheckTool(Globals.hwndToolbar, IDT_VIEW_CHASING_DOCTAIL, FileWatching.MonitoringLog); + EnableTool(Globals.hwndToolbar, IDT_VIEW_CHASING_DOCTAIL, !s_flagPasteBoard); CheckTool(Globals.hwndToolbar, IDT_VIEW_PIN_ON_TOP, Settings.AlwaysOnTop); bool const b1 = SciCall_IsSelectionEmpty(); @@ -12329,11 +12385,18 @@ void CALLBACK PasteBoardTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD if (SciCall_CanPaste()) { bool bAutoIndent2 = Settings.AutoIndent; Settings.AutoIndent = 0; - EditJumpTo(-1, 0); UndoTransActionBegin(); - if (!Sci_IsDocEmpty()) { + + // Paste at the current caret position. Pre-pend the configured + // separator unless this is the first paste of the session OR the + // caret is already at a line start. + DocPos const curPos = SciCall_GetCurrentPos(); + bool const bAtLineStart = (curPos == Sci_GetLineStartPosition(curPos)); + bool const bSkipSep = s_bPasteBoardFirstPaste || bAtLineStart; + + if (!bSkipSep) { if (Settings2.PasteBoardSeparator[0] == L'\x01') { - // default: one EOL between entries + // configured default: one document EOL between entries SciCall_NewLine(); } else if (Settings2.PasteBoardSeparator[0] != L'\0') { @@ -12360,6 +12423,7 @@ void CALLBACK PasteBoardTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD Settings.AutoIndent = bAutoIndent2; s_dwLastPasteSeqNo = dwCurrentSeqNo; s_bLastPasteSeqNoValid = true; + s_bPasteBoardFirstPaste = false; } s_iLastCopyTime = 0; } diff --git a/src/Notepad3.h b/src/Notepad3.h index d9243e1a0..28932943a 100644 --- a/src/Notepad3.h +++ b/src/Notepad3.h @@ -140,6 +140,7 @@ bool FileIO(bool fLoad, const HPATHL hfile_pth, EditFileIOStatus* status, FileLoadFlags fLoadFlags, FileSaveFlags fSaveFlags, bool bSetSavePoint); void CALLBACK PasteBoardTimerProc(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime); +bool IsPasteBoardActive(void); void InstallFileWatching(const bool bInstall); void AutoSaveStart(bool bReset);