diff --git a/src/new/questions.rs b/src/new/questions.rs index 8e39e6f..5c94d36 100644 --- a/src/new/questions.rs +++ b/src/new/questions.rs @@ -122,10 +122,30 @@ async fn run_wizard(mut cli: NewCli) -> Result> { cli.name = Some(answer); } - // 2. Language / build system. - let mut preselected: Option = None; - match &detection { - Detection::Single(id) if detection_decides => { + // 2. Language / build system. An explicit `--lang` wins over any + // detection (flag > detected/probe > default): it is never + // overwritten and the question is never re-asked — the detection is + // only logged for information. + match language_choice(cli.lang.as_deref(), &detection, detection_decides) { + LanguageChoice::Flag => match &detection { + Detection::Single(id) => log::info!( + "Detected: {} project in {}; --lang takes precedence", + id.display_name(), + detect_dir.display() + ), + Detection::Ambiguous(candidates) => log::info!( + "Several build systems found in {} ({}); --lang takes \ + precedence", + detect_dir.display(), + candidates + .iter() + .map(|id| id.as_str()) + .collect::>() + .join(", ") + ), + Detection::Empty => {} + }, + LanguageChoice::Detected(id) => { log::info!( "Detected: {} project in {}", id.display_name(), @@ -133,12 +153,16 @@ async fn run_wizard(mut cli: NewCli) -> Result> { ); cli.lang = Some(id.as_str().to_string()); } - Detection::Single(id) => { - // A skeleton was asked for: still ask, preselecting the - // detected ecosystem. - preselected = Some(*id); + LanguageChoice::Ask(preselected) => { + let menu = language_menu(&[]); + let default = preselected + .unwrap_or(TemplateId::Empty) + .display_name() + .to_string(); + let id = select_template(&menu, &default)?; + cli.lang = Some(id.as_str().to_string()); } - Detection::Ambiguous(candidates) => { + LanguageChoice::Ambiguous(candidates) => { log::info!( "Several build systems found in {} ({}): candidates listed \ first, the highest-precedence one preselected", @@ -149,20 +173,10 @@ async fn run_wizard(mut cli: NewCli) -> Result> { .collect::>() .join(", ") ); - let menu = language_menu(candidates); + let menu = language_menu(&candidates); let id = select_template(&menu, &menu[0])?; cli.lang = Some(id.as_str().to_string()); } - Detection::Empty => {} - } - if cli.lang.is_none() { - let menu = language_menu(&[]); - let default = preselected - .unwrap_or(TemplateId::Empty) - .display_name() - .to_string(); - let id = select_template(&menu, &default)?; - cli.lang = Some(id.as_str().to_string()); } let template = TemplateId::parse(cli.lang.as_deref().unwrap_or_default())?; @@ -690,6 +704,42 @@ fn language_menu(candidates: &[TemplateId]) -> Vec { .collect() } +/// What the language step does with the detection result: how an explicit +/// `--lang` flag combines with it (flag > detected/probe > default). +#[derive(Debug, PartialEq, Eq)] +enum LanguageChoice { + /// An explicit `--lang` wins: it is kept as-is, the question is never + /// re-asked and the detection stays informational. + Flag, + /// No flag, confident detection, packaging the detected directory: the + /// detection decides. + Detected(TemplateId), + /// Ask the question, preselecting the detected ecosystem (a skeleton + /// was asked for); `None` when nothing was detected. + Ask(Option), + /// Ask the question with the ambiguous candidates listed first (the + /// highest-precedence one preselected). + Ambiguous(Vec), +} + +/// The language step's decision for one wizard run. With the flag absent +/// this mirrors the historical behavior exactly; the flag always wins. +fn language_choice( + flag: Option<&str>, + detection: &Detection, + detection_decides: bool, +) -> LanguageChoice { + if flag.is_some() { + return LanguageChoice::Flag; + } + match detection { + Detection::Single(id) if detection_decides => LanguageChoice::Detected(*id), + Detection::Single(id) => LanguageChoice::Ask(Some(*id)), + Detection::Ambiguous(candidates) => LanguageChoice::Ambiguous(candidates.clone()), + Detection::Empty => LanguageChoice::Ask(None), + } +} + /// The license menu: the curated SPDX list plus the free-text entry. fn license_menu() -> Vec { KNOWN_LICENSES @@ -1043,6 +1093,59 @@ mod tests { assert_eq!(unique.len(), menu.len()); } + /// An explicit `--lang` wins over any detection (the flag outranks the + /// detection and the defaults): the flag is kept and the question is + /// never re-asked, whatever the detection found (regression: a + /// confident detection used to overwrite the flag and an ambiguous one + /// re-asked the question). + #[test] + fn language_choice_flag_wins_over_detection() { + let detections = [ + Detection::Single(Tid::Rust), + Detection::Ambiguous(vec![Tid::Rust, Tid::Python]), + Detection::Empty, + ]; + for detection in &detections { + for decides in [false, true] { + assert_eq!( + language_choice(Some("python"), detection, decides), + LanguageChoice::Flag, + "detection {detection:?}, decides {decides}" + ); + } + } + } + + /// Without the flag the detection behaves exactly as before: it decides + /// for a confident detection (packaging the detected directory), asks + /// with a preselection for a skeleton run, lists the candidates first + /// when ambiguous, and falls back to the plain menu otherwise. + #[test] + fn language_choice_without_flag_follows_detection() { + assert_eq!( + language_choice(None, &Detection::Single(Tid::Rust), true), + LanguageChoice::Detected(Tid::Rust) + ); + // Skeleton run: ask, preselecting the detected ecosystem. + assert_eq!( + language_choice(None, &Detection::Single(Tid::Rust), false), + LanguageChoice::Ask(Some(Tid::Rust)) + ); + assert_eq!( + language_choice( + None, + &Detection::Ambiguous(vec![Tid::Go, Tid::Python]), + true + ), + LanguageChoice::Ambiguous(vec![Tid::Go, Tid::Python]) + ); + // Nothing detected: plain menu, the empty template preselected. + assert_eq!( + language_choice(None, &Detection::Empty, false), + LanguageChoice::Ask(None) + ); + } + #[test] fn license_menu_and_defaults() { let menu = license_menu();