From d561f50ab1402bc4e86cd053d556b3d6234aab96 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 23 Sep 2021 16:27:55 -0700 Subject: [PATCH 01/26] Extract from Workspace a Project model that stores a window's worktrees --- server/src/rpc.rs | 6 +-- zed/src/file_finder.rs | 2 +- zed/src/lib.rs | 1 + zed/src/project.rs | 88 +++++++++++++++++++++++++++++++++ zed/src/workspace.rs | 110 ++++++++++++----------------------------- 5 files changed, 125 insertions(+), 82 deletions(-) create mode 100644 zed/src/project.rs diff --git a/server/src/rpc.rs b/server/src/rpc.rs index a9ffdad8997d8e2338223b0afdfdd05e3981fd1b..ea0a6c2968be6e39346c0fb834c72a53d30edcd2 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -1157,13 +1157,13 @@ mod tests { ); }); workspace_b - .condition(&cx_b, |workspace, _| workspace.worktrees().len() == 1) + .condition(&cx_b, |workspace, cx| workspace.worktrees(cx).len() == 1) .await; let local_worktree_id_b = workspace_b.read_with(&cx_b, |workspace, cx| { let active_pane = workspace.active_pane().read(cx); assert!(active_pane.active_item().is_none()); - workspace.worktrees().iter().next().unwrap().id() + workspace.worktrees(cx).first().unwrap().id() }); workspace_b .update(&mut cx_b, |worktree, cx| { @@ -1180,7 +1180,7 @@ mod tests { tree.as_local_mut().unwrap().unshare(cx); }); workspace_b - .condition(&cx_b, |workspace, _| workspace.worktrees().len() == 0) + .condition(&cx_b, |workspace, cx| workspace.worktrees(cx).len() == 0) .await; workspace_b.read_with(&cx_b, |workspace, cx| { let active_pane = workspace.active_pane().read(cx); diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 2749bbe59d7e3dd9d4132b1d3654a01fb91fe50a..caf560c16a54cd6ceadf2f4e035d25d3b26566a2 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -385,7 +385,7 @@ impl FileFinder { .workspace .upgrade(&cx)? .read(cx) - .worktrees() + .worktrees(cx) .iter() .map(|tree| tree.read(cx).snapshot()) .collect::>(); diff --git a/zed/src/lib.rs b/zed/src/lib.rs index b7fa3f83d06b00f730945cccd40f2f7a7869a077..bd09e386ee087add09233abf9d61014b6481f846 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -9,6 +9,7 @@ pub mod http; pub mod language; pub mod menus; pub mod people_panel; +pub mod project; pub mod project_browser; pub mod rpc; pub mod settings; diff --git a/zed/src/project.rs b/zed/src/project.rs new file mode 100644 index 0000000000000000000000000000000000000000..f05e4e4a8d94b0c4a0a2a7d0d408789ddc9a0039 --- /dev/null +++ b/zed/src/project.rs @@ -0,0 +1,88 @@ +use super::worktree::Worktree; +use anyhow::Result; +use gpui::{Entity, ModelContext, ModelHandle, Task}; + +pub struct Project { + worktrees: Vec>, +} + +pub enum Event {} + +impl Project { + pub fn new() -> Self { + Self { + worktrees: Default::default(), + } + } + + pub fn worktrees(&self) -> &[ModelHandle] { + &self.worktrees + } + + pub fn worktree_for_id(&self, id: usize) -> Option> { + self.worktrees + .iter() + .find(|worktree| worktree.id() == id) + .cloned() + } + + pub fn add_worktree(&mut self, worktree: ModelHandle) { + self.worktrees.push(worktree); + } + + pub fn share_worktree( + &self, + remote_id: u64, + cx: &mut ModelContext, + ) -> Option>> { + for worktree in &self.worktrees { + let task = worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().and_then(|worktree| { + if worktree.remote_id() == Some(remote_id) { + Some(worktree.share(cx)) + } else { + None + } + }) + }); + if task.is_some() { + return task; + } + } + None + } + + pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext) { + for worktree in &self.worktrees { + if worktree.update(cx, |worktree, cx| { + if let Some(worktree) = worktree.as_local_mut() { + if worktree.remote_id() == Some(remote_id) { + worktree.unshare(cx); + return true; + } + } + false + }) { + break; + } + } + } + + pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext) { + self.worktrees.retain(|worktree| { + worktree.update(cx, |worktree, cx| { + if let Some(worktree) = worktree.as_remote_mut() { + if worktree.remote_id() == id { + worktree.close_all_buffers(cx); + return false; + } + } + true + }) + }); + } +} + +impl Entity for Project { + type Event = Event; +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 456b9e7042ab648be9e895d71f518718facba654..8d46a11309d97de050045c96dcfe07abba066a88 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -8,6 +8,7 @@ use crate::{ fs::Fs, language::LanguageRegistry, people_panel::{JoinWorktree, LeaveWorktree, PeoplePanel, ShareWorktree, UnshareWorktree}, + project::Project, project_browser::ProjectBrowser, rpc, settings::Settings, @@ -34,7 +35,7 @@ pub use pane_group::*; use postage::{prelude::Stream, watch}; use sidebar::{Side, Sidebar, ToggleSidebarItem}; use std::{ - collections::{hash_map::Entry, HashMap, HashSet}, + collections::{hash_map::Entry, HashMap}, future::Future, path::{Path, PathBuf}, sync::Arc, @@ -349,7 +350,7 @@ pub struct Workspace { right_sidebar: Sidebar, panes: Vec>, active_pane: ViewHandle, - worktrees: HashSet>, + project: ModelHandle, items: Vec>, loading_items: HashMap< (usize, Arc), @@ -360,6 +361,8 @@ pub struct Workspace { impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { + let project = cx.add_model(|_| Project::new()); + let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); cx.subscribe(&pane, move |me, _, event, cx| { @@ -424,15 +427,15 @@ impl Workspace { fs: app_state.fs.clone(), left_sidebar, right_sidebar, - worktrees: Default::default(), + project, items: Default::default(), loading_items: Default::default(), _observe_current_user, } } - pub fn worktrees(&self) -> &HashSet> { - &self.worktrees + pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle] { + &self.project.read(cx).worktrees() } pub fn contains_paths(&self, paths: &[PathBuf], cx: &AppContext) -> bool { @@ -440,7 +443,7 @@ impl Workspace { } pub fn contains_path(&self, path: &Path, cx: &AppContext) -> bool { - for worktree in &self.worktrees { + for worktree in self.worktrees(cx) { let worktree = worktree.read(cx).as_local(); if worktree.map_or(false, |w| w.contains_abs_path(path)) { return true; @@ -451,7 +454,7 @@ impl Workspace { pub fn worktree_scans_complete(&self, cx: &AppContext) -> impl Future + 'static { let futures = self - .worktrees + .worktrees(cx) .iter() .filter_map(|worktree| worktree.read(cx).as_local()) .map(|worktree| worktree.scan_complete()) @@ -511,7 +514,7 @@ impl Workspace { cx.spawn(|this, mut cx| async move { let mut entry_id = None; this.read_with(&cx, |this, cx| { - for tree in this.worktrees.iter() { + for tree in this.worktrees(cx) { if let Some(relative_path) = tree .read(cx) .as_local() @@ -559,7 +562,9 @@ impl Workspace { let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?; this.update(&mut cx, |this, cx| { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - this.worktrees.insert(worktree.clone()); + this.project.update(cx, |project, _| { + project.add_worktree(worktree.clone()); + }); cx.notify(); }); Ok(worktree) @@ -616,7 +621,7 @@ impl Workspace { let (worktree_id, path) = entry.clone(); - let worktree = match self.worktrees.get(&worktree_id).cloned() { + let worktree = match self.project.read(cx).worktree_for_id(worktree_id) { Some(worktree) => worktree, None => { log::error!("worktree {} does not exist", worktree_id); @@ -731,7 +736,7 @@ impl Workspace { if let Some(item) = self.active_item(cx) { let handle = cx.handle(); if item.entry_id(cx.as_ref()).is_none() { - let worktree = self.worktrees.iter().next(); + let worktree = self.worktrees(cx).first(); let start_abs_path = worktree .and_then(|w| w.read(cx).as_local()) .map_or(Path::new(""), |w| w.abs_path()) @@ -830,22 +835,8 @@ impl Workspace { rpc.authenticate_and_connect(&cx).await?; let task = this.update(&mut cx, |this, cx| { - for worktree in &this.worktrees { - let task = worktree.update(cx, |worktree, cx| { - worktree.as_local_mut().and_then(|worktree| { - if worktree.remote_id() == Some(remote_id) { - Some(worktree.share(cx)) - } else { - None - } - }) - }); - - if task.is_some() { - return task; - } - } - None + this.project + .update(cx, |project, cx| project.share_worktree(remote_id, cx)) }); if let Some(share_task) = task { @@ -861,19 +852,8 @@ impl Workspace { fn unshare_worktree(&mut self, action: &UnshareWorktree, cx: &mut ViewContext) { let remote_id = action.0; - for worktree in &self.worktrees { - if worktree.update(cx, |worktree, cx| { - if let Some(worktree) = worktree.as_local_mut() { - if worktree.remote_id() == Some(remote_id) { - worktree.unshare(cx); - return true; - } - } - false - }) { - break; - } - } + self.project + .update(cx, |project, cx| project.unshare_worktree(remote_id, cx)); } fn join_worktree(&mut self, action: &JoinWorktree, cx: &mut ViewContext) { @@ -890,23 +870,15 @@ impl Workspace { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); cx.subscribe(&worktree, move |this, _, event, cx| match event { worktree::Event::Closed => { - this.worktrees.retain(|worktree| { - worktree.update(cx, |worktree, cx| { - if let Some(worktree) = worktree.as_remote_mut() { - if worktree.remote_id() == worktree_id { - worktree.close_all_buffers(cx); - return false; - } - } - true - }) + this.project.update(cx, |project, cx| { + project.close_remote_worktree(worktree_id, cx); }); - cx.notify(); } }) .detach(); - this.worktrees.insert(worktree); + this.project + .update(cx, |project, _| project.add_worktree(worktree)); cx.notify(); }); @@ -919,27 +891,9 @@ impl Workspace { fn leave_worktree(&mut self, action: &LeaveWorktree, cx: &mut ViewContext) { let remote_id = action.0; - cx.spawn(|this, mut cx| { - async move { - this.update(&mut cx, |this, cx| { - this.worktrees.retain(|worktree| { - worktree.update(cx, |worktree, cx| { - if let Some(worktree) = worktree.as_remote_mut() { - if worktree.remote_id() == remote_id { - worktree.close_all_buffers(cx); - return false; - } - } - true - }) - }) - }); - - Ok(()) - } - .log_err() - }) - .detach(); + self.project.update(cx, |project, cx| { + project.close_remote_worktree(remote_id, cx); + }); } fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { @@ -1186,7 +1140,7 @@ pub trait WorkspaceHandle { impl WorkspaceHandle for ViewHandle { fn file_entries(&self, cx: &AppContext) -> Vec<(usize, Arc)> { self.read(cx) - .worktrees() + .worktrees(cx) .iter() .flat_map(|tree| { let tree_id = tree.id(); @@ -1254,8 +1208,8 @@ mod tests { .await; assert_eq!(cx.window_ids().len(), 1); let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); - workspace_1.read_with(&cx, |workspace, _| { - assert_eq!(workspace.worktrees().len(), 2) + workspace_1.read_with(&cx, |workspace, cx| { + assert_eq!(workspace.worktrees(cx).len(), 2) }); cx.update(|cx| { @@ -1433,7 +1387,7 @@ mod tests { cx.read(|cx| { let worktree_roots = workspace .read(cx) - .worktrees() + .worktrees(cx) .iter() .map(|w| w.read(cx).as_local().unwrap().abs_path()) .collect::>(); @@ -1526,7 +1480,7 @@ mod tests { let tree = cx.read(|cx| { workspace .read(cx) - .worktrees() + .worktrees(cx) .iter() .next() .unwrap() From 7eda614c4a778a74b519150034bf09f36baa73e4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 23 Sep 2021 18:14:15 -0700 Subject: [PATCH 02/26] Start work on ProjectPanel --- zed/assets/themes/_base.toml | 4 ++ zed/src/lib.rs | 2 +- zed/src/project.rs | 108 ++++++++++++++++++++++++++------ zed/src/project_browser.rs | 19 ------ zed/src/project_panel.rs | 118 +++++++++++++++++++++++++++++++++++ zed/src/theme.rs | 8 +++ zed/src/workspace.rs | 94 +++++----------------------- zed/src/worktree.rs | 73 +++++++++++++++++----- 8 files changed, 293 insertions(+), 133 deletions(-) delete mode 100644 zed/src/project_browser.rs create mode 100644 zed/src/project_panel.rs diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 3e326ab62c6168673be6888778b3cd11317829e5..40661a4030f78c0248581a23bf09f420a580e3fc 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -159,6 +159,10 @@ extends = "$people_panel.shared_worktree" background = "$state.hover" corner_radius = 6 +[project_panel] +extends = "$panel" +entry = "$text.0" + [selector] background = "$surface.0" padding = 8 diff --git a/zed/src/lib.rs b/zed/src/lib.rs index bd09e386ee087add09233abf9d61014b6481f846..12a38938b85c38ac62b44d64ff9ce5151ef0ff07 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -10,7 +10,7 @@ pub mod language; pub mod menus; pub mod people_panel; pub mod project; -pub mod project_browser; +pub mod project_panel; pub mod rpc; pub mod settings; #[cfg(any(test, feature = "test-support"))] diff --git a/zed/src/project.rs b/zed/src/project.rs index f05e4e4a8d94b0c4a0a2a7d0d408789ddc9a0039..18facab61b313f672dc05ec27b36033d2a801c1e 100644 --- a/zed/src/project.rs +++ b/zed/src/project.rs @@ -1,17 +1,31 @@ -use super::worktree::Worktree; +use crate::{ + fs::Fs, + language::LanguageRegistry, + rpc::Client, + util::TryFutureExt as _, + worktree::{self, Worktree}, + AppState, +}; use anyhow::Result; use gpui::{Entity, ModelContext, ModelHandle, Task}; +use std::{path::Path, sync::Arc}; pub struct Project { worktrees: Vec>, + languages: Arc, + rpc: Arc, + fs: Arc, } pub enum Event {} impl Project { - pub fn new() -> Self { + pub fn new(app_state: &AppState) -> Self { Self { worktrees: Default::default(), + languages: app_state.languages.clone(), + rpc: app_state.rpc.clone(), + fs: app_state.fs.clone(), } } @@ -26,30 +40,88 @@ impl Project { .cloned() } - pub fn add_worktree(&mut self, worktree: ModelHandle) { - self.worktrees.push(worktree); + pub fn add_local_worktree( + &mut self, + path: &Path, + cx: &mut ModelContext, + ) -> Task>> { + let fs = self.fs.clone(); + let rpc = self.rpc.clone(); + let languages = self.languages.clone(); + let path = Arc::from(path); + cx.spawn(|this, mut cx| async move { + let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.add_worktree(worktree.clone(), cx); + }); + Ok(worktree) + }) } - pub fn share_worktree( - &self, + pub fn add_remote_worktree( + &mut self, remote_id: u64, cx: &mut ModelContext, - ) -> Option>> { - for worktree in &self.worktrees { - let task = worktree.update(cx, |worktree, cx| { - worktree.as_local_mut().and_then(|worktree| { - if worktree.remote_id() == Some(remote_id) { - Some(worktree.share(cx)) - } else { - None + ) -> Task>> { + let rpc = self.rpc.clone(); + let languages = self.languages.clone(); + cx.spawn(|this, mut cx| async move { + rpc.authenticate_and_connect(&cx).await?; + let worktree = + Worktree::open_remote(rpc.clone(), remote_id, languages, &mut cx).await?; + this.update(&mut cx, |this, cx| { + cx.subscribe(&worktree, move |this, _, event, cx| match event { + worktree::Event::Closed => { + this.close_remote_worktree(remote_id, cx); + cx.notify(); } }) + .detach(); + this.add_worktree(worktree.clone(), cx); }); - if task.is_some() { - return task; + Ok(worktree) + }) + } + + fn add_worktree(&mut self, worktree: ModelHandle, cx: &mut ModelContext) { + cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); + self.worktrees.push(worktree); + cx.notify(); + } + + pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext) { + let rpc = self.rpc.clone(); + cx.spawn(|this, mut cx| { + async move { + rpc.authenticate_and_connect(&cx).await?; + + let task = this.update(&mut cx, |this, cx| { + for worktree in &this.worktrees { + let task = worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().and_then(|worktree| { + if worktree.remote_id() == Some(remote_id) { + Some(worktree.share(cx)) + } else { + None + } + }) + }); + if task.is_some() { + return task; + } + } + None + }); + + if let Some(task) = task { + task.await?; + } + + Ok(()) } - } - None + .log_err() + }) + .detach(); } pub fn unshare_worktree(&mut self, remote_id: u64, cx: &mut ModelContext) { diff --git a/zed/src/project_browser.rs b/zed/src/project_browser.rs deleted file mode 100644 index 796441c7041e737ec9a78dff95f49dc3cc09595f..0000000000000000000000000000000000000000 --- a/zed/src/project_browser.rs +++ /dev/null @@ -1,19 +0,0 @@ -use gpui::{elements::Empty, Element, Entity, View}; - -pub struct ProjectBrowser; - -pub enum Event {} - -impl Entity for ProjectBrowser { - type Event = Event; -} - -impl View for ProjectBrowser { - fn ui_name() -> &'static str { - "ProjectBrowser" - } - - fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - Empty::new().boxed() - } -} diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..4a017a3fe88beaf51af2141c2c8e21be1b86e83e --- /dev/null +++ b/zed/src/project_panel.rs @@ -0,0 +1,118 @@ +use crate::{ + project::Project, + theme::Theme, + worktree::{self, Worktree}, + Settings, +}; +use gpui::{ + elements::{Empty, Label, List, ListState, Orientation}, + AppContext, Element, ElementBox, Entity, ModelHandle, View, ViewContext, +}; +use postage::watch; + +pub struct ProjectPanel { + project: ModelHandle, + list: ListState, + settings: watch::Receiver, +} + +pub enum Event {} + +impl ProjectPanel { + pub fn new( + project: ModelHandle, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&project, |this, project, cx| { + let project = project.read(cx); + this.list.reset(Self::entry_count(project, cx)); + cx.notify(); + }) + .detach(); + + Self { + list: ListState::new( + { + let project = project.read(cx); + Self::entry_count(project, cx) + }, + Orientation::Top, + 1000., + { + let project = project.clone(); + let settings = settings.clone(); + move |ix, cx| { + let project = project.read(cx); + Self::render_entry_at_index(project, ix, &settings.borrow().theme, cx) + } + }, + ), + project, + settings, + } + } + + fn entry_count(project: &Project, cx: &AppContext) -> usize { + project + .worktrees() + .iter() + .map(|worktree| worktree.read(cx).visible_entry_count()) + .sum() + } + + fn render_entry_at_index( + project: &Project, + mut ix: usize, + theme: &Theme, + cx: &AppContext, + ) -> ElementBox { + for worktree in project.worktrees() { + let worktree = worktree.read(cx); + let visible_entry_count = worktree.visible_entry_count(); + if ix < visible_entry_count { + let entry = worktree.visible_entries(ix).next().unwrap(); + return Self::render_entry(worktree, entry, theme, cx); + } else { + ix -= visible_entry_count; + } + } + Empty::new().boxed() + } + + fn render_entry( + worktree: &Worktree, + entry: &worktree::Entry, + theme: &Theme, + _: &AppContext, + ) -> ElementBox { + let path = &entry.path; + let depth = path.iter().count() as f32; + Label::new( + path.file_name() + .map_or(String::new(), |s| s.to_string_lossy().to_string()), + theme.project_panel.entry.clone(), + ) + .contained() + .with_margin_left(depth * 20.) + .boxed() + } +} + +impl View for ProjectPanel { + fn ui_name() -> &'static str { + "ProjectPanel" + } + + fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + let theme = &self.settings.borrow().theme.project_panel; + List::new(self.list.clone()) + .contained() + .with_style(theme.container) + .boxed() + } +} + +impl Entity for ProjectPanel { + type Event = Event; +} diff --git a/zed/src/theme.rs b/zed/src/theme.rs index a5378fe033c2f70c7767b75a7fe9cc6ec434b47d..a8ef40a37c48c96c8a3a77edffe86f528c949e6f 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -25,6 +25,7 @@ pub struct Theme { pub workspace: Workspace, pub chat_panel: ChatPanel, pub people_panel: PeoplePanel, + pub project_panel: ProjectPanel, pub selector: Selector, pub editor: EditorStyle, pub syntax: SyntaxTheme, @@ -106,6 +107,13 @@ pub struct ChatPanel { pub hovered_sign_in_prompt: TextStyle, } +#[derive(Deserialize)] +pub struct ProjectPanel { + #[serde(flatten)] + pub container: ContainerStyle, + pub entry: TextStyle, +} + #[derive(Deserialize)] pub struct PeoplePanel { #[serde(flatten)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 8d46a11309d97de050045c96dcfe07abba066a88..f5f11e304821d73410806291d110f4d835c02bfb 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -6,15 +6,13 @@ use crate::{ chat_panel::ChatPanel, editor::Buffer, fs::Fs, - language::LanguageRegistry, people_panel::{JoinWorktree, LeaveWorktree, PeoplePanel, ShareWorktree, UnshareWorktree}, project::Project, - project_browser::ProjectBrowser, + project_panel::ProjectPanel, rpc, settings::Settings, user, - util::TryFutureExt as _, - worktree::{self, File, Worktree}, + worktree::{File, Worktree}, AppState, Authenticate, }; use anyhow::Result; @@ -340,7 +338,6 @@ impl Clone for Box { pub struct Workspace { pub settings: watch::Receiver, - languages: Arc, rpc: Arc, user_store: ModelHandle, fs: Arc, @@ -361,7 +358,8 @@ pub struct Workspace { impl Workspace { pub fn new(app_state: &AppState, cx: &mut ViewContext) -> Self { - let project = cx.add_model(|_| Project::new()); + let project = cx.add_model(|_| Project::new(app_state)); + cx.observe(&project, |_, _, cx| cx.notify()).detach(); let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); @@ -374,7 +372,8 @@ impl Workspace { let mut left_sidebar = Sidebar::new(Side::Left); left_sidebar.add_item( "icons/folder-tree-16.svg", - cx.add_view(|_| ProjectBrowser).into(), + cx.add_view(|cx| ProjectPanel::new(project.clone(), app_state.settings.clone(), cx)) + .into(), ); let mut right_sidebar = Sidebar::new(Side::Right); @@ -421,7 +420,6 @@ impl Workspace { panes: vec![pane.clone()], active_pane: pane.clone(), settings: app_state.settings.clone(), - languages: app_state.languages.clone(), rpc: app_state.rpc.clone(), user_store: app_state.user_store.clone(), fs: app_state.fs.clone(), @@ -554,21 +552,8 @@ impl Workspace { path: &Path, cx: &mut ViewContext, ) -> Task>> { - let languages = self.languages.clone(); - let rpc = self.rpc.clone(); - let fs = self.fs.clone(); - let path = Arc::from(path); - cx.spawn(|this, mut cx| async move { - let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?; - this.update(&mut cx, |this, cx| { - cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - this.project.update(cx, |project, _| { - project.add_worktree(worktree.clone()); - }); - cx.notify(); - }); - Ok(worktree) - }) + self.project + .update(cx, |project, cx| project.add_local_worktree(path, cx)) } pub fn toggle_modal(&mut self, cx: &mut ViewContext, add_view: F) @@ -828,72 +813,23 @@ impl Workspace { } fn share_worktree(&mut self, action: &ShareWorktree, cx: &mut ViewContext) { - let rpc = self.rpc.clone(); - let remote_id = action.0; - cx.spawn(|this, mut cx| { - async move { - rpc.authenticate_and_connect(&cx).await?; - - let task = this.update(&mut cx, |this, cx| { - this.project - .update(cx, |project, cx| project.share_worktree(remote_id, cx)) - }); - - if let Some(share_task) = task { - share_task.await?; - } - - Ok(()) - } - .log_err() - }) - .detach(); + self.project + .update(cx, |p, cx| p.share_worktree(action.0, cx)); } fn unshare_worktree(&mut self, action: &UnshareWorktree, cx: &mut ViewContext) { - let remote_id = action.0; self.project - .update(cx, |project, cx| project.unshare_worktree(remote_id, cx)); + .update(cx, |p, cx| p.unshare_worktree(action.0, cx)); } fn join_worktree(&mut self, action: &JoinWorktree, cx: &mut ViewContext) { - let rpc = self.rpc.clone(); - let languages = self.languages.clone(); - let worktree_id = action.0; - - cx.spawn(|this, mut cx| { - async move { - rpc.authenticate_and_connect(&cx).await?; - let worktree = - Worktree::open_remote(rpc.clone(), worktree_id, languages, &mut cx).await?; - this.update(&mut cx, |this, cx| { - cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&worktree, move |this, _, event, cx| match event { - worktree::Event::Closed => { - this.project.update(cx, |project, cx| { - project.close_remote_worktree(worktree_id, cx); - }); - cx.notify(); - } - }) - .detach(); - this.project - .update(cx, |project, _| project.add_worktree(worktree)); - cx.notify(); - }); - - Ok(()) - } - .log_err() - }) - .detach(); + self.project + .update(cx, |p, cx| p.add_remote_worktree(action.0, cx).detach()); } fn leave_worktree(&mut self, action: &LeaveWorktree, cx: &mut ViewContext) { - let remote_id = action.0; - self.project.update(cx, |project, cx| { - project.close_remote_worktree(remote_id, cx); - }); + self.project + .update(cx, |p, cx| p.close_remote_worktree(action.0, cx)); } fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 15cfd29463747ccc4e0b25b50f7cb6fc014f9cb2..4fe9a31b67eb4b80b0cc3770afcf93836d972923 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1474,12 +1474,24 @@ impl Snapshot { self.entries_by_path.summary().file_count } + pub fn visible_entry_count(&self) -> usize { + self.entries_by_path.summary().visible_count + } + pub fn visible_file_count(&self) -> usize { self.entries_by_path.summary().visible_file_count } - pub fn files(&self, start: usize) -> FileIter { - FileIter::all(self, start) + pub fn files(&self, start: usize) -> EntryIter { + EntryIter::files(self, start) + } + + pub fn visible_entries(&self, start: usize) -> EntryIter { + EntryIter::visible(self, start) + } + + pub fn visible_files(&self, start: usize) -> EntryIter { + EntryIter::visible_files(self, start) } pub fn paths(&self) -> impl Iterator> { @@ -1490,10 +1502,6 @@ impl Snapshot { .map(|entry| &entry.path) } - pub fn visible_files(&self, start: usize) -> FileIter { - FileIter::visible(self, start) - } - fn child_entries<'a>(&'a self, path: &'a Path) -> ChildEntriesIter<'a> { ChildEntriesIter::new(path, self) } @@ -1891,22 +1899,31 @@ impl sum_tree::Item for Entry { fn summary(&self) -> Self::Summary { let file_count; + let visible_count; let visible_file_count; if self.is_file() { file_count = 1; if self.is_ignored { + visible_count = 0; visible_file_count = 0; } else { + visible_count = 1; visible_file_count = 1; } } else { file_count = 0; visible_file_count = 0; + if self.is_ignored { + visible_count = 0; + } else { + visible_count = 1; + } } EntrySummary { max_path: self.path.clone(), file_count, + visible_count, visible_file_count, } } @@ -1925,6 +1942,7 @@ pub struct EntrySummary { max_path: Arc, file_count: usize, visible_file_count: usize, + visible_count: usize, } impl Default for EntrySummary { @@ -1932,6 +1950,7 @@ impl Default for EntrySummary { Self { max_path: Arc::from(Path::new("")), file_count: 0, + visible_count: 0, visible_file_count: 0, } } @@ -1943,6 +1962,7 @@ impl sum_tree::Summary for EntrySummary { fn add_summary(&mut self, rhs: &Self, _: &()) { self.max_path = rhs.max_path.clone(); self.file_count += rhs.file_count; + self.visible_count += rhs.visible_count; self.visible_file_count += rhs.visible_file_count; } } @@ -2054,6 +2074,15 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { } } +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct VisibleCount(usize); + +impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleCount { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + self.0 += summary.visible_count; + } +} + #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct VisibleFileCount(usize); @@ -2555,31 +2584,42 @@ impl WorktreeHandle for ModelHandle { } } -pub enum FileIter<'a> { - All(Cursor<'a, Entry, FileCount, ()>), - Visible(Cursor<'a, Entry, VisibleFileCount, ()>), +pub enum EntryIter<'a> { + Files(Cursor<'a, Entry, FileCount, ()>), + Visible(Cursor<'a, Entry, VisibleCount, ()>), + VisibleFiles(Cursor<'a, Entry, VisibleFileCount, ()>), } -impl<'a> FileIter<'a> { - fn all(snapshot: &'a Snapshot, start: usize) -> Self { +impl<'a> EntryIter<'a> { + fn files(snapshot: &'a Snapshot, start: usize) -> Self { let mut cursor = snapshot.entries_by_path.cursor(); cursor.seek(&FileCount(start), Bias::Right, &()); - Self::All(cursor) + Self::Files(cursor) } fn visible(snapshot: &'a Snapshot, start: usize) -> Self { let mut cursor = snapshot.entries_by_path.cursor(); - cursor.seek(&VisibleFileCount(start), Bias::Right, &()); + cursor.seek(&VisibleCount(start), Bias::Right, &()); Self::Visible(cursor) } + fn visible_files(snapshot: &'a Snapshot, start: usize) -> Self { + let mut cursor = snapshot.entries_by_path.cursor(); + cursor.seek(&VisibleFileCount(start), Bias::Right, &()); + Self::VisibleFiles(cursor) + } + fn next_internal(&mut self) { match self { - Self::All(cursor) => { + Self::Files(cursor) => { let ix = *cursor.seek_start(); cursor.seek_forward(&FileCount(ix.0 + 1), Bias::Right, &()); } Self::Visible(cursor) => { + let ix = *cursor.seek_start(); + cursor.seek_forward(&VisibleCount(ix.0 + 1), Bias::Right, &()); + } + Self::VisibleFiles(cursor) => { let ix = *cursor.seek_start(); cursor.seek_forward(&VisibleFileCount(ix.0 + 1), Bias::Right, &()); } @@ -2588,13 +2628,14 @@ impl<'a> FileIter<'a> { fn item(&self) -> Option<&'a Entry> { match self { - Self::All(cursor) => cursor.item(), + Self::Files(cursor) => cursor.item(), Self::Visible(cursor) => cursor.item(), + Self::VisibleFiles(cursor) => cursor.item(), } } } -impl<'a> Iterator for FileIter<'a> { +impl<'a> Iterator for EntryIter<'a> { type Item = &'a Entry; fn next(&mut self) -> Option { From 443a7826bb0f84bf035dc6e81d25282c431b093e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 24 Sep 2021 15:12:49 -0700 Subject: [PATCH 03/26] Start work on expanding and collapsing directories in project panel Co-Authored-By: Nathan Sobo --- gpui/src/elements/uniform_list.rs | 12 +- gpui/src/presenter.rs | 12 +- gpui/src/views/select.rs | 6 +- zed/src/fuzzy.rs | 60 +++-- zed/src/main.rs | 3 +- zed/src/project_panel.rs | 423 +++++++++++++++++++++++++----- zed/src/worktree.rs | 191 ++++++++++---- 7 files changed, 553 insertions(+), 154 deletions(-) diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index c82d8aa3d6fc740c3179f50b367348b32816cc76..298f87d3bb095d060dde66033bc575d056b9d26e 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{self, json}, - ElementBox, MutableAppContext, + ElementBox, }; use json::ToJson; use parking_lot::Mutex; @@ -38,7 +38,7 @@ pub struct LayoutState { pub struct UniformList where - F: Fn(Range, &mut Vec, &mut MutableAppContext), + F: Fn(Range, &mut Vec, &mut LayoutContext), { state: UniformListState, item_count: usize, @@ -47,7 +47,7 @@ where impl UniformList where - F: Fn(Range, &mut Vec, &mut MutableAppContext), + F: Fn(Range, &mut Vec, &mut LayoutContext), { pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { @@ -102,7 +102,7 @@ where impl Element for UniformList where - F: Fn(Range, &mut Vec, &mut MutableAppContext), + F: Fn(Range, &mut Vec, &mut LayoutContext), { type LayoutState = LayoutState; type PaintState = (); @@ -124,7 +124,7 @@ where let mut scroll_max = 0.; let mut items = Vec::new(); - (self.append_items)(0..1, &mut items, cx.app); + (self.append_items)(0..1, &mut items, cx); if let Some(first_item) = items.first_mut() { let mut item_size = first_item.layout(item_constraint, cx); item_size.set_x(size.x()); @@ -146,7 +146,7 @@ where self.item_count, start + (size.y() / item_height).ceil() as usize + 1, ); - (self.append_items)(start..end, &mut items, cx.app); + (self.append_items)(start..end, &mut items, cx); for item in &mut items { item.layout(item_constraint, cx); } diff --git a/gpui/src/presenter.rs b/gpui/src/presenter.rs index 354f0a0f821af81040581a7afedb2eb4652acbc2..8bcdd08aadcd779b0fbb1f4eb894bd3d8f267c21 100644 --- a/gpui/src/presenter.rs +++ b/gpui/src/presenter.rs @@ -7,7 +7,7 @@ use crate::{ platform::Event, text_layout::TextLayoutCache, Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel, - ReadView, Scene, View, ViewHandle, + ReadView, Scene, UpdateView, View, ViewHandle, }; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -264,6 +264,16 @@ impl<'a> ReadView for LayoutContext<'a> { } } +impl<'a> UpdateView for LayoutContext<'a> { + fn update_view(&mut self, handle: &ViewHandle, update: F) -> S + where + T: View, + F: FnOnce(&mut T, &mut crate::ViewContext) -> S, + { + self.app.update_view(handle, update) + } +} + impl<'a> ReadModel for LayoutContext<'a> { fn read_model(&self, handle: &ModelHandle) -> &T { self.app.read_model(handle) diff --git a/gpui/src/views/select.rs b/gpui/src/views/select.rs index e257455a7afc3ede7f02fc3489d3855eb5444438..a76f156ebfe0fd31ec842b351680063d6a2d1e50 100644 --- a/gpui/src/views/select.rs +++ b/gpui/src/views/select.rs @@ -126,7 +126,7 @@ impl View for Select { UniformList::new( self.list_state.clone(), self.item_count, - move |mut range, items, mut cx| { + move |mut range, items, cx| { let handle = handle.upgrade(cx).unwrap(); let this = handle.read(cx); let selected_item_ix = this.selected_item_ix; @@ -134,9 +134,9 @@ impl View for Select { items.extend(range.map(|ix| { MouseEventHandler::new::( (handle.id(), ix), - &mut cx, + cx, |mouse_state, cx| { - (handle.read(*cx).render_item)( + (handle.read(cx).render_item)( ix, if ix == selected_item_ix { ItemType::Selected diff --git a/zed/src/fuzzy.rs b/zed/src/fuzzy.rs index dde7f8fd7aff2fd198816f6115119e6128172978..3f767e4a30cf2dc221577e8514933832274b7fb8 100644 --- a/zed/src/fuzzy.rs +++ b/zed/src/fuzzy.rs @@ -278,29 +278,47 @@ pub async fn match_paths( let start = max(tree_start, segment_start) - tree_start; let end = min(tree_end, segment_end) - tree_start; - let entries = if include_ignored { - snapshot.files(start).take(end - start) + if include_ignored { + let paths = snapshot.files(start).take(end - start).map(|entry| { + if let EntryKind::File(char_bag) = entry.kind { + PathMatchCandidate { + path: &entry.path, + char_bag, + } + } else { + unreachable!() + } + }); + matcher.match_paths( + snapshot.id(), + path_prefix, + paths, + results, + &cancel_flag, + ); } else { - snapshot.visible_files(start).take(end - start) + let paths = + snapshot + .visible_files(start) + .take(end - start) + .map(|entry| { + if let EntryKind::File(char_bag) = entry.kind { + PathMatchCandidate { + path: &entry.path, + char_bag, + } + } else { + unreachable!() + } + }); + matcher.match_paths( + snapshot.id(), + path_prefix, + paths, + results, + &cancel_flag, + ); }; - let paths = entries.map(|entry| { - if let EntryKind::File(char_bag) = entry.kind { - PathMatchCandidate { - path: &entry.path, - char_bag, - } - } else { - unreachable!() - } - }); - - matcher.match_paths( - snapshot.id(), - path_prefix, - paths, - results, - &cancel_flag, - ); } if tree_end >= segment_end { break; diff --git a/zed/src/main.rs b/zed/src/main.rs index c88b1465d14742e69341ee7a68513555fa937d08..6070f7eab683421a850c8251afcd3756ed145670 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -13,7 +13,7 @@ use zed::{ channel::ChannelList, chat_panel, editor, file_finder, fs::RealFs, - http, language, menus, rpc, settings, theme_selector, + http, language, menus, project_panel, rpc, settings, theme_selector, user::UserStore, workspace::{self, OpenNew, OpenParams, OpenPaths}, AppState, @@ -55,6 +55,7 @@ fn main() { editor::init(cx); file_finder::init(cx); chat_panel::init(cx); + project_panel::init(cx); theme_selector::init(&app_state, cx); cx.set_menus(menus::menus(&app_state.clone())); diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 4a017a3fe88beaf51af2141c2c8e21be1b86e83e..985ed72529fcefa39c0925d66fe77e5f456ad8ed 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -1,19 +1,41 @@ -use crate::{ - project::Project, - theme::Theme, - worktree::{self, Worktree}, - Settings, -}; +use crate::{project::Project, theme, Settings}; use gpui::{ - elements::{Empty, Label, List, ListState, Orientation}, - AppContext, Element, ElementBox, Entity, ModelHandle, View, ViewContext, + action, + elements::{Label, MouseEventHandler, UniformList, UniformListState}, + Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, ViewContext, + WeakViewHandle, }; use postage::watch; +use std::ops::Range; pub struct ProjectPanel { project: ModelHandle, - list: ListState, + list: UniformListState, + visible_entries: Vec>, + expanded_dir_ids: Vec>, settings: watch::Receiver, + handle: WeakViewHandle, +} + +#[derive(Debug, PartialEq, Eq)] +struct EntryDetails { + filename: String, + depth: usize, + is_dir: bool, + is_expanded: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ProjectEntry { + worktree_ix: usize, + entry_id: usize, +} + +action!(ToggleExpanded, ProjectEntry); +action!(Open, ProjectEntry); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ProjectPanel::toggle_expanded); } pub enum Event {} @@ -24,77 +46,139 @@ impl ProjectPanel { settings: watch::Receiver, cx: &mut ViewContext, ) -> Self { - cx.observe(&project, |this, project, cx| { - let project = project.read(cx); - this.list.reset(Self::entry_count(project, cx)); + cx.observe(&project, |this, _, cx| { + this.update_visible_entries(cx); cx.notify(); }) .detach(); - Self { - list: ListState::new( - { - let project = project.read(cx); - Self::entry_count(project, cx) - }, - Orientation::Top, - 1000., - { - let project = project.clone(); - let settings = settings.clone(); - move |ix, cx| { - let project = project.read(cx); - Self::render_entry_at_index(project, ix, &settings.borrow().theme, cx) - } - }, - ), + let mut this = Self { project, settings, + list: Default::default(), + visible_entries: Default::default(), + expanded_dir_ids: Default::default(), + handle: cx.handle().downgrade(), + }; + this.update_visible_entries(cx); + this + } + + fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { + let ProjectEntry { + worktree_ix, + entry_id, + } = action.0; + let expanded_dir_ids = &mut self.expanded_dir_ids[worktree_ix]; + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + expanded_dir_ids.insert(ix, entry_id); + } } + self.update_visible_entries(cx); } - fn entry_count(project: &Project, cx: &AppContext) -> usize { - project - .worktrees() - .iter() - .map(|worktree| worktree.read(cx).visible_entry_count()) - .sum() + fn update_visible_entries(&mut self, cx: &mut ViewContext) { + let worktrees = self.project.read(cx).worktrees(); + self.visible_entries.clear(); + for (worktree_ix, worktree) in worktrees.iter().enumerate() { + let snapshot = worktree.read(cx).snapshot(); + + if self.expanded_dir_ids.len() <= worktree_ix { + self.expanded_dir_ids + .push(vec![snapshot.root_entry().unwrap().id]) + } + + let expanded_dir_ids = &self.expanded_dir_ids[worktree_ix]; + let mut visible_worktree_entries = Vec::new(); + let mut entry_iter = snapshot.visible_entries(0); + while let Some(item) = entry_iter.item() { + visible_worktree_entries.push(entry_iter.ix()); + if expanded_dir_ids.binary_search(&item.id).is_err() { + if entry_iter.advance_sibling() { + continue; + } + } + entry_iter.advance(); + } + self.visible_entries.push(visible_worktree_entries); + } } - fn render_entry_at_index( - project: &Project, - mut ix: usize, - theme: &Theme, - cx: &AppContext, - ) -> ElementBox { - for worktree in project.worktrees() { - let worktree = worktree.read(cx); - let visible_entry_count = worktree.visible_entry_count(); - if ix < visible_entry_count { - let entry = worktree.visible_entries(ix).next().unwrap(); - return Self::render_entry(worktree, entry, theme, cx); - } else { - ix -= visible_entry_count; + fn append_visible_entries( + &self, + range: Range, + items: &mut Vec, + cx: &mut C, + mut render_item: impl FnMut(ProjectEntry, EntryDetails, &mut C) -> T, + ) { + let worktrees = self.project.read(cx).worktrees().to_vec(); + let mut total_ix = 0; + for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() { + if total_ix >= range.end { + break; + } + if total_ix + visible_worktree_entries.len() <= range.start { + total_ix += visible_worktree_entries.len(); + continue; + } + + let expanded_entry_ids = &self.expanded_dir_ids[worktree_ix]; + let snapshot = worktrees[worktree_ix].read(cx).snapshot(); + let mut cursor = snapshot.visible_entries(0); + for ix in visible_worktree_entries[(range.start - total_ix)..] + .iter() + .copied() + { + cursor.advance_to_ix(ix); + if let Some(entry) = cursor.item() { + let details = EntryDetails { + filename: entry.path.file_name().map_or_else( + || snapshot.root_name().to_string(), + |name| name.to_string_lossy().to_string(), + ), + depth: entry.path.components().count(), + is_dir: entry.is_dir(), + is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), + }; + let entry = ProjectEntry { + worktree_ix, + entry_id: entry.id, + }; + items.push(render_item(entry, details, cx)); + } + total_ix += 1; } } - Empty::new().boxed() } fn render_entry( - worktree: &Worktree, - entry: &worktree::Entry, - theme: &Theme, - _: &AppContext, + entry: ProjectEntry, + details: EntryDetails, + theme: &theme::ProjectPanel, + cx: &mut ViewContext, ) -> ElementBox { - let path = &entry.path; - let depth = path.iter().count() as f32; - Label::new( - path.file_name() - .map_or(String::new(), |s| s.to_string_lossy().to_string()), - theme.project_panel.entry.clone(), + let is_dir = details.is_dir; + MouseEventHandler::new::( + (entry.worktree_ix, entry.entry_id), + cx, + |state, cx| { + Label::new(details.filename, theme.entry.clone()) + .contained() + .with_margin_left(details.depth as f32 * 20.) + .boxed() + }, ) - .contained() - .with_margin_left(depth * 20.) + .on_click(move |cx| { + if is_dir { + cx.dispatch_action(ToggleExpanded(entry)) + } else { + cx.dispatch_action(Open(entry)) + } + }) .boxed() } } @@ -105,14 +189,219 @@ impl View for ProjectPanel { } fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let theme = &self.settings.borrow().theme.project_panel; - List::new(self.list.clone()) - .contained() - .with_style(theme.container) - .boxed() + let settings = self.settings.clone(); + let handle = self.handle.clone(); + UniformList::new( + self.list.clone(), + self.visible_entries.len(), + move |range, items, cx| { + let theme = &settings.borrow().theme.project_panel; + let this = handle.upgrade(cx).unwrap(); + this.update(cx.app, |this, cx| { + this.append_visible_entries(range, items, cx, |entry, details, cx| { + Self::render_entry(entry, details, theme, cx) + }); + }) + }, + ) + .contained() + .with_style(self.settings.borrow().theme.project_panel.container) + .boxed() } } impl Entity for ProjectPanel { type Event = Event; } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::test_app_state; + use gpui::{TestAppContext, ViewHandle}; + use serde_json::json; + use std::{collections::HashSet, path::Path}; + + #[gpui::test] + async fn test_visible_list(mut cx: gpui::TestAppContext) { + let app_state = cx.update(test_app_state); + let settings = app_state.settings.clone(); + let fs = app_state.fs.as_fake(); + + fs.insert_tree( + "/root1", + json!({ + ".dockerignore": "", + ".git": { + "HEAD": "", + }, + "a": { + "0": { "q": "", "r": "", "s": "" }, + "1": { "t": "", "u": "" }, + "2": { "v": "", "w": "", "x": "", "y": "" }, + }, + "b": { + "3": { "Q": "" }, + "4": { "R": "", "S": "", "T": "", "U": "" }, + }, + "c": { + "5": {}, + "6": { "V": "", "W": "" }, + "7": { "X": "" }, + "8": { "Y": {}, "Z": "" } + } + }), + ) + .await; + + let project = cx.add_model(|_| Project::new(&app_state)); + let worktree = project + .update(&mut cx, |project, cx| { + project.add_local_worktree("/root1".as_ref(), cx) + }) + .await + .unwrap(); + worktree + .read_with(&cx, |t, _| t.as_local().unwrap().scan_complete()) + .await; + + let (_, panel) = cx.add_window(|cx| ProjectPanel::new(project, settings, cx)); + assert_eq!( + visible_entry_details(&panel, 0..50, &mut cx), + &[ + EntryDetails { + filename: "root1".to_string(), + depth: 0, + is_dir: true, + is_expanded: true, + }, + EntryDetails { + filename: ".dockerignore".to_string(), + depth: 1, + is_dir: false, + is_expanded: false, + }, + EntryDetails { + filename: "a".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + }, + EntryDetails { + filename: "b".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + }, + EntryDetails { + filename: "c".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + }, + ] + ); + + toggle_expand_dir(&panel, "root1/b", &mut cx); + assert_eq!( + visible_entry_details(&panel, 0..50, &mut cx), + &[ + EntryDetails { + filename: "root1".to_string(), + depth: 0, + is_dir: true, + is_expanded: true, + }, + EntryDetails { + filename: ".dockerignore".to_string(), + depth: 1, + is_dir: false, + is_expanded: false, + }, + EntryDetails { + filename: "a".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + }, + EntryDetails { + filename: "b".to_string(), + depth: 1, + is_dir: true, + is_expanded: true, + }, + EntryDetails { + filename: "3".to_string(), + depth: 2, + is_dir: true, + is_expanded: false, + }, + EntryDetails { + filename: "4".to_string(), + depth: 2, + is_dir: true, + is_expanded: false, + }, + EntryDetails { + filename: "c".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + }, + ] + ); + + fn toggle_expand_dir( + panel: &ViewHandle, + path: impl AsRef, + cx: &mut TestAppContext, + ) { + let path = path.as_ref(); + panel.update(cx, |panel, cx| { + for (worktree_ix, worktree) in panel.project.read(cx).worktrees().iter().enumerate() + { + let worktree = worktree.read(cx); + if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { + let entry_id = worktree.entry_for_path(relative_path).unwrap().id; + panel.toggle_expanded( + &ToggleExpanded(ProjectEntry { + worktree_ix, + entry_id, + }), + cx, + ); + return; + } + } + panic!("no worktree for path {:?}", path); + }); + } + + fn visible_entry_details( + panel: &ViewHandle, + range: Range, + cx: &mut TestAppContext, + ) -> Vec { + let mut result = Vec::new(); + let mut project_entries = HashSet::new(); + panel.update(cx, |panel, cx| { + panel.append_visible_entries( + range, + &mut result, + cx, + |project_entry, details, _| { + assert!( + project_entries.insert(project_entry), + "duplicate project entry {:?} {:?}", + project_entry, + details + ); + details + }, + ); + }); + + result + } + } +} diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 4fe9a31b67eb4b80b0cc3770afcf93836d972923..9704056489f907b9eecde98891ede981b6045f1b 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1482,16 +1482,16 @@ impl Snapshot { self.entries_by_path.summary().visible_file_count } - pub fn files(&self, start: usize) -> EntryIter { - EntryIter::files(self, start) + pub fn files(&self, start: usize) -> EntryIter { + EntryIter::new(self, start) } - pub fn visible_entries(&self, start: usize) -> EntryIter { - EntryIter::visible(self, start) + pub fn visible_entries(&self, start: usize) -> EntryIter { + EntryIter::new(self, start) } - pub fn visible_files(&self, start: usize) -> EntryIter { - EntryIter::visible_files(self, start) + pub fn visible_files(&self, start: usize) -> EntryIter { + EntryIter::new(self, start) } pub fn paths(&self) -> impl Iterator> { @@ -1514,7 +1514,7 @@ impl Snapshot { &self.root_name } - fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { + pub fn entry_for_path(&self, path: impl AsRef) -> Option<&Entry> { let mut cursor = self.entries_by_path.cursor::<_, ()>(); if cursor.seek(&PathSearch::Exact(path.as_ref()), Bias::Left, &()) { cursor.item() @@ -2065,30 +2065,108 @@ impl<'a: 'b, 'b> sum_tree::Dimension<'a, EntrySummary> for PathSearch<'b> { } } -#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] +#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct FileCount(usize); +#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct VisibleFileCount(usize); + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VisibleCountAndPath<'a> { + count: Option, + path: PathSearch<'a>, +} + impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount { fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { self.0 += summary.file_count; } } -#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub struct VisibleCount(usize); +impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount { + fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { + self.0 += summary.visible_file_count; + } +} -impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleCount { +impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleCountAndPath<'a> { fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { - self.0 += summary.visible_count; + if let Some(count) = self.count.as_mut() { + *count += summary.visible_count; + } else { + unreachable!() + } + self.path = PathSearch::Exact(summary.max_path.as_ref()); } } -#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub struct VisibleFileCount(usize); +impl<'a> Ord for VisibleCountAndPath<'a> { + fn cmp(&self, other: &Self) -> cmp::Ordering { + if let Some(count) = self.count { + count.cmp(&other.count.unwrap()) + } else { + self.path.cmp(&other.path) + } + } +} -impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount { - fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { - self.0 += summary.visible_file_count; +impl<'a> PartialOrd for VisibleCountAndPath<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl From for FileCount { + fn from(count: usize) -> Self { + Self(count) + } +} + +impl From for VisibleFileCount { + fn from(count: usize) -> Self { + Self(count) + } +} + +impl<'a> From for VisibleCountAndPath<'a> { + fn from(count: usize) -> Self { + Self { + count: Some(count), + path: PathSearch::default(), + } + } +} + +impl Deref for FileCount { + type Target = usize; + + fn deref(&self) -> &usize { + &self.0 + } +} + +impl Deref for VisibleFileCount { + type Target = usize; + + fn deref(&self) -> &usize { + &self.0 + } +} + +impl<'a> Deref for VisibleCountAndPath<'a> { + type Target = usize; + + fn deref(&self) -> &usize { + self.count.as_ref().unwrap() + } +} + +impl<'a> Default for VisibleCountAndPath<'a> { + fn default() -> Self { + Self { + count: Some(0), + path: Default::default(), + } } } @@ -2584,63 +2662,66 @@ impl WorktreeHandle for ModelHandle { } } -pub enum EntryIter<'a> { - Files(Cursor<'a, Entry, FileCount, ()>), - Visible(Cursor<'a, Entry, VisibleCount, ()>), - VisibleFiles(Cursor<'a, Entry, VisibleFileCount, ()>), +pub struct EntryIter<'a, Dim> { + cursor: Cursor<'a, Entry, Dim, ()>, } -impl<'a> EntryIter<'a> { - fn files(snapshot: &'a Snapshot, start: usize) -> Self { +impl<'a, Dim> EntryIter<'a, Dim> +where + Dim: sum_tree::SeekDimension<'a, EntrySummary> + From + Deref, +{ + fn new(snapshot: &'a Snapshot, start: usize) -> Self { let mut cursor = snapshot.entries_by_path.cursor(); - cursor.seek(&FileCount(start), Bias::Right, &()); - Self::Files(cursor) + cursor.seek(&Dim::from(start), Bias::Right, &()); + Self { cursor } } - fn visible(snapshot: &'a Snapshot, start: usize) -> Self { - let mut cursor = snapshot.entries_by_path.cursor(); - cursor.seek(&VisibleCount(start), Bias::Right, &()); - Self::Visible(cursor) + pub fn ix(&self) -> usize { + *self.cursor.seek_start().deref() } - fn visible_files(snapshot: &'a Snapshot, start: usize) -> Self { - let mut cursor = snapshot.entries_by_path.cursor(); - cursor.seek(&VisibleFileCount(start), Bias::Right, &()); - Self::VisibleFiles(cursor) + pub fn advance_to_ix(&mut self, ix: usize) { + self.cursor.seek_forward(&Dim::from(ix), Bias::Right, &()); } - fn next_internal(&mut self) { - match self { - Self::Files(cursor) => { - let ix = *cursor.seek_start(); - cursor.seek_forward(&FileCount(ix.0 + 1), Bias::Right, &()); - } - Self::Visible(cursor) => { - let ix = *cursor.seek_start(); - cursor.seek_forward(&VisibleCount(ix.0 + 1), Bias::Right, &()); - } - Self::VisibleFiles(cursor) => { - let ix = *cursor.seek_start(); - cursor.seek_forward(&VisibleFileCount(ix.0 + 1), Bias::Right, &()); - } - } + pub fn advance(&mut self) { + self.advance_to_ix(self.ix() + 1); } - fn item(&self) -> Option<&'a Entry> { - match self { - Self::Files(cursor) => cursor.item(), - Self::Visible(cursor) => cursor.item(), - Self::VisibleFiles(cursor) => cursor.item(), + pub fn item(&self) -> Option<&'a Entry> { + self.cursor.item() + } +} + +impl<'a> EntryIter<'a, VisibleCountAndPath<'a>> { + pub fn advance_sibling(&mut self) -> bool { + let start_count = self.cursor.seek_start().count.unwrap(); + while let Some(item) = self.cursor.item() { + self.cursor.seek_forward( + &VisibleCountAndPath { + count: None, + path: PathSearch::Successor(item.path.as_ref()), + }, + Bias::Right, + &(), + ); + if self.cursor.seek_start().count.unwrap() > start_count { + return true; + } } + false } } -impl<'a> Iterator for EntryIter<'a> { +impl<'a, Dim> Iterator for EntryIter<'a, Dim> +where + Dim: sum_tree::SeekDimension<'a, EntrySummary> + From + Deref, +{ type Item = &'a Entry; fn next(&mut self) -> Option { if let Some(entry) = self.item() { - self.next_internal(); + self.advance(); Some(entry) } else { None From 14bc2c685736bb75267337e75cf703208fc589c4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Sep 2021 12:26:15 -0600 Subject: [PATCH 04/26] Fix scrolling of project panel Co-Authored-By: Max Brunsfeld --- gpui/src/elements/uniform_list.rs | 2 +- zed/src/project_panel.rs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index 298f87d3bb095d060dde66033bc575d056b9d26e..aefe30e67b20220d96b7924ac96d91939a910852 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -137,7 +137,7 @@ where size.set_y(size.y().min(scroll_height).max(constraint.min.y())); } - scroll_max = item_height * self.item_count as f32 - size.y(); + scroll_max = (item_height * self.item_count as f32 - size.y()).max(0.); self.autoscroll(scroll_max, size.y(), item_height); items.clear(); diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 500ac03fd09e8eae3476189983f1a77466727b47..5866569a4555ad1418b7413db8021d89e2f12206 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -193,7 +193,10 @@ impl View for ProjectPanel { let handle = self.handle.clone(); UniformList::new( self.list.clone(), - self.visible_entries.len(), + self.visible_entries + .iter() + .map(|worktree_entries| worktree_entries.len()) + .sum(), move |range, items, cx| { let theme = &settings.borrow().theme.project_panel; let this = handle.upgrade(cx).unwrap(); From dde782a0066b54809ed2ae16f2105419bb4a8873 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Sep 2021 12:26:26 -0600 Subject: [PATCH 05/26] Remove top/bottom padding from project panel Co-Authored-By: Max Brunsfeld --- zed/assets/themes/_base.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 40661a4030f78c0248581a23bf09f420a580e3fc..59c232e992145b5f4cc3f5483925add018d21aa9 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -162,6 +162,8 @@ corner_radius = 6 [project_panel] extends = "$panel" entry = "$text.0" +padding.left = "$panel.padding" +padding.right = "$panel.padding" [selector] background = "$surface.0" From bd7bf82d18cdf08a6c651743d60c86f8180029b4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 28 Sep 2021 13:21:19 -0600 Subject: [PATCH 06/26] Load .gitignore files from the Fs object so we can test this with a FakeFs Co-Authored-By: Max Brunsfeld --- zed/src/worktree.rs | 133 +++++++++++++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 31 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 8859e9bf7fcc6670c3bf867e7688947450729b20..7a412d34f7d7228ad3e0ac715fc765e033cc1d1d 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -11,7 +11,7 @@ use crate::{ time::{self, ReplicaId}, util::{Bias, TryFutureExt}, }; -use ::ignore::gitignore::Gitignore; +use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Result}; use futures::{Stream, StreamExt}; pub use fuzzy::{match_paths, PathMatch}; @@ -732,12 +732,15 @@ impl LocalWorktree { next_entry_id: Arc::new(next_entry_id), }; if let Some(metadata) = metadata { - snapshot.insert_entry(Entry::new( - path.into(), - &metadata, - &snapshot.next_entry_id, - snapshot.root_char_bag, - )); + snapshot.insert_entry( + Entry::new( + path.into(), + &metadata, + &snapshot.next_entry_id, + snapshot.root_char_bag, + ), + fs.as_ref(), + ); } let (mut remote_id_tx, remote_id_rx) = watch::channel(); @@ -1156,6 +1159,16 @@ impl LocalWorktree { } } +fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { + let contents = smol::block_on(fs.load(&abs_path))?; + let parent = abs_path.parent().unwrap_or(Path::new("/")); + let mut builder = GitignoreBuilder::new(parent); + for line in contents.lines() { + builder.add_line(Some(abs_path.into()), line)?; + } + Ok(builder.build()?) +} + pub fn refresh_buffer(abs_path: PathBuf, fs: &Arc, cx: &mut ModelContext) { let fs = fs.clone(); cx.spawn(|buffer, mut cx| async move { @@ -1578,16 +1591,24 @@ impl Snapshot { self.entry_for_path(path.as_ref()).map(|e| e.inode) } - fn insert_entry(&mut self, mut entry: Entry) -> Entry { + fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { + println!("insert entry {:?}", entry.path); if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { - let (ignore, err) = Gitignore::new(self.abs_path.join(&entry.path)); - if let Some(err) = err { - log::error!("error in ignore file {:?} - {:?}", &entry.path, err); + let abs_path = self.abs_path.join(&entry.path); + match build_gitignore(&abs_path, fs) { + Ok(ignore) => { + let ignore_dir_path = entry.path.parent().unwrap(); + self.ignores + .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id)); + } + Err(error) => { + log::error!( + "error loading .gitignore file {:?} - {:?}", + &entry.path, + error + ); + } } - - let ignore_dir_path = entry.path.parent().unwrap(); - self.ignores - .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id)); } self.reuse_entry_id(&mut entry); @@ -2206,13 +2227,16 @@ impl BackgroundScanner { // If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored if child_name == *GITIGNORE { - let (ignore, err) = Gitignore::new(&child_abs_path); - if let Some(err) = err { - log::error!("error in ignore file {:?} - {:?}", child_name, err); + match build_gitignore(&child_abs_path, self.fs.as_ref()) { + Ok(ignore) => { + let ignore = Arc::new(ignore); + ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); + new_ignore = Some(ignore); + } + Err(error) => { + log::error!("error loading .gitignore file {:?} - {:?}", child_name, error); + } } - let ignore = Arc::new(ignore); - ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); - new_ignore = Some(ignore); // Update ignore status of any child entries we've already processed to reflect the // ignore file in the current directory. Because `.gitignore` starts with a `.`, @@ -2321,7 +2345,7 @@ impl BackgroundScanner { snapshot.root_char_bag, ); fs_entry.is_ignored = ignore_stack.is_all(); - snapshot.insert_entry(fs_entry); + snapshot.insert_entry(fs_entry, self.fs.as_ref()); if metadata.is_dir { scan_queue_tx .send(ScanJob { @@ -2490,7 +2514,7 @@ async fn refresh_entry( &next_entry_id, root_char_bag, ); - Ok(snapshot.lock().insert_entry(entry)) + Ok(snapshot.lock().insert_entry(entry, fs)) } fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { @@ -2774,6 +2798,50 @@ mod tests { use std::time::UNIX_EPOCH; use std::{env, fmt::Write, os::unix, time::SystemTime}; + #[gpui::test] + async fn test_traversal(cx: gpui::TestAppContext) { + let fs = FakeFs::new(); + fs.insert_tree( + "/root", + json!({ + ".gitignore": "a/b\n", + "a": { + "b": "", + "c": "", + } + }), + ) + .await; + + let tree = Worktree::open_local( + rpc::Client::new(), + Arc::from(Path::new("/root")), + Arc::new(fs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.read_with(&cx, |tree, cx| { + dbg!(tree.entries_by_path.items(&())); + + assert_eq!( + tree.entries(false) + .map(|entry| entry.path.as_ref()) + .collect::>(), + vec![ + Path::new(""), + Path::new(".gitignore"), + Path::new("a"), + Path::new("a/c"), + ] + ); + }) + } + #[gpui::test] async fn test_populate_and_search(cx: gpui::TestAppContext) { let dir = temp_tree(json!({ @@ -3260,14 +3328,17 @@ mod tests { root_char_bag: Default::default(), next_entry_id: next_entry_id.clone(), }; - initial_snapshot.insert_entry(Entry::new( - Path::new("").into(), - &smol::block_on(fs.metadata(root_dir.path())) - .unwrap() - .unwrap(), - &next_entry_id, - Default::default(), - )); + initial_snapshot.insert_entry( + Entry::new( + Path::new("").into(), + &smol::block_on(fs.metadata(root_dir.path())) + .unwrap() + .unwrap(), + &next_entry_id, + Default::default(), + ), + fs.as_ref(), + ); let mut scanner = BackgroundScanner::new( Arc::new(Mutex::new(initial_snapshot.clone())), notify_tx, From e030917769d274012968fdac0742db791721702f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 29 Sep 2021 11:32:06 +0200 Subject: [PATCH 07/26] Refine project panel styling --- gpui/src/elements/container.rs | 5 +++++ zed/assets/themes/_base.toml | 13 ++++++++++--- zed/src/project_panel.rs | 14 +++++++++++--- zed/src/theme.rs | 4 +++- zed/src/worktree.rs | 9 +++++---- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index dd58cf07398ed001f2b05e28c9a7398a4a581c4a..02d4cece951781115ca3820fe258bbfc05e4e747 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -84,6 +84,11 @@ impl Container { self } + pub fn with_padding_left(mut self, padding: f32) -> Self { + self.style.padding.left = padding; + self + } + pub fn with_padding_right(mut self, padding: f32) -> Self { self.style.padding.right = padding; self diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 59c232e992145b5f4cc3f5483925add018d21aa9..8060d5d139abf762d9649f6e05e98d73a4147fcc 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -161,9 +161,16 @@ corner_radius = 6 [project_panel] extends = "$panel" -entry = "$text.0" -padding.left = "$panel.padding" -padding.right = "$panel.padding" +padding = 0 +entry_base_padding = "$panel.padding" + +[project_panel.entry] +extends = "$text.0" +padding = { top = 3, bottom = 3 } + +[project_panel.hovered_entry] +extends = "$project_panel.entry" +background = "$state.hover" [selector] background = "$surface.0" diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 5866569a4555ad1418b7413db8021d89e2f12206..c86a0927ac479a653e1a71c6aff8cdd11af6b3f7 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -2,6 +2,7 @@ use crate::{project::Project, theme, Settings}; use gpui::{ action, elements::{Label, MouseEventHandler, UniformList, UniformListState}, + platform::CursorStyle, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, ViewContext, WeakViewHandle, }; @@ -165,10 +166,16 @@ impl ProjectPanel { MouseEventHandler::new::( (entry.worktree_ix, entry.entry_id), cx, - |state, cx| { - Label::new(details.filename, theme.entry.clone()) + |state, _| { + let style = if state.hovered { + &theme.hovered_entry + } else { + &theme.entry + }; + Label::new(details.filename, style.text.clone()) .contained() - .with_margin_left(details.depth as f32 * 20.) + .with_style(style.container) + .with_padding_left(theme.entry_base_padding + details.depth as f32 * 20.) .boxed() }, ) @@ -179,6 +186,7 @@ impl ProjectPanel { cx.dispatch_action(Open(entry)) } }) + .with_cursor_style(CursorStyle::PointingHand) .boxed() } } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index a8ef40a37c48c96c8a3a77edffe86f528c949e6f..270925e93b7a458f1f4fa53ed189d328255e145c 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -111,7 +111,9 @@ pub struct ChatPanel { pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, - pub entry: TextStyle, + pub entry_base_padding: f32, + pub entry: ContainedText, + pub hovered_entry: ContainedText, } #[derive(Deserialize)] diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 7a412d34f7d7228ad3e0ac715fc765e033cc1d1d..8bb005ed50b73e69aa674b7534d21c5e22848706 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1592,7 +1592,6 @@ impl Snapshot { } fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { - println!("insert entry {:?}", entry.path); if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.abs_path.join(&entry.path); match build_gitignore(&abs_path, fs) { @@ -1665,10 +1664,8 @@ impl Snapshot { fn reuse_entry_id(&mut self, entry: &mut Entry) { if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { - log::info!("reusing removed entry id {}", removed_entry_id); entry.id = removed_entry_id; } else if let Some(existing_entry) = self.entry_for_path(&entry.path) { - log::info!("reusing removed entry id {}", existing_entry.id); entry.id = existing_entry.id; } } @@ -2234,7 +2231,11 @@ impl BackgroundScanner { new_ignore = Some(ignore); } Err(error) => { - log::error!("error loading .gitignore file {:?} - {:?}", child_name, error); + log::error!( + "error loading .gitignore file {:?} - {:?}", + child_name, + error + ); } } From 67c40eb4be5c978dea01b523f19fc6125fe22897 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 29 Sep 2021 13:08:27 +0200 Subject: [PATCH 08/26] Allow opening entries from project panel --- zed/src/project_panel.rs | 4 ++-- zed/src/workspace.rs | 10 +++++++++- zed/src/worktree.rs | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index c86a0927ac479a653e1a71c6aff8cdd11af6b3f7..421aa226881c2bae7b015ec8d893ed73d918aa87 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -28,8 +28,8 @@ struct EntryDetails { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ProjectEntry { - worktree_ix: usize, - entry_id: usize, + pub worktree_ix: usize, + pub entry_id: usize, } action!(ToggleExpanded, ProjectEntry); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index f85829277ca8ad4e71e8d2fd28970936dc38716d..e3309e17a2c9bb8c97e4c0a3fdc7847df711a5bf 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -8,7 +8,7 @@ use crate::{ fs::Fs, people_panel::{JoinWorktree, LeaveWorktree, PeoplePanel, ShareWorktree, UnshareWorktree}, project::Project, - project_panel::ProjectPanel, + project_panel::{self, ProjectPanel}, rpc, settings::Settings, user, @@ -54,6 +54,14 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Workspace::save_active_item); cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::open_new_file); + cx.add_action(|this: &mut Workspace, action: &project_panel::Open, cx| { + if let Some(worktree) = this.worktrees(cx).get(action.0.worktree_ix) { + if let Some(entry) = worktree.read(cx).entry_for_id(action.0.entry_id) { + this.open_entry((worktree.id(), entry.path.clone()), cx) + .map(|task| task.detach()); + } + } + }); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::share_worktree); cx.add_action(Workspace::unshare_worktree); diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 8bb005ed50b73e69aa674b7534d21c5e22848706..23b70d917b37e86e0fb3b6628d8d440d0c1082ab 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1582,7 +1582,7 @@ impl Snapshot { }) } - fn entry_for_id(&self, id: usize) -> Option<&Entry> { + pub fn entry_for_id(&self, id: usize) -> Option<&Entry> { let entry = self.entries_by_id.get(&id, &())?; self.entry_for_path(&entry.path) } From 1519e1d45f37066203e84dd8e8e717769666c063 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 29 Sep 2021 16:50:33 +0200 Subject: [PATCH 09/26] Maintain active entry `Project` and render it in `ProjectPanel` --- zed/assets/themes/_base.toml | 4 ++ zed/src/project.rs | 26 +++++++++- zed/src/project_panel.rs | 92 ++++++++++++++++++++++++++++++++---- zed/src/theme.rs | 1 + zed/src/workspace.rs | 16 +++++++ 5 files changed, 129 insertions(+), 10 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 8060d5d139abf762d9649f6e05e98d73a4147fcc..206aa261345ca7ad812351ffbb99e2bd45574e8d 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -172,6 +172,10 @@ padding = { top = 3, bottom = 3 } extends = "$project_panel.entry" background = "$state.hover" +[project_panel.active_entry] +extends = "$project_panel.entry" +background = "#ff0000" + [selector] background = "$surface.0" padding = 8 diff --git a/zed/src/project.rs b/zed/src/project.rs index 18facab61b313f672dc05ec27b36033d2a801c1e..9ec429a62a586abe6f5c6bd8a26a92d5652eec6b 100644 --- a/zed/src/project.rs +++ b/zed/src/project.rs @@ -12,17 +12,21 @@ use std::{path::Path, sync::Arc}; pub struct Project { worktrees: Vec>, + active_entry: Option<(usize, usize)>, languages: Arc, rpc: Arc, fs: Arc, } -pub enum Event {} +pub enum Event { + ActiveEntryChanged(Option<(usize, usize)>), +} impl Project { pub fn new(app_state: &AppState) -> Self { Self { worktrees: Default::default(), + active_entry: None, languages: app_state.languages.clone(), rpc: app_state.rpc.clone(), fs: app_state.fs.clone(), @@ -89,6 +93,26 @@ impl Project { cx.notify(); } + pub fn set_active_entry( + &mut self, + entry: Option<(usize, Arc)>, + cx: &mut ModelContext, + ) { + let new_active_entry = entry.and_then(|(worktree_id, path)| { + let worktree = self.worktree_for_id(worktree_id)?; + let entry = worktree.read(cx).entry_for_path(path)?; + Some((worktree_id, entry.id)) + }); + if new_active_entry != self.active_entry { + self.active_entry = new_active_entry; + cx.emit(Event::ActiveEntryChanged(new_active_entry)); + } + } + + pub fn active_entry(&self) -> Option<(usize, usize)> { + self.active_entry + } + pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext) { let rpc = self.rpc.clone(); cx.spawn(|this, mut cx| { diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 421aa226881c2bae7b015ec8d893ed73d918aa87..589c809bf9c193ce088dea360167dc54cf5a5d71 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -1,4 +1,7 @@ -use crate::{project::Project, theme, Settings}; +use crate::{ + project::{self, Project}, + theme, Settings, +}; use gpui::{ action, elements::{Label, MouseEventHandler, UniformList, UniformListState}, @@ -24,6 +27,7 @@ struct EntryDetails { depth: usize, is_dir: bool, is_expanded: bool, + is_active: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -48,10 +52,18 @@ impl ProjectPanel { cx: &mut ViewContext, ) -> Self { cx.observe(&project, |this, _, cx| { - this.update_visible_entries(cx); + this.update_visible_entries(false, cx); cx.notify(); }) .detach(); + cx.subscribe(&project, |this, _, event, cx| { + if let project::Event::ActiveEntryChanged(Some((worktree_id, entry_id))) = event { + this.expand_active_entry(*worktree_id, *entry_id, cx); + this.update_visible_entries(true, cx); + cx.notify(); + } + }) + .detach(); let mut this = Self { project, @@ -61,7 +73,7 @@ impl ProjectPanel { expanded_dir_ids: Default::default(), handle: cx.handle().downgrade(), }; - this.update_visible_entries(cx); + this.update_visible_entries(false, cx); this } @@ -79,12 +91,15 @@ impl ProjectPanel { expanded_dir_ids.insert(ix, entry_id); } } - self.update_visible_entries(cx); + self.update_visible_entries(false, cx); } - fn update_visible_entries(&mut self, cx: &mut ViewContext) { - let worktrees = self.project.read(cx).worktrees(); + fn update_visible_entries(&mut self, scroll_to_active_entry: bool, cx: &mut ViewContext) { + let project = self.project.read(cx); + let worktrees = project.worktrees(); self.visible_entries.clear(); + + let mut entry_ix = 0; for (worktree_ix, worktree) in worktrees.iter().enumerate() { let snapshot = worktree.read(cx).snapshot(); @@ -98,6 +113,13 @@ impl ProjectPanel { let mut entry_iter = snapshot.entries(false); while let Some(item) = entry_iter.entry() { visible_worktree_entries.push(entry_iter.offset()); + if scroll_to_active_entry + && project.active_entry() == Some((worktree.id(), item.id)) + { + self.list.scroll_to(entry_ix); + } + + entry_ix += 1; if expanded_dir_ids.binary_search(&item.id).is_err() { if entry_iter.advance_to_sibling() { continue; @@ -109,6 +131,40 @@ impl ProjectPanel { } } + fn expand_active_entry( + &mut self, + worktree_id: usize, + entry_id: usize, + cx: &mut ViewContext, + ) { + let project = self.project.read(cx); + if let Some(worktree) = project.worktree_for_id(worktree_id) { + let worktree_ix = project + .worktrees() + .iter() + .position(|w| w.id() == worktree_id) + .unwrap(); + let expanded_dir_ids = &mut self.expanded_dir_ids[worktree_ix]; + let worktree = worktree.read(cx); + + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + } + fn append_visible_entries( &self, range: Range, @@ -116,7 +172,9 @@ impl ProjectPanel { cx: &mut C, mut render_item: impl FnMut(ProjectEntry, EntryDetails, &mut C) -> T, ) { - let worktrees = self.project.read(cx).worktrees().to_vec(); + let project = self.project.read(cx); + let active_entry = project.active_entry(); + let worktrees = project.worktrees().to_vec(); let mut total_ix = 0; for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() { if total_ix >= range.end { @@ -128,7 +186,8 @@ impl ProjectPanel { } let expanded_entry_ids = &self.expanded_dir_ids[worktree_ix]; - let snapshot = worktrees[worktree_ix].read(cx).snapshot(); + let worktree = &worktrees[worktree_ix]; + let snapshot = worktree.read(cx).snapshot(); let mut cursor = snapshot.entries(false); for ix in visible_worktree_entries[(range.start - total_ix)..] .iter() @@ -144,6 +203,7 @@ impl ProjectPanel { depth: entry.path.components().count(), is_dir: entry.is_dir(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), + is_active: active_entry == Some((worktree.id(), entry.id)), }; let entry = ProjectEntry { worktree_ix, @@ -167,7 +227,9 @@ impl ProjectPanel { (entry.worktree_ix, entry.entry_id), cx, |state, _| { - let style = if state.hovered { + let style = if details.is_active { + &theme.active_entry + } else if state.hovered { &theme.hovered_entry } else { &theme.entry @@ -285,30 +347,35 @@ mod tests { depth: 0, is_dir: true, is_expanded: true, + is_active: false, }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, is_dir: false, is_expanded: false, + is_active: false, }, EntryDetails { filename: "a".to_string(), depth: 1, is_dir: true, is_expanded: false, + is_active: false, }, EntryDetails { filename: "b".to_string(), depth: 1, is_dir: true, is_expanded: false, + is_active: false, }, EntryDetails { filename: "c".to_string(), depth: 1, is_dir: true, is_expanded: false, + is_active: false, }, ] ); @@ -322,42 +389,49 @@ mod tests { depth: 0, is_dir: true, is_expanded: true, + is_active: false, }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, is_dir: false, is_expanded: false, + is_active: false, }, EntryDetails { filename: "a".to_string(), depth: 1, is_dir: true, is_expanded: false, + is_active: false, }, EntryDetails { filename: "b".to_string(), depth: 1, is_dir: true, is_expanded: true, + is_active: false, }, EntryDetails { filename: "3".to_string(), depth: 2, is_dir: true, is_expanded: false, + is_active: false, }, EntryDetails { filename: "4".to_string(), depth: 2, is_dir: true, is_expanded: false, + is_active: false, }, EntryDetails { filename: "c".to_string(), depth: 1, is_dir: true, is_expanded: false, + is_active: false, }, ] ); diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 270925e93b7a458f1f4fa53ed189d328255e145c..b4561abf1f729825e12665b472bf0db08e23e38e 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -114,6 +114,7 @@ pub struct ProjectPanel { pub entry_base_padding: f32, pub entry: ContainedText, pub hovered_entry: ContainedText, + pub active_entry: ContainedText, } #[derive(Deserialize)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e3309e17a2c9bb8c97e4c0a3fdc7847df711a5bf..9478c40a57b8d45144f152537aa7782b013f683b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -371,6 +371,12 @@ impl Workspace { let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane_id = pane.id(); + cx.observe(&pane, move |me, _, cx| { + let active_entry = me.active_entry(cx); + me.project + .update(cx, |project, cx| project.set_active_entry(active_entry, cx)); + }) + .detach(); cx.subscribe(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) }) @@ -725,6 +731,10 @@ impl Workspace { self.active_pane().read(cx).active_item() } + fn active_entry(&self, cx: &ViewContext) -> Option<(usize, Arc)> { + self.active_item(cx).and_then(|item| item.entry_id(cx)) + } + pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext) { if let Some(item) = self.active_item(cx) { let handle = cx.handle(); @@ -843,6 +853,12 @@ impl Workspace { fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { let pane = cx.add_view(|_| Pane::new(self.settings.clone())); let pane_id = pane.id(); + cx.observe(&pane, move |me, _, cx| { + let active_entry = me.active_entry(cx); + me.project + .update(cx, |project, cx| project.set_active_entry(active_entry, cx)); + }) + .detach(); cx.subscribe(&pane, move |me, _, event, cx| { me.handle_pane_event(pane_id, event, cx) }) From d182182ae2545ce66825582e6e071d407831d808 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Sep 2021 22:08:28 -0700 Subject: [PATCH 10/26] Avoid ProjectPanel panic when worktree has no root entry Also, avoid bug where too many UniformList elements were rendered. --- zed/src/project.rs | 9 ++- zed/src/project_panel.rs | 127 ++++++++++++++++++++++----------------- zed/src/worktree.rs | 4 +- 3 files changed, 81 insertions(+), 59 deletions(-) diff --git a/zed/src/project.rs b/zed/src/project.rs index 9ec429a62a586abe6f5c6bd8a26a92d5652eec6b..3f7ef27a455a3527332747b2766ceefe2082dfce 100644 --- a/zed/src/project.rs +++ b/zed/src/project.rs @@ -20,6 +20,7 @@ pub struct Project { pub enum Event { ActiveEntryChanged(Option<(usize, usize)>), + WorktreeRemoved(usize), } impl Project { @@ -166,7 +167,7 @@ impl Project { pub fn close_remote_worktree(&mut self, id: u64, cx: &mut ModelContext) { self.worktrees.retain(|worktree| { - worktree.update(cx, |worktree, cx| { + let keep = worktree.update(cx, |worktree, cx| { if let Some(worktree) = worktree.as_remote_mut() { if worktree.remote_id() == id { worktree.close_all_buffers(cx); @@ -174,7 +175,11 @@ impl Project { } } true - }) + }); + if !keep { + cx.emit(Event::WorktreeRemoved(worktree.id())); + } + keep }); } } diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 589c809bf9c193ce088dea360167dc54cf5a5d71..24bf7a08771fb60affedacd25c0b12d608efb127 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -10,13 +10,16 @@ use gpui::{ WeakViewHandle, }; use postage::watch; -use std::ops::Range; +use std::{ + collections::{hash_map, HashMap}, + ops::Range, +}; pub struct ProjectPanel { project: ModelHandle, list: UniformListState, visible_entries: Vec>, - expanded_dir_ids: Vec>, + expanded_dir_ids: HashMap>, settings: watch::Receiver, handle: WeakViewHandle, } @@ -56,9 +59,16 @@ impl ProjectPanel { cx.notify(); }) .detach(); - cx.subscribe(&project, |this, _, event, cx| { - if let project::Event::ActiveEntryChanged(Some((worktree_id, entry_id))) = event { - this.expand_active_entry(*worktree_id, *entry_id, cx); + cx.subscribe(&project, |this, _, event, cx| match event { + project::Event::ActiveEntryChanged(entry) => { + if let Some((worktree_id, entry_id)) = entry { + this.expand_active_entry(*worktree_id, *entry_id, cx); + this.update_visible_entries(true, cx); + cx.notify(); + } + } + project::Event::WorktreeRemoved(id) => { + this.expanded_dir_ids.remove(id); this.update_visible_entries(true, cx); cx.notify(); } @@ -82,16 +92,18 @@ impl ProjectPanel { worktree_ix, entry_id, } = action.0; - let expanded_dir_ids = &mut self.expanded_dir_ids[worktree_ix]; - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - } - Err(ix) => { - expanded_dir_ids.insert(ix, entry_id); + let worktree_id = self.project.read(cx).worktrees()[worktree_ix].id(); + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + expanded_dir_ids.insert(ix, entry_id); + } } + self.update_visible_entries(false, cx); } - self.update_visible_entries(false, cx); } fn update_visible_entries(&mut self, scroll_to_active_entry: bool, cx: &mut ViewContext) { @@ -100,15 +112,23 @@ impl ProjectPanel { self.visible_entries.clear(); let mut entry_ix = 0; - for (worktree_ix, worktree) in worktrees.iter().enumerate() { + for worktree in worktrees { let snapshot = worktree.read(cx).snapshot(); + let worktree_id = worktree.id(); + + let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) { + hash_map::Entry::Occupied(e) => e.into_mut(), + hash_map::Entry::Vacant(e) => { + // The first time a worktree's root entry becomes available, + // mark that root entry as expanded. + if let Some(entry) = snapshot.root_entry() { + e.insert(vec![entry.id]).as_slice() + } else { + &[] + } + } + }; - if self.expanded_dir_ids.len() <= worktree_ix { - self.expanded_dir_ids - .push(vec![snapshot.root_entry().unwrap().id]) - } - - let expanded_dir_ids = &self.expanded_dir_ids[worktree_ix]; let mut visible_worktree_entries = Vec::new(); let mut entry_iter = snapshot.entries(false); while let Some(item) = entry_iter.entry() { @@ -138,13 +158,10 @@ impl ProjectPanel { cx: &mut ViewContext, ) { let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(worktree_id) { - let worktree_ix = project - .worktrees() - .iter() - .position(|w| w.id() == worktree_id) - .unwrap(); - let expanded_dir_ids = &mut self.expanded_dir_ids[worktree_ix]; + if let Some((worktree, expanded_dir_ids)) = project + .worktree_for_id(worktree_id) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { let worktree = worktree.read(cx); if let Some(mut entry) = worktree.entry_for_id(entry_id) { @@ -165,31 +182,36 @@ impl ProjectPanel { } } - fn append_visible_entries( + fn for_each_visible_entry( &self, range: Range, - items: &mut Vec, cx: &mut C, - mut render_item: impl FnMut(ProjectEntry, EntryDetails, &mut C) -> T, + mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut C), ) { let project = self.project.read(cx); let active_entry = project.active_entry(); let worktrees = project.worktrees().to_vec(); - let mut total_ix = 0; + let mut ix = 0; for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() { - if total_ix >= range.end { - break; + if ix >= range.end { + return; } - if total_ix + visible_worktree_entries.len() <= range.start { - total_ix += visible_worktree_entries.len(); + if ix + visible_worktree_entries.len() <= range.start { + ix += visible_worktree_entries.len(); continue; } - let expanded_entry_ids = &self.expanded_dir_ids[worktree_ix]; + let end_ix = range.end.min(ix + visible_worktree_entries.len()); let worktree = &worktrees[worktree_ix]; + let expanded_entry_ids = self + .expanded_dir_ids + .get(&worktree.id()) + .map(Vec::as_slice) + .unwrap_or(&[]); let snapshot = worktree.read(cx).snapshot(); let mut cursor = snapshot.entries(false); - for ix in visible_worktree_entries[(range.start - total_ix)..] + + for ix in visible_worktree_entries[(range.start - ix)..(end_ix - ix)] .iter() .copied() { @@ -209,10 +231,10 @@ impl ProjectPanel { worktree_ix, entry_id: entry.id, }; - items.push(render_item(entry, details, cx)); + callback(entry, details, cx); } - total_ix += 1; } + ix = end_ix; } } @@ -271,9 +293,11 @@ impl View for ProjectPanel { let theme = &settings.borrow().theme.project_panel; let this = handle.upgrade(cx).unwrap(); this.update(cx.app, |this, cx| { - this.append_visible_entries(range, items, cx, |entry, details, cx| { - Self::render_entry(entry, details, theme, cx) + let prev_len = items.len(); + this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| { + items.push(Self::render_entry(entry, details, theme, cx)); }); + let count = items.len() - prev_len; }) }, ) @@ -470,20 +494,15 @@ mod tests { let mut result = Vec::new(); let mut project_entries = HashSet::new(); panel.update(cx, |panel, cx| { - panel.append_visible_entries( - range, - &mut result, - cx, - |project_entry, details, _| { - assert!( - project_entries.insert(project_entry), - "duplicate project entry {:?} {:?}", - project_entry, - details - ); + panel.for_each_visible_entry(range, cx, |project_entry, details, _| { + assert!( + project_entries.insert(project_entry), + "duplicate project entry {:?} {:?}", + project_entry, details - }, - ); + ); + result.push(details); + }); }); result diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 23b70d917b37e86e0fb3b6628d8d440d0c1082ab..eaeefcd46257f293530eaee73488fd30461a9cc5 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -2826,9 +2826,7 @@ mod tests { cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - tree.read_with(&cx, |tree, cx| { - dbg!(tree.entries_by_path.items(&())); - + tree.read_with(&cx, |tree, _| { assert_eq!( tree.entries(false) .map(|entry| entry.path.as_ref()) From 7a1cdc9ad1f95cfba6b35fca1032077a06117ff7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Sep 2021 10:39:15 +0200 Subject: [PATCH 11/26] Fix panic when rendering multiple worktrees in project panel --- zed/src/project_panel.rs | 106 +++++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 24bf7a08771fb60affedacd25c0b12d608efb127..39925c0215a2e1d8e0e104c3632c3e7375bf989e 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -12,6 +12,7 @@ use gpui::{ use postage::watch; use std::{ collections::{hash_map, HashMap}, + ffi::OsStr, ops::Range, }; @@ -209,19 +210,18 @@ impl ProjectPanel { .map(Vec::as_slice) .unwrap_or(&[]); let snapshot = worktree.read(cx).snapshot(); + let root_name = OsStr::new(snapshot.root_name()); let mut cursor = snapshot.entries(false); - for ix in visible_worktree_entries[(range.start - ix)..(end_ix - ix)] + for ix in visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix] .iter() .copied() { cursor.advance_to_offset(ix); if let Some(entry) = cursor.entry() { + let filename = entry.path.file_name().unwrap_or(root_name); let details = EntryDetails { - filename: entry.path.file_name().map_or_else( - || snapshot.root_name().to_string(), - |name| name.to_string_lossy().to_string(), - ), + filename: filename.to_string_lossy().to_string(), depth: entry.path.components().count(), is_dir: entry.is_dir(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), @@ -293,11 +293,9 @@ impl View for ProjectPanel { let theme = &settings.borrow().theme.project_panel; let this = handle.upgrade(cx).unwrap(); this.update(cx.app, |this, cx| { - let prev_len = items.len(); this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| { items.push(Self::render_entry(entry, details, theme, cx)); }); - let count = items.len() - prev_len; }) }, ) @@ -350,15 +348,34 @@ mod tests { }), ) .await; + fs.insert_tree( + "/root2", + json!({ + "d": { + "9": "" + }, + "e": {} + }), + ) + .await; let project = cx.add_model(|_| Project::new(&app_state)); - let worktree = project + let root1 = project .update(&mut cx, |project, cx| { project.add_local_worktree("/root1".as_ref(), cx) }) .await .unwrap(); - worktree + root1 + .read_with(&cx, |t, _| t.as_local().unwrap().scan_complete()) + .await; + let root2 = project + .update(&mut cx, |project, cx| { + project.add_local_worktree("/root2".as_ref(), cx) + }) + .await + .unwrap(); + root2 .read_with(&cx, |t, _| t.as_local().unwrap().scan_complete()) .await; @@ -401,7 +418,28 @@ mod tests { is_expanded: false, is_active: false, }, - ] + EntryDetails { + filename: "root2".to_string(), + depth: 0, + is_dir: true, + is_expanded: true, + is_active: false + }, + EntryDetails { + filename: "d".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + is_active: false + }, + EntryDetails { + filename: "e".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + is_active: false + } + ], ); toggle_expand_dir(&panel, "root1/b", &mut cx); @@ -457,6 +495,54 @@ mod tests { is_expanded: false, is_active: false, }, + EntryDetails { + filename: "root2".to_string(), + depth: 0, + is_dir: true, + is_expanded: true, + is_active: false + }, + EntryDetails { + filename: "d".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + is_active: false + }, + EntryDetails { + filename: "e".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + is_active: false + } + ] + ); + + assert_eq!( + visible_entry_details(&panel, 5..8, &mut cx), + [ + EntryDetails { + filename: "4".to_string(), + depth: 2, + is_dir: true, + is_expanded: false, + is_active: false + }, + EntryDetails { + filename: "c".to_string(), + depth: 1, + is_dir: true, + is_expanded: false, + is_active: false + }, + EntryDetails { + filename: "root2".to_string(), + depth: 0, + is_dir: true, + is_expanded: true, + is_active: false + } ] ); From 789d5dfaee93123b5bb1694fa6ec522d21301f53 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Sep 2021 14:13:07 +0200 Subject: [PATCH 12/26] Allow selecting prev and next entries via the keyboard --- zed/src/project_panel.rs | 160 ++++++++++++++++++++++++++++++++++----- 1 file changed, 142 insertions(+), 18 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 39925c0215a2e1d8e0e104c3632c3e7375bf989e..63d757c9a1ca3a195b21b39a6c219fa61124994d 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -1,13 +1,17 @@ use crate::{ project::{self, Project}, - theme, Settings, + theme, worktree, Settings, }; use gpui::{ action, elements::{Label, MouseEventHandler, UniformList, UniformListState}, + keymap::{ + self, + menu::{SelectNext, SelectPrev}, + }, platform::CursorStyle, - Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, ViewContext, - WeakViewHandle, + AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, + ViewContext, WeakViewHandle, }; use postage::watch; use std::{ @@ -21,10 +25,18 @@ pub struct ProjectPanel { list: UniformListState, visible_entries: Vec>, expanded_dir_ids: HashMap>, + active_entry: Option, settings: watch::Receiver, handle: WeakViewHandle, } +#[derive(Copy, Clone)] +struct ActiveEntry { + worktree_id: usize, + entry_id: usize, + index: usize, +} + #[derive(Debug, PartialEq, Eq)] struct EntryDetails { filename: String, @@ -45,6 +57,8 @@ action!(Open, ProjectEntry); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::toggle_expanded); + cx.add_action(ProjectPanel::select_prev); + cx.add_action(ProjectPanel::select_next); } pub enum Event {} @@ -56,21 +70,21 @@ impl ProjectPanel { cx: &mut ViewContext, ) -> Self { cx.observe(&project, |this, _, cx| { - this.update_visible_entries(false, cx); + this.update_visible_entries(None, cx); cx.notify(); }) .detach(); cx.subscribe(&project, |this, _, event, cx| match event { project::Event::ActiveEntryChanged(entry) => { - if let Some((worktree_id, entry_id)) = entry { - this.expand_active_entry(*worktree_id, *entry_id, cx); - this.update_visible_entries(true, cx); + if let Some((worktree_id, entry_id)) = *entry { + this.expand_active_entry(worktree_id, entry_id, cx); + this.update_visible_entries(Some((worktree_id, entry_id)), cx); cx.notify(); } } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); - this.update_visible_entries(true, cx); + this.update_visible_entries(None, cx); cx.notify(); } }) @@ -82,9 +96,10 @@ impl ProjectPanel { list: Default::default(), visible_entries: Default::default(), expanded_dir_ids: Default::default(), + active_entry: None, handle: cx.handle().downgrade(), }; - this.update_visible_entries(false, cx); + this.update_visible_entries(None, cx); this } @@ -103,13 +118,101 @@ impl ProjectPanel { expanded_dir_ids.insert(ix, entry_id); } } - self.update_visible_entries(false, cx); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus_self(); + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(active_entry) = self.active_entry { + if active_entry.index > 0 { + let (worktree_id, entry) = self + .visible_entry_for_index(active_entry.index - 1, cx) + .unwrap(); + self.active_entry = Some(ActiveEntry { + worktree_id, + entry_id: entry.id, + index: active_entry.index - 1, + }); + self.autoscroll(); + cx.notify(); + } + } else { + self.select_first(cx); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(active_entry) = self.active_entry { + if let Some((worktree_id, entry)) = + self.visible_entry_for_index(active_entry.index + 1, cx) + { + self.active_entry = Some(ActiveEntry { + worktree_id, + entry_id: entry.id, + index: active_entry.index + 1, + }); + self.autoscroll(); + cx.notify(); + } + } else { + self.select_first(cx); + } + } + + fn select_first(&mut self, cx: &mut ViewContext) { + if let Some(worktree) = self.project.read(cx).worktrees().first() { + let worktree_id = worktree.id(); + let worktree = worktree.read(cx); + if let Some(root_entry) = worktree.root_entry() { + self.active_entry = Some(ActiveEntry { + worktree_id, + entry_id: root_entry.id, + index: 0, + }); + self.autoscroll(); + cx.notify(); + } + } + } + + fn autoscroll(&mut self) { + if let Some(active_entry) = self.active_entry { + self.list.scroll_to(active_entry.index); } } - fn update_visible_entries(&mut self, scroll_to_active_entry: bool, cx: &mut ViewContext) { + fn visible_entry_for_index<'a>( + &self, + target_ix: usize, + cx: &'a AppContext, + ) -> Option<(usize, &'a worktree::Entry)> { let project = self.project.read(cx); - let worktrees = project.worktrees(); + let mut offset = None; + let mut ix = 0; + for (worktree_ix, visible_entries) in self.visible_entries.iter().enumerate() { + if target_ix < ix + visible_entries.len() { + let worktree = &project.worktrees()[worktree_ix]; + offset = Some((worktree, visible_entries[target_ix - ix])); + break; + } else { + ix += visible_entries.len(); + } + } + + offset.and_then(|(worktree, offset)| { + let mut entries = worktree.read(cx).entries(false); + entries.advance_to_offset(offset); + Some((worktree.id(), entries.entry()?)) + }) + } + + fn update_visible_entries( + &mut self, + new_active_entry: Option<(usize, usize)>, + cx: &mut ViewContext, + ) { + let worktrees = self.project.read(cx).worktrees(); self.visible_entries.clear(); let mut entry_ix = 0; @@ -134,10 +237,22 @@ impl ProjectPanel { let mut entry_iter = snapshot.entries(false); while let Some(item) = entry_iter.entry() { visible_worktree_entries.push(entry_iter.offset()); - if scroll_to_active_entry - && project.active_entry() == Some((worktree.id(), item.id)) - { - self.list.scroll_to(entry_ix); + if let Some(new_active_entry) = new_active_entry { + if new_active_entry == (worktree.id(), item.id) { + self.active_entry = Some(ActiveEntry { + worktree_id, + entry_id: item.id, + index: entry_ix, + }); + } + } else if self.active_entry.map_or(false, |e| { + e.worktree_id == worktree_id && e.entry_id == item.id + }) { + self.active_entry = Some(ActiveEntry { + worktree_id, + entry_id: item.id, + index: entry_ix, + }); } entry_ix += 1; @@ -150,6 +265,8 @@ impl ProjectPanel { } self.visible_entries.push(visible_worktree_entries); } + + self.autoscroll(); } fn expand_active_entry( @@ -190,7 +307,6 @@ impl ProjectPanel { mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut C), ) { let project = self.project.read(cx); - let active_entry = project.active_entry(); let worktrees = project.worktrees().to_vec(); let mut ix = 0; for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() { @@ -225,7 +341,9 @@ impl ProjectPanel { depth: entry.path.components().count(), is_dir: entry.is_dir(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), - is_active: active_entry == Some((worktree.id(), entry.id)), + is_active: self.active_entry.map_or(false, |e| { + e.worktree_id == worktree.id() && e.entry_id == entry.id + }), }; let entry = ProjectEntry { worktree_ix, @@ -303,6 +421,12 @@ impl View for ProjectPanel { .with_style(self.settings.borrow().theme.project_panel.container) .boxed() } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } } impl Entity for ProjectPanel { From 19325fd3f1541e2eca0518ba065cc16d216f84cb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Sep 2021 14:38:21 +0200 Subject: [PATCH 13/26] Allow expanding/collapsing active entry using the keyboard --- zed/src/project_panel.rs | 80 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 63d757c9a1ca3a195b21b39a6c219fa61124994d..7690754aab1a354879a123deef9942c87b5d11a9 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -8,6 +8,7 @@ use gpui::{ keymap::{ self, menu::{SelectNext, SelectPrev}, + Binding, }, platform::CursorStyle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, @@ -52,13 +53,21 @@ pub struct ProjectEntry { pub entry_id: usize, } +action!(ExpandActiveEntry); +action!(CollapseActiveEntry); action!(ToggleExpanded, ProjectEntry); action!(Open, ProjectEntry); pub fn init(cx: &mut MutableAppContext) { + cx.add_action(ProjectPanel::expand_active_entry); + cx.add_action(ProjectPanel::collapse_active_entry); cx.add_action(ProjectPanel::toggle_expanded); cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); + cx.add_bindings([ + Binding::new("right", ExpandActiveEntry, None), + Binding::new("left", CollapseActiveEntry, None), + ]); } pub enum Event {} @@ -77,7 +86,7 @@ impl ProjectPanel { cx.subscribe(&project, |this, _, event, cx| match event { project::Event::ActiveEntryChanged(entry) => { if let Some((worktree_id, entry_id)) = *entry { - this.expand_active_entry(worktree_id, entry_id, cx); + this.expand_entry(worktree_id, entry_id, cx); this.update_visible_entries(Some((worktree_id, entry_id)), cx); cx.notify(); } @@ -103,6 +112,68 @@ impl ProjectPanel { this } + fn expand_active_entry(&mut self, _: &ExpandActiveEntry, cx: &mut ViewContext) { + if let Some(active_entry) = self.active_entry { + let project = self.project.read(cx); + if let Some(worktree) = project.worktree_for_id(active_entry.worktree_id) { + if let Some(entry) = worktree.read(cx).entry_for_id(active_entry.entry_id) { + if entry.is_dir() { + if let Some(expanded_dir_ids) = + self.expanded_dir_ids.get_mut(&active_entry.worktree_id) + { + match expanded_dir_ids.binary_search(&active_entry.entry_id) { + Ok(_) => self.select_next(&SelectNext, cx), + Err(ix) => { + expanded_dir_ids.insert(ix, active_entry.entry_id); + self.update_visible_entries(None, cx); + cx.notify(); + } + } + } + } else { + } + } + } + } + } + + fn collapse_active_entry(&mut self, _: &CollapseActiveEntry, cx: &mut ViewContext) { + if let Some(active_entry) = self.active_entry { + let project = self.project.read(cx); + if let Some(worktree) = project.worktree_for_id(active_entry.worktree_id) { + let worktree = worktree.read(cx); + if let Some(mut entry) = worktree.entry_for_id(active_entry.entry_id) { + if let Some(expanded_dir_ids) = + self.expanded_dir_ids.get_mut(&active_entry.worktree_id) + { + loop { + match expanded_dir_ids.binary_search(&entry.id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + self.update_visible_entries( + Some((active_entry.worktree_id, entry.id)), + cx, + ); + cx.notify(); + break; + } + Err(_) => { + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + } + } + } + } + } + fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { let ProjectEntry { worktree_ix, @@ -269,12 +340,7 @@ impl ProjectPanel { self.autoscroll(); } - fn expand_active_entry( - &mut self, - worktree_id: usize, - entry_id: usize, - cx: &mut ViewContext, - ) { + fn expand_entry(&mut self, worktree_id: usize, entry_id: usize, cx: &mut ViewContext) { let project = self.project.read(cx); if let Some((worktree, expanded_dir_ids)) = project .worktree_for_id(worktree_id) From 2f508af017a7aeaa08722c4a3cc386e2632a7d40 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Sep 2021 14:41:12 +0200 Subject: [PATCH 14/26] Rename active entry to selected entry --- zed/assets/themes/_base.toml | 2 +- zed/src/project_panel.rs | 76 ++++++++++++++++++------------------ zed/src/theme.rs | 2 +- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 206aa261345ca7ad812351ffbb99e2bd45574e8d..a001cf7687dfab6d7e99a1813fe0835859a70f36 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -172,7 +172,7 @@ padding = { top = 3, bottom = 3 } extends = "$project_panel.entry" background = "$state.hover" -[project_panel.active_entry] +[project_panel.selected_entry] extends = "$project_panel.entry" background = "#ff0000" diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 7690754aab1a354879a123deef9942c87b5d11a9..5c74279b864d694226cbe3b0b29c04130d26b280 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -26,13 +26,13 @@ pub struct ProjectPanel { list: UniformListState, visible_entries: Vec>, expanded_dir_ids: HashMap>, - active_entry: Option, + selected_entry: Option, settings: watch::Receiver, handle: WeakViewHandle, } #[derive(Copy, Clone)] -struct ActiveEntry { +struct SelectedEntry { worktree_id: usize, entry_id: usize, index: usize, @@ -59,8 +59,8 @@ action!(ToggleExpanded, ProjectEntry); action!(Open, ProjectEntry); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ProjectPanel::expand_active_entry); - cx.add_action(ProjectPanel::collapse_active_entry); + cx.add_action(ProjectPanel::expand_selected_entry); + cx.add_action(ProjectPanel::collapse_selected_entry); cx.add_action(ProjectPanel::toggle_expanded); cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); @@ -105,26 +105,26 @@ impl ProjectPanel { list: Default::default(), visible_entries: Default::default(), expanded_dir_ids: Default::default(), - active_entry: None, + selected_entry: None, handle: cx.handle().downgrade(), }; this.update_visible_entries(None, cx); this } - fn expand_active_entry(&mut self, _: &ExpandActiveEntry, cx: &mut ViewContext) { - if let Some(active_entry) = self.active_entry { + fn expand_selected_entry(&mut self, _: &ExpandActiveEntry, cx: &mut ViewContext) { + if let Some(selected_entry) = self.selected_entry { let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(active_entry.worktree_id) { - if let Some(entry) = worktree.read(cx).entry_for_id(active_entry.entry_id) { + if let Some(worktree) = project.worktree_for_id(selected_entry.worktree_id) { + if let Some(entry) = worktree.read(cx).entry_for_id(selected_entry.entry_id) { if entry.is_dir() { if let Some(expanded_dir_ids) = - self.expanded_dir_ids.get_mut(&active_entry.worktree_id) + self.expanded_dir_ids.get_mut(&selected_entry.worktree_id) { - match expanded_dir_ids.binary_search(&active_entry.entry_id) { + match expanded_dir_ids.binary_search(&selected_entry.entry_id) { Ok(_) => self.select_next(&SelectNext, cx), Err(ix) => { - expanded_dir_ids.insert(ix, active_entry.entry_id); + expanded_dir_ids.insert(ix, selected_entry.entry_id); self.update_visible_entries(None, cx); cx.notify(); } @@ -137,21 +137,21 @@ impl ProjectPanel { } } - fn collapse_active_entry(&mut self, _: &CollapseActiveEntry, cx: &mut ViewContext) { - if let Some(active_entry) = self.active_entry { + fn collapse_selected_entry(&mut self, _: &CollapseActiveEntry, cx: &mut ViewContext) { + if let Some(selected_entry) = self.selected_entry { let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(active_entry.worktree_id) { + if let Some(worktree) = project.worktree_for_id(selected_entry.worktree_id) { let worktree = worktree.read(cx); - if let Some(mut entry) = worktree.entry_for_id(active_entry.entry_id) { + if let Some(mut entry) = worktree.entry_for_id(selected_entry.entry_id) { if let Some(expanded_dir_ids) = - self.expanded_dir_ids.get_mut(&active_entry.worktree_id) + self.expanded_dir_ids.get_mut(&selected_entry.worktree_id) { loop { match expanded_dir_ids.binary_search(&entry.id) { Ok(ix) => { expanded_dir_ids.remove(ix); self.update_visible_entries( - Some((active_entry.worktree_id, entry.id)), + Some((selected_entry.worktree_id, entry.id)), cx, ); cx.notify(); @@ -195,15 +195,15 @@ impl ProjectPanel { } fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(active_entry) = self.active_entry { - if active_entry.index > 0 { + if let Some(selected_entry) = self.selected_entry { + if selected_entry.index > 0 { let (worktree_id, entry) = self - .visible_entry_for_index(active_entry.index - 1, cx) + .visible_entry_for_index(selected_entry.index - 1, cx) .unwrap(); - self.active_entry = Some(ActiveEntry { + self.selected_entry = Some(SelectedEntry { worktree_id, entry_id: entry.id, - index: active_entry.index - 1, + index: selected_entry.index - 1, }); self.autoscroll(); cx.notify(); @@ -214,14 +214,14 @@ impl ProjectPanel { } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(active_entry) = self.active_entry { + if let Some(selected_entry) = self.selected_entry { if let Some((worktree_id, entry)) = - self.visible_entry_for_index(active_entry.index + 1, cx) + self.visible_entry_for_index(selected_entry.index + 1, cx) { - self.active_entry = Some(ActiveEntry { + self.selected_entry = Some(SelectedEntry { worktree_id, entry_id: entry.id, - index: active_entry.index + 1, + index: selected_entry.index + 1, }); self.autoscroll(); cx.notify(); @@ -236,7 +236,7 @@ impl ProjectPanel { let worktree_id = worktree.id(); let worktree = worktree.read(cx); if let Some(root_entry) = worktree.root_entry() { - self.active_entry = Some(ActiveEntry { + self.selected_entry = Some(SelectedEntry { worktree_id, entry_id: root_entry.id, index: 0, @@ -248,8 +248,8 @@ impl ProjectPanel { } fn autoscroll(&mut self) { - if let Some(active_entry) = self.active_entry { - self.list.scroll_to(active_entry.index); + if let Some(selected_entry) = self.selected_entry { + self.list.scroll_to(selected_entry.index); } } @@ -280,7 +280,7 @@ impl ProjectPanel { fn update_visible_entries( &mut self, - new_active_entry: Option<(usize, usize)>, + new_selected_entry: Option<(usize, usize)>, cx: &mut ViewContext, ) { let worktrees = self.project.read(cx).worktrees(); @@ -308,18 +308,18 @@ impl ProjectPanel { let mut entry_iter = snapshot.entries(false); while let Some(item) = entry_iter.entry() { visible_worktree_entries.push(entry_iter.offset()); - if let Some(new_active_entry) = new_active_entry { - if new_active_entry == (worktree.id(), item.id) { - self.active_entry = Some(ActiveEntry { + if let Some(new_selected_entry) = new_selected_entry { + if new_selected_entry == (worktree.id(), item.id) { + self.selected_entry = Some(SelectedEntry { worktree_id, entry_id: item.id, index: entry_ix, }); } - } else if self.active_entry.map_or(false, |e| { + } else if self.selected_entry.map_or(false, |e| { e.worktree_id == worktree_id && e.entry_id == item.id }) { - self.active_entry = Some(ActiveEntry { + self.selected_entry = Some(SelectedEntry { worktree_id, entry_id: item.id, index: entry_ix, @@ -407,7 +407,7 @@ impl ProjectPanel { depth: entry.path.components().count(), is_dir: entry.is_dir(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), - is_active: self.active_entry.map_or(false, |e| { + is_active: self.selected_entry.map_or(false, |e| { e.worktree_id == worktree.id() && e.entry_id == entry.id }), }; @@ -434,7 +434,7 @@ impl ProjectPanel { cx, |state, _| { let style = if details.is_active { - &theme.active_entry + &theme.selected_entry } else if state.hovered { &theme.hovered_entry } else { diff --git a/zed/src/theme.rs b/zed/src/theme.rs index b4561abf1f729825e12665b472bf0db08e23e38e..4fed6e34a220bc3f65712f7122f513244ba1a16e 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -114,7 +114,7 @@ pub struct ProjectPanel { pub entry_base_padding: f32, pub entry: ContainedText, pub hovered_entry: ContainedText, - pub active_entry: ContainedText, + pub selected_entry: ContainedText, } #[derive(Deserialize)] From e98731876ce440cc68b88f6599f66f1ef0150f82 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Sep 2021 15:05:05 +0200 Subject: [PATCH 15/26] :lipstick: --- zed/src/project_panel.rs | 233 +++++++++++++++++++-------------------- 1 file changed, 116 insertions(+), 117 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 5c74279b864d694226cbe3b0b29c04130d26b280..a7f8ee094909d689505a5aecaf9afa65940b7b62 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -1,6 +1,8 @@ use crate::{ project::{self, Project}, - theme, worktree, Settings, + theme, + worktree::{self, Worktree}, + Settings, }; use gpui::{ action, @@ -26,13 +28,13 @@ pub struct ProjectPanel { list: UniformListState, visible_entries: Vec>, expanded_dir_ids: HashMap>, - selected_entry: Option, + selection: Option, settings: watch::Receiver, handle: WeakViewHandle, } #[derive(Copy, Clone)] -struct SelectedEntry { +struct Selection { worktree_id: usize, entry_id: usize, index: usize, @@ -44,7 +46,7 @@ struct EntryDetails { depth: usize, is_dir: bool, is_expanded: bool, - is_active: bool, + is_selected: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -53,8 +55,8 @@ pub struct ProjectEntry { pub entry_id: usize, } -action!(ExpandActiveEntry); -action!(CollapseActiveEntry); +action!(ExpandSelectedEntry); +action!(CollapseSelectedEntry); action!(ToggleExpanded, ProjectEntry); action!(Open, ProjectEntry); @@ -65,8 +67,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); cx.add_bindings([ - Binding::new("right", ExpandActiveEntry, None), - Binding::new("left", CollapseActiveEntry, None), + Binding::new("right", ExpandSelectedEntry, None), + Binding::new("left", CollapseSelectedEntry, None), ]); } @@ -84,18 +86,17 @@ impl ProjectPanel { }) .detach(); cx.subscribe(&project, |this, _, event, cx| match event { - project::Event::ActiveEntryChanged(entry) => { - if let Some((worktree_id, entry_id)) = *entry { - this.expand_entry(worktree_id, entry_id, cx); - this.update_visible_entries(Some((worktree_id, entry_id)), cx); - cx.notify(); - } + project::Event::ActiveEntryChanged(Some((worktree_id, entry_id))) => { + this.expand_entry(*worktree_id, *entry_id, cx); + this.update_visible_entries(Some((*worktree_id, *entry_id)), cx); + cx.notify(); } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); this.update_visible_entries(None, cx); cx.notify(); } + _ => {} }) .detach(); @@ -105,68 +106,60 @@ impl ProjectPanel { list: Default::default(), visible_entries: Default::default(), expanded_dir_ids: Default::default(), - selected_entry: None, + selection: None, handle: cx.handle().downgrade(), }; this.update_visible_entries(None, cx); this } - fn expand_selected_entry(&mut self, _: &ExpandActiveEntry, cx: &mut ViewContext) { - if let Some(selected_entry) = self.selected_entry { - let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(selected_entry.worktree_id) { - if let Some(entry) = worktree.read(cx).entry_for_id(selected_entry.entry_id) { - if entry.is_dir() { - if let Some(expanded_dir_ids) = - self.expanded_dir_ids.get_mut(&selected_entry.worktree_id) - { - match expanded_dir_ids.binary_search(&selected_entry.entry_id) { - Ok(_) => self.select_next(&SelectNext, cx), - Err(ix) => { - expanded_dir_ids.insert(ix, selected_entry.entry_id); - self.update_visible_entries(None, cx); - cx.notify(); - } - } - } - } else { + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) { + expanded_dir_ids + } else { + return; + }; + + if entry.is_dir() { + match expanded_dir_ids.binary_search(&entry.id) { + Ok(_) => self.select_next(&SelectNext, cx), + Err(ix) => { + expanded_dir_ids.insert(ix, entry.id); + self.update_visible_entries(None, cx); + cx.notify(); } } + } else { } } } - fn collapse_selected_entry(&mut self, _: &CollapseActiveEntry, cx: &mut ViewContext) { - if let Some(selected_entry) = self.selected_entry { - let project = self.project.read(cx); - if let Some(worktree) = project.worktree_for_id(selected_entry.worktree_id) { - let worktree = worktree.read(cx); - if let Some(mut entry) = worktree.entry_for_id(selected_entry.entry_id) { - if let Some(expanded_dir_ids) = - self.expanded_dir_ids.get_mut(&selected_entry.worktree_id) - { - loop { - match expanded_dir_ids.binary_search(&entry.id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - self.update_visible_entries( - Some((selected_entry.worktree_id, entry.id)), - cx, - ); - cx.notify(); - break; - } - Err(_) => { - if let Some(parent_entry) = - entry.path.parent().and_then(|p| worktree.entry_for_path(p)) - { - entry = parent_entry; - } else { - break; - } - } - } + fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, mut entry)) = self.selected_entry(cx) { + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) { + expanded_dir_ids + } else { + return; + }; + + loop { + match expanded_dir_ids.binary_search(&entry.id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + self.update_visible_entries(Some((worktree.id(), entry.id)), cx); + cx.notify(); + break; + } + Err(_) => { + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; } } } @@ -195,33 +188,29 @@ impl ProjectPanel { } fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(selected_entry) = self.selected_entry { - if selected_entry.index > 0 { - let (worktree_id, entry) = self - .visible_entry_for_index(selected_entry.index - 1, cx) - .unwrap(); - self.selected_entry = Some(SelectedEntry { - worktree_id, - entry_id: entry.id, - index: selected_entry.index - 1, - }); - self.autoscroll(); - cx.notify(); - } + if let Some(selection) = self.selection { + let prev_ix = selection.index.saturating_sub(1); + let (worktree, entry) = self.visible_entry_for_index(prev_ix, cx).unwrap(); + self.selection = Some(Selection { + worktree_id: worktree.id(), + entry_id: entry.id, + index: prev_ix, + }); + self.autoscroll(); + cx.notify(); } else { self.select_first(cx); } } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(selected_entry) = self.selected_entry { - if let Some((worktree_id, entry)) = - self.visible_entry_for_index(selected_entry.index + 1, cx) - { - self.selected_entry = Some(SelectedEntry { - worktree_id, + if let Some(selection) = self.selection { + let next_ix = selection.index + 1; + if let Some((worktree, entry)) = self.visible_entry_for_index(next_ix, cx) { + self.selection = Some(Selection { + worktree_id: worktree.id(), entry_id: entry.id, - index: selected_entry.index + 1, + index: next_ix, }); self.autoscroll(); cx.notify(); @@ -236,7 +225,7 @@ impl ProjectPanel { let worktree_id = worktree.id(); let worktree = worktree.read(cx); if let Some(root_entry) = worktree.root_entry() { - self.selected_entry = Some(SelectedEntry { + self.selection = Some(Selection { worktree_id, entry_id: root_entry.id, index: 0, @@ -248,8 +237,8 @@ impl ProjectPanel { } fn autoscroll(&mut self) { - if let Some(selected_entry) = self.selected_entry { - self.list.scroll_to(selected_entry.index); + if let Some(selection) = self.selection { + self.list.scroll_to(selection.index); } } @@ -257,13 +246,13 @@ impl ProjectPanel { &self, target_ix: usize, cx: &'a AppContext, - ) -> Option<(usize, &'a worktree::Entry)> { + ) -> Option<(&'a Worktree, &'a worktree::Entry)> { let project = self.project.read(cx); let mut offset = None; let mut ix = 0; for (worktree_ix, visible_entries) in self.visible_entries.iter().enumerate() { if target_ix < ix + visible_entries.len() { - let worktree = &project.worktrees()[worktree_ix]; + let worktree = project.worktrees()[worktree_ix].read(cx); offset = Some((worktree, visible_entries[target_ix - ix])); break; } else { @@ -272,12 +261,22 @@ impl ProjectPanel { } offset.and_then(|(worktree, offset)| { - let mut entries = worktree.read(cx).entries(false); + let mut entries = worktree.entries(false); entries.advance_to_offset(offset); - Some((worktree.id(), entries.entry()?)) + Some((worktree, entries.entry()?)) }) } + fn selected_entry<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(&'a Worktree, &'a worktree::Entry)> { + let selection = self.selection?; + let project = self.project.read(cx); + let worktree = project.worktree_for_id(selection.worktree_id)?.read(cx); + Some((worktree, worktree.entry_for_id(selection.entry_id)?)) + } + fn update_visible_entries( &mut self, new_selected_entry: Option<(usize, usize)>, @@ -310,16 +309,16 @@ impl ProjectPanel { visible_worktree_entries.push(entry_iter.offset()); if let Some(new_selected_entry) = new_selected_entry { if new_selected_entry == (worktree.id(), item.id) { - self.selected_entry = Some(SelectedEntry { + self.selection = Some(Selection { worktree_id, entry_id: item.id, index: entry_ix, }); } - } else if self.selected_entry.map_or(false, |e| { + } else if self.selection.map_or(false, |e| { e.worktree_id == worktree_id && e.entry_id == item.id }) { - self.selected_entry = Some(SelectedEntry { + self.selection = Some(Selection { worktree_id, entry_id: item.id, index: entry_ix, @@ -407,7 +406,7 @@ impl ProjectPanel { depth: entry.path.components().count(), is_dir: entry.is_dir(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), - is_active: self.selected_entry.map_or(false, |e| { + is_selected: self.selection.map_or(false, |e| { e.worktree_id == worktree.id() && e.entry_id == entry.id }), }; @@ -433,7 +432,7 @@ impl ProjectPanel { (entry.worktree_ix, entry.entry_id), cx, |state, _| { - let style = if details.is_active { + let style = if details.is_selected { &theme.selected_entry } else if state.hovered { &theme.hovered_entry @@ -578,56 +577,56 @@ mod tests { depth: 0, is_dir: true, is_expanded: true, - is_active: false, + is_selected: false, }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, is_dir: false, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "a".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "b".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "c".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "root2".to_string(), depth: 0, is_dir: true, is_expanded: true, - is_active: false + is_selected: false }, EntryDetails { filename: "d".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false + is_selected: false }, EntryDetails { filename: "e".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false + is_selected: false } ], ); @@ -641,70 +640,70 @@ mod tests { depth: 0, is_dir: true, is_expanded: true, - is_active: false, + is_selected: false, }, EntryDetails { filename: ".dockerignore".to_string(), depth: 1, is_dir: false, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "a".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "b".to_string(), depth: 1, is_dir: true, is_expanded: true, - is_active: false, + is_selected: false, }, EntryDetails { filename: "3".to_string(), depth: 2, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "4".to_string(), depth: 2, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "c".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false, + is_selected: false, }, EntryDetails { filename: "root2".to_string(), depth: 0, is_dir: true, is_expanded: true, - is_active: false + is_selected: false }, EntryDetails { filename: "d".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false + is_selected: false }, EntryDetails { filename: "e".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false + is_selected: false } ] ); @@ -717,21 +716,21 @@ mod tests { depth: 2, is_dir: true, is_expanded: false, - is_active: false + is_selected: false }, EntryDetails { filename: "c".to_string(), depth: 1, is_dir: true, is_expanded: false, - is_active: false + is_selected: false }, EntryDetails { filename: "root2".to_string(), depth: 0, is_dir: true, is_expanded: true, - is_active: false + is_selected: false } ] ); From 41fea2de1c80bb1005b78c429841462fde07a2b5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 30 Sep 2021 16:19:24 +0200 Subject: [PATCH 16/26] Open buffer when trying to expand a file --- zed/src/project_panel.rs | 111 +++++++++++++++++++++++++-------------- zed/src/workspace.rs | 13 +---- 2 files changed, 75 insertions(+), 49 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index a7f8ee094909d689505a5aecaf9afa65940b7b62..5e6747de7812eb27426a30b6795a00436540a884 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -1,6 +1,7 @@ use crate::{ project::{self, Project}, theme, + workspace::Workspace, worktree::{self, Worktree}, Settings, }; @@ -14,7 +15,7 @@ use gpui::{ }, platform::CursorStyle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, - ViewContext, WeakViewHandle, + ViewContext, ViewHandle, WeakViewHandle, }; use postage::watch; use std::{ @@ -51,7 +52,7 @@ struct EntryDetails { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ProjectEntry { - pub worktree_ix: usize, + pub worktree_id: usize, pub entry_id: usize, } @@ -66,51 +67,73 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::toggle_expanded); cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); + cx.add_action(ProjectPanel::open_entry); cx.add_bindings([ Binding::new("right", ExpandSelectedEntry, None), Binding::new("left", CollapseSelectedEntry, None), ]); } -pub enum Event {} +pub enum Event { + OpenedEntry { worktree_id: usize, entry_id: usize }, +} impl ProjectPanel { pub fn new( project: ModelHandle, settings: watch::Receiver, - cx: &mut ViewContext, - ) -> Self { - cx.observe(&project, |this, _, cx| { - this.update_visible_entries(None, cx); - cx.notify(); - }) - .detach(); - cx.subscribe(&project, |this, _, event, cx| match event { - project::Event::ActiveEntryChanged(Some((worktree_id, entry_id))) => { - this.expand_entry(*worktree_id, *entry_id, cx); - this.update_visible_entries(Some((*worktree_id, *entry_id)), cx); - cx.notify(); - } - project::Event::WorktreeRemoved(id) => { - this.expanded_dir_ids.remove(id); + cx: &mut ViewContext, + ) -> ViewHandle { + let project_panel = cx.add_view(|cx: &mut ViewContext| { + cx.observe(&project, |this, _, cx| { this.update_visible_entries(None, cx); cx.notify(); + }) + .detach(); + cx.subscribe(&project, |this, _, event, cx| match event { + project::Event::ActiveEntryChanged(Some((worktree_id, entry_id))) => { + this.expand_entry(*worktree_id, *entry_id, cx); + this.update_visible_entries(Some((*worktree_id, *entry_id)), cx); + cx.notify(); + } + project::Event::WorktreeRemoved(id) => { + this.expanded_dir_ids.remove(id); + this.update_visible_entries(None, cx); + cx.notify(); + } + _ => {} + }) + .detach(); + + let mut this = Self { + project: project.clone(), + settings, + list: Default::default(), + visible_entries: Default::default(), + expanded_dir_ids: Default::default(), + selection: None, + handle: cx.handle().downgrade(), + }; + this.update_visible_entries(None, cx); + this + }); + cx.subscribe(&project_panel, move |workspace, _, event, cx| match event { + Event::OpenedEntry { + worktree_id, + entry_id, + } => { + if let Some(worktree) = project.read(cx).worktree_for_id(*worktree_id) { + if let Some(entry) = worktree.read(cx).entry_for_id(*entry_id) { + workspace + .open_entry((worktree.id(), entry.path.clone()), cx) + .map(|t| t.detach()); + } + } } - _ => {} }) .detach(); - let mut this = Self { - project, - settings, - list: Default::default(), - visible_entries: Default::default(), - expanded_dir_ids: Default::default(), - selection: None, - handle: cx.handle().downgrade(), - }; - this.update_visible_entries(None, cx); - this + project_panel } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { @@ -132,6 +155,11 @@ impl ProjectPanel { } } } else { + let event = Event::OpenedEntry { + worktree_id: worktree.id(), + entry_id: entry.id, + }; + cx.emit(event); } } } @@ -169,10 +197,10 @@ impl ProjectPanel { fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { let ProjectEntry { - worktree_ix, + worktree_id, entry_id, } = action.0; - let worktree_id = self.project.read(cx).worktrees()[worktree_ix].id(); + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { match expanded_dir_ids.binary_search(&entry_id) { Ok(ix) => { @@ -203,6 +231,13 @@ impl ProjectPanel { } } + fn open_entry(&mut self, action: &Open, cx: &mut ViewContext) { + cx.emit(Event::OpenedEntry { + worktree_id: action.0.worktree_id, + entry_id: action.0.entry_id, + }); + } + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { if let Some(selection) = self.selection { let next_ix = selection.index + 1; @@ -411,7 +446,7 @@ impl ProjectPanel { }), }; let entry = ProjectEntry { - worktree_ix, + worktree_id: worktree.id(), entry_id: entry.id, }; callback(entry, details, cx); @@ -429,7 +464,7 @@ impl ProjectPanel { ) -> ElementBox { let is_dir = details.is_dir; MouseEventHandler::new::( - (entry.worktree_ix, entry.entry_id), + (entry.worktree_id, entry.entry_id), cx, |state, _| { let style = if details.is_selected { @@ -568,7 +603,8 @@ mod tests { .read_with(&cx, |t, _| t.as_local().unwrap().scan_complete()) .await; - let (_, panel) = cx.add_window(|cx| ProjectPanel::new(project, settings, cx)); + let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx)); + let panel = workspace.update(&mut cx, |_, cx| ProjectPanel::new(project, settings, cx)); assert_eq!( visible_entry_details(&panel, 0..50, &mut cx), &[ @@ -742,14 +778,13 @@ mod tests { ) { let path = path.as_ref(); panel.update(cx, |panel, cx| { - for (worktree_ix, worktree) in panel.project.read(cx).worktrees().iter().enumerate() - { + for worktree in panel.project.read(cx).worktrees() { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; panel.toggle_expanded( &ToggleExpanded(ProjectEntry { - worktree_ix, + worktree_id: worktree.id(), entry_id, }), cx, diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 9478c40a57b8d45144f152537aa7782b013f683b..81f1e10a41a0429501bd1546778dfa5e05f2272d 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -8,7 +8,7 @@ use crate::{ fs::Fs, people_panel::{JoinWorktree, LeaveWorktree, PeoplePanel, ShareWorktree, UnshareWorktree}, project::Project, - project_panel::{self, ProjectPanel}, + project_panel::ProjectPanel, rpc, settings::Settings, user, @@ -54,14 +54,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Workspace::save_active_item); cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::open_new_file); - cx.add_action(|this: &mut Workspace, action: &project_panel::Open, cx| { - if let Some(worktree) = this.worktrees(cx).get(action.0.worktree_ix) { - if let Some(entry) = worktree.read(cx).entry_for_id(action.0.entry_id) { - this.open_entry((worktree.id(), entry.path.clone()), cx) - .map(|task| task.detach()); - } - } - }); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::share_worktree); cx.add_action(Workspace::unshare_worktree); @@ -386,8 +378,7 @@ impl Workspace { let mut left_sidebar = Sidebar::new(Side::Left); left_sidebar.add_item( "icons/folder-tree-16.svg", - cx.add_view(|cx| ProjectPanel::new(project.clone(), app_state.settings.clone(), cx)) - .into(), + ProjectPanel::new(project.clone(), app_state.settings.clone(), cx).into(), ); let mut right_sidebar = Sidebar::new(Side::Right); From 18a379f20ca6028a1dbf7406e4822e17a9c53905 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 30 Sep 2021 10:50:58 -0600 Subject: [PATCH 17/26] Scope the left and right bindings in the project panel --- zed/src/project_panel.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 5e6747de7812eb27426a30b6795a00436540a884..e7142be0a2f62a97b0674f058713449fe6b5a964 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -69,8 +69,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::open_entry); cx.add_bindings([ - Binding::new("right", ExpandSelectedEntry, None), - Binding::new("left", CollapseSelectedEntry, None), + Binding::new("right", ExpandSelectedEntry, Some("ProjectPanel")), + Binding::new("left", CollapseSelectedEntry, Some("ProjectPanel")), ]); } From 6fba17a5e1ea9481c3f4278953844c9af00a7546 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 10:24:47 -0700 Subject: [PATCH 18/26] Add disclosure arrows to the project panel Co-Authored-By: Nathan Sobo --- zed/assets/icons/disclosure-closed.svg | 3 ++ zed/assets/icons/disclosure-open.svg | 3 ++ zed/assets/themes/_base.toml | 5 +++- zed/src/project_panel.rs | 38 ++++++++++++++++++++++++-- zed/src/theme.rs | 16 +++++++++-- 5 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 zed/assets/icons/disclosure-closed.svg create mode 100644 zed/assets/icons/disclosure-open.svg diff --git a/zed/assets/icons/disclosure-closed.svg b/zed/assets/icons/disclosure-closed.svg new file mode 100644 index 0000000000000000000000000000000000000000..87a898787e9476a410d4c32cf6fdf659e3189fa6 --- /dev/null +++ b/zed/assets/icons/disclosure-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/icons/disclosure-open.svg b/zed/assets/icons/disclosure-open.svg new file mode 100644 index 0000000000000000000000000000000000000000..3a76a74d3116118a0e84be54a743325d2b9796bf --- /dev/null +++ b/zed/assets/icons/disclosure-open.svg @@ -0,0 +1,3 @@ + + + diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index a001cf7687dfab6d7e99a1813fe0835859a70f36..236e308543f27cd7b0e4a5102b1840cd9cf2349e 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -165,8 +165,11 @@ padding = 0 entry_base_padding = "$panel.padding" [project_panel.entry] -extends = "$text.0" +text = "$text.1" padding = { top = 3, bottom = 3 } +icon_color = "$text.3.color" +icon_size = 8 +icon_spacing = 8 [project_panel.hovered_entry] extends = "$project_panel.entry" diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index e7142be0a2f62a97b0674f058713449fe6b5a964..82ae9f1892291c85883617315cff7de11f09a9b1 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -7,7 +7,10 @@ use crate::{ }; use gpui::{ action, - elements::{Label, MouseEventHandler, UniformList, UniformListState}, + elements::{ + Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, Svg, + UniformList, UniformListState, + }, keymap::{ self, menu::{SelectNext, SelectPrev}, @@ -474,7 +477,38 @@ impl ProjectPanel { } else { &theme.entry }; - Label::new(details.filename, style.text.clone()) + Flex::row() + .with_child( + ConstrainedBox::new( + Align::new( + ConstrainedBox::new(if is_dir { + if details.is_expanded { + Svg::new("icons/disclosure-open.svg") + .with_color(style.icon_color) + .boxed() + } else { + Svg::new("icons/disclosure-closed.svg") + .with_color(style.icon_color) + .boxed() + } + } else { + Empty::new().boxed() + }) + .with_max_width(style.icon_size) + .with_max_height(style.icon_size) + .boxed(), + ) + .boxed(), + ) + .with_width(style.icon_size) + .boxed(), + ) + .with_child( + Label::new(details.filename, style.text.clone()) + .contained() + .with_margin_left(style.icon_spacing) + .boxed(), + ) .contained() .with_style(style.container) .with_padding_left(theme.entry_base_padding + details.depth as f32 * 20.) diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 4fed6e34a220bc3f65712f7122f513244ba1a16e..1153b76dce054b403d2628c91028a92a298f2835 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -112,9 +112,19 @@ pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, pub entry_base_padding: f32, - pub entry: ContainedText, - pub hovered_entry: ContainedText, - pub selected_entry: ContainedText, + pub entry: ProjectPanelEntry, + pub hovered_entry: ProjectPanelEntry, + pub selected_entry: ProjectPanelEntry, +} + +#[derive(Deserialize)] +pub struct ProjectPanelEntry { + #[serde(flatten)] + pub container: ContainerStyle, + pub text: TextStyle, + pub icon_color: Color, + pub icon_size: f32, + pub icon_spacing: f32, } #[derive(Deserialize)] From e37ea6c6c88deef9ac8cdb9d2ffa46093d1db8de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 13:28:45 -0700 Subject: [PATCH 19/26] Avoid rendering artifacts when label text is shaped with a non-integer width Co-Authored-By: Nathan Sobo --- gpui/src/elements/label.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index c1e048eb93e0c945db450f2112e7b20a1d735cda..4ae1504c1fb0a9dea6c9afbd2b6dc1f1a44711ff 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -135,7 +135,10 @@ impl Element for Label { ); let size = vec2f( - line.width().max(constraint.min.x()).min(constraint.max.x()), + line.width() + .ceil() + .max(constraint.min.x()) + .min(constraint.max.x()), cx.font_cache .line_height(self.style.text.font_id, self.style.text.font_size), ); From 177306d494e75292ef2169b66d0c4a4f390fe542 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 13:29:26 -0700 Subject: [PATCH 20/26] Add 'overlay' property to border For containers, this causes the border to be drawn on top of the child element. Co-Authored-By: Nathan Sobo --- gpui/src/elements/container.rs | 54 +++++++++++++++++++++++++--------- gpui/src/scene.rs | 6 ++++ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 02d4cece951781115ca3820fe258bbfc05e4e747..026542989e45e25cdaad0acc8a01086cdbf966b2 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -167,7 +167,10 @@ impl Element for Container { constraint: SizeConstraint, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - let size_buffer = self.margin_size() + self.padding_size() + self.border_size(); + let mut size_buffer = self.margin_size() + self.padding_size(); + if !self.style.border.overlay { + size_buffer += self.border_size(); + } let child_constraint = SizeConstraint { min: (constraint.min - size_buffer).max(Vector2F::zero()), max: (constraint.max - size_buffer).max(Vector2F::zero()), @@ -196,20 +199,43 @@ impl Element for Container { color: shadow.color, }); } - cx.scene.push_quad(Quad { - bounds: quad_bounds, - background: self.style.background_color, - border: self.style.border, - corner_radius: self.style.corner_radius, - }); - let child_origin = quad_bounds.origin() - + vec2f(self.style.padding.left, self.style.padding.top) - + vec2f( - self.style.border.left_width(), - self.style.border.top_width(), - ); - self.child.paint(child_origin, visible_bounds, cx); + let child_origin = + quad_bounds.origin() + vec2f(self.style.padding.left, self.style.padding.top); + + if self.style.border.overlay { + cx.scene.push_quad(Quad { + bounds: quad_bounds, + background: self.style.background_color, + border: Default::default(), + corner_radius: self.style.corner_radius, + }); + + self.child.paint(child_origin, visible_bounds, cx); + + cx.scene.push_layer(None); + cx.scene.push_quad(Quad { + bounds: quad_bounds, + background: Default::default(), + border: self.style.border, + corner_radius: self.style.corner_radius, + }); + cx.scene.pop_layer(); + } else { + cx.scene.push_quad(Quad { + bounds: quad_bounds, + background: self.style.background_color, + border: self.style.border, + corner_radius: self.style.corner_radius, + }); + + let child_origin = child_origin + + vec2f( + self.style.border.left_width(), + self.style.border.top_width(), + ); + self.child.paint(child_origin, visible_bounds, cx); + } } fn dispatch_event( diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 1b9c863647205a09fcafde18c2abdda1aa36e922..b833ffe627d07be3ef255eef9072f4a704d56825 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -69,6 +69,7 @@ pub struct Icon { pub struct Border { pub width: f32, pub color: Color, + pub overlay: bool, pub top: bool, pub right: bool, pub bottom: bool, @@ -85,6 +86,8 @@ impl<'de> Deserialize<'de> for Border { pub width: f32, pub color: Color, #[serde(default)] + pub overlay: bool, + #[serde(default)] pub top: bool, #[serde(default)] pub right: bool, @@ -98,6 +101,7 @@ impl<'de> Deserialize<'de> for Border { let mut border = Border { width: data.width, color: data.color, + overlay: data.overlay, top: data.top, bottom: data.bottom, left: data.left, @@ -329,6 +333,7 @@ impl Border { Self { width, color, + overlay: false, top: false, left: false, bottom: false, @@ -340,6 +345,7 @@ impl Border { Self { width, color, + overlay: false, top: true, left: true, bottom: true, From 5fb112ac4c85209a8ba755d6aadb15334c72b569 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 13:30:24 -0700 Subject: [PATCH 21/26] Vertically align root folder in project panel with tabs and sidebar icons Co-Authored-By: Nathan Sobo --- gpui/src/elements/uniform_list.rs | 32 +++++++++++++++++++++----- zed/assets/themes/_base.toml | 24 +++++++++++--------- zed/src/project_panel.rs | 12 ++++++++-- zed/src/theme.rs | 16 +++++++------ zed/src/workspace/pane.rs | 37 +++++++------------------------ zed/src/workspace/sidebar.rs | 15 +++++-------- 6 files changed, 72 insertions(+), 64 deletions(-) diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index aefe30e67b20220d96b7924ac96d91939a910852..f499801e6384380eed8875068c498a3ffa930ab3 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -43,6 +43,8 @@ where state: UniformListState, item_count: usize, append_items: F, + padding_top: f32, + padding_bottom: f32, } impl UniformList @@ -54,9 +56,21 @@ where state, item_count, append_items, + padding_top: 0., + padding_bottom: 0., } } + pub fn with_padding_top(mut self, padding: f32) -> Self { + self.padding_top = padding; + self + } + + pub fn with_padding_bottom(mut self, padding: f32) -> Self { + self.padding_bottom = padding; + self + } + fn scroll( &self, _: Vector2F, @@ -84,7 +98,7 @@ where } if let Some(item_ix) = state.scroll_to.take() { - let item_top = item_ix as f32 * item_height; + let item_top = self.padding_top + item_ix as f32 * item_height; let item_bottom = item_top + item_height; if item_top < state.scroll_top { @@ -137,11 +151,16 @@ where size.set_y(size.y().min(scroll_height).max(constraint.min.y())); } - scroll_max = (item_height * self.item_count as f32 - size.y()).max(0.); + let scroll_height = + item_height * self.item_count as f32 + self.padding_top + self.padding_bottom; + scroll_max = (scroll_height - size.y()).max(0.); self.autoscroll(scroll_max, size.y(), item_height); items.clear(); - let start = cmp::min((self.scroll_top() / item_height) as usize, self.item_count); + let start = cmp::min( + ((self.scroll_top() - self.padding_top) / item_height) as usize, + self.item_count, + ); let end = cmp::min( self.item_count, start + (size.y() / item_height).ceil() as usize + 1, @@ -173,8 +192,11 @@ where ) -> Self::PaintState { cx.scene.push_layer(Some(bounds)); - let mut item_origin = - bounds.origin() - vec2f(0.0, self.state.scroll_top() % layout.item_height); + let mut item_origin = bounds.origin() + - vec2f( + 0., + (self.state.scroll_top() - self.padding_top) % layout.item_height, + ); for item in &mut layout.items { item.paint(item_origin, visible_bounds, cx); diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 236e308543f27cd7b0e4a5102b1840cd9cf2349e..ddbd0fc86ea7793c6a3d2551cba33177e76b9cbd 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -18,6 +18,7 @@ padding = { right = 4 } width = 16 [workspace.tab] +height = 34 text = "$text.2" padding = { left = 12, right = 12 } icon_width = 8 @@ -26,10 +27,11 @@ icon_close = "$text.2.color" icon_close_active = "$text.0.color" icon_dirty = "$status.info" icon_conflict = "$status.warn" -border = { left = true, bottom = true, width = 1, color = "$border.0" } +border = { left = true, bottom = true, width = 1, color = "$border.0", overlay = true } [workspace.active_tab] extends = "$workspace.tab" +border.bottom = false background = "$surface.1" text = "$text.0" @@ -41,13 +43,14 @@ border = { right = true, width = 1, color = "$border.0" } padding = { left = 1 } background = "$border.0" -[workspace.sidebar.icon] -color = "$text.2.color" -height = 18 +[workspace.sidebar.item] +icon_color = "$text.2.color" +icon_size = 18 +height = "$workspace.tab.height" -[workspace.sidebar.active_icon] -extends = "$workspace.sidebar.icon" -color = "$text.0.color" +[workspace.sidebar.active_item] +extends = "$workspace.sidebar.item" +icon_color = "$text.0.color" [workspace.left_sidebar] extends = "$workspace.sidebar" @@ -58,7 +61,7 @@ extends = "$workspace.sidebar" border = { width = 1, color = "$border.0", left = true } [panel] -padding = 12 +padding = { top = 12, left = 12, bottom = 12, right = 12 } [chat_panel] extends = "$panel" @@ -161,12 +164,11 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding = 0 -entry_base_padding = "$panel.padding" +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" -padding = { top = 3, bottom = 3 } +height = 22 icon_color = "$text.3.color" icon_size = 8 icon_spacing = 8 diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 82ae9f1892291c85883617315cff7de11f09a9b1..06f706734602d8fede968451a6d8ea6af5c8c06b 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -507,11 +507,15 @@ impl ProjectPanel { Label::new(details.filename, style.text.clone()) .contained() .with_margin_left(style.icon_spacing) + .aligned() + .left() .boxed(), ) + .constrained() + .with_height(theme.entry.height) .contained() .with_style(style.container) - .with_padding_left(theme.entry_base_padding + details.depth as f32 * 20.) + .with_padding_left(theme.container.padding.left + details.depth as f32 * 20.) .boxed() }, ) @@ -534,6 +538,8 @@ impl View for ProjectPanel { fn render(&mut self, _: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { let settings = self.settings.clone(); + let mut container_style = settings.borrow().theme.project_panel.container; + let padding = std::mem::take(&mut container_style.padding); let handle = self.handle.clone(); UniformList::new( self.list.clone(), @@ -551,8 +557,10 @@ impl View for ProjectPanel { }) }, ) + .with_padding_top(padding.top) + .with_padding_bottom(padding.bottom) .contained() - .with_style(self.settings.borrow().theme.project_panel.container) + .with_style(container_style) .boxed() } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 1153b76dce054b403d2628c91028a92a298f2835..0788cacb47ef07f4e3260e0646a25ab655e338d5 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -67,6 +67,7 @@ pub struct OfflineIcon { #[derive(Clone, Deserialize)] pub struct Tab { + pub height: f32, #[serde(flatten)] pub container: ContainerStyle, #[serde(flatten)] @@ -84,14 +85,15 @@ pub struct Sidebar { #[serde(flatten)] pub container: ContainerStyle, pub width: f32, - pub icon: SidebarIcon, - pub active_icon: SidebarIcon, + pub item: SidebarItem, + pub active_item: SidebarItem, pub resize_handle: ContainerStyle, } #[derive(Deserialize)] -pub struct SidebarIcon { - pub color: Color, +pub struct SidebarItem { + pub icon_color: Color, + pub icon_size: f32, pub height: f32, } @@ -107,18 +109,18 @@ pub struct ChatPanel { pub hovered_sign_in_prompt: TextStyle, } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, - pub entry_base_padding: f32, pub entry: ProjectPanelEntry, pub hovered_entry: ProjectPanelEntry, pub selected_entry: ProjectPanelEntry, } -#[derive(Deserialize)] +#[derive(Debug, Deserialize)] pub struct ProjectPanelEntry { + pub height: f32, #[serde(flatten)] pub container: ContainerStyle, pub text: TextStyle, diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 31ec57354ad131f8542f211c5ee88cfb2520ad8f..6af62e8d1d80bf84e0392ac7510ee4f4733933ea 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -2,12 +2,11 @@ use super::{ItemViewHandle, SplitDirection}; use crate::settings::Settings; use gpui::{ action, - color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle, - Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, + Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, }; use postage::watch; use std::{cmp, path::Path, sync::Arc}; @@ -180,10 +179,6 @@ impl Pane { fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme; - let line_height = cx.font_cache().line_height( - theme.workspace.tab.label.text.font_id, - theme.workspace.tab.label.text.font_size, - ); enum Tabs {} let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { @@ -202,12 +197,11 @@ impl Pane { title.push('…'); } - let mut style = theme.workspace.tab.clone(); - if is_active { - style = theme.workspace.active_tab.clone(); - style.container.border.bottom = false; - style.container.padding.bottom += style.container.border.width; - } + let mut style = if is_active { + theme.workspace.active_tab.clone() + } else { + theme.workspace.tab.clone() + }; if ix == 0 { style.container.border.left = false; } @@ -319,26 +313,11 @@ impl Pane { }) } - // Ensure there's always a minimum amount of space after the last tab, - // so that the tab's border doesn't abut the window's border. - let mut border = Border::bottom(1.0, Color::default()); - border.color = theme.workspace.tab.container.border.color; - - row.add_child( - ConstrainedBox::new( - Container::new(Empty::new().boxed()) - .with_border(border) - .boxed(), - ) - .with_min_width(20.) - .named("fixed-filler"), - ); - row.add_child( Expanded::new( 0.0, Container::new(Empty::new().boxed()) - .with_border(border) + .with_border(theme.workspace.tab.container.border) .boxed(), ) .named("filler"), @@ -348,7 +327,7 @@ impl Pane { }); ConstrainedBox::new(tabs.boxed()) - .with_height(line_height + 16.) + .with_height(theme.workspace.tab.height) .named("tabs") } } diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 3ceba15df0051701873c472a993ced0fe0b0ed64..5e8d8c2f9c1f9ee7e5586bf32db5699fc124757b 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -68,11 +68,6 @@ impl Sidebar { pub fn render(&self, settings: &Settings, cx: &mut RenderContext) -> ElementBox { let side = self.side; - let theme = &settings.theme; - let line_height = cx.font_cache().line_height( - theme.workspace.tab.label.text.font_id, - theme.workspace.tab.label.text.font_size, - ); let theme = self.theme(settings); ConstrainedBox::new( @@ -80,9 +75,9 @@ impl Sidebar { Flex::column() .with_children(self.items.iter().enumerate().map(|(item_index, item)| { let theme = if Some(item_index) == self.active_item_ix { - &theme.active_icon + &theme.active_item } else { - &theme.icon + &theme.item }; enum SidebarButton {} MouseEventHandler::new::( @@ -93,15 +88,15 @@ impl Sidebar { Align::new( ConstrainedBox::new( Svg::new(item.icon_path) - .with_color(theme.color) + .with_color(theme.icon_color) .boxed(), ) - .with_height(theme.height) + .with_height(theme.icon_size) .boxed(), ) .boxed(), ) - .with_height(line_height + 16.0) + .with_height(theme.height) .boxed() }, ) From fef0bdf6c3557807db81c006ae1f693715c877ff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 13:44:07 -0700 Subject: [PATCH 22/26] Don't autoscroll the project panel when worktree changes Co-Authored-By: Nathan Sobo --- zed/src/project_panel.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 06f706734602d8fede968451a6d8ea6af5c8c06b..d6830de7a3523398bb9b5ab57116015b311940d5 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -97,6 +97,7 @@ impl ProjectPanel { project::Event::ActiveEntryChanged(Some((worktree_id, entry_id))) => { this.expand_entry(*worktree_id, *entry_id, cx); this.update_visible_entries(Some((*worktree_id, *entry_id)), cx); + this.autoscroll(); cx.notify(); } project::Event::WorktreeRemoved(id) => { @@ -373,8 +374,6 @@ impl ProjectPanel { } self.visible_entries.push(visible_worktree_entries); } - - self.autoscroll(); } fn expand_entry(&mut self, worktree_id: usize, entry_id: usize, cx: &mut ViewContext) { From ad92bfe79f8d3b4f07d80b2689e34832a5caefe8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 14:06:09 -0700 Subject: [PATCH 23/26] Avoid duplicate notifications for the same entity in the same effect cycle Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index ebe6c89a8ace2a965138658a8faaccab66b2f1ef..b11fcb4b97c10c623a4d1d46a0609453d11c044c 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -312,6 +312,7 @@ impl App { let mut state = self.0.borrow_mut(); state.pending_flushes += 1; let result = callback(&mut *state); + state.pending_notifications.clear(); state.flush_effects(); result } @@ -668,6 +669,7 @@ pub struct MutableAppContext { debug_elements_callbacks: HashMap crate::json::Value>>, foreground: Rc, pending_effects: VecDeque, + pending_notifications: HashSet, pending_flushes: usize, flushing_effects: bool, next_cursor_style_handle_id: Arc, @@ -708,6 +710,7 @@ impl MutableAppContext { debug_elements_callbacks: HashMap::new(), foreground, pending_effects: VecDeque::new(), + pending_notifications: HashSet::new(), pending_flushes: 0, flushing_effects: false, next_cursor_style_handle_id: Default::default(), @@ -1015,10 +1018,18 @@ impl MutableAppContext { observations: Some(Arc::downgrade(&self.observations)), } } + pub(crate) fn notify_model(&mut self, model_id: usize) { + if self.pending_notifications.insert(model_id) { + self.pending_effects + .push_back(Effect::ModelNotification { model_id }); + } + } pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) { - self.pending_effects - .push_back(Effect::ViewNotification { window_id, view_id }); + if self.pending_notifications.insert(view_id) { + self.pending_effects + .push_back(Effect::ViewNotification { window_id, view_id }); + } } pub fn dispatch_action( @@ -1400,6 +1411,7 @@ impl MutableAppContext { refreshing = true; } } + self.pending_notifications.clear(); self.remove_dropped_entities(); } else { self.remove_dropped_entities(); @@ -1411,6 +1423,7 @@ impl MutableAppContext { if self.pending_effects.is_empty() { self.flushing_effects = false; + self.pending_notifications.clear(); break; } else { refreshing = false; @@ -1983,11 +1996,7 @@ impl<'a, T: Entity> ModelContext<'a, T> { } pub fn notify(&mut self) { - self.app - .pending_effects - .push_back(Effect::ModelNotification { - model_id: self.model_id, - }); + self.app.notify_model(self.model_id); } pub fn subscribe( From 6007fa6ab37020ef84e47dfd5f63481dd12837be Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 14:16:28 -0700 Subject: [PATCH 24/26] Style the active entry in the project panel Co-Authored-By: Nathan Sobo --- zed/assets/themes/_base.toml | 6 +++++- zed/src/project_panel.rs | 11 +++++------ zed/src/theme.rs | 1 + 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index ddbd0fc86ea7793c6a3d2551cba33177e76b9cbd..e03b3eb1340d5f39f8d2c9c31eb003838d008f98 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -179,7 +179,11 @@ background = "$state.hover" [project_panel.selected_entry] extends = "$project_panel.entry" -background = "#ff0000" +text = { extends = "$text.0" } + +[project_panel.hovered_selected_entry] +extends = "$project_panel.hovered_entry" +text = { extends = "$text.0" } [selector] background = "$surface.0" diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index d6830de7a3523398bb9b5ab57116015b311940d5..94348b92f986593664661475e35152948048814f 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -469,12 +469,11 @@ impl ProjectPanel { (entry.worktree_id, entry.entry_id), cx, |state, _| { - let style = if details.is_selected { - &theme.selected_entry - } else if state.hovered { - &theme.hovered_entry - } else { - &theme.entry + let style = match (details.is_selected, state.hovered) { + (false, false) => &theme.entry, + (false, true) => &theme.hovered_entry, + (true, false) => &theme.selected_entry, + (true, true) => &theme.hovered_selected_entry, }; Flex::row() .with_child( diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 0788cacb47ef07f4e3260e0646a25ab655e338d5..59bd2c5d6ec7682b72fc4f493328066ae8f740a7 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -116,6 +116,7 @@ pub struct ProjectPanel { pub entry: ProjectPanelEntry, pub hovered_entry: ProjectPanelEntry, pub selected_entry: ProjectPanelEntry, + pub hovered_selected_entry: ProjectPanelEntry, } #[derive(Debug, Deserialize)] From 8dcd38c25a0afdd8baa9fce93af5fcf5ada37151 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 15:02:27 -0700 Subject: [PATCH 25/26] Fix ProjectPanel test failure --- zed/src/project_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/project_panel.rs b/zed/src/project_panel.rs index 94348b92f986593664661475e35152948048814f..3fdb0c442ae083f5585b0ceb63c24da4f6c34926 100644 --- a/zed/src/project_panel.rs +++ b/zed/src/project_panel.rs @@ -737,7 +737,7 @@ mod tests { depth: 1, is_dir: true, is_expanded: true, - is_selected: false, + is_selected: true, }, EntryDetails { filename: "3".to_string(), From 2b3e5945c64d78784751340b205b95356cd8f41f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 30 Sep 2021 15:20:03 -0700 Subject: [PATCH 26/26] Add key bindings to toggle the project panel - Use `cmd-1` to open the project panel and toggle focus between it and the workspace center. - Use `cmd-shift-1` to open or close the project panel. --- gpui/src/app.rs | 5 +++++ zed/src/workspace.rs | 39 +++++++++++++++++++++++++++++++++++- zed/src/workspace/sidebar.rs | 14 ++++++++++--- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index b11fcb4b97c10c623a4d1d46a0609453d11c044c..5536b78bf583a5483ae9052587ec88e179ed9960 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2900,6 +2900,11 @@ impl AnyViewHandle { TypeId::of::() == self.view_type } + pub fn is_focused(&self, cx: &AppContext) -> bool { + cx.focused_view_id(self.window_id) + .map_or(false, |focused_id| focused_id == self.view_id) + } + pub fn downcast(self) -> Option> { if self.is::() { let result = Some(ViewHandle { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 81f1e10a41a0429501bd1546778dfa5e05f2272d..f382af321512095256b30c0b906af684c89d9e31 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -12,6 +12,7 @@ use crate::{ rpc, settings::Settings, user, + workspace::sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus}, worktree::{File, Worktree}, AppState, Authenticate, }; @@ -31,7 +32,6 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::{prelude::Stream, watch}; -use sidebar::{Side, Sidebar, ToggleSidebarItem}; use std::{ collections::{hash_map::Entry, HashMap}, future::Future, @@ -55,6 +55,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::open_new_file); cx.add_action(Workspace::toggle_sidebar_item); + cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_action(Workspace::share_worktree); cx.add_action(Workspace::unshare_worktree); cx.add_action(Workspace::join_worktree); @@ -62,6 +63,22 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_bindings(vec![ Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), + Binding::new( + "cmd-shift-!", + ToggleSidebarItem(SidebarItemId { + side: Side::Left, + item_index: 0, + }), + None, + ), + Binding::new( + "cmd-1", + ToggleSidebarItemFocus(SidebarItemId { + side: Side::Left, + item_index: 0, + }), + None, + ), ]); pane::init(cx); } @@ -805,6 +822,26 @@ impl Workspace { cx.notify(); } + pub fn toggle_sidebar_item_focus( + &mut self, + action: &ToggleSidebarItemFocus, + cx: &mut ViewContext, + ) { + let sidebar = match action.0.side { + Side::Left => &mut self.left_sidebar, + Side::Right => &mut self.right_sidebar, + }; + sidebar.activate_item(action.0.item_index); + if let Some(active_item) = sidebar.active_item() { + if active_item.is_focused(cx) { + cx.focus_self(); + } else { + cx.focus(active_item); + } + } + cx.notify(); + } + pub fn debug_elements(&mut self, _: &DebugElements, cx: &mut ViewContext) { match to_string_pretty(&cx.debug_elements()) { Ok(json) => { diff --git a/zed/src/workspace/sidebar.rs b/zed/src/workspace/sidebar.rs index 5e8d8c2f9c1f9ee7e5586bf32db5699fc124757b..377e5a16f590d3cd0b750ec1e317ed8c31371887 100644 --- a/zed/src/workspace/sidebar.rs +++ b/zed/src/workspace/sidebar.rs @@ -23,10 +23,11 @@ struct Item { view: AnyViewHandle, } -action!(ToggleSidebarItem, ToggleArg); +action!(ToggleSidebarItem, SidebarItemId); +action!(ToggleSidebarItemFocus, SidebarItemId); #[derive(Clone)] -pub struct ToggleArg { +pub struct SidebarItemId { pub side: Side, pub item_index: usize, } @@ -45,6 +46,10 @@ impl Sidebar { self.items.push(Item { icon_path, view }); } + pub fn activate_item(&mut self, item_ix: usize) { + self.active_item_ix = Some(item_ix); + } + pub fn toggle_item(&mut self, item_ix: usize) { if self.active_item_ix == Some(item_ix) { self.active_item_ix = None; @@ -102,7 +107,10 @@ impl Sidebar { ) .with_cursor_style(CursorStyle::PointingHand) .on_mouse_down(move |cx| { - cx.dispatch_action(ToggleSidebarItem(ToggleArg { side, item_index })) + cx.dispatch_action(ToggleSidebarItem(SidebarItemId { + side, + item_index, + })) }) .boxed() }))