Skip to main content

endbasic_std/
help.rs

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