Detailed changes
@@ -28,7 +28,7 @@ impl gpui::View for TextView {
"View"
}
- fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox {
+ fn render(&self, _: &gpui::RenderContext<Self>) -> gpui::ElementBox {
TextElement.boxed()
}
}
@@ -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<Self>) {}
fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
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<ElementBox> {
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::<T>,
+ },
+ )
}
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<T>,
+}
+
+impl<'a, T: View> RenderContext<'a, T> {
+ pub fn handle(&self) -> WeakViewHandle<T> {
+ WeakViewHandle::new(self.window_id, self.view_id)
+ }
+}
+
impl AsRef<AppContext> for &AppContext {
fn as_ref(&self) -> &AppContext {
self
}
}
+impl<V: View> Deref for RenderContext<'_, V> {
+ type Target = AppContext;
+
+ fn deref(&self) -> &Self::Target {
+ &self.app
+ }
+}
+
impl<M> AsRef<AppContext> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> 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<Self>) -> ElementBox {
Empty::new().boxed()
}
@@ -3674,7 +3703,7 @@ mod tests {
"test view"
}
- fn render(&self, _: &AppContext) -> ElementBox {
+ fn render(&self, _: &RenderContext<Self>) -> ElementBox {
Empty::new().boxed()
}
}
@@ -3719,7 +3748,7 @@ mod tests {
"test view"
}
- fn render(&self, _: &AppContext) -> ElementBox {
+ fn render(&self, _: &RenderContext<Self>) -> ElementBox {
Empty::new().boxed()
}
}
@@ -3742,7 +3771,7 @@ mod tests {
"test view"
}
- fn render(&self, _: &AppContext) -> ElementBox {
+ fn render(&self, _: &RenderContext<Self>) -> ElementBox {
Empty::new().boxed()
}
}
@@ -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<Cow<[u8]>>;
+ fn list(&self, path: &str) -> Vec<Cow<'static, str>>;
}
impl AssetSource for () {
@@ -12,6 +13,10 @@ impl AssetSource for () {
path
))
}
+
+ fn list(&self, _: &str) -> Vec<Cow<'static, str>> {
+ vec![]
+ }
}
pub struct AssetCache {
@@ -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<Mutex<StateInner>>);
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<usize>,
@@ -57,11 +51,11 @@ impl<F> UniformList<F>
where
F: Fn(Range<usize>, &mut Vec<ElementBox>, &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
}
@@ -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<Self>) -> gpui::ElementBox {
gpui::Element::boxed(gpui::elements::Empty)
}
}
@@ -1,4 +1,4 @@
-extends = "base"
+extends = "_base"
[variables]
elevation_1 = 0x050101
@@ -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
@@ -10,4 +10,8 @@ impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path))
}
+
+ fn list(&self, path: &str) -> Vec<std::borrow::Cow<'static, str>> {
+ Self::iter().filter(|p| p.starts_with(path)).collect()
+ }
}
@@ -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<Self>) -> ElementBox {
EditorElement::new(self.handle.clone()).boxed()
}
@@ -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<Self>) -> ElementBox {
let settings = self.settings.borrow();
Align::new(
@@ -267,31 +266,30 @@ impl FileFinder {
})
}
- fn toggle(workspace_view: &mut Workspace, _: &(), cx: &mut ViewContext<Workspace>) {
- 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>) {
+ 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<FileFinder>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
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<Self>) {
- cx.notify();
- }
-
fn confirm(&mut self, _: &(), cx: &mut ViewContext<Self>) {
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;
@@ -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<Settings>,
- pub languages: std::sync::Arc<language::LanguageRegistry>,
- pub themes: std::sync::Arc<settings::ThemeRegistry>,
- pub rpc_router: std::sync::Arc<ForegroundRouter>,
+ pub settings_tx: Arc<Mutex<watch::Sender<Settings>>>,
+ pub settings: watch::Receiver<Settings>,
+ pub languages: Arc<language::LanguageRegistry>,
+ pub themes: Arc<settings::ThemeRegistry>,
+ pub rpc_router: Arc<ForegroundRouter>,
pub rpc: rpc::Client,
- pub fs: std::sync::Arc<dyn fs::Fs>,
+ pub fs: Arc<dyn fs::Fs>,
}
pub fn init(cx: &mut gpui::MutableAppContext) {
@@ -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() {
@@ -133,6 +133,18 @@ impl ThemeRegistry {
})
}
+ pub fn list(&self) -> impl Iterator<Item = String> {
+ 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<Arc<Theme>> {
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::<Vec<_>>(),
+ &["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<std::borrow::Cow<'static, str>> {
+ self.0
+ .iter()
+ .copied()
+ .filter_map(|(path, _)| {
+ if path.starts_with(prefix) {
+ Some(path.into())
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
}
}
@@ -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<AppState> {
- 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(),
@@ -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<Mutex<watch::Sender<Settings>>>,
+ settings: watch::Receiver<Settings>,
+ registry: Arc<ThemeRegistry>,
+ matches: Vec<StringMatch>,
+ query_buffer: ViewHandle<Editor>,
+ list_state: UniformListState,
+ selected_index: usize,
+}
+
+pub fn init(cx: &mut MutableAppContext, app_state: &Arc<AppState>) {
+ 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<Mutex<watch::Sender<Settings>>>,
+ settings: watch::Receiver<Settings>,
+ registry: Arc<ThemeRegistry>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<AppState>,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ 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<Self>) {
+ 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<Self>) {
+ 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<Self>) {
+ 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>) {
+ // self.selected_index = *selected_index;
+ // self.confirm(&(), cx);
+ // }
+
+ fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
+ let background = cx.background().clone();
+ let candidates = self
+ .registry
+ .list()
+ .map(|name| StringMatchCandidate {
+ char_bag: name.as_str().into(),
+ string: name,
+ })
+ .collect::<Vec<_>>();
+ 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<ThemePicker>,
+ event: &Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ match event {
+ Event::Dismissed => {
+ workspace.dismiss_modal(cx);
+ }
+ }
+ }
+
+ fn on_query_editor_event(
+ &mut self,
+ _: ViewHandle<Editor>,
+ event: &editor::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ editor::Event::Edited => self.update_matches(cx),
+ editor::Event::Blurred => cx.emit(Event::Dismissed),
+ _ => {}
+ }
+ }
+
+ fn render_matches(&self, cx: &RenderContext<Self>) -> 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<Self>) -> 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<Self>) {
+ 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
+ }
+}
@@ -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<Self>) -> 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::<Workspace>(cx.window_ids()[0]).unwrap();
- workspace_view_1.read_with(&cx, |workspace, _| {
+ let workspace_1 = cx.root_view::<Workspace>(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);
});
}
}
@@ -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<Self>) -> ElementBox {
if let Some(active_item) = self.active_item() {
Flex::column()
.with_child(self.render_tabs(cx))
@@ -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(),
)
})
@@ -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<usize>,
+ last_positions: Vec<usize>,
+ score_matrix: Vec<Option<f64>>,
+ best_position_matrix: Vec<usize>,
+}
+
+trait Match: Ord {
+ fn score(&self) -> f64;
+ fn set_positions(&mut self, positions: Vec<usize>);
+}
+
+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<Path>,
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<usize>) {
+ self.positions = positions;
+ }
+}
+
+impl Match for StringMatch {
+ fn score(&self) -> f64 {
+ self.score
+ }
+
+ fn set_positions(&mut self, positions: Vec<usize>) {
+ 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<usize>,
+ 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<Ordering> {
+ 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<executor::Background>,
+) -> Vec<StringMatch> {
+ let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
+ let query = query.chars().collect::<Vec<_>>();
+
+ 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::<Vec<_>>();
+
+ 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<AtomicBool>,
+ cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch>
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<Item = MatchCandidate<'a>>,
- query: &[char],
- lowercase_query: &[char],
- query_chars: CharBag,
- smart_case: bool,
- results: &mut Vec<PathMatch>,
- max_results: usize,
- min_score: &mut f64,
- match_positions: &mut Vec<usize>,
- last_positions: &mut Vec<usize>,
- score_matrix: &mut Vec<Option<f64>>,
- best_position_matrix: &mut Vec<usize>,
- cancel_flag: &AtomicBool,
-) {
- let mut path_chars = Vec::new();
- let mut lowercase_path_chars = Vec::new();
+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,
+ }
+ }
- let prefix = if include_root_name {
- snapshot.root_name()
- } else {
- ""
+ fn match_strings(
+ &mut self,
+ candidates: &[StringMatchCandidate],
+ results: &mut Vec<StringMatch>,
+ cancel_flag: &AtomicBool,
+ ) {
+ self.match_internal(
+ &[],
+ &[],
+ candidates.iter(),
+ results,
+ cancel_flag,
+ |candidate, score| StringMatch {
+ score,
+ positions: Vec::new(),
+ string: candidate.string.to_string(),
+ },
+ )
}
- .chars()
- .collect::<Vec<_>>();
- let lowercase_prefix = prefix
- .iter()
- .map(|c| c.to_ascii_lowercase())
- .collect::<Vec<_>>();
- for candidate in path_entries {
- if !candidate.char_bag.is_superset(query_chars) {
- continue;
+ fn match_paths(
+ &mut self,
+ snapshot: &Snapshot,
+ include_root_name: bool,
+ path_entries: impl Iterator<Item = PathMatchCandidate<'a>>,
+ results: &mut Vec<PathMatch>,
+ cancel_flag: &AtomicBool,
+ ) {
+ let tree_id = snapshot.id;
+ let prefix = if include_root_name {
+ snapshot.root_name()
+ } else {
+ ""
}
+ .chars()
+ .collect::<Vec<_>>();
+ let lowercase_prefix = prefix
+ .iter()
+ .map(|c| c.to_ascii_lowercase())
+ .collect::<Vec<_>>();
+ self.match_internal(
+ &prefix,
+ &lowercase_prefix,
+ path_entries,
+ results,
+ cancel_flag,
+ |candidate, score| PathMatch {
+ score,
+ tree_id,
+ positions: Vec::new(),
+ path: candidate.path.clone(),
+ include_root_name,
+ },
+ )
+ }
- if cancel_flag.load(atomic::Ordering::Relaxed) {
- break;
- }
+ fn match_internal<C: MatchCandidate, R, F>(
+ &mut self,
+ prefix: &[char],
+ lowercase_prefix: &[char],
+ candidates: impl Iterator<Item = C>,
+ results: &mut Vec<R>,
+ 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;
+ }
- 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 cancel_flag.load(atomic::Ordering::Relaxed) {
+ break;
+ }
- if !find_last_positions(
- last_positions,
- &lowercase_prefix,
- &lowercase_path_chars,
- &lowercase_query[..],
- ) {
- continue;
- }
+ 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());
+ }
- 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,
- &prefix,
- &lowercase_prefix,
- smart_case,
- &last_positions,
- score_matrix,
- best_position_matrix,
- match_positions,
- *min_score,
- );
+ if !self.find_last_positions(&lowercase_prefix, &lowercase_candidate_chars) {
+ continue;
+ }
- if score > 0.0 {
- let mat = PathMatch {
- tree_id: snapshot.id,
- path: candidate.path.clone(),
- score,
- positions: match_positions.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;
+ 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<usize>,
- 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;
+ 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 {
+ 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<f64>],
- 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;
+ true
}
- 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;
+ 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;
}
- cur_start = match_char_ix + 1;
- match_positions[i] = byte_ix;
- }
- score
-}
+ 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;
+ }
-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<f64>],
- 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;
+ score
}
- let path_len = prefix.len() + path.len();
-
- if let Some(memoized) = score_matrix[query_idx * path_len + path_idx] {
- return memoized;
- }
+ 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 mut score = 0.0;
- let mut best_position = 0;
+ let path_len = prefix.len() + path.len();
- let query_char = query_cased[query_idx];
- let limit = last_positions[query_idx];
+ if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
+ return memoized;
+ }
- 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 == '\\';
+ let mut score = 0.0;
+ let mut best_position = 0;
- if query_idx == 0 && is_path_sep {
- last_slash = j;
- }
+ let query_char = self.lowercase_query[query_idx];
+ let limit = self.last_positions[query_idx];
- if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
- let curr = if j < prefix.len() {
- prefix[j]
+ let mut last_slash = 0;
+ for j in path_idx..=limit {
+ let path_char = if j < prefix.len() {
+ lowercase_prefix[j]
} else {
- path[j - prefix.len()]
+ path_cased[j - prefix.len()]
};
+ let is_path_sep = path_char == '/' || path_char == '\\';
+
+ if query_idx == 0 && is_path_sep {
+ last_slash = j;
+ }
- let mut char_score = 1.0;
- if j > path_idx {
- let last = if j - 1 < prefix.len() {
- prefix[j - 1]
+ 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()]
+ };
+
+ 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,
+ );
+ }
}
- }
- // 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;
- }
+ // 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;
+ }
- let mut multiplier = char_score;
+ 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;
- }
+ // 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;
+ 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;
}
- 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;
-
- if new_score > score {
- score = new_score;
- best_position = j;
- // Optimization: can't score better than 1.
- if new_score == 1.0 {
- break;
+ 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]);
-
- last_positions.resize(4, 0);
- let result = find_last_positions(
- &mut last_positions,
- &['z', 'e', 'd', '/'],
- &['z', 'e', 'd', '/', 'f'],
- &['z', '/', 'z', 'f'],
- );
+ assert_eq!(matcher.last_positions, vec![2, 4]);
+
+ 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::<Vec<_>>();
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,
);