Skip to content

Commit 78fba86

Browse files
committedMar 3, 2022
Picker performance improvements
1 parent 0ff3e3e commit 78fba86

File tree

4 files changed

+108
-52
lines changed

4 files changed

+108
-52
lines changed
 

‎Cargo.lock

+7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎helix-term/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,8 @@ serde = { version = "1.0", features = ["derive"] }
6161
grep-regex = "0.1.9"
6262
grep-searcher = "0.1.8"
6363

64+
# Remove once retain_mut lands in stable rust
65+
retain_mut = "0.1.7"
66+
6467
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
6568
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

‎helix-term/src/ui/mod.rs

+17-27
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ pub fn regex_prompt(
9898

9999
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
100100
use ignore::{types::TypesBuilder, WalkBuilder};
101-
use std::time;
101+
use std::time::Instant;
102+
103+
let now = Instant::now();
102104

103105
let mut walk_builder = WalkBuilder::new(&root);
104106
walk_builder
@@ -116,56 +118,44 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
116118

117119
// We want to exclude files that the editor can't handle yet
118120
let mut type_builder = TypesBuilder::new();
119-
120121
type_builder
121122
.add(
122123
"compressed",
123124
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
124125
)
125-
// This shouldn't panic as the above is static, but if it ever
126-
// is changed and becomes invalid it will catch here rather than
127-
// being unnoticed.
128126
.expect("Invalid type definition");
129127
type_builder.negate("all");
130-
131-
if let Ok(excluded_types) = type_builder.build() {
132-
walk_builder.types(excluded_types);
133-
}
128+
let excluded_types = type_builder
129+
.build()
130+
.expect("failed to build excluded_types");
131+
walk_builder.types(excluded_types);
134132

135133
// We want files along with their modification date for sorting
136134
let files = walk_builder.build().filter_map(|entry| {
137135
let entry = entry.ok()?;
138-
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
139-
if entry.path().is_dir() {
136+
137+
// This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
138+
let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false);
139+
140+
if is_dir {
140141
// Will give a false positive if metadata cannot be read (eg. permission error)
141142
return None;
142143
}
143144

144-
let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| {
145-
metadata
146-
.accessed()
147-
.or_else(|_| metadata.modified())
148-
.or_else(|_| metadata.created())
149-
.unwrap_or(time::UNIX_EPOCH)
150-
});
151-
152-
Some((entry.into_path(), time))
145+
Some(entry.into_path())
153146
});
154147

