Liking cljdoc? Tell your friends :D

Draft Buffer Implementation

Character-level text buffer for incremental typing with efficient insertion and deletion (backspace) operations.


← Back to Layer 7


Table of Contents

  1. Overview
  2. Data Structure
  3. Core Operations
  4. Unicode Handling
  5. Usage Examples
  6. Performance Characteristics
  7. Implementation Details
  8. Testing

Overview

DraftBuffer is a character-level text buffer optimized for incremental typing scenarios. It supports:

  • $\mathcal{O}(1)$ character insertion (append)
  • $\mathcal{O}(1)$ character deletion (backspace)
  • Unicode correctness (operates on char, not bytes)
  • Minimal allocations (amortized constant time growth)

Why Character-Level?

Problem: String operations in Rust are byte-oriented, but users think in characters.

let text = "café";
assert_eq!(text.len(), 5);        // 5 bytes (é is 2 bytes)
assert_eq!(text.chars().count(), 4); // 4 characters

// Removing last byte corrupts UTF-8:
let bad = &text[..4]; // "caf" + invalid byte ❌

// Character-level is correct:
let chars: Vec<char> = text.chars().collect();
let correct = &chars[..3]; // ['c', 'a', 'f'] ✓

Solution: DraftBuffer uses VecDeque<char> for correct character semantics.

Use Cases

Use CaseBenefit
Code EditorCharacter-by-character identifier entry
AutocompleteBuild query string incrementally
REPLTrack partial expressions as user types
Undo/RedoCheckpoint buffer state at word boundaries

Data Structure

Internal Representation

pub struct DraftBuffer {
    /// Character storage (VecDeque for efficient push/pop on both ends)
    chars: VecDeque<char>,
}

Why VecDeque<char> instead of String?

ConsiderationStringVecDeque<char> (chosen)
Character-level deleteRequires byte boundary checks✓ $\mathcal{O}(1)$ pop_back()
Unicode correctnessEasy to break (byte-level)✓ Guaranteed correct
Memory efficiency✓ Compact (UTF-8)✗ 4 bytes per char
Insertion/deletionReallocation for shrink✓ $\mathcal{O}(1)$ amortized
String conversion✓ Zero-copy✗ $\mathcal{O}(n)$ collect

Trade-off Decision: Correctness and $\mathcal{O}(1)$ operations outweigh memory overhead for typical identifier lengths (5-20 chars = 20-80 bytes vs 5-20 bytes).

Memory Layout

VecDeque<char> layout (simplified):

┌──────────────────────────────────┐
│ capacity: 16                     │  8 bytes (usize)
│ start: 0                         │  8 bytes (usize)
│ length: 5                        │  8 bytes (usize)
├──────────────────────────────────┤
│ buffer: ['h', 'e', 'l', 'l', 'o']│  64 bytes (16 × 4)
│         [_, _, _, _, _, ...]     │
└──────────────────────────────────┘

Total: 24 bytes (header) + 64 bytes (capacity) = 88 bytes

Growth Strategy: VecDeque doubles capacity when full (2x, 4x, 8x, ...).


Core Operations

Construction

// Empty buffer
pub fn new() -> Self;

// With initial capacity (avoids reallocation)
pub fn with_capacity(capacity: usize) -> Self;

// From existing string
pub fn from_string(s: &str) -> Self;

Complexity: All $\mathcal{O}(n)$ for from_string (must iterate characters), $\mathcal{O}(1)$ otherwise.

Example:

use liblevenshtein::contextual::DraftBuffer;

// Empty buffer
let mut buffer = DraftBuffer::new();

// Pre-allocated (typical identifier length)
let mut buffer2 = DraftBuffer::with_capacity(32);

// From existing string
let mut buffer3 = DraftBuffer::from_string("hello");
assert_eq!(buffer3.as_str(), "hello");

Character Insertion

pub fn insert(&mut self, ch: char);

Behavior: Appends character to end of buffer.

Complexity: $\mathcal{O}(1)$ amortized (may trigger reallocation).

Example:

let mut buffer = DraftBuffer::new();
buffer.insert('h');
buffer.insert('i');
assert_eq!(buffer.as_str(), "hi");

Multi-character insertion:

// Insert string character-by-character
for ch in "hello".chars() {
    buffer.insert(ch);
}
assert_eq!(buffer.as_str(), "hello");

Character Deletion (Backspace)

pub fn delete(&mut self) -> Option<char>;

Behavior: Removes and returns last character.

Returns:

  • Some(ch) if character was deleted
  • None if buffer was empty

Complexity: $\mathcal{O}(1)$ - no allocation, just decrements length.

