emoji: repair one-shot candidate selection

This commit is contained in:
2026-08-11 23:30:41 +09:00
parent 0fff17c8b8
commit 2ae87a3926
14 changed files with 473 additions and 54 deletions

View File

@@ -43,5 +43,6 @@ test:
verify-map: verify-map:
python3 map/mktelex.py | cmp - map/telex.map python3 map/mktelex.py | cmp - map/telex.map
python3 -B map/mkemoji | cmp - map/emoji.dict python3 -B map/mkemoji | cmp - map/emoji.dict
python3 -B tests/mkemoji_test.py
.PHONY: all check test verify-map clean xim bench .PHONY: all check test verify-map clean xim bench

View File

@@ -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, The suite covers the fixed-size UTF-8 string, hash table, trie fixtures,
Japanese/Hangul/Telex state transitions, dictionary candidates, engine Japanese/Hangul/Telex state transitions, dictionary candidates, engine
selection state, and production map loading. Tests use explicit case tables, selection state, IPC request fields, and production map loading. Tests use
boundary values, and small regressions for repaired core contracts. IPC and GUI explicit case tables, boundary values, and small regressions for repaired core
protocol stacks are outside this suite and require separate integration checks. contracts. Full IPC and GUI protocol stacks require separate integration checks.
## Run ## Run
@@ -69,7 +69,7 @@ Switch input modes with Ctrl + key:
P Toggle preedit echo P Toggle preedit echo
Type romanized input. Select candidates with 1-9 or arrow keys. 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 ### Emoji and symbols
@@ -82,13 +82,17 @@ preedit echo.
안녕 → Ctrl+E → smile → Enter → 😀 안녕 → Ctrl+E → smile → Enter → 😀
There is no auto-insert. Enter commits the first candidate (or the current 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 one); arrows and Tab move, and Esc cancels. Bare 1 through 9 select an existing
digits are part of the query. 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 `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 by one or more tab-separated aliases. Aliases may be multilingual and ASCII
`map/mkemoji > map/emoji.dict` after editing it; `make verify-map` checks the letters are case-insensitive. Results and aliases are limited to 64 Unicode
generated dictionary and the Telex map. 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 😀 웃음 웃다 스마일 smile grin

2
ipc.c
View File

@@ -38,7 +38,7 @@ ipcunpackreq(const unsigned char req[Ipcreqsz], int *want, unsigned int *mod,
unsigned int *key) unsigned int *key)
{ {
*want = req[0] != 0; *want = req[0] != 0;
*mod = req[1]; *mod = req[1] & Mmask;
*key = (unsigned int)req[2] | *key = (unsigned int)req[2] |
((unsigned int)req[3] << 8) | ((unsigned int)req[3] << 8) |
((unsigned int)req[4] << 16) | ((unsigned int)req[4] << 16) |

6
ipc.h
View File

@@ -7,6 +7,7 @@
/* /*
* Request: [want-preedit, modifiers, key byte 0, ..., key byte 3]. * 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...], * Response: [eaten, commit-length-low, commit-length-high, commit...],
* followed, when requested, by * followed, when requested, by
* [preedit-length-low, preedit-length-high, preedit...]. * [preedit-length-low, preedit-length-high, preedit...].
@@ -22,13 +23,14 @@ enum
Kesc = Kspec|0x1b, Kesc = Kspec|0x1b,
Kup = Kspec|0x52, Kup = Kspec|0x52,
Kdown = Kspec|0x54, Kdown = Kspec|0x54,
Kshift = Kspec|0xe1, Kmodfirst = Kspec|0xe1,
Ksuper = Kspec|0xeb, Kmodlast = Kspec|0xee,
Mshift = 1<<0, Mshift = 1<<0,
Mctrl = 1<<2, Mctrl = 1<<2,
Malt = 1<<3, Malt = 1<<3,
Msuper = 1<<6, Msuper = 1<<6,
Mmask = Mshift|Mctrl|Malt|Msuper,
Ipcreqsz = 6, Ipcreqsz = 6,
Ipclensz = 2, Ipclensz = 2,

View File

@@ -14,7 +14,7 @@
: ☹ ☺ : ☹ ☺
:( ☹ :( ☹
:) ☺ :) ☺
< ← ≤ < ← ≤ ♥
<- ← <- ←
<3 ♥ <3 ♥
<= ≤ <= ≤
@@ -24,7 +24,7 @@
=> ⇒ => ⇒
> ≥ > ≥
>= ≥ >= ≥
^ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁱ ⁿ ^ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽ ⁾ ⁺ ⁻ ⁼ ⁰ ⁱ ⁿ
^( ⁽ ^( ⁽
^) ⁾ ^) ⁾
^+ ⁺ ^+ ⁺
@@ -42,7 +42,7 @@
^= ⁼ ^= ⁼
^i ⁱ ^i ⁱ
^n ⁿ ^n ⁿ
_ ₍ ₎ ₊ ₋ ₌ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₐ ₑ ₒ ₓ _ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍ ₎ ₊ ₋ ₌ ₀ ₐ ₑ ₒ ₓ
_( ₍ _( ₍
_) ₎ _) ₎
_+ ₊ _+ ₊

View File

@@ -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
₀ _0 ₀ _0
¹ ^1
₁ _1
² ^2
₂ _2
♥ <3
³ ^3
₃ _3
⁴ ^4
₄ _4
⁵ ^5
₅ _5
⁶ ^6
₆ _6
⁷ ^7
₇ _7
⁸ ^8
₈ _8
⁹ ^9
₉ _9
ₐ _a ₐ _a
α alpha α alpha
β beta β beta

View File

@@ -7,6 +7,7 @@ from pathlib import Path
SOURCE = Path(__file__).with_name("emoji.src") SOURCE = Path(__file__).with_name("emoji.src")
MAXRUNES = 64
def fold(s): def fold(s):
@@ -14,22 +15,30 @@ def fold(s):
return unicodedata.normalize("NFC", s) return unicodedata.normalize("NFC", s)
def hascontrol(s):
return any(unicodedata.category(c) == "Cc" for c in s)
def read(path): def read(path):
entries = [] entries = []
with path.open(encoding="utf-8") as src: with path.open(encoding="utf-8") as src:
for lineno, raw in enumerate(src, 1): for lineno, raw in enumerate(src, 1):
line = raw.rstrip("\r\n") 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 continue
fields = line.split("\t") fields = line.split("\t")
if len(fields) < 2: if len(fields) < 2:
raise ValueError(f"{path}:{lineno}: need a result and an alias") raise ValueError(f"{path}:{lineno}: need a result and an alias")
result = unicodedata.normalize("NFC", fields[0]) 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") raise ValueError(f"{path}:{lineno}: bad result")
for field in fields[1:]: for field in fields[1:]:
alias = fold(field) 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") raise ValueError(f"{path}:{lineno}: bad alias")
entries.append((result, alias)) entries.append((result, alias))
return entries return entries

View File

@@ -53,6 +53,7 @@ setkouho(Dictres *res)
static void static void
show(void) show(void)
{ {
static Drawcmd old;
Drawcmd dc; Drawcmd dc;
Str *pre; Str *pre;
int i, first, n; int i, first, n;
@@ -73,7 +74,10 @@ show(void)
if(dc.pre.n == 0 && n == 0 && !visible) if(dc.pre.n == 0 && n == 0 && !visible)
return; return;
visible = dc.pre.n != 0 || n != 0; 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 static void
@@ -227,11 +231,19 @@ transmap(Im *im, Rune c)
static Rune static Rune
ctrlkey(Rune c) ctrlkey(Rune c)
{ {
if(c >= 'A' && c <= 'Z')
c += 'a' - 'A';
if(c >= 'a' && c <= 'z') if(c >= 'a' && c <= 'z')
return c - 'a' + 1; return c - 'a' + 1;
return c; return c;
} }
static int
ismodkey(u32int ks)
{
return ks >= Kmodfirst && ks <= Kmodlast;
}
static void static void
foldascii(Str *s) foldascii(Str *s)
{ {
@@ -369,8 +381,8 @@ searchkey(u32int ks, u32int mod, Str *com)
int n, off; int n, off;
Rune c; Rune c;
if(ks == Ksuper || ks == Kshift) if(ismodkey(ks))
return 1; return 0;
if(ks == Kdown || ks == Kup){ if(ks == Kdown || ks == Kup){
if(im.nkouho == 0) if(im.nkouho == 0)
return 1; return 1;
@@ -383,7 +395,7 @@ searchkey(u32int ks, u32int mod, Str *com)
} }
n = ks - '1'; n = ks - '1';
off = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0; 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); pickemoji(off + n, com);
return 1; return 1;
} }
@@ -454,8 +466,8 @@ keystroke(u32int ks, u32int mod, Str *com)
int n, off; int n, off;
Rune c; Rune c;
if(ks == Ksuper || ks == Kshift) if(ismodkey(ks))
return 1; return 0;
if(search.on) if(search.on)
return searchkey(ks, mod, com); return searchkey(ks, mod, com);
if(ks == Kdown || ks == Kup){ if(ks == Kdown || ks == Kup){
@@ -568,6 +580,16 @@ imhandlekey(Keyreq *kr)
chansend(kr->reply, &res); 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 void
imthread(void*) imthread(void*)
{ {
@@ -587,11 +609,7 @@ imthread(void*)
imhandlekey(&kr); imhandlekey(&kr);
break; break;
case 1: case 1:
if(!search.on && res.lang == im.l->lang && dictresult(&res);
scmp(&res.key, &im.pre) == 0){
setkouho(&res);
show();
}
break; break;
} }
} }

View File

@@ -5,9 +5,9 @@ LIBS = -lthread -lbio
PROG = unit_test PROG = unit_test
TESTSRC = unit_test.c test_util.c str_test.c hash_test.c trie_test.c \ 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) 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) PARENTOBJ = $(PARENTSRC:%.c=unit_%.o)
OBJS = $(TESTOBJ) $(PARENTOBJ) OBJS = $(TESTOBJ) $(PARENTOBJ)

View File

@@ -302,11 +302,19 @@ emojibegin(Emojifix *f, int showpre)
f->popup = popup; f->popup = popup;
f->visible = visible; f->visible = visible;
draindraw(nil); 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, "alpha", "α");
setemoji(&l->dict, "smil", "😀 😄"); setemoji(&l->dict, "smil", "😀 😄");
setemoji(&l->dict, "smile", "😀 😄"); setemoji(&l->dict, "smile", "😀 😄");
setemoji(&l->dict, "웃음", "😀 😄 😂"); 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(); init();
im.l = getlang(LangKO); im.l = getlang(LangKO);
popup = showpre; popup = showpre;
@@ -345,8 +353,10 @@ engine_emoji_single_candidate(struct ct *t)
Emojifix f; Emojifix f;
Str com; Str com;
emojibegin(&f, 0); emojibegin(&f, 1);
sclear(&com); 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, keystroke('e', Mctrl, &com));
CT_CHECK(t, search.on); CT_CHECK(t, search.on);
CT_EQ_INT(t, LangKO, im.l->lang); 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); checkstr(t, "explicit candidate commit", "α", &com);
CT_CHECK(t, !search.on); CT_CHECK(t, !search.on);
CT_EQ_INT(t, LangKO, im.l->lang); 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); emojiend(&f);
} }
@@ -376,6 +390,18 @@ engine_emoji_queries(struct ct *t)
emojibegin(&f, 1); emojibegin(&f, 1);
sclear(&com); 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)); CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "smile", &com)){ if(typekeys(t, "smile", &com)){
checkstr(t, "English alias", "smile", &search.text); checkstr(t, "English alias", "smile", &search.text);
@@ -384,11 +410,19 @@ engine_emoji_queries(struct ct *t)
checkstr(t, "backspaced query", "smil", &search.text); checkstr(t, "backspaced query", "smil", &search.text);
CT_EQ_INT(t, 2, im.nkouho); CT_EQ_INT(t, 2, im.nkouho);
CT_CHECK(t, keystroke('e', 0, &com)); 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); checkstr(t, "selected English alias", "😄", &com);
CT_EQ_INT(t, LangKO, im.l->lang); 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); sclear(&com);
CT_CHECK(t, keystroke('e', Mctrl, &com)); CT_CHECK(t, keystroke('e', Mctrl, &com));
if(typekeys(t, "dntdma", &com)){ if(typekeys(t, "dntdma", &com)){
@@ -405,11 +439,187 @@ engine_emoji_queries(struct ct *t)
emojiend(&f); 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 void
engine_emoji_start_and_unknown(struct ct *t) engine_emoji_start_and_unknown(struct ct *t)
{ {
Emojifix f; Emojifix f;
Str com; Str com;
char want[Maxrunes+1];
int i;
emojibegin(&f, 0); emojibegin(&f, 0);
sclear(&com); sclear(&com);
@@ -427,5 +637,67 @@ engine_emoji_start_and_unknown(struct ct *t)
CT_EQ_INT(t, LangKO, im.l->lang); 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); emojiend(&f);
} }

15
tests/ipc_test.c Normal file
View 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
View 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()

View File

@@ -35,8 +35,13 @@ void engine_language_switch_state(struct ct*);
void engine_telex_history_bound(struct ct*); void engine_telex_history_bound(struct ct*);
void engine_emoji_single_candidate(struct ct*); void engine_emoji_single_candidate(struct ct*);
void engine_emoji_queries(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_start_and_unknown(struct ct*);
void engine_emoji_dictionary_identity(struct ct*);
void dictionary_candidates(struct ct*); void dictionary_candidates(struct ct*);
void dictionary_misses_clear_result(struct ct*); void dictionary_misses_clear_result(struct ct*);
void ipc_masks_modifiers(struct ct*);
#endif #endif

View File

@@ -82,9 +82,14 @@ static const struct ct_test tests[] = {
{ "engine/telex-history-bound", engine_telex_history_bound }, { "engine/telex-history-bound", engine_telex_history_bound },
{ "engine/emoji-single-candidate", engine_emoji_single_candidate }, { "engine/emoji-single-candidate", engine_emoji_single_candidate },
{ "engine/emoji-queries", engine_emoji_queries }, { "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-start-and-unknown", engine_emoji_start_and_unknown },
{ "engine/emoji-dictionary-identity", engine_emoji_dictionary_identity },
{ "dict/candidates", dictionary_candidates }, { "dict/candidates", dictionary_candidates },
{ "dict/misses-clear-result", dictionary_misses_clear_result }, { "dict/misses-clear-result", dictionary_misses_clear_result },
{ "ipc/masks-modifiers", ipc_masks_modifiers },
}; };
void void
@@ -92,7 +97,7 @@ threadmain(int argc, char **argv)
{ {
int i, status; int i, status;
drawc = chancreate(sizeof(Drawcmd), 1024); drawc = chancreate(sizeof(Drawcmd), 4);
keyc = chancreate(sizeof(Keyreq), 0); keyc = chancreate(sizeof(Keyreq), 0);
dictreqc = chancreate(sizeof(Dictreq), 64); dictreqc = chancreate(sizeof(Dictreq), 64);
dictresc = chancreate(sizeof(Dictres), 0); dictresc = chancreate(sizeof(Dictres), 0);