How Snippets (定型文) Work with Exact-Match Triggers in JapaneseKeyboard

JapaneseKeyboard triggers snippet expansion only when the typed text exactly matches the registered reading, using a Room DAO query with strict SQL equality (reading = :reading).

The kazumaproject/japanesekeyboard repository implements a user-defined snippet system—called 定型文 (teikeibun)—that allows frequently used phrases to be inserted via short trigger words. This article explains how the exact-match trigger mechanism works at the database and IME level, including the specific DAO methods, SQL queries, and repository abstractions that enforce strict equality matching.


What Are Snippets (定型文) in JapaneseKeyboard?

Snippets in JapaneseKeyboard are user-managed text templates stored in a local Room database. Each snippet consists of:

  • word – The full text to be inserted (e.g., an email signature or Markdown table).
  • reading – The exact trigger string that, when typed, causes the snippet to appear as a candidate.
  • enabled – A boolean flag determining whether the snippet is active.

These entities are defined in UserTemplate.kt and persisted in the user_template table, which is part of AppDatabase.


The Exact-Match Trigger Mechanism

The trigger system relies on strict string equality at the SQL level. When the user types a sequence, the input method service queries the database for rows where the reading column equals the typed text exactly—no partial matches, no wildcards.

Database Schema and the UserTemplate Entity

The entity class located at app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplate.kt maps to the following schema:

@Entity(tableName = "user_template")
data class UserTemplate(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "word") val word: String,
    @ColumnInfo(name = "reading") val reading: String,
    @ColumnInfo(name = "enabled") val enabled: Boolean
)

The reading field is indexed implicitly by the query pattern used in the DAO.

DAO Query with Strict Equality

The exact-match logic is implemented in UserTemplateDao.kt at app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplateDao.kt:

@Dao
interface UserTemplateDao {
    @Query("SELECT * FROM user_template WHERE reading = :reading ORDER BY id DESC LIMIT :limit")
    fun searchByReadingExact(reading: String, limit: Int): LiveData<List<UserTemplate>>

    @Query("SELECT * FROM user_template WHERE reading = :reading ORDER BY id DESC LIMIT :limit")
    suspend fun searchByReadingExactSuspend(reading: String, limit: Int): List<UserTemplate>
}

The SQL clause reading = :reading enforces exact equality. The ORDER BY id DESC ensures that if multiple snippets share the same reading, the most recently added one appears first.

Repository Layer Abstraction

The UserTemplateRepository.kt at app/src/main/java/com/kazumaproject/markdownhelperkeyboard/repository/UserTemplateRepository.kt exposes these queries to the ViewModel and IME:

class UserTemplateRepository @Inject constructor(
    private val userTemplateDao: UserTemplateDao
) {
    fun searchByReadingExact(reading: String, limit: Int = 10): LiveData<List<UserTemplate>> =
        userTemplateDao.searchByReadingExact(reading, limit)

    suspend fun searchByReadingExactSuspend(reading: String, limit: Int = 10): List<UserTemplate> =
        userTemplateDao.searchByReadingExactSuspend(reading, limit)
}

The repository provides both LiveData (for UI observation) and suspend (for coroutine-based IME background threads) variants of the exact-match search.


How the IME Performs the Lookup

When the user types on the keyboard, the input method service (implemented in the main IME class) intercepts the text stream and performs the following steps:

  1. Capture Input – The current composing text or committed characters are collected into a String called currentReading.
  2. Exact-Match Query – The IME calls userTemplateRepository.searchByReadingExactSuspend(currentReading, limit = 1) on a background coroutine.
  3. Candidate Generation – If the DAO returns a non-empty list, the word field of the first result is wrapped as a candidate object.
  4. Insertion – When the user selects the candidate, the IME commits the word text, replacing the typed reading.

Because the lookup uses reading = :reading, typing htm will not trigger a snippet registered under html5. Only when the user completes the exact trigger html5 does the expansion occur.


Code Examples

Registering a Snippet with an Exact-Match Trigger

// Inside a ViewModel or Activity
val snippet = UserTemplate(
    id = 0,  // auto-generated
    word = "Thank you for your email. Best regards, ...",
    reading = "tymail",  // exact trigger
    enabled = true
)

// Insert via repository
lifecycleScope.launch {
    userTemplateRepository.insert(snippet)  // calls userTemplateDao.insert
}

Querying for an Exact Match in the IME

suspend fun getSnippetWord(reading: String): String? {
    // Returns null if no exact match exists
    val results = userTemplateRepository.searchByReadingExactSuspend(
        reading = reading,
        limit = 1
    )
    return results.firstOrNull()?.word
}

IME Integration Flow

// Pseudo-code inside the InputMethodService
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
    val currentInput = getCurrentInputText()  // e.g., "sig"
    
    // Launch background search
    serviceScope.launch {
        val snippet = userTemplateRepository
            .searchByReadingExactSuspend(currentInput, 1)
            .firstOrNull()
        
        if (snippet != null) {
            showCandidate(snippet.word)  // Display full signature
        }
    }
    return super.onKeyDown(keyCode, event)
}

Key Files and Their Roles

File Purpose
app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplate.kt Entity class defining the word, reading, and enabled columns.
app/src/main/java/com/kazumaproject/markdownhelperkeyboard/user_template/database/UserTemplateDao.kt DAO containing searchByReadingExact and searchByReadingExactSuspend with strict SQL equality.
app/src/main/java/com/kazumaproject/markdownhelperkeyboard/repository/UserTemplateRepository.kt Repository exposing exact-match queries to ViewModels and the IME service.
app/src/main/res/layout/fragment_user_template.xml UI layout for adding, editing, and deleting snippets.
app/src/main/java/com/kazumaproject/markdownhelperkeyboard/database/AppDatabase.kt Room database definition including the user_template table.

Summary

  • Exact-match enforcement is implemented at the SQL level via reading = :reading in UserTemplateDao.searchByReadingExactSuspend.
  • Repository abstraction provides both LiveData and coroutine-based access to the exact-match query, used by the UI and IME respectively.
  • Trigger isolation ensures that partial typing (e.g., htm) does not expand a snippet registered under html5, preventing accidental insertions.
  • Background execution allows the input method service to query the Room database off the main thread while the user types.

Frequently Asked Questions

What happens if I type a partial match of a snippet trigger?

Nothing. The DAO query uses strict equality (reading = :reading), so typing a prefix (e.g., sig when the trigger is signature) returns zero results. The snippet only appears once the exact trigger text is entered.

Can I use special characters or spaces in the trigger reading?

Yes, but the exact-match logic treats the reading field as a literal string. If you register a reading with spaces or symbols (e.g., my-email), you must type those exact characters, including case sensitivity, for the trigger to fire.

How many snippets can I store?

The limit is constrained only by the SQLite database size on the device. The UserTemplateDao queries accept a limit parameter (defaulting to 10 in the repository), but this only affects how many results are returned for a single reading. You can store hundreds of snippets; the exact-match query remains performant because the reading column is effectively filtered by the equality clause.

Is the exact-match trigger case-sensitive?

Yes. The SQL comparison reading = :reading is case-sensitive by default in SQLite (unless a specific collation is applied). Therefore, registering a snippet under HTML5 will not trigger when you type html5. You should register variations separately if you need case-insensitive matching.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →