## 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/repl_recording.rs

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