artifactize_core/monitor/live/
query.rs1use super::{Branch, Place, Waiting};
16use crate::review::search::{self, Completion};
17use std::collections::BTreeSet;
18
19pub const DEFAULT: &str = "sort:recent";
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23enum Key {
24 Repo,
25 Branch,
26 Sort,
27}
28
29impl Key {
30 const ALL: [Self; 3] = [Self::Repo, Self::Branch, Self::Sort];
31
32 fn name(self) -> &'static str {
33 match self {
34 Self::Repo => "repo",
35 Self::Branch => "branch",
36 Self::Sort => "sort",
37 }
38 }
39
40 fn parse(name: &str) -> Option<Self> {
41 Self::ALL
42 .into_iter()
43 .find(|key| key.name().eq_ignore_ascii_case(name))
44 }
45}
46
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
49pub enum Sort {
50 #[default]
52 Recent,
53 Name,
55}
56
57impl Sort {
58 const ALL: [Self; 2] = [Self::Recent, Self::Name];
59
60 fn name(self) -> &'static str {
61 match self {
62 Self::Recent => "recent",
63 Self::Name => "name",
64 }
65 }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq)]
69enum Term {
70 Text(String),
72 Repo(String),
74 Branch(String),
76}
77
78#[derive(Debug, Clone, Default, PartialEq, Eq)]
80pub struct Query {
81 terms: Vec<Term>,
82 sort: Sort,
83}
84
85impl std::str::FromStr for Query {
86 type Err = String;
87
88 fn from_str(text: &str) -> Result<Self, String> {
89 let mut query = Self::default();
90 let mut sorted = false;
91 for (word, quoted) in search::words(text) {
92 let Some((name, value)) = word.split_once(':').filter(|_| !quoted) else {
93 query.terms.push(Term::Text(word.to_lowercase()));
94 continue;
95 };
96 let key = Key::parse(name).ok_or_else(|| {
97 format!(
98 "Unknown key `{name}:`; use repo:, branch: or sort:, or quote the word to search for it."
99 )
100 })?;
101 if value.is_empty() {
102 return Err(format!("`{}:` needs a value.", key.name()));
103 }
104 let lower = value.to_lowercase();
105 match key {
106 Key::Repo => query.terms.push(Term::Repo(lower)),
107 Key::Branch => query.terms.push(Term::Branch(lower)),
108 Key::Sort if sorted => return Err("Use one sort: term.".into()),
109 Key::Sort => {
110 query.sort = Sort::ALL
111 .into_iter()
112 .find(|sort| sort.name() == lower)
113 .ok_or_else(|| {
114 format!("Unknown order `sort:{value}`; use sort:recent or sort:name.")
115 })?;
116 sorted = true;
117 }
118 }
119 }
120 Ok(query)
121 }
122}
123
124fn contains(field: &str, text: &str) -> bool {
125 field.to_lowercase().contains(text)
126}
127
128impl Query {
129 pub fn sort(&self) -> Sort {
130 self.sort
131 }
132
133 fn matches(&self, project: &str, branch: &str, eval: Option<&str>) -> bool {
134 self.terms.iter().all(|term| match term {
135 Term::Text(text) => [Some(project), Some(branch), eval]
136 .into_iter()
137 .flatten()
138 .any(|field| contains(field, text)),
139 Term::Repo(text) => contains(project, text),
140 Term::Branch(text) => contains(branch, text),
141 })
142 }
143
144 pub fn branch(&self, branch: &Branch) -> bool {
145 self.matches(&branch.project, &branch.name, None)
146 }
147
148 pub fn waiting(&self, waiting: &Waiting) -> bool {
149 self.matches(
150 &waiting.label.project,
151 &waiting.label.branch,
152 Some(&waiting.entry.eval),
153 )
154 }
155}
156
157fn compare(sort: Sort, a: &Branch, b: &Branch) -> std::cmp::Ordering {
159 let names = (&a.project, &a.name, &a.place).cmp(&(&b.project, &b.name, &b.place));
160 match sort {
161 Sort::Recent => b.latest().cmp(&a.latest()).then(names),
163 Sort::Name => names,
164 }
165}
166
167pub(super) fn order(branches: &mut [Branch], sort: Sort) {
169 branches.sort_by(|a, b| compare(sort, a, b));
170}
171
172pub fn show<'a>(
175 query: &Query,
176 branches: &'a [Branch],
177 waiting: &'a [Waiting],
178 starred: &BTreeSet<Place>,
179) -> (Vec<&'a Branch>, Vec<&'a Waiting>) {
180 let mut kept: Vec<&Branch> = branches
181 .iter()
182 .filter(|branch| query.branch(branch))
183 .collect();
184 let first = |branch: &Branch| !starred.contains(&branch.place);
185 kept.sort_by(|a, b| {
186 first(a)
187 .cmp(&first(b))
188 .then_with(|| compare(query.sort, a, b))
189 });
190 let waiting = waiting
191 .iter()
192 .filter(|waiting| query.waiting(waiting))
193 .collect();
194 (kept, waiting)
195}
196
197pub fn complete(text: &str, branches: &[Branch], waiting: &[Waiting]) -> Completion {
200 let (keep, term) = search::last_term(text);
201 let candidates = match term.split_once(':').filter(|_| !term.starts_with('"')) {
202 None if term.starts_with('"') => Vec::new(),
204 None => {
205 let lower = term.to_lowercase();
206 Key::ALL
207 .iter()
208 .map(|key| format!("{}:", key.name()))
209 .filter(|key| key.starts_with(&lower))
210 .collect()
211 }
212 Some((name, partial)) => match Key::parse(name) {
213 None => Vec::new(),
214 Some(key) => {
215 let partial = partial.trim_matches('"').to_lowercase();
216 let values: BTreeSet<String> = match key {
217 Key::Repo => branches
218 .iter()
219 .map(|branch| branch.project.clone())
220 .chain(waiting.iter().map(|waiting| waiting.label.project.clone()))
221 .collect(),
222 Key::Branch => branches
223 .iter()
224 .map(|branch| branch.name.clone())
225 .chain(waiting.iter().map(|waiting| waiting.label.branch.clone()))
226 .collect(),
227 Key::Sort => Sort::ALL
228 .iter()
229 .map(|sort| sort.name().to_owned())
230 .collect(),
231 };
232 values
233 .into_iter()
234 .filter(|value| !value.is_empty() && value.to_lowercase().contains(&partial))
235 .map(|value| search::quote(key.name(), &value))
236 .collect()
237 }
238 },
239 };
240 Completion {
241 keep: keep.to_owned(),
242 candidates,
243 }
244}