From b30d0daabf7340f3186687e98c2a09825b174c0b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Aug 2021 14:55:27 -0700 Subject: [PATCH] Add a theme picker Co-Authored-By: Nathan Sobo --- gpui/examples/text.rs | 2 +- gpui/src/app.rs | 71 +- gpui/src/assets.rs | 7 +- gpui/src/elements/uniform_list.rs | 16 +- server/src/tests.rs | 2 +- zed/assets/themes/{base.toml => _base.toml} | 0 zed/assets/themes/dark.toml | 2 +- zed/assets/themes/light.toml | 21 + zed/src/assets.rs | 4 + zed/src/editor.rs | 6 +- zed/src/file_finder.rs | 32 +- zed/src/lib.rs | 19 +- zed/src/main.rs | 11 +- zed/src/settings.rs | 47 +- zed/src/test.rs | 4 +- zed/src/theme_picker.rs | 308 +++++++ zed/src/workspace.rs | 16 +- zed/src/workspace/pane.rs | 5 +- zed/src/worktree.rs | 8 +- zed/src/worktree/fuzzy.rs | 838 +++++++++++--------- 20 files changed, 982 insertions(+), 437 deletions(-) rename zed/assets/themes/{base.toml => _base.toml} (100%) create mode 100644 zed/assets/themes/light.toml create mode 100644 zed/src/theme_picker.rs diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 11c327e2bb..58314b5e94 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -28,7 +28,7 @@ impl gpui::View for TextView { "View" } - fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + fn render(&self, _: &gpui::RenderContext) -> gpui::ElementBox { TextElement.boxed() } } diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 05e5ee15c4..6d857952e2 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -36,9 +36,9 @@ pub trait Entity: 'static + Send + Sync { fn release(&mut self, _: &mut MutableAppContext) {} } -pub trait View: Entity { +pub trait View: Entity + Sized { fn ui_name() -> &'static str; - fn render<'a>(&self, cx: &AppContext) -> ElementBox; + fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox; fn on_focus(&mut self, _: &mut ViewContext) {} fn on_blur(&mut self, _: &mut ViewContext) {} fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -1503,7 +1503,7 @@ impl AppContext { pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { self.views .get(&(window_id, view_id)) - .map(|v| v.render(self)) + .map(|v| v.render(window_id, view_id, self)) .ok_or(anyhow!("view not found")) } @@ -1512,7 +1512,7 @@ impl AppContext { .iter() .filter_map(|((win_id, view_id), view)| { if *win_id == window_id { - Some((*view_id, view.render(self))) + Some((*view_id, view.render(*win_id, *view_id, self))) } else { None } @@ -1650,7 +1650,7 @@ pub trait AnyView: Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); fn ui_name(&self) -> &'static str; - fn render<'a>(&self, cx: &AppContext) -> ElementBox; + fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; @@ -1676,8 +1676,16 @@ where T::ui_name() } - fn render<'a>(&self, cx: &AppContext) -> ElementBox { - View::render(self, cx) + fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox { + View::render( + self, + &RenderContext { + window_id, + view_id, + app: cx, + view_type: PhantomData::, + }, + ) } fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) { @@ -2094,12 +2102,33 @@ impl<'a, T: View> ViewContext<'a, T> { } } +pub struct RenderContext<'a, T: View> { + pub app: &'a AppContext, + window_id: usize, + view_id: usize, + view_type: PhantomData, +} + +impl<'a, T: View> RenderContext<'a, T> { + pub fn handle(&self) -> WeakViewHandle { + WeakViewHandle::new(self.window_id, self.view_id) + } +} + impl AsRef for &AppContext { fn as_ref(&self) -> &AppContext { self } } +impl Deref for RenderContext<'_, V> { + type Target = AppContext; + + fn deref(&self) -> &Self::Target { + &self.app + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx @@ -3004,7 +3033,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3067,7 +3096,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { let mouse_down_count = self.mouse_down_count.clone(); EventHandler::new(Empty::new().boxed()) .on_mouse_down(move |_| { @@ -3129,7 +3158,7 @@ mod tests { "View" } - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3169,7 +3198,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3222,7 +3251,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3272,7 +3301,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3315,7 +3344,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3362,7 +3391,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3420,7 +3449,7 @@ mod tests { } impl View for ViewA { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3438,7 +3467,7 @@ mod tests { } impl View for ViewB { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3541,7 +3570,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3674,7 +3703,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3719,7 +3748,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3742,7 +3771,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } diff --git a/gpui/src/assets.rs b/gpui/src/assets.rs index 63c6c07570..ac0d72dee9 100644 --- a/gpui/src/assets.rs +++ b/gpui/src/assets.rs @@ -1,8 +1,9 @@ use anyhow::{anyhow, Result}; use std::{borrow::Cow, cell::RefCell, collections::HashMap}; -pub trait AssetSource: 'static { +pub trait AssetSource: 'static + Send + Sync { fn load(&self, path: &str) -> Result>; + fn list(&self, path: &str) -> Vec>; } impl AssetSource for () { @@ -12,6 +13,10 @@ impl AssetSource for () { path )) } + + fn list(&self, _: &str) -> Vec> { + vec![] + } } pub struct AssetCache { diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index b414a20430..74ebccdf37 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -13,17 +13,10 @@ use json::ToJson; use parking_lot::Mutex; use std::{cmp, ops::Range, sync::Arc}; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct UniformListState(Arc>); impl UniformListState { - pub fn new() -> Self { - Self(Arc::new(Mutex::new(StateInner { - scroll_top: 0.0, - scroll_to: None, - }))) - } - pub fn scroll_to(&self, item_ix: usize) { self.0.lock().scroll_to = Some(item_ix); } @@ -33,6 +26,7 @@ impl UniformListState { } } +#[derive(Default)] struct StateInner { scroll_top: f32, scroll_to: Option, @@ -57,11 +51,11 @@ impl UniformList where F: Fn(Range, &mut Vec, &AppContext), { - pub fn new(state: UniformListState, item_count: usize, build_items: F) -> Self { + pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { state, item_count, - append_items: build_items, + append_items, } } @@ -79,7 +73,7 @@ where let mut state = self.state.0.lock(); state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max); - cx.dispatch_action("uniform_list:scroll", state.scroll_top); + cx.notify(); true } diff --git a/server/src/tests.rs b/server/src/tests.rs index 8767155ca0..66d9047467 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -607,7 +607,7 @@ impl gpui::View for EmptyView { "empty view" } - fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { gpui::Element::boxed(gpui::elements::Empty) } } diff --git a/zed/assets/themes/base.toml b/zed/assets/themes/_base.toml similarity index 100% rename from zed/assets/themes/base.toml rename to zed/assets/themes/_base.toml diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 2376293c8a..fdf5a5adee 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,4 +1,4 @@ -extends = "base" +extends = "_base" [variables] elevation_1 = 0x050101 diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml new file mode 100644 index 0000000000..77757ac260 --- /dev/null +++ b/zed/assets/themes/light.toml @@ -0,0 +1,21 @@ +extends = "_base" + +[variables] +elevation_1 = 0xffffff +elevation_2 = 0xf3f3f3 +elevation_3 = 0xececec +elevation_4 = 0x3a3b3c +text_dull = 0xacacac +text_bright = 0x111111 +text_normal = 0x333333 + +[syntax] +keyword = 0x0000fa +function = 0x795e26 +string = 0xa82121 +type = 0x267f29 +number = 0xb5cea8 +comment = 0x6a9955 +property = 0x4e94ce +variant = 0x4fc1ff +constant = 0x9cdcfe diff --git a/zed/src/assets.rs b/zed/src/assets.rs index 072a1a0496..e7c0103421 100644 --- a/zed/src/assets.rs +++ b/zed/src/assets.rs @@ -10,4 +10,8 @@ impl AssetSource for Assets { fn load(&self, path: &str) -> Result> { Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } + + fn list(&self, path: &str) -> Vec> { + Self::iter().filter(|p| p.starts_with(path)).collect() + } } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 3911970249..705afaeb20 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -18,8 +18,8 @@ pub use element::*; use gpui::{ color::ColorU, font_cache::FamilyId, fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, Task, TextLayoutCache, View, - ViewContext, WeakViewHandle, + ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task, + TextLayoutCache, View, ViewContext, WeakViewHandle, }; use postage::watch; use serde::{Deserialize, Serialize}; @@ -2533,7 +2533,7 @@ impl Entity for Editor { } impl View for Editor { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { EditorElement::new(self.handle.clone()).boxed() } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index d7f0f8fff1..794b40face 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -11,8 +11,8 @@ use gpui::{ fonts::{Properties, Weight}, geometry::vector::vec2f, keymap::{self, Binding}, - AppContext, Axis, Border, Entity, MutableAppContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + AppContext, Axis, Border, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use postage::watch; use std::{ @@ -45,7 +45,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action("file_finder:select", FileFinder::select); cx.add_action("menu:select_prev", FileFinder::select_prev); cx.add_action("menu:select_next", FileFinder::select_next); - cx.add_action("uniform_list:scroll", FileFinder::scroll); cx.add_bindings(vec![ Binding::new("cmd-p", "file_finder:toggle", None), @@ -68,7 +67,7 @@ impl View for FileFinder { "FileFinder" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( @@ -267,31 +266,30 @@ impl FileFinder { }) } - fn toggle(workspace_view: &mut Workspace, _: &(), cx: &mut ViewContext) { - workspace_view.toggle_modal(cx, |cx, workspace_view| { - let workspace = cx.handle(); - let finder = - cx.add_view(|cx| Self::new(workspace_view.settings.clone(), workspace, cx)); + fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext) { + workspace.toggle_modal(cx, |cx, workspace| { + let handle = cx.handle(); + let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx)); cx.subscribe_to_view(&finder, Self::on_event); finder }); } fn on_event( - workspace_view: &mut Workspace, + workspace: &mut Workspace, _: ViewHandle, event: &Event, cx: &mut ViewContext, ) { match event { Event::Selected(tree_id, path) => { - workspace_view + workspace .open_entry((*tree_id, path.clone()), cx) .map(|d| d.detach()); - workspace_view.dismiss_modal(cx); + workspace.dismiss_modal(cx); } Event::Dismissed => { - workspace_view.dismiss_modal(cx); + workspace.dismiss_modal(cx); } } } @@ -318,7 +316,7 @@ impl FileFinder { matches: Vec::new(), selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), - list_state: UniformListState::new(), + list_state: Default::default(), } } @@ -388,10 +386,6 @@ impl FileFinder { cx.notify(); } - fn scroll(&mut self, _: &f32, cx: &mut ViewContext) { - cx.notify(); - } - fn confirm(&mut self, _: &(), cx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected_index()) { cx.emit(Event::Selected(m.tree_id, m.path.clone())); @@ -426,7 +420,7 @@ impl FileFinder { false, false, 100, - cancel_flag.clone(), + cancel_flag.as_ref(), background, ) .await; diff --git a/zed/src/lib.rs b/zed/src/lib.rs index b8cfc02c08..2ae8ad0aed 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,5 +1,3 @@ -use zrpc::ForegroundRouter; - pub mod assets; pub mod editor; pub mod file_finder; @@ -12,6 +10,7 @@ pub mod settings; mod sum_tree; #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod theme_picker; mod time; mod util; pub mod workspace; @@ -19,13 +18,19 @@ pub mod worktree; pub use settings::Settings; +use futures::lock::Mutex; +use postage::watch; +use std::sync::Arc; +use zrpc::ForegroundRouter; + pub struct AppState { - pub settings: postage::watch::Receiver, - pub languages: std::sync::Arc, - pub themes: std::sync::Arc, - pub rpc_router: std::sync::Arc, + pub settings_tx: Arc>>, + pub settings: watch::Receiver, + pub languages: Arc, + pub themes: Arc, + pub rpc_router: Arc, pub rpc: rpc::Client, - pub fs: std::sync::Arc, + pub fs: Arc, } pub fn init(cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index 9cbf082b8c..94e3cb066b 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -2,13 +2,14 @@ #![allow(non_snake_case)] use fs::OpenOptions; +use futures::lock::Mutex; use log::LevelFilter; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ self, assets, editor, file_finder, fs::RealFs, - language, menus, rpc, settings, + language, menus, rpc, settings, theme_picker, workspace::{self, OpenParams}, worktree::{self}, AppState, @@ -21,12 +22,14 @@ fn main() { let app = gpui::App::new(assets::Assets).unwrap(); let themes = settings::ThemeRegistry::new(assets::Assets); - let (_, settings) = settings::channel_with_themes(&app.font_cache(), &themes).unwrap(); + let (settings_tx, settings) = + settings::channel_with_themes(&app.font_cache(), &themes).unwrap(); let languages = Arc::new(language::LanguageRegistry::new()); languages.set_theme(&settings.borrow().theme); let mut app_state = AppState { languages: languages.clone(), + settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, rpc_router: Arc::new(ForegroundRouter::new()), @@ -40,12 +43,14 @@ fn main() { &app_state.rpc, Arc::get_mut(&mut app_state.rpc_router).unwrap(), ); + let app_state = Arc::new(app_state); + zed::init(cx); workspace::init(cx); editor::init(cx); file_finder::init(cx); + theme_picker::init(cx, &app_state); - let app_state = Arc::new(app_state); cx.set_menus(menus::menus(&app_state.clone())); if stdout_is_a_pty() { diff --git a/zed/src/settings.rs b/zed/src/settings.rs index cc90a5a443..79ee243ae0 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -133,6 +133,18 @@ impl ThemeRegistry { }) } + pub fn list(&self) -> impl Iterator { + self.assets.list("themes/").into_iter().filter_map(|path| { + let filename = path.strip_prefix("themes/")?; + let theme_name = filename.strip_suffix(".toml")?; + if theme_name.starts_with('_') { + None + } else { + Some(theme_name.to_string()) + } + }) + } + pub fn get(&self, name: &str) -> Result> { if let Some(theme) = self.themes.lock().get(name) { return Ok(theme.clone()); @@ -497,8 +509,10 @@ mod tests { fn test_parse_extended_theme() { let assets = TestAssets(&[ ( - "themes/base.toml", + "themes/_base.toml", r#" + abstract = true + [ui] tab_background = 0x111111 tab_text = "$variable_1" @@ -511,7 +525,7 @@ mod tests { ( "themes/light.toml", r#" - extends = "base" + extends = "_base" [variables] variable_1 = 0x333333 @@ -524,6 +538,16 @@ mod tests { background = 0x666666 "#, ), + ( + "themes/dark.toml", + r#" + extends = "_base" + + [variables] + variable_1 = 0x555555 + variable_2 = 0x666666 + "#, + ), ]); let registry = ThemeRegistry::new(assets); @@ -533,6 +557,11 @@ mod tests { assert_eq!(theme.ui.tab_text, ColorU::from_u32(0x333333ff)); assert_eq!(theme.editor.background, ColorU::from_u32(0x666666ff)); assert_eq!(theme.editor.default_text, ColorU::from_u32(0x444444ff)); + + assert_eq!( + registry.list().collect::>(), + &["light".to_string(), "dark".to_string()] + ); } #[test] @@ -585,5 +614,19 @@ mod tests { Err(anyhow!("no such path {}", path)) } } + + fn list(&self, prefix: &str) -> Vec> { + self.0 + .iter() + .copied() + .filter_map(|(path, _)| { + if path.starts_with(prefix) { + Some(path.into()) + } else { + None + } + }) + .collect() + } } } diff --git a/zed/src/test.rs b/zed/src/test.rs index f1367775f2..7d7d6e3ed6 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -6,6 +6,7 @@ use crate::{ time::ReplicaId, AppState, }; +use futures::lock::Mutex; use gpui::{AppContext, Entity, ModelHandle}; use smol::channel; use std::{ @@ -154,10 +155,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) { } pub fn build_app_state(cx: &AppContext) -> Arc { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap(); let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(()); Arc::new(AppState { + settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, languages: languages.clone(), diff --git a/zed/src/theme_picker.rs b/zed/src/theme_picker.rs new file mode 100644 index 0000000000..3aacf69ebf --- /dev/null +++ b/zed/src/theme_picker.rs @@ -0,0 +1,308 @@ +use std::{cmp, sync::Arc}; + +use crate::{ + editor::{self, Editor}, + settings::ThemeRegistry, + workspace::Workspace, + worktree::fuzzy::{match_strings, StringMatch, StringMatchCandidate}, + AppState, Settings, +}; +use futures::lock::Mutex; +use gpui::{ + color::ColorF, + elements::{ + Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, + UniformList, UniformListState, + }, + fonts::{Properties, Weight}, + geometry::vector::vec2f, + keymap::{self, Binding}, + AppContext, Axis, Border, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, + ViewContext, ViewHandle, +}; +use postage::watch; + +pub struct ThemePicker { + settings_tx: Arc>>, + settings: watch::Receiver, + registry: Arc, + matches: Vec, + query_buffer: ViewHandle, + list_state: UniformListState, + selected_index: usize, +} + +pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { + cx.add_action("theme_picker:confirm", ThemePicker::confirm); + // cx.add_action("file_finder:select", ThemePicker::select); + cx.add_action("menu:select_prev", ThemePicker::select_prev); + cx.add_action("menu:select_next", ThemePicker::select_next); + cx.add_action("theme_picker:toggle", ThemePicker::toggle); + + cx.add_bindings(vec![ + Binding::new("cmd-k cmd-t", "theme_picker:toggle", None).with_arg(app_state.clone()), + Binding::new("escape", "theme_picker:toggle", Some("ThemePicker")) + .with_arg(app_state.clone()), + Binding::new("enter", "theme_picker:confirm", Some("ThemePicker")), + ]); +} + +pub enum Event { + Dismissed, +} + +impl ThemePicker { + fn new( + settings_tx: Arc>>, + settings: watch::Receiver, + registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); + cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event); + + let mut this = Self { + settings, + settings_tx, + registry, + query_buffer, + matches: Vec::new(), + list_state: Default::default(), + selected_index: 0, + }; + this.update_matches(cx); + this + } + + fn toggle( + workspace: &mut Workspace, + app_state: &Arc, + cx: &mut ViewContext, + ) { + workspace.toggle_modal(cx, |cx, _| { + let picker = cx.add_view(|cx| { + Self::new( + app_state.settings_tx.clone(), + app_state.settings.clone(), + app_state.themes.clone(), + cx, + ) + }); + cx.subscribe_to_view(&picker, Self::on_event); + picker + }); + } + + fn confirm(&mut self, _: &(), cx: &mut ViewContext) { + if let Some(mat) = self.matches.get(self.selected_index) { + let settings_tx = self.settings_tx.clone(); + if let Ok(theme) = self.registry.get(&mat.string) { + cx.foreground() + .spawn(async move { + settings_tx.lock().await.borrow_mut().theme = theme; + }) + .detach(); + } + } + cx.emit(Event::Dismissed); + } + + fn select_prev(&mut self, _: &(), cx: &mut ViewContext) { + if self.selected_index > 0 { + self.selected_index -= 1; + } + self.list_state.scroll_to(self.selected_index); + cx.notify(); + } + + fn select_next(&mut self, _: &(), cx: &mut ViewContext) { + if self.selected_index + 1 < self.matches.len() { + self.selected_index += 1; + } + self.list_state.scroll_to(self.selected_index); + cx.notify(); + } + + // fn select(&mut self, selected_index: &usize, cx: &mut ViewContext) { + // self.selected_index = *selected_index; + // self.confirm(&(), cx); + // } + + fn update_matches(&mut self, cx: &mut ViewContext) { + let background = cx.background().clone(); + let candidates = self + .registry + .list() + .map(|name| StringMatchCandidate { + char_bag: name.as_str().into(), + string: name, + }) + .collect::>(); + let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx)); + + self.matches = if query.is_empty() { + candidates + .into_iter() + .map(|candidate| StringMatch { + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + smol::block_on(match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + )) + }; + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(cx), + editor::Event::Blurred => cx.emit(Event::Dismissed), + _ => {} + } + } + + fn render_matches(&self, cx: &RenderContext) -> ElementBox { + if self.matches.is_empty() { + let settings = self.settings.borrow(); + return Container::new( + Label::new( + "No matches".into(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_default_color(settings.theme.editor.default_text.0) + .boxed(), + ) + .with_margin_top(6.0) + .named("empty matches"); + } + + let handle = cx.handle(); + let list = UniformList::new( + self.list_state.clone(), + self.matches.len(), + move |mut range, items, cx| { + let cx = cx.as_ref(); + let picker = handle.upgrade(cx).unwrap(); + let picker = picker.read(cx); + let start = range.start; + range.end = cmp::min(range.end, picker.matches.len()); + items.extend( + picker.matches[range] + .iter() + .enumerate() + .map(move |(i, path_match)| picker.render_match(path_match, start + i)), + ); + }, + ); + + Container::new(list.boxed()) + .with_margin_top(6.0) + .named("matches") + } + + fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox { + let settings = self.settings.borrow(); + let theme = &settings.theme.ui; + let bold = *Properties::new().weight(Weight::BOLD); + + let mut container = Container::new( + Label::new( + theme_match.string.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_default_color(theme.modal_match_text.0) + .with_highlights( + theme.modal_match_text_highlight.0, + bold, + theme_match.positions.clone(), + ) + .boxed(), + ) + .with_uniform_padding(6.0) + .with_background_color(if index == self.selected_index { + theme.modal_match_background_active.0 + } else { + theme.modal_match_background.0 + }); + + if index == self.selected_index || index < self.matches.len() - 1 { + container = container.with_border(Border::bottom(1.0, theme.modal_match_border)); + } + + container.boxed() + } +} + +impl Entity for ThemePicker { + type Event = Event; +} + +impl View for ThemePicker { + fn ui_name() -> &'static str { + "ThemePicker" + } + + fn render(&self, cx: &RenderContext) -> ElementBox { + let settings = self.settings.borrow(); + + Align::new( + ConstrainedBox::new( + Container::new( + Flex::new(Axis::Vertical) + .with_child(ChildView::new(self.query_buffer.id()).boxed()) + .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) + .boxed(), + ) + .with_margin_top(12.0) + .with_uniform_padding(6.0) + .with_corner_radius(6.0) + .with_background_color(settings.theme.ui.modal_background) + .with_shadow(vec2f(0., 4.), 12., ColorF::new(0.0, 0.0, 0.0, 0.5).to_u8()) + .boxed(), + ) + .with_max_width(600.0) + .with_max_height(400.0) + .boxed(), + ) + .top() + .named("theme picker") + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_buffer); + } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index d0d774a817..1006d5bc61 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -13,8 +13,8 @@ use crate::{ use anyhow::{anyhow, Result}; use gpui::{ elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, - Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task, View, - ViewContext, ViewHandle, WeakModelHandle, + Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakModelHandle, }; use log::error; pub use pane::*; @@ -879,7 +879,7 @@ impl View for Workspace { "Workspace" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Container::new( Stack::new() @@ -974,8 +974,8 @@ mod tests { }) .await; assert_eq!(cx.window_ids().len(), 1); - let workspace_view_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); - workspace_view_1.read_with(&cx, |workspace, _| { + let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); + workspace_1.read_with(&cx, |workspace, _| { assert_eq!(workspace.worktrees().len(), 2) }); @@ -1380,9 +1380,9 @@ mod tests { assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone())); cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - let workspace_view = workspace.read(cx); - assert_eq!(workspace_view.panes.len(), 1); - assert_eq!(workspace_view.active_pane(), &pane_1); + let workspace = workspace.read(cx); + assert_eq!(workspace.panes.len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); }); } } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 2b152a6996..9102e9f813 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -5,7 +5,8 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, - AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext, ViewHandle, + AppContext, Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, + ViewHandle, }; use postage::watch; use std::{cmp, path::Path, sync::Arc}; @@ -371,7 +372,7 @@ impl View for Pane { "Pane" } - fn render<'a>(&self, cx: &AppContext) -> ElementBox { + fn render<'a>(&self, cx: &RenderContext) -> ElementBox { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 041a94770d..83ae844dba 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,5 +1,5 @@ mod char_bag; -mod fuzzy; +pub(crate) mod fuzzy; mod ignore; use self::{char_bag::CharBag, ignore::IgnoreStack}; @@ -2615,6 +2615,7 @@ mod tests { tree.snapshot() }); + let cancel_flag = Default::default(); let results = cx .read(|cx| { match_paths( @@ -2624,7 +2625,7 @@ mod tests { false, false, 10, - Default::default(), + &cancel_flag, cx.background().clone(), ) }) @@ -2667,6 +2668,7 @@ mod tests { assert_eq!(tree.file_count(), 0); tree.snapshot() }); + let cancel_flag = Default::default(); let results = cx .read(|cx| { match_paths( @@ -2676,7 +2678,7 @@ mod tests { false, false, 10, - Default::default(), + &cancel_flag, cx.background().clone(), ) }) diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 0fb406fc98..a0f2c57edc 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -2,6 +2,7 @@ use super::{char_bag::CharBag, EntryKind, Snapshot}; use crate::util; use gpui::executor; use std::{ + borrow::Cow, cmp::{max, min, Ordering}, path::Path, sync::atomic::{self, AtomicBool}, @@ -12,8 +13,31 @@ const BASE_DISTANCE_PENALTY: f64 = 0.6; const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; const MIN_DISTANCE_PENALTY: f64 = 0.2; +struct Matcher<'a> { + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + min_score: f64, + match_positions: Vec, + last_positions: Vec, + score_matrix: Vec>, + best_position_matrix: Vec, +} + +trait Match: Ord { + fn score(&self) -> f64; + fn set_positions(&mut self, positions: Vec); +} + +trait MatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool; + fn to_string<'a>(&'a self) -> Cow<'a, str>; +} + #[derive(Clone, Debug)] -pub struct MatchCandidate<'a> { +pub struct PathMatchCandidate<'a> { pub path: &'a Arc, pub char_bag: CharBag, } @@ -27,6 +51,82 @@ pub struct PathMatch { pub include_root_name: bool, } +#[derive(Clone, Debug)] +pub struct StringMatchCandidate { + pub string: String, + pub char_bag: CharBag, +} + +impl Match for PathMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl Match for StringMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl<'a> MatchCandidate for PathMatchCandidate<'a> { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.path.to_string_lossy() + } +} + +impl<'a> MatchCandidate for &'a StringMatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.string.as_str().into() + } +} + +#[derive(Clone, Debug)] +pub struct StringMatch { + pub score: f64, + pub positions: Vec, + pub string: String, +} + +impl PartialEq for StringMatch { + fn eq(&self, other: &Self) -> bool { + self.score.eq(&other.score) + } +} + +impl Eq for StringMatch {} + +impl PartialOrd for StringMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for StringMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .partial_cmp(&other.score) + .unwrap_or(Ordering::Equal) + .then_with(|| self.string.cmp(&other.string)) + } +} + impl PartialEq for PathMatch { fn eq(&self, other: &Self) -> bool { self.score.eq(&other.score) @@ -51,6 +151,62 @@ impl Ord for PathMatch { } } +pub async fn match_strings( + candidates: &[StringMatchCandidate], + query: &str, + smart_case: bool, + max_results: usize, + cancel_flag: &AtomicBool, + background: Arc, +) -> Vec { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + + let lowercase_query = &lowercase_query; + let query = &query; + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let num_cpus = background.num_cpus().min(candidates.len()); + let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) + .map(|_| Vec::with_capacity(max_results)) + .collect::>(); + + background + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let cancel_flag = &cancel_flag; + scope.spawn(async move { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + matcher.match_strings( + &candidates[segment_start..segment_end], + results, + cancel_flag, + ); + }); + } + }) + .await; + + let mut results = Vec::new(); + for segment_result in segment_results { + if results.is_empty() { + results = segment_result; + } else { + util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a)); + } + } + results +} + pub async fn match_paths<'a, T>( snapshots: T, query: &str, @@ -58,7 +214,7 @@ pub async fn match_paths<'a, T>( include_ignored: bool, smart_case: bool, max_results: usize, - cancel_flag: Arc, + cancel_flag: &AtomicBool, background: Arc, ) -> Vec where @@ -78,7 +234,7 @@ where let lowercase_query = &lowercase_query; let query = &query; - let query_chars = CharBag::from(&lowercase_query[..]); + let query_char_bag = CharBag::from(&lowercase_query[..]); let num_cpus = background.num_cpus().min(path_count); let segment_size = (path_count + num_cpus - 1) / num_cpus; @@ -90,18 +246,16 @@ where .scoped(|scope| { for (segment_idx, results) in segment_results.iter_mut().enumerate() { let snapshots = snapshots.clone(); - let cancel_flag = &cancel_flag; scope.spawn(async move { let segment_start = segment_idx * segment_size; let segment_end = segment_start + segment_size; - - let mut min_score = 0.0; - let mut last_positions = Vec::new(); - last_positions.resize(query.len(), 0); - let mut match_positions = Vec::new(); - match_positions.resize(query.len(), 0); - let mut score_matrix = Vec::new(); - let mut best_position_matrix = Vec::new(); + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); let mut tree_start = 0; for snapshot in snapshots { @@ -123,7 +277,7 @@ where }; let paths = entries.map(|entry| { if let EntryKind::File(char_bag) = entry.kind { - MatchCandidate { + PathMatchCandidate { path: &entry.path, char_bag, } @@ -132,21 +286,11 @@ where } }); - match_single_tree_paths( + matcher.match_paths( snapshot, include_root_name, paths, - query, - lowercase_query, - query_chars, - smart_case, results, - max_results, - &mut min_score, - &mut match_positions, - &mut last_positions, - &mut score_matrix, - &mut best_position_matrix, &cancel_flag, ); } @@ -171,322 +315,335 @@ where results } -fn match_single_tree_paths<'a>( - snapshot: &Snapshot, - include_root_name: bool, - path_entries: impl Iterator>, - query: &[char], - lowercase_query: &[char], - query_chars: CharBag, - smart_case: bool, - results: &mut Vec, - max_results: usize, - min_score: &mut f64, - match_positions: &mut Vec, - last_positions: &mut Vec, - score_matrix: &mut Vec>, - best_position_matrix: &mut Vec, - cancel_flag: &AtomicBool, -) { - let mut path_chars = Vec::new(); - let mut lowercase_path_chars = Vec::new(); - - let prefix = if include_root_name { - snapshot.root_name() - } else { - "" +impl<'a> Matcher<'a> { + fn new( + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + ) -> Self { + Self { + query, + lowercase_query, + query_char_bag, + min_score: 0.0, + last_positions: vec![0; query.len()], + match_positions: vec![0; query.len()], + score_matrix: Vec::new(), + best_position_matrix: Vec::new(), + smart_case, + max_results, + } } - .chars() - .collect::>(); - let lowercase_prefix = prefix - .iter() - .map(|c| c.to_ascii_lowercase()) + + fn match_strings( + &mut self, + candidates: &[StringMatchCandidate], + results: &mut Vec, + cancel_flag: &AtomicBool, + ) { + self.match_internal( + &[], + &[], + candidates.iter(), + results, + cancel_flag, + |candidate, score| StringMatch { + score, + positions: Vec::new(), + string: candidate.string.to_string(), + }, + ) + } + + fn match_paths( + &mut self, + snapshot: &Snapshot, + include_root_name: bool, + path_entries: impl Iterator>, + results: &mut Vec, + cancel_flag: &AtomicBool, + ) { + let tree_id = snapshot.id; + let prefix = if include_root_name { + snapshot.root_name() + } else { + "" + } + .chars() .collect::>(); - - for candidate in path_entries { - if !candidate.char_bag.is_superset(query_chars) { - continue; - } - - if cancel_flag.load(atomic::Ordering::Relaxed) { - break; - } - - path_chars.clear(); - lowercase_path_chars.clear(); - for c in candidate.path.to_string_lossy().chars() { - path_chars.push(c); - lowercase_path_chars.push(c.to_ascii_lowercase()); - } - - if !find_last_positions( - last_positions, - &lowercase_prefix, - &lowercase_path_chars, - &lowercase_query[..], - ) { - continue; - } - - let matrix_len = query.len() * (path_chars.len() + prefix.len()); - score_matrix.clear(); - score_matrix.resize(matrix_len, None); - best_position_matrix.clear(); - best_position_matrix.resize(matrix_len, 0); - - let score = score_match( - &query[..], - &lowercase_query[..], - &path_chars, - &lowercase_path_chars, + let lowercase_prefix = prefix + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect::>(); + self.match_internal( &prefix, &lowercase_prefix, - smart_case, - &last_positions, - score_matrix, - best_position_matrix, - match_positions, - *min_score, - ); - - if score > 0.0 { - let mat = PathMatch { - tree_id: snapshot.id, - path: candidate.path.clone(), + path_entries, + results, + cancel_flag, + |candidate, score| PathMatch { score, - positions: match_positions.clone(), + tree_id, + positions: Vec::new(), + path: candidate.path.clone(), include_root_name, - }; - if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { - if results.len() < max_results { - results.insert(i, mat); - } else if i < results.len() { - results.pop(); - results.insert(i, mat); - } - if results.len() == max_results { - *min_score = results.last().unwrap().score; + }, + ) + } + + fn match_internal( + &mut self, + prefix: &[char], + lowercase_prefix: &[char], + candidates: impl Iterator, + results: &mut Vec, + cancel_flag: &AtomicBool, + build_match: F, + ) where + R: Match, + F: Fn(&C, f64) -> R, + { + let mut candidate_chars = Vec::new(); + let mut lowercase_candidate_chars = Vec::new(); + + for candidate in candidates { + if !candidate.has_chars(self.query_char_bag) { + continue; + } + + if cancel_flag.load(atomic::Ordering::Relaxed) { + break; + } + + candidate_chars.clear(); + lowercase_candidate_chars.clear(); + for c in candidate.to_string().chars() { + candidate_chars.push(c); + lowercase_candidate_chars.push(c.to_ascii_lowercase()); + } + + if !self.find_last_positions(&lowercase_prefix, &lowercase_candidate_chars) { + continue; + } + + let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len()); + self.score_matrix.clear(); + self.score_matrix.resize(matrix_len, None); + self.best_position_matrix.clear(); + self.best_position_matrix.resize(matrix_len, 0); + + let score = self.score_match( + &candidate_chars, + &lowercase_candidate_chars, + &prefix, + &lowercase_prefix, + ); + + if score > 0.0 { + let mut mat = build_match(&candidate, score); + if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { + if results.len() < self.max_results { + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } else if i < results.len() { + results.pop(); + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } + if results.len() == self.max_results { + self.min_score = results.last().unwrap().score(); + } } } } } -} -fn find_last_positions( - last_positions: &mut Vec, - prefix: &[char], - path: &[char], - query: &[char], -) -> bool { - let mut path = path.iter(); - let mut prefix_iter = prefix.iter(); - for (i, char) in query.iter().enumerate().rev() { - if let Some(j) = path.rposition(|c| c == char) { - last_positions[i] = j + prefix.len(); - } else if let Some(j) = prefix_iter.rposition(|c| c == char) { - last_positions[i] = j; - } else { - return false; - } - } - true -} - -fn score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - prefix: &[char], - lowercase_prefix: &[char], - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - match_positions: &mut [usize], - min_score: f64, -) -> f64 { - let score = recursive_score_match( - query, - query_cased, - path, - path_cased, - prefix, - lowercase_prefix, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - 0, - 0, - query.len() as f64, - ) * query.len() as f64; - - if score <= 0.0 { - return 0.0; - } - - let path_len = prefix.len() + path.len(); - let mut cur_start = 0; - let mut byte_ix = 0; - let mut char_ix = 0; - for i in 0..query.len() { - let match_char_ix = best_position_matrix[i * path_len + cur_start]; - while char_ix < match_char_ix { - let ch = prefix - .get(char_ix) - .or_else(|| path.get(char_ix - prefix.len())) - .unwrap(); - byte_ix += ch.len_utf8(); - char_ix += 1; - } - cur_start = match_char_ix + 1; - match_positions[i] = byte_ix; - } - - score -} - -fn recursive_score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - prefix: &[char], - lowercase_prefix: &[char], - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - min_score: f64, - query_idx: usize, - path_idx: usize, - cur_score: f64, -) -> f64 { - if query_idx == query.len() { - return 1.0; - } - - let path_len = prefix.len() + path.len(); - - if let Some(memoized) = score_matrix[query_idx * path_len + path_idx] { - return memoized; - } - - let mut score = 0.0; - let mut best_position = 0; - - let query_char = query_cased[query_idx]; - let limit = last_positions[query_idx]; - - let mut last_slash = 0; - for j in path_idx..=limit { - let path_char = if j < prefix.len() { - lowercase_prefix[j] - } else { - path_cased[j - prefix.len()] - }; - let is_path_sep = path_char == '/' || path_char == '\\'; - - if query_idx == 0 && is_path_sep { - last_slash = j; - } - - if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { - let curr = if j < prefix.len() { - prefix[j] + fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool { + let mut path = path.iter(); + let mut prefix_iter = prefix.iter(); + for (i, char) in self.query.iter().enumerate().rev() { + if let Some(j) = path.rposition(|c| c == char) { + self.last_positions[i] = j + prefix.len(); + } else if let Some(j) = prefix_iter.rposition(|c| c == char) { + self.last_positions[i] = j; } else { - path[j - prefix.len()] - }; + return false; + } + } + true + } - let mut char_score = 1.0; - if j > path_idx { - let last = if j - 1 < prefix.len() { - prefix[j - 1] + fn score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + ) -> f64 { + let score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + 0, + 0, + self.query.len() as f64, + ) * self.query.len() as f64; + + if score <= 0.0 { + return 0.0; + } + + let path_len = prefix.len() + path.len(); + let mut cur_start = 0; + let mut byte_ix = 0; + let mut char_ix = 0; + for i in 0..self.query.len() { + let match_char_ix = self.best_position_matrix[i * path_len + cur_start]; + while char_ix < match_char_ix { + let ch = prefix + .get(char_ix) + .or_else(|| path.get(char_ix - prefix.len())) + .unwrap(); + byte_ix += ch.len_utf8(); + char_ix += 1; + } + cur_start = match_char_ix + 1; + self.match_positions[i] = byte_ix; + } + + score + } + + fn recursive_score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + query_idx: usize, + path_idx: usize, + cur_score: f64, + ) -> f64 { + if query_idx == self.query.len() { + return 1.0; + } + + let path_len = prefix.len() + path.len(); + + if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] { + return memoized; + } + + let mut score = 0.0; + let mut best_position = 0; + + let query_char = self.lowercase_query[query_idx]; + let limit = self.last_positions[query_idx]; + + let mut last_slash = 0; + for j in path_idx..=limit { + let path_char = if j < prefix.len() { + lowercase_prefix[j] + } else { + path_cased[j - prefix.len()] + }; + let is_path_sep = path_char == '/' || path_char == '\\'; + + if query_idx == 0 && is_path_sep { + last_slash = j; + } + + if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + let curr = if j < prefix.len() { + prefix[j] } else { - path[j - 1 - prefix.len()] + path[j - prefix.len()] }; - if last == '/' { - char_score = 0.9; - } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { - char_score = 0.8; - } else if last.is_lowercase() && curr.is_uppercase() { - char_score = 0.8; - } else if last == '.' { - char_score = 0.7; - } else if query_idx == 0 { - char_score = BASE_DISTANCE_PENALTY; - } else { - char_score = MIN_DISTANCE_PENALTY.max( - BASE_DISTANCE_PENALTY - - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, - ); - } - } + let mut char_score = 1.0; + if j > path_idx { + let last = if j - 1 < prefix.len() { + prefix[j - 1] + } else { + path[j - 1 - prefix.len()] + }; - // Apply a severe penalty if the case doesn't match. - // This will make the exact matches have higher score than the case-insensitive and the - // path insensitive matches. - if (smart_case || curr == '/') && query[query_idx] != curr { - char_score *= 0.001; - } - - let mut multiplier = char_score; - - // Scale the score based on how deep within the path we found the match. - if query_idx == 0 { - multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; - } - - let mut next_score = 1.0; - if min_score > 0.0 { - next_score = cur_score * multiplier; - // Scores only decrease. If we can't pass the previous best, bail - if next_score < min_score { - // Ensure that score is non-zero so we use it in the memo table. - if score == 0.0 { - score = 1e-18; + if last == '/' { + char_score = 0.9; + } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { + char_score = 0.8; + } else if last.is_lowercase() && curr.is_uppercase() { + char_score = 0.8; + } else if last == '.' { + char_score = 0.7; + } else if query_idx == 0 { + char_score = BASE_DISTANCE_PENALTY; + } else { + char_score = MIN_DISTANCE_PENALTY.max( + BASE_DISTANCE_PENALTY + - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, + ); } - continue; } - } - let new_score = recursive_score_match( - query, - query_cased, - path, - path_cased, - prefix, - lowercase_prefix, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - query_idx + 1, - j + 1, - next_score, - ) * multiplier; + // Apply a severe penalty if the case doesn't match. + // This will make the exact matches have higher score than the case-insensitive and the + // path insensitive matches. + if (self.smart_case || curr == '/') && self.query[query_idx] != curr { + char_score *= 0.001; + } - if new_score > score { - score = new_score; - best_position = j; - // Optimization: can't score better than 1. - if new_score == 1.0 { - break; + let mut multiplier = char_score; + + // Scale the score based on how deep within the path we found the match. + if query_idx == 0 { + multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; + } + + let mut next_score = 1.0; + if self.min_score > 0.0 { + next_score = cur_score * multiplier; + // Scores only decrease. If we can't pass the previous best, bail + if next_score < self.min_score { + // Ensure that score is non-zero so we use it in the memo table. + if score == 0.0 { + score = 1e-18; + } + continue; + } + } + + let new_score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + query_idx + 1, + j + 1, + next_score, + ) * multiplier; + + if new_score > score { + score = new_score; + best_position = j; + // Optimization: can't score better than 1. + if new_score == 1.0 { + break; + } } } } - } - if best_position != 0 { - best_position_matrix[query_idx * path_len + path_idx] = best_position; - } + if best_position != 0 { + self.best_position_matrix[query_idx * path_len + path_idx] = best_position; + } - score_matrix[query_idx * path_len + path_idx] = Some(score); - score + self.score_matrix[query_idx * path_len + path_idx] = Some(score); + score + } } #[cfg(test)] @@ -496,34 +653,22 @@ mod tests { #[test] fn test_get_last_positions() { - let mut last_positions = vec![0; 2]; - let result = find_last_positions( - &mut last_positions, - &['a', 'b', 'c'], - &['b', 'd', 'e', 'f'], - &['d', 'c'], - ); + let mut query: &[char] = &['d', 'c']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); assert_eq!(result, false); - last_positions.resize(2, 0); - let result = find_last_positions( - &mut last_positions, - &['a', 'b', 'c'], - &['b', 'd', 'e', 'f'], - &['c', 'd'], - ); + query = &['c', 'd']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); assert_eq!(result, true); - assert_eq!(last_positions, vec![2, 4]); + assert_eq!(matcher.last_positions, vec![2, 4]); - last_positions.resize(4, 0); - let result = find_last_positions( - &mut last_positions, - &['z', 'e', 'd', '/'], - &['z', 'e', 'd', '/', 'f'], - &['z', '/', 'z', 'f'], - ); + query = &['z', '/', 'z', 'f']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']); assert_eq!(result, true); - assert_eq!(last_positions, vec![0, 3, 4, 8]); + assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]); } #[test] @@ -604,20 +749,17 @@ mod tests { for (i, path) in paths.iter().enumerate() { let lowercase_path = path.to_lowercase().chars().collect::>(); let char_bag = CharBag::from(lowercase_path.as_slice()); - path_entries.push(MatchCandidate { + path_entries.push(PathMatchCandidate { char_bag, path: path_arcs.get(i).unwrap(), }); } - let mut match_positions = Vec::new(); - let mut last_positions = Vec::new(); - match_positions.resize(query.len(), 0); - last_positions.resize(query.len(), 0); + let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100); let cancel_flag = AtomicBool::new(false); let mut results = Vec::new(); - match_single_tree_paths( + matcher.match_paths( &Snapshot { id: 0, scan_id: 0, @@ -632,17 +774,7 @@ mod tests { }, false, path_entries.into_iter(), - &query[..], - &lowercase_query[..], - query_chars, - smart_case, &mut results, - 100, - &mut 0.0, - &mut match_positions, - &mut last_positions, - &mut Vec::new(), - &mut Vec::new(), &cancel_flag, );