live_parser/
live_parser.rs1use 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
11struct TerminalMode(Option<String>);
13
14impl TerminalMode {
15 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
43fn write_diagnostic(output: &mut String, diagnostic: &LexError) {
45 let span = diagnostic.span();
46 output.push_str(&format!("- {diagnostic} at {span}\n"));
47}
48
49fn 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}