-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
File picker: allow parallel traversal w/o sorting #12688
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,7 @@ use helix_stdx::rope; | |
use helix_view::theme::Style; | ||
pub use markdown::Markdown; | ||
pub use menu::Menu; | ||
pub use picker::{Column as PickerColumn, FileLocation, Picker}; | ||
pub use picker::{Column as PickerColumn, FileLocation, Injector, Picker}; | ||
pub use popup::Popup; | ||
pub use prompt::{Prompt, PromptEvent}; | ||
pub use spinner::{ProgressSpinners, Spinner}; | ||
|
@@ -189,9 +189,6 @@ type FilePicker = Picker<PathBuf, PathBuf>; | |
|
||
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker { | ||
use ignore::{types::TypesBuilder, WalkBuilder}; | ||
use std::time::Instant; | ||
|
||
let now = Instant::now(); | ||
|
||
let dedup_symlinks = config.file_picker.deduplicate_links; | ||
let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone()); | ||
|
@@ -205,12 +202,14 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi | |
.git_ignore(config.file_picker.git_ignore) | ||
.git_global(config.file_picker.git_global) | ||
.git_exclude(config.file_picker.git_exclude) | ||
.sort_by_file_name(|name1, name2| name1.cmp(name2)) | ||
.max_depth(config.file_picker.max_depth) | ||
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks)); | ||
|
||
walk_builder.add_custom_ignore_filename(helix_loader::config_dir().join("ignore")); | ||
walk_builder.add_custom_ignore_filename(".helix/ignore"); | ||
if config.file_picker.sorted { | ||
walk_builder.sort_by_file_name(|name1, name2| name1.cmp(name2)); | ||
} | ||
|
||
// We want to exclude files that the editor can't handle yet | ||
let mut type_builder = TypesBuilder::new(); | ||
|
@@ -225,14 +224,6 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi | |
.build() | ||
.expect("failed to build excluded_types"); | ||
walk_builder.types(excluded_types); | ||
let mut files = walk_builder.build().filter_map(|entry| { | ||
let entry = entry.ok()?; | ||
if !entry.file_type()?.is_file() { | ||
return None; | ||
} | ||
Some(entry.into_path()) | ||
}); | ||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); | ||
|
||
let columns = [PickerColumn::new( | ||
"path", | ||
|
@@ -254,11 +245,37 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi | |
} | ||
}) | ||
.with_preview(|_editor, path| Some((path.as_path().into(), None))); | ||
let injector = picker.injector(); | ||
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); | ||
inject_files(picker.injector(), walk_builder, config.file_picker.sorted); | ||
|
||
picker | ||
} | ||
|
||
fn inject_files( | ||
injector: Injector<PathBuf, PathBuf>, | ||
mut walk_builder: ignore::WalkBuilder, | ||
sorted: bool, | ||
) { | ||
use ignore::WalkState; | ||
use std::collections::HashSet; | ||
use std::time::{Duration, Instant}; | ||
|
||
// How long we are willing to wait for results before displaying the file picker. | ||
let now = Instant::now(); | ||
let timeout = now + Duration::from_millis(30); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would probably be a config option as well. Otherwise, it's unclear how to reliably trigger all code paths from the integration tests. |
||
|
||
// Keep the track of injected files to prevent duplicates. | ||
let mut injected_files = HashSet::new(); | ||
let mut files_iter = walk_builder.build().filter_map(|entry| { | ||
let entry = entry.ok()?; | ||
if !entry.file_type()?.is_file() { | ||
return None; | ||
} | ||
Some(entry.into_path()) | ||
}); | ||
|
||
let mut hit_timeout = false; | ||
for file in &mut files { | ||
for file in &mut files_iter { | ||
injected_files.insert(file.clone()); | ||
if injector.push(file).is_err() { | ||
break; | ||
} | ||
|
@@ -268,15 +285,49 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi | |
} | ||
} | ||
if hit_timeout { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A possible alternative would be to drop this timeout to build and return an (empty) |
||
// Finish injecting the files in a background thread to not block displaying the file picker. | ||
std::thread::spawn(move || { | ||
for file in files { | ||
if injector.push(file).is_err() { | ||
break; | ||
// We want to lazily traverse the file tree as the file picker window can be closed | ||
// before a full traversal is complete. If we are not required to present the file list | ||
// in the sorted order, we can build an instance of WalkParallel to speed things up. | ||
if sorted { | ||
for file in files_iter { | ||
if injector.push(file).is_err() { | ||
break; | ||
} | ||
} | ||
} else { | ||
let injector = &injector; | ||
let returned_files = &injected_files; | ||
let num_threads = std::thread::available_parallelism().map_or(0, |v| v.get()); | ||
|
||
let walk_parallel = walk_builder.threads(num_threads).build_parallel(); | ||
walk_parallel.run(|| { | ||
Box::new(move |path| { | ||
if let Ok(path) = path { | ||
if path | ||
.file_type() | ||
.is_some_and(|file_type| file_type.is_file()) | ||
{ | ||
let path = path.into_path(); | ||
if !returned_files.contains(&path) && injector.push(path).is_err() { | ||
// Injector is shut down. Tell WalkParallel to stop. | ||
return WalkState::Quit; | ||
} | ||
} | ||
} | ||
|
||
// If path is a directory entry, continue descending recursively. | ||
WalkState::Continue | ||
}) | ||
}); | ||
} | ||
|
||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); | ||
}); | ||
} else { | ||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); | ||
} | ||
picker | ||
} | ||
|
||
type FileExplorer = Picker<(PathBuf, bool), (PathBuf, Style)>; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that this log predates lazy traversal of the file tree. As it was, it would record how long it takes to build an instance of
Walk
, but, I think, we are more interested in how long it takes to traverse the file tree and inject all file entries into aPicker
?