Detailed changes
@@ -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
@@ -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"))]
@@ -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>) {
@@ -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()
- }
-}
@@ -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;
+}
@@ -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)]
@@ -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> {
@@ -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> {