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