Picker performance improvements

pull/1741/head
Blaž Hrastnik 2 years ago
parent 0ff3e3ea38
commit 78fba8683b
No known key found for this signature in database
GPG Key ID: 1238B9C4AD889640

7
Cargo.lock generated

@ -449,6 +449,7 @@ dependencies = [
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"pulldown-cmark", "pulldown-cmark",
"retain_mut",
"serde", "serde",
"serde_json", "serde_json",
"signal-hook", "signal-hook",
@ -845,6 +846,12 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "retain_mut"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c31b5c4033f8fdde8700e4657be2c497e7288f01515be52168c631e2e4d4086"
[[package]] [[package]]
name = "ropey" name = "ropey"
version = "1.3.2" version = "1.3.2"

@ -61,5 +61,8 @@ serde = { version = "1.0", features = ["derive"] }
grep-regex = "0.1.9" grep-regex = "0.1.9"
grep-searcher = "0.1.8" grep-searcher = "0.1.8"
# Remove once retain_mut lands in stable rust
retain_mut = "0.1.7"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

@ -98,7 +98,9 @@ pub fn regex_prompt(
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> { pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder}; use ignore::{types::TypesBuilder, WalkBuilder};
use std::time; use std::time::Instant;
let now = Instant::now();
let mut walk_builder = WalkBuilder::new(&root); let mut walk_builder = WalkBuilder::new(&root);
walk_builder walk_builder
@ -116,56 +118,44 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
// We want to exclude files that the editor can't handle yet // We want to exclude files that the editor can't handle yet
let mut type_builder = TypesBuilder::new(); let mut type_builder = TypesBuilder::new();
type_builder type_builder
.add( .add(
"compressed", "compressed",
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}", "*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
) )
// This shouldn't panic as the above is static, but if it ever
// is changed and becomes invalid it will catch here rather than
// being unnoticed.
.expect("Invalid type definition"); .expect("Invalid type definition");
type_builder.negate("all"); type_builder.negate("all");
let excluded_types = type_builder
if let Ok(excluded_types) = type_builder.build() { .build()
walk_builder.types(excluded_types); .expect("failed to build excluded_types");
} walk_builder.types(excluded_types);
// We want files along with their modification date for sorting // We want files along with their modification date for sorting
let files = walk_builder.build().filter_map(|entry| { let files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?; let entry = entry.ok()?;
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if entry.path().is_dir() { // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
if is_dir {
// Will give a false positive if metadata cannot be read (eg. permission error) // Will give a false positive if metadata cannot be read (eg. permission error)
return None; return None;
} }
let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| { Some(entry.into_path())
metadata
.accessed()
.or_else(|_| metadata.modified())
.or_else(|_| metadata.created())
.unwrap_or(time::UNIX_EPOCH)
});
Some((entry.into_path(), time))
}); });
// Cap the number of files if we aren't in a git project, preventing // Cap the number of files if we aren't in a git project, preventing
// hangs when using the picker in your home directory // hangs when using the picker in your home directory
let mut files: Vec<_> = if root.join(".git").is_dir() { let files: Vec<_> = if root.join(".git").is_dir() {
files.collect() files.collect()
} else { } else {
const MAX: usize = 8192; // const MAX: usize = 8192;
const MAX: usize = 100_000;
files.take(MAX).collect() files.take(MAX).collect()
}; };
// Most recently modified first log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
files.sort_by_key(|file| std::cmp::Reverse(file.1));
// Strip the time data so we can send just the paths to the FilePicker
let files = files.into_iter().map(|(path, _)| path).collect();
FilePicker::new( FilePicker::new(
files, files,

@ -13,8 +13,10 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget; use tui::widgets::Widget;
use std::time::Instant;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cmp::Reverse,
collections::HashMap, collections::HashMap,
io::Read, io::Read,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -286,7 +288,8 @@ pub struct Picker<T> {
cursor: usize, cursor: usize,
// pattern: String, // pattern: String,
prompt: Prompt, prompt: Prompt,
/// Wheather to truncate the start (default true) previous_pattern: String,
/// Whether to truncate the start (default true)
pub truncate_start: bool, pub truncate_start: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>, format_fn: Box<dyn Fn(&T) -> Cow<str>>,
@ -303,9 +306,7 @@ impl<T> Picker<T> {
"".into(), "".into(),
None, None,
ui::completers::none, ui::completers::none,
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
//
},
); );
let mut picker = Self { let mut picker = Self {
@ -315,44 +316,99 @@ impl<T> Picker<T> {
filters: Vec::new(), filters: Vec::new(),
cursor: 0, cursor: 0,
prompt, prompt,
previous_pattern: String::new(),
truncate_start: true, truncate_start: true,
format_fn: Box::new(format_fn), format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
completion_height: 0, completion_height: 0,
}; };
// TODO: scoring on empty input should just use a fastpath // scoring on empty input:
picker.score(); // TODO: just reuse score()
picker.matches.extend(
picker
.options
.iter()
.enumerate()
.map(|(index, _option)| (index, 0)),
);
picker picker
} }
pub fn score(&mut self) { pub fn score(&mut self) {
let now = Instant::now();
let pattern = &self.prompt.line; let pattern = &self.prompt.line;
// reuse the matches allocation if pattern == &self.previous_pattern {
self.matches.clear(); return;
self.matches.extend( }
self.options
.iter() if pattern.is_empty() {
.enumerate() // Fast path for no pattern.
.filter_map(|(index, option)| { self.matches.clear();
// filter options first before matching self.matches.extend(
if !self.filters.is_empty() { self.options
self.filters.binary_search(&index).ok()?; .iter()
.enumerate()
.map(|(index, _option)| (index, 0)),
);
} else if pattern.starts_with(&self.previous_pattern) {
// TODO: remove when retain_mut is in stable rust
use retain_mut::RetainMut;
// optimization: if the pattern is a more specific version of the previous one
// then we can score the filtered set.
#[allow(unstable_name_collisions)]
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
// TODO: maybe using format_fn isn't the best idea here
let text = (self.format_fn)(option);
match self.matcher.fuzzy_match(&text, pattern) {
Some(s) => {
// Update the score
*score = s;
true
} }
// TODO: maybe using format_fn isn't the best idea here None => false,
let text = (self.format_fn)(option); }
// Highlight indices are computed lazily in the render function });
self.matcher
.fuzzy_match(&text, pattern) self.matches
.map(|score| (index, score)) .sort_unstable_by_key(|(_, score)| Reverse(*score));
}), } else {
); self.matches.clear();
self.matches.sort_unstable_by_key(|(_, score)| -score); self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
if !self.filters.is_empty() {
// TODO: this filters functionality seems inefficient,
// instead store and operate on filters if any
self.filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
let text = (self.format_fn)(option);
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
self.matches
.sort_unstable_by_key(|(_, score)| Reverse(*score));
}
log::debug!("picker score {:?}", Instant::now().duration_since(now));
// reset cursor position // reset cursor position
self.cursor = 0; self.cursor = 0;
self.previous_pattern.clone_from(pattern);
} }
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)

Loading…
Cancel
Save