@@ -5,6 +5,7 @@ use std::cmp::Ordering;
55use std:: fs;
66use std:: path:: { Path , PathBuf } ;
77
8+ use edit:: arena:: scratch_arena;
89use edit:: framebuffer:: IndexedColor ;
910use edit:: helpers:: * ;
1011use edit:: input:: { kbmod, vk} ;
@@ -135,8 +136,6 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
135136 draw_dialog_saveas_refresh_files ( state) ;
136137 }
137138
138- let files = state. file_picker_entries . as_ref ( ) . unwrap ( ) ;
139-
140139 ctx. scrollarea_begin (
141140 "directory" ,
142141 Size {
@@ -152,16 +151,20 @@ pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
152151 ctx. next_block_id_mixin ( state. file_picker_pending_dir_revision ) ;
153152 ctx. list_begin ( "files" ) ;
154153 ctx. inherit_focus ( ) ;
155- for entry in files {
156- match ctx. list_item ( false , entry. as_str ( ) ) {
157- ListSelection :: Unchanged => { }
158- ListSelection :: Selected => {
159- state. file_picker_pending_name = entry. as_path ( ) . into ( )
154+
155+ for entries in state. file_picker_entries . as_ref ( ) . unwrap ( ) {
156+ for entry in entries {
157+ match ctx. list_item ( false , entry. as_str ( ) ) {
158+ ListSelection :: Unchanged => { }
159+ ListSelection :: Selected => {
160+ state. file_picker_pending_name = entry. as_path ( ) . into ( )
161+ }
162+ ListSelection :: Activated => activated = true ,
160163 }
161- ListSelection :: Activated => activated = true ,
164+ ctx . attr_overflow ( Overflow :: TruncateMiddle ) ;
162165 }
163- ctx. attr_overflow ( Overflow :: TruncateMiddle ) ;
164166 }
167+
165168 ctx. list_end ( ) ;
166169 }
167170 ctx. scrollarea_end ( ) ;
@@ -300,81 +303,102 @@ fn draw_file_picker_update_path(state: &mut State) -> Option<PathBuf> {
300303
301304fn draw_dialog_saveas_refresh_files ( state : & mut State ) {
302305 let dir = state. file_picker_pending_dir . as_path ( ) ;
303- let mut files = Vec :: new ( ) ;
304- let mut off = 0 ;
306+ // ["..", directories, files]
307+ let mut dirs_files = [ Vec :: new ( ) , Vec :: new ( ) , Vec :: new ( ) ] ;
305308
306309 #[ cfg( windows) ]
307310 if dir. as_os_str ( ) . is_empty ( ) {
308311 // If the path is empty, we are at the drive picker.
309312 // Add all drives as entries.
310313 for drive in edit:: sys:: drives ( ) {
311- files . push ( DisplayablePathBuf :: from_string ( format ! ( "{drive}:\\ " ) ) ) ;
314+ dirs_files [ 1 ] . push ( DisplayablePathBuf :: from_string ( format ! ( "{drive}:\\ " ) ) ) ;
312315 }
313316
314- state. file_picker_entries = Some ( files ) ;
317+ state. file_picker_entries = Some ( dirs_files ) ;
315318 return ;
316319 }
317320
318321 if cfg ! ( windows) || dir. parent ( ) . is_some ( ) {
319- files. push ( DisplayablePathBuf :: from ( ".." ) ) ;
320- off = 1 ;
322+ dirs_files[ 0 ] . push ( DisplayablePathBuf :: from ( ".." ) ) ;
321323 }
322324
323325 if let Ok ( iter) = fs:: read_dir ( dir) {
324326 for entry in iter. flatten ( ) {
325327 if let Ok ( metadata) = entry. metadata ( ) {
326328 let mut name = entry. file_name ( ) ;
327- if metadata. is_dir ( )
329+ let dir = metadata. is_dir ( )
328330 || ( metadata. is_symlink ( )
329- && fs:: metadata ( entry. path ( ) ) . is_ok_and ( |m| m. is_dir ( ) ) )
330- {
331+ && fs:: metadata ( entry. path ( ) ) . is_ok_and ( |m| m. is_dir ( ) ) ) ;
332+ let idx = if dir { 1 } else { 2 } ;
333+
334+ if dir {
331335 name. push ( "/" ) ;
332336 }
333- files. push ( DisplayablePathBuf :: from ( name) ) ;
337+
338+ dirs_files[ idx] . push ( DisplayablePathBuf :: from ( name) ) ;
334339 }
335340 }
336341 }
337342
338- // Sort directories first, then by name, case-insensitive.
339- files [ off.. ] . sort_by ( |a, b| {
340- let a = a. as_bytes ( ) ;
341- let b = b. as_bytes ( ) ;
343+ for entries in & mut dirs_files [ 1 .. ] {
344+ entries . sort_by ( |a, b| {
345+ let a = a. as_bytes ( ) ;
346+ let b = b. as_bytes ( ) ;
342347
343- let a_is_dir = a. last ( ) == Some ( & b'/' ) ;
344- let b_is_dir = b. last ( ) == Some ( & b'/' ) ;
348+ let a_is_dir = a. last ( ) == Some ( & b'/' ) ;
349+ let b_is_dir = b. last ( ) == Some ( & b'/' ) ;
345350
346- match b_is_dir. cmp ( & a_is_dir) {
347- Ordering :: Equal => icu:: compare_strings ( a, b) ,
348- other => other,
349- }
350- } ) ;
351+ match b_is_dir. cmp ( & a_is_dir) {
352+ Ordering :: Equal => icu:: compare_strings ( a, b) ,
353+ other => other,
354+ }
355+ } ) ;
356+ }
351357
352- state. file_picker_entries = Some ( files ) ;
358+ state. file_picker_entries = Some ( dirs_files ) ;
353359}
354360
361+ #[ inline( never) ]
355362fn update_autocomplete_suggestions ( state : & mut State ) {
356363 state. file_picker_autocomplete . clear ( ) ;
357364
358365 if state. file_picker_pending_name . as_os_str ( ) . is_empty ( ) {
359366 return ;
360367 }
361368
369+ let scratch = scratch_arena ( None ) ;
362370 let needle = state. file_picker_pending_name . as_os_str ( ) . as_encoded_bytes ( ) ;
363371 let mut matches = Vec :: new ( ) ;
364372
365- if let Some ( entries) = & state. file_picker_entries {
366- // Remove the first entry, which is always "..".
367- for entry in & entries[ 1 . min ( entries. len ( ) ) ..] {
368- let haystack = entry. as_bytes ( ) ;
369- // We only want items that are longer than the needle,
370- // because we're looking for suggestions, not for matches.
371- if haystack. len ( ) > needle. len ( )
372- && let haystack = & haystack[ ..needle. len ( ) ]
373- && icu:: compare_strings ( haystack, needle) == Ordering :: Equal
374- {
375- matches. push ( entry. clone ( ) ) ;
376- if matches. len ( ) >= 5 {
377- break ; // Limit to 5 suggestions
373+ // Using binary search below we'll quickly find the lower bound
374+ // of items that match the needle (= share a common prefix).
375+ //
376+ // The problem is finding the upper bound. Here I'm using a trick:
377+ // By appending U+10FFFF (the highest possible Unicode code point)
378+ // we create a needle that naturally yields an upper bound.
379+ let mut needle_upper_bound = Vec :: with_capacity_in ( needle. len ( ) + 4 , & * scratch) ;
380+ needle_upper_bound. extend_from_slice ( needle) ;
381+ needle_upper_bound. extend_from_slice ( b"\xf4 \x8f \xbf \xbf " ) ;
382+
383+ if let Some ( dirs_files) = & state. file_picker_entries {
384+ ' outer: for entries in & dirs_files[ 1 ..] {
385+ let lower = entries
386+ . binary_search_by ( |entry| icu:: compare_strings ( entry. as_bytes ( ) , needle) )
387+ . unwrap_or_else ( |i| i) ;
388+
389+ for entry in & entries[ lower..] {
390+ let haystack = entry. as_bytes ( ) ;
391+ match icu:: compare_strings ( haystack, & needle_upper_bound) {
392+ Ordering :: Less => {
393+ matches. push ( entry. clone ( ) ) ;
394+ if matches. len ( ) >= 5 {
395+ break ' outer; // Limit to 5 suggestions
396+ }
397+ }
398+ // We're looking for suggestions, not for matches.
399+ Ordering :: Equal => { }
400+ // No more matches possible.
401+ Ordering :: Greater => break ,
378402 }
379403 }
380404 }
0 commit comments