Notepad3/.github/copilot-instructions.md

213 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
Scripts under `Build\` (PowerShell under `Build\scripts\`):
- `Build\Build_x64.cmd [Release|Debug]` — single platform (also `_Win32`, `_ARM64`, `_x64_AVX2`)
- `Build\BuildAll.cmd` — all four platforms
- `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).
Default config is Release.
### Invoking MSBuild from PowerShell
`msbuild` is **not** on the PATH in PowerShell. Do **not** call it directly — it will fail with "not recognized". Locate it via `vswhere.exe` first:
```powershell
$msbuild = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" `
-latest -requires Microsoft.Component.MSBuild `
-find MSBuild\**\Bin\MSBuild.exe | Select-Object -First 1
& $msbuild Notepad3.sln /m /p:Configuration=Release /p:Platform=x64 /nologo /v:minimal
Write-Host "Exit code: $LASTEXITCODE"
```
Check `$LASTEXITCODE` (0 = success) and scan the last output lines for `-> Notepad3.exe` or `error C` entries. The verbose output is long — use `Select-String` or tail filtering rather than reading it all.
## Core Modules (`src\`)
- `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.
## Vendored Libraries
`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).
## grepWin (`grepWin\`) — external tool, not built from source
Pre-built exes under `grepWin\portables\`; `.lang` files under `grepWin\translations\`. Runtime lookup in `src\Dialogs.c`: `Settings2.GrepWinPath``<ModuleDir>\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`).
## Lexers (`src\StyleLexers\`)
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.
Easy-to-miss touchpoints — derivable but only if you know to look:
- **Property setter arm in `EditLexer.c`.** If the lexer exposes Scintilla properties (e.g. `lexer.foo.allow.X`), add a `case SCLEX_FOO:` in the top-of-file dispatch that calls `SciCall_SetProperty(...)`. Without it the `OptionsXXX` constructor defaults silently apply — symptom is "feature looks broken" (e.g. comments highlighted as ERROR).
- **Shared `SCLEX_*` values**: `SCLEX_CPP` is the lexer for **C, C#, Java, JS, RC**; `SCLEX_D` for **D + Go**; `SCLEX_PYTHON` for **Python + Awk**. Property-arm or `EDITLEXER` table changes under those `SCLEX_*` cases ripple to every wiring using that lexer (e.g. adding `SCE_C_USERLITERAL` affects all SCLEX_CPP languages).
- **Comment-toggle arms.** If the lexer has comments, add `case SCLEX_FOO:` in BOTH `Lexer_GetStreamCommentStrgs` and `Lexer_GetLineCommentStrg` in `EditLexer.c`, or Edit > Toggle Block/Line Comment is a no-op.
- **Theme INI sections live under `pszName` (4th `EDITLEXER` field), not the lexer name string.** Each new lexer needs a `[<pszName>]` block in every theme INI: `Build\Notepad3.ini`, `Build\Themes\*.ini`, `res\StdDarkModeScheme.ini`, locale variants `Build\Notepad3_<locale>.ini`. Renaming `pszName` orphans existing user style customizations.
- **New style rows need theme INI entries too** — same rule as new lexers, just per-row. Each `EDITLEXER` row's label string (e.g. `L"User Literal"`) needs a matching `User Literal=<spec>` line in every theme INI's `[<pszName>]` block. Without it, the EDITLEXER inline default applies and the row is invisible to theme switching. `lexilla/wscite/*.properties` (one per language) are useful colour references.
- **Homebrew lexers in `lexilla/lexers_x/`** (6 files): `LexAHK`, `LexCSV`, `LexHTML` (NP3 fork — used by both `SCLEX_HTML` and `SCLEX_XML`), `LexJSON5`, `LexKotlin`, `LexVerilog` (`SCLEX_VERILOG` + `SCLEX_SYSVERILOG`). Their `SCE_*_*` enums live in `lexilla/lexers_x/SciXLexer.h`, not the stock `lexilla/include/SciLexer.h``#include "lexers_x/SciXLexer.h"` if you need the homebrew constants.
## Localization (`language\`)
26 locales under `np3_LANG_COUNTRY\`. Language packs build as separate DLLs.
**Invariant:** every `IDS_MUI_*` defined in `common_res.h` must exist in **all** `strings_*.rc` files. A missing entry breaks the corresponding language DLL build. For bulk insertions across locales, use a `.venv/Scripts/python.exe` script — `sed`/`perl` `\n` escaping is unreliable in Cygwin.
**Same rule for `IDS_LEX_*`** — these live in `language\np3_*\lexer_*.rc` (not `strings_*.rc`).
### Adding a string resource
1. `#define IDS_MUI_XXX <id>` 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.
## Clipboard Monitoring (Pasteboard Mode)
Runtime toggle `IDM_EDIT_TOGGLE_PASTEBOARD`; `/B` enables at startup. Not persisted. `IsPasteBoardActive()` in `Notepad3.h` is the cross-TU query.
- **Mutex with Tail** (`FileWatching.MonitoringLog`): each greys the other in `MsgInitMenu`, the tail toolbar button, and the "Monitoring Log" checkbox in `ChangeNotifyDlgProc`. `/B` + persisted `MonitoringLog=true``/B` wins the session, INI preserved.
- **`/B` initial auto-paste vs runtime toggle**: `/B` pastes the current clipboard once — only on an empty untitled buffer (no file arg, no `/c`, no auto-loaded MRU). Runtime toggle never auto-pastes. Preserve this asymmetry.
- `Settings2.PasteBoardSeparator`: `\x01` = one document EOL; `\0` = none; else verbatim. Also suppressed on first paste after enable and when caret is at line start.
## File I/O
- `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.
## PCRE2 (`scintilla\pcre2\`)
`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`.
Replacement backref syntax: `$0`-`$99`, `\0`-`\9`, `${name}`, `${+name}`.
URL hotspot regex: `HYPLNK_REGEX_FULL` in `src\Edit.c` — matches `https?://`, `ftp://`, `file:///`, `file://`, `mailto:`, `www.`, `ftp.`. Trailing group excludes `.,:?!`.
## DarkMode (`src\DarkMode\`)
Windows 10/11 dark mode via IAT hooks on uxtheme/user32 (stub DLLs included).
## ARM64
Supported: Win32 (x86), x64, x64_AVX2, ARM64. **ARM 32-bit is not supported**`Release|ARM` maps to Win32.
- 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
### Formatting & encoding
- `.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.
### Types
- `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 (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_`.
Example wrappers:
```c
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)
```
### Scintilla/Lexilla versions
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
Use existing structs (`Globals`, `Settings`, `Settings2`, `Flags`, `Paths`). Don't add new globals.
### Dialog return values (`InfoBoxLng`)
`InfoBoxLng()` returns `MAKELONG(button, mode)` — never compare/switch on the raw return when the suppression key is non-NULL: a saved-answer replay sets HIWORD too, so `result == IDNO` is false. Use `IsYesOkay()` / `IsRetryContinue()` / `IsNoCancelClose()` (in `src\Dialogs.h`), or extract via `INFOBOX_ANSW(r)` (LOWORD) and `INFOBOX_MODE(r)` (HIWORD). Switching on the raw value is a latent bug if a suppression key is ever added.
### Portable INI design
- INI beside the exe. No registry. Runs on defaults if none exists (user creates via "Save Settings Now").
- **Admin redirect**: `Notepad3.ini=<path>` 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 in BOTH places**: a commented entry under `[Settings2]` in `Build\Notepad3.ini` AND a `#### \`Name=default\`` heading + prose paragraph in the matching topical section of `readme\config\Configuration.md` (the user-facing reference). Updating only the INI is incomplete.
### 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.
### Creating directories
`Path_CreateDirectoryEx(hpth)` wraps `SHCreateDirectoryExW`. Success: `SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))`. Reference: `CreateIniFile()` in `Config.cpp`.
### Long-path / PathLib wrappers
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):
| 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 |
### Path comparison
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 (`Notepad3.h`) for grouping; they also throttle notifications during bulk edits.
### WriteAccessBuf — dangling pointer anti-pattern
**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`.
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).