Skip to content

Commit aea9e68

Browse files
authored
feat: M1.4.3 - Beatrice REPL Implementation (Interactive Shell) (#228)
* feat: add REPL module structure with special commands (Task 1) * feat: implement REPL main loop with special command handling (Task 2) * test: add REPL integration tests for all features (Task 3) * docs: add comprehensive REPL implementation documentation (Task 4)
1 parent 77d5bc6 commit aea9e68

7 files changed

Lines changed: 997 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//! Command completion for REPL (stub for future enhancement)
2+
3+
#[derive(Debug, Clone)]
4+
pub struct CompletionProvider;
5+
6+
impl CompletionProvider {
7+
pub fn new() -> Self {
8+
Self
9+
}
10+
11+
pub fn complete(&self, _partial: &str) -> Vec<String> {
12+
vec![]
13+
}
14+
}
15+
16+
impl Default for CompletionProvider {
17+
fn default() -> Self {
18+
Self::new()
19+
}
20+
}

crates/mimi-cli/src/repl/editor.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Editor for REPL (Phase 1: minimal, Phase 2: rustyline)
2+
3+
#[derive(Debug, Clone)]
4+
pub struct ReplEditor {
5+
history: Vec<String>,
6+
max_history: usize,
7+
}
8+
9+
impl ReplEditor {
10+
pub fn new(max_history: usize) -> Self {
11+
Self {
12+
history: Vec::new(),
13+
max_history,
14+
}
15+
}
16+
17+
pub fn add_to_history(&mut self, line: &str) {
18+
self.history.push(line.to_string());
19+
if self.history.len() > self.max_history {
20+
self.history.remove(0);
21+
}
22+
}
23+
24+
pub fn get_history(&self, count: usize) -> Vec<String> {
25+
let start = if self.history.len() > count {
26+
self.history.len() - count
27+
} else {
28+
0
29+
};
30+
self.history[start..].to_vec()
31+
}
32+
}

crates/mimi-cli/src/repl/loop.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//! Main REPL event loop
2+
3+
use std::io::{self, BufRead, Write};
4+
5+
use super::{PromptState, ReplConfig, ReplEditor, SpecialCommand};
6+
7+
pub async fn run_repl(config: ReplConfig) -> io::Result<()> {
8+
let mut editor = ReplEditor::new(config.max_history);
9+
let stdin = io::stdin();
10+
let reader = stdin.lock();
11+
let mut lines = reader.lines();
12+
13+
let mut prompt_state = PromptState::Default;
14+
let mut multiline_buffer = String::new();
15+
16+
run_startup_script(&config, &mut editor).await;
17+
18+
print_prompt(prompt_state);
19+
20+
while let Some(Ok(line)) = lines.next() {
21+
let line = line.trim();
22+
23+
if line.is_empty() {
24+
print_prompt(prompt_state);
25+
continue;
26+
}
27+
28+
if line.ends_with('\\') {
29+
multiline_buffer.push_str(&line[..line.len() - 1]);
30+
multiline_buffer.push(' ');
31+
prompt_state = PromptState::Continuation;
32+
print_prompt(prompt_state);
33+
continue;
34+
}
35+
36+
if !multiline_buffer.is_empty() {
37+
multiline_buffer.push_str(line);
38+
} else {
39+
multiline_buffer = line.to_string();
40+
}
41+
42+
editor.add_to_history(&multiline_buffer);
43+
44+
match execute_line(&multiline_buffer, &mut editor) {
45+
LineExecution::Exit => break,
46+
LineExecution::Continue => {},
47+
}
48+
49+
multiline_buffer.clear();
50+
prompt_state = PromptState::Default;
51+
print_prompt(prompt_state);
52+
}
53+
54+
println!();
55+
Ok(())
56+
}
57+
58+
enum LineExecution {
59+
Exit,
60+
Continue,
61+
}
62+
63+
fn execute_line(line: &str, editor: &mut ReplEditor) -> LineExecution {
64+
if let Some(cmd) = SpecialCommand::parse(line) {
65+
let output = cmd.execute();
66+
67+
if cmd == SpecialCommand::Exit {
68+
return LineExecution::Exit;
69+
}
70+
71+
if cmd == SpecialCommand::Clear {
72+
print!("{}", output);
73+
let _ = io::stdout().flush();
74+
} else if matches!(cmd, SpecialCommand::History(_)) {
75+
let history_lines = editor.get_history(20);
76+
for (i, h_line) in history_lines.iter().enumerate() {
77+
println!(" {}: {}", i + 1, h_line);
78+
}
79+
} else {
80+
println!("{}", output);
81+
}
82+
} else {
83+
println!(
84+
"REPL: parsed command '{}' (execution deferred to M1.4.6)",
85+
line
86+
);
87+
}
88+
89+
LineExecution::Continue
90+
}
91+
92+
fn print_prompt(state: PromptState) {
93+
print!("{}", state.prompt_string());
94+
let _ = io::stdout().flush();
95+
}
96+
97+
async fn run_startup_script(config: &ReplConfig, editor: &mut ReplEditor) {
98+
if let Some(script_path) = &config.startup_script {
99+
if let Ok(content) = std::fs::read_to_string(script_path) {
100+
for script_line in content.lines() {
101+
let trimmed = script_line.trim();
102+
if !trimmed.is_empty() && !trimmed.starts_with('#') {
103+
println!("{}{}", PromptState::Default.prompt_string(), trimmed);
104+
editor.add_to_history(trimmed);
105+
match execute_line(trimmed, editor) {
106+
LineExecution::Exit => break,
107+
LineExecution::Continue => {},
108+
}
109+
}
110+
}
111+
}
112+
}
113+
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
119+
#[test]
120+
fn test_line_execution_exit() {
121+
let mut editor = ReplEditor::new(1000);
122+
let result = execute_line(".exit", &mut editor);
123+
assert!(matches!(result, LineExecution::Exit));
124+
}
125+
126+
#[test]
127+
fn test_line_execution_regular() {
128+
let mut editor = ReplEditor::new(1000);
129+
let result = execute_line("some command", &mut editor);
130+
assert!(matches!(result, LineExecution::Continue));
131+
}
132+
133+
#[test]
134+
fn test_print_prompt_default() {
135+
print_prompt(PromptState::Default);
136+
}
137+
}

crates/mimi-cli/src/repl/mod.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//! Interactive REPL for continuous user interaction
2+
3+
pub mod completion;
4+
pub mod editor;
5+
pub mod r#loop;
6+
pub mod special_commands;
7+
8+
pub use editor::ReplEditor;
9+
pub use r#loop::run_repl;
10+
pub use special_commands::SpecialCommand;
11+
12+
#[derive(Debug, Clone)]
13+
pub struct ReplConfig {
14+
pub history_file: String,
15+
pub max_history: usize,
16+
pub no_history: bool,
17+
pub startup_script: Option<String>,
18+
pub completion: bool,
19+
}
20+
21+
impl Default for ReplConfig {
22+
fn default() -> Self {
23+
Self {
24+
history_file: format!(
25+
"{}/.mimi/repl_history",
26+
std::env::var("HOME").unwrap_or_else(|_| ".".to_string())
27+
),
28+
max_history: 1000,
29+
no_history: false,
30+
startup_script: None,
31+
completion: true,
32+
}
33+
}
34+
}
35+
36+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37+
pub enum PromptState {
38+
Default,
39+
Following(u32),
40+
Debug,
41+
Continuation,
42+
}
43+
44+
impl PromptState {
45+
pub fn prompt_string(&self) -> String {
46+
match self {
47+
Self::Default => "mimi> ".to_string(),
48+
Self::Following(task_id) => format!("mimi [task-{}]> ", task_id),
49+
Self::Debug => "mimi [debug]> ".to_string(),
50+
Self::Continuation => "... ".to_string(),
51+
}
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use super::*;
58+
59+
#[test]
60+
fn test_default_config() {
61+
let config = ReplConfig::default();
62+
assert_eq!(config.max_history, 1000);
63+
assert!(!config.no_history);
64+
assert!(config.completion);
65+
}
66+
67+
#[test]
68+
fn test_prompt_strings() {
69+
assert_eq!(PromptState::Default.prompt_string(), "mimi> ");
70+
assert_eq!(
71+
PromptState::Following(123).prompt_string(),
72+
"mimi [task-123]> "
73+
);
74+
assert_eq!(PromptState::Debug.prompt_string(), "mimi [debug]> ");
75+
assert_eq!(PromptState::Continuation.prompt_string(), "... ");
76+
}
77+
}

0 commit comments

Comments
 (0)