Add a theme picker

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2021-08-02 14:55:27 -07:00
parent e080739d57
commit b30d0daabf
20 changed files with 982 additions and 437 deletions

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -1,4 +1,4 @@
extends = "base"
extends = "_base"
[variables]
elevation_1 = 0x050101

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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;

View File

@ -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) {

View File

@ -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() {

View File

@ -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()
}
}
}

View File

@ -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(),

308
zed/src/theme_picker.rs Normal file
View File

@ -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
}
}

View File

@ -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);
});
}
}

View File

@ -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))

View File

@ -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(),
)
})

View File

@ -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();
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::<Vec<_>>();
let lowercase_prefix = prefix
.iter()
.map(|c| c.to_ascii_lowercase())
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(),
},
)
}
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<_>>();
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::<Vec<_>>();
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<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;
}
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<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;
}
}
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;
}
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<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;
}
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::<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,
);