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