diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 6d9231bfb..3ae38ed89 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -114,6 +114,29 @@ Resource-based MUI system with 27+ locales. Each locale has a directory `np3_LAN 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. +#### 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 + +```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) +``` + +The `msg` argument is the suffix after `SCI_` (e.g., `UNDO` for `SCI_UNDO`). + +### 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 @@ -124,3 +147,12 @@ Add missing wrapper calls to `SciCall.h` if needed. ### 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. + +### INI file / portable-app design + +Notepad3 follows a **portable-app** design for its configuration file (`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()`. diff --git a/src/Config/Config.cpp b/src/Config/Config.cpp index 2a10f2c39..f99b92ba2 100644 --- a/src/Config/Config.cpp +++ b/src/Config/Config.cpp @@ -1000,10 +1000,23 @@ static bool _HandleIniFileRedirect(LPCWSTR lpszSecName, LPCWSTR lpszKeyName, HPA Path_Sanitize(hredirect); Path_FreeExtra(hredirect, 0); if (_CheckAndSetIniFile(hredirect)) { + // Redirect target exists — use it Path_Swap(hpth_in_out, hredirect); } else { - Path_CanonicalizeEx(hpth_in_out, Paths.ModuleDirectory); + // Redirect target doesn't exist — try to create it + // (admin explicitly configured this redirect) + Path_ExpandEnvStrings(hredirect); + if (Path_IsRelative(hredirect)) { + Path_RelativeToApp(hredirect, false, false, true); + } + if (CreateIniFile(hredirect, NULL)) { + Path_Swap(hpth_in_out, hredirect); + } else { + // Creation failed — remember target for "Save Settings Now" + Path_Reset(Paths.IniFileDefault, Path_Get(hredirect)); + Path_CanonicalizeEx(hpth_in_out, Paths.ModuleDirectory); + } } result = true; } @@ -1093,7 +1106,12 @@ extern "C" bool CreateIniFile(const HPATHL hini_pth, DWORD* pdwFileSize_out) Path_RemoveFileSpec(hdir_path); if (Path_IsNotEmpty(hdir_path)) { // Use SHCreateDirectoryExW to create all intermediate directories - fixes #5075 - SHCreateDirectoryExW(NULL, Path_Get(hdir_path), NULL); + HRESULT const hr = SHCreateDirectoryExW(NULL, Path_Get(hdir_path), NULL); + if (FAILED(hr) && (hr != HRESULT_FROM_WIN32(ERROR_ALREADY_EXISTS))) { + Path_Release(hdir_path); + if (pdwFileSize_out) { *pdwFileSize_out = 0UL; } + return false; + } } Path_Release(hdir_path); @@ -1113,6 +1131,8 @@ extern "C" bool CreateIniFile(const HPATHL hini_pth, DWORD* pdwFileSize_out) StrgFormat(msg, L"CreateIniFile(%s): FAILED TO CREATE INITIAL INI FILE!", fileName); MsgBoxLastError(StrgGet(msg), 0); StrgDestroy(msg); + if (pdwFileSize_out) { *pdwFileSize_out = 0UL; } + return false; } } else { HANDLE hFile = CreateFileW(Path_Get(hini_pth), @@ -1137,7 +1157,7 @@ extern "C" bool CreateIniFile(const HPATHL hini_pth, DWORD* pdwFileSize_out) *pdwFileSize_out = dwFileSize; } - return CanAccessPath(Paths.IniFile, GENERIC_WRITE); + return CanAccessPath(hini_pth, GENERIC_WRITE); } return false; } diff --git a/src/Edit.c b/src/Edit.c index 5d4da6227..7268373be 100644 --- a/src/Edit.c +++ b/src/Edit.c @@ -8300,13 +8300,21 @@ static void _UpdateIndicators(const int indicator, const int indicator2nd, break; // wrong match } + // URL-specific: if match ends with single-quote and is preceded by one, strip trailing quote + DocPos mlen_adj = mlen; + if ((indicator == INDIC_NP3_HYPERLINK) && (mlen_adj > 4)) { + if ((SciCall_GetCharAt(end - 1) == '\'') && (start > 0) && (SciCall_GetCharAt(start - 1) == '\'')) { + --mlen_adj; + } + } + _ClearIndicatorInRange(indicator, indicator2nd, start_m, end); SciCall_SetIndicatorCurrent(indicator); - SciCall_IndicatorFillRange(start, mlen); + SciCall_IndicatorFillRange(start, mlen_adj); if (indicator2nd >= 0) { SciCall_SetIndicatorCurrent(indicator2nd); - SciCall_IndicatorFillRange(start, mlen); + SciCall_IndicatorFillRange(start, mlen_adj); } // next occurrence diff --git a/src/Helpers.c b/src/Helpers.c index 3d3978b61..61948e850 100644 --- a/src/Helpers.c +++ b/src/Helpers.c @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -295,6 +296,36 @@ HRESULT PrivateSetCurrentProcessExplicitAppUserModelID(PCWSTR AppID) } +//============================================================================= +// +// SetWindowAppUserModelID() +// +HRESULT SetWindowAppUserModelID(HWND hwnd, PCWSTR AppID) +{ + if (!hwnd || StrIsEmpty(AppID)) { + return S_OK; + } + if (StringCchCompareXI(AppID, L"(default)") == 0) { + return S_OK; + } + + IPropertyStore* pps = NULL; + HRESULT hr = SHGetPropertyStoreForWindow(hwnd, &IID_IPropertyStore, (void**)&pps); + if (SUCCEEDED(hr)) { + PROPVARIANT pv; + PropVariantInit(&pv); + pv.vt = VT_LPWSTR; + hr = SHStrDupW(AppID, &pv.pwszVal); + if (SUCCEEDED(hr)) { + hr = pps->lpVtbl->SetValue(pps, &PKEY_AppUserModel_ID, &pv); + PropVariantClear(&pv); + } + pps->lpVtbl->Release(pps); + } + return hr; +} + + //============================================================================= // // IsProcessElevated() diff --git a/src/Helpers.h b/src/Helpers.h index 617604cff..cfe9d1f02 100644 --- a/src/Helpers.h +++ b/src/Helpers.h @@ -417,6 +417,7 @@ static inline int IsFullHD(HWND hwnd, int resX, int resY) // ---------------------------------------------------------------------------- HRESULT PrivateSetCurrentProcessExplicitAppUserModelID(PCWSTR AppID); +HRESULT SetWindowAppUserModelID(HWND hwnd, PCWSTR AppID); bool IsProcessElevated(); //bool IsUserAdmin(); diff --git a/src/Notepad3.c b/src/Notepad3.c index 3e88ba203..91fea3377 100644 --- a/src/Notepad3.c +++ b/src/Notepad3.c @@ -1773,6 +1773,7 @@ HWND InitInstance(const HINSTANCE hInstance, int nCmdShow) DrawMenuBar(hwndMain); Globals.hwndMain = hwndMain; // make main window globaly available + SetWindowAppUserModelID(hwndMain, Settings2.AppUserModelID); HPATHL hfile_pth = Path_Copy(s_pthArgFilePath); FileLoadFlags fLoadFlags = FLF_None;