Example:

let mut buffer = DraftBuffer::from_string("test");
assert_eq!(buffer.delete(), Some('t')); // "tes"
assert_eq!(buffer.delete(), Some('s')); // "te"
assert_eq!(buffer.as_str(), "te");

String Conversion

pub fn as_str(&self) -> String;
pub fn as_bytes(&self) -> Vec<u8>;

Complexity: $\mathcal{O}(n)$ - must collect characters into String.

Note: Not zero-copy due to character-level storage.

Example:

let buffer = DraftBuffer::from_string("test");
let s: String = buffer.as_str();
assert_eq!(s, "test");

let bytes: Vec<u8> = buffer.as_bytes();
assert_eq!(bytes, b"test");

Truncation

pub fn truncate(&mut self, len: usize);

Behavior: Truncates buffer to len characters (no-op if len >= current length).

Complexity: $\mathcal{O}(k)$ where k = (current_len - len).

Use Case: Restore to checkpoint position.

Example:

let mut buffer = DraftBuffer::from_string("hello world");
buffer.truncate(5);
assert_eq!(buffer.as_str(), "hello");

Clear

pub fn clear(&mut self);

Behavior: Removes all characters (sets length to 0).

Complexity: $\mathcal{O}(1)$ - does not deallocate.

Example:

let mut buffer = DraftBuffer::from_string("test");
buffer.clear();
assert!(buffer.is_empty());

Length & Empty Check

pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;

Complexity: $\mathcal{O}(1)$.

Example:

let buffer = DraftBuffer::from_string("hello");
assert_eq!(buffer.len(), 5);
assert!(!buffer.is_empty());

Unicode Handling

Why Unicode Matters

Multi-Byte Characters:

CharacterUTF-8 Byteschar Size
'a' (ASCII)1 byte4 bytes
'é' (Latin)2 bytes4 bytes
'世' (CJK)3 bytes4 bytes
'😀' (Emoji)4 bytes4 bytes

Problem with Byte-Level Operations:

let mut s = String::from("café");
s.pop(); // Removes 1 byte, but é is 2 bytes
// Result: "caf�" (invalid UTF-8) ❌

Solution with DraftBuffer:

let mut buffer = DraftBuffer::from_string("café");
buffer.delete(); // Removes 1 character ('é')
assert_eq!(buffer.as_str(), "caf"); // Valid UTF-8 ✓

Unicode Examples

Example 1: Emoji

let mut buffer = DraftBuffer::new();
buffer.insert('😀'); // 4-byte UTF-8 sequence
buffer.insert('🚀'); // 4-byte UTF-8 sequence
assert_eq!(buffer.len(), 2); // 2 characters, not 8 bytes
assert_eq!(buffer.as_str(), "😀🚀");

buffer.delete(); // Remove '🚀'
assert_eq!(buffer.as_str(), "😀");

Example 2: CJK Text

let mut buffer = DraftBuffer::from_string("世界");
assert_eq!(buffer.len(), 2); // 2 characters, not 6 bytes

buffer.delete(); // Remove '界'
assert_eq!(buffer.as_str(), "世");

Example 3: Mixed Scripts

let mut buffer = DraftBuffer::new();
buffer.insert('H');  // 1-byte
buffer.insert('é');  // 2-byte
buffer.insert('世'); // 3-byte
buffer.insert('😀'); // 4-byte

assert_eq!(buffer.len(), 4); // 4 characters
assert_eq!(buffer.as_str().len(), 10); // 10 bytes total

// All deletions work correctly
assert_eq!(buffer.delete(), Some('😀'));
assert_eq!(buffer.delete(), Some('世'));
assert_eq!(buffer.delete(), Some('é'));
assert_eq!(buffer.delete(), Some('H'));

Grapheme Clusters (Advanced)

Note: DraftBuffer operates on Unicode scalar values (char), not grapheme clusters.

Grapheme cluster = user-perceived character (may be multiple chars):

let flag = "🇺🇸"; // 2 chars: '🇺' + '🇸'
let buffer = DraftBuffer::from_string(flag);
assert_eq!(buffer.len(), 2); // 2 chars, 1 grapheme cluster

// Deleting once removes one char (breaks the flag)
buffer.delete();
assert_eq!(buffer.as_str(), "🇺"); // Incomplete flag

For grapheme-aware operations, use the unicode-segmentation crate with a custom buffer implementation.


Usage Examples

Example 1: Incremental Typing Simulation

use liblevenshtein::contextual::DraftBuffer;

let mut buffer = DraftBuffer::new();

