The preedit sent back to the owner was always impre(), the reading, so かく stayed underlined in the application while the popup showed 書く and Enter inserted 書く. Nothing was hidden -- the popup had it -- but the text did not stand where it was going to land, and mozc, fcitx5 and kime all put the candidate inline. Only the reply changes. impre() still answers the reading everywhere it is the reading that is wanted: commitim, the okurigana mark, the seed Ctrl+H takes, and snapshot, so a popup that draws the preedit itself keeps the reading above the list where it says something the highlighted row does not. Consulting im.sel here is safe where consulting it inside impre() would not be: transstr memsets a local Im, whose sel is 0 and not -1, and this reply only ever reads the engine's own. Nothing in the suite pinned the old answer, so the test is new. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2440 lines
64 KiB
C
2440 lines
64 KiB
C
#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;
|
||
|
||
takeback = 0;
|
||
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, char *before)
|
||
{
|
||
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;
|
||
if(before != nil)
|
||
req.surround = mkstr(before);
|
||
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, nil);
|
||
}
|
||
|
||
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, 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', "ㅏ", "ㅏㅏㅏ" },
|
||
};
|
||
Drawcmd dc;
|
||
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);
|
||
|
||
/* A chosen candidate is what the client shows and what Enter
|
||
* commits; the popup keeps the reading above the list. */
|
||
init();
|
||
im.l = getlang(LangJP);
|
||
draindraw(nil);
|
||
ownerrequestcap(&owner, 1, Keypress, 'k', 0);
|
||
res = ownerrequestcap(&owner, 1, Keypress, 'a', 0);
|
||
checkstr(t, "the reading before a candidate", "か", &res.preedit);
|
||
im.kouho[0] = mkstr("蚊");
|
||
im.nkouho = 1;
|
||
res = ownerrequestcap(&owner, 1, Keypress, Kdown, 0);
|
||
checkstr(t, "the candidate the client shows", "蚊", &res.preedit);
|
||
clientpre = 0;
|
||
redraw();
|
||
if(CT_CHECK(t, draindraw(&dc) > 0))
|
||
checkstr(t, "the popup keeps the reading", "か", &dc.pre);
|
||
res = ownerrequestcap(&owner, 1, Keypress, Kret, 0);
|
||
checkstr(t, "Enter commits what was shown", "蚊", &res.commit);
|
||
CT_EQ_INT(t, 0, res.preedit.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]);
|
||
}
|
||
/* Backspace edits the reading, so the split it marked is gone. */
|
||
CT_CHECK(t, keystroke(Kback, 0, &com));
|
||
CT_CHECK(t, keystroke('k', 0, &com));
|
||
CT_CHECK(t, keystroke('u', 0, &com));
|
||
if(CT_EQ_INT(t, 3, im.nkouho))
|
||
checkstr(t, "the mark did not outlive its kana", "確",
|
||
&im.kouho[0]);
|
||
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);
|
||
|
||
/* Nothing to convert to: the key commits and goes on to the client. */
|
||
init();
|
||
im.l = getlang(LangJPK);
|
||
sclear(&com);
|
||
if(typekeys(t, "kaku", &com)){
|
||
CT_CHECK(t, !keystroke(' ', 0, &com));
|
||
checkstr(t, "Space commits the Katakana", "カク", &com);
|
||
CT_EQ_INT(t, 0, im.nkouho);
|
||
}
|
||
}
|
||
|
||
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 word is committed one syllable at a time, so Ctrl+H reaches back
|
||
* into the text the client already has, and a pick takes it back.
|
||
*/
|
||
void
|
||
engine_hanja_reaches_back(struct ct *t)
|
||
{
|
||
Searchfix f;
|
||
Keyres res;
|
||
Str com;
|
||
char owner;
|
||
|
||
hanjabegin(&f, LangKO);
|
||
surround = mkstr("민");
|
||
im.pre = mkstr("국");
|
||
sclear(&com);
|
||
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
|
||
goto cleanup;
|
||
checkstr(t, "the client's syllable joined the reading", "민국",
|
||
&search.text);
|
||
checkstr(t, "what the client already has", "민", &search.back);
|
||
CT_EQ_INT(t, 1, im.nkouho);
|
||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||
checkstr(t, "the word converted", "民國", &com);
|
||
CT_EQ_INT(t, 1, takeback);
|
||
|
||
/* Escape gives back what was pending, and nothing of the client's. */
|
||
surround = mkstr("민");
|
||
im.pre = mkstr("국");
|
||
sclear(&com);
|
||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||
checkstr(t, "only the pending syllable comes back", "국", &im.pre);
|
||
CT_EQ_INT(t, 0, takeback);
|
||
|
||
/* A reading nothing is keyed by stops the reach where it stands. */
|
||
surround = mkstr("가");
|
||
im.pre = mkstr("국");
|
||
sclear(&com);
|
||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||
checkstr(t, "no reach without a reading", "국", &search.text);
|
||
CT_EQ_INT(t, 0, search.back.n);
|
||
CT_CHECK(t, keystroke(Kesc, 0, &com));
|
||
|
||
/* A query without candidates types only what the client lacks. */
|
||
surround = mkstr("민");
|
||
im.pre = mkstr("국");
|
||
sclear(&com);
|
||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||
if(typekeys(t, "r", &com)){
|
||
CT_EQ_INT(t, 0, im.nkouho);
|
||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||
checkstr(t, "the client's syllable is not typed again", "국ㄱ",
|
||
&com);
|
||
CT_EQ_INT(t, 0, takeback);
|
||
}
|
||
|
||
/* Backspace gives the reach back before it eats what was typed. */
|
||
surround = mkstr("민");
|
||
im.pre = mkstr("국");
|
||
sclear(&com);
|
||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||
checkstr(t, "the reach joined the reading", "민국", &search.text);
|
||
CT_CHECK(t, keystroke(Kback, 0, &com));
|
||
checkstr(t, "the client's syllable goes back first", "국",
|
||
&search.text);
|
||
CT_EQ_INT(t, 0, search.back.n);
|
||
sclear(&com);
|
||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||
checkstr(t, "only what was typed is committed", "국", &com);
|
||
CT_EQ_INT(t, 0, takeback);
|
||
|
||
/* Nothing pending is no reading, and reaches into nothing. */
|
||
surround = mkstr("민");
|
||
sclear(&im.pre);
|
||
sclear(&com);
|
||
CT_CHECK(t, keystroke('h', Mctrl, &com));
|
||
CT_EQ_INT(t, 0, search.back.n);
|
||
CT_EQ_INT(t, 0, im.nkouho);
|
||
if(typekeys(t, "als", &com)){
|
||
checkstr(t, "the reading typed is the whole query", "민",
|
||
&search.text);
|
||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||
checkstr(t, "the client's syllable is not converted", "民",
|
||
&com);
|
||
CT_EQ_INT(t, 0, takeback);
|
||
}
|
||
|
||
/* The frontend's own request carries the text and is told to take it. */
|
||
init();
|
||
im.l = getlang(LangKO);
|
||
ownerrequestatcap(&owner, 1, Keypress, 'r', 0, nil, "민");
|
||
ownerrequestatcap(&owner, 1, Keypress, 'n', 0, nil, "민");
|
||
res = ownerrequestatcap(&owner, 1, Keypress, 'r', 0, nil, "민");
|
||
checkstr(t, "the syllable the request composed", "국", &res.preedit);
|
||
CT_EQ_INT(t, 0, res.del);
|
||
ownerrequestatcap(&owner, 1, Keypress, 'h', Mctrl, nil, "민");
|
||
checkstr(t, "the request's own text joined the reading", "민국",
|
||
&search.text);
|
||
res = ownerrequestatcap(&owner, 1, Keypress, Kret, 0, nil, "민");
|
||
checkstr(t, "the word the request converted", "民國", &res.commit);
|
||
CT_EQ_INT(t, 1, res.del);
|
||
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);
|
||
/* Two readings under one prefix may share a word; it shows once. */
|
||
setdict(f.dictlang->dict, "민민", "民");
|
||
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);
|
||
}
|