ibus: the text before the cursor, and the runes to take back

RequireSurroundingText asks the client to send its text; it arrives
through SetSurroundingText as an IBusText and a cursor counted in
runes, and DeleteSurroundingText asks for runes back before the commit
that replaces them.  The official libibus client now proves the whole
turn: 한자 typed, the Hanja key, Enter, and 漢字 arrives with the 한
taken away.

A client that has set EffectivePostProcessKeyEvent is never asked for
its text.  It reads the key's commits back after ProcessKeyEvent
returns, and a DeleteSurroundingText signal is not one of the things
that reply can carry, so the deletion would land after the text it was
meant to make room for and eat the wrong runes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 14:31:44 +09:00
parent 910bf51347
commit 921e632eae
3 changed files with 179 additions and 7 deletions

92
ibus.c
View File

@@ -16,6 +16,7 @@ enum
Relmask = 1<<30, Relmask = 1<<30,
/* IBus wire constants; the daemon does not link libibus. */ /* IBus wire constants; the daemon does not link libibus. */
Ibuscappreedit = 1<<0, Ibuscappreedit = 1<<0,
Ibuscapsurround = 1<<5,
Ibusattrunderline = 1, Ibusattrunderline = 1,
Ibusunderlinesingle = 1, Ibusunderlinesingle = 1,
Ibuspreeditclear = 0, Ibuspreeditclear = 0,
@@ -51,6 +52,7 @@ struct Ictx
int npost; int npost;
Post post[Maxpost]; Post post[Maxpost];
Caret caret; Caret caret;
Str surround; /* the client's own text just before the cursor */
}; };
static DBusWatch *watches[Maxwatches]; static DBusWatch *watches[Maxwatches];
@@ -304,6 +306,11 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res)
kr.ks = ks; kr.ks = ks;
kr.mod = mod; kr.mod = mod;
kr.caret = ctx->caret; kr.caret = ctx->caret;
/* A client that reads the key's effects back afterwards takes a
* DeleteSurroundingText signal after them, which is too late to be
* a taking back: it is never asked for text it cannot lose. */
if(!ctx->postprocess)
kr.surround = ctx->surround;
kr.reply = replyc; kr.reply = replyc;
chansend(keyc, &kr); chansend(keyc, &kr);
chanrecv(replyc, res); chanrecv(replyc, res);
@@ -464,6 +471,34 @@ emitpreedit(Ictx *ctx, const char *text)
preowner = nil; preowner = nil;
} }
/* The client is to take back the runes the reading reached into. */
static void
emitdelete(Ictx *ctx, int n)
{
DBusMessage *sig;
dbus_int32_t off;
dbus_uint32_t nrune;
off = -n;
nrune = n;
sig = newsignal(ctx->path, "DeleteSurroundingText");
dbus_message_append_args(sig, DBUS_TYPE_INT32, &off,
DBUS_TYPE_UINT32, &nrune, DBUS_TYPE_INVALID);
dbus_connection_send(ctx->conn, sig, nil);
dbus_message_unref(sig);
}
/* Nothing arrives until the client is asked to send it. */
static void
emitrequiresurround(Ictx *ctx)
{
DBusMessage *sig;
sig = newsignal(ctx->path, "RequireSurroundingText");
dbus_connection_send(ctx->conn, sig, nil);
dbus_message_unref(sig);
}
/* Another frontend may have taken the engine; then our preedit is stale. */ /* Another frontend may have taken the engine; then our preedit is stale. */
static void static void
checkpreowner(void) checkpreowner(void)
@@ -665,6 +700,8 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx)
ctx->keying = 1; ctx->keying = 1;
if(restart) if(restart)
emitpreedit(ctx, ""); emitpreedit(ctx, "");
if(res.del > 0)
emitdelete(ctx, res.del);
if(commit[0] != '\0') if(commit[0] != '\0')
emitcommit(ctx, commit); emitcommit(ctx, commit);
if(text[0] != '\0') if(text[0] != '\0')
@@ -732,6 +769,7 @@ handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx)
flushcontext(ctx, Keyrelease); flushcontext(ctx, Keyrelease);
ctx->focused = 0; ctx->focused = 0;
memset(&ctx->caret, 0, sizeof ctx->caret); memset(&ctx->caret, 0, sizeof ctx->caret);
sclear(&ctx->surround);
return reply(c, m, DBUS_TYPE_INVALID, nil); return reply(c, m, DBUS_TYPE_INVALID, nil);
} }
@@ -746,6 +784,8 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap, DBUS_TYPE_INVALID); dbus_message_get_args(m, nil, DBUS_TYPE_UINT32, &cap, DBUS_TYPE_INVALID);
old = ctx->cap; old = ctx->cap;
ctx->cap = cap; ctx->cap = cap;
if((cap & ~old) & Ibuscapsurround)
emitrequiresurround(ctx);
if(ctx->focused){ if(ctx->focused){
sendrequest(ctx, Keycap, 0, 0, &res); sendrequest(ctx, Keycap, 0, 0, &res);
if((old & Ibuscappreedit) != (ctx->cap & Ibuscappreedit)){ if((old & Ibuscappreedit) != (ctx->cap & Ibuscappreedit)){
@@ -759,6 +799,57 @@ handlecap(DBusConnection *c, DBusMessage *m, Ictx *ctx)
return reply(c, m, DBUS_TYPE_INVALID, nil); return reply(c, m, DBUS_TYPE_INVALID, nil);
} }
/* IBusText is (sa{sv}sv); the string in it is all we want. */
static int
getibustext(DBusMessageIter *value, const char **s)
{
DBusMessageIter st;
int i;
if(dbus_message_iter_get_arg_type(value) != DBUS_TYPE_STRUCT)
return 0;
dbus_message_iter_recurse(value, &st);
for(i = 0; i < 2; i++)
if(!dbus_message_iter_next(&st))
return 0;
if(dbus_message_iter_get_arg_type(&st) != DBUS_TYPE_STRING)
return 0;
dbus_message_iter_get_basic(&st, s);
return 1;
}
/* The bytes the first n runes of s take; IBus counts a cursor in runes. */
static int
runebytes(const char *s, int n)
{
Rune r;
char *p;
for(p = (char*)s; n > 0 && *p != '\0'; n--)
p += chartorune(&r, p);
return p - s;
}
/* The text around the cursor; a reading can use what comes before it. */
static DBusHandlerResult
handlesurround(DBusConnection *c, DBusMessage *m, Ictx *ctx)
{
DBusMessageIter it, v;
dbus_uint32_t cursor;
const char *s;
if(!dbus_message_iter_init(m, &it))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetSurroundingText expects text, cursor, and anchor");
dbus_message_iter_recurse(&it, &v);
if(!getibustext(&v, &s) || !dbus_message_iter_next(&it))
return handleerror(c, m, DBUS_ERROR_INVALID_ARGS,
"SetSurroundingText expects an IBusText");
dbus_message_iter_get_basic(&it, &cursor);
stail(&ctx->surround, (char*)s, runebytes(s, cursor));
return reply(c, m, DBUS_TYPE_INVALID, nil);
}
static int static int
getproperty(DBusMessage *m, const char **iface, const char **name, getproperty(DBusMessage *m, const char **iface, const char **name,
DBusMessageIter *value) DBusMessageIter *value)
@@ -913,6 +1004,7 @@ static struct {
{"SetCursorLocation", "iiii", handlecursor}, {"SetCursorLocation", "iiii", handlecursor},
{"SetCursorLocationRelative", "iiii", handleok}, {"SetCursorLocationRelative", "iiii", handleok},
{"SetCapabilities", "u", handlecap}, {"SetCapabilities", "u", handlecap},
{"SetSurroundingText", "vuu", handlesurround},
{"SetEngine", "s", handleok}, {"SetEngine", "s", handleok},
}; };

View File

@@ -30,6 +30,12 @@ struct Log
int rcommit; int rcommit;
int rdone; int rdone;
int flushed; int flushed;
int hanja;
int required;
int deleted;
int delbefore;
int delcount;
int converted;
}; };
static void static void
@@ -142,6 +148,17 @@ commit(IBusInputContext *ctx, IBusText *text, void *arg)
log->commit++; log->commit++;
s = ibus_text_get_text(text); s = ibus_text_get_text(text);
attrs = ibus_text_get_attributes(text); attrs = ibus_text_get_attributes(text);
if(log->hanja){
/* The word converted, with the client\'s own syllable taken back. */
if(strcmp(s, "漢字") == 0 && log->deleted && log->delbefore == -1 &&
log->delcount == 1)
log->converted = 1;
else
log->invalid = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
return;
}
if(log->repeat == 3){ if(log->repeat == 3){
/* Focus loss hands the pending syllable back as a commit. */ /* Focus loss hands the pending syllable back as a commit. */
if(strcmp(s, "") == 0) if(strcmp(s, "") == 0)
@@ -184,6 +201,32 @@ commit(IBusInputContext *ctx, IBusText *text, void *arg)
g_main_loop_quit(log->loop); g_main_loop_quit(log->loop);
} }
static void
required(IBusInputContext *ctx, void *arg)
{
Log *log;
(void)ctx;
log = arg;
log->required = 1;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static void
deleted(IBusInputContext *ctx, gint offset, guint n, void *arg)
{
Log *log;
(void)ctx;
log = arg;
log->deleted = 1;
log->delbefore = offset;
log->delcount = n;
if(log->loop != NULL && log->waiting != NULL && *log->waiting)
g_main_loop_quit(log->loop);
}
static gboolean static gboolean
timeout(void *arg) timeout(void *arg)
{ {
@@ -214,7 +257,8 @@ int
main(int argc, char **argv) main(int argc, char **argv)
{ {
IBusBus *bus; IBusBus *bus;
IBusInputContext *a, *b; IBusInputContext *a, *b, *c;
IBusText *around;
Log log; Log log;
int ok; int ok;
@@ -229,7 +273,7 @@ main(int argc, char **argv)
return 1; return 1;
} }
memset(&log, 0, sizeof log); memset(&log, 0, sizeof log);
b = NULL; b = c = NULL;
ibus_init(); ibus_init();
bus = ibus_bus_new(); bus = ibus_bus_new();
if(bus == NULL || !ibus_bus_is_connected(bus)){ if(bus == NULL || !ibus_bus_is_connected(bus)){
@@ -286,20 +330,55 @@ main(int argc, char **argv)
log.repeat = 3; log.repeat = 3;
ibus_input_context_focus_out(a); ibus_input_context_focus_out(a);
ok = ok && waitflag(&log, &log.flushed); ok = ok && waitflag(&log, &log.flushed);
if(log.legacy == 0 || log.modern != 0 || log.commit != 4 || /*
log.invalid || !log.done || !log.atext || !log.aclear || * A word is committed a syllable at a time, so the Hanja key must
!log.retook || log.clearctx != a || !log.rdone) * reach into what the client already holds and ask for it back.
*/
c = ibus_bus_create_input_context(bus, "strans-libibus-smoke-c");
if(c == NULL)
ok = 0; ok = 0;
else{
g_signal_connect(c, "commit-text", G_CALLBACK(commit), &log);
g_signal_connect(c, "require-surrounding-text",
G_CALLBACK(required), &log);
g_signal_connect(c, "delete-surrounding-text",
G_CALLBACK(deleted), &log);
log.repeat = 0;
ibus_input_context_set_capabilities(c,
IBUS_CAP_PREEDIT_TEXT|IBUS_CAP_SURROUNDING_TEXT);
ibus_input_context_focus_in(c);
ok = ok && waitflag(&log, &log.required);
around = ibus_text_new_from_string("저는 한");
ibus_input_context_set_surrounding_text(c, around, 4, 4);
log.hanja = 1;
ok = ok && ibus_input_context_process_key_event(c, 's', 0,
IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(c, 'w', 0, 0) &&
ibus_input_context_process_key_event(c, 'k', 0, 0) &&
ibus_input_context_process_key_event(c, 'h', 0,
IBUS_CONTROL_MASK) &&
ibus_input_context_process_key_event(c, IBUS_KEY_Return,
0, 0) && waitflag(&log, &log.converted);
log.hanja = 0;
}
if(log.legacy == 0 || log.modern != 0 || log.commit != 5 ||
log.invalid || !log.done || !log.atext || !log.aclear ||
!log.retook || log.clearctx != a || !log.rdone || !log.converted)
ok = 0;
if(c != NULL)
g_object_unref(c);
if(b != NULL) if(b != NULL)
g_object_unref(b); g_object_unref(b);
g_object_unref(a); g_object_unref(a);
g_object_unref(bus); g_object_unref(bus);
if(!ok){ if(!ok){
fprintf(stderr, fprintf(stderr,
"ibus_client_smoke: legacy=%d modern=%d commit=%d invalid=%d preedit=%d committed=%d transfer-text=%d a-clear=%d retook=%d repeat=%s flushed=%d\n", "ibus_client_smoke: legacy=%d modern=%d commit=%d invalid=%d preedit=%d committed=%d transfer-text=%d a-clear=%d retook=%d repeat=%s flushed=%d required=%d deleted=%d,%d converted=%d\n",
log.legacy, log.modern, log.commit, log.invalid, log.legacy, log.modern, log.commit, log.invalid,
log.sawpreedit, log.sawcommit, log.atext, log.aclear, log.sawpreedit, log.sawcommit, log.atext, log.aclear,
log.retook, log.revent, log.flushed); log.retook, log.revent, log.flushed, log.required,
log.delbefore, log.delcount, log.converted);
return 1; return 1;
} }
printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n"); printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n");

View File

@@ -129,6 +129,7 @@ runcontract(char *address)
"SetCapabilities", "SetCapabilities",
"SetCursorLocation", "SetCursorLocation",
"SetCursorLocationRelative", "SetCursorLocationRelative",
"SetSurroundingText",
}; };
DBusConnection *conn; DBusConnection *conn;
char path[96]; char path[96];