mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 04:33:13 +03:00
Add a theme picker
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
e080739d57
commit
b30d0daabf
@ -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
|
||||
|
21
zed/assets/themes/light.toml
Normal file
21
zed/assets/themes/light.toml
Normal 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
|
@ -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(),
|
||||
|
308
zed/src/theme_picker.rs
Normal file
308
zed/src/theme_picker.rs
Normal 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
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user