Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}