InputConnection.setComposingText(text, 1) shows underlined in-progress text in many apps. commitText finalizes. Mid-word suggestions need a buffer the IME owns so a tap can replace the whole partial word. Without composing you only insert characters and scrape getTextBeforeCursor forever.
File: ComposingBuffer.kt
class ComposingBuffer {
val text: String
fun append(char: String): String
fun backspace(): Boolean // false if empty
fun flush(): String // return copy and clear
fun setText(s: String) // clear then append (recompose)
fun clear()
}
No Android types. Unit-testable alone (ComposingBufferTest.kt).
In handleKey else-branch for character keys (simplified):
text = kbState.resolveLabel(key)onCharCommitted, returntext == ";": append, setComposingText, snippet suggestionscontext = prevCommittedWordkbState.onCharCommitted() and refresh key draw statePunctuation set is a fixed char set in CodeKeyboardIME (period, comma, operators, quotes, etc.).
"space" -> {
flushComposing(ic)
ic?.commitText(" ", 1)
val next = bigramModel.nextWords(prevCommittedWord, n = 5)
if (next.isNotEmpty()) suggestionBar.update("", next)
}
val word = composing.flush()
if (word.isNotEmpty()) {
ic?.commitText(word, 1)
wordLearner.learnFromFlush(word)
kbState.onCharCommitted()
// metrics...
if (prevCommittedWord.isNotEmpty())
bigramModel.recordTransition(prevCommittedWord, word)
prevCommittedWord = word
}
keystrokesSinceCommit = 0
suggestionBar.clear() // space path may refill immediately after
Note: the space handler calls flush (which clears the bar) then may show bigrams.
Problem: user taps elsewhere; composing region is stale; next char would replace the wrong span.
Also: IME calls that change selection must not look like user taps.
val imedriven = SystemClock.uptimeMillis() < expectSelectionUpdateBy
if (composing.text.isEmpty()) return
if (imedriven) return
// if cursor outside candidatesStart..candidatesEnd (or candidates unset):
ic.finishComposingText()
composing.clear()
suggestionBar.clear()
When composing is empty and backspace deletes one committed character:
deleteSurroundingText(1, 0) (or DEL key event fallback)recomposeWordAtCursor(ic)val before = ic.getTextBeforeCursor(RECOMPOSE_SCAN_CHARS, 0) // 50
val fragment = before.takeLastWhile { it.isLetterOrDigit() || it == '\'' }
// get absolute cursor via ExtractedText
expectSelectionUpdateBy = now + 500L
ic.beginBatchEdit()
ic.finishComposingText()
ic.setComposingRegion(absCursor - fragment.length, absCursor)
composing.setText(fragment)
ic.endBatchEdit()
suggestionBar.update(fragment, suggestionStrategy.suggest(fragment, 5, prevCommittedWord))
Tests: BackspaceRecomposeTest.kt. Debug logs used tag CKB_COMPOSE during development.
commitText("") to delete selectioncomposing.backspace(): update composing text + suggestionsflushComposing(ic)
// if IME_FLAG_NO_ENTER_ACTION or action NONE/UNSPECIFIED -> KEYCODE_ENTER
// else performEditorAction(action), fallback KEYCODE_ENTER
Search/done fields need performEditorAction.
Start: set supportsComposing, clear composing and prevCommittedWord, finishComposingText, clear bar.
Finish: finish composing, clear buffer/bar, scheduleUserTrieFlush() saves user.trie.
| Claim | Evidence |
|---|---|
| RECOMPOSE_SCAN_CHARS = 50 | CodeKeyboardIME companion |
| expectSelectionUpdateBy + 500ms | recomposeWordAtCursor |
| flush records bigram and WordLearner | flushComposing |
| space shows nextWords with empty word | handleKey “space” |
| ComposingBuffer API | ComposingBuffer.kt |
| Enter uses imeOptions mask | handleKey “enter” |