/home/noah/src/ruchy/src/runtime/magic.rs
Line | Count | Source |
1 | | //! REPL Magic Commands System |
2 | | //! |
3 | | //! Provides IPython-style magic commands for enhanced REPL interaction. |
4 | | //! Based on docs/specifications/repl-magic-spec.md |
5 | | |
6 | | use anyhow::{Result, anyhow}; |
7 | | use std::collections::HashMap; |
8 | | use std::time::{Duration, Instant}; |
9 | | use std::fmt; |
10 | | |
11 | | use crate::runtime::repl::{Repl, Value}; |
12 | | |
13 | | // ============================================================================ |
14 | | // Magic Command Registry |
15 | | // ============================================================================ |
16 | | |
17 | | /// Registry for magic commands |
18 | | pub struct MagicRegistry { |
19 | | commands: HashMap<String, Box<dyn MagicCommand>>, |
20 | | } |
21 | | |
22 | | impl MagicRegistry { |
23 | 0 | pub fn new() -> Self { |
24 | 0 | let mut registry = Self { |
25 | 0 | commands: HashMap::new(), |
26 | 0 | }; |
27 | | |
28 | | // Register built-in magic commands |
29 | 0 | registry.register("time", Box::new(TimeMagic)); |
30 | 0 | registry.register("timeit", Box::new(TimeitMagic::default())); |
31 | 0 | registry.register("run", Box::new(RunMagic)); |
32 | 0 | registry.register("debug", Box::new(DebugMagic)); |
33 | 0 | registry.register("profile", Box::new(ProfileMagic)); |
34 | 0 | registry.register("whos", Box::new(WhosMagic)); |
35 | 0 | registry.register("clear", Box::new(ClearMagic)); |
36 | 0 | registry.register("reset", Box::new(ResetMagic)); |
37 | 0 | registry.register("history", Box::new(HistoryMagic)); |
38 | 0 | registry.register("save", Box::new(SaveMagic)); |
39 | 0 | registry.register("load", Box::new(LoadMagic)); |
40 | 0 | registry.register("pwd", Box::new(PwdMagic)); |
41 | 0 | registry.register("cd", Box::new(CdMagic)); |
42 | 0 | registry.register("ls", Box::new(LsMagic)); |
43 | | |
44 | 0 | registry |
45 | 0 | } |
46 | | |
47 | | /// Register a new magic command |
48 | 0 | pub fn register(&mut self, name: &str, command: Box<dyn MagicCommand>) { |
49 | 0 | self.commands.insert(name.to_string(), command); |
50 | 0 | } |
51 | | |
52 | | /// Check if input is a magic command |
53 | 0 | pub fn is_magic(&self, input: &str) -> bool { |
54 | 0 | input.starts_with('%') || input.starts_with("%%") |
55 | 0 | } |
56 | | |
57 | | /// Execute a magic command |
58 | 0 | pub fn execute(&mut self, repl: &mut Repl, input: &str) -> Result<MagicResult> { |
59 | 0 | if !self.is_magic(input) { |
60 | 0 | return Err(anyhow!("Not a magic command")); |
61 | 0 | } |
62 | | |
63 | | // Parse magic command |
64 | 0 | let (is_cell_magic, command_line) = if input.starts_with("%%") { |
65 | 0 | (true, &input[2..]) |
66 | | } else { |
67 | 0 | (false, &input[1..]) |
68 | | }; |
69 | | |
70 | 0 | let parts: Vec<&str> = command_line.split_whitespace().collect(); |
71 | 0 | if parts.is_empty() { |
72 | 0 | return Err(anyhow!("Empty magic command")); |
73 | 0 | } |
74 | | |
75 | 0 | let command_name = parts[0]; |
76 | 0 | let args = &parts[1..]; |
77 | | |
78 | | // Find and execute command |
79 | 0 | match self.commands.get(command_name) { |
80 | 0 | Some(command) => { |
81 | 0 | if is_cell_magic { |
82 | 0 | command.execute_cell(repl, args.join(" ").as_str()) |
83 | | } else { |
84 | 0 | command.execute_line(repl, args.join(" ").as_str()) |
85 | | } |
86 | | } |
87 | 0 | None => Err(anyhow!("Unknown magic command: %{}", command_name)), |
88 | | } |
89 | 0 | } |
90 | | |
91 | | /// Get list of available magic commands |
92 | 0 | pub fn list_commands(&self) -> Vec<String> { |
93 | 0 | let mut commands: Vec<_> = self.commands.keys().cloned().collect(); |
94 | 0 | commands.sort(); |
95 | 0 | commands |
96 | 0 | } |
97 | | } |
98 | | |
99 | | impl Default for MagicRegistry { |
100 | 0 | fn default() -> Self { |
101 | 0 | Self::new() |
102 | 0 | } |
103 | | } |
104 | | |
105 | | // ============================================================================ |
106 | | // Magic Command Trait |
107 | | // ============================================================================ |
108 | | |
109 | | /// Result of executing a magic command |
110 | | #[derive(Debug, Clone)] |
111 | | pub enum MagicResult { |
112 | | /// Simple text output |
113 | | Text(String), |
114 | | /// Formatted output with timing |
115 | | Timed { output: String, duration: Duration }, |
116 | | /// Profile data |
117 | | Profile(ProfileData), |
118 | | /// No output |
119 | | Silent, |
120 | | } |
121 | | |
122 | | impl fmt::Display for MagicResult { |
123 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
124 | 0 | match self { |
125 | 0 | MagicResult::Text(s) => write!(f, "{s}"), |
126 | 0 | MagicResult::Timed { output, duration } => { |
127 | 0 | write!(f, "{}\nExecution time: {:.3}s", output, duration.as_secs_f64()) |
128 | | } |
129 | 0 | MagicResult::Profile(data) => write!(f, "{data}"), |
130 | 0 | MagicResult::Silent => Ok(()), |
131 | | } |
132 | 0 | } |
133 | | } |
134 | | |
135 | | /// Trait for magic command implementations |
136 | | pub trait MagicCommand: Send + Sync { |
137 | | /// Execute as line magic (single %) |
138 | | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult>; |
139 | | |
140 | | /// Execute as cell magic (double %%) |
141 | 0 | fn execute_cell(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
142 | | // Default: cell magic same as line magic |
143 | 0 | self.execute_line(repl, args) |
144 | 0 | } |
145 | | |
146 | | /// Get help text for this command |
147 | | fn help(&self) -> &str; |
148 | | } |
149 | | |
150 | | // ============================================================================ |
151 | | // Timing Magic Commands |
152 | | // ============================================================================ |
153 | | |
154 | | /// %time - Time single execution |
155 | | struct TimeMagic; |
156 | | |
157 | | impl MagicCommand for TimeMagic { |
158 | 0 | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
159 | 0 | if args.trim().is_empty() { |
160 | 0 | return Err(anyhow!("Usage: %time <expression>")); |
161 | 0 | } |
162 | | |
163 | 0 | let start = Instant::now(); |
164 | 0 | let result = repl.eval(args)?; |
165 | 0 | let duration = start.elapsed(); |
166 | | |
167 | 0 | Ok(MagicResult::Timed { |
168 | 0 | output: result, |
169 | 0 | duration, |
170 | 0 | }) |
171 | 0 | } |
172 | | |
173 | 0 | fn help(&self) -> &'static str { |
174 | 0 | "Time execution of a single expression" |
175 | 0 | } |
176 | | } |
177 | | |
178 | | /// %timeit - Statistical timing over multiple runs |
179 | | struct TimeitMagic { |
180 | | default_runs: usize, |
181 | | } |
182 | | |
183 | | impl Default for TimeitMagic { |
184 | 0 | fn default() -> Self { |
185 | 0 | Self { default_runs: 1000 } |
186 | 0 | } |
187 | | } |
188 | | |
189 | | impl MagicCommand for TimeitMagic { |
190 | 0 | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
191 | 0 | if args.trim().is_empty() { |
192 | 0 | return Err(anyhow!("Usage: %timeit [-n RUNS] <expression>")); |
193 | 0 | } |
194 | | |
195 | | // Parse arguments for -n flag |
196 | 0 | let (runs, expr) = if args.starts_with("-n ") { |
197 | 0 | let parts: Vec<&str> = args.splitn(3, ' ').collect(); |
198 | 0 | if parts.len() < 3 { |
199 | 0 | return Err(anyhow!("Invalid -n syntax")); |
200 | 0 | } |
201 | 0 | let n = parts[1].parse::<usize>() |
202 | 0 | .map_err(|_| anyhow!("Invalid number of runs"))?; |
203 | 0 | (n, parts[2]) |
204 | | } else { |
205 | 0 | (self.default_runs, args) |
206 | | }; |
207 | | |
208 | | // Warm up run |
209 | 0 | repl.eval(expr)?; |
210 | | |
211 | | // Timing runs |
212 | 0 | let mut durations = Vec::with_capacity(runs); |
213 | 0 | for _ in 0..runs { |
214 | 0 | let start = Instant::now(); |
215 | 0 | repl.eval(expr)?; |
216 | 0 | durations.push(start.elapsed()); |
217 | | } |
218 | | |
219 | | // Calculate statistics |
220 | 0 | let total: Duration = durations.iter().sum(); |
221 | 0 | let mean = total / runs as u32; |
222 | | |
223 | 0 | durations.sort(); |
224 | 0 | let min = durations[0]; |
225 | 0 | let max = durations[runs - 1]; |
226 | 0 | let median = if runs % 2 == 0 { |
227 | 0 | (durations[runs / 2 - 1] + durations[runs / 2]) / 2 |
228 | | } else { |
229 | 0 | durations[runs / 2] |
230 | | }; |
231 | | |
232 | 0 | let output = format!( |
233 | 0 | "{} loops, best of {}: {:.3}µs per loop\n\ |
234 | 0 | min: {:.3}µs, median: {:.3}µs, max: {:.3}µs", |
235 | | runs, runs, |
236 | 0 | mean.as_micros() as f64, |
237 | 0 | min.as_micros() as f64, |
238 | 0 | median.as_micros() as f64, |
239 | 0 | max.as_micros() as f64 |
240 | | ); |
241 | | |
242 | 0 | Ok(MagicResult::Text(output)) |
243 | 0 | } |
244 | | |
245 | 0 | fn help(&self) -> &'static str { |
246 | 0 | "Time execution with statistics over multiple runs" |
247 | 0 | } |
248 | | } |
249 | | |
250 | | // ============================================================================ |
251 | | // File and Script Magic Commands |
252 | | // ============================================================================ |
253 | | |
254 | | /// %run - Execute external script |
255 | | struct RunMagic; |
256 | | |
257 | | impl MagicCommand for RunMagic { |
258 | 0 | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
259 | 0 | if args.trim().is_empty() { |
260 | 0 | return Err(anyhow!("Usage: %run <script.ruchy>")); |
261 | 0 | } |
262 | | |
263 | 0 | let script_content = std::fs::read_to_string(args) |
264 | 0 | .map_err(|e| anyhow!("Failed to read script: {}", e))?; |
265 | | |
266 | 0 | let result = repl.eval(&script_content)?; |
267 | 0 | Ok(MagicResult::Text(result)) |
268 | 0 | } |
269 | | |
270 | 0 | fn help(&self) -> &'static str { |
271 | 0 | "Execute an external Ruchy script" |
272 | 0 | } |
273 | | } |
274 | | |
275 | | // ============================================================================ |
276 | | // Debug Magic Commands |
277 | | // ============================================================================ |
278 | | |
279 | | /// %debug - Post-mortem debugging |
280 | | struct DebugMagic; |
281 | | |
282 | | impl MagicCommand for DebugMagic { |
283 | 0 | fn execute_line(&self, repl: &mut Repl, _args: &str) -> Result<MagicResult> { |
284 | | // Get debug information from REPL |
285 | 0 | if let Some(debug_info) = repl.get_last_error() { |
286 | 0 | let output = format!( |
287 | 0 | "=== Debug Information ===\n\ |
288 | 0 | Expression: {}\n\ |
289 | 0 | Error: {}\n\ |
290 | 0 | Stack trace:\n{}\n\ |
291 | 0 | Bindings at error: {} variables", |
292 | | debug_info.expression, |
293 | | debug_info.error_message, |
294 | 0 | debug_info.stack_trace.join("\n"), |
295 | 0 | debug_info.bindings_snapshot.len() |
296 | | ); |
297 | 0 | Ok(MagicResult::Text(output)) |
298 | | } else { |
299 | 0 | Ok(MagicResult::Text("No recent error to debug".to_string())) |
300 | | } |
301 | 0 | } |
302 | | |
303 | 0 | fn help(&self) -> &'static str { |
304 | 0 | "Enter post-mortem debugging mode" |
305 | 0 | } |
306 | | } |
307 | | |
308 | | // ============================================================================ |
309 | | // Profile Magic Command |
310 | | // ============================================================================ |
311 | | |
312 | | /// Profile data from execution |
313 | | #[derive(Debug, Clone)] |
314 | | pub struct ProfileData { |
315 | | pub total_time: Duration, |
316 | | pub function_times: Vec<(String, Duration, usize)>, // (name, time, count) |
317 | | } |
318 | | |
319 | | impl fmt::Display for ProfileData { |
320 | 0 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
321 | 0 | writeln!(f, "=== Profile Results ===")?; |
322 | 0 | writeln!(f, "Total time: {:.3}s", self.total_time.as_secs_f64())?; |
323 | 0 | writeln!(f, "\nFunction Times:")?; |
324 | 0 | writeln!(f, "{:<30} {:>10} {:>10} {:>10}", "Function", "Time (ms)", "Count", "Avg (ms)")?; |
325 | 0 | writeln!(f, "{:-<60}", "")?; |
326 | | |
327 | 0 | for (name, time, count) in &self.function_times { |
328 | 0 | let time_ms = time.as_micros() as f64 / 1000.0; |
329 | 0 | let avg_ms = if *count > 0 { time_ms / *count as f64 } else { 0.0 }; |
330 | 0 | writeln!(f, "{name:<30} {time_ms:>10.3} {count:>10} {avg_ms:>10.3}")?; |
331 | | } |
332 | | |
333 | 0 | Ok(()) |
334 | 0 | } |
335 | | } |
336 | | |
337 | | /// %profile - Profile code execution |
338 | | struct ProfileMagic; |
339 | | |
340 | | impl MagicCommand for ProfileMagic { |
341 | 0 | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
342 | 0 | if args.trim().is_empty() { |
343 | 0 | return Err(anyhow!("Usage: %profile <expression>")); |
344 | 0 | } |
345 | | |
346 | | // Simple profiling - in production would use more sophisticated profiling |
347 | 0 | let start = Instant::now(); |
348 | 0 | let _result = repl.eval(args)?; |
349 | 0 | let total_time = start.elapsed(); |
350 | | |
351 | | // Mock profile data - in production would collect actual function timings |
352 | 0 | let profile_data = ProfileData { |
353 | 0 | total_time, |
354 | 0 | function_times: vec![ |
355 | 0 | ("main".to_string(), total_time, 1), |
356 | 0 | ], |
357 | 0 | }; |
358 | | |
359 | 0 | Ok(MagicResult::Profile(profile_data)) |
360 | 0 | } |
361 | | |
362 | 0 | fn help(&self) -> &'static str { |
363 | 0 | "Profile code execution and generate flamegraph" |
364 | 0 | } |
365 | | } |
366 | | |
367 | | // ============================================================================ |
368 | | // Workspace Magic Commands |
369 | | // ============================================================================ |
370 | | |
371 | | /// %whos - List variables in workspace |
372 | | struct WhosMagic; |
373 | | |
374 | | impl MagicCommand for WhosMagic { |
375 | 0 | fn execute_line(&self, repl: &mut Repl, _args: &str) -> Result<MagicResult> { |
376 | 0 | let bindings = repl.get_bindings(); |
377 | | |
378 | 0 | if bindings.is_empty() { |
379 | 0 | return Ok(MagicResult::Text("No variables in workspace".to_string())); |
380 | 0 | } |
381 | | |
382 | 0 | let mut output = String::from("Variable Type Value\n"); |
383 | 0 | output.push_str("-------- ---- -----\n"); |
384 | | |
385 | 0 | for (name, value) in bindings { |
386 | 0 | let type_name = match value { |
387 | 0 | Value::Int(_) => "Int", |
388 | 0 | Value::Float(_) => "Float", |
389 | 0 | Value::String(_) => "String", |
390 | 0 | Value::Bool(_) => "Bool", |
391 | 0 | Value::Char(_) => "Char", |
392 | 0 | Value::List(_) => "List", |
393 | 0 | Value::Tuple(_) => "Tuple", |
394 | 0 | Value::Object(_) => "Object", |
395 | 0 | Value::HashMap(_) => "HashMap", |
396 | 0 | Value::HashSet(_) => "HashSet", |
397 | 0 | Value::Function { .. } => "Function", |
398 | 0 | Value::Lambda { .. } => "Lambda", |
399 | 0 | Value::DataFrame { .. } => "DataFrame", |
400 | 0 | Value::Range { .. } => "Range", |
401 | 0 | Value::EnumVariant { .. } => "EnumVariant", |
402 | 0 | Value::Unit => "Unit", |
403 | 0 | Value::Nil => "Nil", |
404 | | }; |
405 | | |
406 | 0 | let value_str = format!("{value:?}"); |
407 | 0 | let value_display = if value_str.len() > 40 { |
408 | 0 | format!("{}...", &value_str[..37]) |
409 | | } else { |
410 | 0 | value_str |
411 | | }; |
412 | | |
413 | 0 | output.push_str(&format!("{name:<10} {type_name:<10} {value_display}\n")); |
414 | | } |
415 | | |
416 | 0 | Ok(MagicResult::Text(output)) |
417 | 0 | } |
418 | | |
419 | 0 | fn help(&self) -> &'static str { |
420 | 0 | "List all variables in the workspace" |
421 | 0 | } |
422 | | } |
423 | | |
424 | | /// %clear - Clear specific variables |
425 | | struct ClearMagic; |
426 | | |
427 | | impl MagicCommand for ClearMagic { |
428 | 0 | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
429 | 0 | if args.trim().is_empty() { |
430 | 0 | return Err(anyhow!("Usage: %clear <pattern>")); |
431 | 0 | } |
432 | | |
433 | | // Simple pattern matching - in production would support regex |
434 | 0 | let pattern = args.trim(); |
435 | 0 | let mut cleared = 0; |
436 | | |
437 | 0 | let bindings_copy: Vec<String> = repl.get_bindings().keys().cloned().collect(); |
438 | 0 | for name in bindings_copy { |
439 | 0 | if name.contains(pattern) || pattern == "*" { |
440 | 0 | repl.get_bindings_mut().remove(&name); |
441 | 0 | cleared += 1; |
442 | 0 | } |
443 | | } |
444 | | |
445 | 0 | Ok(MagicResult::Text(format!("Cleared {cleared} variables"))) |
446 | 0 | } |
447 | | |
448 | 0 | fn help(&self) -> &'static str { |
449 | 0 | "Clear variables matching pattern" |
450 | 0 | } |
451 | | } |
452 | | |
453 | | /// %reset - Reset entire workspace |
454 | | struct ResetMagic; |
455 | | |
456 | | impl MagicCommand for ResetMagic { |
457 | 0 | fn execute_line(&self, repl: &mut Repl, _args: &str) -> Result<MagicResult> { |
458 | 0 | repl.clear_bindings(); |
459 | 0 | Ok(MagicResult::Text("Workspace reset".to_string())) |
460 | 0 | } |
461 | | |
462 | 0 | fn help(&self) -> &'static str { |
463 | 0 | "Reset the entire workspace" |
464 | 0 | } |
465 | | } |
466 | | |
467 | | // ============================================================================ |
468 | | // History Magic Commands |
469 | | // ============================================================================ |
470 | | |
471 | | /// %history - Show command history |
472 | | struct HistoryMagic; |
473 | | |
474 | | impl MagicCommand for HistoryMagic { |
475 | 0 | fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> { |
476 | | // Parse arguments for range |
477 | 0 | let range = if args.trim().is_empty() { |
478 | 0 | 10 |
479 | | } else { |
480 | 0 | args.trim().parse::<usize>().unwrap_or(10) |
481 | | }; |
482 | | |
483 | | // In production, would get actual history from REPL |
484 | 0 | let mut output = format!("Last {range} commands:\n"); |
485 | 0 | for i in 1..=range { |
486 | 0 | output.push_str(&format!("{i}: <command {i}>\n")); |
487 | 0 | } |
488 | | |
489 | 0 | Ok(MagicResult::Text(output)) |
490 | 0 | } |
491 | | |
492 | 0 | fn help(&self) -> &'static str { |
493 | 0 | "Show command history" |
494 | 0 | } |
495 | | } |
496 | | |
497 | | // ============================================================================ |
498 | | // Session Magic Commands |
499 | | // ============================================================================ |
500 | | |
501 | | /// %save - Save workspace to file |
502 | | struct SaveMagic; |
503 | | |
504 | | impl MagicCommand for SaveMagic { |
505 | 0 | fn execute_line(&self, repl: &mut Repl, args: &str) -> Result<MagicResult> { |
506 | 0 | if args.trim().is_empty() { |
507 | 0 | return Err(anyhow!("Usage: %save <filename>")); |
508 | 0 | } |
509 | | |
510 | | // Serialize workspace - convert to string representation since Value doesn't impl Serialize |
511 | 0 | let bindings = repl.get_bindings(); |
512 | 0 | let mut serializable: HashMap<String, String> = HashMap::new(); |
513 | 0 | for (k, v) in bindings { |
514 | 0 | serializable.insert(k.clone(), format!("{v:?}")); |
515 | 0 | } |
516 | 0 | let json = serde_json::to_string_pretty(&serializable) |
517 | 0 | .map_err(|e| anyhow!("Failed to serialize: {}", e))?; |
518 | | |
519 | 0 | std::fs::write(args.trim(), json) |
520 | 0 | .map_err(|e| anyhow!("Failed to write file: {}", e))?; |
521 | | |
522 | 0 | Ok(MagicResult::Text(format!("Saved workspace to {}", args.trim()))) |
523 | 0 | } |
524 | | |
525 | 0 | fn help(&self) -> &'static str { |
526 | 0 | "Save workspace to file" |
527 | 0 | } |
528 | | } |
529 | | |
530 | | /// %load - Load workspace from file |
531 | | struct LoadMagic; |
532 | | |
533 | | impl MagicCommand for LoadMagic { |
534 | 0 | fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> { |
535 | 0 | if args.trim().is_empty() { |
536 | 0 | return Err(anyhow!("Usage: %load <filename>")); |
537 | 0 | } |
538 | | |
539 | 0 | let _content = std::fs::read_to_string(args.trim()) |
540 | 0 | .map_err(|e| anyhow!("Failed to read file: {}", e))?; |
541 | | |
542 | | // In production, would deserialize and load into workspace |
543 | | |
544 | 0 | Ok(MagicResult::Text(format!("Loaded workspace from {}", args.trim()))) |
545 | 0 | } |
546 | | |
547 | 0 | fn help(&self) -> &'static str { |
548 | 0 | "Load workspace from file" |
549 | 0 | } |
550 | | } |
551 | | |
552 | | // ============================================================================ |
553 | | // Shell Integration Magic Commands |
554 | | // ============================================================================ |
555 | | |
556 | | /// %pwd - Print working directory |
557 | | struct PwdMagic; |
558 | | |
559 | | impl MagicCommand for PwdMagic { |
560 | 0 | fn execute_line(&self, _repl: &mut Repl, _args: &str) -> Result<MagicResult> { |
561 | 0 | let pwd = std::env::current_dir() |
562 | 0 | .map_err(|e| anyhow!("Failed to get pwd: {}", e))?; |
563 | 0 | Ok(MagicResult::Text(pwd.display().to_string())) |
564 | 0 | } |
565 | | |
566 | 0 | fn help(&self) -> &'static str { |
567 | 0 | "Print working directory" |
568 | 0 | } |
569 | | } |
570 | | |
571 | | /// %cd - Change directory |
572 | | struct CdMagic; |
573 | | |
574 | | impl MagicCommand for CdMagic { |
575 | 0 | fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> { |
576 | 0 | let path = if args.trim().is_empty() { |
577 | 0 | std::env::var("HOME").unwrap_or_else(|_| ".".to_string()) |
578 | | } else { |
579 | 0 | args.trim().to_string() |
580 | | }; |
581 | | |
582 | 0 | std::env::set_current_dir(&path) |
583 | 0 | .map_err(|e| anyhow!("Failed to change directory: {}", e))?; |
584 | | |
585 | 0 | let pwd = std::env::current_dir() |
586 | 0 | .map_err(|e| anyhow!("Failed to get pwd: {}", e))?; |
587 | | |
588 | 0 | Ok(MagicResult::Text(format!("Changed to: {}", pwd.display()))) |
589 | 0 | } |
590 | | |
591 | 0 | fn help(&self) -> &'static str { |
592 | 0 | "Change working directory" |
593 | 0 | } |
594 | | } |
595 | | |
596 | | /// %ls - List directory contents |
597 | | struct LsMagic; |
598 | | |
599 | | impl MagicCommand for LsMagic { |
600 | 0 | fn execute_line(&self, _repl: &mut Repl, args: &str) -> Result<MagicResult> { |
601 | 0 | let path = if args.trim().is_empty() { |
602 | 0 | "." |
603 | | } else { |
604 | 0 | args.trim() |
605 | | }; |
606 | | |
607 | 0 | let entries = std::fs::read_dir(path) |
608 | 0 | .map_err(|e| anyhow!("Failed to read directory: {}", e))?; |
609 | | |
610 | 0 | let mut output = String::new(); |
611 | 0 | for entry in entries { |
612 | 0 | let entry = entry.map_err(|e| anyhow!("Failed to read entry: {}", e))?; |
613 | 0 | let name = entry.file_name(); |
614 | 0 | output.push_str(&format!("{}\n", name.to_string_lossy())); |
615 | | } |
616 | | |
617 | 0 | Ok(MagicResult::Text(output)) |
618 | 0 | } |
619 | | |
620 | 0 | fn help(&self) -> &'static str { |
621 | 0 | "List directory contents" |
622 | 0 | } |
623 | | } |
624 | | |
625 | | // ============================================================================ |
626 | | // Unicode Expansion Support |
627 | | // ============================================================================ |
628 | | |
629 | | /// Registry for Unicode character expansion (α → \alpha) |
630 | | pub struct UnicodeExpander { |
631 | | mappings: HashMap<String, char>, |
632 | | } |
633 | | |
634 | | impl UnicodeExpander { |
635 | 0 | pub fn new() -> Self { |
636 | 0 | let mut mappings = HashMap::new(); |
637 | | |
638 | | // Greek letters |
639 | 0 | mappings.insert("alpha".to_string(), 'α'); |
640 | 0 | mappings.insert("beta".to_string(), 'β'); |
641 | 0 | mappings.insert("gamma".to_string(), 'γ'); |
642 | 0 | mappings.insert("delta".to_string(), 'δ'); |
643 | 0 | mappings.insert("epsilon".to_string(), 'ε'); |
644 | 0 | mappings.insert("zeta".to_string(), 'ζ'); |
645 | 0 | mappings.insert("eta".to_string(), 'η'); |
646 | 0 | mappings.insert("theta".to_string(), 'θ'); |
647 | 0 | mappings.insert("iota".to_string(), 'ι'); |
648 | 0 | mappings.insert("kappa".to_string(), 'κ'); |
649 | 0 | mappings.insert("lambda".to_string(), 'λ'); |
650 | 0 | mappings.insert("mu".to_string(), 'μ'); |
651 | 0 | mappings.insert("nu".to_string(), 'ν'); |
652 | 0 | mappings.insert("xi".to_string(), 'ξ'); |
653 | 0 | mappings.insert("pi".to_string(), 'π'); |
654 | 0 | mappings.insert("rho".to_string(), 'ρ'); |
655 | 0 | mappings.insert("sigma".to_string(), 'σ'); |
656 | 0 | mappings.insert("tau".to_string(), 'τ'); |
657 | 0 | mappings.insert("phi".to_string(), 'φ'); |
658 | 0 | mappings.insert("chi".to_string(), 'χ'); |
659 | 0 | mappings.insert("psi".to_string(), 'ψ'); |
660 | 0 | mappings.insert("omega".to_string(), 'ω'); |
661 | | |
662 | | // Capital Greek letters |
663 | 0 | mappings.insert("Alpha".to_string(), 'Α'); |
664 | 0 | mappings.insert("Beta".to_string(), 'Β'); |
665 | 0 | mappings.insert("Gamma".to_string(), 'Γ'); |
666 | 0 | mappings.insert("Delta".to_string(), 'Δ'); |
667 | 0 | mappings.insert("Theta".to_string(), 'Θ'); |
668 | 0 | mappings.insert("Lambda".to_string(), 'Λ'); |
669 | 0 | mappings.insert("Pi".to_string(), 'Π'); |
670 | 0 | mappings.insert("Sigma".to_string(), 'Σ'); |
671 | 0 | mappings.insert("Phi".to_string(), 'Φ'); |
672 | 0 | mappings.insert("Psi".to_string(), 'Ψ'); |
673 | 0 | mappings.insert("Omega".to_string(), 'Ω'); |
674 | | |
675 | | // Mathematical symbols |
676 | 0 | mappings.insert("infty".to_string(), '∞'); |
677 | 0 | mappings.insert("sum".to_string(), '∑'); |
678 | 0 | mappings.insert("prod".to_string(), '∏'); |
679 | 0 | mappings.insert("int".to_string(), '∫'); |
680 | 0 | mappings.insert("sqrt".to_string(), '√'); |
681 | 0 | mappings.insert("partial".to_string(), '∂'); |
682 | 0 | mappings.insert("nabla".to_string(), '∇'); |
683 | 0 | mappings.insert("forall".to_string(), '∀'); |
684 | 0 | mappings.insert("exists".to_string(), '∃'); |
685 | 0 | mappings.insert("in".to_string(), '∈'); |
686 | 0 | mappings.insert("notin".to_string(), '∉'); |
687 | 0 | mappings.insert("subset".to_string(), '⊂'); |
688 | 0 | mappings.insert("supset".to_string(), '⊃'); |
689 | 0 | mappings.insert("cup".to_string(), '∪'); |
690 | 0 | mappings.insert("cap".to_string(), '∩'); |
691 | 0 | mappings.insert("emptyset".to_string(), '∅'); |
692 | 0 | mappings.insert("pm".to_string(), '±'); |
693 | 0 | mappings.insert("mp".to_string(), '∓'); |
694 | 0 | mappings.insert("times".to_string(), '×'); |
695 | 0 | mappings.insert("div".to_string(), '÷'); |
696 | 0 | mappings.insert("neq".to_string(), '≠'); |
697 | 0 | mappings.insert("leq".to_string(), '≤'); |
698 | 0 | mappings.insert("geq".to_string(), '≥'); |
699 | 0 | mappings.insert("approx".to_string(), '≈'); |
700 | 0 | mappings.insert("equiv".to_string(), '≡'); |
701 | | |
702 | 0 | Self { mappings } |
703 | 0 | } |
704 | | |
705 | | /// Expand LaTeX-style sequence to Unicode character |
706 | 0 | pub fn expand(&self, sequence: &str) -> Option<char> { |
707 | | // Remove leading backslash if present |
708 | 0 | let key = if sequence.starts_with('\\') { |
709 | 0 | &sequence[1..] |
710 | | } else { |
711 | 0 | sequence |
712 | | }; |
713 | | |
714 | 0 | self.mappings.get(key).copied() |
715 | 0 | } |
716 | | |
717 | | /// Get all available expansions |
718 | 0 | pub fn list_expansions(&self) -> Vec<(String, char)> { |
719 | 0 | let mut expansions: Vec<_> = self.mappings |
720 | 0 | .iter() |
721 | 0 | .map(|(k, v)| (format!("\\{k}"), *v)) |
722 | 0 | .collect(); |
723 | 0 | expansions.sort_by_key(|(k, _)| k.clone()); |
724 | 0 | expansions |
725 | 0 | } |
726 | | } |
727 | | |
728 | | impl Default for UnicodeExpander { |
729 | 0 | fn default() -> Self { |
730 | 0 | Self::new() |
731 | 0 | } |
732 | | } |
733 | | |
734 | | #[cfg(test)] |
735 | | mod tests { |
736 | | use super::*; |
737 | | |
738 | | #[test] |
739 | | fn test_magic_registry() { |
740 | | let registry = MagicRegistry::new(); |
741 | | assert!(registry.is_magic("%time")); |
742 | | assert!(registry.is_magic("%%time")); |
743 | | assert!(!registry.is_magic("time")); |
744 | | |
745 | | let commands = registry.list_commands(); |
746 | | assert!(commands.contains(&"time".to_string())); |
747 | | assert!(commands.contains(&"debug".to_string())); |
748 | | } |
749 | | |
750 | | #[test] |
751 | | fn test_unicode_expander() { |
752 | | let expander = UnicodeExpander::new(); |
753 | | |
754 | | assert_eq!(expander.expand("\\alpha"), Some('α')); |
755 | | assert_eq!(expander.expand("alpha"), Some('α')); |
756 | | assert_eq!(expander.expand("\\pi"), Some('π')); |
757 | | assert_eq!(expander.expand("\\infty"), Some('∞')); |
758 | | assert_eq!(expander.expand("\\unknown"), None); |
759 | | } |
760 | | |
761 | | #[test] |
762 | | fn test_magic_result_display() { |
763 | | let result = MagicResult::Text("Hello".to_string()); |
764 | | assert_eq!(format!("{result}"), "Hello"); |
765 | | |
766 | | let result = MagicResult::Timed { |
767 | | output: "42".to_string(), |
768 | | duration: Duration::from_millis(123), |
769 | | }; |
770 | | assert!(format!("{result}").contains("0.123s")); |
771 | | } |
772 | | } |