Agent skill
nucleo-matcher
High-performance fuzzy matching library for filtering choices in prompts
Install this agent skill to your Project
npx add-skill https://github.com/johnlindquist/script-kit-next/tree/main/.opencode/skill/nucleo-matcher
SKILL.md
nucleo-matcher
nucleo-matcher is a high-performance fuzzy matching library from the Helix editor project. It's 10-100x faster than alternatives like fuzzy-matcher (skim) or fzf.
Why It's Fast
- Optimized SIMD operations - Uses CPU-level optimizations for character comparisons
- Reusable scratch memory - Matcher allocates ~135KB once, then reuses it (no allocations during matching)
- Automatic algorithm fallback - Switches from O(mn) to O(n) greedy matching for large inputs
- Prefilter acceleration - Uses memchr for fast substring scanning before full matching
- UTF-32 internal representation - Avoids repeated UTF-8 decoding overhead
Key Types
Matcher
The core matching engine. Expensive to create (~135KB heap allocation), so reuse it.
use nucleo_matcher::{Matcher, Config};
// Create once, reuse many times
let mut matcher = Matcher::new(Config::DEFAULT);
// For file paths, use path-optimized config
let mut matcher = Matcher::new(Config::DEFAULT.match_paths());
Pattern
Parses query strings and handles multi-word matching. Preferred over calling Matcher directly.
use nucleo_matcher::pattern::{Pattern, CaseMatching, Normalization};
// Parse with fzf-style syntax (^ for prefix, $ for suffix, etc.)
let pattern = Pattern::parse("foo bar", CaseMatching::Ignore, Normalization::Smart);
// Or without special syntax parsing
let pattern = Pattern::new("foo bar", CaseMatching::Ignore, Normalization::Smart, AtomKind::Fuzzy);
Utf32Str
Efficient UTF-32 string wrapper. Requires a buffer to avoid allocations.
use nucleo_matcher::Utf32Str;
let mut buf: Vec<char> = Vec::new();
let haystack = Utf32Str::new("hello world", &mut buf);
Config
Controls matcher behavior:
normalize: bool- Normalize latin chars to ASCII (default: true)ignore_case: bool- Case-insensitive matching (default: true)prefer_prefix: bool- Bonus for matches near start (default: false)match_paths()- Preset for file path matching
CaseMatching (enum)
Ignore- Always case-insensitive (a == A)Respect- Always case-sensitive (a != A)Smart- Case-insensitive if query is all lowercase, else sensitive
Normalization (enum)
Smart- Normalize accented characters intelligentlyNever- No normalization
Usage in script-kit-gpui
The codebase uses a NucleoCtx wrapper for optimal performance:
/// Context for nucleo fuzzy matching that reuses allocations across calls.
pub(crate) struct NucleoCtx {
pattern: Pattern,
matcher: Matcher,
buf: Vec<char>, // Reused buffer for Utf32Str conversion
}
impl NucleoCtx {
pub fn new(query: &str) -> Self {
let pattern = Pattern::parse(
query,
CaseMatching::Ignore,
Normalization::Smart,
);
Self {
pattern,
matcher: Matcher::new(Config::DEFAULT),
buf: Vec::with_capacity(64), // Pre-allocate for typical strings
}
}
#[inline]
pub fn score(&mut self, haystack: &str) -> Option<u32> {
self.buf.clear(); // Clear buffer, keep capacity
let utf32 = Utf32Str::new(haystack, &mut self.buf);
self.pattern.score(utf32, &mut self.matcher)
}
}
Scoring Flow in Search Functions
pub fn fuzzy_search_scripts(scripts: &[Arc<Script>], query: &str) -> Vec<ScriptMatch> {
let query_lower = query.to_lowercase();
// Create context ONCE for all items
let mut nucleo = NucleoCtx::new(&query_lower);
for script in scripts {
let mut score = 0i32;
// Fuzzy match using nucleo
if let Some(nucleo_s) = nucleo.score(&script.name) {
// Scale score (0-1000+) to match other weights
score += 50 + (nucleo_s / 20) as i32;
}
// Also check filename, description, etc.
if let Some(nucleo_s) = nucleo.score(&filename) {
score += 35 + (nucleo_s / 30) as i32;
}
}
}
Matching Patterns (fzf-style syntax with Pattern::parse)
| Prefix/Suffix | Meaning | Example |
|---|---|---|
| (none) | Fuzzy match | bar matches "foobar" |
^ |
Prefix match | ^foo matches "foobar" |
$ |
Suffix match | bar$ matches "foobar" |
' |
Exact substring | 'bar matches "foobar" exactly |
! |
Inverse match | !bar excludes items with "bar" |
Multiple words are AND-ed together:
// "foo bar" matches items containing BOTH "foo" AND "bar" (in any order)
Pattern::parse("foo bar", CaseMatching::Ignore, Normalization::Smart)
Performance Tips
DO: Reuse Matcher and Buffer
// GOOD: Create once, reuse
let mut ctx = NucleoCtx::new(query);
for item in items {
ctx.score(&item.name);
}
DON'T: Create Matcher Per Item
// BAD: 135KB allocation per item!
for item in items {
let mut matcher = Matcher::new(Config::DEFAULT);
// ...
}
DO: Pre-allocate Buffer with Capacity
// GOOD: Pre-allocate for typical string sizes
buf: Vec::with_capacity(64)
DO: Clear Buffer, Not Re-allocate
// GOOD: Keeps allocated memory
self.buf.clear();
// BAD: Re-allocates
self.buf = Vec::new();
DO: Use ASCII Fast-Path When Possible
// script-kit-gpui combines ASCII fast-path with nucleo fallback
if query_is_ascii && item.name.is_ascii() {
// Fast byte-level comparison
if let Some(pos) = find_ignore_ascii_case(&item.name, &query_lower) {
score += 100;
}
}
// Then add nucleo fuzzy score
if let Some(nucleo_s) = nucleo.score(&item.name) {
score += 50 + (nucleo_s / 20) as i32;
}
DO: Use match_paths() for File Paths
// Applies bonuses appropriate for path separators
let matcher = Matcher::new(Config::DEFAULT.match_paths());
Anti-patterns
Creating Matcher in Hot Loop
// WRONG: ~135KB allocation per iteration
for item in items {
let mut matcher = Matcher::new(Config::DEFAULT);
}
Not Reusing UTF-32 Buffer
// WRONG: New allocation per call
fn score(haystack: &str, pattern: &Pattern, matcher: &mut Matcher) -> Option<u32> {
let mut buf = Vec::new(); // Allocates every time!
let utf32 = Utf32Str::new(haystack, &mut buf);
pattern.score(utf32, matcher)
}
Using Direct Matcher Methods Instead of Pattern
// WRONG for most use cases - Pattern handles multi-word, case folding, etc.
matcher.fuzzy_match(haystack, needle)
// RIGHT: Pattern handles complexity
pattern.score(haystack, &mut matcher)
Not Scaling Nucleo Scores
// WRONG: Raw nucleo scores (0-1000+) don't mix with other scoring weights
score += nucleo_s as i32;
// RIGHT: Scale to match your scoring system
score += 50 + (nucleo_s / 20) as i32;
Blocking UI Thread with Large Lists
// WRONG: Blocks UI for large lists
let matches = Pattern::parse(query, ...).match_list(huge_list, &mut matcher);
// RIGHT: Use high-level `nucleo` crate for async/threaded matching
// Or batch process in background
Score Ranges
- 0: No match
- 100-200: Weak fuzzy match (scattered characters)
- 200-500: Good fuzzy match (consecutive characters)
- 500-1000+: Excellent match (prefix/exact substring)
References
- docs.rs/nucleo-matcher
- GitHub: helix-editor/nucleo
- Local implementation:
src/scripts/search.rs
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
Generate Component Documentation
Based on existing docs styles and specific API implementations, and referencing same name stories, generate comprehensive documentation for the new component.
Generate Component Story
Generate a comprehensive story for a new component for as example.
new-component
How to write a new component of GPUI Component.
troubleshooting
Diagnose and fix common Script Kit issues. Use when the user reports bugs, crashes, missing features, or unexpected behavior in Script Kit GPUI.
script-authoring
Create and manage TypeScript scripts for Script Kit. Use when the user wants to write a new script, edit an existing script, or understand Script Kit's SDK and metadata system.
agents
Create mdflow-backed agent files for Script Kit. Use when the user wants to create AI agents, configure agent backends (Claude, Gemini, Codex), or manage agent metadata.
Didn't find tool you were looking for?