## 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
    }