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