155148
// Cap the number of files if we aren't in a git project, preventing
156149
// hangs when using the picker in your home directory
157-
let mut files: Vec<_> = if root.join(".git").is_dir() {
150+
let files: Vec<_> = if root.join(".git").is_dir() {
158151
files.collect()
159152
} else {
160-
const MAX: usize = 8192;
153+
// const MAX: usize = 8192;
154+
const MAX: usize = 100_000;
161155
files.take(MAX).collect()
162156
};
163157

164-
// Most recently modified first
165-
files.sort_by_key(|file| std::cmp::Reverse(file.1));
166-
167-
// Strip the time data so we can send just the paths to the FilePicker
168-
let files = files.into_iter().map(|(path, _)| path).collect();
158+
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
169159

170160
FilePicker::new(
171161
files,

‎helix-term/src/ui/picker.rs

+81-25
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
1313
use fuzzy_matcher::FuzzyMatcher;
1414
use tui::widgets::Widget;
1515

16+
use std::time::Instant;
1617
use std::{
1718
borrow::Cow,
19+
cmp::Reverse,
1820
collections::HashMap,
1921
io::Read,
2022
path::{Path, PathBuf},
@@ -286,7 +288,8 @@ pub struct Picker<T> {
286288
cursor: usize,
287289
// pattern: String,
288290
prompt: Prompt,
289-
/// Wheather to truncate the start (default true)
291+
previous_pattern: String,
292+
/// Whether to truncate the start (default true)
290293
pub truncate_start: bool,
291294

292295
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
@@ -303,9 +306,7 @@ impl<T> Picker<T> {
303306
"".into(),
304307
None,
305308
ui::completers::none,
306-
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
307-
//
308-
},
309+
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
309310
);
310311

311312
let mut picker = Self {
@@ -315,44 +316,99 @@ impl<T> Picker<T> {
315316
filters: Vec::new(),
316317
cursor: 0,
317318
prompt,
319+
previous_pattern: String::new(),
318320
truncate_start: true,
319321
format_fn: Box::new(format_fn),
320322
callback_fn: Box::new(callback_fn),
321323
completion_height: 0,
322324
};
323325

324-
// TODO: scoring on empty input should just use a fastpath
325-
picker.score();
326+
// scoring on empty input:
327+
// TODO: just reuse score()
328+
picker.matches.extend(
329+
picker
330+
.options
331+
.iter()
332+
.enumerate()
333+
.map(|(index, _option)| (index, 0)),
334+
);
326335

327336
picker
328337
}
329338

330339
pub fn score(&mut self) {
340+
let now = Instant::now();
341+
331342
let pattern = &self.prompt.line;
332343

333-
// reuse the matches allocation
334-
self.matches.clear();
335-
self.matches.extend(
336-
self.options
337-
.iter()
338-
.enumerate()
339-
.filter_map(|(index, option)| {
340-
// filter options first before matching
341-
if !self.filters.is_empty() {
342-
self.filters.binary_search(&index).ok()?;
344+
if pattern == &self.previous_pattern {
345+
return;
346+
}
347+
348+
if pattern.is_empty() {
349+
// Fast path for no pattern.
350+
self.matches.clear();
351+
self.matches.extend(
352+
self.options
353+
.iter()
354+
.enumerate()
355+
.map(|(index, _option)| (index, 0)),
356+
);
357+
} else if pattern.starts_with(&self.previous_pattern) {
358+
// TODO: remove when retain_mut is in stable rust
359+
use retain_mut::RetainMut;
360+
361+
// optimization: if the pattern is a more specific version of the previous one
362+
// then we can score the filtered set.
363+
#[allow(unstable_name_collisions)]
364+
self.matches.retain_mut(|(index, score)| {
365+
let option = &self.options[*index];
366+
// TODO: maybe using format_fn isn't the best idea here
367+
let text = (self.format_fn)(option);
368+
369+
match self.matcher.fuzzy_match(&text, pattern) {
370+
Some(s) => {
371+
// Update the score
372+
*score = s;
373+
true
343374
}
344-
// TODO: maybe using format_fn isn't the best idea here
345-
let text = (self.format_fn)(option);
346-
// Highlight indices are computed lazily in the render function
347-
self.matcher
348-
.fuzzy_match(&text, pattern)
349-
.map(|score| (index, score))
350-
}),
351-
);
352-
self.matches.sort_unstable_by_key(|(_, score)| -score);
375+
None => false,
376+
}
377+
});
378+
379+
self.matches
380+
.sort_unstable_by_key(|(_, score)| Reverse(*score));
381+
} else {
382+
self.matches.clear();
383+
self.matches.extend(
384+
self.options
385+
.iter()
386+
.enumerate()
387+
.filter_map(|(index, option)| {
388+
// filter options first before matching
389+
if !self.filters.is_empty() {
390+
// TODO: this filters functionality seems inefficient,
391+
// instead store and operate on filters if any
392+
self.filters.binary_search(&index).ok()?;
393+
}
394+
395+
// TODO: maybe using format_fn isn't the best idea here
396+
let text = (self.format_fn)(option);
397+
398+
self.matcher
399+
.fuzzy_match(&text, pattern)
400+
.map(|score| (index, score))
401+
}),
402+
);
403+
self.matches
404+
.sort_unstable_by_key(|(_, score)| Reverse(*score));
405+
}
406+
407+
log::debug!("picker score {:?}", Instant::now().duration_since(now));
353408

354409
// reset cursor position
355410
self.cursor = 0;
411+
self.previous_pattern.clone_from(pattern);
356412
}
357413

358414
/// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)

0 commit comments

Comments
 (0)