use crate::shared::i18n::Locale;
#[derive(Debug, Clone, PartialEq)]
pub enum RagCommand {
Add { path: String, recursive: bool },
Delete { path: String },
List,
Rebuild,
}
pub fn parse(input: &str, loc: &Locale) -> Option<Result<RagCommand, String>> {
let mut tokens = input.split_whitespace();
let first = tokens.next()?;
if !first.eq_ignore_ascii_case("/rag") {
return None;
}
let Some(sub) = tokens.next() else {
return Some(Err(loc.tf(
"ui.rag.err.missing_subcommand",
&[("usage", loc.t("ui.rag.usage"))],
)));
};
let rest: Vec<&str> = tokens.collect();
if sub.eq_ignore_ascii_case("add") {
let (path, recursive) = match extract_path(&rest, loc) {
Ok(parts) => parts,
Err(msg) => return Some(Err(msg)),
};
Some(Ok(RagCommand::Add { path, recursive }))
} else if sub.eq_ignore_ascii_case("remove") {
let (path, _recursive) = match extract_path(&rest, loc) {
Ok(parts) => parts,
Err(msg) => return Some(Err(msg)),
};
Some(Ok(RagCommand::Delete { path }))
} else if sub.eq_ignore_ascii_case("list") {
Some(Ok(RagCommand::List))
} else if sub.eq_ignore_ascii_case("rebuild") {
Some(Ok(RagCommand::Rebuild))
} else {
Some(Err(loc.tf(
"ui.rag.err.unknown_subcommand",
&[("sub", sub), ("usage", loc.t("ui.rag.usage"))],
)))
}
}
fn extract_path(tokens: &[&str], loc: &Locale) -> Result<(String, bool), String> {
let mut recursive = false;
let mut path_parts: Vec<&str> = Vec::new();
for tok in tokens {
match *tok {
"-r" | "--recursive" => recursive = true,
_ => path_parts.push(tok),
}
}
let joined = path_parts.join(" ");
let path = joined.trim().trim_matches(|c| c == '"' || c == '\'').trim();
if path.is_empty() {
return Err(loc.tf(
"ui.rag.err.missing_path",
&[("usage", loc.t("ui.rag.usage"))],
));
}
Ok((path.to_string(), recursive))
}
#[cfg(test)]
mod tests {
use super::*;
fn ru() -> &'static Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::Ru)
}
fn add(path: &str, recursive: bool) -> Option<Result<RagCommand, String>> {
Some(Ok(RagCommand::Add {
path: path.into(),
recursive,
}))
}
#[test]
fn non_rag_input_is_none() {
assert_eq!(parse("regular message", ru()), None);
assert_eq!(parse("/help something", ru()), None);
assert_eq!(parse("", ru()), None);
}
#[test]
fn parses_file_and_dir() {
assert_eq!(
parse("/rag add d:\\dir\\file.txt", ru()),
add("d:\\dir\\file.txt", false)
);
assert_eq!(parse("/rag add d:\\dir", ru()), add("d:\\dir", false));
}
#[test]
fn parses_recursive_flag_anywhere() {
assert_eq!(parse("/rag add d:\\dir -r", ru()), add("d:\\dir", true));
assert_eq!(parse("/rag add -r d:\\dir", ru()), add("d:\\dir", true));
assert_eq!(
parse("/rag add d:\\dir --recursive", ru()),
add("d:\\dir", true)
);
}
#[test]
fn path_with_spaces_and_quotes() {
assert_eq!(
parse("/rag add C:\\Program Files\\docs -r", ru()),
add("C:\\Program Files\\docs", true)
);
assert_eq!(
parse("/rag add \"d:\\my docs\"", ru()),
add("d:\\my docs", false)
);
}
#[test]
fn command_is_case_insensitive() {
assert_eq!(parse("/RAG ADD d:\\x", ru()), add("d:\\x", false));
}
fn del(path: &str) -> Option<Result<RagCommand, String>> {
Some(Ok(RagCommand::Delete { path: path.into() }))
}
#[test]
fn parses_remove() {
assert_eq!(
parse("/rag remove d:\\dir\\file.txt", ru()),
del("d:\\dir\\file.txt")
);
assert_eq!(parse("/rag remove d:\\dir", ru()), del("d:\\dir"));
assert_eq!(parse("/rag remove d:\\dir -r", ru()), del("d:\\dir"));
}
#[test]
fn delete_is_not_a_command() {
assert!(matches!(
parse("/rag delete d:\\dir\\file.txt", ru()),
Some(Err(_))
));
}
#[test]
fn parses_list_and_rebuild() {
assert_eq!(parse("/rag list", ru()), Some(Ok(RagCommand::List)));
assert_eq!(parse("/RAG List", ru()), Some(Ok(RagCommand::List)));
assert_eq!(parse("/rag rebuild", ru()), Some(Ok(RagCommand::Rebuild)));
assert_eq!(
parse("/rag list everything", ru()),
Some(Ok(RagCommand::List))
);
}
#[test]
fn errors_on_missing_parts() {
assert!(matches!(parse("/rag", ru()), Some(Err(_))));
assert!(matches!(parse("/rag add", ru()), Some(Err(_))));
assert!(matches!(parse("/rag add -r", ru()), Some(Err(_))));
assert!(matches!(parse("/rag remove", ru()), Some(Err(_))));
assert!(matches!(parse("/rag purge x", ru()), Some(Err(_))));
}
#[test]
fn errors_are_localized_for_all_langs() {
for &lang in crate::shared::i18n::Lang::ALL {
let loc = crate::shared::i18n::locale(lang);
for input in ["/rag", "/rag add", "/rag remove", "/rag purge x"] {
let Some(Err(msg)) = parse(input, loc) else {
panic!("expected a syntax error for {input:?} in {lang:?}");
};
assert!(
!msg.contains('{') && !msg.contains('}'),
"unsubstituted placeholder in {lang:?}: {msg}"
);
if lang == crate::shared::i18n::Lang::En {
assert!(
!msg.chars().any(|c| ('\u{0400}'..='\u{04FF}').contains(&c)),
"Cyrillic leaked into the en message: {msg}"
);
}
}
}
}
}