Add configurable offline layout correction and latest Windows release

This commit is contained in:
2026-10-27 23:50:04 +02:00
parent f683e4178c
commit f1af998029
25 changed files with 142458 additions and 27 deletions
+153 -1
View File
@@ -1,3 +1,4 @@
#include "autocorrect.h"
#include "ocr.h"
#include "richtext.h"
#include "storage.h"
@@ -29,10 +30,34 @@ void rejects(const std::function<void()> &f, const char *name)
require(thrown, name);
}
} // namespace
int main()
int main(int argc, char **argv)
{
if (argc > 1 && std::string(argv[1]) == "--hidden-start-window")
{
HWND w = CreateWindowExW(0, L"STATIC", L"KSwitcher visibility regression", WS_POPUP, 0, 0, 1, 1,
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
ks::showRequestedWindow(w);
bool visible = w && IsWindowVisible(w);
if (w)
DestroyWindow(w);
return visible ? 0 : 1;
}
try
{
STARTUPINFOW startup{sizeof(startup)};
startup.dwFlags = STARTF_USESHOWWINDOW;
startup.wShowWindow = SW_HIDE;
PROCESS_INFORMATION child{};
std::wstring command = L"\"" + ks::executable() + L"\" --hidden-start-window";
require(CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr,
&startup, &child),
"start visibility test with hidden startup state");
DWORD wait = WaitForSingleObject(child.hProcess, 10000), exitCode = 1;
GetExitCodeProcess(child.hProcess, &exitCode);
CloseHandle(child.hThread);
CloseHandle(child.hProcess);
require(wait == WAIT_OBJECT_0 && exitCode == 0,
"first explicit window request is visible after hidden process launch");
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
HWND richParent = CreateWindowExW(0, L"STATIC", L"Rich preview test", WS_POPUP, 0, 0, 300, 200,
nullptr, nullptr, GetModuleHandleW(nullptr), nullptr);
@@ -93,6 +118,100 @@ int main()
require(ks::convert(L"hello привет", en, ru) == L"руддщ привет", "mixed text preserves target words");
require(ks::convert(L"htdm.dftim", en, ru) == L"ревьюваешь", "punctuation in words");
std::vector<ks::Layout> layouts{en, ru, uk};
ks::AutoCorrector corrector;
ks::Settings autoSettings;
require(!autoSettings.autoCorrect, "auto-correction defaults off");
autoSettings.cycle = {L"en", L"uk"};
corrector.configure(L"dictionaries", autoSettings.autoLanguages);
auto suggested = corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings);
require(suggested && suggested->text == L"привіт " && suggested->target == 2,
"dictionary corrects Ukrainian and retains trailing space");
suggested = corrector.suggest(L"lsv ", 0, layouts, autoSettings);
require(suggested && suggested->text == L"дім " && suggested->target == 2,
"three-letter Ukrainian word is corrected");
autoSettings.autoMinLength = 4;
require(!corrector.suggest(L"lsv ", 0, layouts, autoSettings),
"configured minimum excludes short words");
require(!corrector.suggest(L"(lsv); ", 0, layouts, autoSettings),
"punctuation does not count towards minimum length");
autoSettings.autoMinLength = 3;
require(!corrector.suggest(L"the ", 0, layouts, autoSettings),
"known three-letter English word stays unchanged");
require(!corrector.suggest(L"ls ", 0, layouts, autoSettings),
"two-letter input remains below automatic correction minimum");
suggested = corrector.suggest(L"ljvfiyz ", 0, layouts, autoSettings);
require(suggested && suggested->text == L"домашня " && suggested->target == 2,
"less frequent Ukrainian inflections are eligible for correction");
require(autoSettings.autoMaxRank == 0, "default frequency limit includes entire dictionary");
autoSettings.autoMaxRank = 21944;
require(!corrector.suggest(L"ljvfiyz ", 0, layouts, autoSettings), "rank above limit is excluded");
autoSettings.autoMaxRank = 21945;
require(corrector.suggest(L"ljvfiyz ", 0, layouts, autoSettings).has_value(),
"rank limit is inclusive");
autoSettings.autoMaxRank = 1;
require(!corrector.suggest(L"hello ", 0, layouts, autoSettings),
"rank limit never disables known-word protection");
autoSettings.autoMaxRank = 0;
suggested = corrector.suggest(L"(ljvf ", 0, layouts, autoSettings);
require(suggested && suggested->text == L"(дома ", "opening parenthesis is preserved");
for (auto pair : {std::pair{L"(ljvf) ", L"(дома) "},
{L"[ljvf] ", L"[дома] "},
{L"{ljvf} ", L"{дома} "},
{L"«ljvf» ", L"«дома» "},
{L"“ljvf” ", L"“дома” "},
{L"ljvf) ", L"дома) "},
{L"([{ljvf}]), ", L"([{дома}]), "}})
{
suggested = corrector.suggest(pair.first, 0, layouts, autoSettings);
require(suggested && suggested->text == pair.second, "paired and closing delimiters preserved");
}
require(ks::autoClosingSelection(L")] "), "closing-only selection can be extended");
require(!ks::autoClosingSelection(L"word) ") && !ks::autoClosingSelection(L"@ "),
"selection extension excludes words and arbitrary punctuation");
require(!corrector.suggest(L"user@(ljvf) ", 0, layouts, autoSettings),
"wrapped email suffix is not a word");
autoSettings.autoPrefixChars += L"#";
autoSettings.autoSuffixChars += L"%";
suggested = corrector.suggest(L"#ljvf%; ", 0, layouts, autoSettings);
require(suggested && suggested->text == L"#дома%; ", "custom delimiters are preserved");
require(ks::autoClosingSelection(L"%; ", autoSettings), "custom closing selection is supported");
autoSettings.autoPrefixChars.clear();
require(!corrector.suggest(L"(ljvf ", 0, layouts, autoSettings),
"removed prefix delimiter is respected");
autoSettings.autoSuffixChars.clear();
require(!corrector.suggest(L"ljvf; ", 0, layouts, autoSettings),
"removed suffix delimiter is respected");
autoSettings.autoPrefixChars = ks::Settings{}.autoPrefixChars;
autoSettings.autoSuffixChars = ks::Settings{}.autoSuffixChars;
ks::Run parenthesized;
parenthesized.keys = L"(ljvf ";
require(ks::matchAutoInput(parenthesized, L"ljvf ", layouts, en) == 0,
"editor selection may omit the opening parenthesis");
require(ks::matchAutoInput(parenthesized, L"(ljvf ", layouts, en) == 0,
"editor selection may include the opening parenthesis");
parenthesized.keys = L"OLDljvf ";
require(ks::matchAutoInput(parenthesized, L"ljvf ", layouts, en) < 0,
"automatic verification never accepts an arbitrary word suffix");
suggested = corrector.suggest(L"Ghbdsn! ", 0, layouts, autoSettings);
require(suggested && suggested->text == L"Привіт! ", "title case and punctuation retained");
suggested = corrector.suggest(L"руддщ ", 2, layouts, autoSettings);
require(suggested && suggested->text == L"hello ", "dictionary corrects English");
for (auto word : {L"hello ", L"привіт ", L"GHBDSN ", L"ghBdsn ", L"ghbdsn123 ", L"user@ghbdsn ",
L"https://ghbdsn ", L"gh ", L"qzxqzxqzx ", L"oldghbdsn "})
require(!corrector.suggest(word, 0, layouts, autoSettings), "unsafe or known word unchanged");
autoSettings.autoNever = {L"ghbdsn"};
require(!corrector.suggest(L"Ghbdsn ", 0, layouts, autoSettings), "personal exception ignores case");
autoSettings.autoNever.clear();
autoSettings.cycle = {L"en"};
require(!corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings), "disabled target layout excluded");
autoSettings.cycle = {L"en", L"uk"};
corrector.reject(L"Ghbdsn; ");
require(!corrector.suggest(L"ghbdsn ", 0, layouts, autoSettings),
"undo rejection survives punctuation");
require(corrector.memoryBytes() < 3 * 1024 * 1024, "two dictionaries fit within 3 MiB");
require(!ks::autoCorrectionAllowed(nullptr, autoSettings), "missing focused editor fails closed");
ks::AutoCorrector missing;
rejects([&] { missing.configure(L"missing-dictionaries", {L"en"}); }, "missing dictionary rejected");
require(ks::convertTo(L"ghbdtn", layouts, 1, 0) == L"привет", "hint conversion");
require(ks::convertTo(L"привет", layouts, 1, 1) == L"привет", "same source is unchanged");
require(ks::convertTo(L"?", layouts, 1, 0) == L",", "punctuation conversion");
@@ -228,6 +347,13 @@ int main()
}
ks::Settings settings;
settings.elevatedAutostart = true;
settings.autoCorrect = true;
settings.autoMaxRank = 10000;
settings.autoMinLength = 5;
settings.autoPrefixChars = L"(#";
settings.autoSuffixChars = L");%";
settings.autoNever = {L"myWord"};
settings.autoExcluded = {L"Code.exe"};
settings.cycle = {L"en", L"uk"};
settings.snippets = {{L";address", L"Line 1\r\n{cursor}Line 2"}};
ks::saveSettings(temp / L"settings.json", settings);
@@ -235,6 +361,32 @@ int main()
require(restored.snippets[0].text == settings.snippets[0].text, "settings snippet roundtrip");
require(restored.cycle == settings.cycle, "layout order roundtrip");
require(restored.elevatedAutostart, "elevated autostart setting roundtrip");
require(restored.autoMaxRank == 10000, "frequency rank limit roundtrip");
require(restored.autoMinLength == 5, "minimum word length roundtrip");
require(ks::loadSettings(temp / L"defaults.json", {}).autoMinLength == 3,
"missing minimum length defaults to three");
for (int badLength : {0, 33})
{
auto invalidLength = settings;
invalidLength.autoMinLength = badLength;
rejects([&] { ks::validateSettings(invalidLength); }, "invalid minimum word length rejected");
}
require(restored.autoPrefixChars == settings.autoPrefixChars &&
restored.autoSuffixChars == settings.autoSuffixChars,
"custom punctuation settings roundtrip");
auto invalidCharacters = settings;
invalidCharacters.autoPrefixChars = L"a";
rejects([&] { ks::validateSettings(invalidCharacters); },
"letters cannot be treated as word delimiters");
require(ks::loadSettings(temp / L"defaults.json", {}).autoMaxRank == 0,
"missing frequency limit defaults to all words");
auto invalidRank = settings;
invalidRank.autoMaxRank = -1;
rejects([&] { ks::validateSettings(invalidRank); }, "negative frequency limit rejected");
require(restored.autoCorrect && restored.autoLanguages == settings.autoLanguages &&
restored.autoNever == settings.autoNever &&
restored.autoExcluded == settings.autoExcluded,
"auto-correction settings roundtrip");
require(!ks::loadSettings(temp / L"defaults.json", {}).elevatedAutostart,
"elevated autostart is opt-in for existing and default settings");
auto invalid = settings;