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 #[must_use = "use the returned help content (if any)"]
207 pub fn get(&mut self, id: HelpId) -> Option<&HelpContent> {
208 if matches!(self.entries.get(&id), Some(Entry::Lazy(_)))
210 && let Some(Entry::Lazy(provider)) = self.entries.remove(&id)
211 {
212 let content = provider();
213 self.entries.insert(id, Entry::Loaded(content));
214 }
215 match self.entries.get(&id) {
216 Some(Entry::Loaded(c)) => Some(c),
217 _ => None,
218 }
219 }
220
221 #[must_use = "use the returned help content (if any)"]
223 pub fn peek(&self, id: HelpId) -> Option<&HelpContent> {
224 match self.entries.get(&id) {
225 Some(Entry::Loaded(c)) => Some(c),
226 _ => None,
227 }
228 }
229
230 #[must_use = "use the returned help content (if any)"]
235 pub fn resolve(&mut self, id: HelpId) -> Option<&HelpContent> {
236 let chain = self.ancestor_chain(id);
238 for &cid in &chain {
240 if matches!(self.entries.get(&cid), Some(Entry::Lazy(_)))
241 && let Some(Entry::Lazy(provider)) = self.entries.remove(&cid)
242 {
243 let content = provider();
244 self.entries.insert(cid, Entry::Loaded(content));
245 }
246 }
247 for &cid in &chain {
249 if let Some(Entry::Loaded(c)) = self.entries.get(&cid) {
250 return Some(c);
251 }
252 }
253 None
254 }
255
256 #[must_use]
258 pub fn contains(&self, id: HelpId) -> bool {
259 self.entries.contains_key(&id)
260 }
261
262 #[inline]
264 #[must_use]
265 pub fn len(&self) -> usize {
266 self.entries.len()
267 }
268
269 #[inline]
271 #[must_use]
272 pub fn is_empty(&self) -> bool {
273 self.entries.is_empty()
274 }
275
276 pub fn ids(&self) -> impl Iterator<Item = HelpId> + '_ {
278 self.entries.keys().copied()
279 }
280
281 fn ancestor_chain(&self, id: HelpId) -> Vec<HelpId> {
283 let mut chain = vec![id];
284 let mut cursor = id;
285 while let Some(&parent) = self.parents.get(&cursor) {
286 chain.push(parent);
287 cursor = parent;
288 }
289 chain
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 fn sample_content(short: &str) -> HelpContent {
298 HelpContent {
299 short: short.into(),
300 long: None,
301 keybindings: Vec::new(),
302 see_also: Vec::new(),
303 }
304 }
305
306 #[test]
309 fn register_and_get() {
310 let mut reg = HelpRegistry::new();
311 let id = HelpId(1);
312 reg.register(id, sample_content("tooltip"));
313 assert_eq!(reg.get(id).unwrap().short, "tooltip");
314 }
315
316 #[test]
317 fn missing_key_returns_none() {
318 let mut reg = HelpRegistry::new();
319 assert!(reg.get(HelpId(999)).is_none());
320 }
321
322 #[test]
323 fn register_overwrites() {
324 let mut reg = HelpRegistry::new();
325 let id = HelpId(1);
326 reg.register(id, sample_content("old"));
327 reg.register(id, sample_content("new"));
328 assert_eq!(reg.get(id).unwrap().short, "new");
329 }
330
331 #[test]
332 fn unregister() {
333 let mut reg = HelpRegistry::new();
334 let id = HelpId(1);
335 reg.register(id, sample_content("x"));
336 assert!(reg.unregister(id));
337 assert!(reg.get(id).is_none());
338 assert!(!reg.unregister(id));
339 }
340
341 #[test]
342 fn len_and_is_empty() {
343 let mut reg = HelpRegistry::new();
344 assert!(reg.is_empty());
345 assert_eq!(reg.len(), 0);
346 reg.register(HelpId(1), sample_content("a"));
347 reg.register(HelpId(2), sample_content("b"));
348 assert_eq!(reg.len(), 2);
349 assert!(!reg.is_empty());
350 }
351
352 #[test]
353 fn contains() {
354 let mut reg = HelpRegistry::new();
355 let id = HelpId(1);
356 assert!(!reg.contains(id));
357 reg.register(id, sample_content("x"));
358 assert!(reg.contains(id));
359 }
360
361 #[test]
362 fn ids_iteration() {
363 let mut reg = HelpRegistry::new();
364 reg.register(HelpId(10), sample_content("a"));
365 reg.register(HelpId(20), sample_content("b"));
366 let mut ids: Vec<_> = reg.ids().collect();
367 ids.sort_by_key(|h| h.0);
368 assert_eq!(ids, vec![HelpId(10), HelpId(20)]);
369 }
370
371 #[test]
374 fn lazy_provider_called_on_get() {
375 let mut reg = HelpRegistry::new();
376 let id = HelpId(1);
377 reg.register_lazy(id, || sample_content("lazy"));
378 assert!(reg.peek(id).is_none()); assert_eq!(reg.get(id).unwrap().short, "lazy");
380 assert!(reg.peek(id).is_some()); }
382
383 #[test]
384 fn lazy_provider_overwritten_by_register() {
385 let mut reg = HelpRegistry::new();
386 let id = HelpId(1);
387 reg.register_lazy(id, || sample_content("lazy"));
388 reg.register(id, sample_content("eager"));
389 assert_eq!(reg.get(id).unwrap().short, "eager");
390 }
391
392 #[test]
393 fn register_overwrites_lazy() {
394 let mut reg = HelpRegistry::new();
395 let id = HelpId(1);
396 reg.register_lazy(id, || sample_content("first"));
397 reg.register_lazy(id, || sample_content("second"));
398 assert_eq!(reg.get(id).unwrap().short, "second");
399 }
400
401 #[test]
404 fn resolve_walks_parents() {
405 let mut reg = HelpRegistry::new();
406 let child = HelpId(1);
407 let parent = HelpId(2);
408 let grandparent = HelpId(3);
409
410 reg.register(grandparent, sample_content("app help"));
411 reg.set_parent(child, parent);
412 reg.set_parent(parent, grandparent);
413
414 assert_eq!(reg.resolve(child).unwrap().short, "app help");
416 }
417
418 #[test]
419 fn resolve_prefers_nearest() {
420 let mut reg = HelpRegistry::new();
421 let child = HelpId(1);
422 let parent = HelpId(2);
423 let grandparent = HelpId(3);
424
425 reg.register(parent, sample_content("container help"));
426 reg.register(grandparent, sample_content("app help"));
427 reg.set_parent(child, parent);
428 reg.set_parent(parent, grandparent);
429
430 assert_eq!(reg.resolve(child).unwrap().short, "container help");
432 }
433
434 #[test]
435 fn resolve_returns_own_content_first() {
436 let mut reg = HelpRegistry::new();
437 let child = HelpId(1);
438 let parent = HelpId(2);
439
440 reg.register(child, sample_content("widget help"));
441 reg.register(parent, sample_content("container help"));
442 reg.set_parent(child, parent);
443
444 assert_eq!(reg.resolve(child).unwrap().short, "widget help");
445 }
446
447 #[test]
448 fn resolve_no_content_returns_none() {
449 let mut reg = HelpRegistry::new();
450 let child = HelpId(1);
451 let parent = HelpId(2);
452 reg.set_parent(child, parent);
453 assert!(reg.resolve(child).is_none());
454 }
455
456 #[test]
457 fn set_parent_rejects_self_cycle() {
458 let mut reg = HelpRegistry::new();
459 let id = HelpId(1);
460 assert!(!reg.set_parent(id, id));
461 }
462
463 #[test]
464 fn set_parent_rejects_indirect_cycle() {
465 let mut reg = HelpRegistry::new();
466 let a = HelpId(1);
467 let b = HelpId(2);
468 let c = HelpId(3);
469
470 assert!(reg.set_parent(a, b));
471 assert!(reg.set_parent(b, c));
472 assert!(!reg.set_parent(c, a));
474 }
475
476 #[test]
477 fn clear_parent() {
478 let mut reg = HelpRegistry::new();
479 let child = HelpId(1);
480 let parent = HelpId(2);
481
482 reg.register(parent, sample_content("parent"));
483 reg.set_parent(child, parent);
484 assert!(reg.resolve(child).is_some());
485
486 reg.clear_parent(child);
487 assert!(reg.resolve(child).is_none());
488 }
489
490 #[test]
493 fn keybindings_stored() {
494 let mut reg = HelpRegistry::new();
495 let id = HelpId(1);
496 reg.register(
497 id,
498 HelpContent {
499 short: "Editor".into(),
500 long: Some("Main text editor".into()),
501 keybindings: vec![
502 Keybinding::new("Ctrl+S", "Save"),
503 Keybinding::new("Ctrl+Q", "Quit"),
504 ],
505 see_also: vec![HelpId(2)],
506 },
507 );
508 let content = reg.get(id).unwrap();
509 assert_eq!(content.keybindings.len(), 2);
510 assert_eq!(content.keybindings[0].key, "Ctrl+S");
511 assert_eq!(content.keybindings[0].action, "Save");
512 assert_eq!(content.see_also, vec![HelpId(2)]);
513 }
514
515 #[test]
516 fn help_content_short_constructor() {
517 let c = HelpContent::short("tooltip");
518 assert_eq!(c.short, "tooltip");
519 assert!(c.long.is_none());
520 assert!(c.keybindings.is_empty());
521 assert!(c.see_also.is_empty());
522 }
523
524 #[test]
525 fn help_id_display() {
526 assert_eq!(HelpId(42).to_string(), "help:42");
527 }
528
529 #[test]
532 fn resolve_forces_lazy_in_parent() {
533 let mut reg = HelpRegistry::new();
534 let child = HelpId(1);
535 let parent = HelpId(2);
536
537 reg.register_lazy(parent, || sample_content("lazy parent"));
538 reg.set_parent(child, parent);
539
540 assert_eq!(reg.resolve(child).unwrap().short, "lazy parent");
541 assert!(reg.peek(parent).is_some());
543 }
544
545 #[test]
548 fn empty_registry_resolve() {
549 let mut reg = HelpRegistry::new();
550 assert!(reg.resolve(HelpId(1)).is_none());
551 }
552
553 #[test]
554 fn deep_hierarchy() {
555 let mut reg = HelpRegistry::new();
556 for i in 0..4u64 {
558 assert!(reg.set_parent(HelpId(i), HelpId(i + 1)));
559 }
560 reg.register(HelpId(4), sample_content("root"));
561 assert_eq!(reg.resolve(HelpId(0)).unwrap().short, "root");
562 }
563
564 #[test]
565 fn set_parent_allows_reparenting() {
566 let mut reg = HelpRegistry::new();
567 let child = HelpId(1);
568 let p1 = HelpId(2);
569 let p2 = HelpId(3);
570
571 reg.register(p1, sample_content("first parent"));
572 reg.register(p2, sample_content("second parent"));
573
574 reg.set_parent(child, p1);
575 assert_eq!(reg.resolve(child).unwrap().short, "first parent");
576
577 reg.set_parent(child, p2);
579 assert_eq!(reg.resolve(child).unwrap().short, "second parent");
580 }
581
582 #[test]
583 fn unregister_does_not_remove_parent_link() {
584 let mut reg = HelpRegistry::new();
585 let child = HelpId(1);
586 let parent = HelpId(2);
587 let grandparent = HelpId(3);
588
589 reg.register(parent, sample_content("parent"));
590 reg.register(grandparent, sample_content("grandparent"));
591 reg.set_parent(child, parent);
592 reg.set_parent(parent, grandparent);
593
594 reg.unregister(parent);
596 assert_eq!(reg.resolve(child).unwrap().short, "grandparent");
597 }
598}