// User types "function"
for ch in "function".chars() {
    buffer.insert(ch);
    println!("Draft: '{}'", buffer.as_str());
}
// Output:
// Draft: 'f'
// Draft: 'fu'
// Draft: 'fun'
// Draft: 'func'
// Draft: 'funct'
// Draft: 'functi'
// Draft: 'functio'
// Draft: 'function'

assert_eq!(buffer.as_str(), "function");

Example 2: Backspace Handling

let mut buffer = DraftBuffer::from_string("hello");

// User hits backspace 2 times
buffer.delete(); // Remove 'o'
buffer.delete(); // Remove 'l'

assert_eq!(buffer.as_str(), "hel");
assert_eq!(buffer.len(), 3);

Example 3: Typo Correction

let mut buffer = DraftBuffer::new();

// User types "functoin" (typo)
for ch in "functoin".chars() {
    buffer.insert(ch);
}

// User notices typo, backspaces 3 characters
buffer.delete(); // Remove 'n'
buffer.delete(); // Remove 'i'
buffer.delete(); // Remove 'o'

// User types correct ending
buffer.insert('i');
buffer.insert('o');
buffer.insert('n');

assert_eq!(buffer.as_str(), "function");

Example 4: Word Completion

let mut buffer = DraftBuffer::new();

// User types "prin"
for ch in "prin".chars() {
    buffer.insert(ch);
}

// Autocomplete suggests "print", user accepts
// Clear and insert full word
buffer.clear();
for ch in "print".chars() {
    buffer.insert(ch);
}

assert_eq!(buffer.as_str(), "print");

Example 5: Checkpoint and Restore

let mut buffer = DraftBuffer::from_string("hello");

// Save checkpoint (current length)
let checkpoint = buffer.len();

// User types more
buffer.insert(' ');
buffer.insert('w');
buffer.insert('o');
buffer.insert('r');
buffer.insert('l');
buffer.insert('d');

assert_eq!(buffer.as_str(), "hello world");

// Undo to checkpoint
buffer.truncate(checkpoint);
assert_eq!(buffer.as_str(), "hello");

Example 6: Unicode Typing

let mut buffer = DraftBuffer::new();

// User types emoji
buffer.insert('H');
buffer.insert('i');
buffer.insert(' ');
buffer.insert('😀');

assert_eq!(buffer.len(), 4);
assert_eq!(buffer.as_str(), "Hi 😀");

// Backspace removes emoji
buffer.delete();
assert_eq!(buffer.as_str(), "Hi ");

Example 7: Conversion to/from String

// From string
let buffer1 = DraftBuffer::from_string("test");
let buffer2 = DraftBuffer::from(String::from("test"));
let buffer3: DraftBuffer = "test".into();

// To string
let buffer = DraftBuffer::from_string("hello");
let s: String = buffer.as_str();
assert_eq!(s, "hello");

// Display trait
println!("{}", buffer); // Prints: hello

Performance Characteristics

Operation Complexity

OperationTime ComplexityAllocates?Notes
new()$\mathcal{O}(1)$Yes (header)Minimal allocation
with_capacity(n)$\mathcal{O}(1)$Yes (n chars)Pre-allocates buffer
from_string(s)$\mathcal{O}(n)$YesMust iterate characters
insert(ch)$\mathcal{O}(1)$ amortizedRare2x growth when full
delete()$\mathcal{O}(1)$NoJust decrements length
as_str()$\mathcal{O}(n)$YesCollects into String
clear()$\mathcal{O}(1)$NoSets length to 0
truncate(len)$\mathcal{O}(k)$Nok = chars removed
len()$\mathcal{O}(1)$NoField access
is_empty()$\mathcal{O}(1)$NoLength check

Benchmarks

Test Environment: Intel Xeon E5-2699 v3 @ 2.30GHz, Rust 1.75, release build

OperationTime (ns)Throughput
insert('a')~10100M ops/sec
insert('😀')~10100M ops/sec
delete()~8125M ops/sec
as_str() (10 chars)~3529M ops/sec
as_str() (100 chars)~2803.6M ops/sec
clear()~3333M ops/sec
truncate(n)~5 + 2nVaries

Key Observations:

  • Character operations sub-10ns (extremely fast)
  • Unicode characters same cost as ASCII
  • as_str() linear in length (expected for collection)
  • All operations suitable for interactive typing

Memory Usage

Per-Buffer Overhead:

  • VecDeque header: 24 bytes (capacity, start, length)
  • Character storage: 4 bytes × capacity
  • Total: 24 + (4 × capacity) bytes

Growth Pattern:

