Files
strans/tests/engine_test.c
Hojun-Cho d9e27e8b01 engine: a Hanja reading answers with the words it begins
hanjaquery matched the reading exactly, so a word was reachable only
once every syllable of it had been typed, and the popup went blank on
the way there.  Measured over map/hanja.dict's 187,304 readings:
95,024 proper prefixes of a word answer with nothing today, and 66,731
of those are the keystroke just before the word completes -- 대한민 is
one, so 대한민국 looks absent until the last key lands.

dictprefix walks the entry at a node before its children, so the
reading's own conversions keep their place and the words follow: 34,441
readings gain candidates and none of the 187,304 has its existing order
changed.  A jamo reading is untouched, since the two keyspaces do not
meet and ㅁ has no children.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 13:54:42 +09:00

2291 lines
59 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "strans.c"
#include "test.h"
static int typekeys(struct ct*, char*, Str*);
static int draindraw(Drawcmd*);
/* One key from the owner, as imhandlekey drives the engine. */
static int
keystroke(u32int ks, u32int mod, Str *com)
{
int eaten;
eaten = keymeaningful(ks) && transition(ks, mod, com);
redraw();
return eaten;
}
void
testengineinit(int lang)
{
init();
im.l = getlang(lang);
}
void
testenginehandle(Keyreq *req)
{
imhandlekey(req);
}
void*
testengineowner(void)
{
return activeowner;
}
void
testenginepreedit(Str *preedit)
{
impre(&im, preedit);
}
void
testenginecaret(Caret *at)
{
*at = caret;
}
void
vietnamese_state_lifetime(struct ct *t)
{
Str com;
init();
im.l = getlang(LangVI);
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, !keystroke(Kret, 0, &com));
checkstr(t, "committed Telex", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangVI);
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, !keystroke(Kesc, 0, &com));
checkstr(t, "Escape commits Telex", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangVI);
im.pre = mkstr("as");
im.raw = mkstr("as");
sclear(&com);
CT_CHECK(t, dotrans('q', &com));
checkstr(t, "completed Telex", "á", &com);
checkstr(t, "next preedit", "q", &im.pre);
checkstr(t, "next raw input", "q", &im.raw);
}
void
engine_clears_candidates(struct ct *t)
{
Str com;
init();
im.l = getlang(LangKO);
im.pre = mkstr("");
im.nkouho = 2;
im.sel = 1;
sclear(&com);
dotrans('k', &com);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_backspace_clears_candidates(struct ct *t)
{
Lang *lang;
Str com, shown;
lang = getlang(LangJP);
init();
im.l = lang;
sclear(&com);
CT_CHECK(t, keystroke('n', 0, &com));
CT_CHECK(t, keystroke('a', 0, &com));
impre(&im, &shown);
checkstr(t, "before backspace", "", &shown);
im.kouho[0] = mkstr("stale-candidate");
im.nkouho = 1;
im.sel = 0;
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "chosen candidate backspace", "na", &im.raw);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspace commit", "", &com);
checkstr(t, "backspace deletes the kana shown", "", &im.raw);
impre(&im, &shown);
checkstr(t, "shown preedit after backspace", "", &shown);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
}
void
engine_selects_visible_candidate(struct ct *t)
{
static const struct { int n, sel; Rune key; char *want; } cases[] = {
{ Maxdisp, 0, '9', "c9" },
{ Maxkouho, Maxdisp, '1', "c10" },
{ Maxkouho, 2*Maxdisp, '9', "c27" },
};
char name[8];
Str com;
int i, j;
for(i = 0; i < nelem(cases); i++){
init();
for(j = 0; j < cases[i].n; j++){
snprint(name, sizeof name, "c%d", j+1);
im.kouho[j] = mkstr(name);
}
im.nkouho = cases[i].n;
im.sel = cases[i].sel;
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
checkstr(t, cases[i].want, cases[i].want, &com);
CT_EQ_INT(t, 0, im.nkouho);
}
}
void
engine_candidate_shortcut_modifiers(struct ct *t)
{
static u32int mods[] = { Mshift, Mctrl, Malt, Msuper };
Str com, selected;
int i;
for(i = 0; i < nelem(mods); i++){
init();
im.kouho[9] = mkstr("must-not-select");
im.nkouho = 10;
im.sel = -1;
selected = im.kouho[9];
sclear(&com);
CT_CHECK(t, !keystroke('1', mods[i], &com));
CT_CHECK(t, scmp(&com, &selected) != 0);
}
}
static Keyres
ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
Caret *caret)
{
Channel *reply;
Keyreq req;
Keyres res;
reply = chancreate(sizeof(Keyres), 1);
memset(&req, 0, sizeof req);
req.owner = owner;
req.clientpre = cap;
req.op = op;
req.ks = key;
req.mod = mod;
if(caret != nil)
req.caret = *caret;
req.reply = reply;
imhandlekey(&req);
chanrecv(reply, &res);
chanfree(reply);
return res;
}
static Keyres
ownerrequestat(void *owner, int op, Rune key, u32int mod, Caret *caret)
{
return ownerrequestatcap(owner, 0, op, key, mod, caret);
}
static Keyres
ownerrequest(void *owner, int op, Rune key, u32int mod)
{
return ownerrequestat(owner, op, key, mod, nil);
}
static Keyres
ownerrequestcap(void *owner, int cap, int op, Rune key, u32int mod)
{
return ownerrequestatcap(owner, cap, op, key, mod, nil);
}
void
engine_active_owner_lifecycle(struct ct *t)
{
Keyres res;
Str shown;
char a, b, c;
init();
im.l = getlang(LangJP);
res = ownerrequest(&a, Keypress, 'k', 0);
CT_CHECK(t, res.eaten);
res = ownerrequest(&a, Keypress, 'a', 0);
checkstr(t, "first owner preedit", "", &res.preedit);
CT_EQ_PTR(t, &a, activeowner);
res = ownerrequest(&b, Keypress, 'n', 0);
checkstr(t, "takeover resets old preedit", "", &res.preedit);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&a, Keyreset, 0, 0);
checkstr(t, "the taken owner gets its reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n);
impre(&im, &shown);
checkstr(t, "stale reset preserves owner", "", &shown);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&b, Keypress, 'y', 0);
res = ownerrequest(&b, Keypress, 'a', 0);
checkstr(t, "current owner continues", "にゃ", &res.preedit);
ownerrequest(&a, Keyrelease, 0, 0);
impre(&im, &shown);
checkstr(t, "stale release preserves owner", "にゃ", &shown);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&b, Keyrelease, 0, 0);
checkstr(t, "release hands the reading back", "にゃ", &res.commit);
CT_EQ_PTR(t, nil, activeowner);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
ownerrequest(&c, Keypress, 0, 0);
CT_EQ_PTR(t, nil, activeowner);
res = ownerrequest(&c, Keypress, 'k', 0);
CT_EQ_PTR(t, &c, activeowner);
checkstr(t, "next owner first key", "k", &res.preedit);
/* Typing is being heard from, as much as a reset or a release is. */
init();
im.l = getlang(LangJP);
ownerrequest(&a, Keypress, 'k', 0);
ownerrequest(&a, Keypress, 'a', 0);
ownerrequest(&b, Keypress, 'n', 0);
CT_EQ_PTR(t, &b, activeowner);
res = ownerrequest(&a, Keypress, 'k', 0);
checkstr(t, "the taken owner types and gets its reading back", "",
&res.commit);
CT_EQ_PTR(t, &a, activeowner);
res = ownerrequest(&b, Keyreset, 0, 0);
checkstr(t, "the owner it took from keeps its own", "", &res.commit);
}
void
engine_active_owner_reset(struct ct *t)
{
Caret at;
Keyres res;
char owner;
memset(&at, 0, sizeof at);
at.valid = 1;
at.x = 10;
at.y = 20;
at.h = 14;
init();
im.l = getlang(LangJP);
ownerrequestat(&owner, Keypress, 'k', 0, &at);
res = ownerrequest(&owner, Keypress, 'a', 0);
checkstr(t, "preedit before reset", "", &res.preedit);
res = ownerrequest(&owner, Keyreset, 0, 0);
CT_EQ_PTR(t, &owner, activeowner);
checkstr(t, "reset hands the reading back", "", &res.commit);
CT_EQ_INT(t, 0, res.preedit.n);
res = ownerrequest(&owner, Keypress, 'n', 0);
checkstr(t, "same owner after reset", "", &res.preedit);
}
void
engine_active_owner_caret(struct ct *t)
{
Caret a, moved;
Drawcmd dc;
char one, two;
memset(&a, 0, sizeof a);
a.valid = 1;
a.x = 10;
a.y = 20;
a.h = 14;
moved = a;
moved.x = 80;
init();
draindraw(nil);
im.l = getlang(LangEN);
ownerrequestat(&one, Keypress, 'x', 0, &a);
CT_CHECK(t, caret.valid);
CT_EQ_INT(t, 10, caret.x);
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 80, caret.x);
CT_EQ_INT(t, 0, draindraw(nil));
im.kouho[0] = mkstr("candidate");
im.nkouho = 1;
im.sel = 0;
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 80, dc.caret.x);
moved.x = 90;
ownerrequestat(&two, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 80, caret.x);
CT_EQ_INT(t, 0, draindraw(nil));
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 90, caret.x);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 90, dc.caret.x);
ownerrequestat(&one, Keycaret, 0, 0, &moved);
CT_EQ_INT(t, 0, draindraw(nil));
ownerrequestat(&one, Keypress, 'x', 0, &moved);
CT_EQ_INT(t, 0, draindraw(nil));
moved.x = 100;
ownerrequestat(&one, Keypress, 'x', 0, &moved);
CT_EQ_INT(t, 100, caret.x);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 100, dc.caret.x);
ownerrequest(&two, Keypress, 'n', 0);
CT_EQ_PTR(t, &two, activeowner);
CT_CHECK(t, !caret.valid);
}
void
engine_vietnamese_client_preedit(struct ct *t)
{
static const struct {
char *keys;
char *shown;
} cases[] = {
{ "as", "á" },
{ "aa", "â" },
{ "ddoong", "đông" },
{ "hoaf", "hoà" },
{ "quas", "quá" },
};
Drawcmd dc;
Keyres res;
Str com, shown;
char *k, owner;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangVI);
draindraw(nil);
sclear(&com);
for(k = cases[i].keys; *k != '\0'; k++){
res = ownerrequestcap(&owner, 1,
Keypress, *k, 0);
sappend(&com, &res.commit);
if(!res.eaten)
sputr(&com, *k);
}
/* What the client shows, what the popup shows, and what
* Enter commits are the same composed text. */
shown = com;
sappend(&shown, &res.preedit);
checkstr(t, "client preedit", cases[i].shown, &shown);
clientpre = 0;
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, scmp(&dc.pre, &res.preedit));
CT_CHECK(t, !keystroke(Kret, 0, &com));
checkstr(t, "commit", cases[i].shown, &com);
}
}
void
engine_commit_contract(struct ct *t)
{
static const struct {
Rune key;
char *one;
char *all;
} cases[] = {
{ 'z', "", "ㅋㅋㅋ" },
{ 'r', "", "ㄱㄱㄱ" },
{ 'd', "", "ㅇㅇㅇ" },
{ 'k', "", "ㅏㅏㅏ" },
};
Keyres res;
Str all, com;
char owner;
int i, j;
for(j = 0; j < nelem(cases); j++){
init();
im.l = getlang(LangKO);
sclear(&all);
for(i = 0; i < 3; i++){
res = ownerrequestcap(&owner, 1,
Keypress, cases[j].key, 0);
CT_CHECK(t, res.eaten);
checkstr(t, "repeated jamo commit",
i == 0 ? "" : cases[j].one, &res.commit);
checkstr(t, "repeated jamo preedit", cases[j].one,
&res.preedit);
sappend(&all, &res.commit);
}
sappend(&all, &res.preedit);
checkstr(t, "repeated jamo total", cases[j].all, &all);
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "return commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
im.kouho[0] = mkstr("c1");
im.kouho[1] = mkstr("c2");
im.nkouho = 2;
im.sel = 0;
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected candidate", "c2", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
im.nkouho = 1;
im.sel = -1;
CT_CHECK(t, !keystroke('!', 0, &com));
checkstr(t, "uneaten-key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
/* Without candidates the movement keys commit like any other. */
init();
im.l = getlang(LangKO);
sclear(&com);
if(!typekeys(t, "dy", &com))
return;
CT_CHECK(t, !keystroke(Kdown, 0, &com));
checkstr(t, "arrow commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, "ka", &com))
return;
/* A key outside the map commits the reading and passes on. */
CT_CHECK(t, !keystroke(0xf008, 0, &com));
checkstr(t, "non-ASCII key commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
}
void
engine_telex_history_bound(struct ct *t)
{
Str com;
Rune tone;
int i;
init();
im.l = getlang(LangVI);
sclear(&com);
CT_CHECK(t, keystroke('a', 0, &com));
for(i = 0; i < Maxrunes; i++){
tone = i % 2 == 0 ? 's' : 'f';
if(!CT_CHECK(t, keystroke(tone, 0, &com)))
break;
}
checkstr(t, "bounded Telex commit", "à", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
}
void
engine_korean_modifiers_and_backspace(struct ct *t)
{
static const struct {
Rune key;
char *plain;
char *shifted;
} cases[] = {
{ 'r', "", "" }, { 'e', "", "" },
{ 'q', "", "" }, { 't', "", "" },
{ 'w', "", "" }, { 'o', "", "" },
{ 'p', "", "" },
};
Drawcmd dc;
Str com;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
checkstr(t, "plain Korean key", cases[i].plain, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, Mshift, &com));
checkstr(t, "shifted Korean key", cases[i].shifted, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key - 'a' + 'A', 0, &com));
checkstr(t, "Caps Lock Korean key", cases[i].plain, &im.pre);
init();
im.l = getlang(LangKO);
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, Mshift, &com));
checkstr(t, "Shift+Caps Korean key", cases[i].shifted, &im.pre);
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rkr", &com)){
checkstr(t, "Korean engine preedit", "", &im.pre);
draindraw(nil);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Korean final backspace", "", &im.pre);
if(CT_CHECK(t, draindraw(&dc) == 1))
checkstr(t, "Korean backspace redraw", "", &dc.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Korean vowel backspace", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
CT_CHECK(t, !keystroke(Kback, 0, &com));
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, !keystroke(Kback, Mctrl, &com));
checkstr(t, "Ctrl+Backspace commits and passes", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
}
}
void
engine_direct_language_modes(struct ct *t)
{
Drawcmd dc;
Str com;
char owner;
init();
draindraw(nil);
sclear(&com);
CT_EQ_INT(t, LangEN, im.l->lang);
/* A switch shows the mode it switched to until the next key. */
CT_CHECK(t, keystroke('s', Mctrl, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "", &dc.pre);
CT_CHECK(t, keystroke('t', Mctrl, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "A", &dc.pre);
CT_CHECK(t, !keystroke('a', 0, &com));
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, dc.pre.n);
CT_CHECK(t, !keystroke('a', 0, &com));
CT_EQ_INT(t, 0, draindraw(nil));
CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->trans);
CT_CHECK(t, !keystroke('a', 0, &com));
CT_CHECK(t, !keystroke('Z', Mshift, &com));
checkstr(t, "English direct commit", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.lang);
CT_CHECK(t, keystroke('s', Mctrl, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_EQ_PTR(t, nil, im.l->mapname);
CT_EQ_PTR(t, nil, im.l->map);
CT_EQ_PTR(t, nil, im.l->dictname);
CT_CHECK(t, strcmp(getlang(LangHANJA)->dictname, "hanja") == 0);
CT_CHECK(t, keystroke('r', 0, &com));
checkstr(t, "Korean without map", "", &im.pre);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
/* Leaving takes the mark away as surely as typing does. */
init();
draindraw(nil);
ownerrequest(&owner, Keypress, 's', Mctrl);
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "mode mark", "", &dc.pre);
ownerrequest(&owner, Keyrelease, 0, 0);
if(CT_CHECK(t, draindraw(&dc) > 0))
CT_EQ_INT(t, 0, dc.pre.n);
}
void
engine_language_switch_state(struct ct *t)
{
static const struct {
char *name;
Rune key;
int mod;
int lang;
char *pre;
char *raw;
char *commit;
int stale;
} steps[] = {
{ "select Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone", 's', 0, LangVI, "as", "as", "", 0 },
{ "switch to Korean", 's', Mctrl, LangKO, "", "", "á", 1 },
{ "Korean initial", 'r', 0, LangKO, "", "", "", 0 },
{ "Korean syllable", 'k', 0, LangKO, "", "", "", 0 },
{ "switch to Vietnamese", 'v', Mctrl, LangVI, "", "", "", 0 },
{ "Vietnamese a again", 'a', 0, LangVI, "a", "a", "", 0 },
{ "Vietnamese tone again", 's', 0, LangVI, "as", "as", "", 0 },
{ "commit Vietnamese", 't', Mctrl, LangEN, "", "", "á", 0 },
};
char where[80];
Str com;
int eaten, i;
init();
for(i = 0; i < nelem(steps); i++){
if(steps[i].stale){
im.kouho[0] = mkstr("stale");
im.nkouho = 1;
im.sel = -1;
}
sclear(&com);
eaten = keystroke(steps[i].key, steps[i].mod, &com);
if(eaten != 1){
CT_ERRORF(t, "%s: want eaten 1, got %d",
steps[i].name, eaten);
break;
}
if(im.l == nil || im.l->lang != steps[i].lang){
CT_ERRORF(t, "%s: want language %d, got %d",
steps[i].name, steps[i].lang,
im.l == nil ? -1 : im.l->lang);
break;
}
snprint(where, sizeof where, "%s preedit", steps[i].name);
if(!checkstr(t, where, steps[i].pre, &im.pre))
break;
snprint(where, sizeof where, "%s raw", steps[i].name);
if(!checkstr(t, where, steps[i].raw, &im.raw))
break;
snprint(where, sizeof where, "%s commit", steps[i].name);
if(!checkstr(t, where, steps[i].commit, &com))
break;
if(im.nkouho != 0 || im.sel != -1){
CT_ERRORF(t, "%s: want candidates 0 and selection -1, got %d and %d",
steps[i].name, im.nkouho, im.sel);
break;
}
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, !keystroke('p', Mctrl, &com));
checkstr(t, "Korean Ctrl passthrough", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
}
init();
im.l = getlang(LangVI);
sclear(&com);
if(typekeys(t, "as", &com)){
CT_CHECK(t, !keystroke('p', Mctrl, &com));
checkstr(t, "Vietnamese Ctrl passthrough", "á", &com);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
}
}
typedef struct Searchfix Searchfix;
struct Searchfix
{
Im im;
Search search;
Lang *dictlang;
Trie *dict;
int clientpre;
Drawcmd lastdraw;
};
static int
draindraw(Drawcmd *last)
{
Drawcmd dc = {0};
int n;
n = 0;
while(channbrecv(drawc, &dc) > 0){
n++;
if(last != nil)
*last = dc;
}
return n;
}
static void
setcandidates(int n)
{
char name[8];
int i;
clearkouho();
for(i = 0; i < n; i++){
snprint(name, sizeof name, "c%d", i+1);
im.kouho[i] = mkstr(name);
}
im.nkouho = n;
}
static void
candidatebegin(int n)
{
init();
im.l = getlang(LangJP);
draindraw(nil);
setcandidates(n);
}
static int
checkcandidatepage(struct ct *t, char *where, int first, int n, int sel)
{
Drawcmd dc;
char want[8];
redraw();
if(!CT_CHECK(t, draindraw(&dc) > 0))
return 0;
if(!CT_EQ_INT(t, n, dc.nkouho) || !CT_EQ_INT(t, sel, dc.sel) ||
!CT_EQ_INT(t, first, dc.first) ||
!CT_EQ_INT(t, im.nkouho, dc.total))
return 0;
if(n == 0)
return 1;
snprint(want, sizeof want, "c%d", first+1);
if(!checkstr(t, where, want, &dc.kouho[0]))
return 0;
snprint(want, sizeof want, "c%d", first+n);
return checkstr(t, where, want, &dc.kouho[n-1]);
}
void
engine_candidate_page_metadata(struct ct *t)
{
static const struct {
int total;
int sel;
int first;
int shown;
int local;
} cases[] = {
{ 1, 0, 0, 1, 0 },
{ 9, 8, 0, 9, 8 },
{ 10, 8, 0, 9, 8 },
{ 10, 9, 9, 1, 0 },
{ 18, 8, 0, 9, 8 },
{ 18, 17, 9, 9, 8 },
{ 19, 0, 0, 9, 0 },
{ 32, 8, 0, 9, 8 },
{ 32, 17, 9, 9, 8 },
{ 32, 26, 18, 9, 8 },
{ 32, 31, 27, 5, 4 },
};
int i;
for(i = 0; i < nelem(cases); i++){
candidatebegin(cases[i].total);
im.sel = cases[i].sel;
checkcandidatepage(t, "candidate page metadata",
cases[i].first, cases[i].shown, cases[i].local);
}
}
void
engine_candidate_page_snapshots(struct ct *t)
{
Drawcmd dc;
/* Clearing a visible popup must publish an empty snapshot. */
candidatebegin(1);
redraw();
draindraw(nil);
setcandidates(0);
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
CT_EQ_INT(t, 0, dc.first);
CT_EQ_INT(t, 0, dc.total);
}
}
void
engine_candidate_page_movement(struct ct *t)
{
static const struct {
int n;
int sel;
Rune key;
int want;
} cases[] = {
{ 1, 0, Kup, 0 },
{ 9, 8, Kdown, 8 },
{ 10, 8, Kdown, 9 },
{ 10, 9, Kup, 8 },
{ 18, 8, Kpgdown, 17 },
{ 19, 17, Kdown, 18 },
{ 19, 18, Kup, 17 },
{ 32, 26, Kdown, 27 },
{ 32, 27, Kup, 26 },
{ 32, 0, Kpgup, 0 },
{ 32, 9, Kpgup, 0 },
{ 32, 22, Kpgdown, 31 },
{ 32, 31, Kpgup, 22 },
{ 32, 31, Kpgdown, 31 },
{ 32, -1, Kdown, 0 },
{ 32, -1, Kup, 0 },
{ 32, -1, Kpgdown, 9 },
{ 32, -1, Kpgup, 0 },
};
int first, i, n;
Str com;
for(i = 0; i < nelem(cases); i++){
candidatebegin(cases[i].n);
im.sel = cases[i].sel;
sclear(&com);
CT_CHECK(t, keystroke(cases[i].key, 0, &com));
CT_EQ_INT(t, cases[i].want, im.sel);
first = cases[i].want / Maxdisp * Maxdisp;
n = cases[i].n - first;
if(n > Maxdisp)
n = Maxdisp;
checkcandidatepage(t, "moved candidate page", first, n,
cases[i].want-first);
}
}
void
engine_candidate_completion(struct ct *t)
{
Str com;
init();
im.l = getlang(LangJP);
im.pre = mkstr("reading");
setcandidates(1);
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "unselected candidate commits reading", "reading", &com);
CT_EQ_INT(t, 0, im.nkouho);
init();
im.l = getlang(LangJP);
im.pre = mkstr("reading");
setcandidates(2);
sclear(&com);
CT_CHECK(t, keystroke(Ktab, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Ktab, Mshift, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Tab steps through the candidates", "c2", &com);
candidatebegin(2);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "explicit candidate", "c2", &com);
candidatebegin(2);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "ordinary zero reading", "reading", &com);
candidatebegin(0);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Enter without candidates", "reading", &com);
/* A digit types on until Space engages the list. */
candidatebegin(3);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, !keystroke('2', 0, &com));
checkstr(t, "digit before the list is engaged", "reading", &com);
CT_EQ_INT(t, 0, im.nkouho);
candidatebegin(3);
im.pre = mkstr("reading");
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke('2', 0, &com));
checkstr(t, "digit after the list is engaged", "c2", &com);
/* A digit with no such row on the page is an ordinary key: it
* confirms the candidate paged to and passes on. */
candidatebegin(32);
im.pre = mkstr("reading");
im.sel = 27;
sclear(&com);
CT_CHECK(t, !keystroke('9', 0, &com));
checkstr(t, "unavailable short-page digit", "c28", &com);
CT_EQ_INT(t, 0, im.nkouho);
}
static void
setdict(Trie *dict, char *key, char *val)
{
trieput(dict, key, strlen(key), val, strlen(val));
}
static void
searchsave(Searchfix *f, int lang)
{
f->im = im;
f->search = search;
f->dictlang = getlang(lang);
f->dict = f->dictlang->dict;
f->clientpre = clientpre;
f->lastdraw = lastdraw;
draindraw(nil);
}
static void
emojibegin(Searchfix *f, int showpre)
{
searchsave(f, LangEMOJI);
f->dictlang->dict = trienew();
setdict(f->dictlang->dict, "a", "A B");
setdict(f->dictlang->dict, "", "B C");
setdict(f->dictlang->dict, "alpha", "α");
setdict(f->dictlang->dict, "smil", "😀 😄");
setdict(f->dictlang->dict, "smile", "😀 😄");
setdict(f->dictlang->dict, "웃음", "😀 😄 😂");
setdict(f->dictlang->dict, "えがお", "😀 😄 😊");
setdict(f->dictlang->dict, "エガオ", "😀 😄 😊");
setdict(f->dictlang->dict, "heart", "❤️");
setdict(f->dictlang->dict, "coffee", "☕️");
setdict(f->dictlang->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
setdict(f->dictlang->dict, "^0", "");
setdict(f->dictlang->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
setdict(f->dictlang->dict, "<", "← ≤ ♥ ≠");
setdict(f->dictlang->dict, "many",
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
init();
im.l = getlang(LangKO);
clientpre = showpre ? 0 : 1;
}
static void
searchend(Searchfix *f)
{
Trie *dict;
dict = f->dictlang->dict;
f->dictlang->dict = f->dict;
trieclose(dict);
draindraw(nil);
im = f->im;
search = f->search;
clientpre = f->clientpre;
lastdraw = f->lastdraw;
}
void
engine_popup_preedit_capability(struct ct *t)
{
Drawcmd dc;
Keyres kres;
Lang *jp;
Trie *saved;
char client, inactive;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = trienew();
setdict(jp->dict, "", "");
init();
im.l = jp;
draindraw(nil);
ownerrequestcap(&client, 1, Keypress, 'k', 0);
ownerrequestcap(&client, 1, Keypress, 'a', 0);
CT_EQ_INT(t, 1, im.nkouho);
redraw();
if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "client-preedit candidate", "", &dc.kouho[0]);
}
kres = ownerrequestcap(&client, 0, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
checkstr(t, "capability response preedit", "", &kres.preedit);
if(CT_CHECK(t, draindraw(&dc) > 0)){
checkstr(t, "popup preedit", "", &dc.pre);
CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "popup candidate", "", &dc.kouho[0]);
}
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "retained popup candidate", "", &im.kouho[0]);
kres = ownerrequestcap(&client, 0, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&inactive, 1, Keycap, 0, 0);
CT_CHECK(t, !kres.eaten);
CT_EQ_INT(t, 0, kres.preedit.n);
CT_EQ_PTR(t, &client, activeowner);
CT_EQ_INT(t, 0, clientpre);
CT_EQ_INT(t, 0, draindraw(nil));
kres = ownerrequestcap(&client, 1, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
checkstr(t, "restored client preedit", "", &kres.preedit);
if(CT_CHECK(t, draindraw(&dc) > 0)){
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho);
CT_EQ_INT(t, -1, dc.sel);
checkstr(t, "restored client candidate", "", &dc.kouho[0]);
}
CT_EQ_INT(t, 1, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "retained client candidate", "", &im.kouho[0]);
kres = ownerrequestcap(&client, 1, Keycap, 0, 0);
CT_CHECK(t, kres.eaten);
CT_EQ_INT(t, 0, draindraw(nil));
ownerrequest(&client, Keyrelease, 0, 0);
draindraw(nil);
trieclose(jp->dict);
jp->dict = saved;
}
static void
hanjabegin(Searchfix *f, int lang)
{
searchsave(f, LangHANJA);
f->dictlang->dict = trienew();
setdict(f->dictlang->dict, "", "漢 韓");
setdict(f->dictlang->dict, "", "");
setdict(f->dictlang->dict, "", "");
setdict(f->dictlang->dict, "민국", "民國");
init();
im.l = getlang(lang);
}
static int
typekeys(struct ct *t, char *keys, Str *com)
{
for(; *keys != '\0'; keys++)
if(!CT_CHECK(t, keystroke((uchar)*keys, 0, com)))
return 0;
return 1;
}
void
engine_japanese_readings(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "nya", "にゃ" },
{ "n'ya", "んや" },
{ "nnya", "んや" },
{ "nnnya", "んにゃ" },
{ "konnnichiha", "こんにちは" },
{ "wi", "うぃ" },
{ "vu", "" },
{ "xtu", "" },
{ "axtu", "あっ" },
{ "lla", "っぁ" },
{ "matcha", "まっちゃ" },
{ "zi", "" },
{ "[", "" },
{ "kin'youbi", "きんようび" },
};
static const struct {
char *raw;
char *want;
} replay[] = {
{ "kitte", "きって" },
{ "matcha", "まっちゃ" },
{ "baggu", "ばっぐ" },
{ "kanj", "かんj" },
{ "n'ya", "んや" },
};
Str com, raw, shown;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangJP);
sclear(&com);
if(!typekeys(t, cases[i].keys, &com))
continue;
checkstr(t, cases[i].keys, "", &com);
impre(&im, &shown);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed reading", cases[i].want, &com);
}
for(i = 0; i < nelem(replay); i++){
raw = mkstr(replay[i].raw);
transstr(getlang(LangJP), nil, &raw, &shown);
checkstr(t, replay[i].raw, replay[i].want, &shown);
}
}
void
engine_japanese_candidates(struct ct *t)
{
Trie *saved;
Lang *jp;
Str com, shown;
jp = getlang(LangJP);
saved = jp->dict;
jp->dict = trienew();
setdict(jp->dict, "かんじ", "漢字 幹事");
init();
im.l = jp;
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "complete Japanese reading", "かんじ", &shown);
if(!CT_EQ_INT(t, 2, im.nkouho))
goto cleanup;
CT_EQ_INT(t, -1, im.sel);
checkstr(t, "candidate one", "漢字", &im.kouho[0]);
checkstr(t, "candidate two", "幹事", &im.kouho[1]);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "selected Kanji", "幹事", &com);
CT_EQ_INT(t, 0, im.nkouho);
/* Space converts: it steps through the candidates and wraps;
* typing on takes the chosen one; Backspace goes back. */
if(!typekeys(t, "kanji", &com))
goto cleanup;
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 2, im.sel);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(' ', Mshift, &com));
CT_EQ_INT(t, 2, im.sel);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, -1, im.sel);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke('w', 0, &com));
CT_CHECK(t, keystroke('o', 0, &com));
checkstr(t, "typing on takes the candidate", "漢字", &com);
impre(&im, &shown);
checkstr(t, "reading after the candidate", "", &shown);
sclear(&com);
CT_CHECK(t, keystroke(' ', 0, &com));
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "Space offers Katakana without candidates", "",
&im.kouho[0]);
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Katakana committed", "", &com);
CT_CHECK(t, !keystroke(' ', 0, &com));
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(' ', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho))
checkstr(t, "Katakana after the candidates", "カンジ",
&im.kouho[2]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* SKK's okuri-ari entries convert verbs: stem plus okurigana. */
setdict(jp->dict, "かk", "書 描");
setdict(jp->dict, "かく", "");
if(!typekeys(t, "kaku", &com))
goto cleanup;
if(CT_EQ_INT(t, 3, im.nkouho)){
checkstr(t, "exact candidate first", "", &im.kouho[0]);
checkstr(t, "okurigana put back", "書く", &im.kouho[1]);
checkstr(t, "second okuri candidate", "描く", &im.kouho[2]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "おおk", "");
if(!typekeys(t, "ookii", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "two-kana okurigana", "大きい", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* Shift on the okurigana asks for that split alone, first. */
sclear(&com);
if(!typekeys(t, "ka", &com))
goto cleanup;
CT_CHECK(t, keystroke('K', Mshift, &com));
CT_CHECK(t, keystroke('u', 0, &com));
if(CT_EQ_INT(t, 3, im.nkouho)){
checkstr(t, "marked okurigana first", "書く", &im.kouho[0]);
checkstr(t, "then the second", "描く", &im.kouho[1]);
checkstr(t, "then the reading's own", "", &im.kouho[2]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
setdict(jp->dict, "いt", "言 行");
setdict(jp->dict, "いっt", "言 行");
if(!typekeys(t, "itta", &com))
goto cleanup;
if(CT_EQ_INT(t, 2, im.nkouho)){
checkstr(t, "sokuon written in kana", "言った", &im.kouho[0]);
checkstr(t, "sokuon written in kana", "行った", &im.kouho[1]);
}
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* じ is keyed j, as SKK types it; ざずぜぞ keep z. */
setdict(jp->dict, "しんj", "");
setdict(jp->dict, "まz", "");
if(!typekeys(t, "shinjiru", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "じ okurigana", "信じる", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
if(!typekeys(t, "mazeru", &com))
goto cleanup;
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "ぜ okurigana", "混ぜる", &im.kouho[0]);
CT_CHECK(t, keystroke(Kesc, 0, &com));
/* A romaji typo stays in the reading for Backspace to mend. */
if(!typekeys(t, "kanjr", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "typo kept", "かんjr", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "typo half mended", "かんj", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
if(!typekeys(t, "ji", &com))
goto cleanup;
impre(&im, &shown);
checkstr(t, "typo mended", "かんじ", &shown);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "0 commits the reading", "かんじ", &com);
sclear(&com);
if(!typekeys(t, "wo", &com))
goto cleanup;
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "0 commits a reading without candidates", "", &com);
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(' ', 0, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, -1, im.sel);
impre(&im, &shown);
checkstr(t, "Escape goes back to the reading", "かんじ", &shown);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
CT_EQ_INT(t, 0, im.nkouho);
sclear(&com);
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke('s', Mctrl, &com));
checkstr(t, "language switch takes the candidate", "漢字", &com);
CT_EQ_INT(t, LangKO, im.l->lang);
im.l = jp;
/* Backspace looks a complete shorter reading up again. */
setdict(jp->dict, "かん", "");
if(!typekeys(t, "kanji", &com))
goto cleanup;
CT_CHECK(t, keystroke(Kback, 0, &com));
if(CT_EQ_INT(t, 1, im.nkouho))
checkstr(t, "shorter reading candidate", "", &im.kouho[0]);
cleanup:
trieclose(jp->dict);
jp->dict = saved;
}
void
engine_japanese_backspace_and_boundaries(struct ct *t)
{
Str com, shown;
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace deletes じ", "かん", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace deletes ん", "", &shown);
if(typekeys(t, "kyu", &com)){
CT_CHECK(t, keystroke(Kback, 0, &com));
impre(&im, &shown);
checkstr(t, "backspace deletes ゅ", "かき", &shown);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, !haspre(&im));
}
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, keystroke('k', Mctrl, &com));
checkstr(t, "Hiragana mode boundary", "かんじ", &com);
CT_EQ_INT(t, LangJPK, im.l->lang);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, keystroke('n', Mctrl, &com));
checkstr(t, "Katakana mode boundary", "", &com);
CT_EQ_INT(t, LangJP, im.l->lang);
}
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "kanji", &com)){
CT_CHECK(t, !keystroke(L'', 0, &com));
checkstr(t, "real input boundary", "かんじ", &com);
}
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "KA", &com)){
impre(&im, &shown);
checkstr(t, "uppercase romaji", "", &shown);
}
}
void
engine_katakana_sequences(struct ct *t)
{
static const struct {
char *keys;
char *want;
} cases[] = {
{ "kitte", "キッテ" },
{ "katta", "カッタ" },
{ "gakkou", "ガッコウ" },
{ "matcha", "マッチャ" },
{ "baggu", "バッグ" },
{ "xu", "" },
{ "vu", "" },
};
Str com, shown;
int i;
for(i = 0; i < nelem(cases); i++){
init();
im.l = getlang(LangJPK);
sclear(&com);
if(!typekeys(t, cases[i].keys, &com))
continue;
checkstr(t, cases[i].keys, "", &com);
impre(&im, &shown);
checkstr(t, cases[i].keys, cases[i].want, &shown);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "committed Katakana", cases[i].want, &com);
}
CT_EQ_PTR(t, nil, getlang(LangJPK)->dictname);
}
void
engine_emoji_single_candidate(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
if(CT_CHECK(t, draindraw(&dc) > 0))
checkstr(t, "empty search shows itself", "", &dc.pre);
if(typekeys(t, "alpha", &com)){
checkstr(t, "no automatic commit", "", &com);
checkstr(t, "raw English query", "alpha", &search.text);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "single candidate", "α", &im.kouho[0]);
CT_CHECK(t, draindraw(&dc) > 0);
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 1, dc.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "explicit candidate commit", "α", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
redraw();
CT_CHECK(t, draindraw(&dc) > 0);
CT_EQ_INT(t, 0, dc.pre.n);
CT_EQ_INT(t, 0, dc.nkouho);
}
searchend(&f);
}
void
engine_emoji_queries(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
emojibegin(&f, 1);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('a', 0, &com));
checkstr(t, "raw query wins", "a", &search.text);
CT_EQ_INT(t, 4, im.nkouho);
checkstr(t, "raw candidate", "A", &im.kouho[0]);
checkstr(t, "deduplicated candidate", "B", &im.kouho[1]);
checkstr(t, "raw prefix candidate", "α", &im.kouho[2]);
checkstr(t, "local candidate", "C", &im.kouho[3]);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "prefix popup query", "a", &dc.pre);
CT_EQ_INT(t, 4, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
checkstr(t, "English alias", "smile", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "backspaced query", "smil", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke('e', 0, &com));
CT_CHECK(t, keystroke('2', 0, &com));
checkstr(t, "selected English alias", "😄", &com);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "SMILE", &com)){
checkstr(t, "folded English alias", "smile", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "dntdma", &com)){
checkstr(t, "localized Korean query", "웃음", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "popup query", "웃음", &dc.pre);
CT_EQ_INT(t, 3, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "cancel commit", "", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
searchend(&f);
}
void
engine_emoji_japanese_and_multirune(struct ct *t)
{
Searchfix f;
Str com;
emojibegin(&f, 0);
init();
im.l = getlang(LangJP);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "egao", &com)){
checkstr(t, "localized Hiragana query", "えがお", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
init();
im.l = getlang(LangJPK);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "egao", &com)){
checkstr(t, "localized Katakana query", "エガオ", &search.text);
CT_EQ_INT(t, 3, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
init();
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "heart", &com)){
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "variation-selector emoji", "❤️", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "coffee", &com)){
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "multi-codepoint emoji", "☕️", &com);
}
searchend(&f);
}
void
engine_emoji_digit_aliases(struct ct *t)
{
static const struct {
char *name;
Rune prefix;
Rune digit;
char *want;
} cases[] = {
{ "^1", '^', '1', "¹" },
{ "_2", '_', '2', "" },
{ "<3", '<', '3', "" },
};
Searchfix f;
Str com;
int i;
emojibegin(&f, 0);
for(i = 0; i < nelem(cases); i++){
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke(cases[i].prefix, Mshift, &com));
CT_CHECK(t, im.nkouho >= cases[i].digit - '0');
CT_CHECK(t, keystroke(cases[i].digit, 0, &com));
checkstr(t, cases[i].name, cases[i].want, &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('^', Mshift, &com));
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "zero query", "^0", &search.text);
checkstr(t, "zero does not commit", "", &com);
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "zero alias", "", &com);
sclear(&com);
im.l = getlang(LangEN);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('9', 0, &com));
checkstr(t, "missing number query", "smile9", &search.text);
checkstr(t, "missing number commit", "", &com);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
searchend(&f);
}
void
engine_emoji_navigation(struct ct *t)
{
Drawcmd dc = {0};
Searchfix f;
Str com;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "many", &com)){
CT_EQ_INT(t, 12, im.nkouho);
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kpgdown, 0, &com));
CT_EQ_INT(t, Maxdisp, im.sel);
CT_CHECK(t, draindraw(&dc) > 0);
checkstr(t, "second-page first row", "c10", &dc.kouho[0]);
checkstr(t, "second-page last row", "c12", &dc.kouho[2]);
CT_EQ_INT(t, 3, dc.nkouho);
CT_EQ_INT(t, 0, dc.sel);
CT_CHECK(t, keystroke('1', 0, &com));
checkstr(t, "second-page row one", "c10", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Ktab, Mshift, &com));
CT_EQ_INT(t, 1, im.sel);
CT_CHECK(t, keystroke(Ktab, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
searchend(&f);
}
void
engine_search_candidate_keys(struct ct *t)
{
Searchfix f;
Str com;
emojibegin(&f, 0);
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "search default Enter", "😀", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "missing", &com)){
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, -1, im.sel);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "search query Enter", "missing", &com);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
CT_CHECK(t, keystroke('^', Mshift, &com));
CT_CHECK(t, keystroke('0', 0, &com));
checkstr(t, "search zero query", "^0", &search.text);
checkstr(t, "search zero commit", "", &com);
CT_CHECK(t, search.lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "many", &com)){
im.sel = Maxdisp;
CT_CHECK(t, keystroke('1', Mshift, &com));
checkstr(t, "modified search digit commit", "", &com);
checkstr(t, "modified search digit query", "many1", &search.text);
CT_CHECK(t, search.lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
searchend(&f);
}
void
engine_emoji_preedit_languages(struct ct *t)
{
Searchfix f;
Str com, shown;
emojibegin(&f, 0);
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, !keystroke(0, 0, &com));
impre(&im, &shown);
checkstr(t, "modifier preserves Japanese", "", &shown);
CT_CHECK(t, keystroke('E', Mctrl, &com));
checkstr(t, "committed Japanese preedit", "", &com);
CT_CHECK(t, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, !keystroke('E', Mctrl|Malt, &com));
CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_CHECK(t, !keystroke('E', Mctrl|Msuper, &com));
CT_EQ_INT(t, 0, search.lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_CHECK(t, !keystroke('V', Mctrl|Mshift, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('E', Mctrl, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('a', 0, &com));
CT_CHECK(t, !keystroke(0, 0, &com));
CT_CHECK(t, search.lang);
checkstr(t, "modifier preserves search", "a", &search.text);
CT_CHECK(t, !keystroke('P', Mctrl|Mshift, &com));
CT_CHECK(t, !search.lang);
sclear(&com);
if(typekeys(t, "na", &com)){
impre(&im, &shown);
checkstr(t, "Japanese resumed", "", &shown);
}
}
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "rk", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Korean preedit", "", &com);
if(typekeys(t, "smile", &com)){
CT_CHECK(t, keystroke('1', 0, &com));
checkstr(t, "Korean selection", "가😀", &com);
sclear(&com);
if(typekeys(t, "rk", &com))
checkstr(t, "Korean resumed", "", &im.pre);
}
}
init();
im.l = getlang(LangVI);
sclear(&com);
if(typekeys(t, "as", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Telex preedit", "á", &com);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.lang);
CT_EQ_PTR(t, getlang(LangVI), im.l);
sclear(&com);
if(typekeys(t, "as", &com)){
impre(&im, &shown);
checkstr(t, "Telex resumed", "á", &shown);
checkstr(t, "Telex raw resumed", "as", &im.raw);
}
}
searchend(&f);
}
void
engine_emoji_start_and_unknown(struct ct *t)
{
Searchfix f;
Str com;
char want[Maxrunes+1];
int i;
emojibegin(&f, 0);
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xyz", &com)){
/* Keys that match nothing show what they type in the language. */
CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com));
checkstr(t, "special commits shown query", "", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
im.l = getlang(LangEN);
/* A language switch commits the query like any other pending text. */
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xy", &com)){
CT_CHECK(t, keystroke('t', Mctrl, &com));
checkstr(t, "language switch commits query", "xy", &com);
CT_CHECK(t, !search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
}
/* So do the search keys, ending or changing the search. */
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xy", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "search key commits query", "xy", &com);
CT_CHECK(t, !search.lang);
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "xy", &com)){
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "other search commits query", "xy", &com);
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
}
sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com));
for(i = 0; i < Maxrunes; i++)
CT_CHECK(t, keystroke('q', 0, &com));
CT_EQ_INT(t, Maxrunes, search.raw.n);
CT_CHECK(t, search.lang);
CT_CHECK(t, !keystroke('z', 0, &com));
memset(want, 'q', Maxrunes);
want[Maxrunes] = '\0';
checkstr(t, "bounded query commit", want, &com);
CT_CHECK(t, !search.lang);
searchend(&f);
}
void
engine_hanja_search(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
CT_EQ_INT(t, LangHANJA, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
if(!typekeys(t, "gks", &com))
goto cleanup;
checkstr(t, "Hanja reading", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "navigated Hanja", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
cleanup:
searchend(&f);
}
void
engine_hanja_unknown_and_cancel(struct ct *t)
{
static const struct {
char *keys;
char *text;
} empty[] = {
{ "rmf", "" },
{ "r", "" },
{ "gksrmf", "한글" },
};
Searchfix f;
Str com;
int i;
hanjabegin(&f, LangKO);
for(i = 0; i < nelem(empty); i++){
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) ||
!typekeys(t, empty[i].keys, &com))
goto cleanup;
checkstr(t, "Hanja reading without candidates",
empty[i].text, &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Hanja commit without candidates",
empty[i].text, &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
}
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "cancelled Hanja", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, 0, search.lang);
cleanup:
searchend(&f);
}
/*
* A reading is the start of the longer words it begins: its own
* conversions come first, and past them theirs.
*/
void
engine_hanja_word_prefix(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)) ||
!typekeys(t, "als", &com))
goto cleanup;
checkstr(t, "the reading typed so far", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
checkstr(t, "the reading's own conversion", "", &im.kouho[0]);
checkstr(t, "the word it begins", "民國", &im.kouho[1]);
CT_CHECK(t, keystroke(Kdown, 0, &com));
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "a word longer than the reading", "民國", &com);
cleanup:
searchend(&f);
}
void
engine_hanja_korean_keys(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke('r', Mshift, &com));
CT_CHECK(t, keystroke('k', 0, &com));
checkstr(t, "shifted Hanja reading", "", &search.text);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke('R', 0, &com));
CT_CHECK(t, keystroke('k', 0, &com));
checkstr(t, "Caps Lock Hanja reading", "", &search.text);
CT_EQ_INT(t, 1, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
searchend(&f);
}
void
engine_hanja_backspace(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangKO);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
if(!typekeys(t, "gks", &com))
goto cleanup;
checkstr(t, "Hanja before backspace", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Hanja final backspace", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "Hanja vowel backspace", "", &search.text);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "empty Hanja search", "", &search.text);
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, search.lang);
/* Ctrl+H converts the pending syllable: it seeds the query. */
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "nothing committed before Hanja", "", &com);
checkstr(t, "pending syllable is the query", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kback, 0, &com));
checkstr(t, "seed backspaced", "", &search.text);
CT_EQ_INT(t, LangHANJA, search.lang);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangKO, im.l->lang);
/* Escape gives the seed back; keys typed compose from it. */
if(!typekeys(t, "gks", &com))
goto cleanup;
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_EQ_INT(t, 0, search.lang);
checkstr(t, "seed pending again", "", &im.pre);
checkstr(t, "nothing committed", "", &com);
CT_CHECK(t, keystroke(Kback, 0, &com));
CT_CHECK(t, keystroke('h', Mctrl, &com));
checkstr(t, "seed 하", "", &search.text);
CT_CHECK(t, keystroke('s', 0, &com));
checkstr(t, "key composes with the seed", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "seed alone comes back", "", &im.pre);
CT_CHECK(t, !keystroke(Kesc, 0, &com));
checkstr(t, "Korean Escape commits", "", &com);
CT_EQ_INT(t, 0, im.pre.n);
cleanup:
searchend(&f);
}
void
engine_hanja_input_languages(struct ct *t)
{
Searchfix f;
Str com;
hanjabegin(&f, LangEN);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "gks", &com))
goto cleanup;
checkstr(t, "English Hanja input", "gks", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "English Hanja commit", "gks", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
init();
im.l = getlang(LangJP);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "ka", &com))
goto cleanup;
checkstr(t, "Japanese Hanja input", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "Japanese Hanja commit", "", &com);
CT_EQ_INT(t, LangJP, im.l->lang);
/* The keyboard's own keys: 한자 seeds like Ctrl+H, 한/영 toggles. */
init();
im.l = getlang(LangKO);
sclear(&com);
if(typekeys(t, "gks", &com)){
CT_CHECK(t, keystroke(Khanja, 0, &com));
checkstr(t, "Hanja key seed", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Khangul, 0, &com));
checkstr(t, "Hangul key commits the query", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Khangul, 0, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_CHECK(t, keystroke(Kzenkaku, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kkana, 0, &com));
CT_EQ_INT(t, LangJPK, im.l->lang);
CT_CHECK(t, keystroke(Kkana, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke(Kzenkaku, 0, &com));
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Khenkan, 0, &com));
CT_EQ_INT(t, LangJP, im.l->lang);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, keystroke(Khenkan, 0, &com));
CT_EQ_INT(t, 0, im.sel);
CT_CHECK(t, keystroke(Kmuhenkan, 0, &com));
checkstr(t, "無変換 commits", "", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
}
}
init();
im.l = getlang(LangEN);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_CHECK(t, keystroke(L'', 0, &com));
checkstr(t, "direct Hanja input", "", &search.text);
CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "direct Hanja selection", "", &com);
CT_EQ_INT(t, LangEN, im.l->lang);
init();
im.l = getlang(LangJP);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
if(!typekeys(t, "ka", &com))
goto cleanup;
CT_CHECK(t, !keystroke('e', Mctrl|Malt, &com));
checkstr(t, "modified search boundary", "", &com);
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
sclear(&com);
CT_CHECK(t, !keystroke('h', Mctrl|Msuper, &com));
CT_EQ_INT(t, 0, search.lang);
CT_EQ_INT(t, LangJP, im.l->lang);
cleanup:
searchend(&f);
}
static u32int
stressrand(u32int *state)
{
*state = *state * 1664525U + 1013904223U;
return *state;
}
static int
stressstr(Str *s)
{
return s->n >= 0 && s->n <= Maxrunes;
}
void
engine_randomized_stress(struct ct *t)
{
static Rune text[] = {
'a', 'e', 'g', 'i', 'j', 'k', 'm', 'n', 'o', 'r', 's', 't',
'u', 'w', 'y', '1', '2', '9', '\'', ' ', L'',
};
static Rune modes[] = { 't', 'n', 'k', 's', 'v' };
Searchfix f;
Keyres res;
Caret pos;
u32int rnd, mod;
char contexts[3];
void *owner, *owners[3];
int i, j, op;
emojibegin(&f, 0);
owners[0] = &contexts[0];
owners[1] = &contexts[1];
owners[2] = &contexts[2];
rnd = 0x5eed1234U;
for(i = 0; i < 16000; i++){
rnd = stressrand(&rnd);
owner = owners[(rnd >> 8) % nelem(owners)];
op = rnd % 28;
mod = (rnd >> 16) & Mmask;
memset(&pos, 0, sizeof pos);
pos.valid = (rnd >> 25) & 1;
pos.x = (rnd >> 3) & 0x3ff;
pos.y = (rnd >> 13) & 0x3ff;
pos.h = 1 + ((rnd >> 18) & 0x1f);
if(op < 13)
res = ownerrequestat(owner, Keypress,
text[(rnd >> 5) % nelem(text)], mod, &pos);
else if(op == 13)
res = ownerrequestat(owner, Keypress, Kback, 0, &pos);
else if(op == 14)
res = ownerrequestat(owner, Keypress, Kret, 0, &pos);
else if(op == 15)
res = ownerrequestat(owner, Keypress, Kesc, 0, &pos);
else if(op == 16)
res = ownerrequestat(owner, Keypress, Kdown, 0, &pos);
else if(op == 17)
res = ownerrequestat(owner, Keypress, Kup, 0, &pos);
else if(op == 18)
res = ownerrequestat(owner, Keypress, Ktab,
(rnd & 0x1000) ? Mshift : 0, &pos);
else if(op == 19)
res = ownerrequestat(owner, Keypress,
modes[(rnd >> 12) % nelem(modes)], Mctrl, &pos);
else if(op == 20)
res = ownerrequestat(owner, Keypress, 'e', Mctrl, &pos);
else if(op == 21)
res = ownerrequest(owner, Keyreset, 0, 0);
else if(op == 22)
res = ownerrequest(owner, Keyrelease, 0, 0);
else if(op == 23)
res = ownerrequestat(owner, Keycaret, 0, 0, &pos);
else if(op == 24)
res = ownerrequest(owner, Keypress,
0, 0);
else if(op == 25)
res = ownerrequestat(owner, Keypress, 'p', Mctrl, &pos);
else if(op == 26)
res = ownerrequestat(owner, Keypress, 0xf008, 0, &pos);
else
res = ownerrequestat(owner, Keypress,
'1' + ((rnd >> 11) % 9), 0, &pos);
if(!stressstr(&res.commit) || !stressstr(&res.preedit) ||
!stressstr(&im.pre) || !stressstr(&im.raw) ||
!stressstr(&search.raw) || !stressstr(&search.text) ||
im.l == nil || im.nkouho < 0 || im.nkouho > Maxkouho ||
im.sel < -1 || im.sel >= Maxkouho ||
(im.nkouho > 0 && im.sel >= im.nkouho)){
CT_ERRORF(t, "invalid engine state after stress operation %d", i);
break;
}
for(j = 0; j < im.nkouho; j++)
if(!stressstr(&im.kouho[j])){
CT_ERRORF(t, "invalid candidate after stress operation %d", i);
i = 16000;
break;
}
if(activeowner != nil && activeowner != owners[0] &&
activeowner != owners[1] && activeowner != owners[2]){
CT_ERRORF(t, "unknown active owner after stress operation %d", i);
break;
}
}
searchend(&f);
}
void
engine_full_boundary_passthrough(struct ct *t)
{
Str com, shown;
int i;
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes;
sclear(&com);
CT_CHECK(t, !keystroke(L'', 0, &com));
CT_EQ_INT(t, Maxrunes, com.n);
for(i = 0; i < Maxrunes; i++)
CT_EQ_UINT(t, L'', com.r[i]);
CT_EQ_INT(t, 0, im.pre.n);
CT_EQ_INT(t, 0, im.raw.n);
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes-1; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes-1;
sclear(&com);
CT_CHECK(t, !keystroke(L'', 0, &com));
CT_EQ_INT(t, Maxrunes-1, com.n);
CT_EQ_INT(t, 0, im.pre.n);
/* Typing on at the limit commits the kana and keeps the syllable. */
init();
im.l = getlang(LangJP);
for(i = 0; i < Maxrunes-1; i++)
im.pre.r[i] = L'';
im.pre.n = Maxrunes-1;
im.raw = mkstr("k");
sclear(&com);
CT_CHECK(t, keystroke('a', 0, &com));
CT_EQ_INT(t, Maxrunes-1, com.n);
impre(&im, &shown);
checkstr(t, "syllable after the flush", "", &shown);
}