diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 8060d5d139..206aa26134 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 18facab61b..9ec429a62a 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 421aa22688..589c809bf9 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 270925e93b..b4561abf1f 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 e3309e17a2..9478c40a57 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) })