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.
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 scripts live in Build\ (PowerShell under Build\scripts\):
Build\Build_x64.cmd [Release|Debug]— single platform (also_Win32,_ARM64,_x64_AVX2)Build\BuildAll.cmd— all platformsmsbuild Notepad3.sln /m /p:Configuration=Release /p:Platform=x64— CI equivalentBuild\Clean.cmd— clean outputs- Run
nuget restore Notepad3.slnonce before first build. - Run
Version.ps1before building to regeneratesrc\VersionEx.h(formatMajor.YY.Mdd.Build; build number inVersions\build.txt). - Tests:
test\TestFileVersion.cmd,test\TestAhkNotepad3.cmd(needs AutoHotkey). CI matrix in.github/workflows/build.yml(windows-2022).
Default configuration is Release.
| File | Purpose |
|---|---|
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) |
MsgInitMenu()(WM_INITMENU) — enable/check state.MsgCommand()(WM_COMMAND) — thin dispatcher delegating to static sub-handlers, each returningtrueif it handled:
| Handler | Scope |
|---|---|
_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[] |
Runtime toggle IDM_EDIT_TOGGLE_PASTEBOARD; /B enables at startup. Not persisted (process-local). IsPasteBoardActive() in Notepad3.h is the cross-TU query.
- Mutex with Tail (
FileWatching.MonitoringLog): each mode 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 — but 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= no separator; anything else = verbatim. Separator is also suppressed on the first paste after enable and when the caret is at a line start.
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\.
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 → <ModuleDir>\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.
styleLexXXX.c defines an EDITLEXER (see any existing file for the struct), register in the 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.
- 26 locales under
language\np3_LANG_COUNTRY\. Language DLLs are separate projects in the solution. - Invariant: every
IDS_MUI_*defined incommon_res.hmust exist in all 26strings_*.rcfiles. A missing entry breaks the corresponding language DLL build. For bulk insertions across locales, use a.venv/Scripts/python.exescript —sed/perl\nescaping is unreliable in Cygwin. - Same rule for
IDS_LEX_*— these live inlanguage\np3_*\lexer_*.rc(notstrings_*.rc). .rcfiles are UTF-8 WITHOUT BOM, CRLF line endings. Never write with BOM. UseBuild\rc_to_utf8.cmdto strip accidental BOMs. In PowerShell use[System.Text.UTF8Encoding]::new($false); in Python write\r\nexplicitly.Build\Notepad3.ini,Build\minipath.iniare UTF-8 WITH BOM (EF BB BF). Preserve it.
#define IDS_MUI_XXX <id>inlanguage\common_res.h(13xxx errors/warnings, 14xxx info/prompts).- Add the English string to
language\np3_en_us\strings_en_us.rc. - Add the same English text as placeholder to all other 25 locale files (translators update later).
- Display via
InfoBoxLng()/MessageBoxLng(); check withIsYesOkay().Settings.MuteMessageBeepcontrols silent vs. sound — provide both paths.
- INI sits beside the exe. No registry. Runs on defaults if no INI exists (user creates via "Save Settings Now").
- Admin redirect:
Notepad3.ini=<path>in[Notepad3]redirects to a per-user path (up to 2 levels). Redirect targets ARE auto-created viaCreateIniFileEx(). Paths.IniFile= active writable INI;Paths.IniFileDefault= recovery fallback.- Init flow:
FindIniFile()→TestIniFile()→CreateIniFile()→LoadSettings(). SettingsVersiondefaults toCFG_VER_CURRENTwhen missing — empty/new INI gets current defaults, not legacy treatment.bIniFileFromScratchis set when INI is 0 bytes, cleared afterSaveAllSettings(). While set,MuiLanguage.csuppresses writingPreferredLanguageLocaleNameto keep fresh INIs clean.- Empty lexer sections are pruned in
Style_ToIniSection()viaIniSectionGetKeyCount()afterStyle_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 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.
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). Passing CPI withbLoad=trueproduces wrong results. Reference:MRU_Save()usesfalse.- Defaults that depend on
DefaultEncoding(e.g.SkipANSICodePageDetection,LoadASCIIasUTF8) must be recalculated in_SaveSettings()beforeSAVE_VALUE_IF_NOT_EQ_DEFAULTfires, since encoding can change at runtime. See thebCurrentEncUTF8block.
FileSave()/FileLoad()(inNotepad3.c) →FileIO()→EditSaveFile()/EditLoadFile()(inEdit.c).- Atomic save via temp file +
ReplaceFileWcontrolled bySettings2.AtomicFileSave. - Error codes land in
Globals.dwLastError; checkERROR_ACCESS_DENIED,ERROR_PATH_NOT_FOUNDbefore falling back to generic error. - File watching (
InstallFileWatching()) usesFindFirstChangeNotificationWon the parent directory. MustInstallFileWatching(false)before save andInstallFileWatching(true)after.
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):
| 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 |
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). Success: SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)). Reference pattern in CreateIniFile() (Config.cpp).
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.
Replacement backref syntax: $0-$99, \0-\9, ${name}, ${+name}.
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.
src\DarkMode\ — Windows 10/11 dark mode via IAT hooks on uxtheme/user32 (stub DLLs included).
Supported: Win32 (x86), x64, x64_AVX2, ARM64. ARM 32-bit is not supported — the Release|ARM solution config maps to Win32.
- Both ARM64 and x64 define
_WIN64. Use_M_ARM64(or helperNP3_BUILD_ARM64inTypeDefs.h) to distinguish. - Rendering default:
SC_TECHNOLOGY_DIRECTWRITERETAIN(2) instead ofSC_TECHNOLOGY_DIRECTWRITE(1) — preserves Direct2D back buffer, avoids flicker on Qualcomm Adreno + Win11 25H2 DWM. Main window also getsWS_EX_COMPOSITED. User can override viaRenderingTechnology/ View menu. - Build config:
CETCompat=false(CET is x86/x64 only),TargetMachine=MachineARM64,_WIN64defined. Fix scripts:Build\scripts\FixARM64{CETCompat,CrossCompile,OutDir}.ps1. - grepWin: no native ARM64 — uses x64 exe via emulation (
#if defined(_M_ARM64)inNotepad3.c). MsgThemeChanged()wraps bar recreate / lexer reset / restyle inWM_SETREDRAW FALSE/TRUEand does a singleRedrawWindow()at end. DarkModeRedrawWindow()inListViewUtil.hppomitsRDW_ERASEto avoid background flash.
- Formatting: LLVM-based
.clang-formatinsrc\— 4-space indent, Stroustrup braces, left-aligned pointers, no column limit, no include sorting..editorconfigenforces UTF-8/CRLF; Lexilla code uses tabs (upstream preserved). - Strings:
strsafe.hthroughout; deprecated string functions disabled. - Types:
DocPos/DocPosU/DocLn(not rawint).cpi_enc_tfor encodings.HSTRINGW/HPATHL(opaque) instead of rawWCHAR*.NOMINMAXis global — usemin()/max()or typed equivalents. - Scintilla: always use
SciCall.hwrappers. Add missing wrappers there. Naming:DeclareSciCall{V|R}{0|01|1|2}— V=void, R=return; 0/1/2 = param count;01= lParam-only. Themsgarg is the suffix afterSCI_. - 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. InfoBoxLng()returnsMAKELONG(button, mode)— never compare/switch on the raw return when the suppression key is non-NULL: a saved-answer replay sets HIWORD too, soresult == IDNOis false. UseIsYesOkay()/IsRetryContinue()/IsNoCancelClose()(insrc/Dialogs.h), or extract viaINFOBOX_ANSW(r)(LOWORD) andINFOBOX_MODE(r)(HIWORD). Switching on the raw value is a latent bug if a suppression key is ever added.
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.
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).
.venv\ (Python 3.14) for scripting tasks — bulk locale edits, code generation, etc.
.venv/Scripts/python.exe <script.py>
.venv/Scripts/pip.exe install <package>System Python is not installed; python3 fails. Python beats sed/perl under Cygwin for literal string insertions (reliable \r\n).