Maintain active entry Project and render it in ProjectPanel

This commit is contained in:
Antonio Scandurra 2021-09-29 16:50:33 +02:00
parent 67c40eb4be
commit 1519e1d45f
5 changed files with 129 additions and 10 deletions

View file

@ -172,6 +172,10 @@ padding = { top = 3, bottom = 3 }
extends = "$project_panel.entry" extends = "$project_panel.entry"
background = "$state.hover" background = "$state.hover"
[project_panel.active_entry]
extends = "$project_panel.entry"
background = "#ff0000"
[selector] [selector]
background = "$surface.0" background = "$surface.0"
padding = 8 padding = 8

View file

@ -12,17 +12,21 @@ use std::{path::Path, sync::Arc};
pub struct Project { pub struct Project {
worktrees: Vec<ModelHandle<Worktree>>, worktrees: Vec<ModelHandle<Worktree>>,
active_entry: Option<(usize, usize)>,
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
rpc: Arc<Client>, rpc: Arc<Client>,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
} }
pub enum Event {} pub enum Event {
ActiveEntryChanged(Option<(usize, usize)>),
}
impl Project { impl Project {
pub fn new(app_state: &AppState) -> Self { pub fn new(app_state: &AppState) -> Self {
Self { Self {
worktrees: Default::default(), worktrees: Default::default(),
active_entry: None,
languages: app_state.languages.clone(), languages: app_state.languages.clone(),
rpc: app_state.rpc.clone(), rpc: app_state.rpc.clone(),
fs: app_state.fs.clone(), fs: app_state.fs.clone(),
@ -89,6 +93,26 @@ impl Project {
cx.notify(); cx.notify();
} }
pub fn set_active_entry(
&mut self,
entry: Option<(usize, Arc<Path>)>,
cx: &mut ModelContext<Self>,
) {
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<Self>) { pub fn share_worktree(&self, remote_id: u64, cx: &mut ModelContext<Self>) {
let rpc = self.rpc.clone(); let rpc = self.rpc.clone();
cx.spawn(|this, mut cx| { cx.spawn(|this, mut cx| {

View file

@ -1,4 +1,7 @@
use crate::{project::Project, theme, Settings}; use crate::{
project::{self, Project},
theme, Settings,
};
use gpui::{ use gpui::{
action, action,
elements::{Label, MouseEventHandler, UniformList, UniformListState}, elements::{Label, MouseEventHandler, UniformList, UniformListState},
@ -24,6 +27,7 @@ struct EntryDetails {
depth: usize, depth: usize,
is_dir: bool, is_dir: bool,
is_expanded: bool, is_expanded: bool,
is_active: bool,
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
@ -48,10 +52,18 @@ impl ProjectPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Self { ) -> Self {
cx.observe(&project, |this, _, cx| { cx.observe(&project, |this, _, cx| {
this.update_visible_entries(cx); this.update_visible_entries(false, cx);
cx.notify(); cx.notify();
}) })
.detach(); .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 { let mut this = Self {
project, project,
@ -61,7 +73,7 @@ impl ProjectPanel {
expanded_dir_ids: Default::default(), expanded_dir_ids: Default::default(),
handle: cx.handle().downgrade(), handle: cx.handle().downgrade(),
}; };
this.update_visible_entries(cx); this.update_visible_entries(false, cx);
this this
} }
@ -79,12 +91,15 @@ impl ProjectPanel {
expanded_dir_ids.insert(ix, entry_id); 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<Self>) { fn update_visible_entries(&mut self, scroll_to_active_entry: bool, cx: &mut ViewContext<Self>) {
let worktrees = self.project.read(cx).worktrees(); let project = self.project.read(cx);
let worktrees = project.worktrees();
self.visible_entries.clear(); self.visible_entries.clear();
let mut entry_ix = 0;
for (worktree_ix, worktree) in worktrees.iter().enumerate() { for (worktree_ix, worktree) in worktrees.iter().enumerate() {
let snapshot = worktree.read(cx).snapshot(); let snapshot = worktree.read(cx).snapshot();
@ -98,6 +113,13 @@ impl ProjectPanel {
let mut entry_iter = snapshot.entries(false); let mut entry_iter = snapshot.entries(false);
while let Some(item) = entry_iter.entry() { while let Some(item) = entry_iter.entry() {
visible_worktree_entries.push(entry_iter.offset()); 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 expanded_dir_ids.binary_search(&item.id).is_err() {
if entry_iter.advance_to_sibling() { if entry_iter.advance_to_sibling() {
continue; continue;
@ -109,6 +131,40 @@ impl ProjectPanel {
} }
} }
fn expand_active_entry(
&mut self,
worktree_id: usize,
entry_id: usize,
cx: &mut ViewContext<Self>,
) {
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<C: ReadModel, T>( fn append_visible_entries<C: ReadModel, T>(
&self, &self,
range: Range<usize>, range: Range<usize>,
@ -116,7 +172,9 @@ impl ProjectPanel {
cx: &mut C, cx: &mut C,
mut render_item: impl FnMut(ProjectEntry, EntryDetails, &mut C) -> T, 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; let mut total_ix = 0;
for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() { for (worktree_ix, visible_worktree_entries) in self.visible_entries.iter().enumerate() {
if total_ix >= range.end { if total_ix >= range.end {
@ -128,7 +186,8 @@ impl ProjectPanel {
} }
let expanded_entry_ids = &self.expanded_dir_ids[worktree_ix]; 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); let mut cursor = snapshot.entries(false);
for ix in visible_worktree_entries[(range.start - total_ix)..] for ix in visible_worktree_entries[(range.start - total_ix)..]
.iter() .iter()
@ -144,6 +203,7 @@ impl ProjectPanel {
depth: entry.path.components().count(), depth: entry.path.components().count(),
is_dir: entry.is_dir(), is_dir: entry.is_dir(),
is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
is_active: active_entry == Some((worktree.id(), entry.id)),
}; };
let entry = ProjectEntry { let entry = ProjectEntry {
worktree_ix, worktree_ix,
@ -167,7 +227,9 @@ impl ProjectPanel {
(entry.worktree_ix, entry.entry_id), (entry.worktree_ix, entry.entry_id),
cx, cx,
|state, _| { |state, _| {
let style = if state.hovered { let style = if details.is_active {
&theme.active_entry
} else if state.hovered {
&theme.hovered_entry &theme.hovered_entry
} else { } else {
&theme.entry &theme.entry
@ -285,30 +347,35 @@ mod tests {
depth: 0, depth: 0,
is_dir: true, is_dir: true,
is_expanded: true, is_expanded: true,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: ".dockerignore".to_string(), filename: ".dockerignore".to_string(),
depth: 1, depth: 1,
is_dir: false, is_dir: false,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "a".to_string(), filename: "a".to_string(),
depth: 1, depth: 1,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "b".to_string(), filename: "b".to_string(),
depth: 1, depth: 1,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "c".to_string(), filename: "c".to_string(),
depth: 1, depth: 1,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
] ]
); );
@ -322,42 +389,49 @@ mod tests {
depth: 0, depth: 0,
is_dir: true, is_dir: true,
is_expanded: true, is_expanded: true,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: ".dockerignore".to_string(), filename: ".dockerignore".to_string(),
depth: 1, depth: 1,
is_dir: false, is_dir: false,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "a".to_string(), filename: "a".to_string(),
depth: 1, depth: 1,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "b".to_string(), filename: "b".to_string(),
depth: 1, depth: 1,
is_dir: true, is_dir: true,
is_expanded: true, is_expanded: true,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "3".to_string(), filename: "3".to_string(),
depth: 2, depth: 2,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "4".to_string(), filename: "4".to_string(),
depth: 2, depth: 2,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
EntryDetails { EntryDetails {
filename: "c".to_string(), filename: "c".to_string(),
depth: 1, depth: 1,
is_dir: true, is_dir: true,
is_expanded: false, is_expanded: false,
is_active: false,
}, },
] ]
); );

View file

@ -114,6 +114,7 @@ pub struct ProjectPanel {
pub entry_base_padding: f32, pub entry_base_padding: f32,
pub entry: ContainedText, pub entry: ContainedText,
pub hovered_entry: ContainedText, pub hovered_entry: ContainedText,
pub active_entry: ContainedText,
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View file

@ -371,6 +371,12 @@ impl Workspace {
let pane = cx.add_view(|_| Pane::new(app_state.settings.clone())); let pane = cx.add_view(|_| Pane::new(app_state.settings.clone()));
let pane_id = pane.id(); 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| { cx.subscribe(&pane, move |me, _, event, cx| {
me.handle_pane_event(pane_id, event, cx) me.handle_pane_event(pane_id, event, cx)
}) })
@ -725,6 +731,10 @@ impl Workspace {
self.active_pane().read(cx).active_item() self.active_pane().read(cx).active_item()
} }
fn active_entry(&self, cx: &ViewContext<Self>) -> Option<(usize, Arc<Path>)> {
self.active_item(cx).and_then(|item| item.entry_id(cx))
}
pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) { pub fn save_active_item(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
if let Some(item) = self.active_item(cx) { if let Some(item) = self.active_item(cx) {
let handle = cx.handle(); let handle = cx.handle();
@ -843,6 +853,12 @@ impl Workspace {
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> { fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
let pane = cx.add_view(|_| Pane::new(self.settings.clone())); let pane = cx.add_view(|_| Pane::new(self.settings.clone()));
let pane_id = pane.id(); 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| { cx.subscribe(&pane, move |me, _, event, cx| {
me.handle_pane_event(pane_id, event, cx) me.handle_pane_event(pane_id, event, cx)
}) })