1use crate::model::{Config, ExpandResult};
2
3#[derive(Debug, Clone, PartialEq)]
5pub enum SkipReason {
6 SelfLoop,
8 ConditionFailed {
10 found_commands: Vec<String>,
11 missing_commands: Vec<String>,
12 },
13}
14
15#[derive(Debug, Clone, PartialEq)]
21pub enum WhichResult {
22 Expanded {
24 key: String,
25 expansion: String,
26 rule_index: usize,
27 satisfied_conditions: Vec<String>,
29 skipped: Vec<(usize, SkipReason)>,
31 },
32 AllSkipped {
34 token: String,
35 skipped: Vec<(usize, SkipReason)>,
36 },
37 NoMatch { token: String },
39}
40
41pub fn expand<F>(config: &Config, token: &str, command_exists: F) -> ExpandResult
45where
46 F: Fn(&str) -> bool,
47{
48 for abbr in &config.abbr {
49 if abbr.key != token {
50 continue;
51 }
52 if abbr.key == abbr.expand {
54 continue;
55 }
56 if let Some(cmds) = &abbr.when_command_exists {
58 if !cmds.iter().all(|c| command_exists(c)) {
59 continue;
60 }
61 }
62 return ExpandResult::Expanded(abbr.expand.clone());
63 }
64 ExpandResult::PassThrough(token.to_string())
65}
66
67pub fn which_abbr<F>(config: &Config, token: &str, command_exists: F) -> WhichResult
74where
75 F: Fn(&str) -> bool,
76{
77 let mut skipped: Vec<(usize, SkipReason)> = Vec::new();
78 let mut any_key_matched = false;
79
80 for (i, abbr) in config.abbr.iter().enumerate() {
81 if abbr.key != token {
82 continue;
83 }
84 any_key_matched = true;
85
86 if abbr.key == abbr.expand {
87 skipped.push((i, SkipReason::SelfLoop));
88 continue;
89 }
90 if let Some(cmds) = &abbr.when_command_exists {
91 let (found, missing): (Vec<String>, Vec<String>) =
92 cmds.iter().cloned().partition(|c| command_exists(c));
93 if !missing.is_empty() {
94 skipped.push((
95 i,
96 SkipReason::ConditionFailed {
97 found_commands: found,
98 missing_commands: missing,
99 },
100 ));
101 continue;
102 }
103 }
104 return WhichResult::Expanded {
105 key: abbr.key.clone(),
106 expansion: abbr.expand.clone(),
107 rule_index: i,
108 satisfied_conditions: abbr
109 .when_command_exists
110 .as_deref()
111 .unwrap_or(&[])
112 .to_vec(),
113 skipped,
114 };
115 }
116
117 if any_key_matched {
118 WhichResult::AllSkipped {
119 token: token.to_string(),
120 skipped,
121 }
122 } else {
123 WhichResult::NoMatch {
124 token: token.to_string(),
125 }
126 }
127}
128
129pub fn list(config: &Config) -> Vec<(&str, &str)> {
131 config
132 .abbr
133 .iter()
134 .map(|a| (a.key.as_str(), a.expand.as_str()))
135 .collect()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::model::{Abbr, Config};
142
143 fn cfg(abbrs: Vec<Abbr>) -> Config {
144 Config {
145 version: 1,
146 keybind: crate::model::KeybindConfig::default(),
147 abbr: abbrs,
148 }
149 }
150
151 fn abbr(key: &str, expand: &str) -> Abbr {
152 Abbr {
153 key: key.into(),
154 expand: expand.into(),
155 when_command_exists: None,
156 }
157 }
158
159 fn abbr_when(key: &str, exp: &str, cmds: Vec<&str>) -> Abbr {
160 Abbr {
161 key: key.into(),
162 expand: exp.into(),
163 when_command_exists: Some(cmds.into_iter().map(String::from).collect()),
164 }
165 }
166
167 #[test]
168 fn match_expands() {
169 let c = cfg(vec![abbr("gcm", "git commit -m")]);
170 assert_eq!(
171 expand(&c, "gcm", |_| true),
172 ExpandResult::Expanded("git commit -m".into())
173 );
174 }
175
176 #[test]
177 fn no_match_passes_through() {
178 let c = cfg(vec![abbr("gcm", "git commit -m")]);
179 assert_eq!(
180 expand(&c, "xyz", |_| true),
181 ExpandResult::PassThrough("xyz".into())
182 );
183 }
184
185 #[test]
186 fn selects_correct_abbr() {
187 let c = cfg(vec![
188 abbr("gcm", "git commit -m"),
189 abbr("gp", "git push"),
190 ]);
191 assert_eq!(
192 expand(&c, "gp", |_| true),
193 ExpandResult::Expanded("git push".into())
194 );
195 }
196
197 #[test]
198 fn key_eq_expand_passes_through() {
199 let c = cfg(vec![abbr("ls", "ls")]);
200 assert_eq!(
201 expand(&c, "ls", |_| true),
202 ExpandResult::PassThrough("ls".into())
203 );
204 }
205
206 #[test]
207 fn when_command_exists_present() {
208 let c = cfg(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
209 assert_eq!(
210 expand(&c, "ls", |_| true),
211 ExpandResult::Expanded("lsd".into())
212 );
213 }
214
215 #[test]
216 fn when_command_exists_absent() {
217 let c = cfg(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
218 assert_eq!(
219 expand(&c, "ls", |_| false),
220 ExpandResult::PassThrough("ls".into())
221 );
222 }
223
224 #[test]
225 fn duplicate_key_self_loop_then_real_expands() {
226 let c = cfg(vec![abbr("ls", "ls"), abbr("ls", "lsd")]);
228 assert_eq!(expand(&c, "ls", |_| true), ExpandResult::Expanded("lsd".into()));
229 }
230
231 #[test]
232 fn duplicate_key_failed_condition_then_real_expands() {
233 let c = cfg(vec![abbr_when("ls", "lsd", vec!["lsd"]), abbr("ls", "ls2")]);
235 assert_eq!(expand(&c, "ls", |_| false), ExpandResult::Expanded("ls2".into()));
236 }
237
238 #[test]
239 fn which_abbr_duplicate_self_loop_then_expanded() {
240 let c = cfg(vec![abbr("ls", "ls"), abbr("ls", "lsd")]);
241 let result = which_abbr(&c, "ls", |_| true);
242 match result {
243 WhichResult::Expanded { expansion, skipped, .. } => {
244 assert_eq!(expansion, "lsd");
245 assert_eq!(skipped.len(), 1);
246 assert_eq!(skipped[0].0, 0); assert!(matches!(skipped[0].1, SkipReason::SelfLoop));
248 }
249 other => panic!("expected Expanded, got {other:?}"),
250 }
251 }
252
253 #[test]
254 fn which_abbr_all_skipped_returns_all_skipped() {
255 let c = cfg(vec![abbr_when("ls", "lsd", vec!["lsd"])]);
256 let result = which_abbr(&c, "ls", |_| false);
257 match result {
258 WhichResult::AllSkipped { skipped, .. } => {
259 assert_eq!(skipped.len(), 1);
260 assert!(matches!(
261 &skipped[0].1,
262 SkipReason::ConditionFailed { missing_commands, .. }
263 if missing_commands == &["lsd"]
264 ));
265 }
266 other => panic!("expected AllSkipped, got {other:?}"),
267 }
268 }
269
270 #[test]
271 fn which_abbr_no_match() {
272 let c = cfg(vec![abbr("gcm", "git commit -m")]);
273 assert!(matches!(which_abbr(&c, "xyz", |_| true), WhichResult::NoMatch { .. }));
274 }
275
276 #[test]
277 fn list_returns_all_pairs() {
278 let c = cfg(vec![
279 abbr("gcm", "git commit -m"),
280 abbr("gp", "git push"),
281 ]);
282 let pairs = list(&c);
283 assert_eq!(pairs, vec![("gcm", "git commit -m"), ("gp", "git push")]);
284 }
285}