oak_repl/lib.rs
1#![warn(missing_docs)]
2#![doc = include_str!("readme.md")]
3
4use crossterm::{
5 cursor::MoveToColumn,
6 event::{self, Event, KeyCode, KeyModifiers},
7 execute,
8 terminal::{self, Clear, ClearType},
9};
10use oak_highlight::{AnsiExporter, Exporter, HighlightResult, OakHighlighter};
11use std::io::{self, Write};
12
13use std::{
14 error::Error,
15 fmt::{Display, Formatter},
16};
17
18/// Errors that can occur during REPL execution.
19///
20/// This enum covers I/O errors from the terminal and custom errors
21/// from the language integration layer.
22#[derive(Debug)]
23pub enum ReplError {
24 /// An I/O error occurred during terminal communication or file access.
25 Io(std::io::Error),
26 /// A custom error occurred within the language-specific handler.
27 Other(String),
28}
29
30impl Display for ReplError {
31 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
32 match self {
33 ReplError::Io(e) => write!(f, "IO error: {}", e),
34 ReplError::Other(s) => write!(f, "{}", s),
35 }
36 }
37}
38
39impl Error for ReplError {}
40
41impl From<std::io::Error> for ReplError {
42 fn from(e: std::io::Error) -> Self {
43 ReplError::Io(e)
44 }
45}
46
47impl From<String> for ReplError {
48 fn from(s: String) -> Self {
49 ReplError::Other(s)
50 }
51}
52
53impl From<&str> for ReplError {
54 fn from(s: &str) -> Self {
55 ReplError::Other(s.to_string())
56 }
57}
58
59/// The result of handling a line in the REPL.
60///
61/// This indicates whether the REPL should continue running or terminate
62/// after processing the current input.
63pub enum HandleResult {
64 /// Continue the REPL session and wait for the next input.
65 Continue,
66 /// Exit the REPL session immediately.
67 Exit,
68}
69
70/// Interface for language integration in the REPL.
71///
72/// Implement this trait to provide language-specific behavior like
73/// syntax highlighting, completion checking, and code execution.
74///
75/// # Usage Scenario
76///
77/// The `ReplHandler` is used by [`OakRepl`] to:
78/// 1. Customize the prompt based on whether it's a new command or a continuation.
79/// 2. Provide syntax highlighting for the current input line.
80/// 3. Determine if a multi-line input is complete and ready for execution.
81/// 4. Execute the collected code and decide whether to continue the REPL loop.
82///
83/// # Example
84///
85/// ```rust
86/// use oak_repl::{HandleResult, ReplError, ReplHandler};
87///
88/// struct MyHandler;
89///
90/// impl ReplHandler for MyHandler {
91/// fn prompt(&self, is_continuation: bool) -> &str {
92/// if is_continuation { "... " } else { ">>> " }
93/// }
94///
95/// fn is_complete(&self, code: &str) -> bool {
96/// code.ends_with(';')
97/// }
98///
99/// fn handle_line(&mut self, line: &str) -> Result<HandleResult, ReplError> {
100/// println!("Executing: {}", line);
101/// Ok(HandleResult::Continue)
102/// }
103/// }
104/// ```
105pub trait ReplHandler {
106 /// Get syntax highlighting for the given code.
107 ///
108 /// Returns `None` if no highlighting should be applied.
109 fn highlight<'a>(&self, _code: &'a str) -> Option<HighlightResult<'a>> {
110 None
111 }
112
113 /// Returns the prompt string to display.
114 ///
115 /// `is_continuation` is true if the REPL is in multi-line input mode
116 /// (i.e., the previous line was not complete).
117 fn prompt(&self, is_continuation: bool) -> &str;
118
119 /// Checks if the current input buffer represents a complete statement.
120 ///
121 /// If this returns `false`, the REPL will enter multi-line mode and
122 /// allow the user to continue typing.
123 fn is_complete(&self, code: &str) -> bool;
124
125 /// Executes the given line (or multiple lines) of code.
126 ///
127 /// Returns a `HandleResult` indicating whether to continue or exit.
128 fn handle_line(&mut self, line: &str) -> Result<HandleResult, ReplError>;
129
130 /// Gets the current indentation level for the next line in multi-line mode.
131 ///
132 /// This is used for auto-indentation when the user presses Enter in
133 /// the middle of a multi-line block.
134 fn get_indent(&self, _code: &str) -> usize {
135 // No indentation by default
136 0
137 }
138}
139
140/// A buffer for managing lines of text in the REPL.
141///
142/// `LineBuffer` handles single and multi-line input, cursor positioning,
143/// and basic editing operations like insertion and backspace.
144pub struct LineBuffer {
145 /// The lines of text in the buffer.
146 lines: Vec<String>,
147 /// The index of the current line being edited.
148 current_line: usize,
149 /// The cursor position (character offset) within the current line.
150 cursor_pos: usize,
151}
152
153impl LineBuffer {
154 /// Creates a new empty `LineBuffer`.
155 pub fn new() -> Self {
156 Self { lines: vec![String::new()], current_line: 0, cursor_pos: 0 }
157 }
158
159 /// Inserts a character at the current cursor position.
160 pub fn insert(&mut self, ch: char) {
161 self.lines[self.current_line].insert(self.cursor_pos, ch);
162 self.cursor_pos += 1;
163 }
164
165 /// Removes the character before the current cursor position (backspace).
166 ///
167 /// Returns `true` if a character or line was removed, `false` if the
168 /// buffer was already at the very beginning.
169 pub fn backspace(&mut self) -> bool {
170 if self.cursor_pos > 0 {
171 self.cursor_pos -= 1;
172 self.lines[self.current_line].remove(self.cursor_pos);
173 true
174 }
175 else if self.current_line > 0 {
176 // Merge with the previous line
177 let current = self.lines.remove(self.current_line);
178 self.current_line -= 1;
179 self.cursor_pos = self.lines[self.current_line].chars().count();
180 self.lines[self.current_line].push_str(¤t);
181 true
182 }
183 else {
184 false
185 }
186 }
187
188 /// Returns the full text content of the buffer as a single string.
189 ///
190 /// Multiple lines are joined with newline characters.
191 pub fn full_text(&self) -> String {
192 self.lines.join("\n")
193 }
194
195 /// Clears the buffer and resets the cursor to the beginning.
196 pub fn clear(&mut self) {
197 self.lines = vec![String::new()];
198 self.current_line = 0;
199 self.cursor_pos = 0;
200 }
201
202 /// Returns `true` if the buffer is completely empty.
203 pub fn is_empty(&self) -> bool {
204 self.lines.len() == 1 && self.lines[0].is_empty()
205 }
206}
207
208/// The main REPL engine.
209///
210/// `OakRepl` manages the terminal interface, handles user input,
211/// and coordinates with a `ReplHandler` to provide language-specific
212/// functionality.
213///
214/// # Example
215///
216/// ```no_run
217/// use oak_repl::{HandleResult, OakRepl, ReplError, ReplHandler};
218///
219/// struct MyHandler;
220/// impl ReplHandler for MyHandler {
221/// fn prompt(&self, _: bool) -> &str {
222/// "> "
223/// }
224/// fn is_complete(&self, code: &str) -> bool {
225/// true
226/// }
227/// fn handle_line(&mut self, line: &str) -> Result<HandleResult, ReplError> {
228/// println!("You typed: {}", line);
229/// Ok(HandleResult::Continue)
230/// }
231/// }
232///
233/// let mut repl = OakRepl::new(MyHandler);
234/// repl.run().expect("REPL failed");
235/// ```
236pub struct OakRepl<H: ReplHandler> {
237 /// The handler that implements language-specific logic.
238 handler: H,
239}
240
241impl<H: ReplHandler> OakRepl<H> {
242 /// Creates a new `OakRepl` with the given handler.
243 pub fn new(handler: H) -> Self {
244 Self { handler }
245 }
246
247 /// Runs the REPL loop.
248 ///
249 /// This method takes control of the terminal (enabling raw mode)
250 /// and blocks until the user exits or an unrecoverable error occurs.
251 pub fn run(&mut self) -> Result<(), ReplError> {
252 let mut stdout = io::stdout();
253 let mut line_buf = LineBuffer::new();
254 let mut is_continuation = false;
255 let _highlighter = OakHighlighter::new();
256 let exporter = AnsiExporter;
257
258 terminal::enable_raw_mode()?;
259
260 loop {
261 // Draw the current line
262 execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
263 let prompt = self.handler.prompt(is_continuation);
264
265 let current_line_text = &line_buf.lines[line_buf.current_line];
266
267 // Syntax highlighting
268 let displayed_text = if let Some(highlighted) = self.handler.highlight(current_line_text) { exporter.export(&highlighted) } else { current_line_text.clone() };
269
270 write!(stdout, "{}{}", prompt, displayed_text)?;
271
272 let cursor_col = (prompt.chars().count() + line_buf.cursor_pos) as u16;
273 execute!(stdout, MoveToColumn(cursor_col))?;
274 stdout.flush()?;
275
276 if let Event::Key(key_event) = event::read()? {
277 match key_event.code {
278 KeyCode::Char('c') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
279 println!("\nInterrupted");
280 line_buf.clear();
281 is_continuation = false;
282 continue;
283 }
284 KeyCode::Char('d') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
285 if line_buf.is_empty() {
286 println!("\nEOF");
287 break;
288 }
289 }
290 KeyCode::Char(ch) => {
291 line_buf.insert(ch);
292 }
293 KeyCode::Enter => {
294 let full_code = line_buf.full_text();
295
296 if self.handler.is_complete(&full_code) {
297 terminal::disable_raw_mode()?;
298 println!();
299
300 match self.handler.handle_line(&full_code) {
301 Ok(HandleResult::Exit) => break,
302 Ok(HandleResult::Continue) => {}
303 Err(e) => eprintln!("Error: {}", e),
304 }
305
306 line_buf.clear();
307 is_continuation = false;
308 terminal::enable_raw_mode()?;
309 }
310 else {
311 // Continue multi-line input
312 println!();
313 line_buf.lines.push(String::new());
314 line_buf.current_line += 1;
315 line_buf.cursor_pos = 0;
316 is_continuation = true;
317
318 // Auto-indent
319 let indent_size = self.handler.get_indent(&full_code);
320 for _ in 0..indent_size {
321 line_buf.insert(' ');
322 }
323 }
324 }
325 KeyCode::Backspace => {
326 line_buf.backspace();
327 }
328 KeyCode::Left => {
329 if line_buf.cursor_pos > 0 {
330 line_buf.cursor_pos -= 1;
331 }
332 }
333 KeyCode::Right => {
334 if line_buf.cursor_pos < line_buf.lines[line_buf.current_line].chars().count() {
335 line_buf.cursor_pos += 1
336 }
337 }
338 _ => {}
339 }
340 }
341 }
342
343 terminal::disable_raw_mode()?;
344 Ok(())
345 }
346}