use crate::entities::workspace::CommandSlot;
use crate::shared::i18n::Locale;
#[derive(Debug, Clone, PartialEq)]
pub enum ProjectCommand {
Attach { path: String },
Detach,
Status,
Slot {
slot: CommandSlot,
action: SlotAction,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlotAction {
Set(String),
Show,
Clear,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProjectProgress {
Attached { root: String, name: String },
Detached { root: String },
Status {
root: Option<String>,
commands: Vec<(CommandSlot, Option<String>)>,
},
CommandSet { slot: CommandSlot, line: String },
CommandShown {
slot: CommandSlot,
line: Option<String>,
},
CommandCleared { slot: CommandSlot, had: bool },
CommandRefused { line: String, ch: char },
Failed(String),
}
pub fn parse(input: &str, loc: &Locale) -> Option<Result<ProjectCommand, String>> {
let usage = || loc.t("ui.project.usage");
let (sub, rest) = match crate::features::slash::head(input, "project")? {
crate::features::slash::Head::Bare => {
return Some(Err(
loc.tf("ui.project.err.missing_subcommand", &[("usage", usage())])
));
}
crate::features::slash::Head::Sub { sub, rest } => (sub, rest),
};
let attach = || match crate::features::slash::argument(&rest) {
Some(path) => Ok(ProjectCommand::Attach { path }),
None => Err(loc.tf("ui.project.err.missing_path", &[("usage", usage())])),
};
let sub_lower = sub.to_ascii_lowercase();
if let Some(slot) = sub_lower.strip_suffix("-cmd").and_then(CommandSlot::parse) {
let action = match crate::features::slash::argument(&rest) {
Some(_) => SlotAction::Set(rest.join(" ").trim().to_string()),
None => SlotAction::Show,
};
return Some(Ok(ProjectCommand::Slot { slot, action }));
}
Some(match sub_lower.as_str() {
"attach" => attach(),
"detach" => Ok(ProjectCommand::Detach),
"status" => Ok(ProjectCommand::Status),
"clear" => match crate::features::slash::argument(&rest).as_deref() {
Some(word) => match CommandSlot::parse(word) {
Some(slot) => Ok(ProjectCommand::Slot {
slot,
action: SlotAction::Clear,
}),
None => Err(loc.tf(
"ui.project.err.unknown_slot",
&[("slot", word), ("usage", usage())],
)),
},
None => Err(loc.tf("ui.project.err.missing_slot", &[("usage", usage())])),
},
_ => Err(loc.tf(
"ui.project.err.unknown_subcommand",
&[("sub", sub), ("usage", usage())],
)),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn ru() -> &'static Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::Ru)
}
#[test]
fn parses_the_three_subcommands() {
assert_eq!(
parse("/project attach D:/proj", ru()).unwrap().unwrap(),
ProjectCommand::Attach {
path: "D:/proj".into()
}
);
assert_eq!(
parse("/project detach", ru()).unwrap().unwrap(),
ProjectCommand::Detach
);
assert_eq!(
parse("/project status", ru()).unwrap().unwrap(),
ProjectCommand::Status
);
}
#[test]
fn a_path_may_contain_spaces_and_quotes() {
let cmd = parse(r#"/project attach "C:\My Projects\app""#, ru())
.unwrap()
.unwrap();
assert_eq!(
cmd,
ProjectCommand::Attach {
path: r"C:\My Projects\app".into()
}
);
}
#[test]
fn every_slot_sets_shows_and_clears() {
for slot in CommandSlot::ALL {
let key = slot.key();
assert_eq!(
parse(&format!("/project {key}-cmd cargo build --offline"), ru())
.unwrap()
.unwrap(),
ProjectCommand::Slot {
slot,
action: SlotAction::Set("cargo build --offline".into())
}
);
assert_eq!(
parse(&format!("/project {key}-cmd"), ru())
.unwrap()
.unwrap(),
ProjectCommand::Slot {
slot,
action: SlotAction::Show
}
);
assert_eq!(
parse(&format!("/project clear {key}"), ru())
.unwrap()
.unwrap(),
ProjectCommand::Slot {
slot,
action: SlotAction::Clear
}
);
}
}
#[test]
fn a_command_line_keeps_its_own_quoting() {
assert_eq!(
parse(r#"/project test-cmd cargo test --test "my thing""#, ru())
.unwrap()
.unwrap(),
ProjectCommand::Slot {
slot: CommandSlot::Test,
action: SlotAction::Set(r#"cargo test --test "my thing""#.into())
}
);
}
#[test]
fn a_non_command_is_left_alone() {
assert!(parse("project attach x", ru()).is_none());
assert!(parse("tell me about /project", ru()).is_none());
assert!(parse("/projects", ru()).is_none());
}
#[test]
fn subcommands_are_case_insensitive() {
assert_eq!(
parse("/PROJECT Detach", ru()).unwrap().unwrap(),
ProjectCommand::Detach
);
}
#[test]
fn errors_name_a_route_in_every_language() {
for &lang in crate::shared::i18n::Lang::ALL {
let loc = crate::shared::i18n::locale(lang);
let usage = loc.t("ui.project.usage");
for input in [
"/project",
"/project attach",
"/project frobnicate",
"/project clear",
"/project clear frobnicate",
] {
let Some(Err(msg)) = parse(input, loc) else {
panic!("{lang:?}: {input} must be a localized refusal");
};
assert!(
msg.contains(usage),
"{lang:?}: {input} must name the usage: {msg}"
);
assert!(
!msg.contains('{'),
"{lang:?}: unsubstituted placeholder in {msg}"
);
}
}
}
}