MusePi

Natives Text/Search Pipeline

This document maps the @musepi/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:

Implementation files

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 fs_cache.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
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

  1. Callers invoke generated native exports directly; there is no package-local TS wrapper that renames search to searchContent.
  2. Rust option structs in grep.rs deserialize camelCase fields (ignoreCase, maxCount, contextBefore, contextAfter, maxColumns, timeoutMs).
  3. grep creates CancelToken from timeoutMs + AbortSignal and runs inside task::blocking("grep", ...).
  4. search and hasMatch operate on provided string/Uint8Array content and do not scan the filesystem.

Execution branches

Search/collection semantics

Result shaping back to JS

Failure behavior

Malformed regex handling

grep.rs sanitizes braces before regex compile:

2) File discovery (glob) and fuzzy path search (fuzzyFind)

glob and fuzzyFind share fs_cache scans; matching logic differs.

glob flow

  1. Caller passes GlobOptions directly. pattern and path are required in the generated type.
  2. Rust resolves the search path and compiles pattern via glob_util::compile_glob.
  3. Entry source:
    • cache=true -> get_or_scan + optional stale-empty force_rescan.
    • cache=false -> force_rescan(..., store=false) (fresh only).
  4. Filtering:
    • skip .git always;
    • skip node_modules unless requested (includeNodeModules) or pattern mentions node_modules;
    • apply glob match;
    • apply file-type filter; symlink file/dir filters resolve target metadata.
  5. Optional sort by mtime descending (sortByMtime) before truncating to maxResults.

fuzzyFind flow

  1. Rust implementation lives in fd.rs; generated export is fuzzyFind.
  2. Shared scan source from fs_cache with the same cache/no-cache split and stale-empty recheck policy.
  3. Scoring:
    • exact / starts-with / contains / subsequence-based fuzzy score;
    • separator/punctuation-normalized scoring path;
    • directory bonus and deterministic tie-break (score desc, then path asc).
  4. Symlink entries are excluded from fuzzy results.

Failure behavior

Malformed glob handling

glob_util::build_glob_pattern is tolerant:

3) AST search/match/edit (astGrep, astMatch, astEdit)

ast.rs exposes syntax-aware code search and rewrite operations.

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 (fs_cache)

fs_cache stores scan results as normalized relative entries (path, fileType, optional mtime and regular-file size) keyed by:

follow_links affects a fresh scan but is not currently part of the cache key.

Cache state transitions

  1. Miss / disabled
    • TTL is 0 or key absent/expired -> fresh collection.
  2. Hit
    • Entry age is within TTL -> return cached entries + cache_age_ms.
  3. Stale-empty recheck
    • If query yields zero matches and cache age exceeds the empty-result threshold, force one rescan.
  4. Invalidation
    • invalidateFsScanCache(path?):
      • no arg: clear all keys;
      • path arg: remove keys for roots affected by that path.

Stale-result tradeoff

5) ANSI text utilities (text)

These are pure, in-memory utilities.

Boundaries and responsibilities

Key behaviors

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

  1. Caller passes code, optional lang, and ANSI color palette.
  2. Rust resolves syntax by token/name lookup, extension lookup, alias table fallback, then plain-text fallback.
  3. Each line is parsed with syntect ParseState and scope stack.
  4. Scopes map to semantic color categories and ANSI color codes are injected/reset.

Failure behavior

7) Token counting (tokens)

countTokens(input, encoding?) is an in-memory utility.

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 Optional in dir mode ripgrep over files, optional filters/callback

End-to-end lifecycle summary

  1. Caller invokes generated native export with typed options.
  2. Rust validates/normalizes options and builds matcher/search config.
  3. For filesystem flows, entries are scanned (cache hit/miss/rescan where applicable) then filtered/scored/searched.
  4. Worker loops periodically call cancel heartbeat; timeout/abort can terminate execution.
  5. Rust shapes outputs into N-API objects (lineNumber, matchCount, limitReached, etc.).
  6. Generated bindings return typed JS objects and optional per-match callbacks for grep/glob.