use std::env; use std::fs; use std::path::PathBuf; use tempfile::TempDir; use crate::prompt::readline::complete::Completer; use crate::state::{write_logic, write_vars, VarFlags}; use super::*; /// Helper to create a temp directory with test files fn setup_test_files() -> TempDir { let temp_dir = tempfile::tempdir().unwrap(); let path = temp_dir.path(); // Create some test files and directories fs::write(path.join("file1.txt"), "").unwrap(); fs::write(path.join("file2.txt"), "").unwrap(); fs::write(path.join("script.sh"), "").unwrap(); fs::create_dir(path.join("subdir")).unwrap(); fs::write(path.join("subdir/nested.txt"), "").unwrap(); fs::create_dir(path.join("another_dir")).unwrap(); temp_dir } /// Helper to create a test directory in current dir for relative path tests fn setup_local_test_files() -> TempDir { let temp_dir = tempfile::tempdir_in(".").unwrap(); let path = temp_dir.path(); fs::write(path.join("local1.txt"), "").unwrap(); fs::write(path.join("local2.txt"), "").unwrap(); fs::create_dir(path.join("localdir")).unwrap(); temp_dir } // ============================================================================ // Command Completion Tests // ============================================================================ #[test] fn complete_command_from_path() { let mut completer = Completer::new(); // Try to complete "ec" - should find "echo" (which is in PATH) let line = "ec".to_string(); let cursor_pos = 2; let result = completer.complete(line, cursor_pos, 1); assert!(result.is_ok()); let completed = result.unwrap(); // Should have found something assert!(completed.is_some()); let completed_line = completed.unwrap(); // Should contain "echo" assert!(completed_line.starts_with("echo") || completer.candidates.iter().any(|c| c == "echo")); } #[test] fn complete_command_builtin() { let mut completer = Completer::new(); // Try to complete "ex" - should find "export" builtin let line = "ex".to_string(); let cursor_pos = 2; let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Check candidates include "export" assert!(completer.candidates.iter().any(|c| c == "export")); } // NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to set up in tests // TODO: Re-enable once we have a helper to create test functions /* #[test] fn complete_command_function() { write_logic(|l| { // Add a test function - would need to parse "test_func() { echo test; }" // and create proper ShFunc from it // let func = ...; // l.insert_func("test_func", func); let mut completer = Completer::new(); let line = "test_f".to_string(); let cursor_pos = 6; let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Should find test_func assert!(completer.candidates.iter().any(|c| c == "test_func")); // Cleanup l.clear_functions(); }); } */ #[test] fn complete_command_alias() { // Add alias outside of completion call to avoid RefCell borrow conflict write_logic(|l| { l.insert_alias("ll", "ls -la"); }); let mut completer = Completer::new(); let line = "l".to_string(); let cursor_pos = 1; let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Should find ll and ls assert!(completer.candidates.iter().any(|c| c == "ll")); // Cleanup write_logic(|l| { l.clear_aliases(); }); } #[test] fn complete_command_no_matches() { let mut completer = Completer::new(); // Try to complete something that definitely doesn't exist let line = "xyzabc123notacommand".to_string(); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); // Should return None when no matches assert!(result.is_none()); } // ============================================================================ // Filename Completion Tests // ============================================================================ #[test] fn complete_filename_basic() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cat {}/fil", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Should have file1.txt and file2.txt as candidates assert!(completer.candidates.len() >= 2); assert!(completer.candidates.iter().any(|c| c.contains("file1.txt"))); assert!(completer.candidates.iter().any(|c| c.contains("file2.txt"))); } #[test] fn complete_filename_directory() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cd {}/sub", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Should find "subdir" assert!(completer.candidates.iter().any(|c| c.contains("subdir"))); } #[test] fn complete_filename_with_slash() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("ls {}/subdir/", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); // Should complete files in subdir/ if result.is_some() { assert!(completer.candidates.iter().any(|c| c.contains("nested.txt"))); } } #[test] fn complete_filename_preserves_trailing_slash() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cd {}/sub", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); let completed = result.unwrap(); // Directory completions should have trailing slash assert!(completed.ends_with('/')); } #[test] fn complete_filename_relative_path() { let _temp_dir = setup_local_test_files(); let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap(); let mut completer = Completer::new(); let line = format!("cat {}/local", dir_name); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); if result.is_some() { // Should find local1.txt and local2.txt assert!(completer.candidates.len() >= 2); } } #[test] fn complete_filename_current_dir() { let mut completer = Completer::new(); // Complete files in current directory let line = "cat ".to_string(); let cursor_pos = 4; let result = completer.complete(line, cursor_pos, 1).unwrap(); // Should find something in current dir (at least Cargo.toml should exist) if result.is_some() { assert!(!completer.candidates.is_empty()); } } #[test] fn complete_filename_with_dot_slash() { let _temp_dir = setup_local_test_files(); let dir_name = _temp_dir.path().file_name().unwrap().to_str().unwrap(); let mut completer = Completer::new(); let line = format!("./{}/local", dir_name); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1); // Should preserve the ./ if let Ok(Some(completed)) = result { assert!(completed.starts_with("./")); } } // ============================================================================ // Completion After '=' Tests // ============================================================================ #[test] fn complete_after_equals_assignment() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("FOO={}/fil", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Should complete filenames after = assert!(completer.candidates.iter().any(|c| c.contains("file"))); } #[test] fn complete_after_equals_option() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("command --output={}/fil", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); assert!(result.is_some()); // Should complete filenames after = in option assert!(completer.candidates.iter().any(|c| c.contains("file"))); } #[test] fn complete_after_equals_empty() { let mut completer = Completer::new(); let line = "FOO=".to_string(); let cursor_pos = 4; let result = completer.complete(line, cursor_pos, 1).unwrap(); // Should complete files in current directory when path is empty after = if result.is_some() { assert!(!completer.candidates.is_empty()); } } // ============================================================================ // Context Detection Tests // ============================================================================ #[test] fn context_detection_command_position() { let completer = Completer::new(); // At the beginning - command context let (in_cmd, _) = completer.get_completion_context("ech", 3); assert!(in_cmd, "Should be in command context at start"); // After whitespace - still command if no command yet let (in_cmd, _) = completer.get_completion_context(" ech", 5); assert!(in_cmd, "Should be in command context after whitespace"); } #[test] fn context_detection_argument_position() { let completer = Completer::new(); // After a complete command - argument context let (in_cmd, _) = completer.get_completion_context("echo hello", 10); assert!(!in_cmd, "Should be in argument context after command"); let (in_cmd, _) = completer.get_completion_context("ls -la /tmp", 11); assert!(!in_cmd, "Should be in argument context"); } #[test] fn context_detection_nested_command_sub() { let completer = Completer::new(); // Inside $() - should be command context let (in_cmd, _) = completer.get_completion_context("echo \"$(ech", 11); assert!(in_cmd, "Should be in command context inside $()"); // After command in $() - argument context let (in_cmd, _) = completer.get_completion_context("echo \"$(echo hell", 17); assert!(!in_cmd, "Should be in argument context inside $()"); } #[test] fn context_detection_pipe() { let completer = Completer::new(); // After pipe - command context let (in_cmd, _) = completer.get_completion_context("ls | gre", 8); assert!(in_cmd, "Should be in command context after pipe"); } #[test] fn context_detection_command_sep() { let completer = Completer::new(); // After semicolon - command context let (in_cmd, _) = completer.get_completion_context("echo foo; l", 11); assert!(in_cmd, "Should be in command context after semicolon"); // After && - command context let (in_cmd, _) = completer.get_completion_context("true && l", 9); assert!(in_cmd, "Should be in command context after &&"); } // ============================================================================ // Cycling Behavior Tests // ============================================================================ #[test] fn cycle_forward_through_candidates() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cat {}/file", path.display()); let cursor_pos = line.len(); // First tab let result1 = completer.complete(line.clone(), cursor_pos, 1).unwrap(); assert!(result1.is_some()); let first_candidate = completer.selected_candidate().unwrap().clone(); // Second tab - should cycle to next let result2 = completer.complete(line.clone(), cursor_pos, 1).unwrap(); assert!(result2.is_some()); let second_candidate = completer.selected_candidate().unwrap().clone(); // Should be different (if there are multiple candidates) if completer.candidates.len() > 1 { assert_ne!(first_candidate, second_candidate); } } #[test] fn cycle_backward_with_shift_tab() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cat {}/file", path.display()); let cursor_pos = line.len(); // Forward twice completer.complete(line.clone(), cursor_pos, 1).unwrap(); let after_first = completer.selected_idx; completer.complete(line.clone(), cursor_pos, 1).unwrap(); // Backward once (shift-tab = direction -1) completer.complete(line.clone(), cursor_pos, -1).unwrap(); let after_backward = completer.selected_idx; // Should be back to first selection assert_eq!(after_first, after_backward); } #[test] fn cycle_wraps_around() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cat {}/", path.display()); let cursor_pos = line.len(); // Get all candidates completer.complete(line.clone(), cursor_pos, 1).unwrap(); let num_candidates = completer.candidates.len(); if num_candidates > 1 { // Cycle through all and one more for _ in 0..num_candidates { completer.complete(line.clone(), cursor_pos, 1).unwrap(); } // Should wrap back to first (index 0) assert_eq!(completer.selected_idx, 0); } } #[test] fn cycle_reset_on_input_change() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line1 = format!("cat {}/file", path.display()); // Complete once completer.complete(line1.clone(), line1.len(), 1).unwrap(); let candidates_count = completer.candidates.len(); // Change input let line2 = format!("cat {}/script", path.display()); completer.complete(line2.clone(), line2.len(), 1).unwrap(); // Should have different candidates // (or at least should have reset the completer state) assert!(completer.active); } #[test] fn reset_clears_state() { let mut completer = Completer::new(); // Use a prefix that will definitely have completions let line = "ec".to_string(); let result = completer.complete(line, 2, 1).unwrap(); // Only check if we got completions if result.is_some() { // Should have candidates after completion assert!(!completer.candidates.is_empty()); completer.reset(); // After reset, state should be cleared assert!(!completer.active); assert!(completer.candidates.is_empty()); assert_eq!(completer.selected_idx, 0); } } // ============================================================================ // Edge Cases Tests // ============================================================================ #[test] fn complete_empty_input() { let mut completer = Completer::new(); let line = "".to_string(); let cursor_pos = 0; let result = completer.complete(line, cursor_pos, 1).unwrap(); // Empty input might return files in current dir or no completion // Either is valid behavior } #[test] fn complete_whitespace_only() { let mut completer = Completer::new(); let line = " ".to_string(); let cursor_pos = 3; let result = completer.complete(line, cursor_pos, 1); // Should handle gracefully assert!(result.is_ok()); } #[test] fn complete_at_middle_of_word() { let mut completer = Completer::new(); let line = "echo hello world".to_string(); let cursor_pos = 7; // In the middle of "hello" let result = completer.complete(line, cursor_pos, 1); // Should handle cursor in middle of word assert!(result.is_ok()); } #[test] fn complete_with_quotes() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cat \"{}/fil", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1); // Should handle quoted paths assert!(result.is_ok()); } #[test] fn complete_incomplete_command_substitution() { let mut completer = Completer::new(); let line = "echo \"$(ech".to_string(); let cursor_pos = 11; let result = completer.complete(line, cursor_pos, 1); // Should not crash on incomplete command sub assert!(result.is_ok()); } #[test] fn complete_with_multiple_spaces() { let mut completer = Completer::new(); let line = "echo hello world".to_string(); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1); assert!(result.is_ok()); } #[test] fn complete_special_characters_in_filename() { let temp_dir = tempfile::tempdir().unwrap(); let path = temp_dir.path(); // Create files with special characters fs::write(path.join("file-with-dash.txt"), "").unwrap(); fs::write(path.join("file_with_underscore.txt"), "").unwrap(); let mut completer = Completer::new(); let line = format!("cat {}/file", path.display()); let cursor_pos = line.len(); let result = completer.complete(line, cursor_pos, 1).unwrap(); if result.is_some() { // Should handle special chars in filenames assert!(completer.candidates.iter().any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))); } } // ============================================================================ // Integration Tests // ============================================================================ #[test] fn complete_full_workflow() { let temp_dir = setup_test_files(); let path = temp_dir.path(); let mut completer = Completer::new(); let line = format!("cat {}/fil", path.display()); let cursor_pos = line.len(); // Tab 1: Get first completion let result = completer.complete(line.clone(), cursor_pos, 1).unwrap(); assert!(result.is_some()); let completion1 = result.unwrap(); assert!(completion1.contains("file")); // Tab 2: Cycle to next let result = completer.complete(line.clone(), cursor_pos, 1).unwrap(); assert!(result.is_some()); let completion2 = result.unwrap(); // Shift-Tab: Go back let result = completer.complete(line.clone(), cursor_pos, -1).unwrap(); assert!(result.is_some()); let completion3 = result.unwrap(); // Should be back to first assert_eq!(completion1, completion3); } #[test] fn complete_mixed_command_and_file() { let mut completer = Completer::new(); // First part: command completion let line1 = "ech".to_string(); let result1 = completer.complete(line1, 3, 1).unwrap(); assert!(result1.is_some()); // Reset for new completion completer.reset(); // Second part: file completion let line2 = "echo Cargo.tom".to_string(); let result2 = completer.complete(line2, 14, 1).unwrap(); // Both should work assert!(result1.is_some()); }