From e2961a0f1d4e259e3380ad5893dcb3dc004622b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C4=8Cert=C3=ADk?= Date: Sat, 8 Nov 2025 07:35:58 -0700 Subject: [PATCH 1/4] Implement tab completion for . files --- crates/shell/src/completion.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/shell/src/completion.rs b/crates/shell/src/completion.rs index 5f9c020..c161b24 100644 --- a/crates/shell/src/completion.rs +++ b/crates/shell/src/completion.rs @@ -85,7 +85,7 @@ struct FileMatch { } impl FileMatch { - fn from_entry(entry: fs::DirEntry, base_path: &Path) -> Option { + fn from_entry(entry: fs::DirEntry, base_path: &Path, show_hidden: bool) -> Option { let metadata = match entry.metadata() { Ok(m) => m, Err(_) => return None, @@ -93,8 +93,8 @@ impl FileMatch { let name = entry.file_name().into_string().ok()?; - // Skip hidden files - if name.starts_with('.') { + // Skip hidden files unless explicitly requested + if !show_hidden && name.starts_with('.') { return None; } @@ -175,12 +175,13 @@ fn complete_filenames(is_start: bool, word: &str, matches: &mut Vec) { let search_dir = resolve_dir_path(dir_path); let only_executable = (word.starts_with("./") || word.starts_with('/')) && is_start; + let show_hidden = partial_name.starts_with('.'); let files: Vec = fs::read_dir(&search_dir) .into_iter() .flatten() .flatten() - .filter_map(|entry| FileMatch::from_entry(entry, &search_dir)) + .filter_map(|entry| FileMatch::from_entry(entry, &search_dir, show_hidden)) .filter(|f| f.name.starts_with(partial_name)) .filter(|f| !only_executable || f.is_executable || f.is_dir) .collect(); From 2cde04a8ab1e471e149731f74423fa34ec2ff242 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C4=8Cert=C3=ADk?= Date: Sat, 8 Nov 2025 07:44:01 -0700 Subject: [PATCH 2/4] Add tests for tab completion --- Cargo.lock | 1 + crates/shell/Cargo.toml | 3 + crates/shell/src/completion.rs | 104 +++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 2e7b580..da502e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1337,6 +1337,7 @@ dependencies = [ "miette", "parse_datetime 0.8.0", "rustyline", + "tempfile", "tokio", "uu_date", "uu_ls", diff --git a/crates/shell/Cargo.toml b/crates/shell/Cargo.toml index ef2e581..5b74b76 100644 --- a/crates/shell/Cargo.toml +++ b/crates/shell/Cargo.toml @@ -43,6 +43,9 @@ windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Syste ctrlc = "3.4.5" libc = "0.2.170" +[dev-dependencies] +tempfile = "3.14.0" + [package.metadata.release] # Dont publish the binary release = false diff --git a/crates/shell/src/completion.rs b/crates/shell/src/completion.rs index c161b24..93c52e4 100644 --- a/crates/shell/src/completion.rs +++ b/crates/shell/src/completion.rs @@ -251,3 +251,107 @@ impl Highlighter for ShellCompleter { impl Validator for ShellCompleter {} impl Helper for ShellCompleter {} + +#[cfg(test)] +mod tests { + use super::*; + use rustyline::history::DefaultHistory; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_complete_hidden_files_when_starting_with_dot() { + let temp_dir = TempDir::new().unwrap(); + let temp_path = temp_dir.path(); + + // Create some test files and directories + fs::File::create(temp_path.join(".gitignore")).unwrap(); + fs::create_dir(temp_path.join(".github")).unwrap(); + fs::File::create(temp_path.join(".hidden_file")).unwrap(); + fs::File::create(temp_path.join("visible_file.txt")).unwrap(); + + // Test completion with "." prefix + let completer = ShellCompleter::new(HashSet::new()); + let history = DefaultHistory::new(); + let line = format!("cat {}/.gi", temp_path.display()); + let pos = line.len(); + let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + + // Should find .gitignore and .github/ + assert_eq!(matches.len(), 2); + let displays: Vec<&str> = matches.iter().map(|m| m.display.as_str()).collect(); + assert!(displays.contains(&".github/")); + assert!(displays.contains(&".gitignore")); + } + + #[test] + fn test_skip_hidden_files_when_not_starting_with_dot() { + let temp_dir = TempDir::new().unwrap(); + let temp_path = temp_dir.path(); + + // Create some test files and directories + fs::File::create(temp_path.join(".gitignore")).unwrap(); + fs::create_dir(temp_path.join(".github")).unwrap(); + fs::File::create(temp_path.join("visible_file.txt")).unwrap(); + fs::File::create(temp_path.join("another_file.txt")).unwrap(); + + // Test completion without "." prefix + let completer = ShellCompleter::new(HashSet::new()); + let history = DefaultHistory::new(); + let line = format!("cat {}/", temp_path.display()); + let pos = line.len(); + let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + + // Should only find visible files, not hidden ones + let displays: Vec<&str> = matches.iter().map(|m| m.display.as_str()).collect(); + assert!(!displays.iter().any(|d| d.starts_with('.'))); + assert!(displays.len() >= 2); // Should have at least the two visible files + } + + #[test] + fn test_complete_github_directory() { + let temp_dir = TempDir::new().unwrap(); + let temp_path = temp_dir.path(); + + // Create .github directory and other dot files + fs::create_dir(temp_path.join(".github")).unwrap(); + fs::File::create(temp_path.join(".gitignore")).unwrap(); + fs::File::create(temp_path.join(".git_keep")).unwrap(); + + // Test completion with ".gith" prefix + let completer = ShellCompleter::new(HashSet::new()); + let history = DefaultHistory::new(); + let line = format!("cd {}/.gith", temp_path.display()); + let pos = line.len(); + let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + + // Should find .github/ + assert_eq!(matches.len(), 1); + assert_eq!(matches[0].display, ".github/"); + } + + #[test] + fn test_complete_all_hidden_with_dot() { + let temp_dir = TempDir::new().unwrap(); + let temp_path = temp_dir.path(); + + // Create several hidden files + fs::File::create(temp_path.join(".env")).unwrap(); + fs::File::create(temp_path.join(".bashrc")).unwrap(); + fs::create_dir(temp_path.join(".config")).unwrap(); + + // Test completion with just "." prefix + let completer = ShellCompleter::new(HashSet::new()); + let history = DefaultHistory::new(); + let line = format!("ls {}/.", temp_path.display()); + let pos = line.len(); + let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + + // Should find all hidden files + assert!(matches.len() >= 3); + let displays: Vec<&str> = matches.iter().map(|m| m.display.as_str()).collect(); + assert!(displays.contains(&".env")); + assert!(displays.contains(&".bashrc")); + assert!(displays.contains(&".config/")); + } +} From cf88f9d0ef1c39deaf5b2c43f0a79165e184fa6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C4=8Cert=C3=ADk?= Date: Sat, 8 Nov 2025 07:59:35 -0700 Subject: [PATCH 3/4] Use tokio::test for tests --- crates/shell/src/completion.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/shell/src/completion.rs b/crates/shell/src/completion.rs index 93c52e4..67dfc8e 100644 --- a/crates/shell/src/completion.rs +++ b/crates/shell/src/completion.rs @@ -259,8 +259,8 @@ mod tests { use std::fs; use tempfile::TempDir; - #[test] - fn test_complete_hidden_files_when_starting_with_dot() { + #[tokio::test] + async fn test_complete_hidden_files_when_starting_with_dot() { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); @@ -284,8 +284,8 @@ mod tests { assert!(displays.contains(&".gitignore")); } - #[test] - fn test_skip_hidden_files_when_not_starting_with_dot() { + #[tokio::test] + async fn test_skip_hidden_files_when_not_starting_with_dot() { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); @@ -308,8 +308,8 @@ mod tests { assert!(displays.len() >= 2); // Should have at least the two visible files } - #[test] - fn test_complete_github_directory() { + #[tokio::test] + async fn test_complete_github_directory() { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); @@ -330,8 +330,8 @@ mod tests { assert_eq!(matches[0].display, ".github/"); } - #[test] - fn test_complete_all_hidden_with_dot() { + #[tokio::test] + async fn test_complete_all_hidden_with_dot() { let temp_dir = TempDir::new().unwrap(); let temp_path = temp_dir.path(); From 7adaf0f608741d690e5ad8e41ce3537d065299ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20=C4=8Cert=C3=ADk?= Date: Sat, 8 Nov 2025 08:00:48 -0700 Subject: [PATCH 4/4] Fix fmt --- crates/shell/src/completion.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/shell/src/completion.rs b/crates/shell/src/completion.rs index 67dfc8e..3ef502b 100644 --- a/crates/shell/src/completion.rs +++ b/crates/shell/src/completion.rs @@ -275,7 +275,9 @@ mod tests { let history = DefaultHistory::new(); let line = format!("cat {}/.gi", temp_path.display()); let pos = line.len(); - let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + let (_start, matches) = completer + .complete(&line, pos, &Context::new(&history)) + .unwrap(); // Should find .gitignore and .github/ assert_eq!(matches.len(), 2); @@ -300,7 +302,9 @@ mod tests { let history = DefaultHistory::new(); let line = format!("cat {}/", temp_path.display()); let pos = line.len(); - let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + let (_start, matches) = completer + .complete(&line, pos, &Context::new(&history)) + .unwrap(); // Should only find visible files, not hidden ones let displays: Vec<&str> = matches.iter().map(|m| m.display.as_str()).collect(); @@ -323,7 +327,9 @@ mod tests { let history = DefaultHistory::new(); let line = format!("cd {}/.gith", temp_path.display()); let pos = line.len(); - let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + let (_start, matches) = completer + .complete(&line, pos, &Context::new(&history)) + .unwrap(); // Should find .github/ assert_eq!(matches.len(), 1); @@ -345,7 +351,9 @@ mod tests { let history = DefaultHistory::new(); let line = format!("ls {}/.", temp_path.display()); let pos = line.len(); - let (_start, matches) = completer.complete(&line, pos, &Context::new(&history)).unwrap(); + let (_start, matches) = completer + .complete(&line, pos, &Context::new(&history)) + .unwrap(); // Should find all hidden files assert!(matches.len() >= 3);