Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ public final class InputLogic {
private int mSpaceState;
// Never null
private SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance();
// #14 spacing-policy signals — recomputed every keystroke from the suggestion results at zero
// extra native cost (see computeSpacingSignals / setSuggestedWords). Consumed by the upcoming
// signal-driven grace + two-gate Assisted-tier logic.
private boolean mSpacingComplete; // typed word is a real dictionary word
private float mSpacingPrefixRichScore; // fraction of candidates that are completions [0..1]
private final Suggest mSuggest;
private final DictionaryFacilitator mDictionaryFacilitator;
private SingleDictionaryFacilitator mEmojiDictionaryFacilitator;
Expand Down Expand Up @@ -1399,6 +1404,9 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) {
mWordComposer.setAutoCorrection(suggestedWordInfo);
}
mSuggestedWords = suggestedWords;
final SpacingSignals spacingSignals = computeSpacingSignals(suggestedWords);
mSpacingComplete = spacingSignals.complete;
mSpacingPrefixRichScore = spacingSignals.prefixRichScore;
final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect;

// Put a blue underline to a word in TextView which will be auto-corrected.
Expand All @@ -1416,6 +1424,43 @@ public void setSuggestedWords(final SuggestedWords suggestedWords) {
}
}

/**
* #14 spacing-policy signals derived from the current suggestion results, computed every
* keystroke at zero extra native cost.
* <ul>
* <li>{@code complete} — the typed word is a real dictionary word (valid AND not just
* user-typed). A confident "this is a finished word".</li>
* <li>{@code prefixRichScore} — fraction of candidates that are completions (longer words
* sharing this stem), in [0..1]. High = lots left to extend to (keep the word open);
* low = little left (safe to auto-commit).</li>
* </ul>
* Static + pure so it can be unit-tested without a live InputLogic.
*/
static final class SpacingSignals {
final boolean complete;
final float prefixRichScore;
SpacingSignals(final boolean complete, final float prefixRichScore) {
this.complete = complete;
this.prefixRichScore = prefixRichScore;
}
}

static SpacingSignals computeSpacingSignals(final SuggestedWords suggestedWords) {
final int n = suggestedWords.size();
if (n == 0) return new SpacingSignals(false, 0f);
final SuggestedWordInfo typed = suggestedWords.mTypedWordInfo;
final boolean complete = suggestedWords.mTypedWordValid
&& typed != null && typed.mSourceDict != null
&& !Dictionary.TYPE_USER_TYPED.equals(typed.mSourceDict.mDictType);
int completions = 0;
for (int i = 0; i < n; i++) {
if (suggestedWords.getInfo(i).getKind() == SuggestedWordInfo.KIND_COMPLETION) {
completions++;
}
}
return new SpacingSignals(complete, (float) completions / n);
}

/**
* Handle a consumed event.
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-3.0-only
package helium314.keyboard.latin.inputlogic

import helium314.keyboard.latin.SuggestedWords
import helium314.keyboard.latin.SuggestedWords.SuggestedWordInfo
import helium314.keyboard.latin.dictionary.Dictionary
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* Unit tests for [InputLogic.computeSpacingSignals] (#14 spacing policy): the free per-keystroke
* `complete` + `prefixRichScore` signals derived from the suggestion results. Pure logic.
*/
class SpacingSignalsTest {

// mDictType != "user_typed" -> counts as a "real" dictionary source for `complete`.
private val realDict: Dictionary = Dictionary.DICTIONARY_APPLICATION_DEFINED
private val userTyped: Dictionary = Dictionary.DICTIONARY_USER_TYPED

private fun info(word: String, kind: Int, dict: Dictionary): SuggestedWordInfo =
SuggestedWordInfo(word, "", 0, kind, dict,
SuggestedWordInfo.NOT_AN_INDEX, SuggestedWordInfo.NOT_A_CONFIDENCE)

private fun words(typed: SuggestedWordInfo?, typedValid: Boolean,
list: List<SuggestedWordInfo>): SuggestedWords =
SuggestedWords(ArrayList(list), null, typed, typedValid, false, false,
SuggestedWords.INPUT_STYLE_TYPING, SuggestedWords.NOT_A_SEQUENCE_NUMBER)

@Test fun `empty suggestions yield no signals`() {
val s = InputLogic.computeSpacingSignals(SuggestedWords.getEmptyInstance())
assertFalse(s.complete)
assertEquals(0f, s.prefixRichScore, 0f)
}

@Test fun `valid typed word from a real dictionary is complete`() {
val typed = info("the", SuggestedWordInfo.KIND_TYPED, realDict)
assertTrue(InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).complete)
}

@Test fun `valid typed word from the user-typed source is NOT complete`() {
val typed = info("xyzzy", SuggestedWordInfo.KIND_TYPED, userTyped)
assertFalse(InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).complete)
}

@Test fun `invalid typed word is not complete`() {
val typed = info("teh", SuggestedWordInfo.KIND_TYPED, realDict)
assertFalse(InputLogic.computeSpacingSignals(words(typed, false, listOf(typed))).complete)
}

@Test fun `prefix-rich score is the fraction of completions`() {
val typed = info("ba", SuggestedWordInfo.KIND_TYPED, realDict)
val list = listOf(
typed,
info("bad", SuggestedWordInfo.KIND_COMPLETION, realDict),
info("bat", SuggestedWordInfo.KIND_COMPLETION, realDict),
info("ball", SuggestedWordInfo.KIND_COMPLETION, realDict),
)
// 3 completions out of 4 candidates.
assertEquals(0.75f, InputLogic.computeSpacingSignals(words(typed, false, list)).prefixRichScore, 1e-6f)
}

@Test fun `no completions yields zero prefix-rich score`() {
val typed = info("the", SuggestedWordInfo.KIND_TYPED, realDict)
assertEquals(0f,
InputLogic.computeSpacingSignals(words(typed, true, listOf(typed))).prefixRichScore, 0f)
}
}
Loading