use super::state::{GrepSearchContext, stream_grep};
use fff_search::GrepMode;
use nu_engine::command_prelude::*;
use nu_protocol::Range;
use std::ops::Bound;
#[derive(Clone)]
pub struct IdxSearch;
impl Command for IdxSearch {
fn name(&self) -> &str {
"idx search"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.rest(
"pattern",
SyntaxShape::String,
"One or more search patterns.",
)
.switch("regex", "Use regular-expression matching mode.", Some('r'))
.switch("fuzzy", "Use fuzzy line-matching mode.", Some('f'))
.named(
"limit",
SyntaxShape::Int,
"Maximum number of matches to collect.",
Some('l'),
)
.named(
"context",
SyntaxShape::OneOf(vec![SyntaxShape::Range, SyntaxShape::Int]),
"The number of context lines to include before and after each match can be specified as an integer or a range. An integer sets both the before and after context to that number, while a range uses a negative value for lines before and a positive value for lines after (e.g., -3..5).",
Some('c'),
)
.input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::record())))])
.category(Category::FileSystem)
}
fn description(&self) -> &str {
"Search indexed file contents."
}
fn extra_description(&self) -> &str {
"Mode selection: plain text is the default and treats each pattern literally, `--regex` evaluates the patterns as regular expressions, and `--fuzzy` performs approximate line matching."
}
fn examples(&self) -> Vec<Example<'_>> {
vec![
Example {
description: "Search indexed file contents for a plain text pattern.",
example: "idx search hello",
result: None,
},
Example {
description: "Search using a regular expression.",
example: "idx search --regex 'fn \\w+'",
result: None,
},
Example {
description: "Search with multiple patterns simultaneously.",
example: "idx search TODO FIXME HACK",
result: None,
},
Example {
description: "Include 2 lines of context before and 5 lines after each match.",
example: "idx search --context -2..5 error",
result: None,
},
Example {
description: "Brackets and question marks are treated as literal text, not glob patterns.",
example: "idx search 'arr[0]'",
result: None,
},
Example {
description: "Glob patterns with a path separator filter which files to search.",
example: "idx search pattern tests/*",
result: None,
},
Example {
description: "Brace expansion globs also filter which files to search.",
example: "idx search pattern *.{rs,js}",
result: None,
},
]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let patterns: Vec<String> = call.rest(engine_state, stack, 0)?;
if patterns.is_empty() {
return Err(ShellError::MissingParameter {
param_name: "pattern".to_string(),
span: call.head,
});
}
let regex = call.has_flag(engine_state, stack, "regex")?;
let fuzzy = call.has_flag(engine_state, stack, "fuzzy")?;
if regex && fuzzy {
return Err(ShellError::IncompatibleParameters {
left_message: "--regex cannot be used with --fuzzy".to_string(),
left_span: call.get_flag_span(stack, "regex").unwrap_or(call.head),
right_message: "--fuzzy cannot be used with --regex".to_string(),
right_span: call.get_flag_span(stack, "fuzzy").unwrap_or(call.head),
});
}
let limit = call
.get_flag::<i64>(engine_state, stack, "limit")?
.map(|value| {
usize::try_from(value)
.map_err(|_| ShellError::NeedsPositiveValue { span: call.head })
})
.transpose()?
.unwrap_or(50);
let mode = if fuzzy {
GrepMode::Fuzzy
} else if regex {
GrepMode::Regex
} else {
GrepMode::PlainText
};
let (before_context, after_context) = parse_context(
call.get_flag::<Value>(engine_state, stack, "context")?,
call.head,
)?;
let cwd = engine_state.cwd(Some(stack))?.into_std_path_buf();
stream_grep(GrepSearchContext {
patterns: &patterns,
mode,
page_limit: limit,
span: call.head,
before_context,
after_context,
cwd: Some(cwd.as_path()),
signals: engine_state.signals(),
})
}
}
fn parse_context(value: Option<Value>, span: Span) -> Result<(usize, usize), ShellError> {
let unsupported = |msg| ShellError::UnsupportedInput {
msg,
input: "value originates from here".into(),
msg_span: span,
input_span: span,
};
let Some(value) = value else {
return Ok((0, 0));
};
match value {
Value::Int { val, .. } => {
let count = usize::try_from(val).map_err(|_| {
unsupported("Context must be non-negative, or use a range such as -3..5".into())
})?;
Ok((count, count))
}
Value::Range { val, .. } => match *val {
Range::IntRange(range) => {
if range.step() != 1 {
return Err(unsupported(
"Context range must not have an explicit step (e.g. use -3..5, not -3..1..5)".into(),
));
}
let start = range.start();
if start > 0 {
return Err(unsupported(
"Context range start must be <= 0 (use a negative value for before-context, e.g. -3..5)".into(),
));
}
let end = match range.end() {
Bound::Included(end) | Bound::Excluded(end) => end,
Bound::Unbounded => {
return Err(unsupported(
"Context range must have a bounded end (use a positive value for after-context, e.g. -3..5)".into(),
));
}
};
if end < 0 {
return Err(unsupported(
"Context range end must be >= 0 (use a positive value for after-context, e.g. -3..5)".into(),
));
}
let before = usize::try_from(start.unsigned_abs())
.map_err(|_| unsupported("Context range start is too large".into()))?;
let after = usize::try_from(end)
.map_err(|_| unsupported("Context range end is too large".into()))?;
Ok((before, after))
}
Range::FloatRange(_) => Err(unsupported(
"Float ranges are not supported for context".into(),
)),
},
other => Err(unsupported(format!(
"Context must be an integer or range, but got {}",
other.get_type()
))),
}
}