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