live_parser/
live_parser.rs1use 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}