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 {