Skip to main content

live_parser/
live_parser.rs

1use std::io::{self, IsTerminal, Read, Write};
2use std::process::Command;
3
4use m2_syn::{
5    LexError, NativeRealtimeParser, Spanned, TextEdit, format_pretty_tokens, format_pretty_tree,
6};
7
8struct TerminalMode(Option<String>);
9
10impl TerminalMode {
11    fn raw() -> Self {
12        if !io::stdin().is_terminal() {
13            return Self(None);
14        }
15        let Ok(saved) = Command::new("stty").arg("-g").output() else {
16            return Self(None);
17        };
18        if !saved.status.success() {
19            return Self(None);
20        }
21        let saved = String::from_utf8_lossy(&saved.stdout).trim().to_owned();
22        let enabled = Command::new("stty")
23            .args(["raw", "-echo"])
24            .status()
25            .is_ok_and(|status| status.success());
26        Self(enabled.then_some(saved))
27    }
28}
29
30impl Drop for TerminalMode {
31    fn drop(&mut self) {
32        if let Some(saved) = &self.0 {
33            let _ = Command::new("stty").arg(saved).status();
34        }
35    }
36}
37
38fn write_diagnostic(output: &mut String, diagnostic: &LexError) {
39    let span = diagnostic.span();
40    output.push_str(&format!("- {diagnostic} at {span}\n"));
41}
42
43fn render(parser: &NativeRealtimeParser, clear: bool) -> String {
44    let mut output = String::new();
45    if clear {
46        output.push_str("\u{1b}[2J\u{1b}[H");
47    }
48    output.push_str("m2-syn live parser  (Ctrl-C/Ctrl-D exit, Backspace edits)\n\n");
49    output.push_str("SOURCE\n");
50    output.push_str(parser.source());
51    if !parser.source().ends_with('\n') {
52        output.push('\n');
53    }
54
55    let diagnostic_count = parser
56        .cells()
57        .iter()
58        .map(|cell| cell.lexer_diagnostics().len())
59        .sum::<usize>();
60    output.push_str(&format!(
61        "\nSTATE\n{:?}; {} cell(s); {} reparsed; {} lexer diagnostic(s)\n",
62        parser.state(),
63        parser.cells().len(),
64        parser.reparsed_cells().len(),
65        diagnostic_count,
66    ));
67    for cell in parser.cells() {
68        for diagnostic in cell.lexer_diagnostics() {
69            write_diagnostic(&mut output, diagnostic);
70        }
71    }
72
73    output.push_str("\nTOKENS\n");
74    for (index, cell) in parser.cells().iter().enumerate() {
75        output.push_str(&format!(
76            "cell {} · {:?} · {:?}\n",
77            index + 1,
78            cell.boundary(),
79            cell.state()
80        ));
81        output.push_str(&format_pretty_tokens(cell.tokens()));
82    }
83
84    output.push_str("\nSYNTAX\n");
85    for (index, cell) in parser.cells().iter().enumerate() {
86        output.push_str(&format!("cell {}\n", index + 1));
87        if let Some(syntax) = cell.syntax() {
88            output.push_str(&format_pretty_tree(syntax));
89        }
90        if let Some(error) = cell.parse_error() {
91            output.push_str(&format!("{error}\n"));
92        }
93    }
94    output
95}
96
97fn main() -> io::Result<()> {
98    let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
99    let _terminal = TerminalMode::raw();
100    let mut bytes = Vec::new();
101    let mut parser = NativeRealtimeParser::new("");
102    let mut stdout = io::stdout().lock();
103
104    if interactive {
105        stdout.write_all(render(&parser, true).as_bytes())?;
106        stdout.flush()?;
107    }
108
109    for byte in io::stdin().lock().bytes() {
110        let byte = byte?;
111        match byte {
112            3 | 4 => break,
113            8 | 127 => {
114                if let Ok(source) = std::str::from_utf8(&bytes)
115                    && let Some((index, _)) = source.char_indices().next_back()
116                {
117                    bytes.truncate(index);
118                }
119            }
120            b'\r' => bytes.push(b'\n'),
121            byte => bytes.push(byte),
122        }
123        let Ok(source) = std::str::from_utf8(&bytes) else {
124            continue;
125        };
126        if let Some(appended) = source.strip_prefix(parser.source()) {
127            parser.append(appended);
128        } else if parser.source().starts_with(source) {
129            parser
130                .apply_edit(&TextEdit::deletion(source.len(), parser.source().len()))
131                .expect("backspace always removes a valid source suffix");
132        } else {
133            parser = NativeRealtimeParser::new(source);
134        }
135        if interactive {
136            stdout.write_all(render(&parser, true).as_bytes())?;
137            stdout.flush()?;
138        }
139    }
140
141    if !interactive {
142        let source = String::from_utf8(bytes)
143            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
144        debug_assert_eq!(source, parser.source());
145        stdout.write_all(render(&parser, false).as_bytes())?;
146    }
147    Ok(())
148}