Skip to main content

oak_repl/
lib.rs

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