// SPDX-License-Identifier: LGPL-2.1-or-later #include "edit_hook.h" #include "menu_bridge.h" #include "scintilla_messages.h" #include #include #define SCINTILLA_SUBCLASS_ID 0x57415331 #define MAX_RANGE_BYTES 4096 #define SCINTILLA_CUT_MESSAGE L"CthulhuWineScintillaCut" #define SCINTILLA_STATE_PROPERTY L"CthulhuWineScintillaState" typedef struct ScintillaState { BOOL busy, destroyed; BOOL pendingCut; ptrdiff_t cutLength; WCHAR cutText[1024]; } ScintillaState; static LRESULT query(HWND window, UINT message, WPARAM parameter) { return SendMessageW(window, message, parameter, 0); } static void selection(HWND window, ptrdiff_t *start, ptrdiff_t *end) { ptrdiff_t caret = query(window, SCI_GETCURRENTPOS, 0); ptrdiff_t anchor = query(window, SCI_GETANCHOR, 0); *start = caret < anchor ? caret : anchor; *end = caret > anchor ? caret : anchor; } static BOOL simple_selection(HWND window) { return query(window, SCI_GETSELECTIONS, 0) == 1 && !query(window, SCI_SELECTIONISRECTANGLE, 0); } static void read_range(HWND window, ptrdiff_t start, ptrdiff_t end, WCHAR *output) { output[0] = 0; if (start < 0 || end <= start) return; if (end - start > MAX_RANGE_BYTES) { // Stop on a character boundary, including UTF-8 and DBCS characters. end = query(window, SCI_POSITIONBEFORE, start + MAX_RANGE_BYTES + 1); } if (end <= start || end - start > MAX_RANGE_BYTES) return; char bytes[MAX_RANGE_BYTES + 1] = {0}; ScintillaTextRange range = {{start, end}, bytes}; LRESULT copied = SendMessageW(window, SCI_GETTEXTRANGEFULL, 0, (LPARAM)&range); if (copied <= 0 || copied > MAX_RANGE_BYTES) return; UINT codePage = query(window, SCI_GETCODEPAGE, 0); if (!codePage) { // Single-byte documents use the default style's Windows character set. DWORD charset = query(window, SCI_STYLEGETCHARACTERSET, 32); if (charset == DEFAULT_CHARSET) codePage = GetACP(); else { CHARSETINFO info; if (!TranslateCharsetInfo((DWORD *)(UINT_PTR)charset, &info, TCI_SRCCHARSET)) return; codePage = info.ciACP; } } WCHAR converted[MAX_RANGE_BYTES + 1]; int units = MultiByteToWideChar(codePage, 0, bytes, (int)copied, converted, MAX_RANGE_BYTES); if (units > 1023) units = 1023; if (units && converted[units - 1] >= 0xd800 && converted[units - 1] <= 0xdbff) --units; if (units > 0) CopyMemory(output, converted, units * sizeof(WCHAR)); output[units] = 0; } static void current_line(HWND window, WCHAR *output) { ptrdiff_t caret = query(window, SCI_GETCURRENTPOS, 0); LRESULT line = query(window, SCI_LINEFROMPOSITION, caret); read_range(window, query(window, SCI_POSITIONFROMLINE, line), query(window, SCI_GETLINEENDPOSITION, line), output); } static LRESULT CALLBACK scintilla_proc(HWND window, UINT message, WPARAM wp, LPARAM lp, UINT_PTR id, DWORD_PTR reference) { ScintillaState *state = (ScintillaState *)reference; if (message == WM_NCDESTROY) { RemoveWindowSubclass(window, scintilla_proc, id); RemovePropW(window, EDIT_STATE_PROPERTY); RemovePropW(window, SCINTILLA_STATE_PROPERTY); state->destroyed = TRUE; if (!state->busy) free(state); return DefSubclassProc(window, message, wp, lp); } if (state->busy) return DefSubclassProc(window, message, wp, lp); if (message == RegisterWindowMessageW(SCINTILLA_CUT_MESSAGE)) { if (state->pendingCut) { state->pendingCut = FALSE; if (query(window, SCI_GETLENGTH, 0) < state->cutLength) { ptrdiff_t start, end; selection(window, &start, &end); wine_access_notify_text(window, WINE_ACCESS_TEXT_DELETED, state->cutText, start, end, WINE_ACCESS_TEXT_SCINTILLA); } } return 0; } BOOL navigation = message == WM_KEYDOWN && (wp == VK_LEFT || wp == VK_RIGHT || wp == VK_UP || wp == VK_DOWN || wp == VK_HOME || wp == VK_END || wp == VK_PRIOR || wp == VK_NEXT); BOOL requestedFocus = message == RegisterWindowMessageW(EDIT_FOCUS_MESSAGE); BOOL focus = message == WM_SETFOCUS || requestedFocus; BOOL selected = message == SCI_SETSEL; BOOL control = (GetKeyState(VK_CONTROL) & 0x8000) != 0; BOOL backspace = message == WM_KEYDOWN && wp == VK_BACK; BOOL deleting = backspace || (message == WM_KEYDOWN && wp == VK_DELETE) || (message == WM_KEYDOWN && control && wp == 'X') || message == WM_CUT || message == WM_CLEAR || message == SCI_CUT || message == SCI_CLEAR; if (!navigation && !focus && !selected && !deleting) return DefSubclassProc(window, message, wp, lp); state->busy = TRUE; ptrdiff_t oldStart, oldEnd; selection(window, &oldStart, &oldEnd); ptrdiff_t oldLength = query(window, SCI_GETLENGTH, 0); WCHAR removed[1024] = {0}; if (deleting && !query(window, SCI_GETREADONLY, 0) && simple_selection(window)) { ptrdiff_t first = oldStart, last = oldEnd; // Ctrl+Backspace/Delete can remove a word; this hook only predicts plain // character deletion. Selected ranges and cut are always explicit. if (first == last && !control) { if (backspace && first) first = query(window, SCI_POSITIONBEFORE, first); else if (message == WM_KEYDOWN && wp == VK_DELETE && last < oldLength) last = query(window, SCI_POSITIONAFTER, last); // Treat CRLF as one line break for deletion feedback. if (backspace && first && query(window, SCI_GETCHARAT, first) == '\n' && query(window, SCI_GETCHARAT, first - 1) == '\r') --first; if (!backspace && last < oldLength && query(window, SCI_GETCHARAT, last - 1) == '\r' && query(window, SCI_GETCHARAT, last) == '\n') ++last; } read_range(window, first, last, removed); } // Selection feedback reads the old text before the application can change it. WCHAR unselected[1024] = {0}; if ((navigation || selected) && oldStart != oldEnd && simple_selection(window)) read_range(window, oldStart, oldEnd, unselected); LRESULT result = requestedFocus ? 0 : DefSubclassProc(window, message, wp, lp); if (state->destroyed) { free(state); return result; } ptrdiff_t start, end; selection(window, &start, &end); WCHAR text[1024] = {0}; UINT kind = WINE_ACCESS_TEXT_READ; BOOL report = FALSE; if (focus) { current_line(window, text); kind = WINE_ACCESS_TEXT_FOCUS; report = TRUE; } else if ((navigation || selected) && (start != oldStart || end != oldEnd)) { if (start != end && simple_selection(window)) { ptrdiff_t first = start, last = end; kind = WINE_ACCESS_TEXT_SELECTED; if (oldStart != oldEnd && start == oldStart) { first = end < oldEnd ? end : oldEnd; last = end > oldEnd ? end : oldEnd; if (end < oldEnd) kind = WINE_ACCESS_TEXT_UNSELECTED; } else if (oldStart != oldEnd && end == oldEnd) { first = start < oldStart ? start : oldStart; last = start > oldStart ? start : oldStart; if (start > oldStart) kind = WINE_ACCESS_TEXT_UNSELECTED; } read_range(window, first, last, text); } else if (unselected[0]) { lstrcpynW(text, unselected, 1024); kind = WINE_ACCESS_TEXT_UNSELECTED; } else if (navigation && (wp == VK_UP || wp == VK_DOWN || wp == VK_PRIOR || wp == VK_NEXT || ((wp == VK_HOME || wp == VK_END) && control))) { current_line(window, text); } else { ptrdiff_t caret = query(window, SCI_GETCURRENTPOS, 0); ptrdiff_t last = query(window, SCI_POSITIONAFTER, caret); if (control && navigation) { last = SendMessageW(window, SCI_WORDENDPOSITION, caret, FALSE); // Windows word navigation includes trailing spaces. ptrdiff_t limit = query(window, SCI_GETLENGTH, 0); while (last < limit && last - caret < MAX_RANGE_BYTES && query(window, SCI_GETCHARAT, last) == ' ') ++last; } read_range(window, caret, last, text); } report = TRUE; } else if (deleting && removed[0] && query(window, SCI_GETLENGTH, 0) < oldLength) { lstrcpynW(text, removed, 1024); kind = backspace ? WINE_ACCESS_TEXT_BACKSPACED : WINE_ACCESS_TEXT_DELETED; report = TRUE; } if (report) { UINT flags = WINE_ACCESS_TEXT_SCINTILLA; if (query(window, SCI_GETREADONLY, 0)) flags |= WINE_ACCESS_TEXT_READONLY; wine_access_notify_text(window, kind, text, start, end, flags); if (deleting) state->pendingCut = FALSE; } state->busy = FALSE; return result; } BOOL wine_access_watch_scintilla(HWND window) { if (GetPropW(window, EDIT_STATE_PROPERTY)) return FALSE; ScintillaState *state = calloc(1, sizeof(*state)); if (!state) return FALSE; if (!SetWindowSubclass(window, scintilla_proc, SCINTILLA_SUBCLASS_ID, (DWORD_PTR)state)) { free(state); return FALSE; } if (!SetPropW(window, EDIT_STATE_PROPERTY, state) || !SetPropW(window, SCINTILLA_STATE_PROPERTY, state)) { RemovePropW(window, EDIT_STATE_PROPERTY); RemoveWindowSubclass(window, scintilla_proc, SCINTILLA_SUBCLASS_ID); free(state); return FALSE; } HMODULE module; GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, (LPCWSTR)(UINT_PTR)scintilla_proc, &module); return TRUE; } void wine_access_scintilla_input(HWND window, const MSG *message) { if (message->message != WM_KEYDOWN || message->wParam != 'X' || !(GetKeyState(VK_CONTROL) & 0x8000)) return; ScintillaState *state = (ScintillaState *)GetPropW(window, SCINTILLA_STATE_PROPERTY); if (!state || state->busy || query(window, SCI_GETREADONLY, 0) || !simple_selection(window)) return; ptrdiff_t start, end; selection(window, &start, &end); if (start == end) return; // Notepad++ translates Ctrl+X before dispatch and calls Scintilla's direct // function, bypassing the subclass. Capture only the selected bounded range // before translation, then check the result on the application's queue. state->cutLength = query(window, SCI_GETLENGTH, 0); read_range(window, start, end, state->cutText); state->pendingCut = state->cutText[0] && PostMessageW(window, RegisterWindowMessageW(SCINTILLA_CUT_MESSAGE), 0, 0); }