diff --git a/gpui/src/app.rs b/gpui/src/app.rs index ccf474b1b9..6025aed576 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -3,8 +3,9 @@ use crate::{ executor::{self, ForegroundTask}, keymap::{self, Keystroke}, platform::{self, App as _, WindowOptions}, + presenter::Presenter, util::post_inc, - AssetCache, AssetSource, FontCache, Presenter, + AssetCache, AssetSource, FontCache, }; use anyhow::{anyhow, Result}; use keymap::MatchResult; @@ -1440,8 +1441,8 @@ impl<'a, T: Entity> ModelContext<'a, T> { self.app } - pub fn background_executor(&self) -> Arc { - self.app.background.clone() + pub fn background_executor(&self) -> &Arc { + &self.app.background } pub fn halt_stream(&mut self) { @@ -1633,6 +1634,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app } + pub fn background_executor(&self) -> &Arc { + &self.app.background + } + pub fn focus(&mut self, handle: S) where S: Into, diff --git a/gpui/src/elements/mod.rs b/gpui/src/elements/mod.rs index 0e98669c35..19f14b8740 100644 --- a/gpui/src/elements/mod.rs +++ b/gpui/src/elements/mod.rs @@ -10,6 +10,7 @@ mod stack; mod svg; mod uniform_list; +pub use crate::presenter::ChildView; pub use align::*; pub use constrained_box::*; pub use container::*; diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index a5527bad61..0cddfe79e8 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -30,30 +30,28 @@ impl UniformListState { } } -pub struct UniformList +pub struct UniformList where - F: Fn(Range, &AppContext) -> G, - G: Iterator>, + F: Fn(Range, &mut Vec>, &AppContext), { state: UniformListState, item_count: usize, - build_items: F, + append_items: F, scroll_max: Option, items: Vec>, origin: Option, size: Option, } -impl UniformList +impl UniformList where - F: Fn(Range, &AppContext) -> G, - G: Iterator>, + F: Fn(Range, &mut Vec>, &AppContext), { pub fn new(state: UniformListState, item_count: usize, build_items: F) -> Self { Self { state, item_count, - build_items, + append_items: build_items, scroll_max: None, items: Default::default(), origin: None, @@ -115,10 +113,9 @@ where } } -impl Element for UniformList +impl Element for UniformList where - F: Fn(Range, &AppContext) -> G, - G: Iterator>, + F: Fn(Range, &mut Vec>, &AppContext), { fn layout( &mut self, @@ -135,8 +132,9 @@ where let mut item_constraint = SizeConstraint::new(vec2f(size.x(), 0.0), vec2f(size.x(), f32::INFINITY)); - let first_item = (self.build_items)(0..1, app).next(); - if let Some(mut first_item) = first_item { + self.items.clear(); + (self.append_items)(0..1, &mut self.items, app); + if let Some(first_item) = self.items.first_mut() { let mut item_size = first_item.layout(item_constraint, ctx, app); item_size.set_x(size.x()); item_constraint.min = item_size; @@ -158,7 +156,7 @@ where start + (size.y() / item_size.y()).ceil() as usize + 1, ); self.items.clear(); - self.items.extend((self.build_items)(start..end, app)); + (self.append_items)(start..end, &mut self.items, app); self.scroll_max = Some(item_size.y() * self.item_count as f32 - size.y()); diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index 800bfaba18..7c91a6e76a 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -18,4 +18,7 @@ pub mod platform; pub use pathfinder_color as color; pub use pathfinder_geometry as geometry; pub use platform::Event; -pub use presenter::*; +pub use presenter::{ + AfterLayoutContext, Axis, EventContext, LayoutContext, PaintContext, SizeConstraint, + Vector2FExt, +}; diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs new file mode 100644 index 0000000000..312c3dff4b --- /dev/null +++ b/zed/src/file_finder.rs @@ -0,0 +1,476 @@ +use crate::{ + editor::{buffer_view, BufferView}, + settings::Settings, + util, watch, + workspace::{Workspace, WorkspaceView}, + worktree::{match_paths, PathMatch, Worktree}, +}; +use gpui::{ + color::{ColorF, ColorU}, + elements::*, + fonts::{Properties, Weight}, + geometry::vector::vec2f, + keymap::{self, Binding}, + App, AppContext, Axis, Border, Entity, ModelHandle, View, ViewContext, ViewHandle, + WeakViewHandle, +}; +use std::cmp; + +pub struct FileFinder { + handle: WeakViewHandle, + settings: watch::Receiver, + workspace: ModelHandle, + query_buffer: ViewHandle, + search_count: usize, + latest_search_id: usize, + matches: Vec, + selected: usize, + list_state: UniformListState, +} + +pub fn init(app: &mut App) { + app.add_action("file_finder:toggle", FileFinder::toggle); + app.add_action("file_finder:confirm", FileFinder::confirm); + app.add_action("file_finder:select", FileFinder::select); + app.add_action("buffer:move_up", FileFinder::select_prev); + app.add_action("buffer:move_down", FileFinder::select_next); + app.add_action("uniform_list:scroll", FileFinder::scroll); + + app.add_bindings(vec![ + Binding::new("cmd-p", "file_finder:toggle", None), + Binding::new("escape", "file_finder:toggle", Some("FileFinder")), + Binding::new("enter", "file_finder:confirm", Some("FileFinder")), + ]); +} + +pub enum Event { + Selected(usize, usize), + Dismissed, +} + +impl Entity for FileFinder { + type Event = Event; +} + +impl View for FileFinder { + fn ui_name() -> &'static str { + "FileFinder" + } + + fn render(&self, _: &AppContext) -> Box { + 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()).boxed()) + .boxed(), + ) + .with_margin_top(12.0) + .with_uniform_padding(6.0) + .with_corner_radius(6.0) + .with_background_color(ColorU::new(0xff, 0xf2, 0xf2, 0xff)) + // .with_background_color(ColorU::new(0xf2, 0xf2, 0xf2, 0xff)) + .with_shadow( + vec2f(0.0, 4.0), + 12.0, + ColorF::new(0.0, 0.0, 0.0, 0.25).to_u8(), + ) + .boxed(), + ) + .with_max_width(600.0) + .with_max_height(400.0) + .boxed(), + ) + .top_center() + .boxed() + } + + fn on_focus(&mut self, ctx: &mut ViewContext) { + ctx.focus(&self.query_buffer); + } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut ctx = Self::default_keymap_context(); + ctx.set.insert("menu".into()); + ctx + } +} + +impl FileFinder { + fn render_matches(&self) -> Box { + if self.matches.is_empty() { + let settings = smol::block_on(self.settings.read()); + return Container::new( + Label::new( + "No matches".into(), + settings.ui_font_family, + settings.ui_font_size, + ) + .boxed(), + ) + .with_margin_top(6.0) + .boxed(); + } + + let handle = self.handle.clone(); + let list = UniformList::new( + self.list_state.clone(), + self.matches.len(), + move |mut range, items, app| { + let finder = handle.upgrade(app).unwrap(); + let finder = finder.as_ref(app); + let start = range.start; + range.end = cmp::min(range.end, finder.matches.len()); + items.extend(finder.matches[range].iter().enumerate().filter_map( + move |(i, path_match)| finder.render_match(path_match, start + i, app), + )); + }, + ); + + Container::new(list.boxed()) + .with_background_color(ColorU::new(0xf7, 0xf7, 0xf7, 0xff)) + .with_border(Border::all(1.0, ColorU::new(0xdb, 0xdb, 0xdc, 0xff))) + .with_margin_top(6.0) + .boxed() + } + + fn render_match( + &self, + path_match: &PathMatch, + index: usize, + app: &AppContext, + ) -> Option> { + let tree_id = path_match.tree_id; + let entry_id = path_match.entry_id; + + self.worktree(tree_id, app).map(|tree| { + let path = tree.entry_path(entry_id).unwrap(); + let file_name = path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + let mut path = path.to_string_lossy().to_string(); + if path_match.skipped_prefix_len > 0 { + let mut i = 0; + path.retain(|_| util::post_inc(&mut i) >= path_match.skipped_prefix_len) + } + + let path_positions = path_match.positions.clone(); + let file_name_start = path.chars().count() - file_name.chars().count(); + let mut file_name_positions = Vec::new(); + file_name_positions.extend(path_positions.iter().filter_map(|pos| { + if pos >= &file_name_start { + Some(pos - file_name_start) + } else { + None + } + })); + + let settings = smol::block_on(self.settings.read()); + let highlight_color = ColorU::new(0x30, 0x4e, 0xe2, 0xff); + let bold = *Properties::new().weight(Weight::BOLD); + + let mut container = Container::new( + Flex::row() + .with_child( + Container::new( + LineBox::new( + settings.ui_font_family, + settings.ui_font_size, + Svg::new("icons/file-16.svg".into()).boxed(), + ) + .boxed(), + ) + .with_padding_right(6.0) + .boxed(), + ) + .with_child( + Expanded::new( + 1.0, + Flex::column() + .with_child( + Label::new( + file_name, + settings.ui_font_family, + settings.ui_font_size, + ) + .with_highlights(highlight_color, bold, file_name_positions) + .boxed(), + ) + .with_child( + Label::new( + path.into(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_highlights(highlight_color, bold, path_positions) + .boxed(), + ) + .boxed(), + ) + .boxed(), + ) + .boxed(), + ) + .with_uniform_padding(6.0); + + if index == self.selected || index < self.matches.len() - 1 { + container = + container.with_border(Border::bottom(1.0, ColorU::new(0xdb, 0xdb, 0xdc, 0xff))); + } + + if index == self.selected { + container = container.with_background_color(ColorU::new(0xdb, 0xdb, 0xdc, 0xff)); + } + + EventHandler::new(container.boxed()) + .on_mouse_down(move |ctx, _| { + ctx.dispatch_action("file_finder:select", (tree_id, entry_id)); + true + }) + .boxed() + }) + } + + fn toggle(workspace_view: &mut WorkspaceView, _: &(), ctx: &mut ViewContext) { + workspace_view.toggle_modal(ctx, |ctx, workspace_view| { + let handle = ctx.add_view(|ctx| { + Self::new( + workspace_view.settings.clone(), + workspace_view.workspace.clone(), + ctx, + ) + }); + ctx.subscribe_to_view(&handle, Self::on_event); + handle + }); + } + + fn on_event( + workspace_view: &mut WorkspaceView, + _: ViewHandle, + event: &Event, + ctx: &mut ViewContext, + ) { + match event { + Event::Selected(tree_id, entry_id) => { + workspace_view.open_entry((*tree_id, *entry_id), ctx); + workspace_view.dismiss_modal(ctx); + } + Event::Dismissed => { + workspace_view.dismiss_modal(ctx); + } + } + } + + pub fn new( + settings: watch::Receiver, + workspace: ModelHandle, + ctx: &mut ViewContext, + ) -> Self { + ctx.observe(&workspace, Self::workspace_updated); + + let query_buffer = ctx.add_view(|ctx| BufferView::single_line(settings.clone(), ctx)); + ctx.subscribe_to_view(&query_buffer, Self::on_query_buffer_event); + + settings.notify_view_on_change(ctx); + + Self { + handle: ctx.handle(), + settings, + workspace, + query_buffer, + search_count: 0, + latest_search_id: 0, + matches: Vec::new(), + selected: 0, + list_state: UniformListState::new(), + } + } + + fn workspace_updated(&mut self, _: ModelHandle, ctx: &mut ViewContext) { + self.spawn_search(self.query_buffer.as_ref(ctx).text(ctx.app()), ctx); + } + + fn on_query_buffer_event( + &mut self, + _: ViewHandle, + event: &buffer_view::Event, + ctx: &mut ViewContext, + ) { + use buffer_view::Event::*; + match event { + Edited => { + let query = self.query_buffer.as_ref(ctx).text(ctx.app()); + if query.is_empty() { + self.latest_search_id = util::post_inc(&mut self.search_count); + self.matches.clear(); + ctx.notify(); + } else { + self.spawn_search(query, ctx); + } + } + Blurred => ctx.emit(Event::Dismissed), + Activate => {} + } + } + + fn select_prev(&mut self, _: &(), ctx: &mut ViewContext) { + if self.selected > 0 { + self.selected -= 1; + } + self.list_state.scroll_to(self.selected); + ctx.notify(); + } + + fn select_next(&mut self, _: &(), ctx: &mut ViewContext) { + if self.selected + 1 < self.matches.len() { + self.selected += 1; + } + self.list_state.scroll_to(self.selected); + ctx.notify(); + } + + fn scroll(&mut self, _: &f32, ctx: &mut ViewContext) { + ctx.notify(); + } + + fn confirm(&mut self, _: &(), ctx: &mut ViewContext) { + if let Some(m) = self.matches.get(self.selected) { + ctx.emit(Event::Selected(m.tree_id, m.entry_id)); + } + } + + fn select(&mut self, entry: &(usize, usize), ctx: &mut ViewContext) { + let (tree_id, entry_id) = *entry; + log::info!("selected item! {} {}", tree_id, entry_id); + ctx.emit(Event::Selected(tree_id, entry_id)); + } + + fn spawn_search(&mut self, query: String, ctx: &mut ViewContext) { + log::info!("spawn search!"); + + let worktrees = self.worktrees(ctx.app()); + let search_id = util::post_inc(&mut self.search_count); + let task = ctx.background_executor().spawn(async move { + let matches = match_paths(worktrees.as_slice(), &query, false, false, 100); + (search_id, matches) + }); + + ctx.spawn(task, Self::update_matches).detach(); + } + + fn update_matches( + &mut self, + (search_id, matches): (usize, Vec), + ctx: &mut ViewContext, + ) { + if search_id >= self.latest_search_id { + self.latest_search_id = search_id; + self.matches = matches; + self.selected = 0; + self.list_state.scroll_to(0); + ctx.notify(); + } + } + + fn worktree<'a>(&'a self, tree_id: usize, app: &'a AppContext) -> Option<&'a Worktree> { + self.workspace + .as_ref(app) + .worktrees() + .get(&tree_id) + .map(|worktree| worktree.as_ref(app)) + } + + fn worktrees(&self, app: &AppContext) -> Vec { + self.workspace + .as_ref(app) + .worktrees() + .iter() + .map(|worktree| worktree.as_ref(app).clone()) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + editor, settings, + workspace::{Workspace, WorkspaceView}, + }; + use anyhow::Result; + use gpui::App; + use smol::fs; + use tempdir::TempDir; + + #[test] + fn test_matching_paths() -> Result<()> { + App::test((), |mut app| async move { + let tmp_dir = TempDir::new("example")?; + fs::create_dir(tmp_dir.path().join("a")).await?; + fs::write(tmp_dir.path().join("a/banana"), "banana").await?; + fs::write(tmp_dir.path().join("a/bandana"), "bandana").await?; + super::init(&mut app); + editor::init(&mut app); + + let settings = settings::channel(&app.fonts()).unwrap().1; + let workspace = app.add_model(|ctx| Workspace::new(vec![tmp_dir.path().into()], ctx)); + let (window_id, workspace_view) = + app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); + app.finish_pending_tasks().await; // Open and populate worktree. + app.dispatch_action( + window_id, + vec![workspace_view.id()], + "file_finder:toggle".into(), + (), + ); + let (finder, query_buffer) = workspace_view.read(&app, |view, ctx| { + let finder = view + .modal() + .cloned() + .unwrap() + .downcast::() + .unwrap(); + let query_buffer = finder.as_ref(ctx).query_buffer.clone(); + (finder, query_buffer) + }); + + let chain = vec![finder.id(), query_buffer.id()]; + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string()); + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string()); + app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string()); + app.finish_pending_tasks().await; // Complete path search. + + // let view_state = finder.state(&app); + // assert!(view_state.matches.len() > 1); + // app.dispatch_action( + // window_id, + // vec![workspace_view.id(), finder.id()], + // "menu:select_next", + // (), + // ); + // app.dispatch_action( + // window_id, + // vec![workspace_view.id(), finder.id()], + // "file_finder:confirm", + // (), + // ); + // app.finish_pending_tasks().await; // Load Buffer and open BufferView. + // let active_pane = workspace_view.read(&app, |view, _| view.active_pane().clone()); + // assert_eq!( + // active_pane.state(&app), + // pane::State { + // tabs: vec![pane::TabState { + // title: "bandana".into(), + // active: true, + // }] + // } + // ); + Ok(()) + }) + } +} diff --git a/zed/src/lib.rs b/zed/src/lib.rs index baea784d16..a66a892ebf 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,5 +1,6 @@ pub mod assets; pub mod editor; +pub mod file_finder; mod operation_queue; pub mod settings; mod sum_tree; diff --git a/zed/src/main.rs b/zed/src/main.rs index ee9e22e8db..a315f07bcb 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -4,7 +4,7 @@ use log::LevelFilter; use simplelog::SimpleLogger; use std::{fs, path::PathBuf}; use zed::{ - assets, editor, settings, + assets, editor, file_finder, settings, workspace::{self, OpenParams}, }; @@ -20,6 +20,7 @@ fn main() { .on_finish_launching(move || { workspace::init(&mut app); editor::init(&mut app); + file_finder::init(&mut app); if stdout_is_a_pty() { app.platform().activate(true); diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 49aed9967f..5d991cd15d 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,8 +1,7 @@ use super::{ItemViewHandle, SplitDirection}; use crate::{settings::Settings, watch}; use gpui::{ - color::ColorU, elements::*, keymap::Binding, App, AppContext, Border, ChildView, Entity, View, - ViewContext, + color::ColorU, elements::*, keymap::Binding, App, AppContext, Border, Entity, View, ViewContext, }; use std::cmp; diff --git a/zed/src/workspace/pane_group.rs b/zed/src/workspace/pane_group.rs index 96e85634ce..b69c9046ef 100644 --- a/zed/src/workspace/pane_group.rs +++ b/zed/src/workspace/pane_group.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use gpui::{ color::{rgbu, ColorU}, elements::*, - Axis, Border, ChildView, + Axis, Border, }; #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 010cb7740f..a509c4a587 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -1,9 +1,8 @@ use super::{pane, Pane, PaneGroup, SplitDirection, Workspace}; use crate::{settings::Settings, watch}; -use gpui::{color::rgbu, ChildView}; use gpui::{ - elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, View, - ViewContext, ViewHandle, + color::rgbu, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, + View, ViewContext, ViewHandle, }; use log::{error, info}; use std::{collections::HashSet, path::PathBuf};