use anyhow::{Context, Result};
pub fn search(
query: &str,
limit: usize,
no_refresh: bool,
literal: bool,
json: bool,
path: Option<&str>,
snippet_length: Option<i64>,
) -> Result<()> {
let project = grepdown_lib::MDDBProject::open(".").context("Failed to open project")?;
if !no_refresh {
log::info!("Refreshing index...");
project.refresh().context("Failed to refresh index")?;
}
let effective_query = if literal {
grepdown_lib::escape_fts5_query(query)
} else {
query.to_string()
};
let resolved_path = path.map(|p| p.to_string());
let results = match project.search(
&effective_query,
limit,
resolved_path.as_deref(),
snippet_length,
) {
Ok(r) => r,
Err(e) => {
let err: anyhow::Error = e.into();
if !literal && is_fts5_syntax_error(&err) {
let escaped = grepdown_lib::escape_fts5_query(query);
match project.search(&escaped, limit, resolved_path.as_deref(), snippet_length) {
Ok(r) => {
eprintln!(
"Note: original query was not valid FTS5 syntax; treated as literal. \
Use `--literal` to suppress this message."
);
r
}
Err(_) => return Err(friendly_fts5_error(query, err)),
}
} else if is_fts5_syntax_error(&err) {
return Err(friendly_fts5_error(query, err));
} else {
return Err(err.context("Search failed"));
}
}
};
if results.is_empty() {
println!("No results found.");
return Ok(());
}
if json {
println!("{}", serde_json::to_string(&results)?);
return Ok(());
}
for result in results {
let snippet = result
.snippet
.replace("<b>", "\x1b[1m")
.replace("</b>", "\x1b[0m");
println!("\x1b[1;32m{}\x1b[0m", result.path);
println!(" {}", snippet);
println!();
}
Ok(())
}
fn is_fts5_syntax_error(e: &anyhow::Error) -> bool {
e.chain().any(|c| {
let s = c.to_string();
s.contains("fts5: syntax error")
|| s == "unterminated string"
|| s.starts_with("fts5:") && s.contains("syntax")
})
}
fn friendly_fts5_error(query: &str, original: anyhow::Error) -> anyhow::Error {
let escaped = grepdown_lib::escape_fts5_query(query);
anyhow::anyhow!(
"Invalid FTS5 search query: {:?}\n Cause: {}\n\n\
FTS5 reserved syntax:\n \
- `-term` = NOT, `+term` = required, `\"phrase\"` = exact phrase, `prefix*` = suffix wildcard\n \
- Keywords: AND, OR, NOT, NEAR (case-insensitive)\n\n\
To search for this exact string, rerun with `--literal` or use the quoted form: {}.",
query,
original,
escaped,
)
}