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

@@ -10,6 +10,7 @@ keystroke(u32int ks, u32int mod, Str *com)
{
int eaten;
takeback = 0;
eaten = keymeaningful(ks) && transition(ks, mod, com);
redraw();
return eaten;
@@ -175,7 +176,7 @@ engine_candidate_shortcut_modifiers(struct ct *t)
static Keyres
ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
Caret *caret)
Caret *caret, char *before)
{
Channel *reply;
Keyreq req;
@@ -190,6 +191,8 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
req.mod = mod;
if(caret != nil)
req.caret = *caret;
if(before != nil)
req.surround = mkstr(before);
req.reply = reply;
imhandlekey(&req);
chanrecv(reply, &res);
@@ -200,7 +203,7 @@ ownerrequestatcap(void *owner, int cap, int op, Rune key, u32int mod,
static Keyres
ownerrequestat(void *owner, int op, Rune key, u32int mod, Caret *caret)
{
return ownerrequestatcap(owner, 0, op, key, mod, caret);
return ownerrequestatcap(owner, 0, op, key, mod, caret, nil);
}
static Keyres
@@ -212,7 +215,7 @@ ownerrequest(void *owner, int op, Rune key, u32int mod)
static Keyres
ownerrequestcap(void *owner, int cap, int op, Rune key, u32int mod)
{
return ownerrequestatcap(owner, cap, op, key, mod, nil);
return ownerrequestatcap(owner, cap, op, key, mod, nil, nil);
}
void
@@ -1945,6 +1948,81 @@ 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);
}
/* 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.