startsearch let reachback walk the client's text whenever Ctrl+H found
im.sel < 0, without asking whether anything was pending, so a Ctrl+H
pressed to begin a reading took the syllables already written instead.
With the cursor after 입니다 the query became 다, the preedit showed
nothing at all, and Space committed 多 over the 다 the user had written;
typing the reading the key was pressed for gave 다한, so 입니다漢 came
out 입니多恨. A reading is what is pending, and there is none.
The seeded reach is untouched: 한 committed with 자 pending still
converts 한자 as a word and takes the 한 back.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Space and Tab convert a Japanese reading, and a reading with no
candidate to step through was committed and the key eaten. Katakana
mode converts nothing -- katakouho adds the Katakana form only when it
differs from the reading, and in Katakana mode it never does -- so
every Space between two Katakana words was swallowed and had to be
typed twice: カク<Space> committed カク and left no space, and the
second Space passed through only because nothing was pending any more.
Committing and passing the key on gives the space back. Hiragana is
unchanged wherever the reading has kana, since the Katakana form is
always one candidate there; it changes only for a reading that made no
kana at all, かx<Space>, which now commits and spaces too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hanjaquery matched the reading exactly, so a word was reachable only
once every syllable of it had been typed, and the popup went blank on
the way there. Measured over map/hanja.dict's 187,304 readings:
95,024 proper prefixes of a word answer with nothing today, and 66,731
of those are the keystroke just before the word completes -- 대한민 is
one, so 대한민국 looks absent until the last key lands.
dictprefix walks the entry at a node before its children, so the
reading's own conversions keep their place and the words follow: 34,441
readings gain candidates and none of the 187,304 has its existing order
changed. A jamo reading is untouched, since the two keyspaces do not
meet and ㅁ has no children.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
okuriletter() puts じ in the z row, so dictqokuri can never build a key
ending in j. map/kanji.dict has 17 of them, and 8 have no z twin at all:
before after
shinjiru 死んじる 信じる 死んじる
tojiru (nothing) 綴じる 閉じる
tsuujiru (nothing) 通じる
shoujiru (nothing) 生じる
toujiru 問うじる 投じる 問うじる
koujiru 乞うじる 請うじる 高じる 乞うじる 請うじる
gozonji 御存知 御存知 御存じ
majiru 混じる 先じる 交じる 混じる
信じる, 閉じる, 通じる, 生じる, 投じる, 交じる: the dictionary holds every
one and the engine could reach none. What it offered instead was the
next split down -- しんじる falls back to しn, so 死んじる is what the
popup shows for 信じる, and it is the only candidate.
The letter is not ours to choose; it is the one the SKK dictionary was
keyed by, which is the one the user typed. Real SKK carries both spellings
where both are typed, and here that is 9 of the 17 -- えんj/えんz, かんj/かんz
and their kin, identical values on both. Those 8 z rows go unreachable and
lose nothing, because their j twin says the same word. ま is the one pair
that differs, まj=交 混 against まz=混 先, and both stay reachable: じ takes
the j road and ぜ/ず keep the z one.
mazeru 混ぜる 先ぜる unchanged
kanarazu 必ず ... unchanged
mizu 水 見ず unchanged
dekizu 出来ず unchanged
The z row keeps ざずぜぞ, so nothing that is not じ moves.
What this does not cover, deliberately: ち. kanji.dict has 7 keys ending
in c, from users who typed "chi", and okuriletter puts ち in the t row.
Giving c its own row would have to take ち out of t, and t is where the
larger entry lives -- おt is 落 折 負 追 against おc's 落 alone -- so the c
rows stay unreachable and lose nothing. づ needed no such choice: it is
in the d row, もとd is 基, and もとz is its duplicate.
movekouho() answers every one of Up, Down, PageUp and PageDown the same
way when nothing is chosen yet -- `if(im.sel < 0) im.sel = 0;` -- and
delta is thrown away. For Up and Down that is right: the list is drawn
with no cursor, so the first move takes the first candidate. For
PageDown it is not, because there is no page state to move; pagefirst()
derives the page from im.sel alone, so selecting candidate 1 leaves the
page exactly where it was.
dictqjp() leaves sel at -1 on every keystroke, so a Japanese list is
always untouched when it first appears. Type かく, look at its thirty
candidates, and press PageDown for the next nine:
before after
かく, PageDown sel 0, page 0 sel 9, page 9
かく, PageDown twice sel 9, page 9 sel 18, page 18
かく, PageUp sel 0, page 0 unchanged
かく, Down sel 0, page 0 unchanged
Nothing on screen answers the first press but the highlight appearing on
row 1, and the page turns only on the second. fcitx5 pages on the first,
because its candidate list carries a page of its own and
toPageable()->next() does not touch the cursor
(ref-fcitx5-hangul/src/engine.cpp:326-336). strans has one number where
fcitx5 has two, which is the right trade for nine rows and 128
candidates -- but then the number has to move by a page when a page key
asks for one.
`delta == Maxdisp` rather than `delta > 0` is deliberate: Down must still
land on candidate 1, and writing it the loose way fails both this table
and engine/japanese-candidates at engine_test.c:1198, where the language
switch takes 漢字 and would take 幹事 instead. PageUp from an untouched
list still clamps to 0; there is no page above the first.
searchkey shares movekouho, and is unaffected: emojiquery and hanjaquery
both end in selectfirst(), so a search list is never untouched while it
has candidates.
Backspace, Enter, Tab, Escape and the arrow and page keys are matched by
keysym alone -- strans.c:852, 875 and 882 name them, and searchkey names
the same set at strans.c:746-786. Every other key above Kspec falls
through to the catch-all at strans.c:898, `ks >= Kspec || chord(mod)`,
and goes to the application. So the special keys strans knows by name
are the ones it takes under a modifier, and the ones it does not know are
the ones it hands over. That is backwards: a named special key under a
modifier is exactly the one the application has a binding for.
In Korean a syllable is pending for nearly all the time anyone is typing,
ko.c holding one and no more, and the guards at strans.c:876 and 883
return 0 only when nothing is pending -- so the key is eaten precisely
when it is wanted. Type 안녕하세요 and reach for Ctrl+Backspace to take
the word back: 요 loses ㅛ, then ㅇ, and the word itself goes on the third
press. Ctrl+Enter in a chat box, Ctrl+Tab in a browser and Ctrl+PageDown
in either are the same key eaten by the same lines.
before after
Korean 가, Ctrl+Backspace eaten, pre ㄱ passed, commit 가
Korean 가, Alt+Backspace eaten, pre ㄱ passed, commit 가
Korean 가, Super+Backspace eaten, pre ㄱ passed, commit 가
Korean 가, Backspace eaten, pre ㄱ unchanged
Korean 가, Shift+Backspace eaten, pre ㄱ unchanged
かく, Ctrl+Enter eaten, commit かく passed, commit かく
かく, Ctrl+Tab eaten, commit かく passed, commit かく
かく Space, Ctrl+PageDown eaten, sel 0 -> 9 passed, commit 確
かく Space, PageDown eaten, sel 0 -> 9 unchanged
かく Space, Shift+Tab eaten, sel 0 -> 30 unchanged
Ctrl+E sm, Ctrl+Backspace eaten, query s passed, commit sm
Ctrl+E sm, Backspace eaten, query s unchanged
chord() cannot be reused here. It is `(mod & ~Mshift) != 0 && mod !=
Mctrl` and the exclusion is deliberate, since Ctrl+letter is strans's
whole command set and chord() has to let plain Ctrl through. The rule
this needs is the other one -- any modifier that is not Shift -- and
Shift must stay in: Shift+Tab cycles the candidates backwards, pinned by
engine/candidate-completion and engine/emoji-navigation at
engine_test.c:914 and 1649, and Shift on a Korean key is what makes ㅃ.
One line serves both paths because it sits above the searchkey dispatch,
and it has to sit below the switch at strans.c:829: 한자, 한/영, 変換 and
無変換 arrive as keys above Kspec and are rewritten there into the Ctrl
chords they stand for. Above the switch, 한자 would commit and pass
instead of opening the Hanja list; below it, pressing it with Ctrl held
still opens the list, because the switch sets the modifier itself.
What this does not cover: Space, which is below Kspec, so searchkey:763
still picks a candidate on Ctrl+Space inside a search. transition tests
!(mod & ~Mshift) for its own Space at strans.c:865, so the two disagree
there. Left alone: what Ctrl+Space should mean wants its own argument,
not a widened guard.
takelost's own comment says the context the engine was taken from "gets
that text back when it is next heard from", but it was wired to the
Keyrelease and Keyreset arms only. The Keypress arm opens with
sclear(&lost), so the one way a context is most obviously heard from --
somebody typing into it -- is the way that threw its text away.
Compose in A, type in B before A's focus-out arrives, then go back to A
and type: A's reading is gone rather than handed back. That interleave is
not exotic here, it is the premise takelost exists for. Focus-out can
arrive after the next context's keys, which is why the engine changes
hands with text still pending; and a context whose focus-out is still in
flight can be typed into again just as easily as it can be reset.
engine_test.c: the taken owner types and gets its reading back:
want "か", got ""
The call has to go above the owner switch, because that switch's own
sclear(&lost) is what destroys the text -- putting it after the switch
instead fails the same line. Above it, the order also comes out right:
the recovered reading is appended to the commit first and whatever the key
itself commits follows it.
What this does not cover: there is one lost slot. A third context taking
over between the two still destroys the first one's text for good, and
widening that means an array where a single Str is now -- a new structure
for an interleave that needs three contexts and no focus-out from any of
them. Left as it is.
ibus_client_smoke was pinning the defect rather than merely missing it: it
required exactly three commits on context a, and its handler rejected any
commit text but か, so the recovered reading tripped both. Bumping the
count would have been a constant with nothing behind it, so the take-back
got a stage of its own and the text is asserted -- the official libibus
client now watches the whole thing, and without the fix reports
retook=0
One assertion was written and then deleted. takelost now runs on every
key rather than only on the two lifecycle ops, so re-committing the same
text on every keystroke looked like the hazard worth pinning, and "the
reading comes back once" went in beside the others. Making takelost skip
its lostowner = nil left all 90 passing: the owner switch reassigns
lostowner to the context it just displaced, so lostowner is never the
owner of the next key, and the hazard is unreachable. An assertion that
cannot fail is decoration, so it is not in the tree.
90 unit, check-live, check-stress and valgrind clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The mark a mode switch leaves, from 6a6749e, is cleared at the top of
transition(), so the next key takes it away. Nothing takes it away when
there is no next key. reset() clears the pending text, the raw state, the okurigana split
and the search, and leaves modemark alone -- so a release or a lifecycle
reset flushes everything the popup was showing except the one thing that
is still drawn.
Then samedraw() sees a picture identical to the last one and redraw()
sends nothing, and win.c only ever unmaps from a Drawcmd (winshow, the
sole caller of both map and unmap). So the popup stays. Press the 한/영
key, type nothing, and alt-tab: 한 sits above every window, override
redirect and typed as a tooltip, until the next keystroke in any
strans-aware field -- which may be minutes. Counted on a private X
server, viewable override-redirect windows:
before after
at rest 0 0
after Ctrl+S 1 1
after letting go 1 0
reset() is where it belongs rather than the release arm, because the mark
is one more thing that is pending: it is drawn only when nothing else is
(snapshot), and it means the switch has not been typed on yet. Clearing
it there is safe for the switch itself only because transition() zeroes it
at the top and setlang() runs after flush() -- reset() is reached through
that flush, before the new mark is set. That ordering is not obvious, and
it is exactly what engine/direct-language-modes already asserts at
engine_test.c:584; breaking setlang so a switch marks nothing fails that
line and the new ones together.
Not a Wayland defect: wl.c's leave() calls hidepopup() unconditionally
after the release, so its popup was already coming down. X11 was the
frontend that trusted the picture.
The new case goes in engine/direct-language-modes beside the mark
assertions that were already there, and drives imhandlekey rather than
transition, since the defect is on the owner-release path. Without the
fix:
engine_test.c:619: check failed: draindraw(&dc) > 0
that is, the engine published no picture at all. Its second line needs
its own reason to exist, so it was checked by removing the fix and making
samedraw() answer 0 for everything: the picture then arrives and still
carries the mark,
engine_test.c:620: want 0, got 1 (dc.pre.n)
which is the assertion that would otherwise have been decoration. The two
setup lines are load-bearing too -- with setlang marking nothing they fail
rather than passing vacuously, since an empty picture equal to the last
one is never sent.
90 unit, check-live, check-stress and valgrind clean; the count is
unchanged because the case joined a test that already existed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kaku offers the twenty-two readings of かく before 書く, because the SKK
dictionary keys a verb by its stem and gives no frequency to rank the two
lists by. SKK's own answer is the shift key: kaKu says where the
okurigana starts, and that split now comes first — the reading's own
candidates still follow it. Caps Lock sends no Shift, so it marks
nothing, and a word typed without Shift is unchanged.
Ctrl+S, Ctrl+N and the rest changed the mode with nothing to see; the
next key was the only way to tell. The popup now shows A, 한, あ, ア, or
ă until a key is typed, beside the ☺ and 漢 a search already shows.
Every complete reading shows its candidates, so 1-9 were candidate picks
in the middle of composing: ka then 5 committed 家, and heya then 2
typed a 2 the application never asked for. They pick only once Space,
Tab, or an arrow has chosen a candidate, as in Mozc; before that they are
ordinary keys. A search is unchanged: its digits pick from the start.
The unit suite built a Lang of its own for Telex and loaded telex.map
into it, so the tests that drive the real table had to swap the map in
and out around themselves. testmapinit loads it where it belongs.
Two applications' focus events cross — ibus-daemon documents the case —
so the first key of the new one can arrive before the old one says it
lost focus. The engine dropped whatever that context was composing; it
now keeps it and hands it back with the reset or release that follows,
which every frontend already commits. Normal ordering is unchanged.
Cclientpreedit was a one-bit mask that every producer set as "want ?
Cclientpreedit : 0" and every consumer masked out again; nothing else
was ever going to join it. Keyreq carries clientpre, an int that says
whether the client draws the preedit, and the engine and the XIM
context keep it under that name.
Shift, Control, and their kin were mapped to special keys that the
engine then had a range and a check to ignore, and the GTK module made a
round trip to the daemon for each press. ipckeysym maps them to key 0,
which was already the "no key" every frontend and the engine skip;
Kmodfirst, Kmodlast, and ismodkey go.
Backspace undid a keystroke: な became ん, かんじ became かんj, きゃ
became ky. Like every Japanese IME it now deletes the last kana as
shown — な goes, かんじ becomes かん, きゃ becomes き — while a romaji
letter that never became kana still goes one at a time. Tab stepped
through candidates in a search and committed in Japanese; it steps
through them there too, with Space, and Shift+Tab steps back.
Ctrl+E and Ctrl+H gave no sign until a key produced candidates; with
nothing pending the popup even went away. An empty search now shows
☺ or 漢 in the popup, so the user knows the next keys are a query.
The emoji search folded the keys for its lookup and then showed and
committed the folded copy: SMILE became smile in the text. And it
showed the transliteration only while that alone matched, so a Korean
query flipped between 웃 and key soup as it grew. Now the query shown
is the keys while they match anything, else what they type in the
current language, both as typed; only the lookups fold. Space in a
search picks the highlighted result as Enter does, instead of adding a
space no alias needs.
Space and non-ASCII keys with a Korean or Telex syllable pending were
committed as text along with the syllable, while '.', ',' and digits
committed the syllable and passed on: one path for both now, through the
language's own trans, which commits and passes on. Japanese Space is
its own key already.
dictqjp() knows which languages have candidates; its two callers no
longer choose between it and clearkouho(). Every request carries its
owner's caret as the frontend knows it, so a Keypress copies it whether
valid or not, and the XIM frontend need not send a Keycaret to say that
its spot went away.
半角/全角, ひらがな/カタカナ, 変換, and 無変換 committed the reading and did
nothing, like any function key. They now stand for what a Japanese
keyboard means by them: toggling Japanese and English, switching kana
modes, converting (or turning Japanese on), and turning it off; the
Korean keys became a switch of the same shape. The keypad's arrows
and page keys, with NumLock off, fold onto the plain ones as its Enter
and Tab already did.
Ctrl+Shift+V switched to Vietnamese and ate the key, in every terminal
where it pastes; Ctrl+Shift+T and N opened no tab. Only a plain
Ctrl+letter is a strans command now; a chord with Shift, Alt, or Super
commits what is pending and passes, as Alt and Super chords already did.
Caps Lock still works: the keysym's case never mattered.
Four places lower-cased ASCII by hand; commitim repeated impre's three
lines to compute the text it commits; and transition() began by
rejecting modifier keys that imhandlekey's keymeaningful() had already
turned away, kept alive only by tests calling transition() directly.
The test helper now goes through the same gate.
Ctrl+H took the pending syllable as the query, but the keys typed after
it started a composition of their own: gk, Ctrl+H, s showed 하ㄴ, and r,
Ctrl+H, k showed ㄱㅏ. transstr now composes from a pending text, so
the search goes on where the syllable left off. Escape ended the search
and dropped the syllable it had taken; it puts it back as pending text.
Every Japanese IME converts a reading to Katakana on demand; strans made
the user switch to the Katakana mode and type again. Space now adds
the reading in Katakana after the dictionary candidates, so a loanword
the dictionary lacks converts with one Space and Enter, and 漢字 users
still get 漢字 first.
0 committed the reading only while candidates showed; without them it
typed a 0 as well. It now commits any Japanese reading, the counterpart
of 1-9 picking candidates, as the README always said.
A sixth of kanji.dict is SKK's okuri-ari entries, keyed by a stem and
the letter of the okurigana that follows it — かk: 書 描 掛 —, and no
reading ever ends in a letter, so かく offered 確 and 各 but never 書く,
and no verb or adjective could be converted at all. Now every split of a
complete reading is tried, longest stem first, with the okurigana put
back: かく adds 書く and 描く after the exact candidates, よむ gives
読む, あかい 赤い, おおきい 大きい, いった 言った, かいた 書いた. A stem
that ends in っ writes it in kana too, so いt and いっt agree.
A key that could not go on with the pending romaji, when that was not a
syllable either, committed the whole reading to the application and
passed the key on: one slip dumped かんj into the text with no way back.
The letters typed now stay in the reading as they are, like any other
IME shows them, and Backspace mends them; only a key that starts no
syllable at all still ends the composition.
mkemoji wrote a row for every prefix of every alias, so that a query
matched as it was typed; the trie is a prefix index already, and with a
real emoji list those rows would be four times the aliases themselves.
Now emoji.dict has one row per alias, trienode() names a key's node, and
dictlookup walks the entries at and below it, the key's own first, up to
Maxkouho. Trie children are appended rather than pushed, so the walk
keeps the file's order and a bare digit still picks the superscript or
subscript it always did.
The hand-written symbol rows move to symbol.src; emoji.src is left to
the emoji. mkemoji reads both by default, or the files it is given.
hira.map and kata.map knew 152 keys of the 320 every Japanese IME
accepts: no nn, no small kana by x or l (xtu, xya, la, ...), no sya, tya,
zya, jya, dya, cya, tsa, thi, dhi, twu, kye, ye, fya, wha, and zi gave ぢ.
Both are now Mozc's default table in full, kata.map in Katakana, so that
they agree (kata.map alone had di → ディ and a lone v). With Mozc, wi
and we are うぃ and うぇ (wyi and wye the old kana), the v row is ゔ, nn
is ん, and [ ] ~ and the z-prefixed symbols type 「 」 〜 ・ … ← ↓ ↑ →.
n' is a row now, not a case in transjp. A row whose value is っ keeps
its consonant pending only when it is a doubled consonant or tch: xtu
and ltu are っ itself, so a lone っ can finally be typed.
A Korean keyboard sends Hangul and Hangul_Hanja for its two extra keys;
they did nothing but end the composition. Now 한/영 toggles Korean and
English and 한자 opens the Hanja search, by standing in for Ctrl+S,
Ctrl+T, and Ctrl+H, as fcitx5-hangul binds them.
A pending Hangul syllable is real text, and Escape is what vi users
press to leave insert mode; eating it and dropping the syllable served
nobody. fcitx5-hangul commits and passes any key it does not consume,
Escape included; do the same for Korean and Telex. A Japanese reading
is still cancelled: there Escape is the IME's own key.
A complete Japanese reading showed its candidates with the first row
highlighted, yet Enter committed the reading unless the user had moved
to a candidate: the highlight lied. Now sel is -1 while nothing is
chosen, and it alone says what Enter commits; candidatechosen goes.
Every key that ends a composition — Enter, Tab, a language switch, a
special key, a modifier chord, typing on — now commits the chosen
candidate through one flush; before, only Enter and Tab did, and Ctrl+S
after choosing 漢字 committed かんじ.
Space in a Japanese mode is the conversion key, as in every other
Japanese IME: it steps through the candidates (Shift+Space backwards,
both wrapping) and commits a reading that has none, without typing a
space. Backspace and Escape on a chosen candidate go back to the
reading first. Tab in a search wraps through the same cyclekouho.
reset() no longer forgets the caret: a chosen candidate confirmed by
typing on would otherwise redraw the next reading at the pointer.
Shift or Caps Lock made a Japanese key pass through as a Latin capital,
so nothing composed with Caps Lock on; Korean already folds case. Latin
text still has Ctrl+T.
Ctrl+H committed the syllable being composed and opened an empty search,
so converting 한 meant typing it twice; the pending syllable now seeds the
query. Backspace on an already empty search ended it and then reached the
application, deleting a character; it now only ends the search.
Enter with a pending syllable committed it and stopped there, so a chat
message needed Enter twice; Korean input methods commit and pass the key
on. Japanese keeps consuming the key that confirms a reading or a chosen
candidate, as Japanese input methods do.
Clicking elsewhere, changing focus, or any client reset dropped the
composition in the GTK module and IBus (only XIM ResetIC handed it
back), so typing 안녕 and clicking Send lost 녕. The engine now returns
the pending text — a moved-to candidate first, as Enter would — on
Keyreset and Keyrelease; the GTK module asks for it before closing on
focus-out and commits it, IBus commits it on FocusOut, Reset and a
switch to a password field, and XIM commits it on focus loss and hands
it back on ResetIC without a separate capability probe.
Hiragana spelled out 42 doubled syllables (kka, ssha, ttsu, ...) and so
knew none it did not list: matcha, baggu and beddo came out as ma-tc-ha,
ba-gg-u. Katakana already used the engine's small-tsu rule for a few
doubles. Both maps now list every doubled consonant but n, plus tch, and
hiragana gains di, che and fa/fi/fe/fo like katakana.
Ctrl+E during an emoji search, Ctrl+H during a hanja search, or either
during the other search dropped the typed query, while every other
Ctrl key committed it as the README says. Escape is the way to cancel;
the search keys now commit the shown text like a language switch.
Up, Down, PageUp and PageDown with a composition pending and nothing to
move through returned the key uneaten but left the text pending, so the
client moved its cursor and reset the context: the pending Korean
syllable vanished, where Left or Home would have committed it. Let them
fall through to the special-key path that commits and passes the key.
At the limit the key that did not fit was handed back to the client as
plain ASCII, so a long Japanese reading ended in a Latin letter, and a
pending romaji consonant was flushed as ASCII with it (…あka for か).
Commit the completed kana, keep the pending syllable, and transliterate
the key as usual; other languages commit and go on the same way.
server_test, ibus_test and xim_adapter_test each ran their own copy of
the same alt loop that stands in for imthread, and two of them had
private checkstr/checkenginepreedit variants. test_util.c now provides
Pump (trace, optional hold on a chosen op, stop) in its own proc so a
test may block in socket I/O while the engine runs, and test.h declares
the engine hooks once; every .c includes dat.h and fn.h itself as the
rest of the tree does. server_test's three copies of fixture setup and
teardown became serverbegin/serverend.
ko_test and vi_test replayed keys with private copies of transstr's
loop, and test_util reimplemented the shown preedit; the engine exports
impre(Im*, Str*) and transstr() instead, so a change to composition
rules is one edit.
mapget and maplookup were one trie probe with two predicates; mapmatch
returns the match kind and mapget is its exact case. movedelta is a
switch. searchkey's modifier test was unreachable behind transition's,
searchlang's Alt/Super test behind both callers' earlier returns, and
picksearch's range check behind numberedkouho. Korean key folding runs
once at the top of transition. commit() on an empty preedit is a no-op,
so the Ctrl branch and startsearch lose their haspre guards and repeated
commit/reset pairs. Ctrl+language inside a search now commits the shown
query as it does elsewhere instead of dropping it. Language ids are
documented as the Ctrl codes they are; Im parameters no longer shadow
the file's Im.
Every key copied the whole Im and Search rune by rune, compared them
after the transition, and combined the answer with 'eaten', a commit
count that is always zero, and a force flag for caret and owner changes,
while redraw() kept a separate 'visible' bit to know when to send an
empty popup. redraw() now compares the snapshot with the last one sent
and treats two empty popups as equal, so imhandlekey just ends in
redraw(). keystroke() was only a test entry point and moves there.
The dictionary thread ran in imthread's own proc, so a lookup could
only start once the engine blocked, and it was a trie probe anyway; the
emoji and Hanja searches already called dictlookup directly. The
request/result channels, sequence numbers, staleness checks and the
second draw per Japanese key are gone; dictqjp fills the candidates
in place. Emit.dict and Lang.dictq only ever triggered lookups for
Vietnamese, which has no dictionary. dictlookup(Lang*, key, out, max)
returns the count. The Hanja lookup no longer pre-checks for a single
syllable; a reading either has an entry or it does not.
For Vietnamese the preedit is the map key ('as', 'oong'); only the popup
and the commit path looked it up. Clients with inline preedit therefore
showed 'as' while Enter committed 'á'. impre() now maps once for every
reader, and snapshot() no longer maps a second time.
The hash map served a single exact-match lookup that the trie already
answers, at the price of a second container, a second file loader with
its own drift, and a Str-to-UTF-8 conversion on every chain probe. The
files are small (kanji.dict is 7.5k lines), so the trie holds both.
trieopen validates keys and each space-separated value word against Str
and reports path:line; trielookup takes the Str every caller holds and
treats a nil trie as an unloaded map; trienew/trieput exist for tests.
The overflow guards on growth, Trie.root (always 0) and the per-language
init loop written twice are gone.