1use crate::effect::ExEffect;
2use hjkl_engine::Host;
3
4#[derive(Copy, Clone, Debug, Eq, PartialEq)]
6pub enum ArgKind {
7 None,
8 Path,
9 Buffer,
10 Setting,
11 Register,
12 Mark,
13 Raw,
14}
15
16pub type ExHandler<H> = fn(
26 &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
27 &str,
28 Option<crate::range::LineRange>,
29) -> Option<ExEffect>;
30
31pub struct ExCommand<H: Host> {
33 pub name: &'static str,
35 pub aliases: &'static [&'static str],
37 pub arg_kind: ArgKind,
39 pub min_prefix: usize,
41 pub run: ExHandler<H>,
43}
44
45pub struct Registry<H: Host> {
47 cmds: Vec<ExCommand<H>>,
48}
49
50impl<H: Host> Registry<H> {
51 pub fn new() -> Self {
52 Self { cmds: Vec::new() }
53 }
54
55 pub fn add(&mut self, cmd: ExCommand<H>) -> &mut Self {
57 self.cmds.push(cmd);
58 self
59 }
60
61 pub fn resolve(&self, name: &str) -> Option<&ExCommand<H>> {
68 if name.is_empty() {
69 return None;
70 }
71 if let Some(cmd) = self.cmds.iter().find(|c| c.name == name) {
73 return Some(cmd);
74 }
75 if let Some(cmd) = self.cmds.iter().find(|c| c.aliases.contains(&name)) {
77 return Some(cmd);
78 }
79 let candidates: Vec<&ExCommand<H>> = self
81 .cmds
82 .iter()
83 .filter(|c| c.name.starts_with(name) && name.len() >= c.min_prefix)
84 .collect();
85 if candidates.len() == 1 {
86 Some(candidates[0])
87 } else {
88 None
89 }
90 }
91
92 pub fn iter(&self) -> impl Iterator<Item = &ExCommand<H>> {
94 self.cmds.iter()
95 }
96}
97
98impl<H: Host> Default for Registry<H> {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104pub trait HostCmd<Ctx>: Send + Sync {
117 fn name(&self) -> &'static str;
118 fn aliases(&self) -> &'static [&'static str] {
119 &[]
120 }
121 fn min_prefix(&self) -> usize {
122 1
123 }
124 fn arg_kind(&self) -> ArgKind {
125 ArgKind::None
126 }
127 fn run(&self, ctx: &mut Ctx, args: &str) -> Option<crate::effect::ExEffect>;
129}
130
131pub struct HostRegistry<Ctx> {
133 cmds: Vec<Box<dyn HostCmd<Ctx>>>,
134}
135
136impl<Ctx> HostRegistry<Ctx> {
137 pub fn new() -> Self {
138 Self { cmds: Vec::new() }
139 }
140
141 pub fn add(&mut self, cmd: Box<dyn HostCmd<Ctx>>) -> &mut Self {
143 self.cmds.push(cmd);
144 self
145 }
146
147 pub fn resolve(&self, name: &str) -> Option<&dyn HostCmd<Ctx>> {
154 if name.is_empty() {
155 return None;
156 }
157 if let Some(c) = self.cmds.iter().find(|c| c.name() == name) {
159 return Some(c.as_ref());
160 }
161 if let Some(c) = self.cmds.iter().find(|c| c.aliases().contains(&name)) {
163 return Some(c.as_ref());
164 }
165 let candidates: Vec<&dyn HostCmd<Ctx>> = self
167 .cmds
168 .iter()
169 .filter(|c| c.name().starts_with(name) && name.len() >= c.min_prefix())
170 .map(|c| c.as_ref())
171 .collect();
172 if candidates.len() == 1 {
173 Some(candidates[0])
174 } else {
175 None
176 }
177 }
178
179 pub fn iter(&self) -> impl Iterator<Item = &dyn HostCmd<Ctx>> {
181 self.cmds.iter().map(|c| c.as_ref())
182 }
183}
184
185impl<Ctx> Default for HostRegistry<Ctx> {
186 fn default() -> Self {
187 Self::new()
188 }
189}
190
191#[cfg(test)]
192mod host_registry_tests {
193 use super::*;
194 use crate::effect::ExEffect;
195
196 struct TestCtx {
197 value: i32,
198 }
199
200 struct IncrCmd;
201 impl HostCmd<TestCtx> for IncrCmd {
202 fn name(&self) -> &'static str {
203 "increment"
204 }
205 fn aliases(&self) -> &'static [&'static str] {
206 &["incr"]
207 }
208 fn min_prefix(&self) -> usize {
209 3
210 }
211 fn run(&self, ctx: &mut TestCtx, _args: &str) -> Option<ExEffect> {
212 ctx.value += 1;
213 Some(ExEffect::Ok)
214 }
215 }
216
217 struct ArgCmd;
218 impl HostCmd<TestCtx> for ArgCmd {
219 fn name(&self) -> &'static str {
220 "argcmd"
221 }
222 fn min_prefix(&self) -> usize {
223 6
224 }
225 fn run(&self, _ctx: &mut TestCtx, args: &str) -> Option<ExEffect> {
226 if args.is_empty() {
227 None
228 } else {
229 Some(ExEffect::Info(args.to_string()))
230 }
231 }
232 }
233
234 fn make_registry() -> HostRegistry<TestCtx> {
235 let mut reg = HostRegistry::new();
236 reg.add(Box::new(IncrCmd));
237 reg.add(Box::new(ArgCmd));
238 reg
239 }
240
241 #[test]
242 fn resolve_exact_name() {
243 let reg = make_registry();
244 assert!(reg.resolve("increment").is_some());
245 assert!(reg.resolve("argcmd").is_some());
246 }
247
248 #[test]
249 fn resolve_exact_alias() {
250 let reg = make_registry();
251 assert!(reg.resolve("incr").is_some());
252 }
253
254 #[test]
255 fn resolve_prefix() {
256 let reg = make_registry();
257 assert!(reg.resolve("inc").is_some());
259 assert_eq!(reg.resolve("inc").unwrap().name(), "increment");
260 }
261
262 #[test]
263 fn resolve_prefix_too_short() {
264 let reg = make_registry();
265 assert!(reg.resolve("in").is_none());
267 }
268
269 #[test]
270 fn resolve_unknown_returns_none() {
271 let reg = make_registry();
272 assert!(reg.resolve("nonexistent").is_none());
273 assert!(reg.resolve("").is_none());
274 }
275
276 #[test]
277 fn run_mutates_context() {
278 let reg = make_registry();
279 let mut ctx = TestCtx { value: 0 };
280 let cmd = reg.resolve("increment").unwrap();
281 let eff = cmd.run(&mut ctx, "");
282 assert_eq!(eff, Some(ExEffect::Ok));
283 assert_eq!(ctx.value, 1);
284 }
285
286 #[test]
287 fn run_returns_none_to_defer() {
288 let reg = make_registry();
289 let mut ctx = TestCtx { value: 0 };
290 let cmd = reg.resolve("argcmd").unwrap();
291 let eff = cmd.run(&mut ctx, "");
293 assert!(eff.is_none());
294 let eff2 = cmd.run(&mut ctx, "hello");
296 assert_eq!(eff2, Some(ExEffect::Info("hello".to_string())));
297 }
298
299 #[test]
300 fn iter_yields_all_commands() {
301 let reg = make_registry();
302 let names: Vec<&str> = reg.iter().map(|c| c.name()).collect();
303 assert!(names.contains(&"increment"));
304 assert!(names.contains(&"argcmd"));
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use crate::effect::ExEffect;
312 use hjkl_engine::{DefaultHost, Editor};
313
314 fn noop_handler(
315 _editor: &mut Editor<hjkl_buffer::Buffer, DefaultHost>,
316 _args: &str,
317 _range: Option<crate::range::LineRange>,
318 ) -> Option<ExEffect> {
319 Some(ExEffect::Ok)
320 }
321
322 fn make_registry() -> Registry<DefaultHost> {
323 let mut reg = Registry::new();
324 reg.add(ExCommand {
325 name: "quit",
326 aliases: &["quit!"],
327 arg_kind: ArgKind::None,
328 min_prefix: 1,
329 run: noop_handler,
330 });
331 reg.add(ExCommand {
332 name: "write",
333 aliases: &[],
334 arg_kind: ArgKind::Path,
335 min_prefix: 1,
336 run: noop_handler,
337 });
338 reg
339 }
340
341 #[test]
342 fn resolve_exact_name() {
343 let reg = make_registry();
344 assert!(reg.resolve("quit").is_some());
345 assert!(reg.resolve("write").is_some());
346 }
347
348 #[test]
349 fn resolve_exact_alias() {
350 let reg = make_registry();
351 assert!(reg.resolve("quit!").is_some());
352 }
353
354 #[test]
355 fn resolve_prefix() {
356 let reg = make_registry();
357 assert!(reg.resolve("q").is_some());
359 assert!(reg.resolve("w").is_some());
360 }
361
362 #[test]
363 fn resolve_prefix_too_short() {
364 let mut reg = Registry::<DefaultHost>::new();
365 reg.add(ExCommand {
366 name: "quit",
367 aliases: &[],
368 arg_kind: ArgKind::None,
369 min_prefix: 2,
370 run: noop_handler,
371 });
372 assert!(reg.resolve("q").is_none());
374 assert!(reg.resolve("qu").is_some());
376 }
377
378 #[test]
379 fn resolve_unknown_returns_none() {
380 let reg = make_registry();
381 assert!(reg.resolve("nonexistent").is_none());
382 assert!(reg.resolve("").is_none());
383 }
384
385 #[test]
386 fn add_command_works() {
387 let mut reg = Registry::<DefaultHost>::new();
388 reg.add(ExCommand {
389 name: "test",
390 aliases: &[],
391 arg_kind: ArgKind::Raw,
392 min_prefix: 2,
393 run: noop_handler,
394 });
395 assert!(reg.resolve("test").is_some());
396 assert!(reg.resolve("te").is_some());
397 assert!(reg.resolve("t").is_none());
398 }
399}