rl_lang/repl/
logic_loop.rs1use super::{
36 command_handler::handle_command, depth_checker::is_complete, input_eval::eval_input,
37 lines_types::OutputLine, output_render::render_output, syntax_highlighting::highlight,
38 utils::char_to_byte,
39};
40use crossterm::event::{self, Event, KeyCode, KeyModifiers};
41use ratatui::{
42 DefaultTerminal,
43 layout::{Constraint, Direction, Layout},
44 style::{Color, Modifier, Style},
45 text::{Line, Span},
46 widgets::{Block, Borders, Paragraph, Wrap},
47};
48use std::path::PathBuf;
49
50use crate::interpreter::evaluator::Evaluator;
51
52pub fn run_repl(terminal: &mut DefaultTerminal) -> std::io::Result<()> {
59 let mut evaluator = Evaluator::default().with_stdlib();
60 let mut output: Vec<OutputLine> = vec![
61 OutputLine::Info(format!(
62 "rl-lang v{} - type :help for commands",
63 env!("CARGO_PKG_VERSION")
64 )),
65 OutputLine::Separator,
66 ];
67
68 let mut input_buf = String::new(); let mut accumulated = String::new(); let mut cursor_pos: usize = 0; let mut history: Vec<String> = Vec::new();
72 let mut history_idx: Option<usize> = None;
73 let mut attached: Vec<PathBuf> = Vec::new();
74 let mut scroll_offset: usize = 0;
75
76 loop {
77 let is_continuation = !accumulated.is_empty();
79 let prompt = if is_continuation { ".. " } else { ">> " };
80
81 terminal.draw(|frame| {
82 let area = frame.area();
83 let chunks = Layout::default()
84 .direction(Direction::Vertical)
85 .constraints([Constraint::Min(3), Constraint::Length(3)])
86 .split(area);
87
88 let out_lines = render_output(&output);
90 let total = out_lines.len();
91 let visible = chunks[0].height.saturating_sub(2) as usize;
92 let max_scroll = total.saturating_sub(visible);
93 let scroll = max_scroll.saturating_sub(scroll_offset) as u16;
95
96 let output_widget = Paragraph::new(out_lines)
97 .block(
98 Block::default()
99 .borders(Borders::ALL)
100 .border_style(Style::default().fg(Color::DarkGray))
101 .title(Span::styled(
102 " rl ",
103 Style::default()
104 .fg(Color::Cyan)
105 .add_modifier(Modifier::BOLD),
106 )),
107 )
108 .wrap(Wrap { trim: false })
109 .scroll((scroll, 0));
110 frame.render_widget(output_widget, chunks[0]);
111
112 let prompt_color = if is_continuation {
114 Color::Yellow
115 } else {
116 Color::Cyan
117 };
118 let mut input_spans = vec![Span::styled(
119 prompt,
120 Style::default()
121 .fg(prompt_color)
122 .add_modifier(Modifier::BOLD),
123 )];
124
125 if input_buf.is_empty() {
126 input_spans.push(Span::styled("│", Style::default().fg(Color::DarkGray)));
128 } else {
129 let before: String = input_buf.chars().take(cursor_pos).collect();
131 let mut after_chars = input_buf.chars().skip(cursor_pos);
132
133 let mut hl = highlight(&before);
134 input_spans.append(&mut hl);
135
136 match after_chars.next() {
137 None => {
138 input_spans.push(Span::styled(
140 " ",
141 Style::default().bg(Color::Cyan).fg(Color::Black),
142 ));
143 }
144 Some(c) => {
145 let cursor_str = c.to_string();
146 let rest: String = after_chars.collect();
147 input_spans.push(Span::styled(
148 cursor_str,
149 Style::default().bg(Color::Cyan).fg(Color::Black),
150 ));
151 let mut hl2 = highlight(&rest);
152 input_spans.append(&mut hl2);
153 }
154 }
155 }
156
157 let input_widget = Paragraph::new(Line::from(input_spans)).block(
158 Block::default()
159 .borders(Borders::ALL)
160 .border_style(Style::default().fg(Color::DarkGray)),
161 );
162 frame.render_widget(input_widget, chunks[1]);
163 })?;
164
165 if let Event::Key(key) = event::read()? {
167 match (key.modifiers, key.code) {
168 (KeyModifiers::CONTROL, KeyCode::Char('c')) => break,
170
171 (_, KeyCode::Enter) => {
173 let line = input_buf.clone();
174 input_buf.clear();
175 cursor_pos = 0;
176 history_idx = None;
177 scroll_offset = 0;
178
179 if line.trim().is_empty() && accumulated.is_empty() {
180 continue;
181 }
182
183 if accumulated.is_empty() && line.trim().starts_with(':') {
185 if line.trim() == ":exit" {
186 break;
187 }
188 output.push(OutputLine::Input(line.clone()));
189 handle_command(line.trim(), &mut output, &mut evaluator, &mut attached);
190 if !line.trim().is_empty() {
191 history.push(line);
192 }
193 continue;
194 }
195
196 let is_first_line = accumulated.is_empty();
198
199 if !accumulated.is_empty() {
200 accumulated.push('\n');
201 }
202 accumulated.push_str(&line);
203
204 if is_first_line {
207 output.push(OutputLine::Input(line.clone()));
208 } else {
209 output.push(OutputLine::Input(format!(".. {}", line)));
210 }
211
212 if is_complete(&accumulated)
213 && (accumulated.lines().count() > 1
214 || !accumulated.trim_end().ends_with('{'))
215 {
216 let full = accumulated.clone();
217 accumulated.clear();
218 if !full.trim().is_empty() {
219 history.push(full.trim().to_string());
220 }
221
222 let success = eval_input(full.trim(), &mut evaluator, &mut output);
223 if success {
224 output.push(OutputLine::ValidInput(full.trim().to_string()));
225 }
226 }
227 }
228
229 (_, KeyCode::Backspace) if cursor_pos > 0 => {
231 let byte_pos = char_to_byte(&input_buf, cursor_pos - 1);
232 input_buf.remove(byte_pos);
233 cursor_pos -= 1;
234 }
235
236 (_, KeyCode::Delete) if cursor_pos < input_buf.chars().count() => {
238 let byte_pos = char_to_byte(&input_buf, cursor_pos);
239 input_buf.remove(byte_pos);
240 }
241
242 (KeyModifiers::CONTROL, KeyCode::Left) => {
244 let chars: Vec<char> = input_buf.chars().collect();
245 let mut i = cursor_pos;
246 while i > 0 && chars[i - 1] == ' ' {
247 i -= 1;
248 }
249 while i > 0 && chars[i - 1] != ' ' {
250 i -= 1;
251 }
252 cursor_pos = i;
253 }
254 (KeyModifiers::CONTROL, KeyCode::Right) => {
255 let chars: Vec<char> = input_buf.chars().collect();
256 let len = chars.len();
257 let mut i = cursor_pos;
258 while i < len && chars[i] != ' ' {
259 i += 1;
260 }
261 while i < len && chars[i] == ' ' {
262 i += 1;
263 }
264 cursor_pos = i;
265 }
266
267 (_, KeyCode::Left) if cursor_pos > 0 => {
269 cursor_pos = cursor_pos.saturating_sub(1);
270 }
271
272 (_, KeyCode::Right) if cursor_pos < input_buf.chars().count() => {
273 cursor_pos += 1;
274 }
275
276 (_, KeyCode::Home) => cursor_pos = 0,
277
278 (_, KeyCode::End) => cursor_pos = input_buf.chars().count(),
279
280 (KeyModifiers::SHIFT, KeyCode::Up) => {
282 scroll_offset += 1;
283 }
284
285 (KeyModifiers::SHIFT, KeyCode::Down) => {
286 scroll_offset = scroll_offset.saturating_sub(1);
287 }
288
289 (_, KeyCode::Up) => {
291 if history.is_empty() {
292 continue;
293 }
294 let new_idx = match history_idx {
295 None => history.len() - 1,
296 Some(0) => 0,
297 Some(i) => i - 1,
298 };
299 history_idx = Some(new_idx);
300 input_buf = history[new_idx].clone();
301 cursor_pos = input_buf.chars().count();
302 }
303
304 (_, KeyCode::Down) => match history_idx {
306 None => {}
307 Some(i) if i + 1 >= history.len() => {
308 history_idx = None;
309 input_buf.clear();
310 cursor_pos = 0;
311 }
312 Some(i) => {
313 history_idx = Some(i + 1);
314 input_buf = history[i + 1].clone();
315 cursor_pos = input_buf.chars().count();
316 }
317 },
318
319 (_, KeyCode::Esc) => {
321 if !accumulated.is_empty() {
322 accumulated.clear();
323 output.push(OutputLine::Info("cancelled".into()));
324 }
325 input_buf.clear();
326 cursor_pos = 0;
327 }
328
329 (_, KeyCode::Char(c)) => {
331 let byte_pos = char_to_byte(&input_buf, cursor_pos);
332 input_buf.insert(byte_pos, c);
333 cursor_pos += 1;
334 }
335
336 _ => {}
337 }
338 }
339 }
340
341 Ok(())
342}