/home/noah/src/ruchy/src/runtime/repl_recording.rs
Line | Count | Source |
1 | | //! Refactored recording functionality with reduced complexity |
2 | | //! |
3 | | //! Following TDD approach: Each function has complexity < 20 |
4 | | |
5 | | use crate::runtime::repl::Repl; |
6 | | use crate::runtime::replay::{SessionMetadata, SessionRecorder, InputMode}; |
7 | | use crate::runtime::completion::RuchyCompleter; |
8 | | use crate::runtime::Value; |
9 | | use anyhow::Result; |
10 | | use colored::Colorize; |
11 | | use rustyline::{Config, CompletionType, EditMode}; |
12 | | use rustyline::history::DefaultHistory; |
13 | | use std::path::Path; |
14 | | use std::time::SystemTime; |
15 | | |
16 | | impl Repl { |
17 | | /// Create session metadata for recording (complexity: 3) |
18 | 0 | fn create_session_metadata() -> Result<SessionMetadata> { |
19 | | Ok(SessionMetadata { |
20 | 0 | session_id: format!("ruchy-session-{}", |
21 | 0 | SystemTime::now() |
22 | 0 | .duration_since(SystemTime::UNIX_EPOCH)? |
23 | 0 | .as_secs()), |
24 | 0 | created_at: chrono::Utc::now().to_rfc3339(), |
25 | 0 | ruchy_version: env!("CARGO_PKG_VERSION").to_string(), |
26 | 0 | student_id: None, |
27 | 0 | assignment_id: None, |
28 | 0 | tags: vec!["interactive".to_string()], |
29 | | }) |
30 | 0 | } |
31 | | |
32 | | /// Setup rustyline editor with configuration (complexity: 5) |
33 | 0 | fn setup_recording_editor(&self) -> Result<rustyline::Editor<RuchyCompleter, DefaultHistory>> { |
34 | 0 | let config = Config::builder() |
35 | 0 | .history_ignore_space(true) |
36 | 0 | .history_ignore_dups(true)? |
37 | 0 | .completion_type(CompletionType::List) |
38 | 0 | .edit_mode(EditMode::Emacs) |
39 | 0 | .build(); |
40 | | |
41 | 0 | let mut rl = rustyline::Editor::<RuchyCompleter, DefaultHistory>::with_config(config)?; |
42 | | |
43 | 0 | let completer = RuchyCompleter::new(); |
44 | 0 | rl.set_helper(Some(completer)); |
45 | | |
46 | | // Create a session-specific directory for history |
47 | 0 | let temp_dir = std::env::temp_dir().join(format!("ruchy-{}", std::process::id())); |
48 | 0 | std::fs::create_dir_all(&temp_dir)?; |
49 | 0 | let history_path = temp_dir.join("history.txt"); |
50 | 0 | let _ = rl.load_history(&history_path); |
51 | | |
52 | 0 | Ok(rl) |
53 | 0 | } |
54 | | |
55 | | /// Process single line input during recording (complexity: 8) |
56 | 0 | fn process_recorded_input( |
57 | 0 | &mut self, |
58 | 0 | line: String, |
59 | 0 | recorder: &mut SessionRecorder, |
60 | 0 | rl: &mut rustyline::Editor<RuchyCompleter, DefaultHistory>, |
61 | 0 | ) -> Result<bool> { |
62 | 0 | let input = line.trim(); |
63 | | |
64 | | // Record the input |
65 | 0 | let _input_id = recorder.record_input( |
66 | 0 | line.clone(), |
67 | 0 | InputMode::Interactive |
68 | | ); |
69 | | |
70 | | // Check for quit commands |
71 | 0 | if input == ":quit" || input == ":exit" { |
72 | 0 | return Ok(true); // Signal to exit |
73 | 0 | } |
74 | | |
75 | 0 | if !input.is_empty() { |
76 | 0 | rl.add_history_entry(input)?; |
77 | | |
78 | | // Evaluate and record result |
79 | 0 | let result = self.eval(input); |
80 | 0 | let result_for_recording = match &result { |
81 | 0 | Ok(s) => Ok(Value::String(s.clone())), |
82 | 0 | Err(e) => Err(anyhow::anyhow!("{}", e)), |
83 | | }; |
84 | 0 | recorder.record_output(result_for_recording); |
85 | | |
86 | | // Display result |
87 | 0 | match result { |
88 | 0 | Ok(output) if !output.is_empty() => { |
89 | 0 | println!("{output}"); |
90 | 0 | } |
91 | 0 | Err(e) => { |
92 | 0 | eprintln!("{}: {}", "Error".bright_red(), e); |
93 | 0 | } |
94 | 0 | _ => {} |
95 | | } |
96 | 0 | } |
97 | | |
98 | 0 | Ok(false) // Continue running |
99 | 0 | } |
100 | | |
101 | | /// Process multiline input during recording (complexity: 10) |
102 | 0 | fn process_multiline_recorded_input( |
103 | 0 | &mut self, |
104 | 0 | line: String, |
105 | 0 | multiline_buffer: &mut String, |
106 | 0 | in_multiline: &mut bool, |
107 | 0 | recorder: &mut SessionRecorder, |
108 | 0 | rl: &mut rustyline::Editor<RuchyCompleter, DefaultHistory>, |
109 | 0 | ) -> Result<()> { |
110 | 0 | let input = line.trim(); |
111 | | |
112 | 0 | if input.is_empty() { |
113 | | // Empty line ends multiline input |
114 | 0 | let full_input = multiline_buffer.trim().to_string(); |
115 | 0 | if !full_input.is_empty() { |
116 | 0 | rl.add_history_entry(&full_input)?; |
117 | | |
118 | | // Evaluate and record result |
119 | 0 | let result = self.eval(&full_input); |
120 | 0 | let result_for_recording = match &result { |
121 | 0 | Ok(s) => Ok(Value::String(s.clone())), |
122 | 0 | Err(e) => Err(anyhow::anyhow!("{}", e)), |
123 | | }; |
124 | 0 | recorder.record_output(result_for_recording); |
125 | | |
126 | 0 | match result { |
127 | 0 | Ok(output) if !output.is_empty() => { |
128 | 0 | println!("{output}"); |
129 | 0 | } |
130 | 0 | Err(e) => { |
131 | 0 | eprintln!("{}: {}", "Error".bright_red(), e); |
132 | 0 | } |
133 | 0 | _ => {} |
134 | | } |
135 | 0 | } |
136 | 0 | multiline_buffer.clear(); |
137 | 0 | *in_multiline = false; |
138 | 0 | } else { |
139 | 0 | multiline_buffer.push_str(&line); |
140 | 0 | multiline_buffer.push('\n'); |
141 | 0 | } |
142 | | |
143 | 0 | Ok(()) |
144 | 0 | } |
145 | | |
146 | | /// Main recording loop - refactored with reduced complexity (complexity: 15) |
147 | 0 | pub fn run_with_recording_refactored(&mut self, record_file: &Path) -> Result<()> { |
148 | | // Create session metadata |
149 | 0 | let metadata = Self::create_session_metadata()?; |
150 | 0 | let mut recorder = SessionRecorder::new(metadata); |
151 | | |
152 | 0 | println!("{}", format!("🎬 Recording session to: {}", record_file.display()).bright_yellow()); |
153 | | |
154 | | // Setup editor |
155 | 0 | let mut rl = self.setup_recording_editor()?; |
156 | | |
157 | 0 | let mut multiline_buffer = String::new(); |
158 | 0 | let mut in_multiline = false; |
159 | | |
160 | | // Main loop |
161 | | loop { |
162 | 0 | let prompt = if in_multiline { |
163 | 0 | format!("{} ", " ...".bright_black()) |
164 | | } else { |
165 | 0 | format!("{} ", self.get_prompt().bright_green()) |
166 | | }; |
167 | | |
168 | 0 | match rl.readline(&prompt) { |
169 | 0 | Ok(line) => { |
170 | 0 | if in_multiline { |
171 | | // Record multiline input |
172 | 0 | let _input_id = recorder.record_input( |
173 | 0 | line.clone(), |
174 | 0 | InputMode::Paste |
175 | | ); |
176 | | |
177 | 0 | self.process_multiline_recorded_input( |
178 | 0 | line, |
179 | 0 | &mut multiline_buffer, |
180 | 0 | &mut in_multiline, |
181 | 0 | &mut recorder, |
182 | 0 | &mut rl |
183 | 0 | )?; |
184 | | } else { |
185 | 0 | let input = line.trim(); |
186 | | |
187 | 0 | if Self::needs_continuation(input) { |
188 | 0 | // Start multiline input |
189 | 0 | multiline_buffer = format!("{line}\n"); |
190 | 0 | in_multiline = true; |
191 | 0 | |
192 | 0 | // Record the start of multiline |
193 | 0 | let _input_id = recorder.record_input( |
194 | 0 | line.clone(), |
195 | 0 | InputMode::Paste |
196 | 0 | ); |
197 | 0 | } else { |
198 | | // Process single line |
199 | 0 | let should_exit = self.process_recorded_input( |
200 | 0 | line, |
201 | 0 | &mut recorder, |
202 | 0 | &mut rl |
203 | 0 | )?; |
204 | | |
205 | 0 | if should_exit { |
206 | 0 | break; |
207 | 0 | } |
208 | | } |
209 | | } |
210 | | } |
211 | 0 | Err(rustyline::error::ReadlineError::Interrupted) => { |
212 | 0 | println!("{}", "Use :quit to exit".bright_yellow()); |
213 | 0 | } |
214 | 0 | Err(rustyline::error::ReadlineError::Eof) => break, |
215 | 0 | Err(err) => { |
216 | 0 | eprintln!("{}: {:?}", "Error".bright_red(), err); |
217 | 0 | break; |
218 | | } |
219 | | } |
220 | | } |
221 | | |
222 | | // Save recording |
223 | 0 | let session = recorder.into_session(); |
224 | 0 | let session_json = serde_json::to_string_pretty(&session)?; |
225 | 0 | std::fs::write(record_file, session_json)?; |
226 | 0 | println!("{}", format!("📼 Session saved to: {}", record_file.display()).bright_green()); |
227 | | |
228 | 0 | Ok(()) |
229 | 0 | } |
230 | | } |
231 | | |
232 | | #[cfg(test)] |
233 | | mod tests { |
234 | | use super::*; |
235 | | |
236 | | #[test] |
237 | | fn test_create_session_metadata() { |
238 | | let metadata = Repl::create_session_metadata().unwrap(); |
239 | | assert!(metadata.session_id.starts_with("ruchy-session-")); |
240 | | assert_eq!(metadata.ruchy_version, env!("CARGO_PKG_VERSION")); |
241 | | assert_eq!(metadata.tags, vec!["interactive"]); |
242 | | } |
243 | | |
244 | | #[test] |
245 | | fn test_setup_recording_editor() -> Result<()> { |
246 | | let repl = Repl::new()?; |
247 | | |
248 | | // Just verify it doesn't panic |
249 | | let _editor = repl.setup_recording_editor()?; |
250 | | Ok(()) |
251 | | } |
252 | | } |