1#![forbid(unsafe_code)]
2
3use std::collections::HashMap;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct HelpId(pub u64);
46
47impl core::fmt::Display for HelpId {
48 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
49 write!(f, "help:{}", self.0)
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct Keybinding {
56 pub key: String,
58 pub action: String,
60}
61
62impl Keybinding {
63 #[must_use]
65 pub fn new(key: impl Into<String>, action: impl Into<String>) -> Self {
66 Self {
67 key: key.into(),
68 action: action.into(),
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct HelpContent {
76 pub short: String,
78 pub long: Option<String>,
80 pub keybindings: Vec<Keybinding>,
82 pub see_also: Vec<HelpId>,
84}
85
86impl HelpContent {
87 #[must_use]
89 pub fn short(desc: impl Into<String>) -> Self {
90 Self {
91 short: desc.into(),
92 long: None,
93 keybindings: Vec::new(),
94 see_also: Vec::new(),
95 }
96 }
97}
98
99type LazyProvider = Box<dyn FnOnce() -> HelpContent + Send>;
103
104enum Entry {
106 Loaded(HelpContent),
107 Lazy(LazyProvider),
108}
109
110impl core::fmt::Debug for Entry {
111 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
112 match self {
113 Self::Loaded(c) => f.debug_tuple("Loaded").field(c).finish(),
114 Self::Lazy(_) => f.debug_tuple("Lazy").field(&"<fn>").finish(),
115 }
116 }
117}
118
119#[derive(Debug)]
127pub struct HelpRegistry {
128 entries: HashMap<HelpId, Entry>,
129 parents: HashMap<HelpId, HelpId>,
132}
133
134impl Default for HelpRegistry {
135 fn default() -> Self {
136 Self::new()
137 }
138}
139
140impl HelpRegistry {
141 #[must_use]
143 pub fn new() -> Self {
144 Self {
145 entries: HashMap::new(),
146 parents: HashMap::new(),
147 }
148 }
149
150 pub fn register(&mut self, id: HelpId, content: HelpContent) {
154 self.entries.insert(id, Entry::Loaded(content));
155 }
156
157 pub fn register_lazy(
161 &mut self,
162 id: HelpId,
163 provider: impl FnOnce() -> HelpContent + Send + 'static,
164 ) {
165 self.entries.insert(id, Entry::Lazy(Box::new(provider)));
166 }
167
168 pub fn unregister(&mut self, id: HelpId) -> bool {
172 self.entries.remove(&id).is_some()
173 }
174
175 pub fn set_parent(&mut self, child: HelpId, parent: HelpId) -> bool {
183 if child == parent {
185 return false;
186 }
187 let mut cursor = parent;
188 while let Some(&ancestor) = self.parents.get(&cursor) {
189 if ancestor == child {
190 return false;
191 }
192 cursor = ancestor;
193 }
194 self.parents.insert(child, parent);
195 true
196 }
197
198 pub fn clear_parent(&mut self, child: HelpId) {
200 self.parents.remove(&child);
201 }
202
203 pub fn get(&mut self, id: HelpId) -> Option<&HelpContent> {
207 if matches!(self.entries.get(&id), Some(Entry::Lazy(_)))
209 && let Some(Entry::Lazy(provider)) = self.entries.remove(&id)
210 {
211 let content = provider();
212 self.entries.insert(id, Entry::Loaded(content));
213 }
214 match self.entries.get(&id) {
215 Some(Entry::Loaded(c)) => Some(c),
216 _ => None,
217 }
218 }
219
220 #[must_use]
222 pub fn peek(&self, id: HelpId) -> Option<&HelpContent> {
223 match self.entries.get(&id) {
224 Some(Entry::Loaded(c)) => Some(c),
225 _ => None,
226 }
227 }
228
229 pub fn resolve(&mut self, id: HelpId) -> Option<&HelpContent> {
234 let chain = self.ancestor_chain(id);
236 for &cid in &chain {
238 if matches!(self.entries.get(&cid), Some(Entry::Lazy(_)))
239 && let Some(Entry::Lazy(provider)) = self.entries.remove(&cid)
240 {
241 let content = provider();
242 self.entries.insert(cid, Entry::Loaded(content));
243 }
244 }
245 for &cid in &chain {
247 if let Some(Entry::Loaded(c)) = self.entries.get(&cid) {
248 return Some(c);
249 }
250 }
251 None
252 }
253
254 #[must_use]
256 pub fn contains(&self, id: HelpId) -> bool {
257 self.entries.contains_key(&id)
258 }
259
260 #[must_use]
262 pub fn len(&self) -> usize {
263 self.entries.len()
264 }
265
266 #[must_use]
268 pub fn is_empty(&self) -> bool {
269 self.entries.is_empty()
270 }
271
272 pub fn ids(&self) -> impl Iterator<Item = HelpId> + '_ {
274 self.entries.keys().copied()
275 }
276
277 fn ancestor_chain(&self, id: HelpId) -> Vec<HelpId> {
279 let mut chain = vec![id];
280 let mut cursor = id;
281 while let Some(&parent) = self.parents.get(&cursor) {
282 chain.push(parent);
283 cursor = parent;
284 }
285 chain
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 fn sample_content(short: &str) -> HelpContent {
294 HelpContent {
295 short: short.into(),
296 long: None,
297 keybindings: Vec::new(),
298 see_also: Vec::new(),
299 }
300 }
301
302 #[test]
305 fn register_and_get() {
306 let mut reg = HelpRegistry::new();
307 let id = HelpId(1);
308 reg.register(id, sample_content("tooltip"));
309 assert_eq!(reg.get(id).unwrap().short, "tooltip");
310 }
311
312 #[test]
313 fn missing_key_returns_none() {
314 let mut reg = HelpRegistry::new();
315 assert!(reg.get(HelpId(999)).is_none());
316 }
317
318 #[test]
319 fn register_overwrites() {
320 let mut reg = HelpRegistry::new();
321 let id = HelpId(1);
322 reg.register(id, sample_content("old"));
323 reg.register(id, sample_content("new"));
324 assert_eq!(reg.get(id).unwrap().short, "new");
325 }
326
327 #[test]
328 fn unregister() {
329 let mut reg = HelpRegistry::new();
330 let id = HelpId(1);
331 reg.register(id, sample_content("x"));
332 assert!(reg.unregister(id));
333 assert!(reg.get(id).is_none());
334 assert!(!reg.unregister(id));
335 }
336
337 #[test]
338 fn len_and_is_empty() {
339 let mut reg = HelpRegistry::new();
340 assert!(reg.is_empty());
341 assert_eq!(reg.len(), 0);
342 reg.register(HelpId(1), sample_content("a"));
343 reg.register(HelpId(2), sample_content("b"));
344 assert_eq!(reg.len(), 2);
345 assert!(!reg.is_empty());
346 }
347
348 #[test]
349 fn contains() {
350 let mut reg = HelpRegistry::new();
351 let id = HelpId(1);
352 assert!(!reg.contains(id));
353 reg.register(id, sample_content("x"));
354 assert!(reg.contains(id));
355 }
356
357 #[test]
358 fn ids_iteration() {
359 let mut reg = HelpRegistry::new();
360 reg.register(HelpId(10), sample_content("a"));
361 reg.register(HelpId(20), sample_content("b"));
362 let mut ids: Vec<_> = reg.ids().collect();
363 ids.sort_by_key(|h| h.0);
364 assert_eq!(ids, vec![HelpId(10), HelpId(20)]);
365 }
366
367 #[test]
370 fn lazy_provider_called_on_get() {
371 let mut reg = HelpRegistry::new();
372 let id = HelpId(1);
373 reg.register_lazy(id, || sample_content("lazy"));
374 assert!(reg.peek(id).is_none()); assert_eq!(reg.get(id).unwrap().short, "lazy");
376 assert!(reg.peek(id).is_some()); }
378
379 #[test]
380 fn lazy_provider_overwritten_by_register() {
381 let mut reg = HelpRegistry::new();
382 let id = HelpId(1);
383 reg.register_lazy(id, || sample_content("lazy"));
384 reg.register(id, sample_content("eager"));
385 assert_eq!(reg.get(id).unwrap().short, "eager");
386 }
387
388 #[test]
389 fn register_overwrites_lazy() {
390 let mut reg = HelpRegistry::new();
391 let id = HelpId(1);
392 reg.register_lazy(id, || sample_content("first"));
393 reg.register_lazy(id, || sample_content("second"));
394 assert_eq!(reg.get(id).unwrap().short, "second");
395 }
396
397 #[test]
400 fn resolve_walks_parents() {
401 let mut reg = HelpRegistry::new();
402 let child = HelpId(1);
403 let parent = HelpId(2);
404 let grandparent = HelpId(3);
405
406 reg.register(grandparent, sample_content("app help"));
407 reg.set_parent(child, parent);
408 reg.set_parent(parent, grandparent);
409
410 assert_eq!(reg.resolve(child).unwrap().short, "app help");
412 }
413
414 #[test]
415 fn resolve_prefers_nearest() {
416 let mut reg = HelpRegistry::new();
417 let child = HelpId(1);
418 let parent = HelpId(2);
419 let grandparent = HelpId(3);
420
421 reg.register(parent, sample_content("container help"));
422 reg.register(grandparent, sample_content("app help"));
423 reg.set_parent(child, parent);
424 reg.set_parent(parent, grandparent);
425
426 assert_eq!(reg.resolve(child).unwrap().short, "container help");
428 }
429
430 #[test]
431 fn resolve_returns_own_content_first() {
432 let mut reg = HelpRegistry::new();
433 let child = HelpId(1);
434 let parent = HelpId(2);
435
436 reg.register(child, sample_content("widget help"));
437 reg.register(parent, sample_content("container help"));
438 reg.set_parent(child, parent);
439
440 assert_eq!(reg.resolve(child).unwrap().short, "widget help");
441 }
442
443 #[test]
444 fn resolve_no_content_returns_none() {
445 let mut reg = HelpRegistry::new();
446 let child = HelpId(1);
447 let parent = HelpId(2);
448 reg.set_parent(child, parent);
449 assert!(reg.resolve(child).is_none());
450 }
451
452 #[test]
453 fn set_parent_rejects_self_cycle() {
454 let mut reg = HelpRegistry::new();
455 let id = HelpId(1);
456 assert!(!reg.set_parent(id, id));
457 }
458
459 #[test]
460 fn set_parent_rejects_indirect_cycle() {
461 let mut reg = HelpRegistry::new();
462 let a = HelpId(1);
463 let b = HelpId(2);
464 let c = HelpId(3);
465
466 assert!(reg.set_parent(a, b));
467 assert!(reg.set_parent(b, c));
468 assert!(!reg.set_parent(c, a));
470 }
471
472 #[test]
473 fn clear_parent() {
474 let mut reg = HelpRegistry::new();
475 let child = HelpId(1);
476 let parent = HelpId(2);
477
478 reg.register(parent, sample_content("parent"));
479 reg.set_parent(child, parent);
480 assert!(reg.resolve(child).is_some());
481
482 reg.clear_parent(child);
483 assert!(reg.resolve(child).is_none());
484 }
485
486 #[test]
489 fn keybindings_stored() {
490 let mut reg = HelpRegistry::new();
491 let id = HelpId(1);
492 reg.register(
493 id,
494 HelpContent {
495 short: "Editor".into(),
496 long: Some("Main text editor".into()),
497 keybindings: vec![
498 Keybinding::new("Ctrl+S", "Save"),
499 Keybinding::new("Ctrl+Q", "Quit"),
500 ],
501 see_also: vec![HelpId(2)],
502 },
503 );
504 let content = reg.get(id).unwrap();
505 assert_eq!(content.keybindings.len(), 2);
506 assert_eq!(content.keybindings[0].key, "Ctrl+S");
507 assert_eq!(content.keybindings[0].action, "Save");
508 assert_eq!(content.see_also, vec![HelpId(2)]);
509 }
510
511 #[test]
512 fn help_content_short_constructor() {
513 let c = HelpContent::short("tooltip");
514 assert_eq!(c.short, "tooltip");
515 assert!(c.long.is_none());
516 assert!(c.keybindings.is_empty());
517 assert!(c.see_also.is_empty());
518 }
519
520 #[test]
521 fn help_id_display() {
522 assert_eq!(HelpId(42).to_string(), "help:42");
523 }
524
525 #[test]
528 fn resolve_forces_lazy_in_parent() {
529 let mut reg = HelpRegistry::new();
530 let child = HelpId(1);
531 let parent = HelpId(2);
532
533 reg.register_lazy(parent, || sample_content("lazy parent"));
534 reg.set_parent(child, parent);
535
536 assert_eq!(reg.resolve(child).unwrap().short, "lazy parent");
537 assert!(reg.peek(parent).is_some());
539 }
540
541 #[test]
544 fn empty_registry_resolve() {
545 let mut reg = HelpRegistry::new();
546 assert!(reg.resolve(HelpId(1)).is_none());
547 }
548
549 #[test]
550 fn deep_hierarchy() {
551 let mut reg = HelpRegistry::new();
552 for i in 0..4u64 {
554 assert!(reg.set_parent(HelpId(i), HelpId(i + 1)));
555 }
556 reg.register(HelpId(4), sample_content("root"));
557 assert_eq!(reg.resolve(HelpId(0)).unwrap().short, "root");
558 }
559
560 #[test]
561 fn set_parent_allows_reparenting() {
562 let mut reg = HelpRegistry::new();
563 let child = HelpId(1);
564 let p1 = HelpId(2);
565 let p2 = HelpId(3);
566
567 reg.register(p1, sample_content("first parent"));
568 reg.register(p2, sample_content("second parent"));
569
570 reg.set_parent(child, p1);
571 assert_eq!(reg.resolve(child).unwrap().short, "first parent");
572
573 reg.set_parent(child, p2);
575 assert_eq!(reg.resolve(child).unwrap().short, "second parent");
576 }
577
578 #[test]
579 fn unregister_does_not_remove_parent_link() {
580 let mut reg = HelpRegistry::new();
581 let child = HelpId(1);
582 let parent = HelpId(2);
583 let grandparent = HelpId(3);
584
585 reg.register(parent, sample_content("parent"));
586 reg.register(grandparent, sample_content("grandparent"));
587 reg.set_parent(child, parent);
588 reg.set_parent(parent, grandparent);
589
590 reg.unregister(parent);
592 assert_eq!(reg.resolve(child).unwrap().short, "grandparent");
593 }
594}