InsertionsCapacityMemory (bytes)Waste
1424 + 16 = 4012 bytes
5824 + 32 = 5612 bytes
91624 + 64 = 8828 bytes
173224 + 128 = 15260 bytes

Typical Identifier (10 chars):

  • Capacity: 16 (after 2x growth)
  • Memory: 88 bytes
  • Waste: 24 bytes (27%)

Conclusion: Minimal memory footprint for typical use (<100 bytes per buffer).


Implementation Details

Why VecDeque vs Vec?

Comparison:

FeatureVec<char>VecDeque<char> (chosen)
Push back✓ $\mathcal{O}(1)$✓ $\mathcal{O}(1)$
Pop back✓ $\mathcal{O}(1)$✓ $\mathcal{O}(1)$
Push front✗ $\mathcal{O}(n)$✓ $\mathcal{O}(1)$
Pop front✗ $\mathcal{O}(n)$✓ $\mathcal{O}(1)$
Indexing✓ $\mathcal{O}(1)$✓ $\mathcal{O}(1)$
Contiguous memory✓ Yes✗ No (ring buffer)
Memory overheadLowerHigher (~24 bytes)

Decision: VecDeque chosen for:

  • Consistent $\mathcal{O}(1)$ operations on both ends
  • Future support for insert-at-cursor (not just append)
  • Minimal overhead difference (24 bytes) negligible

Current Usage: Only uses push_back()/pop_back(), but VecDeque provides flexibility for future enhancements (e.g., cursor-based editing).

Growth Strategy

VecDeque Growth:

  1. Initial capacity: 4 (or user-specified)
  2. Growth factor: 2× when full
  3. Never shrinks automatically (manual truncate() doesn't deallocate)

Example:

let mut buffer = DraftBuffer::new(); // capacity = 4

for i in 0..5 {
    buffer.insert('a'); // Insert #5 triggers 2x growth to 8
}

// Current state:
// capacity: 8
// length: 5
// memory: 24 + (4 × 8) = 56 bytes

Memory Considerations:

  • No shrinking: Once grown, capacity never decreases
  • Trade-off: Wastes memory for long-then-short buffers vs avoids reallocation thrashing
  • Mitigation: For long-lived buffers, recreate from string periodically

Character Storage vs UTF-8

Alternative Considered: Store as UTF-8 String, convert to Vec<char> on demand.

Rejected Because:

OperationUTF-8 StringVec (chosen)
Insert✓ $\mathcal{O}(1)$ append✓ $\mathcal{O}(1)$ push
Delete✗ $\mathcal{O}(n)$ find boundary✓ $\mathcal{O}(1)$ pop
Length✗ $\mathcal{O}(n)$ count chars✓ $\mathcal{O}(1)$ field
To String✓ $\mathcal{O}(1)$ clone✗ $\mathcal{O}(n)$ collect

Decision: Optimize for insertion/deletion (hot path) at expense of string conversion (cold path).

Thread Safety

DraftBuffer is not Sync or Send by default due to VecDeque.

However, in the engine, buffers are wrapped in Arc<Mutex<HashMap<ContextId, DraftBuffer>>>, providing:

  • Exclusive access via Mutex (no data races)
  • Shareable via Arc (multiple thread references)

Testing

Test Coverage

Included Tests (in src/contextual/draft_buffer.rs):

  1. test_new() - Empty buffer creation
  2. test_insert() - Character insertion
  3. test_delete() - Character deletion
  4. test_delete_empty() - Delete from empty buffer
  5. test_from_str() - String conversion
  6. test_clear() - Buffer clearing
  7. test_truncate() - Truncation
  8. test_truncate_longer() - Truncate with len > current
  9. test_unicode() - Emoji and CJK characters
  10. test_as_bytes() - UTF-8 byte conversion
  11. test_display() - Display trait
  12. test_from_string() - From trait
  13. test_with_capacity() - Capacity pre-allocation
  14. test_incremental_typing() - Realistic typing scenario

Test Coverage: ~100% of public API

Property-Based Testing

Potential Properties (for future proptest integration):

// Property 1: Insert then delete returns to original state
forall buffer B, char C:
    let orig = B.as_str();
    B.insert(C);
    B.delete();
    assert_eq!(B.as_str(), orig);

// Property 2: Length is sum of insertions minus deletions
forall operations OPS:
    let inserts = count(OPS, Insert);
    let deletes = count(OPS, Delete);
    assert_eq!(buffer.len(), inserts - deletes);

// Property 3: Truncate is idempotent
forall buffer B, length N:
    B.truncate(N);
    B.truncate(N);
    assert_eq!(B.len(), min(original_len, N));

← Back to Layer 7

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close