Skip to main content

live_parser/
live_parser.rs

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