1use crate::io::execute_r_with_path2;
10use crate::project::{write_header, write_to_r_lang};
11use rustyline::completion::Completer;
12use rustyline::error::ReadlineError;
13use rustyline::highlight::CmdKind;
14use rustyline::highlight::Highlighter;
15use rustyline::hint::Hinter;
16use rustyline::validate::Validator;
17use rustyline::Helper;
18use rustyline::{Config, Editor};
19use std::borrow::Cow;
20use std::fs;
21use std::fs::OpenOptions;
22use std::io::Write;
23use std::path::PathBuf;
24use typr_core::components::context::config::Environment;
25use typr_core::components::context::Context;
26use typr_core::utils::fluent_parser::FluentParser;
27
28mod colors {
30 pub const RESET: &str = "\x1b[0m";
31 pub const KEYWORD: &str = "\x1b[35m"; pub const FUNCTION: &str = "\x1b[36m"; pub const STRING: &str = "\x1b[32m"; pub const NUMBER: &str = "\x1b[33m"; pub const COMMENT: &str = "\x1b[90m"; pub const OPERATOR: &str = "\x1b[37m"; pub const BRACKET: &str = "\x1b[93m"; pub const ERROR: &str = "\x1b[91m"; pub const OUTPUT: &str = "\x1b[34m"; }
41
42#[derive(Clone)]
44struct RHighlighter;
45
46impl RHighlighter {
47 fn new() -> Self {
48 RHighlighter
49 }
50
51 fn is_r_keyword(word: &str) -> bool {
52 matches!(
53 word,
54 "if" | "else"
55 | "while"
56 | "for"
57 | "in"
58 | "repeat"
59 | "break"
60 | "next"
61 | "function"
62 | "return"
63 | "TRUE"
64 | "FALSE"
65 | "true"
66 | "false"
67 | "NULL"
68 | "NA"
69 | "NaN"
70 | "Inf"
71 | "library"
72 | "require"
73 | "source"
74 | "let"
75 | "type"
76 | "fn"
77 )
78 }
79
80 fn is_r_function(word: &str) -> bool {
81 matches!(
82 word,
83 "print"
84 | "cat"
85 | "paste"
86 | "paste0"
87 | "length"
88 | "sum"
89 | "mean"
90 | "median"
91 | "sd"
92 | "var"
93 | "min"
94 | "max"
95 | "range"
96 | "c"
97 | "list"
98 | "data.frame"
99 | "matrix"
100 | "array"
101 | "factor"
102 | "as.numeric"
103 | "as.character"
104 | "as.logical"
105 | "str"
106 | "summary"
107 | "head"
108 | "tail"
109 | "dim"
110 | "nrow"
111 | "ncol"
112 | "names"
113 | "colnames"
114 | "rownames"
115 | "seq"
116 | "rep"
117 | "sort"
118 | "order"
119 | "unique"
120 | "table"
121 | "subset"
122 | "merge"
123 | "rbind"
124 | "cbind"
125 | "apply"
126 | "lapply"
127 | "sapply"
128 | "tapply"
129 )
130 }
131
132 fn highlight_code(code: &str) -> String {
133 let mut result = String::new();
134 let mut chars = code.chars().peekable();
135 let mut in_string = false;
136 let mut string_delim = ' ';
137 let mut in_comment = false;
138 let mut current_word = String::new();
139
140 while let Some(ch) = chars.next() {
141 if ch == '#' && !in_string {
143 in_comment = true;
144 if !current_word.is_empty() {
145 result.push_str(&Self::colorize_word(¤t_word));
146 current_word.clear();
147 }
148 result.push_str(colors::COMMENT);
149 result.push(ch);
150 continue;
151 }
152
153 if in_comment {
154 result.push(ch);
155 if ch == '\n' {
156 result.push_str(colors::RESET);
157 in_comment = false;
158 }
159 continue;
160 }
161
162 if (ch == '"' || ch == '\'') && !in_string {
164 if !current_word.is_empty() {
165 result.push_str(&Self::colorize_word(¤t_word));
166 current_word.clear();
167 }
168 in_string = true;
169 string_delim = ch;
170 result.push_str(colors::STRING);
171 result.push(ch);
172 continue;
173 }
174
175 if in_string {
176 result.push(ch);
177 if ch == string_delim && chars.peek() != Some(&'\\') {
178 in_string = false;
179 result.push_str(colors::RESET);
180 }
181 continue;
182 }
183
184 if ch.is_numeric() || (ch == '.' && chars.peek().map_or(false, |c| c.is_numeric())) {
186 if !current_word.is_empty() {
187 result.push_str(&Self::colorize_word(¤t_word));
188 current_word.clear();
189 }
190 result.push_str(colors::NUMBER);
191 result.push(ch);
192 while let Some(&next_ch) = chars.peek() {
193 if next_ch.is_numeric() || next_ch == '.' || next_ch == 'e' || next_ch == 'E' {
194 result.push(chars.next().unwrap());
195 } else {
196 break;
197 }
198 }
199 result.push_str(colors::RESET);
200 continue;
201 }
202
203 if "+-*/<>=!&|:".contains(ch) {
205 if !current_word.is_empty() {
206 result.push_str(&Self::colorize_word(¤t_word));
207 current_word.clear();
208 }
209 result.push_str(colors::OPERATOR);
210 result.push(ch);
211 if let Some(&next_ch) = chars.peek() {
213 if matches!(
214 (ch, next_ch),
215 ('<', '-')
216 | ('-', '>')
217 | ('=', '=')
218 | ('!', '=')
219 | ('<', '=')
220 | ('>', '=')
221 | ('&', '&')
222 | ('|', '|')
223 ) {
224 result.push(chars.next().unwrap());
225 }
226 }
227 result.push_str(colors::RESET);
228 continue;
229 }
230
231 if "()[]{}".contains(ch) {
233 if !current_word.is_empty() {
234 result.push_str(&Self::colorize_word(¤t_word));
235 current_word.clear();
236 }
237 result.push_str(colors::BRACKET);
238 result.push(ch);
239 result.push_str(colors::RESET);
240 continue;
241 }
242
243 if ch.is_alphanumeric() || ch == '_' || ch == '.' {
245 current_word.push(ch);
246 } else {
247 if !current_word.is_empty() {
248 result.push_str(&Self::colorize_word(¤t_word));
249 current_word.clear();
250 }
251 result.push(ch);
252 }
253 }
254
255 if !current_word.is_empty() {
257 result.push_str(&Self::colorize_word(¤t_word));
258 }
259
260 if in_comment {
261 result.push_str(colors::RESET);
262 }
263
264 result
265 }
266
267 fn colorize_word(word: &str) -> String {
268 if Self::is_r_keyword(word) {
269 format!("{}{}{}", colors::KEYWORD, word, colors::RESET)
270 } else if Self::is_r_function(word) {
271 format!("{}{}{}", colors::FUNCTION, word, colors::RESET)
272 } else {
273 word.to_string()
274 }
275 }
276}
277
278impl Highlighter for RHighlighter {
279 fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
280 Cow::Owned(Self::highlight_code(line))
281 }
282
283 fn highlight_char(&self, _line: &str, _pos: usize, _cmd_kind: CmdKind) -> bool {
284 true
285 }
286}
287
288impl Hinter for RHighlighter {
289 type Hint = String;
290}
291
292impl Completer for RHighlighter {
293 type Candidate = String;
294}
295
296impl Validator for RHighlighter {}
297
298impl Helper for RHighlighter {}
299
300#[derive(Debug, Clone)]
302pub struct ExecutionResult {
303 pub output: Vec<String>,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq)]
308enum InputState {
309 Normal,
310 MultiLine,
311}
312
313pub struct CliInterface {
315 editor: Editor<RHighlighter, rustyline::history::DefaultHistory>,
316 input_state: InputState,
317 command_buffer: String,
318 history_file: String,
319}
320
321impl CliInterface {
322 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
324 let config = Config::builder().auto_add_history(true).build();
325
326 let highlighter = RHighlighter::new();
327 let mut editor = Editor::with_config(config)?;
328 editor.set_helper(Some(highlighter));
329
330 let history_file = std::env::var("HOME")
332 .or_else(|_| std::env::var("USERPROFILE"))
333 .map(|home| format!("{}/.r_repl_history", home))
334 .unwrap_or_else(|_| ".r_repl_history".to_string());
335
336 let _ = editor.load_history(&history_file);
338
339 Ok(CliInterface {
340 editor,
341 input_state: InputState::Normal,
342 command_buffer: String::new(),
343 history_file,
344 })
345 }
346
347 pub fn show_welcome(&self) {
349 println!(
350 "{}TypR REPL{}: 'exit' to quit",
351 colors::KEYWORD,
352 colors::RESET
353 );
354 }
355
356 pub fn read_line(&mut self) -> Result<String, ReadlineError> {
358 let prompt = match self.input_state {
359 InputState::Normal => format!("{}TypR>{} ", colors::KEYWORD, colors::RESET),
360 InputState::MultiLine => format!("{}...{} ", colors::OPERATOR, colors::RESET),
361 };
362
363 self.editor.readline(&prompt)
364 }
365
366 pub fn process_input(&mut self, input: &str) -> Option<MyCommand> {
368 let trimmed = input.trim();
369
370 if self.input_state == InputState::Normal {
372 match trimmed {
373 "exit" | "quit" => return Some(MyCommand::Exit),
374 "clear" => return Some(MyCommand::Clear),
375 "" => return Some(MyCommand::Empty),
376 _ => {}
377 }
378 }
379
380 if self.input_state == InputState::MultiLine {
382 self.command_buffer.push('\n');
383 }
384 self.command_buffer.push_str(trimmed);
385
386 if self.is_command_complete(&self.command_buffer) {
388 let cmd = self.command_buffer.clone();
389 self.command_buffer.clear();
390 self.input_state = InputState::Normal;
391 Some(MyCommand::Execute(cmd))
392 } else {
393 self.input_state = InputState::MultiLine;
394 None
395 }
396 }
397
398 fn is_command_complete(&self, cmd: &str) -> bool {
400 let open_braces = cmd.matches('{').count();
401 let close_braces = cmd.matches('}').count();
402 let open_parens = cmd.matches('(').count();
403 let close_parens = cmd.matches(')').count();
404 let open_brackets = cmd.matches('[').count();
405 let close_brackets = cmd.matches(']').count();
406
407 open_braces == close_braces
408 && open_parens == close_parens
409 && open_brackets == close_brackets
410 }
411
412 pub fn display_result(&self, result: &ExecutionResult) {
414 for line in &result.output {
415 println!("{}{}{}", colors::OUTPUT, line, colors::RESET);
416 }
417 }
418
419 pub fn display_error(&self, error: &str) {
421 eprintln!("{}Error: {}{}", colors::ERROR, error, colors::RESET);
422 }
423
424 pub fn clear_screen(&mut self) {
426 self.editor.clear_screen().ok();
427 }
428
429 pub fn save_history(&mut self) {
431 if let Err(e) = self.editor.save_history(&self.history_file) {
432 eprintln!("Warning: Unable to save history: {}", e);
433 }
434 }
435
436 pub fn reset_multiline_state(&mut self) {
438 self.input_state = InputState::Normal;
439 self.command_buffer.clear();
440 }
441}
442
443#[derive(Debug)]
445pub enum MyCommand {
446 Execute(String),
447 Exit,
448 Clear,
449 Empty,
450}
451
452#[derive(Debug, Clone)]
453struct TypRExecutor {
454 api: FluentParser,
455}
456
457impl TypRExecutor {
458 pub fn new() -> Self {
459 TypRExecutor {
460 api: FluentParser::new().set_context(Context::default()),
461 }
462 }
463
464 fn get_r_code(self, cmd: &str) -> (Self, String, String) {
465 let (r_code, api) = self.api.push(cmd).run().next_r_code().unwrap();
466 let r_type = api.get_last_type().pretty2();
467 let res = Self {
468 api: api.clone(),
469 ..self
470 };
471 let saved_code = format!("{}\n{}", api.get_saved_r_code(), r_code);
472 (res, saved_code, r_type)
473 }
474
475 fn run_r_code(context: Context, r_code: &str, r_type: &str) -> String {
476 let dir = PathBuf::from(".");
477 let r_file_name = ".repl.R";
478 let _ = fs::remove_file(r_file_name);
479 let mut file = OpenOptions::new()
480 .write(true)
481 .create(true)
482 .open(r_file_name)
483 .unwrap();
484 let _ = file.write_all("source('a_std.R')\n".as_bytes());
485 write_header(context, &dir, Environment::Repl);
486 write_to_r_lang(r_code.to_string(), &dir, r_file_name, Environment::Repl);
487 println!("{}{}{}", colors::NUMBER, r_type, colors::RESET);
488 let res = execute_r_with_path2(&dir, r_file_name);
489 res
490 }
491
492 fn execute(self, cmd: &str) -> Result<(Self, ExecutionResult), String> {
493 let (new, r_code, r_type) = Self::get_r_code(self, cmd);
494 let res = Self::run_r_code(new.api.context.clone(), &r_code, &r_type);
495 Ok((new, ExecutionResult { output: vec![res] }))
496 }
497}
498
499pub struct RRepl {
501 executor: TypRExecutor,
502 cli: CliInterface,
503}
504
505impl RRepl {
506 pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
508 let executor = TypRExecutor::new();
509 let cli = CliInterface::new()?;
510
511 Ok(RRepl { executor, cli })
512 }
513
514 pub fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
516 self.cli.show_welcome();
517
518 loop {
519 match self.cli.read_line() {
520 Ok(line) => {
521 if let Some(command) = self.cli.process_input(&line) {
522 match command {
523 MyCommand::Execute(cmd) => match self.executor.clone().execute(&cmd) {
524 Ok((executor, result)) => {
525 self.executor = executor;
526 self.cli.display_result(&result)
527 }
528 Err(e) => {
529 self.cli.display_error(&format!("Execution failed: {}", e))
530 }
531 },
532 MyCommand::Exit => {
533 println!("\nexiting...");
534 break;
535 }
536 MyCommand::Clear => {
537 self.cli.clear_screen();
538 }
539 MyCommand::Empty => {
540 }
542 }
543 }
544 }
545 Err(ReadlineError::Interrupted) => {
546 println!("\n^C");
548 self.cli.reset_multiline_state();
549 println!("exiting...");
550 break;
551 }
552 Err(ReadlineError::Eof) => {
553 println!("\nexiting...");
555 break;
556 }
557 Err(err) => {
558 self.cli.display_error(&format!("Read error: {}", err));
559 break;
560 }
561 }
562 }
563
564 self.cli.save_history();
566
567 Ok(())
568 }
569}
570
571pub fn start() {
573 match RRepl::new() {
574 Ok(mut repl) => {
575 if let Err(e) = repl.run() {
576 eprintln!("{}REPL error: {}{}", colors::ERROR, e, colors::RESET);
577 std::process::exit(1);
578 }
579 }
580 Err(e) => {
581 eprintln!(
582 "{}Unable to start R process: {}{}",
583 colors::ERROR,
584 e,
585 colors::RESET
586 );
587 eprintln!(" Check that R is installed and in PATH");
588 std::process::exit(1);
589 }
590 }
591}