on master help sign in
transition to nucleo for fuzzy matching (#7814)

* transition to nucleo for fuzzy matching * drop flakey test case since the picker streams in results now any test that relies on the picker containing results is potentially flakely * use crates.io version of nucleo * Fix typo in commands.rs Co-authored-by: Skyler Hawthorne <skyler@dead10ck.com> --------- Co-authored-by: Skyler Hawthorne <skyler@dead10ck.com>

commit0cb595e226c9970989ee1e680ae6b8011d188cbf authorPascal Kuthe <pascal.kuthe@semimod.de> dateWed 30 Aug 2023, 06:26 (3 years ago) parent40d7e6c
26 files changed+757−1052
Cargo.lock+116−10 Cargo.toml+2 helix-core/Cargo.toml+3 helix-core/src/fuzzy.rs+43 helix-core/src/lib.rs+1 helix-event/Cargo.toml+15 helix-event/src/lib.rs+8 helix-event/src/redraw.rs+49 helix-term/Cargo.toml+2−1 helix-term/src/application.rs+2−10 helix-term/src/commands.rs+149−146 helix-term/src/commands/typed.rs+13−25 helix-term/src/ui/fuzzy_match.rs−239 helix-term/src/ui/fuzzy_match/test.rs−47 helix-term/src/ui/menu.rs+24−27 helix-term/src/ui/mod.rs+44−140 helix-term/src/ui/picker.rs+267−278 helix-term/tests/integration.rs−1 helix-term/tests/test/picker.rs−80 helix-vcs/Cargo.toml+1 helix-vcs/src/diff.rs+6−19 helix-vcs/src/diff/worker.rs+3−9 helix-vcs/src/diff/worker/test.rs+1−5 helix-view/Cargo.toml+1 helix-view/src/document.rs+4−5 helix-view/src/editor.rs+3−10
Cargo.lock+116−10
@@ -248,6 +248,12 @@ version = "0.8.4"
248248source = "registry+https://github.com/rust-lang/crates.io-index"
249249checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
250250
251+[[package]]
252+name = "cov-mark"
253+version = "1.1.0"
254+source = "registry+https://github.com/rust-lang/crates.io-index"
255+checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3"
256+
251257[[package]]
252258name = "crc32fast"
253259version = "1.3.2"
@@ -257,6 +263,49 @@ dependencies = [
257263 "cfg-if",
258264]
259265
266+[[package]]
267+name = "crossbeam-channel"
268+version = "0.5.8"
269+source = "registry+https://github.com/rust-lang/crates.io-index"
270+checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
271+dependencies = [
272+ "cfg-if",
273+ "crossbeam-utils",
274+]
275+
276+[[package]]
277+name = "crossbeam-deque"
278+version = "0.8.3"
279+source = "registry+https://github.com/rust-lang/crates.io-index"
280+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
281+dependencies = [
282+ "cfg-if",
283+ "crossbeam-epoch",
284+ "crossbeam-utils",
285+]
286+
287+[[package]]
288+name = "crossbeam-epoch"
289+version = "0.9.15"
290+source = "registry+https://github.com/rust-lang/crates.io-index"
291+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
292+dependencies = [
293+ "autocfg",
294+ "cfg-if",
295+ "crossbeam-utils",
296+ "memoffset",
297+ "scopeguard",
298+]
299+
300+[[package]]
301+name = "crossbeam-utils"
302+version = "0.8.16"
303+source = "registry+https://github.com/rust-lang/crates.io-index"
304+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
305+dependencies = [
306+ "cfg-if",
307+]
308+
260309[[package]]
261310name = "crossterm"
262311version = "0.27.0"
@@ -505,15 +554,6 @@ dependencies = [
505554 "slab",
506555]
507556
508−[[package]]
509−name = "fuzzy-matcher"
510−version = "0.3.7"
511−source = "registry+https://github.com/rust-lang/crates.io-index"
512−checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
513−dependencies = [
514− "thread_local",
515−]
516−
517557[[package]]
518558name = "getrandom"
519559version = "0.2.9"
@@ -1227,7 +1267,9 @@ dependencies = [
12271267 "imara-diff",
12281268 "indoc",
12291269 "log",
1270+ "nucleo",
12301271 "once_cell",
1272+ "parking_lot",
12311273 "quickcheck",
12321274 "regex",
12331275 "ropey",
@@ -1259,6 +1301,14 @@ dependencies = [
12591301 "which",
12601302]
12611303
1304+[[package]]
1305+name = "helix-event"
1306+version = "0.6.0"
1307+dependencies = [
1308+ "parking_lot",
1309+ "tokio",
1310+]
1311+
12621312[[package]]
12631313name = "helix-loader"
12641314version = "0.6.0"
@@ -1315,11 +1365,11 @@ dependencies = [
13151365 "crossterm",
13161366 "fern",
13171367 "futures-util",
1318− "fuzzy-matcher",
13191368 "grep-regex",
13201369 "grep-searcher",
13211370 "helix-core",
13221371 "helix-dap",
1372+ "helix-event",
13231373 "helix-loader",
13241374 "helix-lsp",
13251375 "helix-tui",
@@ -1329,6 +1379,7 @@ dependencies = [
13291379 "indoc",
13301380 "libc",
13311381 "log",
1382+ "nucleo",
13321383 "once_cell",
13331384 "pulldown-cmark",
13341385 "serde",
@@ -1367,6 +1418,7 @@ dependencies = [
13671418 "arc-swap",
13681419 "gix",
13691420 "helix-core",
1421+ "helix-event",
13701422 "imara-diff",
13711423 "log",
13721424 "parking_lot",
@@ -1387,6 +1439,7 @@ dependencies = [
13871439 "futures-util",
13881440 "helix-core",
13891441 "helix-dap",
1442+ "helix-event",
13901443 "helix-loader",
13911444 "helix-lsp",
13921445 "helix-tui",
@@ -1654,6 +1707,15 @@ dependencies = [
16541707 "libc",
16551708]
16561709
1710+[[package]]
1711+name = "memoffset"
1712+version = "0.9.0"
1713+source = "registry+https://github.com/rust-lang/crates.io-index"
1714+checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
1715+dependencies = [
1716+ "autocfg",
1717+]
1718+
16571719[[package]]
16581720name = "minimal-lexical"
16591721version = "0.2.1"
@@ -1700,6 +1762,28 @@ dependencies = [
17001762 "minimal-lexical",
17011763]
17021764
1765+[[package]]
1766+name = "nucleo"
1767+version = "0.2.0"
1768+source = "registry+https://github.com/rust-lang/crates.io-index"
1769+checksum = "0ccab936f2c8ad271bb31430944d98d358f74153566ea323265497f5639b11b6"
1770+dependencies = [
1771+ "nucleo-matcher",
1772+ "parking_lot",
1773+ "rayon",
1774+]
1775+
1776+[[package]]
1777+name = "nucleo-matcher"
1778+version = "0.2.0"
1779+source = "registry+https://github.com/rust-lang/crates.io-index"
1780+checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
1781+dependencies = [
1782+ "cov-mark",
1783+ "memchr",
1784+ "unicode-segmentation",
1785+]
1786+
17031787[[package]]
17041788name = "num-traits"
17051789version = "0.2.15"
@@ -1846,6 +1930,28 @@ dependencies = [
18461930 "getrandom",
18471931]
18481932
1933+[[package]]
1934+name = "rayon"
1935+version = "1.7.0"
1936+source = "registry+https://github.com/rust-lang/crates.io-index"
1937+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
1938+dependencies = [
1939+ "either",
1940+ "rayon-core",
1941+]
1942+
1943+[[package]]
1944+name = "rayon-core"
1945+version = "1.11.0"
1946+source = "registry+https://github.com/rust-lang/crates.io-index"
1947+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
1948+dependencies = [
1949+ "crossbeam-channel",
1950+ "crossbeam-deque",
1951+ "crossbeam-utils",
1952+ "num_cpus",
1953+]
1954+
18491955[[package]]
18501956name = "redox_syscall"
18511957version = "0.2.16"
Cargo.toml+2
@@ -5,6 +5,7 @@ members = [
55 "helix-term",
66 "helix-tui",
77 "helix-lsp",
8+ "helix-event",
89 "helix-dap",
910 "helix-loader",
1011 "helix-vcs",
@@ -35,3 +36,4 @@ package.helix-term.opt-level = 2
3536
3637[workspace.dependencies]
3738tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
39+nucleo = "0.2.0"
helix-core/Cargo.toml+3
@@ -48,6 +48,9 @@ chrono = { version = "0.4", default-features = false, features = ["alloc", "std"
4848etcetera = "0.8"
4949textwrap = "0.16.0"
5050
51+nucleo.workspace = true
52+parking_lot = "0.12"
53+
5154[dev-dependencies]
5255quickcheck = { version = "1", default-features = false }
5356indoc = "2.0.3"
helix-core/src/fuzzy.rsnew file+43
@@ -0,0 +1,43 @@
1+use std::ops::DerefMut;
2+
3+use nucleo::pattern::{AtomKind, CaseMatching, Pattern};
4+use nucleo::Config;
5+use parking_lot::Mutex;
6+
7+pub struct LazyMutex<T> {
8+ inner: Mutex<Option<T>>,
9+ init: fn() -> T,
10+}
11+
12+impl<T> LazyMutex<T> {
13+ pub const fn new(init: fn() -> T) -> Self {
14+ Self {
15+ inner: Mutex::new(None),
16+ init,
17+ }
18+ }
19+
20+ pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
21+ parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
22+ }
23+}
24+
25+pub static MATCHER: LazyMutex<nucleo::Matcher> = LazyMutex::new(nucleo::Matcher::default);
26+
27+/// convenience function to easily fuzzy match
28+/// on a (relatively small list of inputs). This is not recommended for building a full tui
29+/// application that can match large numbers of matches as all matching is done on the current
30+/// thread, effectively blocking the UI
31+pub fn fuzzy_match<T: AsRef<str>>(
32+ pattern: &str,
33+ items: impl IntoIterator<Item = T>,
34+ path: bool,
35+) -> Vec<(T, u32)> {
36+ let mut matcher = MATCHER.lock();
37+ matcher.config = Config::DEFAULT;
38+ if path {
39+ matcher.config.set_match_paths();
40+ }
41+ let pattern = Pattern::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy);
42+ pattern.match_list(items, &mut matcher)
43+}
helix-core/src/lib.rs+1
@@ -7,6 +7,7 @@ pub mod config;
77pub mod diagnostic;
88pub mod diff;
99pub mod doc_formatter;
10+pub mod fuzzy;
1011pub mod graphemes;
1112pub mod history;
1213pub mod increment;
helix-event/Cargo.tomlnew file+15
@@ -0,0 +1,15 @@
1+[package]
2+name = "helix-event"
3+version = "0.6.0"
4+authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
5+edition = "2021"
6+license = "MPL-2.0"
7+categories = ["editor"]
8+repository = "https://github.com/helix-editor/helix"
9+homepage = "https://helix-editor.com"
10+
11+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
12+
13+[dependencies]
14+tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
15+parking_lot = { version = "0.12", features = ["send_guard"] }
helix-event/src/lib.rsnew file+8
@@ -0,0 +1,8 @@
1+//! `helix-event` contains systems that allow (often async) communication between
2+//! different editor components without strongly coupling them. Currently this
3+//! crate only contains some smaller facilities but the intend is to add more
4+//! functionality in the future ( like a generic hook system)
5+
6+pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
7+
8+mod redraw;
helix-event/src/redraw.rsnew file+49
@@ -0,0 +1,49 @@
1+//! Signals that control when/if the editor redraws
2+
3+use std::future::Future;
4+
5+use parking_lot::{RwLock, RwLockReadGuard};
6+use tokio::sync::Notify;
7+
8+/// A `Notify` instance that can be used to (asynchronously) request
9+/// the editor the render a new frame.
10+static REDRAW_NOTIFY: Notify = Notify::const_new();
11+
12+/// A `RwLock` that prevents the next frame from being
13+/// drawn until an exclusive (write) lock can be acquired.
14+/// This allows asynchsonous tasks to acquire `non-exclusive`
15+/// locks (read) to prevent the next frame from being drawn
16+/// until a certain computation has finished.
17+static RENDER_LOCK: RwLock<()> = RwLock::new(());
18+
19+pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
20+
21+/// Requests that the editor is redrawn. The redraws are debounced (currently to
22+/// 30FPS) so this can be called many times without causing a ton of frames to
23+/// be rendered.
24+pub fn request_redraw() {
25+ REDRAW_NOTIFY.notify_one();
26+}
27+
28+/// Returns a future that will yield once a redraw has been asynchronously
29+/// requested using [`request_redraw`].
30+pub fn redraw_requested() -> impl Future<Output = ()> {
31+ REDRAW_NOTIFY.notified()
32+}
33+
34+/// Wait until all locks acquired with [`lock_frame`] have been released.
35+/// This function is called before rendering and is intended to allow the frame
36+/// to wait for async computations that should be included in the current frame.
37+pub fn start_frame() {
38+ drop(RENDER_LOCK.write());
39+ // exhaust any leftover redraw notifications
40+ let notify = REDRAW_NOTIFY.notified();
41+ tokio::pin!(notify);
42+ notify.enable();
43+}
44+
45+/// Acquires the render lock which will prevent the next frame from being drawn
46+/// until the returned guard is dropped.
47+pub fn lock_frame() -> RenderLockGuard {
48+ RENDER_LOCK.read()
49+}
helix-term/Cargo.toml+2−1
@@ -24,6 +24,7 @@ path = "src/main.rs"
2424
2525[dependencies]
2626helix-core = { version = "0.6", path = "../helix-core" }
27+helix-event = { version = "0.6", path = "../helix-event" }
2728helix-view = { version = "0.6", path = "../helix-view" }
2829helix-lsp = { version = "0.6", path = "../helix-lsp" }
2930helix-dap = { version = "0.6", path = "../helix-dap" }
@@ -49,7 +50,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
4950log = "0.4"
5051
5152# File picker
52−fuzzy-matcher = "0.3"
53+nucleo.workspace = true
5354ignore = "0.4"
5455# markdown doc rendering
5556pulldown-cmark = { version = "0.9", default-features = false }
helix-term/src/application.rs+2−10
@@ -257,16 +257,8 @@ impl Application {
257257 scroll: None,
258258 };
259259
260− // Acquire mutable access to the redraw_handle lock
261− // to ensure that there are no tasks running that want to block rendering
262− drop(cx.editor.redraw_handle.1.write().await);
260+ helix_event::start_frame();
263261 cx.editor.needs_redraw = false;
264− {
265− // exhaust any leftover redraw notifications
266− let notify = cx.editor.redraw_handle.0.notified();
267− tokio::pin!(notify);
268− notify.enable();
269− }
270262
271263 let area = self
272264 .terminal
@@ -590,7 +582,7 @@ impl Application {
590582 EditorEvent::LanguageServerMessage((id, call)) => {
591583 self.handle_language_server_message(call, id).await;
592584 // limit render calls for fast language server messages
593− self.editor.redraw_handle.0.notify_one();
585+ helix_event::request_redraw();
594586 }
595587 EditorEvent::DebuggerEvent(payload) => {
596588 let needs_render = self.editor.handle_debugger_message(payload).await;
helix-term/src/commands.rs+149−146
@@ -43,7 +43,6 @@ use helix_view::{
4343};
4444
4545use anyhow::{anyhow, bail, ensure, Context as _};
46−use fuzzy_matcher::FuzzyMatcher;
4746use insert::*;
4847use movement::Movement;
4948
@@ -60,7 +59,7 @@ use crate::{
6059};
6160
6261use crate::job::{self, Jobs};
63−use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
62+use futures_util::{stream::FuturesUnordered, TryStreamExt};
6463use std::{collections::HashMap, fmt, future::Future};
6564use std::{collections::HashSet, num::NonZeroUsize};
6665
@@ -75,7 +74,6 @@ use serde::de::{self, Deserialize, Deserializer};
7574use grep_regex::RegexMatcherBuilder;
7675use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
7776use ignore::{DirEntry, WalkBuilder, WalkState};
78−use tokio_stream::wrappers::UnboundedReceiverStream;
7977
8078pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
8179
@@ -1715,8 +1713,8 @@ fn select_regex(cx: &mut Context) {
17151713 "select:".into(),
17161714 Some(reg),
17171715 ui::completers::none,
1718− move |editor, regex, event| {
1719− let (view, doc) = current!(editor);
1716+ move |cx, regex, event| {
1717+ let (view, doc) = current!(cx.editor);
17201718 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
17211719 return;
17221720 }
@@ -1737,8 +1735,8 @@ fn split_selection(cx: &mut Context) {
17371735 "split:".into(),
17381736 Some(reg),
17391737 ui::completers::none,
1740− move |editor, regex, event| {
1741− let (view, doc) = current!(editor);
1738+ move |cx, regex, event| {
1739+ let (view, doc) = current!(cx.editor);
17421740 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
17431741 return;
17441742 }
@@ -1902,14 +1900,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
19021900 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
19031901 .collect()
19041902 },
1905− move |editor, regex, event| {
1903+ move |cx, regex, event| {
19061904 if event == PromptEvent::Validate {
1907− editor.registers.last_search_register = reg;
1905+ cx.editor.registers.last_search_register = reg;
19081906 } else if event != PromptEvent::Update {
19091907 return;
19101908 }
19111909 search_impl(
1912− editor,
1910+ cx.editor,
19131911 &contents,
19141912 &regex,
19151913 Movement::Move,
@@ -2078,13 +2076,11 @@ fn global_search(cx: &mut Context) {
20782076 }
20792077 }
20802078
2081− let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
20822079 let config = cx.editor.config();
20832080 let smart_case = config.search.smart_case;
20842081 let file_picker_config = config.file_picker.clone();
20852082
20862083 let reg = cx.register.unwrap_or('/');
2087−
20882084 let completions = search_completions(cx, Some(reg));
20892085 ui::regex_prompt(
20902086 cx,
@@ -2097,166 +2093,173 @@ fn global_search(cx: &mut Context) {
20972093 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
20982094 .collect()
20992095 },
2100− move |editor, regex, event| {
2096+ move |cx, regex, event| {
21012097 if event != PromptEvent::Validate {
21022098 return;
21032099 }
2104− editor.registers.last_search_register = reg;
2100+ cx.editor.registers.last_search_register = reg;
21052101
2106− let documents: Vec<_> = editor
2102+ let current_path = doc_mut!(cx.editor).path().cloned();
2103+ let documents: Vec<_> = cx
2104+ .editor
21072105 .documents()
2108− .map(|doc| (doc.path(), doc.text()))
2106+ .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
21092107 .collect();
21102108
21112109 if let Ok(matcher) = RegexMatcherBuilder::new()
21122110 .case_smart(smart_case)
21132111 .build(regex.as_str())
21142112 {
2115− let searcher = SearcherBuilder::new()
2116− .binary_detection(BinaryDetection::quit(b'\x00'))
2117− .build();
2118−
21192113 let search_root = helix_loader::current_working_dir();
21202114 if !search_root.exists() {
2121− editor.set_error("Current working directory does not exist");
2115+ cx.editor
2116+ .set_error("Current working directory does not exist");
21222117 return;
21232118 }
21242119
2120+ let (picker, injector) = Picker::stream(current_path);
2121+
21252122 let dedup_symlinks = file_picker_config.deduplicate_links;
21262123 let absolute_root = search_root
21272124 .canonicalize()
21282125 .unwrap_or_else(|_| search_root.clone());
2129−
2130− WalkBuilder::new(search_root)
2131− .hidden(file_picker_config.hidden)
2132− .parents(file_picker_config.parents)
2133− .ignore(file_picker_config.ignore)
2134− .follow_links(file_picker_config.follow_symlinks)
2135− .git_ignore(file_picker_config.git_ignore)
2136− .git_global(file_picker_config.git_global)
2137− .git_exclude(file_picker_config.git_exclude)
2138− .max_depth(file_picker_config.max_depth)
2139− .filter_entry(move |entry| {
2140− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2141− })
2142− .build_parallel()
2143− .run(|| {
2144− let mut searcher = searcher.clone();
2145− let matcher = matcher.clone();
2146− let all_matches_sx = all_matches_sx.clone();
2147− let documents = &documents;
2148− Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2149− let entry = match entry {
2150− Ok(entry) => entry,
2151− Err(_) => return WalkState::Continue,
2152− };
2153−
2154− match entry.file_type() {
2155− Some(entry) if entry.is_file() => {}
2156− // skip everything else
2157− _ => return WalkState::Continue,
2158− };
2159−
2160− let sink = sinks::UTF8(|line_num, _| {
2161− all_matches_sx
2162− .send(FileResult::new(entry.path(), line_num as usize - 1))
2163− .unwrap();
2164−
2165− Ok(true)
2166− });
2167− let doc = documents.iter().find(|&(doc_path, _)| {
2168− doc_path.map_or(false, |doc_path| doc_path == entry.path())
2169− });
2170−
2171− let result = if let Some((_, doc)) = doc {
2172− // there is already a buffer for this file
2173− // search the buffer instead of the file because it's faster
2174− // and captures new edits without requireing a save
2175− if searcher.multi_line_with_matcher(&matcher) {
2176− // in this case a continous buffer is required
2177− // convert the rope to a string
2178− let text = doc.to_string();
2179− searcher.search_slice(&matcher, text.as_bytes(), sink)
2126+ let injector_ = injector.clone();
2127+
2128+ std::thread::spawn(move || {
2129+ let searcher = SearcherBuilder::new()
2130+ .binary_detection(BinaryDetection::quit(b'\x00'))
2131+ .build();
2132+ WalkBuilder::new(search_root)
2133+ .hidden(file_picker_config.hidden)
2134+ .parents(file_picker_config.parents)
2135+ .ignore(file_picker_config.ignore)
2136+ .follow_links(file_picker_config.follow_symlinks)
2137+ .git_ignore(file_picker_config.git_ignore)
2138+ .git_global(file_picker_config.git_global)
2139+ .git_exclude(file_picker_config.git_exclude)
2140+ .max_depth(file_picker_config.max_depth)
2141+ .filter_entry(move |entry| {
2142+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2143+ })
2144+ .build_parallel()
2145+ .run(|| {
2146+ let mut searcher = searcher.clone();
2147+ let matcher = matcher.clone();
2148+ let injector = injector_.clone();
2149+ let documents = &documents;
2150+ Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2151+ let entry = match entry {
2152+ Ok(entry) => entry,
2153+ Err(_) => return WalkState::Continue,
2154+ };
2155+
2156+ match entry.file_type() {
2157+ Some(entry) if entry.is_file() => {}
2158+ // skip everything else
2159+ _ => return WalkState::Continue,
2160+ };
2161+
2162+ let mut stop = false;
2163+ let sink = sinks::UTF8(|line_num, _| {
2164+ stop = injector
2165+ .push(FileResult::new(entry.path(), line_num as usize - 1))
2166+ .is_err();
2167+
2168+ Ok(!stop)
2169+ });
2170+ let doc = documents.iter().find(|&(doc_path, _)| {
2171+ doc_path
2172+ .as_ref()
2173+ .map_or(false, |doc_path| doc_path == entry.path())
2174+ });
2175+
2176+ let result = if let Some((_, doc)) = doc {
2177+ // there is already a buffer for this file
2178+ // search the buffer instead of the file because it's faster
2179+ // and captures new edits without requiring a save
2180+ if searcher.multi_line_with_matcher(&matcher) {
2181+ // in this case a continous buffer is required
2182+ // convert the rope to a string
2183+ let text = doc.to_string();
2184+ searcher.search_slice(&matcher, text.as_bytes(), sink)
2185+ } else {
2186+ searcher.search_reader(
2187+ &matcher,
2188+ RopeReader::new(doc.slice(..)),
2189+ sink,
2190+ )
2191+ }
21802192 } else {
2181− searcher.search_reader(
2182− &matcher,
2183− RopeReader::new(doc.slice(..)),
2184− sink,
2185− )
2193+ searcher.search_path(&matcher, entry.path(), sink)
2194+ };
2195+
2196+ if let Err(err) = result {
2197+ log::error!(
2198+ "Global search error: {}, {}",
2199+ entry.path().display(),
2200+ err
2201+ );
21862202 }
2187− } else {
2188− searcher.search_path(&matcher, entry.path(), sink)
2189− };
2190−
2191− if let Err(err) = result {
2192− log::error!(
2193− "Global search error: {}, {}",
2194− entry.path().display(),
2195− err
2196− );
2197− }
2198− WalkState::Continue
2199− })
2200− });
2203+ if stop {
2204+ WalkState::Quit
2205+ } else {
2206+ WalkState::Continue
2207+ }
2208+ })
2209+ });
2210+ });
2211+
2212+ cx.jobs.callback(async move {
2213+ let call = move |_: &mut Editor, compositor: &mut Compositor| {
2214+ let picker = Picker::with_stream(
2215+ picker,
2216+ injector,
2217+ move |cx, FileResult { path, line_num }, action| {
2218+ let doc = match cx.editor.open(path, action) {
2219+ Ok(id) => doc_mut!(cx.editor, &id),
2220+ Err(e) => {
2221+ cx.editor.set_error(format!(
2222+ "Failed to open file '{}': {}",
2223+ path.display(),
2224+ e
2225+ ));
2226+ return;
2227+ }
2228+ };
2229+
2230+ let line_num = *line_num;
2231+ let view = view_mut!(cx.editor);
2232+ let text = doc.text();
2233+ if line_num >= text.len_lines() {
2234+ cx.editor.set_error(
2235+ "The line you jumped to does not exist anymore because the file has changed.",
2236+ );
2237+ return;
2238+ }
2239+ let start = text.line_to_char(line_num);
2240+ let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2241+
2242+ doc.set_selection(view.id, Selection::single(start, end));
2243+ if action.align_view(view, doc.id()) {
2244+ align_view(doc, view, Align::Center);
2245+ }
2246+ },
2247+ )
2248+ .with_preview(
2249+ |_editor, FileResult { path, line_num }| {
2250+ Some((path.clone().into(), Some((*line_num, *line_num))))
2251+ },
2252+ );
2253+ compositor.push(Box::new(overlaid(picker)))
2254+ };
2255+ Ok(Callback::EditorCompositor(Box::new(call)))
2256+ })
22012257 } else {
22022258 // Otherwise do nothing
22032259 // log::warn!("Global Search Invalid Pattern")
22042260 }
22052261 },
22062262 );
2207−
2208− let current_path = doc_mut!(cx.editor).path().cloned();
2209−
2210− let show_picker = async move {
2211− let all_matches: Vec<FileResult> =
2212− UnboundedReceiverStream::new(all_matches_rx).collect().await;
2213− let call: job::Callback = Callback::EditorCompositor(Box::new(
2214− move |editor: &mut Editor, compositor: &mut Compositor| {
2215− if all_matches.is_empty() {
2216− if !editor.is_err() {
2217− editor.set_status("No matches found");
2218− }
2219− return;
2220− }
2221−
2222− let picker = Picker::new(
2223− all_matches,
2224− current_path,
2225− move |cx, FileResult { path, line_num }, action| {
2226− let doc = match cx.editor.open(path, action) {
2227− Ok(id) => doc_mut!(cx.editor, &id),
2228− Err(e) => {
2229− cx.editor.set_error(format!(
2230− "Failed to open file '{}': {}",
2231− path.display(),
2232− e
2233− ));
2234− return;
2235− }
2236− };
2237− let line_num = *line_num;
2238− let view = view_mut!(cx.editor);
2239− let text = doc.text();
2240− if line_num >= text.len_lines() {
2241− cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed.");
2242− return;
2243− }
2244− let start = text.line_to_char(line_num);
2245− let end = text.line_to_char((line_num + 1).min(text.len_lines()));
2246−
2247− doc.set_selection(view.id, Selection::single(start, end));
2248− if action.align_view(view, doc.id()){
2249− align_view(doc, view, Align::Center);
2250− }
2251− }).with_preview(|_editor, FileResult { path, line_num }| {
2252− Some((path.clone().into(), Some((*line_num, *line_num))))
2253− });
2254− compositor.push(Box::new(overlaid(picker)));
2255− },
2256− ));
2257− Ok(call)
2258− };
2259− cx.jobs.callback(show_picker);
22602263}
22612264
22622265enum Extend {
@@ -4310,8 +4313,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
43104313 if remove { "remove:" } else { "keep:" }.into(),
43114314 Some(reg),
43124315 ui::completers::none,
4313− move |editor, regex, event| {
4314− let (view, doc) = current!(editor);
4316+ move |cx, regex, event| {
4317+ let (view, doc) = current!(cx.editor);
43154318 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
43164319 return;
43174320 }
helix-term/src/commands/typed.rs+13−25
@@ -5,6 +5,7 @@ use crate::job::Job;
55
66use super::*;
77
8+use helix_core::fuzzy::fuzzy_match;
89use helix_core::{encoding, shellwords::Shellwords};
910use helix_view::document::DEFAULT_LANGUAGE_NAME;
1011use helix_view::editor::{Action, CloseError, ConfigEvent};
@@ -1265,12 +1266,10 @@ fn reload(
12651266 }
12661267
12671268 let scrolloff = cx.editor.config().scrolloff;
1268− let redraw_handle = cx.editor.redraw_handle.clone();
12691269 let (view, doc) = current!(cx.editor);
1270− doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1271− .map(|_| {
1272− view.ensure_cursor_in_view(doc, scrolloff);
1273− })?;
1270+ doc.reload(view, &cx.editor.diff_providers).map(|_| {
1271+ view.ensure_cursor_in_view(doc, scrolloff);
1272+ })?;
12741273 if let Some(path) = doc.path() {
12751274 cx.editor
12761275 .language_servers
@@ -1316,8 +1315,7 @@ fn reload_all(
13161315 // Ensure that the view is synced with the document's history.
13171316 view.sync_changes(doc);
13181317
1319− let redraw_handle = cx.editor.redraw_handle.clone();
1320− doc.reload(view, &cx.editor.diff_providers, redraw_handle)?;
1318+ doc.reload(view, &cx.editor.diff_providers)?;
13211319 if let Some(path) = doc.path() {
13221320 cx.editor
13231321 .language_servers
@@ -2902,28 +2900,18 @@ pub(super) fn command_mode(cx: &mut Context) {
29022900 ":".into(),
29032901 Some(':'),
29042902 |editor: &Editor, input: &str| {
2905− static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2906− Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2907−
29082903 let shellwords = Shellwords::from(input);
29092904 let words = shellwords.words();
29102905
29112906 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
2912− // If the command has not been finished yet, complete commands.
2913− let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
2914− .iter()
2915− .filter_map(|command| {
2916− FUZZY_MATCHER
2917− .fuzzy_match(command.name, input)
2918− .map(|score| (command.name, score))
2919− })
2920− .collect();
2921−
2922− matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
2923− matches
2924− .into_iter()
2925− .map(|(name, _)| (0.., name.into()))
2926− .collect()
2907+ fuzzy_match(
2908+ input,
2909+ TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
2910+ false,
2911+ )
2912+ .into_iter()
2913+ .map(|(name, _)| (0.., name.into()))
2914+ .collect()
29272915 } else {
29282916 // Otherwise, use the command's completer and the last shellword
29292917 // as completion input.
helix-term/src/ui/fuzzy_match.rsdeleted−239
@@ -1,239 +0,0 @@
1−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2−use fuzzy_matcher::FuzzyMatcher;
3−
4−#[cfg(test)]
5−mod test;
6−
7−struct QueryAtom {
8− kind: QueryAtomKind,
9− atom: String,
10− ignore_case: bool,
11− inverse: bool,
12−}
13−impl QueryAtom {
14− fn new(atom: &str) -> Option<QueryAtom> {
15− let mut atom = atom.to_string();
16− let inverse = atom.starts_with('!');
17− if inverse {
18− atom.remove(0);
19− }
20−
21− let mut kind = match atom.chars().next() {
22− Some('^') => QueryAtomKind::Prefix,
23− Some('\'') => QueryAtomKind::Substring,
24− _ if inverse => QueryAtomKind::Substring,
25− _ => QueryAtomKind::Fuzzy,
26− };
27−
28− if atom.starts_with(['^', '\'']) {
29− atom.remove(0);
30− }
31−
32− if atom.is_empty() {
33− return None;
34− }
35−
36− if atom.ends_with('$') && !atom.ends_with("\\$") {
37− atom.pop();
38− kind = if kind == QueryAtomKind::Prefix {
39− QueryAtomKind::Exact
40− } else {
41− QueryAtomKind::Postfix
42− }
43− }
44−
45− Some(QueryAtom {
46− kind,
47− atom: atom.replace('\\', ""),
48− // not ideal but fuzzy_matches only knows ascii uppercase so more consistent
49− // to behave the same
50− ignore_case: kind != QueryAtomKind::Fuzzy
51− && atom.chars().all(|c| c.is_ascii_lowercase()),
52− inverse,
53− })
54− }
55−
56− fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
57− // for inverse there are no indices to return
58− // just return whether we matched
59− if self.inverse {
60− return self.matches(matcher, item);
61− }
62− let buf;
63− let item = if self.ignore_case {
64− buf = item.to_ascii_lowercase();
65− &buf
66− } else {
67− item
68− };
69− let off = match self.kind {
70− QueryAtomKind::Fuzzy => {
71− if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) {
72− indices.extend_from_slice(&fuzzy_indices);
73− return true;
74− } else {
75− return false;
76− }
77− }
78− QueryAtomKind::Substring => {
79− if let Some(off) = item.find(&self.atom) {
80− off
81− } else {
82− return false;
83− }
84− }
85− QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0,
86− QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(),
87− QueryAtomKind::Exact if item == self.atom => 0,
88− _ => return false,
89− };
90−
91− indices.extend(off..(off + self.atom.len()));
92− true
93− }
94−
95− fn matches(&self, matcher: &Matcher, item: &str) -> bool {
96− let buf;
97− let item = if self.ignore_case {
98− buf = item.to_ascii_lowercase();
99− &buf
100− } else {
101− item
102− };
103− let mut res = match self.kind {
104− QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(),
105− QueryAtomKind::Substring => item.contains(&self.atom),
106− QueryAtomKind::Prefix => item.starts_with(&self.atom),
107− QueryAtomKind::Postfix => item.ends_with(&self.atom),
108− QueryAtomKind::Exact => item == self.atom,
109− };
110− if self.inverse {
111− res = !res;
112− }
113− res
114− }
115−}
116−
117−#[derive(Debug, PartialEq, Eq, Clone, Copy)]
118−enum QueryAtomKind {
119− /// Item is a fuzzy match of this behaviour
120− ///
121− /// Usage: `foo`
122− Fuzzy,
123− /// Item contains query atom as a continuous substring
124− ///
125− /// Usage `'foo`
126− Substring,
127− /// Item starts with query atom
128− ///
129− /// Usage: `^foo`
130− Prefix,
131− /// Item ends with query atom
132− ///
133− /// Usage: `foo$`
134− Postfix,
135− /// Item is equal to query atom
136− ///
137− /// Usage `^foo$`
138− Exact,
139−}
140−
141−#[derive(Default)]
142−pub struct FuzzyQuery {
143− first_fuzzy_atom: Option<String>,
144− query_atoms: Vec<QueryAtom>,
145−}
146−
147−fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
148− let mut saw_backslash = false;
149− query.split(move |c| {
150− saw_backslash = match c {
151− ' ' if !saw_backslash => return true,
152− '\\' => true,
153− _ => false,
154− };
155− false
156− })
157−}
158−
159−impl FuzzyQuery {
160− pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) {
161− // TODO: we could be a lot smarter about this
162− let new_query = Self::new(query);
163− let mut is_refinement = query.starts_with(old_query);
164−
165− // if the last atom is an inverse atom adding more text to it
166− // will actually increase the number of matches and we can not refine
167− // the matches.
168− if is_refinement && !self.query_atoms.is_empty() {
169− let last_idx = self.query_atoms.len() - 1;
170− if self.query_atoms[last_idx].inverse
171− && self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom
172− {
173− is_refinement = false;
174− }
175− }
176−
177− (new_query, is_refinement)
178− }
179−
180− pub fn new(query: &str) -> FuzzyQuery {
181− let mut first_fuzzy_query = None;
182− let query_atoms = query_atoms(query)
183− .filter_map(|atom| {
184− let atom = QueryAtom::new(atom)?;
185− if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() {
186− first_fuzzy_query = Some(atom.atom);
187− None
188− } else {
189− Some(atom)
190− }
191− })
192− .collect();
193− FuzzyQuery {
194− first_fuzzy_atom: first_fuzzy_query,
195− query_atoms,
196− }
197− }
198−
199− pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
200− // use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible
201− // this behaviour matches fzf and skim
202− let score = self
203− .first_fuzzy_atom
204− .as_ref()
205− .map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?;
206− if self
207− .query_atoms
208− .iter()
209− .any(|atom| !atom.matches(matcher, item))
210− {
211− return None;
212− }
213− Some(score)
214− }
215−
216− pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
217− let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
218− || Some((0, Vec::new())),
219− |atom| matcher.fuzzy_indices(item, atom),
220− )?;
221−
222− // fast path for the common case of just a single atom
223− if self.query_atoms.is_empty() {
224− return Some((score, indices));
225− }
226−
227− for atom in &self.query_atoms {
228− if !atom.indices(matcher, item, &mut indices) {
229− return None;
230− }
231− }
232−
233− // deadup and remove duplicate matches
234− indices.sort_unstable();
235− indices.dedup();
236−
237− Some((score, indices))
238− }
239−}
helix-term/src/ui/fuzzy_match/test.rsdeleted−47
@@ -1,47 +0,0 @@
1−use crate::ui::fuzzy_match::FuzzyQuery;
2−use crate::ui::fuzzy_match::Matcher;
3−
4−fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
5− let query = FuzzyQuery::new(query);
6− let matcher = Matcher::default();
7− items
8− .iter()
9− .filter_map(|item| {
10− let (_, indices) = query.fuzzy_indices(item, &matcher)?;
11− let matched_string = indices
12− .iter()
13− .map(|&pos| item.chars().nth(pos).unwrap())
14− .collect();
15− Some(matched_string)
16− })
17− .collect()
18−}
19−
20−#[test]
21−fn match_single_value() {
22− let matches = run_test("foo", &["foobar", "foo", "bar"]);
23− assert_eq!(matches, &["foo", "foo"])
24−}
25−
26−#[test]
27−fn match_multiple_values() {
28− let matches = run_test(
29− "foo bar",
30− &["foo bar", "foo bar", "bar foo", "bar", "foo"],
31− );
32− assert_eq!(matches, &["foobar", "foobar", "barfoo"])
33−}
34−
35−#[test]
36−fn space_escape() {
37− let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
38− assert_eq!(matches, &["foo bar"])
39−}
40−
41−#[test]
42−fn trim() {
43− let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
44− assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
45− let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
46− assert_eq!(matches, &["bar foo"])
47−}
helix-term/src/ui/menu.rs+24−27
@@ -1,22 +1,22 @@
1−use std::{borrow::Cow, path::PathBuf};
1+use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
22
33use crate::{
44 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
55 ctrl, key, shift,
66};
7+use helix_core::fuzzy::MATCHER;
8+use nucleo::pattern::{AtomKind, CaseMatching, Pattern};
9+use nucleo::{Config, Utf32Str};
710use tui::{buffer::Buffer as Surface, widgets::Table};
811
912pub use tui::widgets::{Cell, Row};
1013
11−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12−use fuzzy_matcher::FuzzyMatcher;
13−
1414use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
1515use tui::layout::Constraint;
1616
17−pub trait Item {
17+pub trait Item: Sync + Send + 'static {
1818 /// Additional editor state that is used for label calculation.
19− type Data;
19+ type Data: Sync + Send + 'static;
2020
2121 fn format(&self, data: &Self::Data) -> Row;
2222
@@ -51,9 +51,8 @@ pub struct Menu<T: Item> {
5151
5252 cursor: Option<usize>,
5353
54− matcher: Box<Matcher>,
5554 /// (index, score)
56− matches: Vec<(usize, i64)>,
55+ matches: Vec<(u32, u32)>,
5756
5857 widths: Vec<Constraint>,
5958
@@ -75,11 +74,10 @@ impl<T: Item> Menu<T> {
7574 editor_data: <T as Item>::Data,
7675 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
7776 ) -> Self {
78− let matches = (0..options.len()).map(|i| (i, 0)).collect();
77+ let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
7978 Self {
8079 options,
8180 editor_data,
82− matcher: Box::new(Matcher::default().ignore_case()),
8381 matches,
8482 cursor: None,
8583 widths: Vec::new(),
@@ -94,20 +92,19 @@ impl<T: Item> Menu<T> {
9492 pub fn score(&mut self, pattern: &str) {
9593 // reuse the matches allocation
9694 self.matches.clear();
97− self.matches.extend(
98− self.options
99− .iter()
100− .enumerate()
101− .filter_map(|(index, option)| {
102− let text = option.filter_text(&self.editor_data);
103− // TODO: using fuzzy_indices could give us the char idx for match highlighting
104− self.matcher
105− .fuzzy_match(&text, pattern)
106− .map(|score| (index, score))
107− }),
108− );
109− // Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority
110− self.matches.sort_by_key(|(_, score)| -score);
95+ let mut matcher = MATCHER.lock();
96+ matcher.config = Config::DEFAULT;
97+ let pattern = Pattern::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy);
98+ let mut buf = Vec::new();
99+ let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
100+ let text = option.filter_text(&self.editor_data);
101+ pattern
102+ .score(Utf32Str::new(&text, &mut buf), &mut matcher)
103+ .map(|score| (i as u32, score))
104+ });
105+ self.matches.extend(matches);
106+ self.matches
107+ .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
111108
112109 // reset cursor position
113110 self.cursor = None;
@@ -201,7 +198,7 @@ impl<T: Item> Menu<T> {
201198 self.cursor.and_then(|cursor| {
202199 self.matches
203200 .get(cursor)
204− .map(|(index, _score)| &self.options[*index])
201+ .map(|(index, _score)| &self.options[*index as usize])
205202 })
206203 }
207204
@@ -209,7 +206,7 @@ impl<T: Item> Menu<T> {
209206 self.cursor.and_then(|cursor| {
210207 self.matches
211208 .get(cursor)
212− .map(|(index, _score)| &mut self.options[*index])
209+ .map(|(index, _score)| &mut self.options[*index as usize])
213210 })
214211 }
215212
@@ -332,7 +329,7 @@ impl<T: Item + 'static> Component for Menu<T> {
332329 .iter()
333330 .map(|(index, _score)| {
334331 // (index, self.options.get(*index).unwrap()) // get_unchecked
335− &self.options[*index] // get_unchecked
332+ &self.options[*index as usize] // get_unchecked
336333 })
337334 .collect();
338335
helix-term/src/ui/mod.rs+44−140
@@ -1,7 +1,6 @@
11mod completion;
22mod document;
33pub(crate) mod editor;
4−mod fuzzy_match;
54mod info;
65pub mod lsp;
76mod markdown;
@@ -64,7 +63,7 @@ pub fn regex_prompt(
6463 prompt: std::borrow::Cow<'static, str>,
6564 history_register: Option<char>,
6665 completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
67− fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
66+ fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
6867) {
6968 let (view, doc) = current!(cx.editor);
7069 let doc_id = view.doc;
@@ -111,7 +110,7 @@ pub fn regex_prompt(
111110 view.jumps.push((doc_id, snapshot.clone()));
112111 }
113112
114− fun(cx.editor, regex, event);
113+ fun(cx, regex, event);
115114
116115 let (view, doc) = current!(cx.editor);
117116 view.ensure_cursor_in_view(doc, config.scrolloff);
@@ -174,6 +173,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
174173 .git_ignore(config.file_picker.git_ignore)
175174 .git_global(config.file_picker.git_global)
176175 .git_exclude(config.file_picker.git_exclude)
176+ .sort_by_file_name(|name1, name2| name1.cmp(name2))
177177 .max_depth(config.file_picker.max_depth)
178178 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
179179
@@ -190,32 +190,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
190190 .build()
191191 .expect("failed to build excluded_types");
192192 walk_builder.types(excluded_types);
193−
194− // We want files along with their modification date for sorting
195193 let files = walk_builder.build().filter_map(|entry| {
196194 let entry = entry.ok()?;
197− // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
198− if entry.file_type()?.is_file() {
199− Some(entry.into_path())
200− } else {
201− None
195+ if !entry.file_type()?.is_file() {
196+ return None;
202197 }
198+ Some(entry.into_path())
203199 });
204−
205− // Cap the number of files if we aren't in a git project, preventing
206− // hangs when using the picker in your home directory
207− let mut files: Vec<PathBuf> = if root.join(".git").exists() {
208− files.collect()
209− } else {
210− // const MAX: usize = 8192;
211− const MAX: usize = 100_000;
212− files.take(MAX).collect()
213− };
214− files.sort();
215−
216200 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
217201
218− Picker::new(files, root, move |cx, path: &PathBuf, action| {
202+ let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
219203 if let Err(e) = cx.editor.open(path, action) {
220204 let err = if let Some(err) = e.source() {
221205 format!("{}", err)
@@ -225,20 +209,27 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
225209 cx.editor.set_error(err);
226210 }
227211 })
228− .with_preview(|_editor, path| Some((path.clone().into(), None)))
212+ .with_preview(|_editor, path| Some((path.clone().into(), None)));
213+ let injector = picker.injector();
214+ std::thread::spawn(move || {
215+ for file in files {
216+ if injector.push(file).is_err() {
217+ break;
218+ }
219+ }
220+ });
221+ picker
229222}
230223
231224pub mod completers {
232225 use crate::ui::prompt::Completion;
233− use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
234− use fuzzy_matcher::FuzzyMatcher;
226+ use helix_core::fuzzy::fuzzy_match;
235227 use helix_core::syntax::LanguageServerFeature;
236228 use helix_view::document::SCRATCH_BUFFER_NAME;
237229 use helix_view::theme;
238230 use helix_view::{editor::Config, Editor};
239231 use once_cell::sync::Lazy;
240232 use std::borrow::Cow;
241− use std::cmp::Reverse;
242233
243234 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
244235
@@ -247,31 +238,16 @@ pub mod completers {
247238 }
248239
249240 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
250− let mut names: Vec<_> = editor
251− .documents
252− .values()
253− .map(|doc| {
254− let name = doc
255− .relative_path()
256− .map(|p| p.display().to_string())
257− .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
258− ((0..), Cow::from(name))
259− })
260− .collect();
261−
262− let matcher = Matcher::default();
263−
264− let mut matches: Vec<_> = names
265− .into_iter()
266− .filter_map(|(_range, name)| {
267− matcher.fuzzy_match(&name, input).map(|score| (name, score))
268− })
269− .collect();
270−
271− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
272− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
241+ let names = editor.documents.values().map(|doc| {
242+ doc.relative_path()
243+ .map(|p| p.display().to_string().into())
244+ .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
245+ });
273246
274− names
247+ fuzzy_match(input, names, true)
248+ .into_iter()
249+ .map(|(name, _)| ((0..), name))
250+ .collect()
275251 }
276252
277253 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@@ -284,26 +260,10 @@ pub mod completers {
284260 names.sort();
285261 names.dedup();
286262
287− let mut names: Vec<_> = names
263+ fuzzy_match(input, names, false)
288264 .into_iter()
289− .map(|name| ((0..), Cow::from(name)))
290− .collect();
291−
292− let matcher = Matcher::default();
293−
294− let mut matches: Vec<_> = names
295− .into_iter()
296− .filter_map(|(_range, name)| {
297− matcher.fuzzy_match(&name, input).map(|score| (name, score))
298− })
299− .collect();
300−
301− matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
302− (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
303− });
304− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
305−
306− names
265+ .map(|(name, _)| ((0..), name.into()))
266+ .collect()
307267 }
308268
309269 /// Recursive function to get all keys from this value and add them to vec
@@ -330,15 +290,7 @@ pub mod completers {
330290 keys
331291 });
332292
333− let matcher = Matcher::default();
334−
335− let mut matches: Vec<_> = KEYS
336− .iter()
337− .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
338− .collect();
339−
340− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
341− matches
293+ fuzzy_match(input, &*KEYS, false)
342294 .into_iter()
343295 .map(|(name, _)| ((0..), name.into()))
344296 .collect()
@@ -365,8 +317,6 @@ pub mod completers {
365317 }
366318
367319 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
368− let matcher = Matcher::default();
369−
370320 let text: String = "text".into();
371321
372322 let language_ids = editor
@@ -375,27 +325,13 @@ pub mod completers {
375325 .map(|config| &config.language_id)
376326 .chain(std::iter::once(&text));
377327
378− let mut matches: Vec<_> = language_ids
379− .filter_map(|language_id| {
380− matcher
381− .fuzzy_match(language_id, input)
382− .map(|score| (language_id, score))
383− })
384− .collect();
385−
386− matches.sort_unstable_by(|(language1, score1), (language2, score2)| {
387− (Reverse(*score1), language1).cmp(&(Reverse(*score2), language2))
388− });
389−
390− matches
328+ fuzzy_match(input, language_ids, false)
391329 .into_iter()
392− .map(|(language, _score)| ((0..), language.clone().into()))
330+ .map(|(name, _)| ((0..), name.to_owned().into()))
393331 .collect()
394332 }
395333
396334 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
397− let matcher = Matcher::default();
398−
399335 let Some(options) = doc!(editor)
400336 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
401337 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@@ -403,23 +339,9 @@ pub mod completers {
403339 return vec![];
404340 };
405341
406− let mut matches: Vec<_> = options
407− .commands
408− .iter()
409− .filter_map(|command| {
410− matcher
411− .fuzzy_match(command, input)
412− .map(|score| (command, score))
413− })
414− .collect();
415−
416− matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
417− (Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
418− });
419−
420− matches
342+ fuzzy_match(input, &options.commands, false)
421343 .into_iter()
422− .map(|(command, _score)| ((0..), command.clone().into()))
344+ .map(|(name, _)| ((0..), name.to_owned().into()))
423345 .collect()
424346 }
425347
@@ -500,7 +422,7 @@ pub mod completers {
500422
501423 let end = input.len()..;
502424
503− let mut files: Vec<_> = WalkBuilder::new(&dir)
425+ let files = WalkBuilder::new(&dir)
504426 .hidden(false)
505427 .follow_links(false) // We're scanning over depth 1
506428 .git_ignore(git_ignore)
@@ -532,43 +454,25 @@ pub mod completers {
532454 path.push("");
533455 }
534456
535− let path = path.to_str()?.to_owned();
536− Some((end.clone(), Cow::from(path)))
457+ let path = path.into_os_string().into_string().ok()?;
458+ Some(Cow::from(path))
537459 })
538460 }) // TODO: unwrap or skip
539− .filter(|(_, path)| !path.is_empty()) // TODO
540− .collect();
461+ .filter(|path| !path.is_empty());
541462
542463 // if empty, return a list of dirs and files in current dir
543464 if let Some(file_name) = file_name {
544− let matcher = Matcher::default();
545−
546− // inefficient, but we need to calculate the scores, filter out None, then sort.
547− let mut matches: Vec<_> = files
548− .into_iter()
549− .filter_map(|(_range, file)| {
550− matcher
551− .fuzzy_match(&file, &file_name)
552− .map(|score| (file, score))
553− })
554− .collect();
555−
556465 let range = (input.len().saturating_sub(file_name.len()))..;
557−
558− matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
559− (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
560− });
561−
562− files = matches
466+ fuzzy_match(&file_name, files, true)
563467 .into_iter()
564− .map(|(file, _)| (range.clone(), file))
565− .collect();
468+ .map(|(name, _)| (range.clone(), name))
469+ .collect()
566470
567471 // TODO: complete to longest common match
568472 } else {
473+ let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
569474 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
475+ files
570476 }
571−
572− files
573477 }
574478}
helix-term/src/ui/picker.rs+267−278
@@ -7,11 +7,12 @@ use crate::{
77 ui::{
88 self,
99 document::{render_document, LineDecoration, LinePos, TextRenderer},
10− fuzzy_match::FuzzyQuery,
1110 EditorView,
1211 },
1312};
1413use futures_util::{future::BoxFuture, FutureExt};
14+use nucleo::pattern::CaseMatching;
15+use nucleo::{Config, Nucleo, Utf32String};
1516use tui::{
1617 buffer::Buffer as Surface,
1718 layout::Constraint,
@@ -19,16 +20,23 @@ use tui::{
1920 widgets::{Block, BorderType, Borders, Cell, Table},
2021};
2122
22−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
2323use tui::widgets::Widget;
2424
25−use std::cmp::{self, Ordering};
26−use std::{collections::HashMap, io::Read, path::PathBuf};
25+use std::{
26+ collections::HashMap,
27+ io::Read,
28+ path::PathBuf,
29+ sync::{
30+ atomic::{self, AtomicBool},
31+ Arc,
32+ },
33+};
2734
2835use crate::ui::{Prompt, PromptEvent};
2936use helix_core::{
30− char_idx_at_visual_offset, movement::Direction, text_annotations::TextAnnotations,
31− unicode::segmentation::UnicodeSegmentation, Position, Syntax,
37+ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
38+ text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
39+ Syntax,
3240};
3341use helix_view::{
3442 editor::Action,
@@ -114,20 +122,71 @@ impl Preview<'_, '_> {
114122 }
115123}
116124
125+fn item_to_nucleo<T: Item>(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> {
126+ let row = item.format(editor_data);
127+ let mut cells = row.cells.iter();
128+ let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum());
129+ let cell = cells.next()?;
130+ if let Some(cell) = cell.content.lines.first() {
131+ for span in &cell.0 {
132+ text.push_str(&span.content);
133+ }
134+ }
135+
136+ for cell in cells {
137+ text.push(' ');
138+ if let Some(cell) = cell.content.lines.first() {
139+ for span in &cell.0 {
140+ text.push_str(&span.content);
141+ }
142+ }
143+ }
144+ Some((item, text.into()))
145+}
146+
147+pub struct Injector<T: Item> {
148+ dst: nucleo::Injector<T>,
149+ editor_data: Arc<T::Data>,
150+ shutown: Arc<AtomicBool>,
151+}
152+
153+impl<T: Item> Clone for Injector<T> {
154+ fn clone(&self) -> Self {
155+ Injector {
156+ dst: self.dst.clone(),
157+ editor_data: self.editor_data.clone(),
158+ shutown: Arc::new(AtomicBool::new(false)),
159+ }
160+ }
161+}
162+
163+pub struct InjectorShutdown;
164+
165+impl<T: Item> Injector<T> {
166+ pub fn push(&self, item: T) -> Result<(), InjectorShutdown> {
167+ if self.shutown.load(atomic::Ordering::Relaxed) {
168+ return Err(InjectorShutdown);
169+ }
170+
171+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
172+ self.dst.push(item, |dst| dst[0] = matcher_text);
173+ }
174+ Ok(())
175+ }
176+}
177+
117178pub struct Picker<T: Item> {
118− options: Vec<T>,
119− editor_data: T::Data,
120− // filter: String,
121− matcher: Box<Matcher>,
122− matches: Vec<PickerMatch>,
179+ editor_data: Arc<T::Data>,
180+ shutdown: Arc<AtomicBool>,
181+ matcher: Nucleo<T>,
123182
124183 /// Current height of the completions box
125184 completion_height: u16,
126185
127− cursor: usize,
128− // pattern: String,
186+ cursor: u32,
129187 prompt: Prompt,
130− previous_pattern: (String, FuzzyQuery),
188+ previous_pattern: String,
189+
131190 /// Whether to show the preview panel (default true)
132191 show_preview: bool,
133192 /// Constraints for tabular formatting
@@ -144,10 +203,59 @@ pub struct Picker<T: Item> {
144203}
145204
146205impl<T: Item + 'static> Picker<T> {
206+ pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
207+ let matcher = Nucleo::new(
208+ Config::DEFAULT,
209+ Arc::new(helix_event::request_redraw),
210+ None,
211+ 1,
212+ );
213+ let streamer = Injector {
214+ dst: matcher.injector(),
215+ editor_data: Arc::new(editor_data),
216+ shutown: Arc::new(AtomicBool::new(false)),
217+ };
218+ (matcher, streamer)
219+ }
220+
147221 pub fn new(
148222 options: Vec<T>,
149223 editor_data: T::Data,
150224 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
225+ ) -> Self {
226+ let matcher = Nucleo::new(
227+ Config::DEFAULT,
228+ Arc::new(helix_event::request_redraw),
229+ None,
230+ 1,
231+ );
232+ let injector = matcher.injector();
233+ for item in options {
234+ if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) {
235+ injector.push(item, |dst| dst[0] = matcher_text);
236+ }
237+ }
238+ Self::with(
239+ matcher,
240+ Arc::new(editor_data),
241+ Arc::new(AtomicBool::new(false)),
242+ callback_fn,
243+ )
244+ }
245+
246+ pub fn with_stream(
247+ matcher: Nucleo<T>,
248+ injector: Injector<T>,
249+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
250+ ) -> Self {
251+ Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
252+ }
253+
254+ fn with(
255+ matcher: Nucleo<T>,
256+ editor_data: Arc<T::Data>,
257+ shutdown: Arc<AtomicBool>,
258+ callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
151259 ) -> Self {
152260 let prompt = Prompt::new(
153261 "".into(),
@@ -156,14 +264,13 @@ impl<T: Item + 'static> Picker<T> {
156264 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
157265 );
158266
159− let mut picker = Self {
160− options,
267+ Self {
268+ matcher,
161269 editor_data,
162− matcher: Box::default(),
163− matches: Vec::new(),
270+ shutdown,
164271 cursor: 0,
165272 prompt,
166− previous_pattern: (String::new(), FuzzyQuery::default()),
273+ previous_pattern: String::new(),
167274 truncate_start: true,
168275 show_preview: true,
169276 callback_fn: Box::new(callback_fn),
@@ -172,24 +279,15 @@ impl<T: Item + 'static> Picker<T> {
172279 preview_cache: HashMap::new(),
173280 read_buffer: Vec::with_capacity(1024),
174281 file_fn: None,
175− };
176−
177− picker.calculate_column_widths();
178−
179− // scoring on empty input
180− // TODO: just reuse score()
181− picker
182− .matches
183− .extend(picker.options.iter().enumerate().map(|(index, option)| {
184− let text = option.filter_text(&picker.editor_data);
185− PickerMatch {
186− index,
187− score: 0,
188− len: text.chars().count(),
189− }
190− }));
282+ }
283+ }
191284
192− picker
285+ pub fn injector(&self) -> Injector<T> {
286+ Injector {
287+ dst: self.matcher.injector(),
288+ editor_data: self.editor_data.clone(),
289+ shutown: self.shutdown.clone(),
290+ }
193291 }
194292
195293 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -202,122 +300,25 @@ impl<T: Item + 'static> Picker<T> {
202300 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
203301 ) -> Self {
204302 self.file_fn = Some(Box::new(preview_fn));
303+ // assumption: if we have a preview we are matching paths... If this is ever
304+ // not true this could be a separate builder function
305+ self.matcher.update_config(Config::DEFAULT.match_paths());
205306 self
206307 }
207308
208309 pub fn set_options(&mut self, new_options: Vec<T>) {
209− self.options = new_options;
210− self.cursor = 0;
211− self.force_score();
212− self.calculate_column_widths();
213− }
214−
215− /// Calculate the width constraints using the maximum widths of each column
216− /// for the current options.
217− fn calculate_column_widths(&mut self) {
218− let n = self
219− .options
220− .first()
221− .map(|option| option.format(&self.editor_data).cells.len())
222− .unwrap_or_default();
223− let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
224− let row = option.format(&self.editor_data);
225− // maintain max for each column
226− for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
227− let width = cell.content.width();
228− if width > *acc {
229− *acc = width;
230− }
310+ self.matcher.restart(false);
311+ let injector = self.matcher.injector();
312+ for item in new_options {
313+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
314+ injector.push(item, |dst| dst[0] = matcher_text);
231315 }
232− acc
233− });
234− self.widths = max_lens
235− .into_iter()
236− .map(|len| Constraint::Length(len as u16))
237− .collect();
238− }
239−
240− pub fn score(&mut self) {
241− let pattern = self.prompt.line();
242−
243− if pattern == &self.previous_pattern.0 {
244− return;
245316 }
246−
247− let (query, is_refined) = self
248− .previous_pattern
249− .1
250− .refine(pattern, &self.previous_pattern.0);
251−
252− if pattern.is_empty() {
253− // Fast path for no pattern.
254− self.matches.clear();
255− self.matches
256− .extend(self.options.iter().enumerate().map(|(index, option)| {
257− let text = option.filter_text(&self.editor_data);
258− PickerMatch {
259− index,
260− score: 0,
261− len: text.chars().count(),
262− }
263− }));
264− } else if is_refined {
265− // optimization: if the pattern is a more specific version of the previous one
266− // then we can score the filtered set.
267− self.matches.retain_mut(|pmatch| {
268− let option = &self.options[pmatch.index];
269− let text = option.sort_text(&self.editor_data);
270−
271− match query.fuzzy_match(&text, &self.matcher) {
272− Some(s) => {
273− // Update the score
274− pmatch.score = s;
275− true
276− }
277− None => false,
278− }
279− });
280−
281− self.matches.sort_unstable();
282− } else {
283− self.force_score();
284− }
285−
286− // reset cursor position
287− self.cursor = 0;
288− let pattern = self.prompt.line();
289− self.previous_pattern.0.clone_from(pattern);
290− self.previous_pattern.1 = query;
291− }
292−
293− pub fn force_score(&mut self) {
294− let pattern = self.prompt.line();
295−
296− let query = FuzzyQuery::new(pattern);
297− self.matches.clear();
298− self.matches.extend(
299− self.options
300− .iter()
301− .enumerate()
302− .filter_map(|(index, option)| {
303− let text = option.filter_text(&self.editor_data);
304−
305− query
306− .fuzzy_match(&text, &self.matcher)
307− .map(|score| PickerMatch {
308− index,
309− score,
310− len: text.chars().count(),
311− })
312− }),
313− );
314−
315− self.matches.sort_unstable();
316317 }
317318
318319 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
319− pub fn move_by(&mut self, amount: usize, direction: Direction) {
320− let len = self.matches.len();
320+ pub fn move_by(&mut self, amount: u32, direction: Direction) {
321+ let len = self.matcher.snapshot().matched_item_count();
321322
322323 if len == 0 {
323324 // No results, can't move.
@@ -336,12 +337,12 @@ impl<T: Item + 'static> Picker<T> {
336337
337338 /// Move the cursor down by exactly one page. After the last page comes the first page.
338339 pub fn page_up(&mut self) {
339− self.move_by(self.completion_height as usize, Direction::Backward);
340+ self.move_by(self.completion_height as u32, Direction::Backward);
340341 }
341342
342343 /// Move the cursor up by exactly one page. After the first page comes the last page.
343344 pub fn page_down(&mut self) {
344− self.move_by(self.completion_height as usize, Direction::Forward);
345+ self.move_by(self.completion_height as u32, Direction::Forward);
345346 }
346347
347348 /// Move the cursor to the first entry
@@ -351,13 +352,18 @@ impl<T: Item + 'static> Picker<T> {
351352
352353 /// Move the cursor to the last entry
353354 pub fn to_end(&mut self) {
354− self.cursor = self.matches.len().saturating_sub(1);
355+ self.cursor = self
356+ .matcher
357+ .snapshot()
358+ .matched_item_count()
359+ .saturating_sub(1);
355360 }
356361
357362 pub fn selection(&self) -> Option<&T> {
358− self.matches
359− .get(self.cursor)
360− .map(|pmatch| &self.options[pmatch.index])
363+ self.matcher
364+ .snapshot()
365+ .get_matched_item(self.cursor)
366+ .map(|item| item.data)
361367 }
362368
363369 pub fn toggle_preview(&mut self) {
@@ -366,8 +372,17 @@ impl<T: Item + 'static> Picker<T> {
366372
367373 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
368374 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
369− // TODO: recalculate only if pattern changed
370− self.score();
375+ let pattern = self.prompt.line();
376+ // TODO: better track how the pattern has changed
377+ if pattern != &self.previous_pattern {
378+ self.matcher.pattern.reparse(
379+ 0,
380+ pattern,
381+ CaseMatching::Smart,
382+ pattern.starts_with(&self.previous_pattern),
383+ );
384+ self.previous_pattern = pattern.clone();
385+ }
371386 }
372387 EventResult::Consumed(None)
373388 }
@@ -411,12 +426,9 @@ impl<T: Item + 'static> Picker<T> {
411426 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
412427 CachedPreview::LargeFile
413428 }
414− _ => {
415− // TODO: enable syntax highlighting; blocked by async rendering
416− Document::open(path, None, None, editor.config.clone())
417− .map(|doc| CachedPreview::Document(Box::new(doc)))
418− .unwrap_or(CachedPreview::NotFound)
419− }
429+ _ => Document::open(path, None, None, editor.config.clone())
430+ .map(|doc| CachedPreview::Document(Box::new(doc)))
431+ .unwrap_or(CachedPreview::NotFound),
420432 },
421433 )
422434 .unwrap_or(CachedPreview::NotFound);
@@ -495,6 +507,14 @@ impl<T: Item + 'static> Picker<T> {
495507 }
496508
497509 fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
510+ let status = self.matcher.tick(10);
511+ let snapshot = self.matcher.snapshot();
512+ if status.changed {
513+ self.cursor = self
514+ .cursor
515+ .min(snapshot.matched_item_count().saturating_sub(1))
516+ }
517+
498518 let text_style = cx.editor.theme.get("ui.text");
499519 let selected = cx.editor.theme.get("ui.text.focus");
500520 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -515,8 +535,15 @@ impl<T: Item + 'static> Picker<T> {
515535 // -- Render the input bar:
516536
517537 let area = inner.clip_left(1).with_height(1);
538+ // render the prompt first since it will clear its background
539+ self.prompt.render(area, surface, cx);
518540
519− let count = format!("{}/{}", self.matches.len(), self.options.len());
541+ let count = format!(
542+ "{}{}/{}",
543+ if status.running { "(running) " } else { "" },
544+ snapshot.matched_item_count(),
545+ snapshot.item_count(),
546+ );
520547 surface.set_stringn(
521548 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
522549 area.y,
@@ -525,8 +552,6 @@ impl<T: Item + 'static> Picker<T> {
525552 text_style,
526553 );
527554
528− self.prompt.render(area, surface, cx);
529−
530555 // -- Separator
531556 let sep_style = cx.editor.theme.get("ui.background.separator");
532557 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -539,106 +564,89 @@ impl<T: Item + 'static> Picker<T> {
539564 // -- Render the contents:
540565 // subtract area of prompt from top
541566 let inner = inner.clip_top(2);
542−
543− let rows = inner.height;
544− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
567+ let rows = inner.height as u32;
568+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
545569 let cursor = self.cursor.saturating_sub(offset);
570+ let end = offset
571+ .saturating_add(rows)
572+ .min(snapshot.matched_item_count());
573+ let mut indices = Vec::new();
574+ let mut matcher = MATCHER.lock();
575+ matcher.config = Config::DEFAULT;
576+ if self.file_fn.is_some() {
577+ matcher.config.set_match_paths()
578+ }
546579
547− let options = self
548− .matches
549− .iter()
550− .skip(offset)
551− .take(rows as usize)
552− .map(|pmatch| &self.options[pmatch.index])
553− .map(|option| option.format(&self.editor_data))
554− .map(|mut row| {
555− const TEMP_CELL_SEP: &str = " ";
556−
557− let line = row.cell_text().fold(String::new(), |mut s, frag| {
558− s.push_str(&frag);
559− s.push_str(TEMP_CELL_SEP);
560− s
561− });
562−
563− // Items are filtered by using the text returned by menu::Item::filter_text
564− // but we do highlighting here using the text in Row and therefore there
565− // might be inconsistencies. This is the best we can do since only the
566− // text in Row is displayed to the end user.
567− let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
568− .fuzzy_indices(&line, &self.matcher)
569− .unwrap_or_default();
570−
571− let highlight_byte_ranges: Vec<_> = line
572− .char_indices()
573− .enumerate()
574− .filter_map(|(char_idx, (byte_offset, ch))| {
575− highlights
576− .contains(&char_idx)
577− .then(|| byte_offset..byte_offset + ch.len_utf8())
578− })
579− .collect();
580−
581− // The starting byte index of the current (iterating) cell
582− let mut cell_start_byte_offset = 0;
583− for cell in row.cells.iter_mut() {
584− let spans = match cell.content.lines.get(0) {
585− Some(s) => s,
586− None => {
587− cell_start_byte_offset += TEMP_CELL_SEP.len();
588− continue;
589− }
590− };
580+ let options = snapshot.matched_items(offset..end).map(|item| {
581+ snapshot.pattern().column_pattern(0).indices(
582+ item.matcher_columns[0].slice(..),
583+ &mut matcher,
584+ &mut indices,
585+ );
586+ indices.sort_unstable();
587+ indices.dedup();
588+ let mut row = item.data.format(&self.editor_data);
589+
590+ let mut grapheme_idx = 0u32;
591+ let mut indices = indices.drain(..);
592+ let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
593+ if self.widths.len() < row.cells.len() {
594+ self.widths.resize(row.cells.len(), Constraint::Length(0));
595+ }
596+ let mut widths = self.widths.iter_mut();
597+ for cell in &mut row.cells {
598+ let Some(Constraint::Length(max_width)) = widths.next() else {
599+ unreachable!();
600+ };
591601
592− let mut cell_len = 0;
593−
594− let graphemes_with_style: Vec<_> = spans
595− .0
596− .iter()
597− .flat_map(|span| {
598− span.content
599− .grapheme_indices(true)
600− .zip(std::iter::repeat(span.style))
601− })
602− .map(|((grapheme_byte_offset, grapheme), style)| {
603− cell_len += grapheme.len();
604− let start = cell_start_byte_offset;
605−
606− let grapheme_byte_range =
607− grapheme_byte_offset..grapheme_byte_offset + grapheme.len();
608−
609− if highlight_byte_ranges.iter().any(|hl_rng| {
610− hl_rng.start >= start + grapheme_byte_range.start
611− && hl_rng.end <= start + grapheme_byte_range.end
612− }) {
613− (grapheme, style.patch(highlight_style))
614− } else {
615− (grapheme, style)
616− }
617− })
618− .collect();
619−
620− let mut span_list: Vec<(String, Style)> = Vec::new();
621− for (grapheme, style) in graphemes_with_style {
622− if span_list.last().map(|(_, sty)| sty) == Some(&style) {
623− let (string, _) = span_list.last_mut().unwrap();
624− string.push_str(grapheme);
602+ // merge index highlights on top of existing hightlights
603+ let mut span_list = Vec::new();
604+ let mut current_span = String::new();
605+ let mut current_style = Style::default();
606+ let mut width = 0;
607+
608+ let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
609+ for span in spans {
610+ // this looks like a bug on first glance, we are iterating
611+ // graphemes but treating them as char indices. The reason that
612+ // this is correct is that nucleo will only ever consider the first char
613+ // of a grapheme (and discard the rest of the grapheme) so the indices
614+ // returned by nucleo are essentially grapheme indecies
615+ for grapheme in span.content.graphemes(true) {
616+ let style = if grapheme_idx == next_highlight_idx {
617+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
618+ span.style.patch(highlight_style)
625619 } else {
626− span_list.push((String::from(grapheme), style))
620+ span.style
621+ };
622+ if style != current_style {
623+ if !current_span.is_empty() {
624+ span_list.push(Span::styled(current_span, current_style))
625+ }
626+ current_span = String::new();
627+ current_style = style;
627628 }
629+ current_span.push_str(grapheme);
630+ grapheme_idx += 1;
628631 }
632+ width += span.width();
633+ }
629634
630− let spans: Vec<Span> = span_list
631− .into_iter()
632− .map(|(string, style)| Span::styled(string, style))
633− .collect();
634− let spans: Spans = spans.into();
635− *cell = Cell::from(spans);
635+ span_list.push(Span::styled(current_span, current_style));
636+ if width as u16 > *max_width {
637+ *max_width = width as u16;
638+ }
639+ *cell = Cell::from(Spans::from(span_list));
636640
637− cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len();
641+ // spacer
642+ if grapheme_idx == next_highlight_idx {
643+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
638644 }
645+ grapheme_idx += 1;
646+ }
639647
640− row
641− });
648+ row
649+ });
642650
643651 let table = Table::new(options)
644652 .style(text_style)
@@ -654,7 +662,7 @@ impl<T: Item + 'static> Picker<T> {
654662 surface,
655663 &mut TableState {
656664 offset: 0,
657− selected: Some(cursor),
665+ selected: Some(cursor as usize),
658666 },
659667 self.truncate_start,
660668 );
@@ -755,7 +763,7 @@ impl<T: Item + 'static> Picker<T> {
755763 }
756764}
757765
758−impl<T: Item + 'static> Component for Picker<T> {
766+impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
759767 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
760768 // +---------+ +---------+
761769 // |prompt | |preview |
@@ -875,29 +883,10 @@ impl<T: Item + 'static> Component for Picker<T> {
875883 Some((width, height))
876884 }
877885}
878−
879−#[derive(PartialEq, Eq, Debug)]
880−struct PickerMatch {
881− score: i64,
882− index: usize,
883− len: usize,
884−}
885−
886−impl PickerMatch {
887− fn key(&self) -> impl Ord {
888− (cmp::Reverse(self.score), self.len, self.index)
889− }
890−}
891−
892−impl PartialOrd for PickerMatch {
893− fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
894− Some(self.cmp(other))
895− }
896−}
897−
898−impl Ord for PickerMatch {
899− fn cmp(&self, other: &Self) -> Ordering {
900− self.key().cmp(&other.key())
886+impl<T: Item> Drop for Picker<T> {
887+ fn drop(&mut self) {
888+ // ensure we cancel any ongoing background threads streaming into the picker
889+ self.shutdown.store(true, atomic::Ordering::Relaxed)
901890 }
902891}
903892
@@ -910,13 +899,13 @@ pub type DynQueryCallback<T> =
910899
911900/// A picker that updates its contents via a callback whenever the
912901/// query string changes. Useful for live grep, workspace symbols, etc.
913−pub struct DynamicPicker<T: ui::menu::Item + Send> {
902+pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
914903 file_picker: Picker<T>,
915904 query_callback: DynQueryCallback<T>,
916905 query: String,
917906}
918907
919−impl<T: ui::menu::Item + Send> DynamicPicker<T> {
908+impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
920909 pub const ID: &'static str = "dynamic-picker";
921910
922911 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
@@ -928,7 +917,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
928917 }
929918}
930919
931−impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
920+impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
932921 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
933922 self.file_picker.render(area, surface, cx);
934923 }
helix-term/tests/integration.rs−1
@@ -20,7 +20,6 @@ mod test {
2020 mod commands;
2121 mod languages;
2222 mod movement;
23− mod picker;
2423 mod prompt;
2524 mod splits;
2625}
helix-term/tests/test/picker.rsdeleted−80
@@ -1,80 +0,0 @@
1−use std::fs;
2−
3−use helix_core::{path::get_canonicalized_path, Range};
4−use helix_loader::{current_working_dir, set_current_working_dir};
5−use helix_view::{current_ref, editor::Action};
6−use tempfile::{Builder, TempDir};
7−
8−use super::*;
9−
10−#[tokio::test(flavor = "multi_thread")]
11−async fn test_picker_alt_ret() -> anyhow::Result<()> {
12− // Create two files, open the first and run a global search for a word
13− // from the second file. Press <alt-ret> to have helix open the second file in the
14− // new buffer, but not change focus. Then check whether the word is highlighted
15− // correctly and the view of the first file has not changed.
16− let tmp_dir = TempDir::new()?;
17− set_current_working_dir(tmp_dir.path().into())?;
18−
19− let mut app = AppBuilder::new().build()?;
20−
21− log::debug!(
22− "set current working directory to {:?}",
23− current_working_dir()
24− );
25−
26− // Add prefix so helix doesn't hide these files in a picker
27− let files = [
28− Builder::new().prefix("1").tempfile_in(&tmp_dir)?,
29− Builder::new().prefix("2").tempfile_in(&tmp_dir)?,
30− ];
31− let paths = files
32− .iter()
33− .map(|f| get_canonicalized_path(f.path()))
34− .collect::<Vec<_>>();
35−
36− fs::write(&paths[0], "1\n2\n3\n4")?;
37− fs::write(&paths[1], "first\nsecond")?;
38−
39− log::debug!(
40− "created and wrote two temporary files: {:?} & {:?}",
41− paths[0],
42− paths[1]
43− );
44−
45− // Manually open to save the offset, otherwise we won't be able to change the state in the Fn trait
46− app.editor.open(files[0].path(), Action::Replace)?;
47− let view_offset = current_ref!(app.editor).0.offset;
48−
49− test_key_sequences(
50− &mut app,
51− vec![
52− (Some("<space>/"), None),
53− (Some("second<ret>"), None),
54− (
55− Some("<A-ret><esc>"),
56− Some(&|app| {
57− let (view, doc) = current_ref!(app.editor);
58− assert_eq!(doc.path().unwrap(), &paths[0]);
59− let select_ranges = doc.selection(view.id).ranges();
60− assert_eq!(select_ranges[0], Range::new(0, 1));
61− assert_eq!(view.offset, view_offset);
62− }),
63− ),
64− (
65− Some(":buffer<minus>next<ret>"),
66− Some(&|app| {
67− let (view, doc) = current_ref!(app.editor);
68− assert_eq!(doc.path().unwrap(), &paths[1]);
69− let select_ranges = doc.selection(view.id).ranges();
70− assert_eq!(select_ranges.len(), 1);
71− assert_eq!(select_ranges[0], Range::new(6, 12));
72− }),
73− ),
74− ],
75− false,
76− )
77− .await?;
78−
79− Ok(())
80−}
helix-vcs/Cargo.toml+1
@@ -12,6 +12,7 @@ homepage = "https://helix-editor.com"
1212
1313[dependencies]
1414helix-core = { version = "0.6", path = "../helix-core" }
15+helix-event = { version = "0.6", path = "../helix-event" }
1516
1617tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
1718parking_lot = "0.12"
helix-vcs/src/diff.rs+6−19
@@ -2,10 +2,10 @@ use std::ops::Range;
22use std::sync::Arc;
33
44use helix_core::Rope;
5+use helix_event::RenderLockGuard;
56use imara_diff::Algorithm;
67use parking_lot::{Mutex, MutexGuard};
78use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8−use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
99use tokio::task::JoinHandle;
1010use tokio::time::Instant;
1111
@@ -14,11 +14,9 @@ use crate::diff::worker::DiffWorker;
1414mod line_cache;
1515mod worker;
1616
17−type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
18−
1917/// A rendering lock passed to the differ the prevents redraws from occurring
2018struct RenderLock {
21− pub lock: OwnedRwLockReadGuard<()>,
19+ pub lock: RenderLockGuard,
2220 pub timeout: Option<Instant>,
2321}
2422
@@ -38,28 +36,22 @@ struct DiffInner {
3836#[derive(Clone, Debug)]
3937pub struct DiffHandle {
4038 channel: UnboundedSender<Event>,
41− render_lock: Arc<RwLock<()>>,
4239 diff: Arc<Mutex<DiffInner>>,
4340 inverted: bool,
4441}
4542
4643impl DiffHandle {
47− pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
48− DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
44+ pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle {
45+ DiffHandle::new_with_handle(diff_base, doc).0
4946 }
5047
51− fn new_with_handle(
52− diff_base: Rope,
53− doc: Rope,
54− redraw_handle: RedrawHandle,
55− ) -> (DiffHandle, JoinHandle<()>) {
48+ fn new_with_handle(diff_base: Rope, doc: Rope) -> (DiffHandle, JoinHandle<()>) {
5649 let (sender, receiver) = unbounded_channel();
5750 let diff: Arc<Mutex<DiffInner>> = Arc::default();
5851 let worker = DiffWorker {
5952 channel: receiver,
6053 diff: diff.clone(),
6154 new_hunks: Vec::default(),
62− redraw_notify: redraw_handle.0,
6355 diff_finished_notify: Arc::default(),
6456 };
6557 let handle = tokio::spawn(worker.run(diff_base, doc));
@@ -67,7 +59,6 @@ impl DiffHandle {
6759 channel: sender,
6860 diff,
6961 inverted: false,
70− render_lock: redraw_handle.1,
7162 };
7263 (differ, handle)
7364 }
@@ -87,11 +78,7 @@ impl DiffHandle {
8778 /// This function is only intended to be called from within the rendering loop
8879 /// if called from elsewhere it may fail to acquire the render lock and panic
8980 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
90− // unwrap is ok here because the rendering lock is
91− // only exclusively locked during redraw.
92− // This function is only intended to be called
93− // from the core rendering loop where no redraw can happen in parallel
94− let lock = self.render_lock.clone().try_read_owned().unwrap();
81+ let lock = helix_event::lock_frame();
9582 let timeout = if block {
9683 None
9784 } else {
helix-vcs/src/diff/worker.rs+3−9
@@ -23,7 +23,6 @@ pub(super) struct DiffWorker {
2323 pub channel: UnboundedReceiver<Event>,
2424 pub diff: Arc<Mutex<DiffInner>>,
2525 pub new_hunks: Vec<Hunk>,
26− pub redraw_notify: Arc<Notify>,
2726 pub diff_finished_notify: Arc<Notify>,
2827}
2928
@@ -32,11 +31,7 @@ impl DiffWorker {
3231 let mut accumulator = EventAccumulator::new();
3332 accumulator.handle_event(event).await;
3433 accumulator
35− .accumulate_debounced_events(
36− &mut self.channel,
37− self.redraw_notify.clone(),
38− self.diff_finished_notify.clone(),
39− )
34+ .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone())
4035 .await;
4136 (accumulator.doc, accumulator.diff_base)
4237 }
@@ -137,7 +132,6 @@ impl<'a> EventAccumulator {
137132 async fn accumulate_debounced_events(
138133 &mut self,
139134 channel: &mut UnboundedReceiver<Event>,
140− redraw_notify: Arc<Notify>,
141135 diff_finished_notify: Arc<Notify>,
142136 ) {
143137 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
@@ -164,7 +158,7 @@ impl<'a> EventAccumulator {
164158 None => {
165159 tokio::spawn(async move {
166160 diff_finished_notify.notified().await;
167− redraw_notify.notify_one();
161+ helix_event::request_redraw();
168162 });
169163 }
170164 // diff is performed inside the rendering loop
@@ -190,7 +184,7 @@ impl<'a> EventAccumulator {
190184 // and wait until the diff occurs to trigger an async redraw
191185 log::info!("Diff computation timed out, update of diffs might appear delayed");
192186 diff_finished_notify.notified().await;
193− redraw_notify.notify_one();
187+ helix_event::request_redraw()
194188 });
195189 }
196190 // a blocking diff is performed inside the rendering loop
helix-vcs/src/diff/worker/test.rs+1−5
@@ -5,11 +5,7 @@ use crate::diff::{DiffHandle, Hunk};
55
66impl DiffHandle {
77 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
8− DiffHandle::new_with_handle(
9− Rope::from_str(diff_base),
10− Rope::from_str(doc),
11− Default::default(),
12− )
8+ DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc))
139 }
1410 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
1511 let diff = self.diff;
helix-view/Cargo.toml+1
@@ -17,6 +17,7 @@ term = ["crossterm"]
1717bitflags = "2.4"
1818anyhow = "1"
1919helix-core = { version = "0.6", path = "../helix-core" }
20+helix-event = { version = "0.6", path = "../helix-event" }
2021helix-loader = { version = "0.6", path = "../helix-loader" }
2122helix-lsp = { version = "0.6", path = "../helix-lsp" }
2223helix-dap = { version = "0.6", path = "../helix-dap" }
helix-view/src/document.rs+4−5
@@ -33,7 +33,7 @@ use helix_core::{
3333 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
3434};
3535
36−use crate::editor::{Config, RedrawHandle};
36+use crate::editor::Config;
3737use crate::{DocumentId, Editor, Theme, View, ViewId};
3838
3939/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -995,7 +995,6 @@ impl Document {
995995 &mut self,
996996 view: &mut View,
997997 provider_registry: &DiffProviderRegistry,
998− redraw_handle: RedrawHandle,
999998 ) -> Result<(), Error> {
1000999 let encoding = self.encoding;
10011000 let path = self
@@ -1023,7 +1022,7 @@ impl Document {
10231022 self.detect_indent_and_line_ending();
10241023
10251024 match provider_registry.get_diff_base(&path) {
1026− Some(diff_base) => self.set_diff_base(diff_base, redraw_handle),
1025+ Some(diff_base) => self.set_diff_base(diff_base),
10271026 None => self.diff_handle = None,
10281027 }
10291028
@@ -1583,13 +1582,13 @@ impl Document {
15831582 }
15841583
15851584 /// Intialize/updates the differ for this document with a new base.
1586− pub fn set_diff_base(&mut self, diff_base: Vec<u8>, redraw_handle: RedrawHandle) {
1585+ pub fn set_diff_base(&mut self, diff_base: Vec<u8>) {
15871586 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
15881587 if let Some(differ) = &self.diff_handle {
15891588 differ.update_diff_base(diff_base);
15901589 return;
15911590 }
1592− self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle))
1591+ self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone()))
15931592 } else {
15941593 self.diff_handle = None;
15951594 }
helix-view/src/editor.rs+3−10
@@ -32,7 +32,7 @@ use std::{
3232use tokio::{
3333 sync::{
3434 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
35− oneshot, Notify, RwLock,
35+ oneshot,
3636 },
3737 time::{sleep, Duration, Instant, Sleep},
3838};
@@ -925,10 +925,6 @@ pub struct Editor {
925925 pub exit_code: i32,
926926
927927 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
928− /// Allows asynchronous tasks to control the rendering
929− /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
930− /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be acquired
931− pub redraw_handle: RedrawHandle,
932928 pub needs_redraw: bool,
933929 /// Cached position of the cursor calculated during rendering.
934930 /// The content of `cursor_cache` is returned by `Editor::cursor` if
@@ -955,8 +951,6 @@ pub struct Editor {
955951
956952pub type Motion = Box<dyn Fn(&mut Editor)>;
957953
958−pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
959−
960954#[derive(Debug)]
961955pub enum EditorEvent {
962956 DocumentSaved(DocumentSavedEventResult),
@@ -1062,7 +1056,6 @@ impl Editor {
10621056 auto_pairs,
10631057 exit_code: 0,
10641058 config_events: unbounded_channel(),
1065− redraw_handle: Default::default(),
10661059 needs_redraw: false,
10671060 cursor_cache: Cell::new(None),
10681061 completion_request_handle: None,
@@ -1453,7 +1446,7 @@ impl Editor {
14531446 )?;
14541447
14551448 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1456− doc.set_diff_base(diff_base, self.redraw_handle.clone());
1449+ doc.set_diff_base(diff_base);
14571450 }
14581451 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
14591452
@@ -1752,7 +1745,7 @@ impl Editor {
17521745 return EditorEvent::DebuggerEvent(event)
17531746 }
17541747
1755− _ = self.redraw_handle.0.notified() => {
1748+ _ = helix_event::redraw_requested() => {
17561749 if !self.needs_redraw{
17571750 self.needs_redraw = true;
17581751 let timeout = Instant::now() + Duration::from_millis(33);