16 KiB
Natives Text/Search Pipeline
This document maps the @oh-my-pi/pi-natives text/search/code surface from generated JS/TS exports to Rust N-API modules and back to JS result objects.
Terminology follows docs/natives-architecture.md:
- Generated binding: public API in
packages/natives/native/index.d.ts. - Rust module layer: N-API exports in
crates/pi-natives/src/*. - Shared scan cache: optional
pi-walkerdirectory-entry cache used by discovery flows.
Implementation files
packages/natives/native/index.d.tscrates/pi-natives/src/grep.rscrates/pi-natives/src/glob.rscrates/pi-natives/src/glob_util.rscrates/pi-natives/src/iofs.rscrates/pi-walker/src/lib.rscrates/pi-walker/src/cache.rscrates/pi-natives/src/ast.rscrates/pi-natives/src/text.rscrates/pi-natives/src/highlight.rscrates/pi-natives/src/tokens.rs
JS API ↔ Rust export mapping
| JS API | Rust export (#[napi], snake_case -> camelCase) |
Rust module |
|---|---|---|
grep(options, onMatch?) |
grep |
grep.rs |
search(content, options) |
search |
grep.rs |
hasMatch(content, pattern, ignoreCase?, multiline?) |
hasMatch |
grep.rs |
fuzzyFind(options) |
fuzzyFind |
fd.rs |
glob(options, onMatch?) |
glob |
glob.rs |
invalidateFsScanCache(path?) |
invalidateFsScanCache |
iofs.rs |
astGrep(options) |
astGrep |
ast.rs |
astMatch(options) |
astMatch |
ast.rs |
astEdit(options) |
astEdit |
ast.rs |
wrapTextWithAnsi(text, width, tabWidth) |
wrapTextWithAnsi |
text.rs |
truncateToWidth(text, maxWidth, ellipsis, pad, tabWidth) |
truncateToWidth |
text.rs |
sliceWithWidth(line, startCol, length, strict, tabWidth) |
sliceWithWidth |
text.rs |
extractSegments(line, beforeEnd, afterStart, afterLen, strictAfter, tabWidth) |
extractSegments |
text.rs |
visibleWidth(text, tabWidth) |
visibleWidth |
text.rs |
setHangulCompatJamoWidthOverride(value) |
setHangulCompatJamoWidthOverride |
text.rs |
highlightCode(code, lang, colors) |
highlightCode |
highlight.rs |
supportsLanguage(lang) |
supportsLanguage |
highlight.rs |
getSupportedLanguages() |
getSupportedLanguages |
highlight.rs |
countTokens(input, encoding?) |
countTokens |
tokens.rs |
Pipeline overview by subsystem
1) Regex search (grep, search, hasMatch)
Input/options flow
- Callers invoke generated native exports directly; there is no package-local TS wrapper that renames
searchtosearchContent. - Rust option structs in
grep.rsdeserialize camelCase fields includingignoreCase,maxCount,maxCountPerFile,contextBefore,contextAfter,maxColumns, andtimeoutMs. grepcreatesCancelTokenfromtimeoutMs+AbortSignaland runs insidetask::blocking("grep", ...). Filesystem grep does not expose or use the shared walker cache.searchandhasMatchoperate on provided string/Uint8Arraycontent and do not scan the filesystem.
Execution branches
- In-memory branch
search->search_sync/ search helpers over provided content bytes.hasMatchcompiles/checks pattern against provided content and returns a boolean.- No filesystem scan or walker cache.
- Single-file branch
grepresolves path, checks metadata is file, and searches that file.
- Directory branch
- Candidate discovery uses
pi-walkerwithout scan caching. - Walker policy applies hidden/gitignore settings and skips skippable directory errors.
- Entry filtering: file-only + optional glob filter (
glob_util) + optional type filter mapping (js,ts,rust, etc.).
- Candidate discovery uses
Search/collection semantics
- Matcher selection: the Rust regex engine is tried first, then PCRE2 for features such as lookaround/backreferences.
OMP_PCRE2_JIT=0/falsedisables PCRE2 JIT and1enables it; when unset, JIT is enabled except on macOS. - Context resolution:
contextBefore/contextAfteroverride legacycontext.- Non-content modes do not collect context.
- Output modes:
content-> oneGrepMatchper hit.countandfilesWithMatchesmap to count-style entries (lineNumber=0,line="",matchCountset).offsetandmaxCountare applied during aggregation across sorted file results;maxCountPerFilecan additionally prevent one hot file consuming the content-mode budget.- Directory searches use parallel filesystem walking/searching, then aggregate per-file results to preserve global offset/limit semantics. Small ordered callback streams may stop early; larger streams use a bounded ordered window.
Result shaping back to JS
- Rust
SearchResult/GrepResultfields map to TS interfaces via N-API object conversion. - Counters are clamped before crossing N-API where needed.
GrepResult.limitReachedis optional and emitted when true;skippedOversizedreports files skipped over the 4 MiB limit.- Streaming callback receives each shaped
GrepMatchfor content or count-style entries.
Failure behavior
searchreturnsSearchResult.errorfor regex/search failures instead of throwing.greprejects on hard errors such as invalid path or cancellation timeout/abort. Patterns rejected by both regex engines fall back to a literal search rather than producing a regex error.hasMatchreturns a boolean on success; matcher construction uses the same tolerant fallback.- Unreadable/non-regular files in multi-file scans are skipped; oversized files are counted in
skippedOversized.
Malformed regex handling
grep.rs sanitizes braces before regex compile:
- Invalid repetition-like braces are escaped (
{/}->\{/\}) when they cannot form{N},{N,},{N,M}. - This prevents common literal-template fragments (for example
${platform}) from failing as malformed repetition. - A compile failure for an unclosed/unopened group triggers one targeted retry with unescaped parentheses escaped while preserving the rest of the regex.
- If both engines still reject the pattern, the entire original pattern is escaped and searched literally.
2) File discovery (glob) and fuzzy path search (fuzzyFind)
glob and fuzzyFind share the optional pi-walker scan cache; matching logic differs. Cache use defaults to false for both APIs.
glob flow
- Caller passes
GlobOptionsdirectly.patternandpathare required in the generated type. - Rust resolves the search path and compiles pattern via
glob_util::compile_glob. - Entry source:
cache=true-> shared walker cache + optional stale-empty rescan.cache=false-> a fresh scan that neither reads nor updates the cache.
- Filtering:
- skip
.gitalways; - skip
node_modulesunless requested (includeNodeModules) or pattern mentionsnode_modules; - apply glob match;
- apply file-type filter; symlink
file/dirfilters resolve target metadata.
- skip
- Optional sort by mtime descending (
sortByMtime) before truncating tomaxResults.
fuzzyFind flow
- Rust implementation lives in
fd.rs; generated export isfuzzyFind. - Shared
pi-walkerscan source with the same opt-in cache and stale-empty recheck policy. - Scoring:
- exact / starts-with / contains / subsequence-based fuzzy score;
- separator/punctuation-normalized scoring path;
- directory bonus and deterministic tie-break (
score desc, thenpath asc).
- Symlink entries are excluded from fuzzy results.
Failure behavior
- Invalid glob pattern returns an error from
glob_util::compile_glob. - Search root must resolve to an existing directory for directory discovery flows.
- Cancellation/timeouts propagate as abort errors via the caller-supplied walker heartbeat and result-processing checks.
Malformed glob handling
glob_util::build_glob_pattern is tolerant:
- normalizes
\to/, - auto-prefixes simple recursive patterns with
**/whenrecursive=true, - auto-closes unbalanced
{...alternation groups before compile.
3) AST search/match/edit (astGrep, astMatch, astEdit)
ast.rs exposes syntax-aware code search and rewrite operations.
astGrep(options)returns matches with byte/line/column coordinates and optional metavariable bindings.astMatch(options)runs the same patterns against an in-memorysourcestring instead of files;langis required (there is no path to infer it from), and the result keeps matches,totalMatches,limitReached, and parse errors but omits the file-count fields.astEdit(options)returns replacement changes, per-file counts, searched/touched file counts, parse errors, and whether edits were applied.dryRundefaults to true for edit options in the generated documentation.- Options include language override, path/glob/selector, strictness, limits, parse-error policy,
signal, andtimeoutMs.
These exports are direct native APIs used by tooling; they are not mediated by a TS wrapper in packages/natives.
4) Shared scan/cache lifecycle (pi-walker)
pi-walker owns traversal and cache policy. crates/pi-natives/src/iofs.rs contains only JavaScript-facing DTO conversion, error mapping, and the invalidation export.
The cache stores normalized relative entries (path, fileType, optional mtime and regular-file size) keyed by canonical search root plus the complete WalkOptions value, including hidden/gitignore, link-following, filtering, ordering, and detail policy.
Configuration is read from environment once:
FS_SCAN_CACHE_TTL_MS: cache TTL, default1000.FS_SCAN_EMPTY_RECHECK_MS: cached-empty recheck age, default200.FS_SCAN_CACHE_MAX_ENTRIES: maximum entries in the cache map, default16.PI_WALK_WORKERS: walker Rayon pool size, default4.
Cache state transitions
- Disabled / miss / expired
- disabled requests collect fresh without reading or updating the cache;
- enabled misses and entries at or beyond TTL collect fresh and populate it.
- Hit
- an entry younger than TTL returns cached entries and cache age.
- Stale-empty recheck
- when the caller enables configured rechecking, an empty cached query at or beyond the threshold is scanned once again.
- Invalidation
invalidateFsScanCache()clears all keys;invalidateFsScanCache(path)removes cached roots containing that path.
Cache favors low-latency repeated scans over immediate consistency. Explicit invalidation is the correctness hook after writes, edits, renames, or deletes.
5) ANSI text utilities (text)
These are pure, in-memory utilities.
Boundaries and responsibilities
text.rsowns terminal-cell semantics:- ANSI sequence parsing,
- grapheme-aware width and slicing,
- wrap/truncate/slice behavior,
- explicit tab-width parameter on width-sensitive APIs.
grep.rsline truncation (maxColumns) is separate:- simple character-boundary truncation of matched lines with
..., - not ANSI-state-preserving and not terminal-cell width aware.
- simple character-boundary truncation of matched lines with
Key behaviors
wrapTextWithAnsi: wraps by visible width, carries active SGR codes across wrapped lines.truncateToWidth: visible-cell truncation with ellipsis policy (Unicode,Ascii,Omit), optional right padding.sliceWithWidth: column slicing with optional strict width enforcement.extractSegments: extracts before/after segments around an overlay while restoring ANSI state for theaftersegment.setHangulCompatJamoWidthOverride(value)controls U+3131–U+318E width correction for client-terminal compatibility:0uses the platform fallback,1forces one cell,2forces two, and3follows Unicode width.visibleWidth: counts visible terminal cells using caller-supplied tab width.
Failure behavior
Text functions generally return deterministic transformed output; errors are limited to N-API argument/string conversion boundaries.
6) Syntax highlighting (highlight)
highlight.rs is pure transformation; it does not use the filesystem scan cache.
Flow
- Caller passes
code, optionallang, and ANSI color palette. - Rust resolves syntax by token/name lookup, extension lookup, alias table fallback, then plain-text fallback.
- Each line is parsed with syntect
ParseStateand scope stack. - Scopes map to semantic color categories and ANSI color codes are injected/reset.
Failure behavior
- Per-line parse failure does not fail the call: that line is appended unhighlighted and processing continues.
- Unknown/unsupported language falls back to plain text syntax.
7) Token counting (tokens)
countTokens(input, encoding?) is an in-memory utility.
inputmay be a single string or an array of strings.- Arrays return one aggregate count and are encoded in parallel in Rust.
- Default encoding is
O200kBase;Cl100kBaseis also available. - The implementation uses ordinary tokenization, not special-token handling.
Pure utility vs filesystem-dependent flows
| Flow | Filesystem access | Shared cache | Notes |
|---|---|---|---|
search / hasMatch |
No | No | regex on provided bytes/string only |
text module functions |
No | No | ANSI/width utilities only |
highlight module functions |
No | No | syntax + ANSI coloring only |
countTokens |
No | No | tokenization only |
astMatch |
No | No | in-memory syntax-aware match (no disk) |
astGrep / astEdit |
Yes | No | syntax-aware file search/edit |
glob |
Yes | Optional | directory scans + glob filtering |
fuzzyFind |
Yes | Optional | directory scans + fuzzy scoring |
grep (file/dir path) |
Yes | No | walker discovery + regex search, optional filters/callback |
End-to-end lifecycle summary
- Caller invokes generated native export with typed options.
- Rust validates/normalizes options and builds matcher/search config.
- For filesystem flows, entries are scanned (cache hit/miss/rescan where applicable) then filtered/scored/searched.
- Worker loops periodically call cancel heartbeat; timeout/abort can terminate execution.
- Rust shapes outputs into N-API objects (
lineNumber,matchCount,limitReached, etc.). - Generated bindings return typed JS objects and optional per-match callbacks for
grep/glob.