Skip to main content

mittens_engine/scripting/repl/
frontend.rs

1use std::io::{BufRead, Write};
2use std::sync::mpsc;
3
4pub struct MeowMeowReplFrontend {
5    rx: mpsc::Receiver<String>,
6}
7
8impl MeowMeowReplFrontend {
9    pub fn new() -> Result<Self, &'static str> {
10        crate::engine::repl::claim_stdin()?;
11        let (tx, rx) = mpsc::channel();
12        std::thread::spawn(move || {
13            let stdin = std::io::stdin();
14            let mut buffered = String::new();
15            loop {
16                print!("{}", if buffered.is_empty() { "mms> " } else { "... " });
17                let _ = std::io::stdout().flush();
18                let mut line = String::new();
19                match stdin.lock().read_line(&mut line) {
20                    Ok(0) | Err(_) => break,
21                    Ok(_) => {}
22                }
23                buffered.push_str(&line);
24                if input_complete(&buffered) {
25                    if tx.send(std::mem::take(&mut buffered)).is_err() {
26                        break;
27                    }
28                }
29            }
30            crate::engine::repl::release_stdin();
31        });
32        Ok(Self { rx })
33    }
34
35    pub fn try_recv_all(&self) -> Vec<String> {
36        let mut out = Vec::new();
37        while let Ok(source) = self.rx.try_recv() {
38            out.push(source);
39        }
40        out
41    }
42}
43
44pub(crate) fn input_complete(source: &str) -> bool {
45    let mut stack = Vec::new();
46    let mut quote = None;
47    let mut escaped = false;
48    let mut line_comment = false;
49    let chars = source.chars().collect::<Vec<_>>();
50    let mut index = 0;
51    while index < chars.len() {
52        let ch = chars[index];
53        if line_comment {
54            if ch == '\n' {
55                line_comment = false;
56            }
57            index += 1;
58            continue;
59        }
60        if let Some(q) = quote {
61            if escaped {
62                escaped = false;
63            } else if ch == '\\' {
64                escaped = true;
65            } else if ch == q {
66                quote = None;
67            }
68            index += 1;
69            continue;
70        }
71        if ch == '/' && chars.get(index + 1) == Some(&'/') {
72            line_comment = true;
73            index += 2;
74            continue;
75        }
76        match ch {
77            '"' | '\'' => quote = Some(ch),
78            '(' | '[' | '{' => stack.push(ch),
79            ')' => {
80                if stack.last() == Some(&'(') {
81                    stack.pop();
82                }
83            }
84            ']' => {
85                if stack.last() == Some(&'[') {
86                    stack.pop();
87                }
88            }
89            '}' => {
90                if stack.last() == Some(&'{') {
91                    stack.pop();
92                }
93            }
94            _ => {}
95        }
96        index += 1;
97    }
98    quote.is_none() && stack.is_empty()
99}
100
101#[cfg(test)]
102mod tests {
103    use super::input_complete;
104    #[test]
105    fn balances_multiline_and_quoted_delimiters() {
106        assert!(!input_complete("T {\n Text {"));
107        assert!(input_complete("T {\n Text { \"}\" }\n}"));
108        assert!(!input_complete("\"escaped \\\" quote"));
109        assert!(input_complete("query(world, \"#x > Text\")"));
110        assert!(input_complete("// an unmatched { in a comment\n42"));
111    }
112}