Input Routing Strategies for Text Snippet Expansion in vorssaint-utils: Immediate vs After-Delimiter

vorssaint-utils employs two distinct input routing strategies—Immediate and After-Delimiter—defined by the TextSnippet.Expansion enum to control whether snippets expand instantly upon trigger completion or wait for a delimiter character.

vorssaint-utils is an open-source Swift utility library that provides advanced text snippet expansion capabilities. The engine routes keystrokes through configurable input routing strategies that determine exactly when a typed trigger transforms into replacement text. These strategies are implemented in Sources/Vorssaint/Services/Snippets/TextSnippetSupport.swift and exposed through the TextSnippet.Expansion enum.

Overview of the Two Input Routing Strategies

The expansion behavior is governed by the TextSnippet.Expansion enum, which offers two mutually exclusive routing paths:

  • Immediate – Fires as soon as the last character of the trigger is typed, enabling instant replacements without additional keystrokes.
  • After-Delimiter – Waits until the user types a delimiter character (space, Tab, Return, or newline) before expanding the snippet while preserving the delimiter in the output.

Both strategies use a rolling buffer mechanism limited to 64 characters (bufferLimit = 64) and prioritize the longest matching trigger when multiple snippets share similar prefixes.

Strategy 1: Immediate Expansion

When a snippet is configured with .immediate expansion, the engine monitors every keystroke through TextSnippetSupport.bufferAppending. As soon as the buffer ends with a complete trigger sequence, TextSnippetSupport.match(buffer:expansion:snippets:) injects the replacement text instantly.

This strategy is ideal for single-character shortcuts or rapid-fire replacements where you want the text to appear the moment you finish typing the trigger.

let instantSnippet = TextSnippet(
    name: "Instant Email",
    trigger: ";e",
    replacement: "example@domain.com",
    expansion: .immediate,
    enabled: true
)

In this example, typing ;e immediately replaces it with example@domain.com without requiring a space or return key.

Strategy 2: After-Delimiter Expansion

Snippets using .afterDelimiter expansion rely on the TextSnippetSupport.delimiters set to detect boundary characters. When a delimiter is typed, the engine checks if the preceding buffer content matches any enabled snippet with the .afterDelimiter strategy. Upon match, the replacement text is inserted after the delimiter, ensuring natural sentence flow.

This approach prevents accidental mid-word expansions and feels more natural for signature blocks or form templates that appear at the end of sentences.

let delayedSnippet = TextSnippet(
    name: "Signature",
    trigger: ";sig",
    replacement: "Best regards,\nYour Name",
    expansion: .afterDelimiter,
    enabled: true
)

Here, typing ;sig followed by a space expands to Best regards,\nYour Name while keeping the space you typed.

Buffer Routing and Matching Logic

The routing engine in TextSnippetSupport.swift handles the complexity of buffer management and trigger sanitization:

  1. Buffer Management – Every keystroke updates a rolling buffer via TextSnippetSupport.bufferAppending, capped at 64 characters to prevent excessive memory usage.
  2. Trigger Sanitization – Triggers are cleaned of whitespace and truncated to maxTriggerLength = 40 characters using TextSnippetSupport.sanitizedTrigger.
  3. Longest Match Wins – The TextSnippetSupport.match function scans enabled snippets and selects the longest trigger that matches the current buffer end.
  4. Delimiter Detection – When a character exists in TextSnippetSupport.delimiters, the engine prioritizes checking .afterDelimiter matches before falling back to .immediate evaluation.
// Simplified routing logic from TextSnippetSupport.swift
let buffer = TextSnippetSupport.bufferAppending(currentBuffer, typed: key)

if TextSnippetSupport.delimiters.contains(key) {
    if let snippet = TextSnippetSupport.match(
        buffer: buffer,
        expansion: .afterDelimiter,
        snippets: allSnippets
    ) {
        // Insert replacement after the delimiter
    }
}

if let snippet = TextSnippetSupport.match(
    buffer: buffer,
    expansion: .immediate,
    snippets: allSnippets
) {
    // Insert replacement immediately
}

User Interface and Configuration

Users configure these input routing strategies through two primary interface files:

Summary

  • vorssaint-utils implements two input routing strategies—Immediate and After-Delimiter—via the TextSnippet.Expansion enum.
  • Immediate expansion fires instantly when the trigger is completed, managed by TextSnippetSupport.match with .immediate parameter.
  • After-Delimiter expansion waits for space, Tab, Return, or newline from the TextSnippetSupport.delimiters set, preserving the delimiter after expansion.
  • The engine maintains a 64-character rolling buffer and prioritizes the longest matching trigger to resolve conflicts.
  • Configuration occurs in TextSnippetsSettings.swift while the core logic resides in TextSnippetSupport.swift.

Frequently Asked Questions

What is the maximum trigger length supported in vorssaint-utils?

The engine enforces a maxTriggerLength = 40 characters limit through TextSnippetSupport.sanitizedTrigger, automatically truncating longer triggers to prevent buffer overflow and ensure performance.

How does vorssaint-utils resolve conflicts when multiple triggers match?

The matching algorithm in TextSnippetSupport.match implements a "longest trigger wins" policy. If you have both ;email and ;email2, typing ;email2 will expand the latter because the engine selects the longest matching sequence from the enabled snippets list.

Can I customize which characters act as delimiters for After-Delimiter expansion?

The delimiter set is defined as a constant in TextSnippetSupport.delimiters and includes space, Tab, Return, and newline characters. To modify these boundaries, you would need to edit the source in Sources/Vorssaint/Services/Snippets/TextSnippetSupport.swift and rebuild the project.

Does After-Delimiter mode consume the delimiter character?

No, the delimiter is preserved. When the engine detects a delimiter match, it injects the replacement text after the delimiter character, allowing you to continue typing naturally without losing the whitespace or line break you intended to insert.

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 →