From 921e632eae370e8a828b0322a8de13646381e779 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 18 Aug 2026 14:31:44 +0900 Subject: [PATCH] ibus: the text before the cursor, and the runes to take back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- ibus.c | 92 ++++++++++++++++++++++++++++++++++++++ tests/ibus_client_smoke.c | 93 ++++++++++++++++++++++++++++++++++++--- tests/ibus_live_test.c | 1 + 3 files changed, 179 insertions(+), 7 deletions(-) diff --git a/ibus.c b/ibus.c index 6c291af..f6452f3 100644 --- a/ibus.c +++ b/ibus.c @@ -16,6 +16,7 @@ enum Relmask = 1<<30, /* IBus wire constants; the daemon does not link libibus. */ Ibuscappreedit = 1<<0, + Ibuscapsurround = 1<<5, Ibusattrunderline = 1, Ibusunderlinesingle = 1, Ibuspreeditclear = 0, @@ -51,6 +52,7 @@ struct Ictx int npost; Post post[Maxpost]; Caret caret; + Str surround; /* the client's own text just before the cursor */ }; static DBusWatch *watches[Maxwatches]; @@ -304,6 +306,11 @@ sendrequest(Ictx *ctx, int op, u32int ks, u32int mod, Keyres *res) kr.ks = ks; kr.mod = mod; 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; chansend(keyc, &kr); chanrecv(replyc, res); @@ -464,6 +471,34 @@ emitpreedit(Ictx *ctx, const char *text) 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. */ static void checkpreowner(void) @@ -665,6 +700,8 @@ handlekey(DBusConnection *c, DBusMessage *m, Ictx *ctx) ctx->keying = 1; if(restart) emitpreedit(ctx, ""); + if(res.del > 0) + emitdelete(ctx, res.del); if(commit[0] != '\0') emitcommit(ctx, commit); if(text[0] != '\0') @@ -732,6 +769,7 @@ handlefocusout(DBusConnection *c, DBusMessage *m, Ictx *ctx) flushcontext(ctx, Keyrelease); ctx->focused = 0; memset(&ctx->caret, 0, sizeof ctx->caret); + sclear(&ctx->surround); 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); old = ctx->cap; ctx->cap = cap; + if((cap & ~old) & Ibuscapsurround) + emitrequiresurround(ctx); if(ctx->focused){ sendrequest(ctx, Keycap, 0, 0, &res); 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); } +/* 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 getproperty(DBusMessage *m, const char **iface, const char **name, DBusMessageIter *value) @@ -913,6 +1004,7 @@ static struct { {"SetCursorLocation", "iiii", handlecursor}, {"SetCursorLocationRelative", "iiii", handleok}, {"SetCapabilities", "u", handlecap}, + {"SetSurroundingText", "vuu", handlesurround}, {"SetEngine", "s", handleok}, }; diff --git a/tests/ibus_client_smoke.c b/tests/ibus_client_smoke.c index 673d013..7892fbc 100644 --- a/tests/ibus_client_smoke.c +++ b/tests/ibus_client_smoke.c @@ -30,6 +30,12 @@ struct Log int rcommit; int rdone; int flushed; + int hanja; + int required; + int deleted; + int delbefore; + int delcount; + int converted; }; static void @@ -142,6 +148,17 @@ commit(IBusInputContext *ctx, IBusText *text, void *arg) log->commit++; s = ibus_text_get_text(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){ /* Focus loss hands the pending syllable back as a commit. */ if(strcmp(s, "ㅋ") == 0) @@ -184,6 +201,32 @@ commit(IBusInputContext *ctx, IBusText *text, void *arg) 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 timeout(void *arg) { @@ -214,7 +257,8 @@ int main(int argc, char **argv) { IBusBus *bus; - IBusInputContext *a, *b; + IBusInputContext *a, *b, *c; + IBusText *around; Log log; int ok; @@ -229,7 +273,7 @@ main(int argc, char **argv) return 1; } memset(&log, 0, sizeof log); - b = NULL; + b = c = NULL; ibus_init(); bus = ibus_bus_new(); if(bus == NULL || !ibus_bus_is_connected(bus)){ @@ -286,20 +330,55 @@ main(int argc, char **argv) log.repeat = 3; ibus_input_context_focus_out(a); ok = ok && waitflag(&log, &log.flushed); - if(log.legacy == 0 || log.modern != 0 || log.commit != 4 || - log.invalid || !log.done || !log.atext || !log.aclear || - !log.retook || log.clearctx != a || !log.rdone) + /* + * A word is committed a syllable at a time, so the Hanja key must + * 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; + 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) g_object_unref(b); g_object_unref(a); g_object_unref(bus); if(!ok){ 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.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; } printf("official libibus client preedit, commit, owner clear, take-back and focus-out hand-back: ok\n"); diff --git a/tests/ibus_live_test.c b/tests/ibus_live_test.c index 4209884..1359c11 100644 --- a/tests/ibus_live_test.c +++ b/tests/ibus_live_test.c @@ -129,6 +129,7 @@ runcontract(char *address) "SetCapabilities", "SetCursorLocation", "SetCursorLocationRelative", + "SetSurroundingText", }; DBusConnection *conn; char path[96];