hanja: add one-shot Korean search

This commit is contained in:
2026-08-12 19:19:26 +09:00
parent e69883d0ad
commit 0216cc3c8a
4 changed files with 288 additions and 59 deletions

View File

@@ -16,12 +16,20 @@ static int maplookup(Trie*, Str*, Str*);
typedef struct Search Search;
struct Search
{
int on;
int kind;
Str raw;
Str text;
};
static Search search;
enum
{
SearchOff,
SearchEmoji,
SearchHanja,
Hanjakey = 0x08,
};
uvlong
ownernew(void)
{
@@ -115,12 +123,12 @@ show(void)
int i, first, n;
sclear(&dc.pre);
if(search.on)
if(search.kind)
pre = search.text;
else
impre(&pre);
if(popup){
if(search.on || isjp(&im) || im.l->map == nil ||
if(search.kind || isjp(&im) || im.l->map == nil ||
!mapget(im.l->map, &pre, &dc.pre))
dc.pre = pre;
}
@@ -146,7 +154,7 @@ reset(void)
{
sclear(&im.pre);
sclear(&im.raw);
search.on = 0;
search.kind = SearchOff;
sclear(&search.raw);
sclear(&search.text);
memset(&caret, 0, sizeof caret);
@@ -460,13 +468,13 @@ addkouho(Str *s)
}
static void
emojilookup(Str *key, Dictres *res)
searchlookup(int lang, Str *key, Dictres *res)
{
Dictreq req;
req.key = *key;
req.pre = req.key;
req.lang = LangEMOJI;
req.lang = lang;
dictlookup(&req, res);
}
@@ -482,13 +490,13 @@ emojiquery(void)
transstr(im.l, &search.raw, &local);
foldascii(&local);
clearkouho();
emojilookup(&raw, &res);
searchlookup(LangEMOJI, &raw, &res);
rawhit = res.nkouho != 0;
for(i = 0; i < res.nkouho; i++)
addkouho(&res.kouho[i]);
localhit = 0;
if(scmp(&raw, &local) != 0){
emojilookup(&local, &res);
searchlookup(LangEMOJI, &local, &res);
localhit = res.nkouho != 0;
for(i = 0; i < res.nkouho; i++)
addkouho(&res.kouho[i]);
@@ -497,10 +505,44 @@ emojiquery(void)
show();
}
static void
hanjaquery(void)
{
static Dictres res;
Str key;
transstr(getlang(LangKO), &search.raw, &key);
search.text = key;
searchlookup(LangKO, &key, &res);
setkouho(&res);
show();
}
static void
searchquery(void)
{
if(search.kind == SearchEmoji)
emojiquery();
else if(search.kind == SearchHanja)
hanjaquery();
}
static int
searchkind(Rune c, u32int mod)
{
if(c == LangEMOJI)
return SearchEmoji;
if(mod & (Malt|Msuper))
return SearchOff;
if(c == Hanjakey && im.l->lang == LangKO)
return SearchHanja;
return SearchOff;
}
static void
endsearch(void)
{
search.on = 0;
search.kind = SearchOff;
sclear(&search.raw);
sclear(&search.text);
clearkouho();
@@ -508,14 +550,14 @@ endsearch(void)
}
static void
startsearch(Str *com)
startsearch(int kind, Str *com)
{
if(haspre(&im))
commit(com);
sclear(&im.pre);
sclear(&im.raw);
clearkouho();
search.on = 1;
search.kind = kind;
sclear(&search.raw);
sclear(&search.text);
show();
@@ -529,7 +571,7 @@ commitsearch(Str *com)
}
static int
pickemoji(int n, Str *com)
picksearch(int n, Str *com)
{
if(n < 0 || n >= im.nkouho)
return 0;
@@ -559,7 +601,7 @@ searchkey(u32int ks, u32int mod, Str *com)
n = ks - '1';
off = im.sel >= Maxdisp ? im.sel - Maxdisp + 1 : 0;
if(n >= 0 && n < Maxdisp && off + n < im.nkouho && mod == 0){
pickemoji(off + n, com);
picksearch(off + n, com);
return 1;
}
if(ks == Ktab){
@@ -579,7 +621,7 @@ searchkey(u32int ks, u32int mod, Str *com)
}
if(ks == Kret){
if(im.nkouho > 0)
return pickemoji(im.sel >= 0 ? im.sel : 0, com);
return picksearch(im.sel >= 0 ? im.sel : 0, com);
commitsearch(com);
return 1;
}
@@ -587,7 +629,7 @@ searchkey(u32int ks, u32int mod, Str *com)
if(search.raw.n == 0)
return 1;
spopr(&search.raw);
emojiquery();
searchquery();
return 1;
}
if(ks == Kesc){
@@ -596,8 +638,12 @@ searchkey(u32int ks, u32int mod, Str *com)
}
if(mod & Mctrl){
c = ctrlkey(ks);
if(c == LangEMOJI){
endsearch();
n = searchkind(c, mod);
if(n != SearchOff){
if(n == search.kind)
endsearch();
else
startsearch(n, com);
return 1;
}
if(c == 0x10){
@@ -618,8 +664,10 @@ searchkey(u32int ks, u32int mod, Str *com)
commitsearch(com);
return 0;
}
if(search.kind == SearchHanja)
ks = kokey(ks, mod);
sputr(&search.raw, ks);
emojiquery();
searchquery();
return 1;
}
@@ -631,7 +679,7 @@ keystroke(u32int ks, u32int mod, Str *com)
if(ismodkey(ks))
return 0;
if(search.on)
if(search.kind)
return searchkey(ks, mod, com);
if(ks == Kdown || ks == Kup){
if(im.nkouho == 0)
@@ -690,8 +738,9 @@ keystroke(u32int ks, u32int mod, Str *com)
}
if(mod & Mctrl){
c = ctrlkey(ks);
if(c == LangEMOJI){
startsearch(com);
n = searchkind(c, mod);
if(n != SearchOff){
startsearch(n, com);
return 1;
}
if(c == 0x10){
@@ -795,7 +844,7 @@ imhandlekey(Keyreq *kr)
break;
}
if(kr->want && kr->owner == activeowner){
if(search.on)
if(search.kind)
res.preedit = search.text;
else
impre(&res.preedit);
@@ -809,7 +858,7 @@ dictresult(Dictres *res)
Str pre;
impre(&pre);
if(search.on || res->lang != im.l->lang ||
if(search.kind || res->lang != im.l->lang ||
scmp(&res->key, &pre) != 0)
return;
setkouho(res);

View File

@@ -421,10 +421,10 @@ engine_direct_language_modes(struct ct *t)
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.on);
CT_CHECK(t, search.kind);
CT_EQ_INT(t, LangEN, im.l->lang);
CT_CHECK(t, keystroke(Kesc, 0, &com));
CT_CHECK(t, !search.on);
CT_CHECK(t, !search.kind);
CT_CHECK(t, keystroke('s', Mctrl, &com));
CT_EQ_INT(t, LangKO, im.l->lang);
CT_EQ_PTR(t, nil, im.l->mapname);
@@ -535,7 +535,7 @@ draindraw(Drawcmd *last)
}
static void
setemoji(Hmap **dict, char *key, char *val)
setdict(Hmap **dict, char *key, char *val)
{
Str s;
@@ -556,21 +556,21 @@ emojibegin(Emojifix *f, int showpre)
f->visible = visible;
draindraw(nil);
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, "エガオ", "😀 😄 😊");
setemoji(&l->dict, "heart", "❤️");
setemoji(&l->dict, "coffee", "☕️");
setemoji(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
setemoji(&l->dict, "^0", "");
setemoji(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
setemoji(&l->dict, "<", "← ≤ ♥ ≠");
setemoji(&l->dict, "many",
setdict(&l->dict, "a", "A B");
setdict(&l->dict, "", "B C");
setdict(&l->dict, "alpha", "α");
setdict(&l->dict, "smil", "😀 😄");
setdict(&l->dict, "smile", "😀 😄");
setdict(&l->dict, "웃음", "😀 😄 😂");
setdict(&l->dict, "えがお", "😀 😄 😊");
setdict(&l->dict, "エガオ", "😀 😄 😊");
setdict(&l->dict, "heart", "❤️");
setdict(&l->dict, "coffee", "☕️");
setdict(&l->dict, "^", "¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁽");
setdict(&l->dict, "^0", "");
setdict(&l->dict, "_", "₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₍");
setdict(&l->dict, "<", "← ≤ ♥ ≠");
setdict(&l->dict, "many",
"c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12");
init();
im.l = getlang(LangKO);
@@ -594,6 +594,52 @@ emojiend(Emojifix *f)
visible = f->visible;
}
typedef struct Hanjafix Hanjafix;
struct Hanjafix
{
Im im;
Search search;
Hmap *dict;
int popup;
int visible;
};
static void
hanjabegin(Hanjafix *f)
{
Lang *l;
l = getlang(LangKO);
f->im = im;
f->search = search;
f->dict = l->dict;
f->popup = popup;
f->visible = visible;
draindraw(nil);
l->dict = hmapalloc(8);
setdict(&l->dict, "", "漢 韓");
setdict(&l->dict, "", "");
init();
im.l = l;
}
static void
hanjaend(Hanjafix *f)
{
Lang *l;
Hmap *dict;
l = getlang(LangKO);
dict = l->dict;
l->dict = f->dict;
hmapfree(dict);
draindraw(nil);
im = f->im;
search = f->search;
popup = f->popup;
visible = f->visible;
}
static int
typekeys(struct ct *t, char *keys, Str *com)
{
@@ -779,12 +825,12 @@ engine_emoji_single_candidate(struct ct *t)
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_CHECK(t, search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
if(typekeys(t, "alpha", &com)){
checkstr(t, "no automatic commit", "", &com);
checkstr(t, "raw English query", "alpha", &search.text);
CT_CHECK(t, search.on);
CT_CHECK(t, search.kind);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "single candidate", "α", &im.kouho[0]);
CT_CHECK(t, draindraw(&dc) > 0);
@@ -792,7 +838,7 @@ engine_emoji_single_candidate(struct ct *t)
CT_EQ_INT(t, 1, dc.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "explicit candidate commit", "α", &com);
CT_CHECK(t, !search.on);
CT_CHECK(t, !search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
show();
CT_CHECK(t, draindraw(&dc) > 0);
@@ -854,7 +900,7 @@ engine_emoji_queries(struct ct *t)
CT_EQ_INT(t, 3, dc.nkouho);
CT_CHECK(t, keystroke(Kesc, 0, &com));
checkstr(t, "cancel commit", "", &com);
CT_CHECK(t, !search.on);
CT_CHECK(t, !search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
}
emojiend(&f);
@@ -932,7 +978,7 @@ engine_emoji_digit_aliases(struct ct *t)
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_CHECK(t, !search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
}
@@ -952,7 +998,7 @@ engine_emoji_digit_aliases(struct ct *t)
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_CHECK(t, search.kind);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "missing number text", "smile9", &com);
@@ -1028,19 +1074,26 @@ engine_emoji_preedit_languages(struct ct *t)
checkstr(t, "modifier preserves Japanese", "", &shown);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
checkstr(t, "committed Japanese preedit", "", &com);
CT_CHECK(t, search.on);
CT_CHECK(t, search.kind);
CT_EQ_INT(t, LangJP, im.l->lang);
CT_CHECK(t, keystroke('E', Mctrl|Malt, &com));
CT_EQ_INT(t, SearchOff, search.kind);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
CT_CHECK(t, keystroke('E', Mctrl|Msuper, &com));
CT_EQ_INT(t, SearchOff, search.kind);
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
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);
CT_CHECK(t, search.kind);
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, search.kind);
CT_CHECK(t, !keystroke(Kmodfirst+2, 0, &com));
CT_CHECK(t, keystroke('E', Mctrl|Mshift, &com));
CT_CHECK(t, !search.on);
CT_CHECK(t, !search.kind);
sclear(&com);
if(typekeys(t, "na", &com)){
shown = shownpre(&im);
@@ -1070,7 +1123,7 @@ engine_emoji_preedit_languages(struct ct *t)
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_CHECK(t, !search.kind);
CT_EQ_PTR(t, &testvi, im.l);
sclear(&com);
if(typekeys(t, "as", &com)){
@@ -1095,14 +1148,14 @@ engine_emoji_start_and_unknown(struct ct *t)
if(typekeys(t, "dkssud", &com)){
CT_CHECK(t, keystroke('e', Mctrl, &com));
checkstr(t, "committed Korean preedit", "안녕", &com);
CT_CHECK(t, search.on);
CT_CHECK(t, search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
if(typekeys(t, "xyz", &com)){
checkstr(t, "unknown displayed query", "xyz", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "unknown query commit", "안녕xyz", &com);
CT_CHECK(t, !search.on);
CT_CHECK(t, !search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
}
}
@@ -1112,7 +1165,7 @@ engine_emoji_start_and_unknown(struct ct *t)
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_CHECK(t, !search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
}
@@ -1121,12 +1174,12 @@ engine_emoji_start_and_unknown(struct ct *t)
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, search.kind);
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);
CT_CHECK(t, !search.kind);
emojiend(&f);
}
@@ -1157,12 +1210,12 @@ engine_emoji_dictionary_identity(struct ct *t)
checkstr(t, "wrong preedit ignored", "old", &im.kouho[0]);
res.key = im.pre;
search.on = 1;
search.kind = SearchEmoji;
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
checkstr(t, "search response ignored", "old", &im.kouho[0]);
search.on = 0;
search.kind = SearchOff;
res.kouho[0] = mkstr("right");
dictresult(&res);
CT_EQ_INT(t, 1, im.nkouho);
@@ -1171,6 +1224,125 @@ engine_emoji_dictionary_identity(struct ct *t)
emojiend(&f);
}
void
engine_hanja_search(struct ct *t)
{
Hanjafix f;
Str com;
hanjabegin(&f);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
CT_EQ_INT(t, SearchHanja, search.kind);
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, SearchOff, search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
cleanup:
hanjaend(&f);
}
void
engine_hanja_unknown_and_cancel(struct ct *t)
{
Hanjafix f;
Str com;
hanjabegin(&f);
sclear(&com);
if(!CT_CHECK(t, keystroke('h', Mctrl, &com)))
goto cleanup;
if(!typekeys(t, "rmf", &com))
goto cleanup;
checkstr(t, "unknown Hanja reading", "", &search.text);
CT_EQ_INT(t, 0, im.nkouho);
CT_CHECK(t, keystroke(Kret, 0, &com));
checkstr(t, "unknown Hanja commit", "", &com);
CT_EQ_INT(t, SearchOff, search.kind);
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, SearchOff, search.kind);
CT_EQ_INT(t, LangKO, im.l->lang);
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, SearchHanja, search.kind);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, SearchOff, search.kind);
cleanup:
hanjaend(&f);
}
void
engine_hanja_korean_keys(struct ct *t)
{
Hanjafix f;
Str com;
hanjabegin(&f);
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));
hanjaend(&f);
}
void
engine_hanja_korean_only(struct ct *t)
{
Hmap *dict;
Lang *l;
Str com, shown;
init();
im.l = getlang(LangJP);
sclear(&com);
if(typekeys(t, "ka", &com)){
CT_CHECK(t, !keystroke('h', Mctrl, &com));
CT_CHECK(t, !search.kind);
CT_EQ_INT(t, LangJP, im.l->lang);
shown = shownpre(&im);
checkstr(t, "non-Korean Ctrl-H reset", "", &shown);
checkstr(t, "non-Korean Ctrl-H commit", "", &com);
}
l = getlang(LangKO);
dict = l->dict;
l->dict = nil;
init();
im.l = l;
sclear(&com);
CT_CHECK(t, keystroke('h', Mctrl, &com));
CT_EQ_INT(t, SearchHanja, search.kind);
CT_CHECK(t, keystroke(Kesc, 0, &com));
l->dict = dict;
}
static u32int
stressrand(u32int *state)
{

View File

@@ -54,6 +54,10 @@ 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 engine_hanja_search(struct ct*);
void engine_hanja_unknown_and_cancel(struct ct*);
void engine_hanja_korean_keys(struct ct*);
void engine_hanja_korean_only(struct ct*);
void engine_randomized_stress(struct ct*);
void engine_full_boundary_passthrough(struct ct*);
void dictionary_candidates(struct ct*);

View File

@@ -108,6 +108,10 @@ static const struct ct_test tests[] = {
{ "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 },
{ "engine/hanja-search", engine_hanja_search },
{ "engine/hanja-unknown-cancel", engine_hanja_unknown_and_cancel },
{ "engine/hanja-korean-keys", engine_hanja_korean_keys },
{ "engine/hanja-korean-only", engine_hanja_korean_only },
{ "engine/randomized-stress", engine_randomized_stress },
{ "engine/full-boundary-passthrough", engine_full_boundary_passthrough },
{ "dict/candidates", dictionary_candidates },