texlang_stdlib/
repl.rs

1//! Support for running TeX REPLs
2
3use super::script;
4use std::sync::Arc;
5use texlang::prelude as txl;
6use texlang::traits::*;
7use texlang::*;
8use texlang_common as common;
9
10pub struct RunOptions<'a> {
11    pub prompt: &'a str,
12    pub help: &'a str,
13}
14
15#[derive(Default)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
17pub struct Component {
18    help: String,
19    quit_requested: bool,
20}
21
22/// Start a REPL session using the provided VM.
23#[cfg(feature = "repl")]
24pub fn start<S: HasComponent<script::Component> + HasComponent<Component>>(
25    vm: &mut vm::VM<S>,
26    opts: RunOptions,
27) {
28    use linefeed::{Interface, ReadResult};
29
30    let c = HasComponent::<Component>::component_mut(&mut vm.state);
31    c.help = opts.help.into();
32    c.quit_requested = false;
33
34    let reader = Interface::new("").unwrap();
35    reader.set_prompt(opts.prompt).unwrap();
36    script::set_io_writer(vm, std::io::stdout());
37
38    let mut num_commands: Option<usize> = None;
39    loop {
40        // We detect new commands (via \def say) by seeing if the number of commands
41        // have changed. This is incorrect though; in the following case, the
42        // commands will not be updated:
43        //
44        // TeX> {
45        // TeX> \def \Apple{A}
46        // TeX> } \def \Orange{B}
47        //
48        // Instead we need to iterate over all commands in the map and
49        // check the diff.
50        if Some(vm.commands_map.len()) != num_commands {
51            let mut names: Vec<String> = vm
52                .get_commands_as_map_slow()
53                .into_keys()
54                .map(|s| s.to_string())
55                .collect();
56            names.sort();
57            num_commands = Some(names.len());
58            let a = Arc::new(ControlSequenceCompleter { names });
59            reader.set_completer(a);
60        }
61
62        let ReadResult::Input(input) = reader.read_line().unwrap() else {
63            break;
64        };
65        reader.add_history(input.clone());
66
67        vm.clear_sources();
68        vm.push_source("".to_string(), input).unwrap();
69        match script::run(vm) {
70            Ok(()) => (),
71            Err(err) => {
72                println!("{err}");
73                continue;
74            }
75        };
76        if HasComponent::<Component>::component(&vm.state).quit_requested {
77            return;
78        }
79        // TODO: better new line handling in the REPL
80        println!();
81    }
82}
83
84struct ControlSequenceCompleter {
85    names: Vec<String>,
86}
87
88#[cfg(feature = "repl")]
89impl<Term: linefeed::Terminal> linefeed::Completer<Term> for ControlSequenceCompleter {
90    fn complete(
91        &self,
92        word: &str,
93        prompter: &linefeed::Prompter<Term>,
94        start: usize,
95        _end: usize,
96    ) -> Option<Vec<linefeed::Completion>> {
97        if !prompter.buffer()[..start].ends_with('\\') {
98            return None;
99        }
100        let mut completions = Vec::new();
101        for name in &self.names {
102            if name.starts_with(word) {
103                completions.push(linefeed::Completion {
104                    completion: name.to_string(),
105                    display: Some(format!["\\{name}"]),
106                    suffix: linefeed::Suffix::Default,
107                });
108            }
109        }
110        Some(completions)
111    }
112}
113
114/// Get the `\exit` command.
115///
116/// This exits the REPL.
117pub fn get_exit<S: HasComponent<Component>>() -> command::BuiltIn<S> {
118    command::BuiltIn::new_execution(
119        |_: token::Token, input: &mut vm::ExecutionInput<S>| -> txl::Result<()> {
120            HasComponent::<Component>::component_mut(input.state_mut()).quit_requested = true;
121            Err(input.shutdown())
122        },
123    )
124}
125
126/// Get the `\help` command.
127///
128/// This prints help text for the REPL.
129pub fn get_help<S: HasComponent<Component> + common::HasLogging>() -> command::BuiltIn<S> {
130    command::BuiltIn::new_execution(
131        |token: token::Token, input: &mut vm::ExecutionInput<S>| -> txl::Result<()> {
132            let help = HasComponent::<Component>::component(input.state())
133                .help
134                .clone();
135            match writeln![input.state().terminal_out().borrow_mut(), "{help}"] {
136                Ok(_) => Ok(()),
137                Err(err) => Err(input.fatal_error(error::SimpleTokenError::new(
138                    token,
139                    format!["failed to write help text: {err}"],
140                ))),
141            }
142        },
143    )
144}
145
146/// Get the `\doc` command.
147///
148/// This prints the documentation for a TeX command.
149pub fn get_doc<S: TexlangState + common::HasLogging>() -> command::BuiltIn<S> {
150    command::BuiltIn::new_execution(
151        |token: token::Token, input: &mut vm::ExecutionInput<S>| -> txl::Result<()> {
152            let Some(cmd_ref) = Option::<token::CommandRef>::parse(input)? else {
153                return Ok(());
154            };
155            let name = cmd_ref.to_string(input.vm().cs_name_interner());
156            let doc = match input.commands_map().get_command_slow(&cmd_ref) {
157                None => format!["Unknown command {name}"],
158                Some(cmd) => match cmd.doc() {
159                    None => format!["No documentation available for the {name} command"],
160                    Some(doc) => format!["{name}  {doc}"],
161                },
162            };
163            match writeln![input.state().terminal_out().borrow_mut(), "{doc}"] {
164                Ok(_) => Ok(()),
165                Err(err) => Err(input.fatal_error(error::SimpleTokenError::new(
166                    token,
167                    format!["failed to write doc text: {err}"],
168                ))),
169            }
170        },
171    )
172}