use crate::config::Config;
use crate::error::SwitcherError;
use crate::resolver::{sibling_for_target, CurrentMatch};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Direct,
CycleForward,
CycleBackward,
}
pub fn select(
current: &CurrentMatch,
mode: Mode,
target: Option<&str>,
config: &Config,
) -> Result<PathBuf, SwitcherError> {
match mode {
Mode::Direct => {
let target =
target.ok_or_else(|| SwitcherError::Input("--to requires a target name".into()))?;
if !config.targets.contains_key(target) {
return Err(SwitcherError::Input(format!(
"unknown target '{target}'. Known targets: {}",
config
.targets
.keys()
.cloned()
.collect::<Vec<_>>()
.join(", ")
)));
}
sibling_for_target(current, target, config).ok_or_else(|| SwitcherError::NoSibling {
target: Some(target.to_string()),
})
}
Mode::CycleForward | Mode::CycleBackward => cycle(current, mode, config),
}
}
fn cycle(current: &CurrentMatch, mode: Mode, config: &Config) -> Result<PathBuf, SwitcherError> {
let order = &config.cycle.order;
let n: i64 = i64::try_from(order.len())
.map_err(|_| SwitcherError::Config("cycle.order is too large".into()))?;
let dir: i64 = if mode == Mode::CycleForward { 1 } else { -1 };
let start_idx = order.iter().position(|t| t == ¤t.target);
for step in 1..=n {
let raw: i64 = match start_idx {
Some(idx) => {
let idx_i = i64::try_from(idx).expect("position within Vec fits in i64");
idx_i + dir * step
}
None => {
if mode == Mode::CycleForward {
step - 1
} else {
n - step
}
}
};
let idx_i64: i64 = if config.cycle.wrap {
raw.rem_euclid(n)
} else if !(0..n).contains(&raw) {
continue;
} else {
raw
};
let idx = usize::try_from(idx_i64).expect("non-negative after rem_euclid / bounds check");
let candidate_target = &order[idx];
if !current.target.is_empty() && candidate_target == ¤t.target {
continue;
}
if let Some(path) = sibling_for_target(current, candidate_target, config) {
return Ok(path);
}
if !config.cycle.skip_missing {
return Err(SwitcherError::NoSibling {
target: Some(candidate_target.clone()),
});
}
}
Err(SwitcherError::NoSibling { target: None })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resolver::identify_current;
use std::fs;
use std::path::Path;
use tempfile::tempdir;
fn touch(dir: &Path, name: &str) -> PathBuf {
let p = dir.join(name);
fs::write(&p, "").unwrap();
p
}
#[test]
fn cycle_ts_to_html_when_all_exist() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
touch(dir.path(), "foo.component.html");
touch(dir.path(), "foo.component.scss");
touch(dir.path(), "foo.component.spec.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let next = select(&cur, Mode::CycleForward, None, &cfg).unwrap();
assert_eq!(next.file_name().unwrap(), "foo.component.html");
}
#[test]
fn cycle_full_loop() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
touch(dir.path(), "foo.component.html");
touch(dir.path(), "foo.component.scss");
touch(dir.path(), "foo.component.spec.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let html = select(&cur, Mode::CycleForward, None, &cfg).unwrap();
let cur2 = identify_current(&html, &cfg).unwrap();
let scss = select(&cur2, Mode::CycleForward, None, &cfg).unwrap();
let cur3 = identify_current(&scss, &cfg).unwrap();
let spec = select(&cur3, Mode::CycleForward, None, &cfg).unwrap();
let cur4 = identify_current(&spec, &cfg).unwrap();
let back_to_ts = select(&cur4, Mode::CycleForward, None, &cfg).unwrap();
assert_eq!(html.file_name().unwrap(), "foo.component.html");
assert_eq!(scss.file_name().unwrap(), "foo.component.scss");
assert_eq!(spec.file_name().unwrap(), "foo.component.spec.ts");
assert_eq!(back_to_ts.file_name().unwrap(), "foo.component.ts");
}
#[test]
fn cycle_skips_missing_sibling() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
touch(dir.path(), "foo.component.spec.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let next = select(&cur, Mode::CycleForward, None, &cfg).unwrap();
assert_eq!(next.file_name().unwrap(), "foo.component.spec.ts");
}
#[test]
fn cycle_reverse_goes_backwards() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
touch(dir.path(), "foo.component.html");
touch(dir.path(), "foo.component.scss");
touch(dir.path(), "foo.component.spec.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let prev = select(&cur, Mode::CycleBackward, None, &cfg).unwrap();
assert_eq!(prev.file_name().unwrap(), "foo.component.spec.ts");
}
#[test]
fn direct_to_spec() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
touch(dir.path(), "foo.component.spec.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let spec = select(&cur, Mode::Direct, Some("spec"), &cfg).unwrap();
assert_eq!(spec.file_name().unwrap(), "foo.component.spec.ts");
}
#[test]
fn direct_unknown_target_errors() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let err = select(&cur, Mode::Direct, Some("nope"), &cfg).unwrap_err();
assert!(matches!(err, SwitcherError::Input(_)));
}
#[test]
fn direct_missing_sibling_errors() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let err = select(&cur, Mode::Direct, Some("spec"), &cfg).unwrap_err();
match err {
SwitcherError::NoSibling { target } => assert_eq!(target.as_deref(), Some("spec")),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn style_preference_prefers_scss() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "foo.component.ts");
touch(dir.path(), "foo.component.css");
touch(dir.path(), "foo.component.scss");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let style = select(&cur, Mode::Direct, Some("style"), &cfg).unwrap();
assert_eq!(style.file_name().unwrap(), "foo.component.scss");
}
#[test]
fn no_siblings_returns_no_sibling_err() {
let dir = tempdir().unwrap();
let ts = touch(dir.path(), "lonely.component.ts");
let cfg = Config::default();
let cur = identify_current(&ts, &cfg).unwrap();
let err = select(&cur, Mode::CycleForward, None, &cfg).unwrap_err();
assert!(matches!(err, SwitcherError::NoSibling { .. }));
}
}