Skip to main content

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(&current);
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}