Start work on ProjectPanel

Max Brunsfeld created

Change summary

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

Detailed changes

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

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"))]

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<ModelHandle<Worktree>>,
+    languages: Arc<LanguageRegistry>,
+    rpc: Arc<Client>,
+    fs: Arc<dyn Fs>,
 }
 
 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<Worktree>) {
-        self.worktrees.push(worktree);
+    pub fn add_local_worktree(
+        &mut self,
+        path: &Path,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<ModelHandle<Worktree>>> {
+        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<Self>,
-    ) -> Option<Task<Result<u64>>> {
-        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<Result<ModelHandle<Worktree>>> {
+        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<Worktree>, cx: &mut ModelContext<Self>) {
+        cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
+        self.worktrees.push(worktree);
+        cx.notify();
+    }
+
+    pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
+        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<Self>) {

zed/src/project_browser.rs 🔗

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

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<Project>,
+    list: ListState,
+    settings: watch::Receiver<Settings>,
+}
+
+pub enum Event {}
+
+impl ProjectPanel {
+    pub fn new(
+        project: ModelHandle<Project>,
+        settings: watch::Receiver<Settings>,
+        cx: &mut ViewContext<Self>,
+    ) -> 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;
+}

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

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<dyn ItemHandle> {
 
 pub struct Workspace {
     pub settings: watch::Receiver<Settings>,
-    languages: Arc<LanguageRegistry>,
     rpc: Arc<rpc::Client>,
     user_store: ModelHandle<user::UserStore>,
     fs: Arc<dyn Fs>,
@@ -361,7 +358,8 @@ pub struct Workspace {
 
 impl Workspace {
     pub fn new(app_state: &AppState, cx: &mut ViewContext<Self>) -> 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<Self>,
     ) -> Task<Result<ModelHandle<Worktree>>> {
-        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<V, F>(&mut self, cx: &mut ViewContext<Self>, add_view: F)
@@ -828,72 +813,23 @@ impl Workspace {
     }
 
     fn share_worktree(&mut self, action: &ShareWorktree, cx: &mut ViewContext<Self>) {
-        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<Self>) {
-        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<Self>) {
-        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<Self>) {
-        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<Self>) -> ViewHandle<Pane> {

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<Item = &Arc<Path>> {
@@ -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<Path>,
     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<Worktree> {
     }
 }
 
-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<Self::Item> {