15 KiB
CLAUDE.md
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
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
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.
Code Map (src\)
| 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) |
Menu / Command Dispatch
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[] |
Clipboard Monitoring (Pasteboard Mode)
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.
Vendored Libraries
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 Integration (grepWin\)
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.
Adding a Lexer
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.
Localization
- 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.
Adding a string resource
#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.
Portable INI Design
- 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.
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). 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.
File I/O
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.
Long-Path / PathLib wrappers
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 |
Path comparison
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).
Creating directories
Use Path_CreateDirectoryEx(hpth). Success: SUCCEEDED(hr) || (hr == HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS)). Reference pattern in CreateIniFile() (Config.cpp).
PCRE2
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.
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 — 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.
Conventions
- 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.
WriteAccessBuf — dangling pointer anti-pattern
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).
Python Environment
.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).