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#[derive(Debug)]
23pub enum ReplError {
24    /// An I/O error occurred.
25    Io(std::io::Error),
26    /// A custom error occurred.
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.
60pub enum HandleResult {
61    /// Continue the REPL session.
62    Continue,
63    /// Exit the REPL session.
64    Exit,
65}
66
67/// Interface for language integration in the REPL.
68pub trait ReplHandler {
69    /// Get syntax highlighting for the given code.
70    fn highlight<'a>(&self, _code: &'a str) -> Option<HighlightResult<'a>> {
71        None
72    }
73
74    /// The prompt to display. `is_continuation` is true for multi-line input.
75    fn prompt(&self, is_continuation: bool) -> &str;
76
77    /// Check if the input is complete (e.g., all brackets are closed).
78    /// If it returns false, the REPL will enter multi-line input mode.
79    fn is_complete(&self, code: &str) -> bool;
80
81    /// Execute the given line of code.
82    fn handle_line(&mut self, line: &str) -> Result<HandleResult, ReplError>;
83
84    /// Get the current indentation level for the next line in multi-line mode.
85    fn get_indent(&self, _code: &str) -> usize {
86        // No indentation by default
87        0
88    }
89}
90
91/// A buffer for managing lines of text in the REPL.
92pub struct LineBuffer {
93    /// The lines of text in the buffer.
94    lines: Vec<String>,
95    /// The index of the current line being edited.
96    current_line: usize,
97    /// The cursor position within the current line.
98    cursor_pos: usize,
99}
100
101impl LineBuffer {
102    /// Create a new empty line buffer.
103    pub fn new() -> Self {
104        Self { lines: vec![String::new()], current_line: 0, cursor_pos: 0 }
105    }
106
107    /// Insert a character at the current cursor position.
108    pub fn insert(&mut self, ch: char) {
109        self.lines[self.current_line].insert(self.cursor_pos, ch);
110        self.cursor_pos += 1;
111    }
112
113    /// Remove a character before the current cursor position.
114    pub fn backspace(&mut self) -> bool {
115        if self.cursor_pos > 0 {
116            self.cursor_pos -= 1;
117            self.lines[self.current_line].remove(self.cursor_pos);
118            true
119        }
120        else if self.current_line > 0 {
121            // Merge with the previous line
122            let current = self.lines.remove(self.current_line);
123            self.current_line -= 1;
124            self.cursor_pos = self.lines[self.current_line].chars().count();
125            self.lines[self.current_line].push_str(&current);
126            true
127        }
128        else {
129            false
130        }
131    }
132
133    /// Get the full text content of the buffer.
134    pub fn full_text(&self) -> String {
135        self.lines.join("\n")
136    }
137
138    /// Clear the buffer.
139    pub fn clear(&mut self) {
140        self.lines = vec![String::new()];
141        self.current_line = 0;
142        self.cursor_pos = 0;
143    }
144
145    /// Returns true if the buffer is empty.
146    pub fn is_empty(&self) -> bool {
147        self.lines.len() == 1 && self.lines[0].is_empty()
148    }
149}
150
151/// The main REPL engine.
152pub struct OakRepl<H: ReplHandler> {
153    /// The handler that implements language-specific logic.
154    handler: H,
155}
156
157impl<H: ReplHandler> OakRepl<H> {
158    /// Create a new Oak REPL with the given handler.
159    pub fn new(handler: H) -> Self {
160        Self { handler }
161    }
162
163    /// Run the REPL loop.
164    pub fn run(&mut self) -> Result<(), ReplError> {
165        let mut stdout = io::stdout();
166        let mut line_buf = LineBuffer::new();
167        let mut is_continuation = false;
168        let _highlighter = OakHighlighter::new();
169        let exporter = AnsiExporter;
170
171        terminal::enable_raw_mode()?;
172
173        loop {
174            // Draw the current line
175            execute!(stdout, MoveToColumn(0), Clear(ClearType::CurrentLine))?;
176            let prompt = self.handler.prompt(is_continuation);
177
178            let current_line_text = &line_buf.lines[line_buf.current_line];
179
180            // Syntax highlighting
181            let displayed_text = if let Some(highlighted) = self.handler.highlight(current_line_text) { exporter.export(&highlighted) } else { current_line_text.clone() };
182
183            write!(stdout, "{}{}", prompt, displayed_text)?;
184
185            let cursor_col = (prompt.chars().count() + line_buf.cursor_pos) as u16;
186            execute!(stdout, MoveToColumn(cursor_col))?;
187            stdout.flush()?;
188
189            if let Event::Key(key_event) = event::read()? {
190                match key_event.code {
191                    KeyCode::Char('c') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
192                        println!("\nInterrupted");
193                        line_buf.clear();
194                        is_continuation = false;
195                        continue;
196                    }
197                    KeyCode::Char('d') if key_event.modifiers.contains(KeyModifiers::CONTROL) => {
198                        if line_buf.is_empty() {
199                            println!("\nEOF");
200                            break;
201                        }
202                    }
203                    KeyCode::Char(ch) => {
204                        line_buf.insert(ch);
205                    }
206                    KeyCode::Enter => {
207                        let full_code = line_buf.full_text();
208
209                        if self.handler.is_complete(&full_code) {
210                            terminal::disable_raw_mode()?;
211                            println!();
212
213                            match self.handler.handle_line(&full_code) {
214                                Ok(HandleResult::Exit) => break,
215                                Ok(HandleResult::Continue) => {}
216                                Err(e) => eprintln!("Error: {}", e),
217                            }
218
219                            line_buf.clear();
220                            is_continuation = false;
221                            terminal::enable_raw_mode()?;
222                        }
223                        else {
224                            // Continue multi-line input
225                            println!();
226                            line_buf.lines.push(String::new());
227                            line_buf.current_line += 1;
228                            line_buf.cursor_pos = 0;
229                            is_continuation = true;
230
231                            // Auto-indent
232                            let indent_size = self.handler.get_indent(&full_code);
233                            for _ in 0..indent_size {
234                                line_buf.insert(' ');
235                            }
236                        }
237                    }
238                    KeyCode::Backspace => {
239                        line_buf.backspace();
240                    }
241                    KeyCode::Left => {
242                        if line_buf.cursor_pos > 0 {
243                            line_buf.cursor_pos -= 1;
244                        }
245                    }
246                    KeyCode::Right => {
247                        if line_buf.cursor_pos < line_buf.lines[line_buf.current_line].chars().count() {
248                            line_buf.cursor_pos += 1
249                        }
250                    }
251                    _ => {}
252                }
253            }
254        }
255
256        terminal::disable_raw_mode()?;
257        Ok(())
258    }
259}