diff --git a/src/app.cpp b/src/app.cpp index 54f44c7..ca3710f 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1490,11 +1490,17 @@ void App::transform(int command, int snippet) } Run tail = recent; tail.keys = tail.keys.substr(tail.keys.size() - sn.trigger.size()); - if (!tx.select(sn.trigger.size())) - return; autoSelected = true; reason = L"snippet-verification-mismatch"; - text = tx.copy(); + if (farSnippet) + { + if (!tx.select(sn.trigger.size())) return; + text = tx.copy(); + } + else + text = tx.copyRange(sn.trigger.size(), tail.keys, + [&](const std::wstring &candidate) { return tail.match(candidate, available, physicalLayout()) >= 0; }, + [&](const std::wstring &candidate) { return tail.matchSuffix(candidate, available, physicalLayout()) >= 0; }); if (tail.match(text, available, physicalLayout()) < 0) return; automatic = true; diff --git a/src/platform.cpp b/src/platform.cpp index 7eb0e6c..7ccaf56 100644 --- a/src/platform.cpp +++ b/src/platform.cpp @@ -435,7 +435,7 @@ bool needsSelectionDelay(HWND target) { const auto name = lower(processName(target)); return name == L"ms-teams" || name == L"msedgewebview2" || - name == L"chrome" || name == L"msedge"; + name == L"chrome" || name == L"msedge" || name == L"chatgpt" || name == L"codex"; } bool selectPrevious(size_t count, const std::function &valid) { diff --git a/tests/chat_profiles.cpp b/tests/chat_profiles.cpp index 3ac3fe8..1bcbe1e 100644 --- a/tests/chat_profiles.cpp +++ b/tests/chat_profiles.cpp @@ -21,6 +21,7 @@ void contextChecks() ks::validateSettings(scoped); require(ks::snippetAppPriority(scoped.snippets[1], L"ms-teams") > ks::snippetAppPriority(scoped.snippets[0], L"ms-teams"), "app snippet precedes global regardless of list order"); require(ks::snippetAppPriority(scoped.snippets[1], L"Telegram.exe") == 0, "restricted snippet cannot run in another app"); + require(ks::snippetAppPriority(scoped.snippets[1], L"Outlook.EXE") == 2, "restricted snippet also runs in its second application"); require(ks::snippetAppPriority(scoped.snippets[0], L"") == 1, "legacy/global snippets remain available"); auto scopedRoundtrip = ks::decodeSettings(ks::encodeSettings(scoped), {}); require(scopedRoundtrip.snippets[1].apps == std::vector{L"ms-teams", L"outlook"}, "scopes normalize and persist"); @@ -257,7 +258,14 @@ int main() require(IsDlgButtonChecked(snippets,314)==BST_UNCHECKED, "old snippet shows everywhere"); CheckDlgButton(snippets,314,BST_CHECKED); SendMessageW(snippets,WM_COMMAND,314,0); - SetDlgItemTextW(snippets,315,L"Notepad.exe, MS-Teams.EXE"); + SetDlgItemTextW(snippets,315,L"Notepad.exe"); + auto secondApp = SendDlgItemMessageW(snippets,316,CB_ADDSTRING,0,(LPARAM)L"MS-Teams.EXE"); + SendDlgItemMessageW(snippets,316,CB_SETCURSEL,secondApp,0); + SendMessageW(snippets,WM_COMMAND,317,0); + SendMessageW(snippets,WM_COMMAND,317,0); + wchar_t appList[128]{}; + GetDlgItemTextW(snippets,315,appList,128); + require(std::wstring(appList)==L"Notepad.exe, MS-Teams.EXE", "Add app appends a second application without duplicates"); SendMessageW(snippets,WM_COMMAND,IDOK,0); require(!app.snippetsWindow && app.settings.snippets[0].apps==std::vector{L"notepad",L"ms-teams"}, "snippet editor saves selected applications"); require(!saveEnabled() && status()==L"No unsaved changes.","separately saved snippet edit leaves Settings clean"); diff --git a/tests/integration.cpp b/tests/integration.cpp index f087558..ecd2ddb 100644 --- a/tests/integration.cpp +++ b/tests/integration.cpp @@ -22,6 +22,7 @@ namespace ks::App *app{}; HWND host{}, field{}; ks::Clip saved; +bool snippetRecoveryOnly = false; bool captured = false, failed = false; bool conversionOnly = false; bool typosOnly = false; @@ -588,10 +589,29 @@ LRESULT CALLBACK procedure(HWND w, UINT msg, WPARAM wp, LPARAM lp) require(ks::readClipboard(app->window).text() == sentinel, "selected and repeated conversions restore clipboard"); finish(); break; - case 62: + case 64: saved = ks::readClipboard(app->window, 64 * 1024 * 1024); captured = true; app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel)); + app->settings.snippets = {{L";repo-changes-jira", L"expanded{cursor}"}}; + ActivateKeyboardLayout(app->layouts[en].handle, 0); + prepare(L"prefix ;repo-changes-jira", false); + record(L";repo-changes-jira"); + droppedSelectionKeys = 10; + enterPresses = 0; + app->transform(0, 0); + break; + case 65: + require(text() == L"prefix expanded", "long snippet recovers partial selection and preserves prefix"); + require(droppedSelectionKeys == 0 && enterPresses == 0, "snippet recovery consumes missing arrows without Enter"); + require(ks::readClipboard(app->window).text() == sentinel, "snippet recovery restores clipboard"); + if (snippetRecoveryOnly) { finish(); break; } + stage = 62; + break; + case 62: + if (!captured) saved = ks::readClipboard(app->window, 64 * 1024 * 1024); + captured = true; + app->ownSequence = ks::writeClipboard(app->window, ks::textClip(sentinel)); app->settings.snippets = {{L";r", L"unexpected{enter}"}}; ActivateKeyboardLayout(app->layouts[en].handle, 0); prepare(L"prefix ", false); @@ -1038,8 +1058,9 @@ int main(int argc, char **argv) SetWindowSubclass(field, observeEnter, 1, 0); if (typosOnly || (argc > 1 && std::string(argv[1]) == "--typing-only")) stage = 20; - if (argc > 1 && std::string(argv[1]) == "--snippets-only") - stage = 62; + snippetRecoveryOnly = argc > 1 && std::string(argv[1]) == "--snippet-recovery-only"; + if (snippetRecoveryOnly || (argc > 1 && std::string(argv[1]) == "--snippets-only")) + stage = 64; SetTimer(host, 1, 100, nullptr); auto forwarding = SetWindowsHookExW(WH_KEYBOARD_LL,forwardingHook,GetModuleHandleW(nullptr),0); require(forwarding!=nullptr,"downstream forwarding fixture installed before application hook"); diff --git a/tests/sync.cpp b/tests/sync.cpp index c13e61c..91d6002 100644 --- a/tests/sync.cpp +++ b/tests/sync.cpp @@ -145,9 +145,9 @@ int main() } { Settings scoped = local; - scoped.snippets.push_back({L"test", L"Only Teams", L"teams-scope", {L"ms-teams"}}); + scoped.snippets.push_back({L"test", L"Work apps", L"teams-scope", {L"ms-teams", L"outlook"}}); auto wire=sharedSyncSettings(scoped); - require(restoreSyncSettings(wire, local, {}).snippets[1].apps==std::vector{L"ms-teams"}, "sync preserves app scope"); + require(restoreSyncSettings(wire, local, {}).snippets[1].apps==std::vector{L"ms-teams", L"outlook"}, "sync preserves multiple app scopes"); auto leftScope=j,rightScope=j; leftScope["snippets"].push_back({{"id","left-scope"},{"trigger","same"},{"text","left"},{"apps",Json::array({"notepad"})}}); rightScope["snippets"].push_back({{"id","right-scope"},{"trigger","same"},{"text","right"},{"apps",Json::array({"ms-teams"})}});