15 KiB
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 platformsBuild\Clean.cmd— clean outputsnuget restoreonce before first buildVersion.ps1regeneratessrc\VersionEx.h(Major.YY.Mdd.Build, build number inVersions\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:
$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—HSTRINGWdynamic wide-string handle.PathLib.c/h—HPATHLpath 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 acase SCLEX_FOO:in the top-of-file dispatch that callsSciCall_SetProperty(...). Without it theOptionsXXXconstructor defaults silently apply — symptom is "feature looks broken" (e.g. comments highlighted as ERROR). - Comment-toggle arms. If the lexer has comments, add
case SCLEX_FOO:in BOTHLexer_GetStreamCommentStrgsandLexer_GetLineCommentStrginEditLexer.c, or Edit > Toggle Block/Line Comment is a no-op. - Theme INI sections live under
pszName(4thEDITLEXERfield), 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 variantsBuild\Notepad3_<locale>.ini. RenamingpszNameorphans existing user style customizations.
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
#define IDS_MUI_XXX <id>inlanguage\common_res.h(13xxx errors/warnings, 14xxx info/prompts).- English in
language\np3_en_us\strings_en_us.rc. - Same English as placeholder in the 25 other
strings_*.rcfiles (translators update later). - Display via
InfoBoxLng()/MessageBoxLng(); checkIsYesOkay().Settings.MuteMessageBeepcontrols 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 inMsgInitMenu, the tail toolbar button, and the "Monitoring Log" checkbox inChangeNotifyDlgProc./B+ persistedMonitoringLog=true→/Bwins the session, INI preserved. /Binitial auto-paste vs runtime toggle:/Bpastes 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 bySettings2.AtomicFileSave. Globals.dwLastErrorholds Win32 error.FileSave()checksERROR_ACCESS_DENIED,ERROR_PATH_NOT_FOUNDbefore generic fallback.InstallFileWatching()usesFindFirstChangeNotificationWon 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(orNP3_BUILD_ARM64inTypeDefs.h) to distinguish. - Rendering: defaults to
SC_TECHNOLOGY_DIRECTWRITERETAIN(2) — preserves Direct2D back buffer, avoids flicker on Qualcomm Adreno + Win11 25H2 DWM. Main window getsWS_EX_COMPOSITED. User can override viaRenderingTechnology/ View menu. - Build:
CETCompat=false(CET is x86/x64 only),TargetMachine=MachineARM64,_WIN64defined. Fix scripts:Build\scripts\FixARM64{CETCompat,CrossCompile,OutDir}.ps1. - grepWin: uses x64 exe via emulation.
MsgThemeChanged()wraps bar recreate / lexer reset / restyle inWM_SETREDRAW FALSE/TRUE+ singleRedrawWindow()at end. DarkModeRedrawWindow()inListViewUtil.hppomitsRDW_ERASE.
Conventions
Formatting & encoding
.clang-formatinsrc\— 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. UseBuild\rc_to_utf8.cmdto strip. PowerShell:[System.Text.UTF8Encoding]::new($false). Python: write\r\nexplicitly.Build\Notepad3.ini,Build\minipath.ini= UTF-8 WITH BOM (EF BB BF). Preserve it.strsafe.hthroughout; deprecated string functions disabled.
Types
DocPos/DocPosU/DocLnfor document positions/lines (not rawint).cpi_enc_tfor encodings.HSTRINGW/HPATHLinstead of rawWCHAR*buffers.NOMINMAXis global — usemin()/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:
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 viaCreateIniFileEx(). Paths.IniFile= active writable INI;Paths.IniFileDefault= recovery fallback.- Init flow (in
src\Config\Config.cpp):FindIniFile()→TestIniFile()→CreateIniFile()→LoadSettings(). SettingsVersiondefaults toCFG_VER_CURRENTwhen missing — empty/new INI gets current defaults.bIniFileFromScratchis set when INI is 0 bytes, cleared afterSaveAllSettings(). While set,MuiLanguage.csuppresses writingPreferredLanguageLocaleName.- Empty lexer sections are pruned in
Style_ToIniSection()viaIniSectionGetKeyCount()— 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]inBuild\Notepad3.iniAND a#### \Name=default`heading + prose paragraph in the matching topical section ofreadme\config\Configuration.md` (the user-facing reference). Updating only the INI is incomplete.
Save/Load macros (src\Config\Config.cpp)
Encoding_MapIniSettingis 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 withbLoad=trueproduces wrong results. Reference:MRU_Save()usesfalse.- Defaults depending on
DefaultEncoding(e.g.SkipANSICodePageDetection,LoadASCIIasUTF8) must be recalculated in_SaveSettings()beforeSAVE_VALUE_IF_NOT_EQ_DEFAULTfires. See thebCurrentEncUTF8block.
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).