on master help sign in
transition to nucleo for fuzzy matching (#7814)
0cb595ePascal Kuthe
Cargo.lock+116−10
@@ -248,6 +248,12 @@ version = "0.8.4"
248source = "registry+https://github.com/rust-lang/crates.io-index"
248source = "registry+https://github.com/rust-lang/crates.io-index"
249checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
249checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
250
250
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+
251[[package]]
257[[package]]
252name = "crc32fast"
258name = "crc32fast"
253version = "1.3.2"
259version = "1.3.2"
@@ -257,6 +263,49 @@ dependencies = [
257 "cfg-if",
263 "cfg-if",
258]
264]
259
265
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+
260[[package]]
309[[package]]
261name = "crossterm"
310name = "crossterm"
262version = "0.27.0"
311version = "0.27.0"
@@ -505,15 +554,6 @@ dependencies = [
505 "slab",
554 "slab",
506]
555]
507
556
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−
517[[package]]
557[[package]]
518name = "getrandom"
558name = "getrandom"
519version = "0.2.9"
559version = "0.2.9"
@@ -1227,7 +1267,9 @@ dependencies = [
1227 "imara-diff",
1267 "imara-diff",
1228 "indoc",
1268 "indoc",
1229 "log",
1269 "log",
1270+ "nucleo",
1230 "once_cell",
1271 "once_cell",
1272+ "parking_lot",
1231 "quickcheck",
1273 "quickcheck",
1232 "regex",
1274 "regex",
1233 "ropey",
1275 "ropey",
@@ -1259,6 +1301,14 @@ dependencies = [
1259 "which",
1301 "which",
1260]
1302]
1261
1303
1304+[[package]]
1305+name = "helix-event"
1306+version = "0.6.0"
1307+dependencies = [
1308+ "parking_lot",
1309+ "tokio",
1310+]
1311+
1262[[package]]
1312[[package]]
1263name = "helix-loader"
1313name = "helix-loader"
1264version = "0.6.0"
1314version = "0.6.0"
@@ -1315,11 +1365,11 @@ dependencies = [
1315 "crossterm",
1365 "crossterm",
1316 "fern",
1366 "fern",
1317 "futures-util",
1367 "futures-util",
1318− "fuzzy-matcher",
1319 "grep-regex",
1368 "grep-regex",
1320 "grep-searcher",
1369 "grep-searcher",
1321 "helix-core",
1370 "helix-core",
1322 "helix-dap",
1371 "helix-dap",
1372+ "helix-event",
1323 "helix-loader",
1373 "helix-loader",
1324 "helix-lsp",
1374 "helix-lsp",
1325 "helix-tui",
1375 "helix-tui",
@@ -1329,6 +1379,7 @@ dependencies = [
1329 "indoc",
1379 "indoc",
1330 "libc",
1380 "libc",
1331 "log",
1381 "log",
1382+ "nucleo",
1332 "once_cell",
1383 "once_cell",
1333 "pulldown-cmark",
1384 "pulldown-cmark",
1334 "serde",
1385 "serde",
@@ -1367,6 +1418,7 @@ dependencies = [
1367 "arc-swap",
1418 "arc-swap",
1368 "gix",
1419 "gix",
1369 "helix-core",
1420 "helix-core",
1421+ "helix-event",
1370 "imara-diff",
1422 "imara-diff",
1371 "log",
1423 "log",
1372 "parking_lot",
1424 "parking_lot",
@@ -1387,6 +1439,7 @@ dependencies = [
1387 "futures-util",
1439 "futures-util",
1388 "helix-core",
1440 "helix-core",
1389 "helix-dap",
1441 "helix-dap",
1442+ "helix-event",
1390 "helix-loader",
1443 "helix-loader",
1391 "helix-lsp",
1444 "helix-lsp",
1392 "helix-tui",
1445 "helix-tui",
@@ -1654,6 +1707,15 @@ dependencies = [
1654 "libc",
1707 "libc",
1655]
1708]
1656
1709
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+
1657[[package]]
1719[[package]]
1658name = "minimal-lexical"
1720name = "minimal-lexical"
1659version = "0.2.1"
1721version = "0.2.1"
@@ -1700,6 +1762,28 @@ dependencies = [
1700 "minimal-lexical",
1762 "minimal-lexical",
1701]
1763]
1702
1764
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+
1703[[package]]
1787[[package]]
1704name = "num-traits"
1788name = "num-traits"
1705version = "0.2.15"
1789version = "0.2.15"
@@ -1846,6 +1930,28 @@ dependencies = [
1846 "getrandom",
1930 "getrandom",
1847]
1931]
1848
1932
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+
1849[[package]]
1955[[package]]
1850name = "redox_syscall"
1956name = "redox_syscall"
1851version = "0.2.16"
1957version = "0.2.16"
Cargo.toml+2
@@ -5,6 +5,7 @@ members = [
5 "helix-term",
5 "helix-term",
6 "helix-tui",
6 "helix-tui",
7 "helix-lsp",
7 "helix-lsp",
8+ "helix-event",
8 "helix-dap",
9 "helix-dap",
9 "helix-loader",
10 "helix-loader",
10 "helix-vcs",
11 "helix-vcs",
@@ -35,3 +36,4 @@ package.helix-term.opt-level = 2
35
36
36[workspace.dependencies]
37[workspace.dependencies]
37tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
38tree-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"
48etcetera = "0.8"
48etcetera = "0.8"
49textwrap = "0.16.0"
49textwrap = "0.16.0"
50
50
51+nucleo.workspace = true
52+parking_lot = "0.12"
53+
51[dev-dependencies]
54[dev-dependencies]
52quickcheck = { version = "1", default-features = false }
55quickcheck = { version = "1", default-features = false }
53indoc = "2.0.3"
56indoc = "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;
7pub mod diagnostic;
7pub mod diagnostic;
8pub mod diff;
8pub mod diff;
9pub mod doc_formatter;
9pub mod doc_formatter;
10+pub mod fuzzy;
10pub mod graphemes;
11pub mod graphemes;
11pub mod history;
12pub mod history;
12pub mod increment;
13pub 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"
24
24
25[dependencies]
25[dependencies]
26helix-core = { version = "0.6", path = "../helix-core" }
26helix-core = { version = "0.6", path = "../helix-core" }
27+helix-event = { version = "0.6", path = "../helix-event" }
27helix-view = { version = "0.6", path = "../helix-view" }
28helix-view = { version = "0.6", path = "../helix-view" }
28helix-lsp = { version = "0.6", path = "../helix-lsp" }
29helix-lsp = { version = "0.6", path = "../helix-lsp" }
29helix-dap = { version = "0.6", path = "../helix-dap" }
30helix-dap = { version = "0.6", path = "../helix-dap" }
@@ -49,7 +50,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
49log = "0.4"
50log = "0.4"
50
51
51# File picker
52# File picker
52−fuzzy-matcher = "0.3"
53+nucleo.workspace = true
53ignore = "0.4"
54ignore = "0.4"
54# markdown doc rendering
55# markdown doc rendering
55pulldown-cmark = { version = "0.9", default-features = false }
56pulldown-cmark = { version = "0.9", default-features = false }
helix-term/src/application.rs+2−10
@@ -257,16 +257,8 @@ impl Application {
257 scroll: None,
257 scroll: None,
258 };
258 };
259
259
260− // Acquire mutable access to the redraw_handle lock
260+ helix_event::start_frame();
261− // to ensure that there are no tasks running that want to block rendering
262− drop(cx.editor.redraw_handle.1.write().await);
263 cx.editor.needs_redraw = false;
261 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− }
270
262
271 let area = self
263 let area = self
272 .terminal
264 .terminal
@@ -590,7 +582,7 @@ impl Application {
590 EditorEvent::LanguageServerMessage((id, call)) => {
582 EditorEvent::LanguageServerMessage((id, call)) => {
591 self.handle_language_server_message(call, id).await;
583 self.handle_language_server_message(call, id).await;
592 // limit render calls for fast language server messages
584 // limit render calls for fast language server messages
593− self.editor.redraw_handle.0.notify_one();
585+ helix_event::request_redraw();
594 }
586 }
595 EditorEvent::DebuggerEvent(payload) => {
587 EditorEvent::DebuggerEvent(payload) => {
596 let needs_render = self.editor.handle_debugger_message(payload).await;
588 let needs_render = self.editor.handle_debugger_message(payload).await;
helix-term/src/commands.rs+149−146
@@ -43,7 +43,6 @@ use helix_view::{
43};
43};
44
44
45use anyhow::{anyhow, bail, ensure, Context as _};
45use anyhow::{anyhow, bail, ensure, Context as _};
46−use fuzzy_matcher::FuzzyMatcher;
47use insert::*;
46use insert::*;
48use movement::Movement;
47use movement::Movement;
49
48
@@ -60,7 +59,7 @@ use crate::{
60};
59};
61
60
62use crate::job::{self, Jobs};
61use crate::job::{self, Jobs};
63−use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt};
62+use futures_util::{stream::FuturesUnordered, TryStreamExt};
64use std::{collections::HashMap, fmt, future::Future};
63use std::{collections::HashMap, fmt, future::Future};
65use std::{collections::HashSet, num::NonZeroUsize};
64use std::{collections::HashSet, num::NonZeroUsize};
66
65
@@ -75,7 +74,6 @@ use serde::de::{self, Deserialize, Deserializer};
75use grep_regex::RegexMatcherBuilder;
74use grep_regex::RegexMatcherBuilder;
76use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
75use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
77use ignore::{DirEntry, WalkBuilder, WalkState};
76use ignore::{DirEntry, WalkBuilder, WalkState};
78−use tokio_stream::wrappers::UnboundedReceiverStream;
79
77
80pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
78pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
81
79
@@ -1715,8 +1713,8 @@ fn select_regex(cx: &mut Context) {
1715 "select:".into(),
1713 "select:".into(),
1716 Some(reg),
1714 Some(reg),
1717 ui::completers::none,
1715 ui::completers::none,
1718− move |editor, regex, event| {
1716+ move |cx, regex, event| {
1719− let (view, doc) = current!(editor);
1717+ let (view, doc) = current!(cx.editor);
1720 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1718 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1721 return;
1719 return;
1722 }
1720 }
@@ -1737,8 +1735,8 @@ fn split_selection(cx: &mut Context) {
1737 "split:".into(),
1735 "split:".into(),
1738 Some(reg),
1736 Some(reg),
1739 ui::completers::none,
1737 ui::completers::none,
1740− move |editor, regex, event| {
1738+ move |cx, regex, event| {
1741− let (view, doc) = current!(editor);
1739+ let (view, doc) = current!(cx.editor);
1742 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1740 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
1743 return;
1741 return;
1744 }
1742 }
@@ -1902,14 +1900,14 @@ fn searcher(cx: &mut Context, direction: Direction) {
1902 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1900 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
1903 .collect()
1901 .collect()
1904 },
1902 },
1905− move |editor, regex, event| {
1903+ move |cx, regex, event| {
1906 if event == PromptEvent::Validate {
1904 if event == PromptEvent::Validate {
1907− editor.registers.last_search_register = reg;
1905+ cx.editor.registers.last_search_register = reg;
1908 } else if event != PromptEvent::Update {
1906 } else if event != PromptEvent::Update {
1909 return;
1907 return;
1910 }
1908 }
1911 search_impl(
1909 search_impl(
1912− editor,
1910+ cx.editor,
1913 &contents,
1911 &contents,
1914 &regex,
1912 &regex,
1915 Movement::Move,
1913 Movement::Move,
@@ -2078,13 +2076,11 @@ fn global_search(cx: &mut Context) {
2078 }
2076 }
2079 }
2077 }
2080
2078
2081− let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
2082 let config = cx.editor.config();
2079 let config = cx.editor.config();
2083 let smart_case = config.search.smart_case;
2080 let smart_case = config.search.smart_case;
2084 let file_picker_config = config.file_picker.clone();
2081 let file_picker_config = config.file_picker.clone();
2085
2082
2086 let reg = cx.register.unwrap_or('/');
2083 let reg = cx.register.unwrap_or('/');
2087−
2088 let completions = search_completions(cx, Some(reg));
2084 let completions = search_completions(cx, Some(reg));
2089 ui::regex_prompt(
2085 ui::regex_prompt(
2090 cx,
2086 cx,
@@ -2097,166 +2093,173 @@ fn global_search(cx: &mut Context) {
2097 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2093 .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
2098 .collect()
2094 .collect()
2099 },
2095 },
2100− move |editor, regex, event| {
2096+ move |cx, regex, event| {
2101 if event != PromptEvent::Validate {
2097 if event != PromptEvent::Validate {
2102 return;
2098 return;
2103 }
2099 }
2104− editor.registers.last_search_register = reg;
2100+ cx.editor.registers.last_search_register = reg;
2105
2101
2102+ let current_path = doc_mut!(cx.editor).path().cloned();
2106− let documents: Vec<_> = editor
2103+ let documents: Vec<_> = cx
2104+ .editor
2107 .documents()
2105 .documents()
2108− .map(|doc| (doc.path(), doc.text()))
2106+ .map(|doc| (doc.path().cloned(), doc.text().to_owned()))
2109 .collect();
2107 .collect();
2110
2108
2111 if let Ok(matcher) = RegexMatcherBuilder::new()
2109 if let Ok(matcher) = RegexMatcherBuilder::new()
2112 .case_smart(smart_case)
2110 .case_smart(smart_case)
2113 .build(regex.as_str())
2111 .build(regex.as_str())
2114 {
2112 {
2115− let searcher = SearcherBuilder::new()
2116− .binary_detection(BinaryDetection::quit(b'\x00'))
2117− .build();
2118−
2119 let search_root = helix_loader::current_working_dir();
2113 let search_root = helix_loader::current_working_dir();
2120 if !search_root.exists() {
2114 if !search_root.exists() {
2115+ cx.editor
2121− editor.set_error("Current working directory does not exist");
2116+ .set_error("Current working directory does not exist");
2122 return;
2117 return;
2123 }
2118 }
2124
2119
2120+ let (picker, injector) = Picker::stream(current_path);
2121+
2125 let dedup_symlinks = file_picker_config.deduplicate_links;
2122 let dedup_symlinks = file_picker_config.deduplicate_links;
2126 let absolute_root = search_root
2123 let absolute_root = search_root
2127 .canonicalize()
2124 .canonicalize()
2128 .unwrap_or_else(|_| search_root.clone());
2125 .unwrap_or_else(|_| search_root.clone());
2129−
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();
2130− WalkBuilder::new(search_root)
2132+ WalkBuilder::new(search_root)
2131− .hidden(file_picker_config.hidden)
2133+ .hidden(file_picker_config.hidden)
2132− .parents(file_picker_config.parents)
2134+ .parents(file_picker_config.parents)
2133− .ignore(file_picker_config.ignore)
2135+ .ignore(file_picker_config.ignore)
2134− .follow_links(file_picker_config.follow_symlinks)
2136+ .follow_links(file_picker_config.follow_symlinks)
2135− .git_ignore(file_picker_config.git_ignore)
2137+ .git_ignore(file_picker_config.git_ignore)
2136− .git_global(file_picker_config.git_global)
2138+ .git_global(file_picker_config.git_global)
2137− .git_exclude(file_picker_config.git_exclude)
2139+ .git_exclude(file_picker_config.git_exclude)
2138− .max_depth(file_picker_config.max_depth)
2140+ .max_depth(file_picker_config.max_depth)
2139− .filter_entry(move |entry| {
2141+ .filter_entry(move |entry| {
2140− filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2142+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2141− })
2143+ })
2142− .build_parallel()
2144+ .build_parallel()
2143− .run(|| {
2145+ .run(|| {
2144− let mut searcher = searcher.clone();
2146+ let mut searcher = searcher.clone();
2145− let matcher = matcher.clone();
2147+ let matcher = matcher.clone();
2146− let all_matches_sx = all_matches_sx.clone();
2148+ let injector = injector_.clone();
2147− let documents = &documents;
2149+ let documents = &documents;
2148− Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2150+ Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
2149− let entry = match entry {
2151+ let entry = match entry {
2150− Ok(entry) => entry,
2152+ Ok(entry) => entry,
2151− Err(_) => return WalkState::Continue,
2153+ Err(_) => return WalkState::Continue,
2152− };
2154+ };
2153−
2155+
2154− match entry.file_type() {
2156+ match entry.file_type() {
2155− Some(entry) if entry.is_file() => {}
2157+ Some(entry) if entry.is_file() => {}
2156− // skip everything else
2158+ // skip everything else
2157− _ => return WalkState::Continue,
2159+ _ => return WalkState::Continue,
2158− };
2160+ };
2159−
2161+
2162+ let mut stop = false;
2160− let sink = sinks::UTF8(|line_num, _| {
2163+ let sink = sinks::UTF8(|line_num, _| {
2161− all_matches_sx
2164+ stop = injector
2162− .send(FileResult::new(entry.path(), line_num as usize - 1))
2165+ .push(FileResult::new(entry.path(), line_num as usize - 1))
2163− .unwrap();
2166+ .is_err();
2164−
2167+
2165− Ok(true)
2168+ Ok(!stop)
2166− });
2169+ });
2167− let doc = documents.iter().find(|&(doc_path, _)| {
2170+ let doc = documents.iter().find(|&(doc_path, _)| {
2171+ doc_path
2172+ .as_ref()
2168− doc_path.map_or(false, |doc_path| doc_path == entry.path())
2173+ .map_or(false, |doc_path| doc_path == entry.path())
2169− });
2174+ });
2170−
2175+
2171− let result = if let Some((_, doc)) = doc {
2176+ let result = if let Some((_, doc)) = doc {
2172− // there is already a buffer for this file
2177+ // there is already a buffer for this file
2173− // search the buffer instead of the file because it's faster
2178+ // search the buffer instead of the file because it's faster
2174− // and captures new edits without requireing a save
2179+ // and captures new edits without requiring a save
2175− if searcher.multi_line_with_matcher(&matcher) {
2180+ if searcher.multi_line_with_matcher(&matcher) {
2176− // in this case a continous buffer is required
2181+ // in this case a continous buffer is required
2177− // convert the rope to a string
2182+ // convert the rope to a string
2178− let text = doc.to_string();
2183+ let text = doc.to_string();
2179− searcher.search_slice(&matcher, text.as_bytes(), sink)
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+ }
2180 } else {
2192 } else {
2181− searcher.search_reader(
2193+ searcher.search_path(&matcher, entry.path(), sink)
2182− &matcher,
2194+ };
2183− RopeReader::new(doc.slice(..)),
2195+
2184− sink,
2196+ if let Err(err) = result {
2197+ log::error!(
2198+ "Global search error: {}, {}",
2199+ entry.path().display(),
2200+ err
2185− )
2201+ );
2186 }
2202 }
2203+ if stop {
2204+ WalkState::Quit
2187− } else {
2205+ } else {
2188− searcher.search_path(&matcher, entry.path(), sink)
2206+ WalkState::Continue
2189− };
2207+ }
2190−
2208+ })
2191− if let Err(err) = result {
2209+ });
2192− log::error!(
2210+ });
2193− "Global search error: {}, {}",
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 '{}': {}",
2194− entry.path().display(),
2223+ path.display(),
2195− err
2224+ e
2196− );
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;
2197− }
2238+ }
2198− WalkState::Continue
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))))
2199− })
2251+ },
2252+ );
2253+ compositor.push(Box::new(overlaid(picker)))
2200− });
2254+ };
2255+ Ok(Callback::EditorCompositor(Box::new(call)))
2256+ })
2201 } else {
2257 } else {
2202 // Otherwise do nothing
2258 // Otherwise do nothing
2203 // log::warn!("Global Search Invalid Pattern")
2259 // log::warn!("Global Search Invalid Pattern")
2204 }
2260 }
2205 },
2261 },
2206 );
2262 );
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);
2260}
2263}
2261
2264
2262enum Extend {
2265enum Extend {
@@ -4310,8 +4313,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
4310 if remove { "remove:" } else { "keep:" }.into(),
4313 if remove { "remove:" } else { "keep:" }.into(),
4311 Some(reg),
4314 Some(reg),
4312 ui::completers::none,
4315 ui::completers::none,
4313− move |editor, regex, event| {
4316+ move |cx, regex, event| {
4314− let (view, doc) = current!(editor);
4317+ let (view, doc) = current!(cx.editor);
4315 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
4318 if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
4316 return;
4319 return;
4317 }
4320 }
helix-term/src/commands/typed.rs+13−25
@@ -5,6 +5,7 @@ use crate::job::Job;
5
5
6use super::*;
6use super::*;
7
7
8+use helix_core::fuzzy::fuzzy_match;
8use helix_core::{encoding, shellwords::Shellwords};
9use helix_core::{encoding, shellwords::Shellwords};
9use helix_view::document::DEFAULT_LANGUAGE_NAME;
10use helix_view::document::DEFAULT_LANGUAGE_NAME;
10use helix_view::editor::{Action, CloseError, ConfigEvent};
11use helix_view::editor::{Action, CloseError, ConfigEvent};
@@ -1265,12 +1266,10 @@ fn reload(
1265 }
1266 }
1266
1267
1267 let scrolloff = cx.editor.config().scrolloff;
1268 let scrolloff = cx.editor.config().scrolloff;
1268− let redraw_handle = cx.editor.redraw_handle.clone();
1269 let (view, doc) = current!(cx.editor);
1269 let (view, doc) = current!(cx.editor);
1270− doc.reload(view, &cx.editor.diff_providers, redraw_handle)
1270+ doc.reload(view, &cx.editor.diff_providers).map(|_| {
1271− .map(|_| {
1272− view.ensure_cursor_in_view(doc, scrolloff);
1271+ view.ensure_cursor_in_view(doc, scrolloff);
1273− })?;
1272+ })?;
1274 if let Some(path) = doc.path() {
1273 if let Some(path) = doc.path() {
1275 cx.editor
1274 cx.editor
1276 .language_servers
1275 .language_servers
@@ -1316,8 +1315,7 @@ fn reload_all(
1316 // Ensure that the view is synced with the document's history.
1315 // Ensure that the view is synced with the document's history.
1317 view.sync_changes(doc);
1316 view.sync_changes(doc);
1318
1317
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)?;
1321 if let Some(path) = doc.path() {
1319 if let Some(path) = doc.path() {
1322 cx.editor
1320 cx.editor
1323 .language_servers
1321 .language_servers
@@ -2902,28 +2900,18 @@ pub(super) fn command_mode(cx: &mut Context) {
2902 ":".into(),
2900 ":".into(),
2903 Some(':'),
2901 Some(':'),
2904 |editor: &Editor, input: &str| {
2902 |editor: &Editor, input: &str| {
2905− static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
2906− Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
2907−
2908 let shellwords = Shellwords::from(input);
2903 let shellwords = Shellwords::from(input);
2909 let words = shellwords.words();
2904 let words = shellwords.words();
2910
2905
2911 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
2906 if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) {
2912− // If the command has not been finished yet, complete commands.
2907+ fuzzy_match(
2908+ input,
2913− let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST
2909+ TYPABLE_COMMAND_LIST.iter().map(|command| command.name),
2914− .iter()
2910+ false,
2915− .filter_map(|command| {
2911+ )
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()
2912+ .into_iter()
2925− .map(|(name, _)| (0.., name.into()))
2913+ .map(|(name, _)| (0.., name.into()))
2926− .collect()
2914+ .collect()
2927 } else {
2915 } else {
2928 // Otherwise, use the command's completer and the last shellword
2916 // Otherwise, use the command's completer and the last shellword
2929 // as completion input.
2917 // 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};
2
2
3use crate::{
3use crate::{
4 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
4 compositor::{Callback, Component, Compositor, Context, Event, EventResult},
5 ctrl, key, shift,
5 ctrl, key, shift,
6};
6};
7+use helix_core::fuzzy::MATCHER;
8+use nucleo::pattern::{AtomKind, CaseMatching, Pattern};
9+use nucleo::{Config, Utf32Str};
7use tui::{buffer::Buffer as Surface, widgets::Table};
10use tui::{buffer::Buffer as Surface, widgets::Table};
8
11
9pub use tui::widgets::{Cell, Row};
12pub use tui::widgets::{Cell, Row};
10
13
11−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
12−use fuzzy_matcher::FuzzyMatcher;
13−
14use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
14use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
15use tui::layout::Constraint;
15use tui::layout::Constraint;
16
16
17−pub trait Item {
17+pub trait Item: Sync + Send + 'static {
18 /// Additional editor state that is used for label calculation.
18 /// Additional editor state that is used for label calculation.
19− type Data;
19+ type Data: Sync + Send + 'static;
20
20
21 fn format(&self, data: &Self::Data) -> Row;
21 fn format(&self, data: &Self::Data) -> Row;
22
22
@@ -51,9 +51,8 @@ pub struct Menu<T: Item> {
51
51
52 cursor: Option<usize>,
52 cursor: Option<usize>,
53
53
54− matcher: Box<Matcher>,
55 /// (index, score)
54 /// (index, score)
56− matches: Vec<(usize, i64)>,
55+ matches: Vec<(u32, u32)>,
57
56
58 widths: Vec<Constraint>,
57 widths: Vec<Constraint>,
59
58
@@ -75,11 +74,10 @@ impl<T: Item> Menu<T> {
75 editor_data: <T as Item>::Data,
74 editor_data: <T as Item>::Data,
76 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
75 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
77 ) -> Self {
76 ) -> 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();
79 Self {
78 Self {
80 options,
79 options,
81 editor_data,
80 editor_data,
82− matcher: Box::new(Matcher::default().ignore_case()),
83 matches,
81 matches,
84 cursor: None,
82 cursor: None,
85 widths: Vec::new(),
83 widths: Vec::new(),
@@ -94,20 +92,19 @@ impl<T: Item> Menu<T> {
94 pub fn score(&mut self, pattern: &str) {
92 pub fn score(&mut self, pattern: &str) {
95 // reuse the matches allocation
93 // reuse the matches allocation
96 self.matches.clear();
94 self.matches.clear();
97− self.matches.extend(
95+ let mut matcher = MATCHER.lock();
98− self.options
96+ matcher.config = Config::DEFAULT;
99− .iter()
97+ let pattern = Pattern::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy);
100− .enumerate()
98+ let mut buf = Vec::new();
101− .filter_map(|(index, option)| {
99+ let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
102− let text = option.filter_text(&self.editor_data);
100+ let text = option.filter_text(&self.editor_data);
103− // TODO: using fuzzy_indices could give us the char idx for match highlighting
101+ pattern
104− self.matcher
102+ .score(Utf32Str::new(&text, &mut buf), &mut matcher)
105− .fuzzy_match(&text, pattern)
106− .map(|score| (index, score))
103+ .map(|score| (i as u32, score))
107− }),
104+ });
108− );
105+ self.matches.extend(matches);
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);
106+ self.matches
107+ .sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
111
108
112 // reset cursor position
109 // reset cursor position
113 self.cursor = None;
110 self.cursor = None;
@@ -201,7 +198,7 @@ impl<T: Item> Menu<T> {
201 self.cursor.and_then(|cursor| {
198 self.cursor.and_then(|cursor| {
202 self.matches
199 self.matches
203 .get(cursor)
200 .get(cursor)
204− .map(|(index, _score)| &self.options[*index])
201+ .map(|(index, _score)| &self.options[*index as usize])
205 })
202 })
206 }
203 }
207
204
@@ -209,7 +206,7 @@ impl<T: Item> Menu<T> {
209 self.cursor.and_then(|cursor| {
206 self.cursor.and_then(|cursor| {
210 self.matches
207 self.matches
211 .get(cursor)
208 .get(cursor)
212− .map(|(index, _score)| &mut self.options[*index])
209+ .map(|(index, _score)| &mut self.options[*index as usize])
213 })
210 })
214 }
211 }
215
212
@@ -332,7 +329,7 @@ impl<T: Item + 'static> Component for Menu<T> {
332 .iter()
329 .iter()
333 .map(|(index, _score)| {
330 .map(|(index, _score)| {
334 // (index, self.options.get(*index).unwrap()) // get_unchecked
331 // (index, self.options.get(*index).unwrap()) // get_unchecked
335− &self.options[*index] // get_unchecked
332+ &self.options[*index as usize] // get_unchecked
336 })
333 })
337 .collect();
334 .collect();
338
335
helix-term/src/ui/mod.rs+44−140
@@ -1,7 +1,6 @@
1mod completion;
1mod completion;
2mod document;
2mod document;
3pub(crate) mod editor;
3pub(crate) mod editor;
4−mod fuzzy_match;
5mod info;
4mod info;
6pub mod lsp;
5pub mod lsp;
7mod markdown;
6mod markdown;
@@ -64,7 +63,7 @@ pub fn regex_prompt(
64 prompt: std::borrow::Cow<'static, str>,
63 prompt: std::borrow::Cow<'static, str>,
65 history_register: Option<char>,
64 history_register: Option<char>,
66 completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
65 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,
68) {
67) {
69 let (view, doc) = current!(cx.editor);
68 let (view, doc) = current!(cx.editor);
70 let doc_id = view.doc;
69 let doc_id = view.doc;
@@ -111,7 +110,7 @@ pub fn regex_prompt(
111 view.jumps.push((doc_id, snapshot.clone()));
110 view.jumps.push((doc_id, snapshot.clone()));
112 }
111 }
113
112
114− fun(cx.editor, regex, event);
113+ fun(cx, regex, event);
115
114
116 let (view, doc) = current!(cx.editor);
115 let (view, doc) = current!(cx.editor);
117 view.ensure_cursor_in_view(doc, config.scrolloff);
116 view.ensure_cursor_in_view(doc, config.scrolloff);
@@ -174,6 +173,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
174 .git_ignore(config.file_picker.git_ignore)
173 .git_ignore(config.file_picker.git_ignore)
175 .git_global(config.file_picker.git_global)
174 .git_global(config.file_picker.git_global)
176 .git_exclude(config.file_picker.git_exclude)
175 .git_exclude(config.file_picker.git_exclude)
176+ .sort_by_file_name(|name1, name2| name1.cmp(name2))
177 .max_depth(config.file_picker.max_depth)
177 .max_depth(config.file_picker.max_depth)
178 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
178 .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
179
179
@@ -190,32 +190,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
190 .build()
190 .build()
191 .expect("failed to build excluded_types");
191 .expect("failed to build excluded_types");
192 walk_builder.types(excluded_types);
192 walk_builder.types(excluded_types);
193−
194− // We want files along with their modification date for sorting
195 let files = walk_builder.build().filter_map(|entry| {
193 let files = walk_builder.build().filter_map(|entry| {
196 let entry = entry.ok()?;
194 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() {
195+ if !entry.file_type()?.is_file() {
199− Some(entry.into_path())
200− } else {
201− None
196+ return None;
202 }
197 }
198+ Some(entry.into_path())
203 });
199 });
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−
216 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
200 log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
217
201
218− Picker::new(files, root, move |cx, path: &PathBuf, action| {
202+ let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
219 if let Err(e) = cx.editor.open(path, action) {
203 if let Err(e) = cx.editor.open(path, action) {
220 let err = if let Some(err) = e.source() {
204 let err = if let Some(err) = e.source() {
221 format!("{}", err)
205 format!("{}", err)
@@ -225,20 +209,27 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
225 cx.editor.set_error(err);
209 cx.editor.set_error(err);
226 }
210 }
227 })
211 })
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
229}
222}
230
223
231pub mod completers {
224pub mod completers {
232 use crate::ui::prompt::Completion;
225 use crate::ui::prompt::Completion;
233− use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
226+ use helix_core::fuzzy::fuzzy_match;
234− use fuzzy_matcher::FuzzyMatcher;
235 use helix_core::syntax::LanguageServerFeature;
227 use helix_core::syntax::LanguageServerFeature;
236 use helix_view::document::SCRATCH_BUFFER_NAME;
228 use helix_view::document::SCRATCH_BUFFER_NAME;
237 use helix_view::theme;
229 use helix_view::theme;
238 use helix_view::{editor::Config, Editor};
230 use helix_view::{editor::Config, Editor};
239 use once_cell::sync::Lazy;
231 use once_cell::sync::Lazy;
240 use std::borrow::Cow;
232 use std::borrow::Cow;
241− use std::cmp::Reverse;
242
233
243 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
234 pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
244
235
@@ -247,31 +238,16 @@ pub mod completers {
247 }
238 }
248
239
249 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
240 pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
250− let mut names: Vec<_> = editor
241+ let names = editor.documents.values().map(|doc| {
251− .documents
252− .values()
253− .map(|doc| {
254− let name = doc
255− .relative_path()
242+ doc.relative_path()
256− .map(|p| p.display().to_string())
243+ .map(|p| p.display().to_string().into())
257− .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
244+ .unwrap_or_else(|| Cow::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− })
245+ });
269− .collect();
270−
271− matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
272− names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
273
246
274− names
247+ fuzzy_match(input, names, true)
248+ .into_iter()
249+ .map(|(name, _)| ((0..), name))
250+ .collect()
275 }
251 }
276
252
277 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
253 pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@@ -284,26 +260,10 @@ pub mod completers {
284 names.sort();
260 names.sort();
285 names.dedup();
261 names.dedup();
286
262
287− let mut names: Vec<_> = names
263+ fuzzy_match(input, names, false)
288 .into_iter()
264 .into_iter()
289− .map(|name| ((0..), Cow::from(name)))
265+ .map(|(name, _)| ((0..), name.into()))
290− .collect();
266+ .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
307 }
267 }
308
268
309 /// Recursive function to get all keys from this value and add them to vec
269 /// Recursive function to get all keys from this value and add them to vec
@@ -330,15 +290,7 @@ pub mod completers {
330 keys
290 keys
331 });
291 });
332
292
333− let matcher = Matcher::default();
293+ fuzzy_match(input, &*KEYS, false)
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
342 .into_iter()
294 .into_iter()
343 .map(|(name, _)| ((0..), name.into()))
295 .map(|(name, _)| ((0..), name.into()))
344 .collect()
296 .collect()
@@ -365,8 +317,6 @@ pub mod completers {
365 }
317 }
366
318
367 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
319 pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
368− let matcher = Matcher::default();
369−
370 let text: String = "text".into();
320 let text: String = "text".into();
371
321
372 let language_ids = editor
322 let language_ids = editor
@@ -375,27 +325,13 @@ pub mod completers {
375 .map(|config| &config.language_id)
325 .map(|config| &config.language_id)
376 .chain(std::iter::once(&text));
326 .chain(std::iter::once(&text));
377
327
378− let mut matches: Vec<_> = language_ids
379− .filter_map(|language_id| {
380− matcher
381− .fuzzy_match(language_id, input)
328+ fuzzy_match(input, language_ids, false)
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
391 .into_iter()
329 .into_iter()
392− .map(|(language, _score)| ((0..), language.clone().into()))
330+ .map(|(name, _)| ((0..), name.to_owned().into()))
393 .collect()
331 .collect()
394 }
332 }
395
333
396 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
334 pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
397− let matcher = Matcher::default();
398−
399 let Some(options) = doc!(editor)
335 let Some(options) = doc!(editor)
400 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
336 .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
401 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
337 .find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
@@ -403,23 +339,9 @@ pub mod completers {
403 return vec![];
339 return vec![];
404 };
340 };
405
341
406− let mut matches: Vec<_> = options
407− .commands
408− .iter()
409− .filter_map(|command| {
410− matcher
411− .fuzzy_match(command, input)
342+ fuzzy_match(input, &options.commands, false)
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
421 .into_iter()
343 .into_iter()
422− .map(|(command, _score)| ((0..), command.clone().into()))
344+ .map(|(name, _)| ((0..), name.to_owned().into()))
423 .collect()
345 .collect()
424 }
346 }
425
347
@@ -500,7 +422,7 @@ pub mod completers {
500
422
501 let end = input.len()..;
423 let end = input.len()..;
502
424
503− let mut files: Vec<_> = WalkBuilder::new(&dir)
425+ let files = WalkBuilder::new(&dir)
504 .hidden(false)
426 .hidden(false)
505 .follow_links(false) // We're scanning over depth 1
427 .follow_links(false) // We're scanning over depth 1
506 .git_ignore(git_ignore)
428 .git_ignore(git_ignore)
@@ -532,43 +454,25 @@ pub mod completers {
532 path.push("");
454 path.push("");
533 }
455 }
534
456
535− let path = path.to_str()?.to_owned();
457+ let path = path.into_os_string().into_string().ok()?;
536− Some((end.clone(), Cow::from(path)))
458+ Some(Cow::from(path))
537 })
459 })
538 }) // TODO: unwrap or skip
460 }) // TODO: unwrap or skip
539− .filter(|(_, path)| !path.is_empty()) // TODO
461+ .filter(|path| !path.is_empty());
540− .collect();
541
462
542 // if empty, return a list of dirs and files in current dir
463 // if empty, return a list of dirs and files in current dir
543 if let Some(file_name) = file_name {
464 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−
556 let range = (input.len().saturating_sub(file_name.len()))..;
465 let range = (input.len().saturating_sub(file_name.len()))..;
557−
466+ fuzzy_match(&file_name, files, true)
558− matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
559− (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
560− });
561−
562− files = matches
563 .into_iter()
467 .into_iter()
564− .map(|(file, _)| (range.clone(), file))
468+ .map(|(name, _)| (range.clone(), name))
565− .collect();
469+ .collect()
566
470
567 // TODO: complete to longest common match
471 // TODO: complete to longest common match
568 } else {
472 } else {
473+ let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
569 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
474 files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
475+ files
570 }
476 }
571−
572− files
573 }
477 }
574}
478}
helix-term/src/ui/picker.rs+267−278
@@ -7,11 +7,12 @@ use crate::{
7 ui::{
7 ui::{
8 self,
8 self,
9 document::{render_document, LineDecoration, LinePos, TextRenderer},
9 document::{render_document, LineDecoration, LinePos, TextRenderer},
10− fuzzy_match::FuzzyQuery,
11 EditorView,
10 EditorView,
12 },
11 },
13};
12};
14use futures_util::{future::BoxFuture, FutureExt};
13use futures_util::{future::BoxFuture, FutureExt};
14+use nucleo::pattern::CaseMatching;
15+use nucleo::{Config, Nucleo, Utf32String};
15use tui::{
16use tui::{
16 buffer::Buffer as Surface,
17 buffer::Buffer as Surface,
17 layout::Constraint,
18 layout::Constraint,
@@ -19,16 +20,23 @@ use tui::{
19 widgets::{Block, BorderType, Borders, Cell, Table},
20 widgets::{Block, BorderType, Borders, Cell, Table},
20};
21};
21
22
22−use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
23use tui::widgets::Widget;
23use tui::widgets::Widget;
24
24
25−use std::cmp::{self, Ordering};
25+use std::{
26−use std::{collections::HashMap, io::Read, path::PathBuf};
26+ collections::HashMap,
27+ io::Read,
28+ path::PathBuf,
29+ sync::{
30+ atomic::{self, AtomicBool},
31+ Arc,
32+ },
33+};
27
34
28use crate::ui::{Prompt, PromptEvent};
35use crate::ui::{Prompt, PromptEvent};
29use helix_core::{
36use helix_core::{
30− char_idx_at_visual_offset, movement::Direction, text_annotations::TextAnnotations,
37+ char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction,
31− unicode::segmentation::UnicodeSegmentation, Position, Syntax,
38+ text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position,
39+ Syntax,
32};
40};
33use helix_view::{
41use helix_view::{
34 editor::Action,
42 editor::Action,
@@ -114,20 +122,71 @@ impl Preview<'_, '_> {
114 }
122 }
115}
123}
116
124
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+
117pub struct Picker<T: Item> {
178pub struct Picker<T: Item> {
118− options: Vec<T>,
119− editor_data: T::Data,
179+ editor_data: Arc<T::Data>,
120− // filter: String,
180+ shutdown: Arc<AtomicBool>,
121− matcher: Box<Matcher>,
181+ matcher: Nucleo<T>,
122− matches: Vec<PickerMatch>,
123
182
124 /// Current height of the completions box
183 /// Current height of the completions box
125 completion_height: u16,
184 completion_height: u16,
126
185
127− cursor: usize,
186+ cursor: u32,
128− // pattern: String,
129 prompt: Prompt,
187 prompt: Prompt,
130− previous_pattern: (String, FuzzyQuery),
188+ previous_pattern: String,
189+
131 /// Whether to show the preview panel (default true)
190 /// Whether to show the preview panel (default true)
132 show_preview: bool,
191 show_preview: bool,
133 /// Constraints for tabular formatting
192 /// Constraints for tabular formatting
@@ -144,10 +203,59 @@ pub struct Picker<T: Item> {
144}
203}
145
204
146impl<T: Item + 'static> Picker<T> {
205impl<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+
147 pub fn new(
221 pub fn new(
148 options: Vec<T>,
222 options: Vec<T>,
149 editor_data: T::Data,
223 editor_data: T::Data,
150 callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
224 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,
151 ) -> Self {
259 ) -> Self {
152 let prompt = Prompt::new(
260 let prompt = Prompt::new(
153 "".into(),
261 "".into(),
@@ -156,14 +264,13 @@ impl<T: Item + 'static> Picker<T> {
156 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
264 |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {},
157 );
265 );
158
266
159− let mut picker = Self {
267+ Self {
160− options,
268+ matcher,
161 editor_data,
269 editor_data,
162− matcher: Box::default(),
270+ shutdown,
163− matches: Vec::new(),
164 cursor: 0,
271 cursor: 0,
165 prompt,
272 prompt,
166− previous_pattern: (String::new(), FuzzyQuery::default()),
273+ previous_pattern: String::new(),
167 truncate_start: true,
274 truncate_start: true,
168 show_preview: true,
275 show_preview: true,
169 callback_fn: Box::new(callback_fn),
276 callback_fn: Box::new(callback_fn),
@@ -172,24 +279,15 @@ impl<T: Item + 'static> Picker<T> {
172 preview_cache: HashMap::new(),
279 preview_cache: HashMap::new(),
173 read_buffer: Vec::with_capacity(1024),
280 read_buffer: Vec::with_capacity(1024),
174 file_fn: None,
281 file_fn: None,
175− };
282+ }
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− }));
283+ }
191
284
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+ }
193 }
291 }
194
292
195 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
293 pub fn truncate_start(mut self, truncate_start: bool) -> Self {
@@ -202,122 +300,25 @@ impl<T: Item + 'static> Picker<T> {
202 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
300 preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
203 ) -> Self {
301 ) -> Self {
204 self.file_fn = Some(Box::new(preview_fn));
302 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());
205 self
306 self
206 }
307 }
207
308
208 pub fn set_options(&mut self, new_options: Vec<T>) {
309 pub fn set_options(&mut self, new_options: Vec<T>) {
310+ self.matcher.restart(false);
311+ let injector = self.matcher.injector();
209− self.options = new_options;
312+ for item in new_options {
210− self.cursor = 0;
313+ if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) {
211− self.force_score();
314+ injector.push(item, |dst| dst[0] = matcher_text);
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− }
231 }
315 }
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;
245 }
316 }
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();
316 }
317 }
317
318
318 /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`)
319 /// 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+ pub fn move_by(&mut self, amount: u32, direction: Direction) {
320− let len = self.matches.len();
321+ let len = self.matcher.snapshot().matched_item_count();
321
322
322 if len == 0 {
323 if len == 0 {
323 // No results, can't move.
324 // No results, can't move.
@@ -336,12 +337,12 @@ impl<T: Item + 'static> Picker<T> {
336
337
337 /// Move the cursor down by exactly one page. After the last page comes the first page.
338 /// Move the cursor down by exactly one page. After the last page comes the first page.
338 pub fn page_up(&mut self) {
339 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);
340 }
341 }
341
342
342 /// Move the cursor up by exactly one page. After the first page comes the last page.
343 /// Move the cursor up by exactly one page. After the first page comes the last page.
343 pub fn page_down(&mut self) {
344 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);
345 }
346 }
346
347
347 /// Move the cursor to the first entry
348 /// Move the cursor to the first entry
@@ -351,13 +352,18 @@ impl<T: Item + 'static> Picker<T> {
351
352
352 /// Move the cursor to the last entry
353 /// Move the cursor to the last entry
353 pub fn to_end(&mut self) {
354 pub fn to_end(&mut self) {
355+ self.cursor = self
356+ .matcher
357+ .snapshot()
358+ .matched_item_count()
354− self.cursor = self.matches.len().saturating_sub(1);
359+ .saturating_sub(1);
355 }
360 }
356
361
357 pub fn selection(&self) -> Option<&T> {
362 pub fn selection(&self) -> Option<&T> {
358− self.matches
363+ self.matcher
364+ .snapshot()
359− .get(self.cursor)
365+ .get_matched_item(self.cursor)
360− .map(|pmatch| &self.options[pmatch.index])
366+ .map(|item| item.data)
361 }
367 }
362
368
363 pub fn toggle_preview(&mut self) {
369 pub fn toggle_preview(&mut self) {
@@ -366,8 +372,17 @@ impl<T: Item + 'static> Picker<T> {
366
372
367 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
373 fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
368 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
374 if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
375+ let pattern = self.prompt.line();
369− // TODO: recalculate only if pattern changed
376+ // TODO: better track how the pattern has changed
370− self.score();
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+ }
371 }
386 }
372 EventResult::Consumed(None)
387 EventResult::Consumed(None)
373 }
388 }
@@ -411,12 +426,9 @@ impl<T: Item + 'static> Picker<T> {
411 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
426 (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => {
412 CachedPreview::LargeFile
427 CachedPreview::LargeFile
413 }
428 }
414− _ => {
415− // TODO: enable syntax highlighting; blocked by async rendering
416− Document::open(path, None, None, editor.config.clone())
429+ _ => Document::open(path, None, None, editor.config.clone())
417− .map(|doc| CachedPreview::Document(Box::new(doc)))
430+ .map(|doc| CachedPreview::Document(Box::new(doc)))
418− .unwrap_or(CachedPreview::NotFound)
431+ .unwrap_or(CachedPreview::NotFound),
419− }
420 },
432 },
421 )
433 )
422 .unwrap_or(CachedPreview::NotFound);
434 .unwrap_or(CachedPreview::NotFound);
@@ -495,6 +507,14 @@ impl<T: Item + 'static> Picker<T> {
495 }
507 }
496
508
497 fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
509 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+
498 let text_style = cx.editor.theme.get("ui.text");
518 let text_style = cx.editor.theme.get("ui.text");
499 let selected = cx.editor.theme.get("ui.text.focus");
519 let selected = cx.editor.theme.get("ui.text.focus");
500 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
520 let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -515,8 +535,15 @@ impl<T: Item + 'static> Picker<T> {
515 // -- Render the input bar:
535 // -- Render the input bar:
516
536
517 let area = inner.clip_left(1).with_height(1);
537 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);
518
540
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+ );
520 surface.set_stringn(
547 surface.set_stringn(
521 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
548 (area.x + area.width).saturating_sub(count.len() as u16 + 1),
522 area.y,
549 area.y,
@@ -525,8 +552,6 @@ impl<T: Item + 'static> Picker<T> {
525 text_style,
552 text_style,
526 );
553 );
527
554
528− self.prompt.render(area, surface, cx);
529−
530 // -- Separator
555 // -- Separator
531 let sep_style = cx.editor.theme.get("ui.background.separator");
556 let sep_style = cx.editor.theme.get("ui.background.separator");
532 let borders = BorderType::line_symbols(BorderType::Plain);
557 let borders = BorderType::line_symbols(BorderType::Plain);
@@ -539,106 +564,89 @@ impl<T: Item + 'static> Picker<T> {
539 // -- Render the contents:
564 // -- Render the contents:
540 // subtract area of prompt from top
565 // subtract area of prompt from top
541 let inner = inner.clip_top(2);
566 let inner = inner.clip_top(2);
542−
543− let rows = inner.height;
567+ let rows = inner.height as u32;
544− let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
568+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows));
545 let cursor = self.cursor.saturating_sub(offset);
569 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+ }
546
579
547− let options = self
580+ let options = snapshot.matched_items(offset..end).map(|item| {
548− .matches
581+ snapshot.pattern().column_pattern(0).indices(
549− .iter()
582+ item.matcher_columns[0].slice(..),
550− .skip(offset)
583+ &mut matcher,
551− .take(rows as usize)
584+ &mut indices,
552− .map(|pmatch| &self.options[pmatch.index])
585+ );
586+ indices.sort_unstable();
587+ indices.dedup();
553− .map(|option| option.format(&self.editor_data))
588+ let mut row = item.data.format(&self.editor_data);
554− .map(|mut row| {
589+
555− const TEMP_CELL_SEP: &str = " ";
590+ let mut grapheme_idx = 0u32;
556−
591+ let mut indices = indices.drain(..);
557− let line = row.cell_text().fold(String::new(), |mut s, frag| {
592+ let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX);
558− s.push_str(&frag);
593+ if self.widths.len() < row.cells.len() {
559− s.push_str(TEMP_CELL_SEP);
594+ self.widths.resize(row.cells.len(), Constraint::Length(0));
560− s
595+ }
561− });
596+ let mut widths = self.widths.iter_mut();
562−
597+ for cell in &mut row.cells {
563− // Items are filtered by using the text returned by menu::Item::filter_text
598+ let Some(Constraint::Length(max_width)) = widths.next() else {
564− // but we do highlighting here using the text in Row and therefore there
599+ unreachable!();
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− };
600+ };
591
601
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();
592− let mut cell_len = 0;
606+ let mut width = 0;
593−
607+
594− let graphemes_with_style: Vec<_> = spans
608+ let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice());
595− .0
609+ for span in spans {
596− .iter()
610+ // this looks like a bug on first glance, we are iterating
597− .flat_map(|span| {
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
598− span.content
615+ for grapheme in span.content.graphemes(true) {
599− .grapheme_indices(true)
616+ let style = if grapheme_idx == next_highlight_idx {
600− .zip(std::iter::repeat(span.style))
617+ next_highlight_idx = indices.next().unwrap_or(u32::MAX);
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))
618+ span.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);
625 } else {
619 } 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;
627 }
628 }
629+ current_span.push_str(grapheme);
630+ grapheme_idx += 1;
628 }
631 }
632+ width += span.width();
633+ }
629
634
630− let spans: Vec<Span> = span_list
635+ span_list.push(Span::styled(current_span, current_style));
631− .into_iter()
636+ if width as u16 > *max_width {
632− .map(|(string, style)| Span::styled(string, style))
637+ *max_width = width as u16;
633− .collect();
638+ }
634− let spans: Spans = spans.into();
635− *cell = Cell::from(spans);
639+ *cell = Cell::from(Spans::from(span_list));
636
640
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);
638 }
644 }
645+ grapheme_idx += 1;
646+ }
639
647
640− row
648+ row
641− });
649+ });
642
650
643 let table = Table::new(options)
651 let table = Table::new(options)
644 .style(text_style)
652 .style(text_style)
@@ -654,7 +662,7 @@ impl<T: Item + 'static> Picker<T> {
654 surface,
662 surface,
655 &mut TableState {
663 &mut TableState {
656 offset: 0,
664 offset: 0,
657− selected: Some(cursor),
665+ selected: Some(cursor as usize),
658 },
666 },
659 self.truncate_start,
667 self.truncate_start,
660 );
668 );
@@ -755,7 +763,7 @@ impl<T: Item + 'static> Picker<T> {
755 }
763 }
756}
764}
757
765
758−impl<T: Item + 'static> Component for Picker<T> {
766+impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
759 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
767 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
760 // +---------+ +---------+
768 // +---------+ +---------+
761 // |prompt | |preview |
769 // |prompt | |preview |
@@ -875,29 +883,10 @@ impl<T: Item + 'static> Component for Picker<T> {
875 Some((width, height))
883 Some((width, height))
876 }
884 }
877}
885}
878−
886+impl<T: Item> Drop for Picker<T> {
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 {
887+ fn drop(&mut self) {
888− (cmp::Reverse(self.score), self.len, self.index)
888+ // ensure we cancel any ongoing background threads streaming into the picker
889− }
889+ self.shutdown.store(true, atomic::Ordering::Relaxed)
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())
901 }
890 }
902}
891}
903
892
@@ -910,13 +899,13 @@ pub type DynQueryCallback<T> =
910
899
911/// A picker that updates its contents via a callback whenever the
900/// A picker that updates its contents via a callback whenever the
912/// query string changes. Useful for live grep, workspace symbols, etc.
901/// 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> {
914 file_picker: Picker<T>,
903 file_picker: Picker<T>,
915 query_callback: DynQueryCallback<T>,
904 query_callback: DynQueryCallback<T>,
916 query: String,
905 query: String,
917}
906}
918
907
919−impl<T: ui::menu::Item + Send> DynamicPicker<T> {
908+impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
920 pub const ID: &'static str = "dynamic-picker";
909 pub const ID: &'static str = "dynamic-picker";
921
910
922 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
911 pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
@@ -928,7 +917,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
928 }
917 }
929}
918}
930
919
931−impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
920+impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
932 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
921 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
933 self.file_picker.render(area, surface, cx);
922 self.file_picker.render(area, surface, cx);
934 }
923 }
helix-term/tests/integration.rs−1
@@ -20,7 +20,6 @@ mod test {
20 mod commands;
20 mod commands;
21 mod languages;
21 mod languages;
22 mod movement;
22 mod movement;
23− mod picker;
24 mod prompt;
23 mod prompt;
25 mod splits;
24 mod splits;
26}
25}
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"
12
12
13[dependencies]
13[dependencies]
14helix-core = { version = "0.6", path = "../helix-core" }
14helix-core = { version = "0.6", path = "../helix-core" }
15+helix-event = { version = "0.6", path = "../helix-event" }
15
16
16tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
17tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
17parking_lot = "0.12"
18parking_lot = "0.12"
helix-vcs/src/diff.rs+6−19
@@ -2,10 +2,10 @@ use std::ops::Range;
2use std::sync::Arc;
2use std::sync::Arc;
3
3
4use helix_core::Rope;
4use helix_core::Rope;
5+use helix_event::RenderLockGuard;
5use imara_diff::Algorithm;
6use imara_diff::Algorithm;
6use parking_lot::{Mutex, MutexGuard};
7use parking_lot::{Mutex, MutexGuard};
7use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8use tokio::sync::mpsc::{unbounded_channel, UnboundedSender};
8−use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock};
9use tokio::task::JoinHandle;
9use tokio::task::JoinHandle;
10use tokio::time::Instant;
10use tokio::time::Instant;
11
11
@@ -14,11 +14,9 @@ use crate::diff::worker::DiffWorker;
14mod line_cache;
14mod line_cache;
15mod worker;
15mod worker;
16
16
17−type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
18−
19/// A rendering lock passed to the differ the prevents redraws from occurring
17/// A rendering lock passed to the differ the prevents redraws from occurring
20struct RenderLock {
18struct RenderLock {
21− pub lock: OwnedRwLockReadGuard<()>,
19+ pub lock: RenderLockGuard,
22 pub timeout: Option<Instant>,
20 pub timeout: Option<Instant>,
23}
21}
24
22
@@ -38,28 +36,22 @@ struct DiffInner {
38#[derive(Clone, Debug)]
36#[derive(Clone, Debug)]
39pub struct DiffHandle {
37pub struct DiffHandle {
40 channel: UnboundedSender<Event>,
38 channel: UnboundedSender<Event>,
41− render_lock: Arc<RwLock<()>>,
42 diff: Arc<Mutex<DiffInner>>,
39 diff: Arc<Mutex<DiffInner>>,
43 inverted: bool,
40 inverted: bool,
44}
41}
45
42
46impl DiffHandle {
43impl DiffHandle {
47− pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle {
44+ pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle {
48− DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0
45+ DiffHandle::new_with_handle(diff_base, doc).0
49 }
46 }
50
47
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<()>) {
56 let (sender, receiver) = unbounded_channel();
49 let (sender, receiver) = unbounded_channel();
57 let diff: Arc<Mutex<DiffInner>> = Arc::default();
50 let diff: Arc<Mutex<DiffInner>> = Arc::default();
58 let worker = DiffWorker {
51 let worker = DiffWorker {
59 channel: receiver,
52 channel: receiver,
60 diff: diff.clone(),
53 diff: diff.clone(),
61 new_hunks: Vec::default(),
54 new_hunks: Vec::default(),
62− redraw_notify: redraw_handle.0,
63 diff_finished_notify: Arc::default(),
55 diff_finished_notify: Arc::default(),
64 };
56 };
65 let handle = tokio::spawn(worker.run(diff_base, doc));
57 let handle = tokio::spawn(worker.run(diff_base, doc));
@@ -67,7 +59,6 @@ impl DiffHandle {
67 channel: sender,
59 channel: sender,
68 diff,
60 diff,
69 inverted: false,
61 inverted: false,
70− render_lock: redraw_handle.1,
71 };
62 };
72 (differ, handle)
63 (differ, handle)
73 }
64 }
@@ -87,11 +78,7 @@ impl DiffHandle {
87 /// This function is only intended to be called from within the rendering loop
78 /// This function is only intended to be called from within the rendering loop
88 /// if called from elsewhere it may fail to acquire the render lock and panic
79 /// if called from elsewhere it may fail to acquire the render lock and panic
89 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
80 pub fn update_document(&self, doc: Rope, block: bool) -> bool {
90− // unwrap is ok here because the rendering lock is
81+ let lock = helix_event::lock_frame();
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();
95 let timeout = if block {
82 let timeout = if block {
96 None
83 None
97 } else {
84 } else {
helix-vcs/src/diff/worker.rs+3−9
@@ -23,7 +23,6 @@ pub(super) struct DiffWorker {
23 pub channel: UnboundedReceiver<Event>,
23 pub channel: UnboundedReceiver<Event>,
24 pub diff: Arc<Mutex<DiffInner>>,
24 pub diff: Arc<Mutex<DiffInner>>,
25 pub new_hunks: Vec<Hunk>,
25 pub new_hunks: Vec<Hunk>,
26− pub redraw_notify: Arc<Notify>,
27 pub diff_finished_notify: Arc<Notify>,
26 pub diff_finished_notify: Arc<Notify>,
28}
27}
29
28
@@ -32,11 +31,7 @@ impl DiffWorker {
32 let mut accumulator = EventAccumulator::new();
31 let mut accumulator = EventAccumulator::new();
33 accumulator.handle_event(event).await;
32 accumulator.handle_event(event).await;
34 accumulator
33 accumulator
35− .accumulate_debounced_events(
36− &mut self.channel,
37− self.redraw_notify.clone(),
38− self.diff_finished_notify.clone(),
34+ .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone())
39− )
40 .await;
35 .await;
41 (accumulator.doc, accumulator.diff_base)
36 (accumulator.doc, accumulator.diff_base)
42 }
37 }
@@ -137,7 +132,6 @@ impl<'a> EventAccumulator {
137 async fn accumulate_debounced_events(
132 async fn accumulate_debounced_events(
138 &mut self,
133 &mut self,
139 channel: &mut UnboundedReceiver<Event>,
134 channel: &mut UnboundedReceiver<Event>,
140− redraw_notify: Arc<Notify>,
141 diff_finished_notify: Arc<Notify>,
135 diff_finished_notify: Arc<Notify>,
142 ) {
136 ) {
143 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
137 let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC);
@@ -164,7 +158,7 @@ impl<'a> EventAccumulator {
164 None => {
158 None => {
165 tokio::spawn(async move {
159 tokio::spawn(async move {
166 diff_finished_notify.notified().await;
160 diff_finished_notify.notified().await;
167− redraw_notify.notify_one();
161+ helix_event::request_redraw();
168 });
162 });
169 }
163 }
170 // diff is performed inside the rendering loop
164 // diff is performed inside the rendering loop
@@ -190,7 +184,7 @@ impl<'a> EventAccumulator {
190 // and wait until the diff occurs to trigger an async redraw
184 // and wait until the diff occurs to trigger an async redraw
191 log::info!("Diff computation timed out, update of diffs might appear delayed");
185 log::info!("Diff computation timed out, update of diffs might appear delayed");
192 diff_finished_notify.notified().await;
186 diff_finished_notify.notified().await;
193− redraw_notify.notify_one();
187+ helix_event::request_redraw()
194 });
188 });
195 }
189 }
196 // a blocking diff is performed inside the rendering loop
190 // 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};
5
5
6impl DiffHandle {
6impl DiffHandle {
7 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
7 fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) {
8− DiffHandle::new_with_handle(
8+ DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc))
9− Rope::from_str(diff_base),
10− Rope::from_str(doc),
11− Default::default(),
12− )
13 }
9 }
14 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
10 async fn into_diff(self, handle: JoinHandle<()>) -> Vec<Hunk> {
15 let diff = self.diff;
11 let diff = self.diff;
helix-view/Cargo.toml+1
@@ -17,6 +17,7 @@ term = ["crossterm"]
17bitflags = "2.4"
17bitflags = "2.4"
18anyhow = "1"
18anyhow = "1"
19helix-core = { version = "0.6", path = "../helix-core" }
19helix-core = { version = "0.6", path = "../helix-core" }
20+helix-event = { version = "0.6", path = "../helix-event" }
20helix-loader = { version = "0.6", path = "../helix-loader" }
21helix-loader = { version = "0.6", path = "../helix-loader" }
21helix-lsp = { version = "0.6", path = "../helix-lsp" }
22helix-lsp = { version = "0.6", path = "../helix-lsp" }
22helix-dap = { version = "0.6", path = "../helix-dap" }
23helix-dap = { version = "0.6", path = "../helix-dap" }
helix-view/src/document.rs+4−5
@@ -33,7 +33,7 @@ use helix_core::{
33 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
33 ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
34};
34};
35
35
36−use crate::editor::{Config, RedrawHandle};
36+use crate::editor::Config;
37use crate::{DocumentId, Editor, Theme, View, ViewId};
37use crate::{DocumentId, Editor, Theme, View, ViewId};
38
38
39/// 8kB of buffer space for encoding and decoding `Rope`s.
39/// 8kB of buffer space for encoding and decoding `Rope`s.
@@ -995,7 +995,6 @@ impl Document {
995 &mut self,
995 &mut self,
996 view: &mut View,
996 view: &mut View,
997 provider_registry: &DiffProviderRegistry,
997 provider_registry: &DiffProviderRegistry,
998− redraw_handle: RedrawHandle,
999 ) -> Result<(), Error> {
998 ) -> Result<(), Error> {
1000 let encoding = self.encoding;
999 let encoding = self.encoding;
1001 let path = self
1000 let path = self
@@ -1023,7 +1022,7 @@ impl Document {
1023 self.detect_indent_and_line_ending();
1022 self.detect_indent_and_line_ending();
1024
1023
1025 match provider_registry.get_diff_base(&path) {
1024 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),
1027 None => self.diff_handle = None,
1026 None => self.diff_handle = None,
1028 }
1027 }
1029
1028
@@ -1583,13 +1582,13 @@ impl Document {
1583 }
1582 }
1584
1583
1585 /// Intialize/updates the differ for this document with a new base.
1584 /// 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>) {
1587 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
1586 if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
1588 if let Some(differ) = &self.diff_handle {
1587 if let Some(differ) = &self.diff_handle {
1589 differ.update_diff_base(diff_base);
1588 differ.update_diff_base(diff_base);
1590 return;
1589 return;
1591 }
1590 }
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()))
1593 } else {
1592 } else {
1594 self.diff_handle = None;
1593 self.diff_handle = None;
1595 }
1594 }
helix-view/src/editor.rs+3−10
@@ -32,7 +32,7 @@ use std::{
32use tokio::{
32use tokio::{
33 sync::{
33 sync::{
34 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
34 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
35− oneshot, Notify, RwLock,
35+ oneshot,
36 },
36 },
37 time::{sleep, Duration, Instant, Sleep},
37 time::{sleep, Duration, Instant, Sleep},
38};
38};
@@ -925,10 +925,6 @@ pub struct Editor {
925 pub exit_code: i32,
925 pub exit_code: i32,
926
926
927 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
927 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,
932 pub needs_redraw: bool,
928 pub needs_redraw: bool,
933 /// Cached position of the cursor calculated during rendering.
929 /// Cached position of the cursor calculated during rendering.
934 /// The content of `cursor_cache` is returned by `Editor::cursor` if
930 /// The content of `cursor_cache` is returned by `Editor::cursor` if
@@ -955,8 +951,6 @@ pub struct Editor {
955
951
956pub type Motion = Box<dyn Fn(&mut Editor)>;
952pub type Motion = Box<dyn Fn(&mut Editor)>;
957
953
958−pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
959−
960#[derive(Debug)]
954#[derive(Debug)]
961pub enum EditorEvent {
955pub enum EditorEvent {
962 DocumentSaved(DocumentSavedEventResult),
956 DocumentSaved(DocumentSavedEventResult),
@@ -1062,7 +1056,6 @@ impl Editor {
1062 auto_pairs,
1056 auto_pairs,
1063 exit_code: 0,
1057 exit_code: 0,
1064 config_events: unbounded_channel(),
1058 config_events: unbounded_channel(),
1065− redraw_handle: Default::default(),
1066 needs_redraw: false,
1059 needs_redraw: false,
1067 cursor_cache: Cell::new(None),
1060 cursor_cache: Cell::new(None),
1068 completion_request_handle: None,
1061 completion_request_handle: None,
@@ -1453,7 +1446,7 @@ impl Editor {
1453 )?;
1446 )?;
1454
1447
1455 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1448 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);
1457 }
1450 }
1458 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1451 doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
1459
1452
@@ -1752,7 +1745,7 @@ impl Editor {
1752 return EditorEvent::DebuggerEvent(event)
1745 return EditorEvent::DebuggerEvent(event)
1753 }
1746 }
1754
1747
1755− _ = self.redraw_handle.0.notified() => {
1748+ _ = helix_event::redraw_requested() => {
1756 if !self.needs_redraw{
1749 if !self.needs_redraw{
1757 self.needs_redraw = true;
1750 self.needs_redraw = true;
1758 let timeout = Instant::now() + Duration::from_millis(33);
1751 let timeout = Instant::now() + Duration::from_millis(33);