Skip to main content

endbasic_std/
help.rs

1// EndBASIC
2// Copyright 2020 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Interactive help support.
18
19use 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
34/// Raw text for the language reference.
35const LANG_MD: &str = include_str!("lang.md");
36
37/// Color for titles.
38const TITLE_COLOR: u8 = AnsiColor::BrightYellow as u8;
39
40/// Color for references to other topics.
41const LINK_COLOR: u8 = AnsiColor::BrightCyan as u8;
42
43/// Returns the header for the help summary.
44fn 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/// Handler for a specific help topic.
55#[async_trait(?Send)]
56trait Topic {
57    /// Returns the name of the topic.
58    fn name(&self) -> &str;
59
60    /// Returns the human-readable, one-line description of this topic.
61    fn title(&self) -> &str;
62
63    /// Indicates whether this topic shows up in the topics summary or not.
64    fn show_in_summary(&self) -> bool;
65
66    /// Dumps the contents of this topic to the `pager`.
67    async fn describe(&self, pager: &mut Pager<'_>) -> io::Result<()>;
68}
69
70/// A help topic to describe a callable.
71struct 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
140/// Generates the index for a collection of `CallableMetadata`s to use in a `CategoryTopic`.
141fn 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
162/// A help topic to describe a category of callables.
163struct 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            // TODO(jmmv): Should use refill_and_page but continuation lines need special handling
207            // to be indented properly.
208            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
222/// A help topic to describe a non-callable help topic.
223struct 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
264/// Parses the `lang.md` file and extracts a mapping of language reference topics to their
265/// descriptions.
266///
267/// Note that, even if the input looks like Markdown, we do *not* implement a Markdown parser here.
268/// The structure of the file is strict and well-known in advance, so this will panic if there are
269/// problems in the input data.
270fn parse_lang_reference(lang_md: &'static str) -> Vec<(&'static str, &'static str)> {
271    let mut topics = vec![];
272
273    // Cope with Windows checkouts.  It's tempting to make this a build-time conditional on the OS
274    // name, but we don't know how the files are checked out.  Assume CRLF delimiters if we see at
275    // least one of them.
276    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 = &section[..title_end];
294        let section = &section[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 = &section[..end];
300        topics.push((title, content));
301    }
302
303    topics
304}
305
306/// Maintains the collection of topics as a trie indexed by their name.
307struct Topics(Trie<String, Box<dyn Topic>>);
308
309impl Topics {
310    /// Builds an index of the given `callables` and returns a new collection of help topics.
311    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    /// Returns the given topic named `name`, where `name` can be a prefix.
360    ///
361    /// If `name` is not long enough to uniquely identify a topic or if the topic does not exist,
362    /// returns an error.
363    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    /// Returns an iterator over all the topics.
396    fn values(&self) -> radix_trie::iter::Values<'_, String, Box<dyn Topic>> {
397        self.0.values()
398    }
399}
400
401/// The `HELP` command.
402pub 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    /// Creates a new command that writes help messages to `output`.
410    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    /// Prints a summary of all available help topics.
447    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                // TODO(jmmv): Should use refill_and_page but continuation lines need special
462                // handling to be indented properly.
463                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
520/// Adds all help-related commands to the `machine` and makes them write to `console`.
521pub 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    /// A command that does nothing.
530    pub(crate) struct DoNothingCommand {
531        metadata: Rc<CallableMetadata>,
532    }
533
534    impl DoNothingCommand {
535        /// Creates a new instance of the command with the name `DO_NOTHING`.
536        pub(crate) fn new() -> Rc<Self> {
537            DoNothingCommand::new_with_name("DO_NOTHING")
538        }
539
540        /// Creates a new instance of the command with a given `name`.
541        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    /// A function that does nothing that can take any name.
580    pub(crate) struct EmptyFunction {
581        metadata: Rc<CallableMetadata>,
582    }
583
584    impl EmptyFunction {
585        /// Creates a new instance of the function with the name `EMPTY`.
586        pub(crate) fn new() -> Rc<Self> {
587            EmptyFunction::new_with_name("EMPTY")
588        }
589
590        /// Creates a new instance of the function with a given `name`.
591        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}