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(¤t);
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}