1use crate::materialize::World;
5
6const MIN_SHORT: usize = 4;
7
8pub fn short_id(world: &World, id: &str) -> String {
11 let mut len = MIN_SHORT;
12 while len < id.len() {
13 let prefix = &id[..len];
14 let collisions = world
15 .tasks
16 .keys()
17 .filter(|other| other.as_str() != id && other.starts_with(prefix))
18 .count();
19 if collisions == 0 {
20 return format!("lv-{prefix}");
21 }
22 len += 2;
23 }
24 format!("lv-{id}")
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum PrefixError {
29 NotFound(String),
30 Ambiguous(String, Vec<String>),
31}
32
33impl std::fmt::Display for PrefixError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 PrefixError::NotFound(p) => write!(f, "no task matches '{p}'"),
37 PrefixError::Ambiguous(p, ids) => {
38 write!(f, "'{p}' is ambiguous: matches {}", ids.join(", "))
39 }
40 }
41 }
42}
43
44impl std::error::Error for PrefixError {}
45
46pub fn resolve_prefix<'w>(world: &'w World, input: &str) -> Result<&'w str, PrefixError> {
48 let prefix = input.strip_prefix("lv-").unwrap_or(input);
49 let mut matches: Vec<&str> = world
50 .tasks
51 .keys()
52 .filter(|id| id.starts_with(prefix))
53 .map(String::as_str)
54 .collect();
55 match matches.len() {
56 0 => Err(PrefixError::NotFound(input.to_string())),
57 1 => Ok(matches.remove(0)),
58 _ => Err(PrefixError::Ambiguous(
59 input.to_string(),
60 matches.iter().map(|id| short_id_of(id)).collect(),
61 )),
62 }
63}
64
65fn short_id_of(id: &str) -> String {
66 format!("lv-{}", &id[..id.len().min(8)])
67}