engine: Ctrl+H reaches back into the text the client already has

Korean commits a syllable as the next one begins, so by the time the
한자 key is pressed only the last syllable is still ours: typing 한자 and
then Ctrl+H asked about 자 alone and answered 子, leaving 한子 in the
document -- the mixed Hangul and Hanja that was refused as dictionary
data, made by the interaction instead.  To get 漢字 the key had to be
pressed before typing, which no other Korean input method asks for.

A request now carries the client's own text just before the cursor, and
the reading reaches back through it as far as the dictionary still knows
the whole of it: 한 joins 자 and 대한민 joins 국.  Nothing but the
dictionary says where to stop, because a reading is syllables, so a key
holding a space or an already converted Hanja leads nowhere and the
reach ends there.  A pick answers with the count of runes to take back
first; Escape gives back only what was pending, and a query with no
candidate types only the part the client lacks.

A frontend that sends no surrounding text reaches back by nothing and
behaves exactly as before, which is what XIM will keep doing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:17:08 +09:00
parent 997e4c8d93
commit f7277f54a3
5 changed files with 169 additions and 13 deletions

View File

@@ -7,17 +7,23 @@ static void *lostowner; /* the context the engine was taken from */
static Str lost;
static int clientpre; /* the owner draws its own preedit */
static Caret caret;
static Str surround; /* the owner's own text just before the cursor */
static int takeback; /* runes of it a pick takes back before committing */
static Rune modemark; /* the mode the last key switched to */
static int okuriat = -1; /* where Shift marked the okurigana, or -1 */
static Drawcmd lastdraw;
static Emit transjp(Im*, Rune);
static void backjp(Im*);
/* seed is what Ctrl+H found pending; raw the keys typed since; text both. */
/*
* back is the client's own text the reading reached into; seed what
* Ctrl+H found pending; raw the keys typed since; text all three.
*/
typedef struct Search Search;
struct Search
{
int lang;
Str back;
Str seed;
Str raw;
Str text;
@@ -185,6 +191,17 @@ impre(Im *p, Str *s)
*s = p->pre;
}
/* The query as it is still ours to show: the client has the rest. */
static void
searchpre(Str *s)
{
int i;
sclear(s);
for(i = search.back.n; i < search.text.n; i++)
sputr(s, search.text.r[i]);
}
static void
snapshot(Drawcmd *dc)
{
@@ -193,7 +210,7 @@ snapshot(Drawcmd *dc)
memset(dc, 0, sizeof *dc);
if(search.lang)
pre = search.text;
searchpre(&pre);
else
impre(&im, &pre);
if(!clientpre)
@@ -636,7 +653,11 @@ emojiquery(void)
static void
hanjaquery(void)
{
transstr(im.l, &search.seed, &search.raw, &search.text);
Str tail;
transstr(im.l, &search.seed, &search.raw, &tail);
search.text = search.back;
sappend(&search.text, &tail);
clearkouho();
im.nkouho = dictprefix(getlang(LangHANJA)->dict, &search.text,
im.kouho, Maxkouho);
@@ -693,9 +714,12 @@ reset(void)
static void
flush(Str *com)
{
if(search.lang)
sappend(com, &search.text);
else if(im.sel >= 0)
Str pre;
if(search.lang){
searchpre(&pre);
sappend(com, &pre);
}else if(im.sel >= 0)
sappend(com, &im.kouho[im.sel]);
else
commitim(&im, com);
@@ -716,6 +740,46 @@ takelost(void *owner, Str *com)
lostowner = nil;
}
/* The syllables from i to the end of the client's text, then seed. */
static void
reachkey(int i, Str *seed, Str *key)
{
sclear(key);
for(; i < surround.n; i++)
sputr(key, surround.r[i]);
sappend(key, seed);
}
/*
* Ctrl+H converts a word, and a word is committed one syllable at a
* time: only its last syllable is still pending. So the reading reaches
* back into the text the client already has, as far as the dictionary
* still knows the whole of it -- 한자 converts as a word where the 한 is
* the client's and only the 자 is ours. Nothing but the dictionary says
* where to stop: a reading is syllables, so a key holding a space or a
* Hanja leads nowhere and the reach ends there.
*/
static void
reachback(Str *seed, Str *back)
{
Str key;
Trie *t;
int i, best;
t = getlang(LangHANJA)->dict;
best = surround.n;
for(i = surround.n - 1; i >= 0; i--){
if(surround.n - i + seed->n > Maxrunes)
break;
reachkey(i, seed, &key);
if(trienode(t, &key) >= 0)
best = i;
}
sclear(back);
for(i = best; i < surround.n; i++)
sputr(back, surround.r[i]);
}
/*
* A search takes over the keys. Hanja converts the pending syllable, so
* it becomes the query; anything else pending is committed.
@@ -723,23 +787,28 @@ takelost(void *owner, Str *com)
static void
startsearch(int lang, Str *com)
{
Str seed;
Str back, seed;
sclear(&back);
sclear(&seed);
if(lang == LangHANJA && !search.lang && im.sel < 0)
if(lang == LangHANJA && !search.lang && im.sel < 0){
impre(&im, &seed);
else
reachback(&seed, &back);
}else
flush(com);
reset();
search.lang = lang;
search.back = back;
search.seed = seed;
if(lang == LangHANJA)
searchquery();
}
/* The client keeps what the reading reached back into; we take it back. */
static void
picksearch(int n, Str *com)
{
takeback = search.back.n;
sappend(com, &im.kouho[n]);
endsearch();
}
@@ -954,6 +1023,8 @@ init(void)
activeowner = nil;
lostowner = nil;
clientpre = 0;
sclear(&surround);
takeback = 0;
memset(&lastdraw, 0, sizeof lastdraw);
}
@@ -972,6 +1043,7 @@ imhandlekey(Keyreq *kr)
sclear(&res.commit);
sclear(&res.preedit);
res.eaten = 1;
takeback = 0;
switch(kr->op){
case Keyrelease:
takelost(kr->owner, &res.commit);
@@ -1009,6 +1081,7 @@ imhandlekey(Keyreq *kr)
clientpre = kr->clientpre;
}
caret = kr->caret;
surround = kr->surround;
res.eaten = transition(kr->ks, kr->mod, &res.commit);
}
break;
@@ -1016,10 +1089,11 @@ imhandlekey(Keyreq *kr)
redraw();
if(kr->owner == activeowner){
if(search.lang)
res.preedit = search.text;
searchpre(&res.preedit);
else
impre(&im, &res.preedit);
}
res.del = takeback;
chansend(kr->reply, &res);
}