@@ -85,16 +85,16 @@ struct FileMatch {
8585}
8686
8787impl FileMatch {
88- fn from_entry ( entry : fs:: DirEntry , base_path : & Path ) -> Option < Self > {
88+ fn from_entry ( entry : fs:: DirEntry , base_path : & Path , show_hidden : bool ) -> Option < Self > {
8989 let metadata = match entry. metadata ( ) {
9090 Ok ( m) => m,
9191 Err ( _) => return None ,
9292 } ;
9393
9494 let name = entry. file_name ( ) . into_string ( ) . ok ( ) ?;
9595
96- // Skip hidden files
97- if name. starts_with ( '.' ) {
96+ // Skip hidden files unless explicitly requested
97+ if !show_hidden && name. starts_with ( '.' ) {
9898 return None ;
9999 }
100100
@@ -175,12 +175,13 @@ fn complete_filenames(is_start: bool, word: &str, matches: &mut Vec<Pair>) {
175175
176176 let search_dir = resolve_dir_path ( dir_path) ;
177177 let only_executable = ( word. starts_with ( "./" ) || word. starts_with ( '/' ) ) && is_start;
178+ let show_hidden = partial_name. starts_with ( '.' ) ;
178179
179180 let files: Vec < FileMatch > = fs:: read_dir ( & search_dir)
180181 . into_iter ( )
181182 . flatten ( )
182183 . flatten ( )
183- . filter_map ( |entry| FileMatch :: from_entry ( entry, & search_dir) )
184+ . filter_map ( |entry| FileMatch :: from_entry ( entry, & search_dir, show_hidden ) )
184185 . filter ( |f| f. name . starts_with ( partial_name) )
185186 . filter ( |f| !only_executable || f. is_executable || f. is_dir )
186187 . collect ( ) ;
@@ -250,3 +251,115 @@ impl Highlighter for ShellCompleter {
250251impl Validator for ShellCompleter { }
251252
252253impl Helper for ShellCompleter { }
254+
255+ #[ cfg( test) ]
256+ mod tests {
257+ use super :: * ;
258+ use rustyline:: history:: DefaultHistory ;
259+ use std:: fs;
260+ use tempfile:: TempDir ;
261+
262+ #[ tokio:: test]
263+ async fn test_complete_hidden_files_when_starting_with_dot ( ) {
264+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
265+ let temp_path = temp_dir. path ( ) ;
266+
267+ // Create some test files and directories
268+ fs:: File :: create ( temp_path. join ( ".gitignore" ) ) . unwrap ( ) ;
269+ fs:: create_dir ( temp_path. join ( ".github" ) ) . unwrap ( ) ;
270+ fs:: File :: create ( temp_path. join ( ".hidden_file" ) ) . unwrap ( ) ;
271+ fs:: File :: create ( temp_path. join ( "visible_file.txt" ) ) . unwrap ( ) ;
272+
273+ // Test completion with "." prefix
274+ let completer = ShellCompleter :: new ( HashSet :: new ( ) ) ;
275+ let history = DefaultHistory :: new ( ) ;
276+ let line = format ! ( "cat {}/.gi" , temp_path. display( ) ) ;
277+ let pos = line. len ( ) ;
278+ let ( _start, matches) = completer
279+ . complete ( & line, pos, & Context :: new ( & history) )
280+ . unwrap ( ) ;
281+
282+ // Should find .gitignore and .github/
283+ assert_eq ! ( matches. len( ) , 2 ) ;
284+ let displays: Vec < & str > = matches. iter ( ) . map ( |m| m. display . as_str ( ) ) . collect ( ) ;
285+ assert ! ( displays. contains( & ".github/" ) ) ;
286+ assert ! ( displays. contains( & ".gitignore" ) ) ;
287+ }
288+
289+ #[ tokio:: test]
290+ async fn test_skip_hidden_files_when_not_starting_with_dot ( ) {
291+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
292+ let temp_path = temp_dir. path ( ) ;
293+
294+ // Create some test files and directories
295+ fs:: File :: create ( temp_path. join ( ".gitignore" ) ) . unwrap ( ) ;
296+ fs:: create_dir ( temp_path. join ( ".github" ) ) . unwrap ( ) ;
297+ fs:: File :: create ( temp_path. join ( "visible_file.txt" ) ) . unwrap ( ) ;
298+ fs:: File :: create ( temp_path. join ( "another_file.txt" ) ) . unwrap ( ) ;
299+
300+ // Test completion without "." prefix
301+ let completer = ShellCompleter :: new ( HashSet :: new ( ) ) ;
302+ let history = DefaultHistory :: new ( ) ;
303+ let line = format ! ( "cat {}/" , temp_path. display( ) ) ;
304+ let pos = line. len ( ) ;
305+ let ( _start, matches) = completer
306+ . complete ( & line, pos, & Context :: new ( & history) )
307+ . unwrap ( ) ;
308+
309+ // Should only find visible files, not hidden ones
310+ let displays: Vec < & str > = matches. iter ( ) . map ( |m| m. display . as_str ( ) ) . collect ( ) ;
311+ assert ! ( !displays. iter( ) . any( |d| d. starts_with( '.' ) ) ) ;
312+ assert ! ( displays. len( ) >= 2 ) ; // Should have at least the two visible files
313+ }
314+
315+ #[ tokio:: test]
316+ async fn test_complete_github_directory ( ) {
317+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
318+ let temp_path = temp_dir. path ( ) ;
319+
320+ // Create .github directory and other dot files
321+ fs:: create_dir ( temp_path. join ( ".github" ) ) . unwrap ( ) ;
322+ fs:: File :: create ( temp_path. join ( ".gitignore" ) ) . unwrap ( ) ;
323+ fs:: File :: create ( temp_path. join ( ".git_keep" ) ) . unwrap ( ) ;
324+
325+ // Test completion with ".gith" prefix
326+ let completer = ShellCompleter :: new ( HashSet :: new ( ) ) ;
327+ let history = DefaultHistory :: new ( ) ;
328+ let line = format ! ( "cd {}/.gith" , temp_path. display( ) ) ;
329+ let pos = line. len ( ) ;
330+ let ( _start, matches) = completer
331+ . complete ( & line, pos, & Context :: new ( & history) )
332+ . unwrap ( ) ;
333+
334+ // Should find .github/
335+ assert_eq ! ( matches. len( ) , 1 ) ;
336+ assert_eq ! ( matches[ 0 ] . display, ".github/" ) ;
337+ }
338+
339+ #[ tokio:: test]
340+ async fn test_complete_all_hidden_with_dot ( ) {
341+ let temp_dir = TempDir :: new ( ) . unwrap ( ) ;
342+ let temp_path = temp_dir. path ( ) ;
343+
344+ // Create several hidden files
345+ fs:: File :: create ( temp_path. join ( ".env" ) ) . unwrap ( ) ;
346+ fs:: File :: create ( temp_path. join ( ".bashrc" ) ) . unwrap ( ) ;
347+ fs:: create_dir ( temp_path. join ( ".config" ) ) . unwrap ( ) ;
348+
349+ // Test completion with just "." prefix
350+ let completer = ShellCompleter :: new ( HashSet :: new ( ) ) ;
351+ let history = DefaultHistory :: new ( ) ;
352+ let line = format ! ( "ls {}/." , temp_path. display( ) ) ;
353+ let pos = line. len ( ) ;
354+ let ( _start, matches) = completer
355+ . complete ( & line, pos, & Context :: new ( & history) )
356+ . unwrap ( ) ;
357+
358+ // Should find all hidden files
359+ assert ! ( matches. len( ) >= 3 ) ;
360+ let displays: Vec < & str > = matches. iter ( ) . map ( |m| m. display . as_str ( ) ) . collect ( ) ;
361+ assert ! ( displays. contains( & ".env" ) ) ;
362+ assert ! ( displays. contains( & ".bashrc" ) ) ;
363+ assert ! ( displays. contains( & ".config/" ) ) ;
364+ }
365+ }
0 commit comments