emoji: repair one-shot candidate selection
This commit is contained in:
1
Makefile
1
Makefile
@@ -43,5 +43,6 @@ test:
|
||||
verify-map:
|
||||
python3 map/mktelex.py | cmp - map/telex.map
|
||||
python3 -B map/mkemoji | cmp - map/emoji.dict
|
||||
python3 -B tests/mkemoji_test.py
|
||||
|
||||
.PHONY: all check test verify-map clean xim bench
|
||||
|
||||
22
README.md
22
README.md
@@ -29,9 +29,9 @@ mirrors must provide the same sibling; initialize it after cloning with
|
||||
|
||||
The suite covers the fixed-size UTF-8 string, hash table, trie fixtures,
|
||||
Japanese/Hangul/Telex state transitions, dictionary candidates, engine
|
||||
selection state, and production map loading. Tests use explicit case tables,
|
||||
boundary values, and small regressions for repaired core contracts. IPC and GUI
|
||||
protocol stacks are outside this suite and require separate integration checks.
|
||||
selection state, IPC request fields, and production map loading. Tests use
|
||||
explicit case tables, boundary values, and small regressions for repaired core
|
||||
contracts. Full IPC and GUI protocol stacks require separate integration checks.
|
||||
|
||||
## Run
|
||||
|
||||
@@ -69,7 +69,7 @@ Switch input modes with Ctrl + key:
|
||||
P Toggle preedit echo
|
||||
|
||||
Type romanized input. Select candidates with 1-9 or arrow keys.
|
||||
Tab or Enter to commit.
|
||||
Outside one-shot search, Tab or Enter commits.
|
||||
|
||||
### Emoji and symbols
|
||||
|
||||
@@ -82,13 +82,17 @@ preedit echo.
|
||||
안녕 → Ctrl+E → smile → Enter → 😀
|
||||
|
||||
There is no auto-insert. Enter commits the first candidate (or the current
|
||||
one); arrows and Tab move, Alt+1 through Alt+9 select, Esc cancels. Plain
|
||||
digits are part of the query.
|
||||
one); arrows and Tab move, and Esc cancels. Bare 1 through 9 select an existing
|
||||
numbered row; a bare digit with no such row and 0 remain query text. Digit
|
||||
aliases are kept in matching slots, so `^1`, `_2`, and `<3` produce `¹`, `₂`,
|
||||
and `♥`.
|
||||
|
||||
`map/emoji.src` is the source of truth: each UTF-8 TSV row is a result followed
|
||||
by one or more tab-separated aliases. Aliases may be multilingual. Run
|
||||
`map/mkemoji > map/emoji.dict` after editing it; `make verify-map` checks the
|
||||
generated dictionary and the Telex map.
|
||||
by one or more tab-separated aliases. Aliases may be multilingual and ASCII
|
||||
letters are case-insensitive. Results and aliases are limited to 64 Unicode
|
||||
code points; results cannot contain whitespace. Run `map/mkemoji > map/emoji.dict`
|
||||
after editing it; `make verify-map` checks the generated dictionary and the
|
||||
Telex map.
|
||||
|
||||
😀 웃음 웃다 스마일 smile grin
|
||||
|
||||
|
||||
2
ipc.c
2
ipc.c
@@ -38,7 +38,7 @@ ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod,
|
||||
unsigned int *key)
|
||||
{
|
||||
*want = req[0] != 0;
|
||||
*mod = req[1];
|
||||
*mod = req[1] & Mmask;
|
||||
*key = (unsigned int)req[2] |
|
||||
((unsigned int)req[3] << 8) |
|
||||
((unsigned int)req[4] << 16) |
|
||||
|
||||
6
ipc.h
6
ipc.h
@@ -7,6 +7,7 @@
|
||||
|
||||
/*
|
||||
* Request: [want-preedit, modifiers, key byte 0, ..., key byte 3].
|
||||
* Only Mmask modifier bits are retained.
|
||||
* Response: [eaten, commit-length-low, commit-length-high, commit...],
|
||||
* followed, when requested, by
|
||||
* [preedit-length-low, preedit-length-high, preedit...].
|
||||
@@ -22,13 +23,14 @@ enum
|
||||
Kesc = Kspec|0x1b,
|
||||
Kup = Kspec|0x52,
|
||||
Kdown = Kspec|0x54,
|
||||
Kshift = Kspec|0xe1,
|
||||
Ksuper = Kspec|0xeb,
|
||||
Kmodfirst = Kspec|0xe1,
|
||||
Kmodlast = Kspec|0xee,
|
||||
|
||||
Mshift = 1<<0,
|
||||
Mctrl = 1<<2,
|
||||
Malt = 1<<3,
|
||||
Msuper = 1<<6,
|
||||
Mmask = Mshift|Mctrl|Malt|Msuper,
|
||||
|
||||
Ipcreqsz = 6,
|
||||
Ipclensz = 2,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
: ☹ ☺
|
||||
:( ☹
|
||||
:) ☺
|
||||
< ← ≤ ≠ ♥
|
||||
< ← ≤ ♥ ≠
|
||||
<- ←
|
||||
<3 ♥
|
||||
<= ≤
|
||||
@@ -24,7 +24,7 @@
|
||||
=> ⇒
|
||||
> ≥
|
||||
>= ≥
|
||||
^ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁱ ⁿ
|
||||
^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ⁱ ⁿ
|
||||
^( ⁽
|
||||
^) ⁾
|
||||
^+ ⁺
|
||||
@@ -42,7 +42,7 @@
|
||||
^= ⁼
|
||||
^i ⁱ
|
||||
^n ⁿ
|
||||
_ ₍ ₎ ₊ ₋ ₌ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₐ ₑ ₒ ₓ
|
||||
_ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍ ₎ ₊ ₋ ₌ ₀ ₐ ₑ ₒ ₓ
|
||||
_( ₍
|
||||
_) ₎
|
||||
_+ ₊
|
||||
|
||||
@@ -8,12 +8,34 @@
|
||||
÷ ./
|
||||
☹ :(
|
||||
☺ :)
|
||||
|
||||
# Bare 1-9 choose prefix candidates; keep digit aliases in matching slots.
|
||||
← <-
|
||||
≤ <=
|
||||
♥ <3
|
||||
≠ <> !=
|
||||
≡ ==
|
||||
⇒ =>
|
||||
≥ >=
|
||||
¹ ^1
|
||||
² ^2
|
||||
³ ^3
|
||||
⁴ ^4
|
||||
⁵ ^5
|
||||
⁶ ^6
|
||||
⁷ ^7
|
||||
⁸ ^8
|
||||
⁹ ^9
|
||||
₁ _1
|
||||
₂ _2
|
||||
₃ _3
|
||||
₄ _4
|
||||
₅ _5
|
||||
₆ _6
|
||||
₇ _7
|
||||
₈ _8
|
||||
₉ _9
|
||||
|
||||
⁽ ^(
|
||||
⁾ ^)
|
||||
⁺ ^+
|
||||
@@ -27,25 +49,6 @@
|
||||
≈ ~=
|
||||
⁰ ^0
|
||||
₀ _0
|
||||
¹ ^1
|
||||
₁ _1
|
||||
² ^2
|
||||
₂ _2
|
||||
♥ <3
|
||||
³ ^3
|
||||
₃ _3
|
||||
⁴ ^4
|
||||
₄ _4
|
||||
⁵ ^5
|
||||
₅ _5
|
||||
⁶ ^6
|
||||
₆ _6
|
||||
⁷ ^7
|
||||
₇ _7
|
||||
⁸ ^8
|
||||
₈ _8
|
||||
⁹ ^9
|
||||
₉ _9
|
||||
ₐ _a
|
||||
α alpha
|
||||
β beta
|
||||
|
||||
15
map/mkemoji
15
map/mkemoji
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
|
||||
SOURCE = Path(__file__).with_name("emoji.src")
|
||||
MAXRUNES = 64
|
||||
|
||||
|
||||
def fold(s):
|
||||
@@ -14,22 +15,30 @@ def fold(s):
|
||||
return unicodedata.normalize("NFC", s)
|
||||
|
||||
|
||||
def hascontrol(s):
|
||||
return any(unicodedata.category(c) == "Cc" for c in s)
|
||||
|
||||
|
||||
def read(path):
|
||||
entries = []
|
||||
with path.open(encoding="utf-8") as src:
|
||||
for lineno, raw in enumerate(src, 1):
|
||||
line = raw.rstrip("\r\n")
|
||||
if not line.strip() or line.lstrip().startswith("#"):
|
||||
if not line.strip():
|
||||
continue
|
||||
if "\t" not in line and line.lstrip().startswith("#"):
|
||||
continue
|
||||
fields = line.split("\t")
|
||||
if len(fields) < 2:
|
||||
raise ValueError(f"{path}:{lineno}: need a result and an alias")
|
||||
result = unicodedata.normalize("NFC", fields[0])
|
||||
if not result or any(c.isspace() for c in result):
|
||||
if (not result or len(result) > MAXRUNES or hascontrol(result)
|
||||
or any(c.isspace() for c in result)):
|
||||
raise ValueError(f"{path}:{lineno}: bad result")
|
||||
for field in fields[1:]:
|
||||
alias = fold(field)
|
||||
if not alias or alias != alias.strip():
|
||||
if (not alias or len(alias) > MAXRUNES or hascontrol(alias)
|
||||
or alias != alias.strip()):
|
||||
raise ValueError(f"{path}:{lineno}: bad alias")
|
||||
entries.append((result, alias))
|
||||
return entries
|
||||
|
||||
40
strans.c
40
strans.c
@@ -53,6 +53,7 @@ setkouho(Dictres *res)
|
||||
static void
|
||||
show(void)
|
||||
{
|
||||
static Drawcmd old;
|
||||
Drawcmd dc;
|
||||
Str *pre;
|
||||
int i, first, n;
|
||||
@@ -73,7 +74,10 @@ show(void)
|
||||
if(dc.pre.n == 0 && n == 0 && !visible)
|
||||
return;
|
||||
visible = dc.pre.n != 0 || n != 0;
|
||||
chansend(drawc, &dc);
|
||||
/* imthread is the sole producer; keep only the newest pending draw. */
|
||||
while(channbrecv(drawc, &old) > 0)
|
||||
;
|
||||
channbsend(drawc, &dc);
|
||||
}
|
||||
|
||||
static void
|
||||
@@ -227,11 +231,19 @@ transmap(Im *im, Rune c)
|
||||
static Rune
|
||||
ctrlkey(Rune c)
|
||||
{
|
||||
if(c >= 'A' && c <= 'Z')
|
||||
c += 'a' - 'A';
|
||||
if(c >= 'a' && c <= 'z')
|
||||
return c - 'a' + 1;
|
||||
return c;
|
||||
}
|
||||
|
||||
static int
|
||||
ismodkey(u32int ks)
|
||||
{
|
||||
return ks >= Kmodfirst && ks <= Kmodlast;
|
||||
}
|
||||
|
||||
static void
|
||||
foldascii(Str *s)
|
||||
{
|
||||
@@ -369,8 +381,8 @@ searchkey(u32int ks, u32int mod, Str *com)
|
||||
int n, off;
|
||||
Rune c;
|
||||
|
||||
if(ks == Ksuper || ks == Kshift)
|
||||
return 1;
|
||||
if(ismodkey(ks))
|
||||
return 0;
|
||||
if(ks == Kdown || ks == Kup){
|
||||
if(im.nkouho == 0)
|
||||
return 1;
|
||||
@@ -383,7 +395,7 @@ searchkey(u32int ks, u32int mod, Str *com)
|
||||
}
|
||||
n = ks - '1';
|
||||
off = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0;
|
||||
if((mod & Malt) && n >= 0 && n < Maxdisp){
|
||||
if(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){
|
||||
pickemoji(off + n, com);
|
||||
return 1;
|
||||
}
|
||||
@@ -454,8 +466,8 @@ keystroke(u32int ks, u32int mod, Str *com)
|
||||
int n, off;
|
||||
Rune c;
|
||||
|
||||
if(ks == Ksuper || ks == Kshift)
|
||||
return 1;
|
||||
if(ismodkey(ks))
|
||||
return 0;
|
||||
if(search.on)
|
||||
return searchkey(ks, mod, com);
|
||||
if(ks == Kdown || ks == Kup){
|
||||
@@ -568,6 +580,16 @@ imhandlekey(Keyreq *kr)
|
||||
chansend(kr->reply, &res);
|
||||
}
|
||||
|
||||
static void
|
||||
dictresult(Dictres *res)
|
||||
{
|
||||
if(search.on || res->lang != im.l->lang ||
|
||||
scmp(&res->key, &im.pre) != 0)
|
||||
return;
|
||||
setkouho(res);
|
||||
show();
|
||||
}
|
||||
|
||||
void
|
||||
imthread(void*)
|
||||
{
|
||||
@@ -587,11 +609,7 @@ imthread(void*)
|
||||
imhandlekey(&kr);
|
||||
break;
|
||||
case 1:
|
||||
if(!search.on && res.lang == im.l->lang &&
|
||||
scmp(&res.key, &im.pre) == 0){
|
||||
setkouho(&res);
|
||||
show();
|
||||
}
|
||||
dictresult(&res);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ LIBS = -lthread -lbio
|
||||
|
||||
PROG = unit_test
|
||||
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \
|
||||
ko_test.c vi_test.c engine_test.c dict_test.c
|
||||
ko_test.c vi_test.c engine_test.c dict_test.c ipc_test.c
|
||||
TESTOBJ = $(TESTSRC:.c=.o)
|
||||
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c
|
||||
PARENTSRC = str.c hash.c trie.c dict.c ko.c vi.c ipc.c
|
||||
PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
|
||||
OBJS = $(TESTOBJ) $(PARENTOBJ)
|
||||
|
||||
|
||||
@@ -302,11 +302,19 @@ emojibegin(Emojifix *f, int showpre)
|
||||
f->popup = popup;
|
||||
f->visible = visible;
|
||||
draindraw(nil);
|
||||
l->dict = hmapalloc(8);
|
||||
l->dict = hmapalloc(32);
|
||||
setemoji(&l->dict, "a", "A B");
|
||||
setemoji(&l->dict, "ㅁ", "B C");
|
||||
setemoji(&l->dict, "alpha", "α");
|
||||
setemoji(&l->dict, "smil", "😀 😄");
|
||||
setemoji(&l->dict, "smile", "😀 😄");
|
||||
setemoji(&l->dict, "웃음", "😀 😄 😂");
|
||||
setemoji(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
|
||||
setemoji(&l->dict, "^0", "⁰");
|
||||
setemoji(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
|
||||
setemoji(&l->dict, "<", "← ≤ ♥ ≠");
|
||||
setemoji(&l->dict, "many",
|
||||
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
|
||||
init();
|
||||
im.l = getlang(LangKO);
|
||||
popup = showpre;
|
||||
@@ -345,8 +353,10 @@ engine_emoji_single_candidate(struct ct *t)
|
||||
Emojifix f;
|
||||
Str com;
|
||||
|
||||
emojibegin(&f, 0);
|
||||
emojibegin(&f, 1);
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('p', Mctrl, &com));
|
||||
CT_EQ_INT(t, 0, popup);
|
||||
CT_CHECK(t, keystroke('e', Mctrl, &com));
|
||||
CT_CHECK(t, search.on);
|
||||
CT_EQ_INT(t, LangKO, im.l->lang);
|
||||
@@ -363,6 +373,10 @@ engine_emoji_single_candidate(struct ct *t)
|
||||
checkstr(t, "explicit candidate commit", "α", &com);
|
||||
CT_CHECK(t, !search.on);
|
||||
CT_EQ_INT(t, LangKO, im.l->lang);
|
||||
show();
|
||||
CT_CHECK(t, draindraw(&dc) > 0);
|
||||
CT_EQ_INT(t, 0, dc.pre.n);
|
||||
CT_EQ_INT(t, 0, dc.nkouho);
|
||||
}
|
||||
emojiend(&f);
|
||||
}
|
||||
@@ -376,6 +390,18 @@ engine_emoji_queries(struct ct *t)
|
||||
|
||||
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, 3, im.nkouho);
|
||||
checkstr(t, "raw candidate", "A", &im.kouho[0]);
|
||||
checkstr(t, "deduplicated candidate", "B", &im.kouho[1]);
|
||||
checkstr(t, "local candidate", "C", &im.kouho[2]);
|
||||
CT_CHECK(t, draindraw(&dc) > 0);
|
||||
checkstr(t, "prefix popup query", "a", &dc.pre);
|
||||
CT_EQ_INT(t, 3, 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);
|
||||
@@ -384,11 +410,19 @@ engine_emoji_queries(struct ct *t)
|
||||
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', Malt, &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)){
|
||||
@@ -405,11 +439,187 @@ engine_emoji_queries(struct ct *t)
|
||||
emojiend(&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', "♥" },
|
||||
};
|
||||
Emojifix 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.on);
|
||||
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);
|
||||
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.on);
|
||||
CT_EQ_INT(t, 0, im.nkouho);
|
||||
CT_CHECK(t, keystroke(Kret, 0, &com));
|
||||
checkstr(t, "missing number text", "smile9", &com);
|
||||
}
|
||||
emojiend(&f);
|
||||
}
|
||||
|
||||
void
|
||||
engine_emoji_navigation(struct ct *t)
|
||||
{
|
||||
Drawcmd dc = {0};
|
||||
Emojifix f;
|
||||
Str com;
|
||||
int i;
|
||||
|
||||
emojibegin(&f, 0);
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('e', Mctrl, &com));
|
||||
if(typekeys(t, "many", &com)){
|
||||
CT_EQ_INT(t, 12, im.nkouho);
|
||||
for(i = 0; i < Maxdisp+1; i++)
|
||||
CT_CHECK(t, keystroke(Kdown, 0, &com));
|
||||
CT_EQ_INT(t, Maxdisp, im.sel);
|
||||
CT_CHECK(t, draindraw(&dc) > 0);
|
||||
checkstr(t, "scrolled first row", "c2", &dc.kouho[0]);
|
||||
checkstr(t, "scrolled ninth row", "c10", &dc.kouho[8]);
|
||||
CT_EQ_INT(t, 8, dc.sel);
|
||||
CT_CHECK(t, keystroke('1', 0, &com));
|
||||
checkstr(t, "scrolled row one", "c2", &com);
|
||||
}
|
||||
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('e', Mctrl, &com));
|
||||
if(typekeys(t, "many", &com)){
|
||||
for(i = 0; i < 12; i++)
|
||||
CT_CHECK(t, keystroke(Kdown, 0, &com));
|
||||
CT_EQ_INT(t, 11, im.sel);
|
||||
CT_CHECK(t, keystroke('9', 0, &com));
|
||||
checkstr(t, "scrolled row nine", "c12", &com);
|
||||
}
|
||||
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('e', Mctrl, &com));
|
||||
if(typekeys(t, "smile", &com)){
|
||||
CT_CHECK(t, keystroke(Ktab, 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(Kup, 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, "navigated candidate", "😄", &com);
|
||||
}
|
||||
emojiend(&f);
|
||||
}
|
||||
|
||||
void
|
||||
engine_emoji_preedit_languages(struct ct *t)
|
||||
{
|
||||
Emojifix f;
|
||||
Str com, shown;
|
||||
int p;
|
||||
|
||||
emojibegin(&f, 0);
|
||||
init();
|
||||
im.l = getlang(LangJP);
|
||||
sclear(&com);
|
||||
if(typekeys(t, "ka", &com)){
|
||||
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
|
||||
checkstr(t, "modifier preserves Japanese", "ka", &im.pre);
|
||||
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
|
||||
checkstr(t, "committed Japanese preedit", "か", &com);
|
||||
CT_CHECK(t, search.on);
|
||||
CT_EQ_INT(t, LangJP, im.l->lang);
|
||||
CT_CHECK(t, keystroke('a', 0, &com));
|
||||
p = popup;
|
||||
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
|
||||
CT_CHECK(t, search.on);
|
||||
checkstr(t, "modifier preserves search", "a", &search.text);
|
||||
CT_CHECK(t, keystroke('P', Mctrl|Mshift, &com));
|
||||
CT_EQ_INT(t, !p, popup);
|
||||
CT_CHECK(t, search.on);
|
||||
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
|
||||
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
|
||||
CT_CHECK(t, !search.on);
|
||||
sclear(&com);
|
||||
if(typekeys(t, "na", &com)){
|
||||
shown = shownpre(&im);
|
||||
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 = &testvi;
|
||||
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.on);
|
||||
CT_EQ_PTR(t, &testvi, im.l);
|
||||
sclear(&com);
|
||||
if(typekeys(t, "as", &com)){
|
||||
shown = shownpre(&im);
|
||||
checkstr(t, "Telex resumed", "á", &shown);
|
||||
checkstr(t, "Telex raw resumed", "as", &im.raw);
|
||||
}
|
||||
}
|
||||
emojiend(&f);
|
||||
}
|
||||
|
||||
void
|
||||
engine_emoji_start_and_unknown(struct ct *t)
|
||||
{
|
||||
Emojifix f;
|
||||
Str com;
|
||||
char want[Maxrunes+1];
|
||||
int i;
|
||||
|
||||
emojibegin(&f, 0);
|
||||
sclear(&com);
|
||||
@@ -427,5 +637,67 @@ engine_emoji_start_and_unknown(struct ct *t)
|
||||
CT_EQ_INT(t, LangKO, im.l->lang);
|
||||
}
|
||||
}
|
||||
|
||||
sclear(&com);
|
||||
CT_CHECK(t, keystroke('e', Mctrl, &com));
|
||||
if(typekeys(t, "xyz", &com)){
|
||||
CT_CHECK(t, !keystroke(Kspec|0x51, 0, &com));
|
||||
checkstr(t, "special commits shown query", "xyz", &com);
|
||||
CT_CHECK(t, !search.on);
|
||||
CT_EQ_INT(t, LangKO, im.l->lang);
|
||||
}
|
||||
|
||||
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.on);
|
||||
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.on);
|
||||
emojiend(&f);
|
||||
}
|
||||
|
||||
void
|
||||
engine_emoji_dictionary_identity(struct ct *t)
|
||||
{
|
||||
Dictres res;
|
||||
Emojifix f;
|
||||
|
||||
emojibegin(&f, 0);
|
||||
im.pre = mkstr("same");
|
||||
im.kouho[0] = mkstr("old");
|
||||
im.nkouho = 1;
|
||||
im.sel = 0;
|
||||
memset(&res, 0, sizeof res);
|
||||
res.key = im.pre;
|
||||
res.kouho[0] = mkstr("wrong");
|
||||
res.nkouho = 1;
|
||||
res.lang = LangJP;
|
||||
dictresult(&res);
|
||||
CT_EQ_INT(t, 1, im.nkouho);
|
||||
checkstr(t, "wrong language ignored", "old", &im.kouho[0]);
|
||||
|
||||
res.lang = LangKO;
|
||||
res.key = mkstr("other");
|
||||
dictresult(&res);
|
||||
CT_EQ_INT(t, 1, im.nkouho);
|
||||
checkstr(t, "wrong preedit ignored", "old", &im.kouho[0]);
|
||||
|
||||
res.key = im.pre;
|
||||
search.on = 1;
|
||||
dictresult(&res);
|
||||
CT_EQ_INT(t, 1, im.nkouho);
|
||||
checkstr(t, "search response ignored", "old", &im.kouho[0]);
|
||||
|
||||
search.on = 0;
|
||||
res.kouho[0] = mkstr("right");
|
||||
dictresult(&res);
|
||||
CT_EQ_INT(t, 1, im.nkouho);
|
||||
CT_EQ_INT(t, -1, im.sel);
|
||||
checkstr(t, "matching response accepted", "right", &im.kouho[0]);
|
||||
emojiend(&f);
|
||||
}
|
||||
|
||||
15
tests/ipc_test.c
Normal file
15
tests/ipc_test.c
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "test.h"
|
||||
|
||||
void
|
||||
ipc_masks_modifiers(struct ct *t)
|
||||
{
|
||||
uchar buf[Ipcreqsz];
|
||||
u32int key, mod;
|
||||
int want;
|
||||
|
||||
ipcpackreq(buf, 1, Mctrl|Malt|(1<<1)|(1<<4)|(1<<7), 0x1f642);
|
||||
ipcunpackreq(buf, &want, &mod, &key);
|
||||
CT_EQ_INT(t, 1, want);
|
||||
CT_EQ_UINT(t, Mctrl|Malt, mod);
|
||||
CT_EQ_UINT(t, 0x1f642, key);
|
||||
}
|
||||
85
tests/mkemoji_test.py
Normal file
85
tests/mkemoji_test.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MKEMOJI = ROOT / "map" / "mkemoji"
|
||||
|
||||
|
||||
def generate(source=None):
|
||||
args = [sys.executable, "-B", str(MKEMOJI)]
|
||||
if source is not None:
|
||||
args.append(str(source))
|
||||
return subprocess.run(args, capture_output=True, text=True, check=False)
|
||||
|
||||
|
||||
def table(output):
|
||||
return dict(line.split("\t", 1) for line in output.splitlines())
|
||||
|
||||
|
||||
class MkemojiTest(unittest.TestCase):
|
||||
def source(self, text):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
path = Path(tmp.name) / "emoji.src"
|
||||
path.write_text(text, encoding="utf-8")
|
||||
self.addCleanup(tmp.cleanup)
|
||||
return path
|
||||
|
||||
def test_production_digit_slots(self):
|
||||
result = generate()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
data = table(result.stdout)
|
||||
self.assertEqual(data["^"].split()[:9], list("¹²³⁴⁵⁶⁷⁸⁹"))
|
||||
self.assertEqual(data["_"].split()[:9], list("₁₂₃₄₅₆₇₈₉"))
|
||||
self.assertEqual(data["<"].split()[2], "♥")
|
||||
self.assertEqual(data["^1"], "¹")
|
||||
self.assertEqual(data["_2"], "₂")
|
||||
self.assertEqual(data["<3"], "♥")
|
||||
|
||||
def test_fold_normalize_and_exact_first(self):
|
||||
source = self.source(
|
||||
"β\tALPHABET\n"
|
||||
"α\talpha\n"
|
||||
"e\u0301\tE\u0301\n"
|
||||
"#\thash\n"
|
||||
)
|
||||
first = generate(source)
|
||||
second = generate(source)
|
||||
self.assertEqual(first.returncode, 0, first.stderr)
|
||||
self.assertEqual(first.stdout, second.stdout)
|
||||
data = table(first.stdout)
|
||||
self.assertEqual(data["alpha"].split(), ["α", "β"])
|
||||
self.assertEqual(data["al"].split(), ["β", "α"])
|
||||
self.assertEqual(data["é"], "é")
|
||||
self.assertEqual(data["hash"], "#")
|
||||
|
||||
def test_rejects_malformed_and_oversized_rows(self):
|
||||
bad = [
|
||||
"x\n",
|
||||
"bad result\ta\n",
|
||||
"x\t\n",
|
||||
"x\t a\n",
|
||||
"\0\ta\n",
|
||||
"x\ta\0b\n",
|
||||
"x" * 65 + "\ta\n",
|
||||
"x\t" + "a" * 65 + "\n",
|
||||
]
|
||||
for text in bad:
|
||||
with self.subTest(text=repr(text)):
|
||||
result = generate(self.source(text))
|
||||
self.assertEqual(result.returncode, 1)
|
||||
|
||||
def test_accepts_runtime_boundary(self):
|
||||
source = self.source("x" * 64 + "\t" + "a" * 64 + "\n")
|
||||
result = generate(source)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("a" * 64 + "\t" + "x" * 64, result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -35,8 +35,13 @@ void engine_language_switch_state(struct ct*);
|
||||
void engine_telex_history_bound(struct ct*);
|
||||
void engine_emoji_single_candidate(struct ct*);
|
||||
void engine_emoji_queries(struct ct*);
|
||||
void engine_emoji_digit_aliases(struct ct*);
|
||||
void engine_emoji_navigation(struct ct*);
|
||||
void engine_emoji_preedit_languages(struct ct*);
|
||||
void engine_emoji_start_and_unknown(struct ct*);
|
||||
void engine_emoji_dictionary_identity(struct ct*);
|
||||
void dictionary_candidates(struct ct*);
|
||||
void dictionary_misses_clear_result(struct ct*);
|
||||
void ipc_masks_modifiers(struct ct*);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -82,9 +82,14 @@ static const struct ct_test tests[] = {
|
||||
{ "engine/telex-history-bound", engine_telex_history_bound },
|
||||
{ "engine/emoji-single-candidate", engine_emoji_single_candidate },
|
||||
{ "engine/emoji-queries", engine_emoji_queries },
|
||||
{ "engine/emoji-digit-aliases", engine_emoji_digit_aliases },
|
||||
{ "engine/emoji-navigation", engine_emoji_navigation },
|
||||
{ "engine/emoji-preedit-languages", engine_emoji_preedit_languages },
|
||||
{ "engine/emoji-start-and-unknown", engine_emoji_start_and_unknown },
|
||||
{ "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity },
|
||||
{ "dict/candidates", dictionary_candidates },
|
||||
{ "dict/misses-clear-result", dictionary_misses_clear_result },
|
||||
{ "ipc/masks-modifiers", ipc_masks_modifiers },
|
||||
};
|
||||
|
||||
void
|
||||
@@ -92,7 +97,7 @@ threadmain(int argc, char **argv)
|
||||
{
|
||||
int i, status;
|
||||
|
||||
drawc = chancreate(sizeof(Drawcmd), 1024);
|
||||
drawc = chancreate(sizeof(Drawcmd), 4);
|
||||
keyc = chancreate(sizeof(Keyreq), 0);
|
||||
dictreqc = chancreate(sizeof(Dictreq), 64);
|
||||
dictresc = chancreate(sizeof(Dictres), 0);
|
||||
|
||||
Reference in New Issue
Block a user