1use crate::MachineBuilder;
20use crate::console::{AnsiColor, Console, Pager, refill_and_page};
21use crate::exec::CATEGORY;
22use async_trait::async_trait;
23use endbasic_core::{
24 ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
25 ExprType, RequiredValueSyntax, Scope, SingularArgSyntax, SymbolKey,
26};
27use radix_trie::{Trie, TrieCommon};
28use std::borrow::Cow;
29use std::cell::RefCell;
30use std::collections::{BTreeMap, HashMap};
31use std::io;
32use std::rc::Rc;
33
34const LANG_MD: &str = include_str!("lang.md");
36
37const TITLE_COLOR: u8 = AnsiColor::BrightYellow as u8;
39
40const LINK_COLOR: u8 = AnsiColor::BrightCyan as u8;
42
43fn header() -> Vec<String> {
45 vec![
46 "".to_owned(),
47 format!(" This is EndBASIC {}.", env!("CARGO_PKG_VERSION")),
48 "".to_owned(),
49 format!(" Project page at <{}>", env!("CARGO_PKG_HOMEPAGE")),
50 " License GNU AGPLv3+ <https://www.gnu.org/licenses/agpl-3.0.html>".to_owned(),
51 ]
52}
53
54#[async_trait(?Send)]
56trait Topic {
57 fn name(&self) -> &str;
59
60 fn title(&self) -> &str;
62
63 fn show_in_summary(&self) -> bool;
65
66 async fn describe(&self, pager: &mut Pager<'_>) -> io::Result<()>;
68}
69
70struct CallableTopic {
72 name: String,
73 metadata: Rc<CallableMetadata>,
74}
75
76#[async_trait(?Send)]
77impl Topic for CallableTopic {
78 fn name(&self) -> &str {
79 &self.name
80 }
81
82 fn title(&self) -> &str {
83 self.metadata.description().next().unwrap()
84 }
85
86 fn show_in_summary(&self) -> bool {
87 false
88 }
89
90 async fn describe(&self, pager: &mut Pager<'_>) -> io::Result<()> {
91 pager.print("").await?;
92 let previous = pager.color();
93 pager.set_color(Some(TITLE_COLOR), previous.1)?;
94 match self.metadata.return_type() {
95 None => {
96 if self.metadata.is_argless() {
97 refill_and_page(pager, [self.metadata.name()], " ").await?;
98 } else {
99 refill_and_page(
100 pager,
101 [&format!("{} {}", self.metadata.name(), self.metadata.syntax())],
102 " ",
103 )
104 .await?;
105 }
106 }
107 Some(return_type) => {
108 if self.metadata.is_argless() {
109 refill_and_page(
110 pager,
111 [&format!("{}{}", self.metadata.name(), return_type.annotation(),)],
112 " ",
113 )
114 .await?;
115 } else {
116 refill_and_page(
117 pager,
118 [&format!(
119 "{}{}({})",
120 self.metadata.name(),
121 return_type.annotation(),
122 self.metadata.syntax(),
123 )],
124 " ",
125 )
126 .await?;
127 }
128 }
129 }
130 pager.set_color(previous.0, previous.1)?;
131 if !self.metadata.description().count() > 0 {
132 pager.print("").await?;
133 refill_and_page(pager, self.metadata.description(), " ").await?;
134 }
135 pager.print("").await?;
136 Ok(())
137 }
138}
139
140fn callables_to_index(metadatas: &[Rc<CallableMetadata>]) -> BTreeMap<String, &'static str> {
142 let category = metadatas.first().expect("Must have at least one symbol").category();
143
144 let mut index = BTreeMap::default();
145 for metadata in metadatas {
146 debug_assert_eq!(
147 category,
148 metadata.category(),
149 "All commands registered in this category must be equivalent"
150 );
151 let name = match metadata.return_type() {
152 None => metadata.name().to_owned(),
153 Some(return_type) => format!("{}{}", metadata.name(), return_type.annotation()),
154 };
155 let blurb = metadata.description().next().unwrap();
156 let previous = index.insert(name, blurb);
157 assert!(previous.is_none(), "Names should have been unique");
158 }
159 index
160}
161
162struct CategoryTopic {
164 name: &'static str,
165 description: &'static str,
166 index: BTreeMap<String, &'static str>,
167}
168
169#[async_trait(?Send)]
170impl Topic for CategoryTopic {
171 fn name(&self) -> &str {
172 self.name
173 }
174
175 fn title(&self) -> &str {
176 self.name
177 }
178
179 fn show_in_summary(&self) -> bool {
180 true
181 }
182
183 async fn describe(&self, pager: &mut Pager<'_>) -> io::Result<()> {
184 let max_length = self
185 .index
186 .keys()
187 .map(|k| k.len())
188 .reduce(|a, k| if a > k { a } else { k })
189 .expect("Must have at least one item in the index");
190
191 let previous = pager.color();
192
193 let mut lines = self.description.lines().peekable();
194 pager.print("").await?;
195 pager.set_color(Some(TITLE_COLOR), previous.1)?;
196 refill_and_page(pager, lines.next(), " ").await?;
197 pager.set_color(previous.0, previous.1)?;
198 if lines.peek().is_some() {
199 pager.print("").await?;
200 }
201 refill_and_page(pager, lines, " ").await?;
202 pager.print("").await?;
203
204 for (name, blurb) in self.index.iter() {
205 let filler = " ".repeat(max_length - name.len());
206 pager.write(" >> ")?;
209 pager.set_color(Some(LINK_COLOR), previous.1)?;
210 pager.write(&format!("{}{}", name, filler))?;
211 pager.set_color(previous.0, previous.1)?;
212 pager.print(&format!(" {}", blurb)).await?;
213 }
214 pager.print("").await?;
215 refill_and_page(pager, ["Type HELP followed by the name of a topic for details."], " ")
216 .await?;
217 pager.print("").await?;
218 Ok(())
219 }
220}
221
222struct LanguageTopic {
224 name: &'static str,
225 text: &'static str,
226}
227
228#[async_trait(?Send)]
229impl Topic for LanguageTopic {
230 fn name(&self) -> &str {
231 self.name
232 }
233
234 fn title(&self) -> &str {
235 self.text.lines().next().unwrap()
236 }
237
238 fn show_in_summary(&self) -> bool {
239 false
240 }
241
242 async fn describe(&self, pager: &mut Pager<'_>) -> io::Result<()> {
243 let previous = pager.color();
244
245 let mut lines = self.text.lines();
246
247 pager.print("").await?;
248 pager.set_color(Some(TITLE_COLOR), previous.1)?;
249 refill_and_page(pager, [lines.next().expect("Must have at least one line")], " ")
250 .await?;
251 pager.set_color(previous.0, previous.1)?;
252 for line in lines {
253 if line.is_empty() {
254 pager.print("").await?;
255 } else {
256 refill_and_page(pager, [line], " ").await?;
257 }
258 }
259 pager.print("").await?;
260 Ok(())
261 }
262}
263
264fn parse_lang_reference(lang_md: &'static str) -> Vec<(&'static str, &'static str)> {
271 let mut topics = vec![];
272
273 let line_end;
277 let section_start;
278 let body_start;
279 if lang_md.contains("\r\n") {
280 line_end = "\r\n";
281 section_start = "\r\n\r\n# ";
282 body_start = "\r\n\r\n";
283 } else {
284 line_end = "\n";
285 section_start = "\n\n# ";
286 body_start = "\n\n";
287 }
288
289 for (start, _match) in lang_md.match_indices(section_start) {
290 let section = &lang_md[start + section_start.len()..];
291
292 let title_end = section.find(body_start).expect("Hardcoded text must be valid");
293 let title = §ion[..title_end];
294 let section = §ion[title_end + body_start.len()..];
295
296 let end = section.find(section_start).unwrap_or_else(|| {
297 if section.ends_with(line_end) { section.len() - line_end.len() } else { section.len() }
298 });
299 let content = §ion[..end];
300 topics.push((title, content));
301 }
302
303 topics
304}
305
306struct Topics(Trie<String, Box<dyn Topic>>);
308
309impl Topics {
310 fn new(callables: &HashMap<SymbolKey, Rc<CallableMetadata>>) -> Self {
312 fn insert(topics: &mut Trie<String, Box<dyn Topic>>, topic: Box<dyn Topic>) {
313 let key = topic.name().to_ascii_uppercase();
314 topics.insert(key, topic);
315 }
316
317 let mut topics = Trie::default();
318
319 {
320 let mut index = BTreeMap::default();
321
322 for (title, content) in parse_lang_reference(LANG_MD) {
323 let topic = LanguageTopic { name: title, text: content };
324 index.insert(topic.name.to_owned(), topic.text.lines().next().unwrap());
325 insert(&mut topics, Box::from(topic));
326 }
327
328 insert(
329 &mut topics,
330 Box::from(CategoryTopic {
331 name: "Language reference",
332 description: "General language topics",
333 index,
334 }),
335 );
336 }
337
338 let mut categories = HashMap::new();
339 for metadata in callables.values() {
340 let category_title = metadata.category().lines().next().unwrap();
341 categories.entry(category_title).or_insert_with(Vec::default).push(metadata.clone());
342
343 let name = match metadata.return_type() {
344 None => metadata.name().to_owned(),
345 Some(return_type) => format!("{}{}", metadata.name(), return_type.annotation()),
346 };
347
348 insert(&mut topics, Box::from(CallableTopic { name, metadata: metadata.clone() }));
349 }
350 for (name, metadatas) in categories.into_iter() {
351 let description = metadatas.first().expect("Must have at least one symbol").category();
352 let index = callables_to_index(&metadatas);
353 insert(&mut topics, Box::from(CategoryTopic { name, description, index }));
354 }
355
356 Self(topics)
357 }
358
359 fn find(&self, name: &str, scope: &Scope<'_>, narg: u8) -> CallResult<&dyn Topic> {
364 let key = name.to_ascii_uppercase();
365
366 if let Some(topic) = self.0.get(&key) {
367 return Ok(topic.as_ref());
368 }
369
370 match self.0.get_raw_descendant(&key) {
371 Some(subtrie) => {
372 let children: Vec<(&String, &Box<dyn Topic>)> = subtrie.iter().collect();
373 match children[..] {
374 [(_name, topic)] => Ok(topic.as_ref()),
375 _ => {
376 let completions: Vec<String> =
377 children.iter().map(|(name, _topic)| (*name).to_owned()).collect();
378 Err(CallError::Syntax(
379 scope.get_pos(narg),
380 format!(
381 "Ambiguous help topic {}; candidates are: {}",
382 name,
383 completions.join(", ")
384 ),
385 ))
386 }
387 }
388 }
389 None => {
390 Err(CallError::Syntax(scope.get_pos(narg), format!("Unknown help topic {}", name)))
391 }
392 }
393 }
394
395 fn values(&self) -> radix_trie::iter::Values<'_, String, Box<dyn Topic>> {
397 self.0.values()
398 }
399}
400
401pub struct HelpCommand {
403 metadata: Rc<CallableMetadata>,
404 callables: Rc<RefCell<HashMap<SymbolKey, Rc<CallableMetadata>>>>,
405 console: Rc<RefCell<dyn Console>>,
406}
407
408impl HelpCommand {
409 pub fn new(
411 callables: Rc<RefCell<HashMap<SymbolKey, Rc<CallableMetadata>>>>,
412 console: Rc<RefCell<dyn Console>>,
413 ) -> Rc<Self> {
414 Rc::from(Self {
415 metadata: CallableMetadataBuilder::new("HELP")
416 .with_async(true)
417 .with_syntax(&[
418 (&[], None),
419 (
420 &[SingularArgSyntax::RequiredValue(
421 RequiredValueSyntax {
422 name: Cow::Borrowed("topic"),
423 vtype: ExprType::Text,
424 },
425 ArgSepSyntax::End,
426 )],
427 None,
428 ),
429 ])
430 .with_category(CATEGORY)
431 .with_description(
432 "Prints interactive help.
433Without arguments, shows a summary of all available top-level help topics.
434With a single argument, which must be a string, shows detailed information about the given help \
435topic, command, or function.
436Topic names are case-insensitive and can be specified as prefixes, in which case the topic whose \
437name starts with the prefix will be shown. For example, the following invocations are all \
438equivalent: HELP \"CON\", HELP \"console\", HELP \"Console manipulation\".",
439 )
440 .build(),
441 callables,
442 console,
443 })
444 }
445
446 async fn summary(&self, topics: &Topics, pager: &mut Pager<'_>) -> io::Result<()> {
448 for line in header() {
449 refill_and_page(pager, [&line], "").await?;
450 }
451
452 let previous = pager.color();
453
454 pager.print("").await?;
455 pager.set_color(Some(TITLE_COLOR), previous.1)?;
456 refill_and_page(pager, ["Top-level help topics"], " ").await?;
457 pager.set_color(previous.0, previous.1)?;
458 pager.print("").await?;
459 for topic in topics.values() {
460 if topic.show_in_summary() {
461 pager.write(" >> ")?;
464 pager.set_color(Some(LINK_COLOR), previous.1)?;
465 pager.print(topic.title()).await?;
466 pager.set_color(previous.0, previous.1)?;
467 }
468 }
469 pager.print("").await?;
470 refill_and_page(pager, ["Type HELP followed by the name of a topic for details."], " ")
471 .await?;
472 refill_and_page(
473 pager,
474 ["Type HELP \"HELP\" for details on how to specify topic names."],
475 " ",
476 )
477 .await?;
478 refill_and_page(pager, [r#"Type LOAD "DEMOS:/TOUR.BAS": RUN for a guided tour."#], " ")
479 .await?;
480 refill_and_page(pager, [r#"Type END or press CTRL+D to exit."#], " ").await?;
481 pager.print("").await?;
482
483 Ok(())
484 }
485}
486
487#[async_trait(?Send)]
488impl Callable for HelpCommand {
489 fn metadata(&self) -> Rc<CallableMetadata> {
490 self.metadata.clone()
491 }
492
493 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
494 let topics = Topics::new(&self.callables.borrow());
495
496 if scope.nargs() == 0 {
497 let mut console = self.console.borrow_mut();
498 let result = {
499 let mut pager = Pager::new(&mut *console).map_err(CallError::from)?;
500 self.summary(&topics, &mut pager).await
501 };
502 result.map_err(CallError::from)?;
503 } else {
504 debug_assert_eq!(1, scope.nargs());
505 let t = scope.get_string(0).to_owned();
506
507 let topic = topics.find(&t, &scope, 0)?;
508 let mut console = self.console.borrow_mut();
509 let result = {
510 let mut pager = Pager::new(&mut *console).map_err(CallError::from)?;
511 topic.describe(&mut pager).await
512 };
513 result.map_err(CallError::from)?;
514 }
515
516 Ok(())
517 }
518}
519
520pub fn add_all(machine: &mut MachineBuilder, console: Rc<RefCell<dyn Console>>) {
522 machine.add_callable(HelpCommand::new(machine.callables_metadata(), console));
523}
524
525#[cfg(test)]
526pub(crate) mod testutils {
527 use super::*;
528
529 pub(crate) struct DoNothingCommand {
531 metadata: Rc<CallableMetadata>,
532 }
533
534 impl DoNothingCommand {
535 pub(crate) fn new() -> Rc<Self> {
537 DoNothingCommand::new_with_name("DO_NOTHING")
538 }
539
540 pub fn new_with_name(name: &'static str) -> Rc<Self> {
542 Rc::from(Self {
543 metadata: CallableMetadataBuilder::new(name)
544 .with_syntax(&[(
545 &[SingularArgSyntax::RequiredValue(
546 RequiredValueSyntax {
547 name: Cow::Borrowed("sample"),
548 vtype: ExprType::Text,
549 },
550 ArgSepSyntax::End,
551 )],
552 None,
553 )])
554 .with_category(
555 "Testing
556This is a sample category for testing.",
557 )
558 .with_description(
559 "This is the blurb.
560First paragraph of the extended description.
561Second paragraph of the extended description.",
562 )
563 .build(),
564 })
565 }
566 }
567
568 #[async_trait(?Send)]
569 impl Callable for DoNothingCommand {
570 fn metadata(&self) -> Rc<CallableMetadata> {
571 self.metadata.clone()
572 }
573
574 fn exec(&self, _scope: Scope<'_>) -> CallResult<()> {
575 Ok(())
576 }
577 }
578
579 pub(crate) struct EmptyFunction {
581 metadata: Rc<CallableMetadata>,
582 }
583
584 impl EmptyFunction {
585 pub(crate) fn new() -> Rc<Self> {
587 EmptyFunction::new_with_name("EMPTY")
588 }
589
590 pub(crate) fn new_with_name(name: &'static str) -> Rc<Self> {
592 Rc::from(Self {
593 metadata: CallableMetadataBuilder::new(name)
594 .with_return_type(ExprType::Text)
595 .with_syntax(&[(
596 &[SingularArgSyntax::RequiredValue(
597 RequiredValueSyntax {
598 name: Cow::Borrowed("sample"),
599 vtype: ExprType::Text,
600 },
601 ArgSepSyntax::End,
602 )],
603 None,
604 )])
605 .with_category(
606 "Testing
607This is a sample category for testing.",
608 )
609 .with_description(
610 "This is the blurb.
611First paragraph of the extended description.
612Second paragraph of the extended description.",
613 )
614 .build(),
615 })
616 }
617 }
618
619 #[async_trait(?Send)]
620 impl Callable for EmptyFunction {
621 fn metadata(&self) -> Rc<CallableMetadata> {
622 self.metadata.clone()
623 }
624
625 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
626 scope.return_string("irrelevant".to_owned())
627 }
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::testutils::*;
634 use super::*;
635 use crate::MachineBuilder;
636 use crate::console::{CharsXY, Key};
637 use crate::testutils::*;
638 use futures_lite::future::block_on;
639
640 #[test]
641 fn test_parse_lang_reference_empty() {
642 let content = parse_lang_reference("");
643 assert!(content.is_empty());
644 }
645
646 #[test]
647 fn test_parse_lang_reference_junk_only() {
648 let content = parse_lang_reference(
649 "# foo
650# bar
651baz",
652 );
653 assert!(content.is_empty());
654 }
655
656 #[test]
657 fn test_parse_lang_reference_one() {
658 let content = parse_lang_reference(
659 "
660
661# First
662
663This is the first and only topic with
664a couple of lines.
665",
666 );
667 let exp_content =
668 vec![("First", "This is the first and only topic with\na couple of lines.")];
669 assert_eq!(exp_content, content);
670 }
671
672 #[test]
673 fn test_parse_lang_reference_many() {
674 let content = parse_lang_reference(
675 "
676
677# First
678
679This is the first topic with
680a couple of lines.
681
682# Second
683
684This is the second topic with just one line.
685
686# Third
687
688And this is the last one without EOF.",
689 );
690 let exp_content = vec![
691 ("First", "This is the first topic with\na couple of lines."),
692 ("Second", "This is the second topic with just one line."),
693 ("Third", "And this is the last one without EOF."),
694 ];
695 assert_eq!(exp_content, content);
696 }
697
698 #[test]
699 fn test_parse_lang_reference_ignore_header() {
700 let content = parse_lang_reference(
701 "This should be ignored.
702And this.
703#And also this.
704
705# First
706
707This is the first and only topic with just one line.
708",
709 );
710 let exp_content = vec![("First", "This is the first and only topic with just one line.")];
711 assert_eq!(exp_content, content);
712 }
713
714 fn tester_with(callables: Vec<Rc<dyn Callable>>) -> Tester {
715 let metadata = Rc::new(RefCell::new(HashMap::default()));
716 let mut tester = Tester::empty();
717 for callable in callables {
718 metadata
719 .borrow_mut()
720 .insert(SymbolKey::from(callable.metadata().name()), callable.metadata());
721 tester = tester.add_callable(callable);
722 }
723
724 let console = tester.get_console();
725 let help_probe =
726 HelpCommand::new(Rc::new(RefCell::new(HashMap::default())), console.clone());
727 metadata
728 .borrow_mut()
729 .insert(SymbolKey::from(help_probe.metadata().name()), help_probe.metadata());
730 tester.add_callable(HelpCommand::new(metadata, console))
731 }
732
733 fn tester() -> Tester {
734 tester_with(vec![])
735 }
736
737 #[test]
738 fn test_help_summarize_symbols() {
739 let mut t = tester_with(vec![DoNothingCommand::new(), EmptyFunction::new()]);
740 t.get_console().borrow_mut().set_color(Some(100), Some(200)).unwrap();
741 t.run("HELP")
742 .expect_output([CapturedOut::SetColor(Some(100), Some(200))])
743 .expect_prints(header())
744 .expect_prints([""])
745 .expect_output([
746 CapturedOut::SetColor(Some(TITLE_COLOR), Some(200)),
747 CapturedOut::Print(" Top-level help topics".to_owned()),
748 CapturedOut::SetColor(Some(100), Some(200)),
749 ])
750 .expect_prints([""])
751 .expect_output([
752 CapturedOut::Write(" >> ".to_owned()),
753 CapturedOut::SetColor(Some(LINK_COLOR), Some(200)),
754 CapturedOut::Print("Interpreter".to_owned()),
755 CapturedOut::SetColor(Some(100), Some(200)),
756 ])
757 .expect_output([
758 CapturedOut::Write(" >> ".to_owned()),
759 CapturedOut::SetColor(Some(LINK_COLOR), Some(200)),
760 CapturedOut::Print("Language reference".to_owned()),
761 CapturedOut::SetColor(Some(100), Some(200)),
762 ])
763 .expect_output([
764 CapturedOut::Write(" >> ".to_owned()),
765 CapturedOut::SetColor(Some(LINK_COLOR), Some(200)),
766 CapturedOut::Print("Testing".to_owned()),
767 CapturedOut::SetColor(Some(100), Some(200)),
768 ])
769 .expect_prints([
770 "",
771 " Type HELP followed by the name of a topic for details.",
772 " Type HELP \"HELP\" for details on how to specify topic names.",
773 " Type LOAD \"DEMOS:/TOUR.BAS\": RUN for a guided tour.",
774 " Type END or press CTRL+D to exit.",
775 "",
776 ])
777 .check();
778 }
779
780 #[test]
781 fn test_help_includes_scripting_categories() {
782 let console = Rc::new(RefCell::new(MockConsole::default()));
783 let mut machine =
784 MachineBuilder::default().with_console(console.clone()).make_interactive().build();
785
786 machine.compile(&mut "HELP".as_bytes()).unwrap();
787 block_on(machine.exec()).unwrap();
788
789 assert!(
790 console
791 .borrow()
792 .captured_out()
793 .contains(&CapturedOut::Print("Numerical functions".to_owned())),
794 "HELP output must include Numerical functions category"
795 );
796 assert!(
797 console
798 .borrow()
799 .captured_out()
800 .contains(&CapturedOut::Print("String and character functions".to_owned())),
801 "HELP output must include String and character functions category"
802 );
803 }
804
805 #[test]
806 fn test_help_describe_callables_topic() {
807 let mut t = tester_with(vec![DoNothingCommand::new(), EmptyFunction::new()]);
808 t.get_console().borrow_mut().set_color(Some(70), Some(50)).unwrap();
809 t.run(r#"help "testing""#)
810 .expect_output([CapturedOut::SetColor(Some(70), Some(50))])
811 .expect_prints([""])
812 .expect_output([
813 CapturedOut::SetColor(Some(TITLE_COLOR), Some(50)),
814 CapturedOut::Print(" Testing".to_owned()),
815 CapturedOut::SetColor(Some(70), Some(50)),
816 ])
817 .expect_prints(["", " This is a sample category for testing.", ""])
818 .expect_output([
819 CapturedOut::Write(" >> ".to_owned()),
820 CapturedOut::SetColor(Some(LINK_COLOR), Some(50)),
821 CapturedOut::Write("DO_NOTHING".to_owned()),
822 CapturedOut::SetColor(Some(70), Some(50)),
823 CapturedOut::Print(" This is the blurb.".to_owned()),
824 ])
825 .expect_output([
826 CapturedOut::Write(" >> ".to_owned()),
827 CapturedOut::SetColor(Some(LINK_COLOR), Some(50)),
828 CapturedOut::Write("EMPTY$ ".to_owned()),
829 CapturedOut::SetColor(Some(70), Some(50)),
830 CapturedOut::Print(" This is the blurb.".to_owned()),
831 ])
832 .expect_prints(["", " Type HELP followed by the name of a topic for details.", ""])
833 .check();
834 }
835
836 #[test]
837 fn test_help_describe_command() {
838 let mut t = tester_with(vec![DoNothingCommand::new()]);
839 t.get_console().borrow_mut().set_color(Some(20), Some(21)).unwrap();
840 t.run(r#"help "Do_Nothing""#)
841 .expect_output([CapturedOut::SetColor(Some(20), Some(21))])
842 .expect_prints([""])
843 .expect_output([
844 CapturedOut::SetColor(Some(TITLE_COLOR), Some(21)),
845 CapturedOut::Print(" DO_NOTHING sample$".to_owned()),
846 CapturedOut::SetColor(Some(20), Some(21)),
847 ])
848 .expect_prints([
849 "",
850 " This is the blurb.",
851 "",
852 " First paragraph of the extended description.",
853 "",
854 " Second paragraph of the extended description.",
855 "",
856 ])
857 .check();
858 }
859
860 fn do_help_describe_function_test(name: &str) {
861 let mut t = tester_with(vec![EmptyFunction::new()]);
862 t.get_console().borrow_mut().set_color(Some(30), Some(26)).unwrap();
863 t.run(format!(r#"help "{}""#, name))
864 .expect_output([CapturedOut::SetColor(Some(30), Some(26))])
865 .expect_prints([""])
866 .expect_output([
867 CapturedOut::SetColor(Some(TITLE_COLOR), Some(26)),
868 CapturedOut::Print(" EMPTY$(sample$)".to_owned()),
869 CapturedOut::SetColor(Some(30), Some(26)),
870 ])
871 .expect_prints([
872 "",
873 " This is the blurb.",
874 "",
875 " First paragraph of the extended description.",
876 "",
877 " Second paragraph of the extended description.",
878 "",
879 ])
880 .check();
881 }
882
883 #[test]
884 fn test_help_describe_function_without_annotation() {
885 do_help_describe_function_test("Empty")
886 }
887
888 #[test]
889 fn test_help_describe_function_with_annotation() {
890 do_help_describe_function_test("EMPTY$")
891 }
892
893 #[test]
894 fn test_help_eval_arg() {
895 tester_with(vec![DoNothingCommand::new()])
896 .run(r#"topic = "Do_Nothing": HELP topic"#)
897 .expect_prints([""])
898 .expect_output([
899 CapturedOut::SetColor(Some(TITLE_COLOR), None),
900 CapturedOut::Print(" DO_NOTHING sample$".to_owned()),
901 CapturedOut::SetColor(None, None),
902 ])
903 .expect_prints([
904 "",
905 " This is the blurb.",
906 "",
907 " First paragraph of the extended description.",
908 "",
909 " Second paragraph of the extended description.",
910 "",
911 ])
912 .expect_var("topic", "Do_Nothing")
913 .check();
914 }
915
916 #[test]
917 fn test_help_prefix_search() {
918 fn exp_output(name: &str, is_function: bool) -> Vec<CapturedOut> {
919 let spec = if is_function {
920 format!(" {}(sample$)", name)
921 } else {
922 format!(" {} sample$", name)
923 };
924 vec![
925 CapturedOut::Print("".to_owned()),
926 CapturedOut::SetColor(Some(TITLE_COLOR), None),
927 CapturedOut::Print(spec),
928 CapturedOut::SetColor(None, None),
929 CapturedOut::Print("".to_owned()),
930 CapturedOut::Print(" This is the blurb.".to_owned()),
931 CapturedOut::Print("".to_owned()),
932 CapturedOut::Print(" First paragraph of the extended description.".to_owned()),
933 CapturedOut::Print("".to_owned()),
934 CapturedOut::Print(" Second paragraph of the extended description.".to_owned()),
935 CapturedOut::Print("".to_owned()),
936 ]
937 }
938
939 for cmd in &[r#"help "aa""#, r#"help "aab""#, r#"help "aabc""#] {
940 tester_with(vec![
941 EmptyFunction::new_with_name("AABC"),
942 EmptyFunction::new_with_name("ABC"),
943 EmptyFunction::new_with_name("BC"),
944 ])
945 .run(*cmd)
946 .expect_output(exp_output("AABC$", true))
947 .check();
948 }
949
950 for cmd in &[r#"help "b""#, r#"help "bc""#] {
951 tester_with(vec![
952 EmptyFunction::new_with_name("AABC"),
953 EmptyFunction::new_with_name("ABC"),
954 EmptyFunction::new_with_name("BC"),
955 ])
956 .run(*cmd)
957 .expect_output(exp_output("BC$", true))
958 .check();
959 }
960
961 tester_with(vec![
962 DoNothingCommand::new_with_name("AAAB"),
963 DoNothingCommand::new_with_name("AAAA"),
964 DoNothingCommand::new_with_name("AAAAA"),
965 ])
966 .run(r#"help "aaaa""#)
967 .expect_output(exp_output("AAAA", false))
968 .check();
969
970 tester_with(vec![
971 DoNothingCommand::new_with_name("ZAB"),
972 EmptyFunction::new_with_name("ZABC"),
973 EmptyFunction::new_with_name("ZAABC"),
974 ])
975 .run(r#"help "za""#)
976 .expect_err("1:6: Ambiguous help topic za; candidates are: ZAABC$, ZAB, ZABC$")
977 .check();
978 }
979
980 #[test]
981 fn test_help_errors() {
982 let mut t = tester_with(vec![DoNothingCommand::new(), EmptyFunction::new()]);
983
984 t.run(r#"HELP foo bar"#).expect_err("1:10: Unexpected value in expression").check();
985 t.run(r#"HELP foo"#).expect_compilation_err("1:6: Undefined symbol foo").check();
986
987 t.run(r#"HELP "foo", 3"#)
988 .expect_compilation_err("1:1: HELP expected <> | <topic$>")
989 .check();
990 t.run(r#"HELP 3"#).expect_compilation_err("1:6: Expected STRING but found INTEGER").check();
991
992 t.run(r#"HELP "lang%""#).expect_err("1:6: Unknown help topic lang%").check();
993
994 t.run(r#"HELP "foo$""#).expect_err("1:6: Unknown help topic foo$").check();
995 t.run(r#"HELP "foo""#).expect_err("1:6: Unknown help topic foo").check();
996
997 t.run(r#"HELP "do_nothing$""#).expect_err("1:6: Unknown help topic do_nothing$").check();
998 t.run(r#"HELP "empty?""#).expect_err("1:6: Unknown help topic empty?").check();
999
1000 t.run(r#"topic = "foo$": HELP topic$"#)
1001 .expect_err("1:22: Unknown help topic foo$")
1002 .expect_var("topic", "foo$")
1003 .check();
1004
1005 let mut t = tester();
1006 t.run(r#"HELP "undoc""#).expect_err("1:6: Unknown help topic undoc").check();
1007 t.run(r#"undoc = 3: HELP "undoc""#)
1008 .expect_err("1:17: Unknown help topic undoc")
1009 .expect_var("undoc", 3)
1010 .check();
1011
1012 let mut t = tester();
1013 t.run(r#"HELP "undoc""#).expect_err("1:6: Unknown help topic undoc").check();
1014 t.run(r#"DIM undoc(3): HELP "undoc""#)
1015 .expect_err("1:20: Unknown help topic undoc")
1016 .expect_array("undoc", ExprType::Integer, &[3], vec![])
1017 .check();
1018 }
1019
1020 #[test]
1021 fn test_help_paging() {
1022 let mut t = tester();
1023 t.get_console().borrow_mut().set_interactive(true);
1024 t.get_console().borrow_mut().set_size_chars(CharsXY { x: 80, y: 9 });
1025 t.get_console().borrow_mut().add_input_keys(&[Key::NewLine]);
1026 t.get_console().borrow_mut().set_color(Some(100), Some(200)).unwrap();
1027 t.run("HELP")
1028 .expect_output([CapturedOut::SetColor(Some(100), Some(200))])
1029 .expect_prints(header())
1030 .expect_prints([""])
1031 .expect_output([
1032 CapturedOut::SetColor(Some(TITLE_COLOR), Some(200)),
1033 CapturedOut::Print(" Top-level help topics".to_owned()),
1034 CapturedOut::SetColor(Some(100), Some(200)),
1035 ])
1036 .expect_prints([""])
1037 .expect_output([
1038 CapturedOut::SetColor(None, None),
1039 CapturedOut::Print(
1040 " << Press any key for more; ESC or Ctrl+C to stop >> ".to_owned(),
1041 ),
1042 CapturedOut::SetColor(Some(100), Some(200)),
1043 ])
1044 .expect_output([
1045 CapturedOut::Write(" >> ".to_owned()),
1046 CapturedOut::SetColor(Some(LINK_COLOR), Some(200)),
1047 CapturedOut::Print("Interpreter".to_owned()),
1048 CapturedOut::SetColor(Some(100), Some(200)),
1049 ])
1050 .expect_output([
1051 CapturedOut::Write(" >> ".to_owned()),
1052 CapturedOut::SetColor(Some(LINK_COLOR), Some(200)),
1053 CapturedOut::Print("Language reference".to_owned()),
1054 CapturedOut::SetColor(Some(100), Some(200)),
1055 ])
1056 .expect_prints([
1057 "",
1058 " Type HELP followed by the name of a topic for details.",
1059 " Type HELP \"HELP\" for details on how to specify topic names.",
1060 " Type LOAD \"DEMOS:/TOUR.BAS\": RUN for a guided tour.",
1061 " Type END or press CTRL+D to exit.",
1062 "",
1063 ])
1064 .expect_output([
1065 CapturedOut::SetColor(None, None),
1066 CapturedOut::Print(
1067 " << Press any key for more; ESC or Ctrl+C to stop >> ".to_owned(),
1068 ),
1069 CapturedOut::SetColor(Some(100), Some(200)),
1070 ])
1071 .check();
1072 }
1073}