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