216 lines
6.8 KiB
Rust
216 lines
6.8 KiB
Rust
//! N-API bindings for the grep module.
|
|
//!
|
|
//! Wraps `forge_grep` functions and exposes them as JS-callable N-API exports.
|
|
|
|
use napi::bindgen_prelude::*;
|
|
use napi_derive::napi;
|
|
|
|
use crate::task;
|
|
|
|
// ── N-API types (mirroring forge_grep types for the JS boundary) ────────
|
|
|
|
#[napi(object)]
|
|
pub struct NapiContextLine {
|
|
#[napi(js_name = "lineNumber")]
|
|
pub line_number: u32,
|
|
pub line: String,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct NapiSearchMatch {
|
|
#[napi(js_name = "lineNumber")]
|
|
pub line_number: u32,
|
|
pub line: String,
|
|
#[napi(js_name = "contextBefore")]
|
|
pub context_before: Vec<NapiContextLine>,
|
|
#[napi(js_name = "contextAfter")]
|
|
pub context_after: Vec<NapiContextLine>,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct NapiSearchResult {
|
|
pub matches: Vec<NapiSearchMatch>,
|
|
#[napi(js_name = "matchCount")]
|
|
pub match_count: u32,
|
|
#[napi(js_name = "limitReached")]
|
|
pub limit_reached: bool,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct NapiSearchOptions {
|
|
pub pattern: String,
|
|
#[napi(js_name = "ignoreCase")]
|
|
pub ignore_case: Option<bool>,
|
|
pub multiline: Option<bool>,
|
|
#[napi(js_name = "maxCount")]
|
|
pub max_count: Option<u32>,
|
|
#[napi(js_name = "contextBefore")]
|
|
pub context_before: Option<u32>,
|
|
#[napi(js_name = "contextAfter")]
|
|
pub context_after: Option<u32>,
|
|
#[napi(js_name = "maxColumns")]
|
|
pub max_columns: Option<u32>,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct NapiGrepMatch {
|
|
pub path: String,
|
|
#[napi(js_name = "lineNumber")]
|
|
pub line_number: u32,
|
|
pub line: String,
|
|
#[napi(js_name = "contextBefore")]
|
|
pub context_before: Vec<NapiContextLine>,
|
|
#[napi(js_name = "contextAfter")]
|
|
pub context_after: Vec<NapiContextLine>,
|
|
pub truncated: bool,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct NapiGrepResult {
|
|
pub matches: Vec<NapiGrepMatch>,
|
|
#[napi(js_name = "totalMatches")]
|
|
pub total_matches: u32,
|
|
#[napi(js_name = "filesWithMatches")]
|
|
pub files_with_matches: u32,
|
|
#[napi(js_name = "filesSearched")]
|
|
pub files_searched: u32,
|
|
#[napi(js_name = "limitReached")]
|
|
pub limit_reached: bool,
|
|
}
|
|
|
|
#[napi(object)]
|
|
pub struct NapiGrepOptions {
|
|
pub pattern: String,
|
|
pub path: String,
|
|
pub glob: Option<String>,
|
|
#[napi(js_name = "ignoreCase")]
|
|
pub ignore_case: Option<bool>,
|
|
pub multiline: Option<bool>,
|
|
pub hidden: Option<bool>,
|
|
pub gitignore: Option<bool>,
|
|
#[napi(js_name = "maxCount")]
|
|
pub max_count: Option<u32>,
|
|
#[napi(js_name = "contextBefore")]
|
|
pub context_before: Option<u32>,
|
|
#[napi(js_name = "contextAfter")]
|
|
pub context_after: Option<u32>,
|
|
#[napi(js_name = "maxColumns")]
|
|
pub max_columns: Option<u32>,
|
|
}
|
|
|
|
// ── Conversion helpers ────────────────────────────────────────────────
|
|
|
|
fn clamp_u32(value: u64) -> u32 {
|
|
value.min(u32::MAX as u64) as u32
|
|
}
|
|
|
|
fn convert_context_line(cl: forge_grep::ContextLine) -> NapiContextLine {
|
|
NapiContextLine {
|
|
line_number: clamp_u32(cl.line_number),
|
|
line: cl.line,
|
|
}
|
|
}
|
|
|
|
fn convert_search_match(m: forge_grep::SearchMatch) -> NapiSearchMatch {
|
|
NapiSearchMatch {
|
|
line_number: clamp_u32(m.line_number),
|
|
line: m.line,
|
|
context_before: m
|
|
.context_before
|
|
.into_iter()
|
|
.map(convert_context_line)
|
|
.collect(),
|
|
context_after: m
|
|
.context_after
|
|
.into_iter()
|
|
.map(convert_context_line)
|
|
.collect(),
|
|
truncated: m.truncated,
|
|
}
|
|
}
|
|
|
|
fn convert_file_match(m: forge_grep::FileMatch) -> NapiGrepMatch {
|
|
NapiGrepMatch {
|
|
path: m.path,
|
|
line_number: clamp_u32(m.line_number),
|
|
line: m.line,
|
|
context_before: m
|
|
.context_before
|
|
.into_iter()
|
|
.map(convert_context_line)
|
|
.collect(),
|
|
context_after: m
|
|
.context_after
|
|
.into_iter()
|
|
.map(convert_context_line)
|
|
.collect(),
|
|
truncated: m.truncated,
|
|
}
|
|
}
|
|
|
|
// ── Exported N-API functions ──────────────────────────────────────────
|
|
|
|
/// Search in-memory content for a regex pattern.
|
|
///
|
|
/// Accepts a Buffer/Uint8Array or a string. Returns matches with line numbers
|
|
/// and optional context lines.
|
|
#[napi(js_name = "search")]
|
|
pub fn search(content: Buffer, options: NapiSearchOptions) -> Result<NapiSearchResult> {
|
|
let opts = forge_grep::SearchOptions {
|
|
pattern: options.pattern,
|
|
ignore_case: options.ignore_case.unwrap_or(false),
|
|
multiline: options.multiline.unwrap_or(false),
|
|
max_count: options.max_count.map(u64::from),
|
|
context_before: options.context_before.unwrap_or(0),
|
|
context_after: options.context_after.unwrap_or(0),
|
|
max_columns: options.max_columns.map(|v| v as usize),
|
|
};
|
|
|
|
match forge_grep::search_content(content.as_ref(), &opts) {
|
|
Ok(result) => Ok(NapiSearchResult {
|
|
matches: result
|
|
.matches
|
|
.into_iter()
|
|
.map(convert_search_match)
|
|
.collect(),
|
|
match_count: clamp_u32(result.match_count),
|
|
limit_reached: result.limit_reached,
|
|
}),
|
|
Err(err) => Err(Error::from_reason(err)),
|
|
}
|
|
}
|
|
|
|
/// Search files on disk for a regex pattern.
|
|
///
|
|
/// Walks the directory tree respecting `.gitignore` and optional glob filters.
|
|
/// Returns matches with file paths, line numbers, and optional context.
|
|
#[napi(js_name = "grep")]
|
|
pub fn grep(options: NapiGrepOptions) -> task::Async<NapiGrepResult> {
|
|
task::blocking("grep", (), move |_ct| {
|
|
let opts = forge_grep::GrepOptions {
|
|
pattern: options.pattern,
|
|
path: options.path,
|
|
glob: options.glob,
|
|
ignore_case: options.ignore_case.unwrap_or(false),
|
|
multiline: options.multiline.unwrap_or(false),
|
|
hidden: options.hidden.unwrap_or(false),
|
|
gitignore: options.gitignore.unwrap_or(true),
|
|
max_count: options.max_count.map(u64::from),
|
|
context_before: options.context_before.unwrap_or(0),
|
|
context_after: options.context_after.unwrap_or(0),
|
|
max_columns: options.max_columns.map(|v| v as usize),
|
|
};
|
|
|
|
match forge_grep::search_path(&opts) {
|
|
Ok(result) => Ok(NapiGrepResult {
|
|
matches: result.matches.into_iter().map(convert_file_match).collect(),
|
|
total_matches: clamp_u32(result.total_matches),
|
|
files_with_matches: result.files_with_matches,
|
|
files_searched: result.files_searched,
|
|
limit_reached: result.limit_reached,
|
|
}),
|
|
Err(err) => Err(Error::from_reason(err)),
|
|
}
|
|
})
|
|
}
|