Start work on expanding and collapsing directories in project panel
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
co-authored by
Nathan Sobo
parent
7eda614c4a
commit
443a7826bb
@@ -5,7 +5,7 @@ use crate::{
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{self, json},
|
||||
ElementBox, MutableAppContext,
|
||||
ElementBox,
|
||||
};
|
||||
use json::ToJson;
|
||||
use parking_lot::Mutex;
|
||||
@@ -38,7 +38,7 @@ pub struct LayoutState {
|
||||
|
||||
pub struct UniformList<F>
|
||||
where
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
|
||||
{
|
||||
state: UniformListState,
|
||||
item_count: usize,
|
||||
@@ -47,7 +47,7 @@ where
|
||||
|
||||
impl<F> UniformList<F>
|
||||
where
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
|
||||
{
|
||||
pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self {
|
||||
Self {
|
||||
@@ -102,7 +102,7 @@ where
|
||||
|
||||
impl<F> Element for UniformList<F>
|
||||
where
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut MutableAppContext),
|
||||
F: Fn(Range<usize>, &mut Vec<ElementBox>, &mut LayoutContext),
|
||||
{
|
||||
type LayoutState = LayoutState;
|
||||
type PaintState = ();
|
||||
@@ -124,7 +124,7 @@ where
|
||||
let mut scroll_max = 0.;
|
||||
|
||||
let mut items = Vec::new();
|
||||
(self.append_items)(0..1, &mut items, cx.app);
|
||||
(self.append_items)(0..1, &mut items, cx);
|
||||
if let Some(first_item) = items.first_mut() {
|
||||
let mut item_size = first_item.layout(item_constraint, cx);
|
||||
item_size.set_x(size.x());
|
||||
@@ -146,7 +146,7 @@ where
|
||||
self.item_count,
|
||||
start + (size.y() / item_height).ceil() as usize + 1,
|
||||
);
|
||||
(self.append_items)(start..end, &mut items, cx.app);
|
||||
(self.append_items)(start..end, &mut items, cx);
|
||||
for item in &mut items {
|
||||
item.layout(item_constraint, cx);
|
||||
}
|
||||
|
||||
+11
-1
@@ -7,7 +7,7 @@ use crate::{
|
||||
platform::Event,
|
||||
text_layout::TextLayoutCache,
|
||||
Action, AnyAction, AssetCache, ElementBox, Entity, FontSystem, ModelHandle, ReadModel,
|
||||
ReadView, Scene, View, ViewHandle,
|
||||
ReadView, Scene, UpdateView, View, ViewHandle,
|
||||
};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use serde_json::json;
|
||||
@@ -264,6 +264,16 @@ impl<'a> ReadView for LayoutContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> UpdateView for LayoutContext<'a> {
|
||||
fn update_view<T, F, S>(&mut self, handle: &ViewHandle<T>, update: F) -> S
|
||||
where
|
||||
T: View,
|
||||
F: FnOnce(&mut T, &mut crate::ViewContext<T>) -> S,
|
||||
{
|
||||
self.app.update_view(handle, update)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ReadModel for LayoutContext<'a> {
|
||||
fn read_model<T: Entity>(&self, handle: &ModelHandle<T>) -> &T {
|
||||
self.app.read_model(handle)
|
||||
|
||||
@@ -126,7 +126,7 @@ impl View for Select {
|
||||
UniformList::new(
|
||||
self.list_state.clone(),
|
||||
self.item_count,
|
||||
move |mut range, items, mut cx| {
|
||||
move |mut range, items, cx| {
|
||||
let handle = handle.upgrade(cx).unwrap();
|
||||
let this = handle.read(cx);
|
||||
let selected_item_ix = this.selected_item_ix;
|
||||
@@ -134,9 +134,9 @@ impl View for Select {
|
||||
items.extend(range.map(|ix| {
|
||||
MouseEventHandler::new::<Item, _, _, _>(
|
||||
(handle.id(), ix),
|
||||
&mut cx,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
(handle.read(*cx).render_item)(
|
||||
(handle.read(cx).render_item)(
|
||||
ix,
|
||||
if ix == selected_item_ix {
|
||||
ItemType::Selected
|
||||
|
||||
+40
-22
@@ -278,29 +278,47 @@ pub async fn match_paths(
|
||||
|
||||
let start = max(tree_start, segment_start) - tree_start;
|
||||
let end = min(tree_end, segment_end) - tree_start;
|
||||
let entries = if include_ignored {
|
||||
snapshot.files(start).take(end - start)
|
||||
} else {
|
||||
snapshot.visible_files(start).take(end - start)
|
||||
};
|
||||
let paths = entries.map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
if include_ignored {
|
||||
let paths = snapshot.files(start).take(end - start).map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
|
||||
matcher.match_paths(
|
||||
snapshot.id(),
|
||||
path_prefix,
|
||||
paths,
|
||||
results,
|
||||
&cancel_flag,
|
||||
);
|
||||
});
|
||||
matcher.match_paths(
|
||||
snapshot.id(),
|
||||
path_prefix,
|
||||
paths,
|
||||
results,
|
||||
&cancel_flag,
|
||||
);
|
||||
} else {
|
||||
let paths =
|
||||
snapshot
|
||||
.visible_files(start)
|
||||
.take(end - start)
|
||||
.map(|entry| {
|
||||
if let EntryKind::File(char_bag) = entry.kind {
|
||||
PathMatchCandidate {
|
||||
path: &entry.path,
|
||||
char_bag,
|
||||
}
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
});
|
||||
matcher.match_paths(
|
||||
snapshot.id(),
|
||||
path_prefix,
|
||||
paths,
|
||||
results,
|
||||
&cancel_flag,
|
||||
);
|
||||
};
|
||||
}
|
||||
if tree_end >= segment_end {
|
||||
break;
|
||||
|
||||
+2
-1
@@ -13,7 +13,7 @@ use zed::{
|
||||
channel::ChannelList,
|
||||
chat_panel, editor, file_finder,
|
||||
fs::RealFs,
|
||||
http, language, menus, rpc, settings, theme_selector,
|
||||
http, language, menus, project_panel, rpc, settings, theme_selector,
|
||||
user::UserStore,
|
||||
workspace::{self, OpenNew, OpenParams, OpenPaths},
|
||||
AppState,
|
||||
@@ -55,6 +55,7 @@ fn main() {
|
||||
editor::init(cx);
|
||||
file_finder::init(cx);
|
||||
chat_panel::init(cx);
|
||||
project_panel::init(cx);
|
||||
theme_selector::init(&app_state, cx);
|
||||
|
||||
cx.set_menus(menus::menus(&app_state.clone()));
|
||||
|
||||
+357
-68
@@ -1,19 +1,41 @@
|
||||
use crate::{
|
||||
project::Project,
|
||||
theme::Theme,
|
||||
worktree::{self, Worktree},
|
||||
Settings,
|
||||
};
|
||||
use crate::{project::Project, theme, Settings};
|
||||
use gpui::{
|
||||
elements::{Empty, Label, List, ListState, Orientation},
|
||||
AppContext, Element, ElementBox, Entity, ModelHandle, View, ViewContext,
|
||||
action,
|
||||
elements::{Label, MouseEventHandler, UniformList, UniformListState},
|
||||
Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, ViewContext,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use postage::watch;
|
||||
use std::ops::Range;
|
||||
|
||||
pub struct ProjectPanel {
|
||||
project: ModelHandle<Project>,
|
||||
list: ListState,
|
||||
list: UniformListState,
|
||||
visible_entries: Vec<Vec<usize>>,
|
||||
expanded_dir_ids: Vec<Vec<usize>>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
handle: WeakViewHandle<Self>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct EntryDetails {
|
||||
filename: String,
|
||||
depth: usize,
|
||||
is_dir: bool,
|
||||
is_expanded: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ProjectEntry {
|
||||
worktree_ix: usize,
|
||||
entry_id: usize,
|
||||
}
|
||||
|
||||
action!(ToggleExpanded, ProjectEntry);
|
||||
action!(Open, ProjectEntry);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ProjectPanel::toggle_expanded);
|
||||
}
|
||||
|
||||
pub enum Event {}
|
||||
@@ -24,77 +46,139 @@ impl ProjectPanel {
|
||||
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.observe(&project, |this, _, cx| {
|
||||
this.update_visible_entries(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)
|
||||
}
|
||||
},
|
||||
),
|
||||
let mut this = Self {
|
||||
project,
|
||||
settings,
|
||||
}
|
||||
list: Default::default(),
|
||||
visible_entries: Default::default(),
|
||||
expanded_dir_ids: Default::default(),
|
||||
handle: cx.handle().downgrade(),
|
||||
};
|
||||
this.update_visible_entries(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn entry_count(project: &Project, cx: &AppContext) -> usize {
|
||||
project
|
||||
.worktrees()
|
||||
.iter()
|
||||
.map(|worktree| worktree.read(cx).visible_entry_count())
|
||||
.sum()
|
||||
fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext<Self>) {
|
||||
let ProjectEntry {
|
||||
worktree_ix,
|
||||
entry_id,
|
||||
} = action.0;
|
||||
let expanded_dir_ids = &mut self.expanded_dir_ids[worktree_ix];
|
||||
match expanded_dir_ids.binary_search(&entry_id) {
|
||||
Ok(ix) => {
|
||||
expanded_dir_ids.remove(ix);
|
||||
}
|
||||
Err(ix) => {
|
||||
expanded_dir_ids.insert(ix, entry_id);
|
||||
}
|
||||
}
|
||||
self.update_visible_entries(cx);
|
||||
}
|
||||
|
||||
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;
|
||||
fn update_visible_entries(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let worktrees = self.project.read(cx).worktrees();
|
||||
self.visible_entries.clear();
|
||||
for (worktree_ix, worktree) in worktrees.iter().enumerate() {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
|
||||
if self.expanded_dir_ids.len() <= worktree_ix {
|
||||
self.expanded_dir_ids
|
||||
.push(vec![snapshot.root_entry().unwrap().id])
|
||||
}
|
||||
|
||||
let expanded_dir_ids = &self.expanded_dir_ids[worktree_ix];
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = snapshot.visible_entries(0);
|
||||
while let Some(item) = entry_iter.item() {
|
||||
visible_worktree_entries.push(entry_iter.ix());
|
||||
if expanded_dir_ids.binary_search(&item.id).is_err() {
|
||||
if entry_iter.advance_sibling() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
entry_iter.advance();
|
||||
}
|
||||
self.visible_entries.push(visible_worktree_entries);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_visible_entries<C: ReadModel, T>(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
items: &mut Vec<T>,
|
||||
cx: &mut C,
|
||||
mut render_item: impl FnMut(ProjectEntry, EntryDetails, &mut C) -> T,
|
||||
) {
|
||||
let worktrees = self.project.read(cx).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 {
|
||||
break;
|
||||
}
|
||||
if total_ix + visible_worktree_entries.len() <= range.start {
|
||||
total_ix += visible_worktree_entries.len();
|
||||
continue;
|
||||
}
|
||||
|
||||
let expanded_entry_ids = &self.expanded_dir_ids[worktree_ix];
|
||||
let snapshot = worktrees[worktree_ix].read(cx).snapshot();
|
||||
let mut cursor = snapshot.visible_entries(0);
|
||||
for ix in visible_worktree_entries[(range.start - total_ix)..]
|
||||
.iter()
|
||||
.copied()
|
||||
{
|
||||
cursor.advance_to_ix(ix);
|
||||
if let Some(entry) = cursor.item() {
|
||||
let details = EntryDetails {
|
||||
filename: entry.path.file_name().map_or_else(
|
||||
|| snapshot.root_name().to_string(),
|
||||
|name| name.to_string_lossy().to_string(),
|
||||
),
|
||||
depth: entry.path.components().count(),
|
||||
is_dir: entry.is_dir(),
|
||||
is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
|
||||
};
|
||||
let entry = ProjectEntry {
|
||||
worktree_ix,
|
||||
entry_id: entry.id,
|
||||
};
|
||||
items.push(render_item(entry, details, cx));
|
||||
}
|
||||
total_ix += 1;
|
||||
}
|
||||
}
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
worktree: &Worktree,
|
||||
entry: &worktree::Entry,
|
||||
theme: &Theme,
|
||||
_: &AppContext,
|
||||
entry: ProjectEntry,
|
||||
details: EntryDetails,
|
||||
theme: &theme::ProjectPanel,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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(),
|
||||
let is_dir = details.is_dir;
|
||||
MouseEventHandler::new::<Self, _, _, _>(
|
||||
(entry.worktree_ix, entry.entry_id),
|
||||
cx,
|
||||
|state, cx| {
|
||||
Label::new(details.filename, theme.entry.clone())
|
||||
.contained()
|
||||
.with_margin_left(details.depth as f32 * 20.)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.contained()
|
||||
.with_margin_left(depth * 20.)
|
||||
.on_click(move |cx| {
|
||||
if is_dir {
|
||||
cx.dispatch_action(ToggleExpanded(entry))
|
||||
} else {
|
||||
cx.dispatch_action(Open(entry))
|
||||
}
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
@@ -105,14 +189,219 @@ impl View for 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()
|
||||
let settings = self.settings.clone();
|
||||
let handle = self.handle.clone();
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries.len(),
|
||||
move |range, items, cx| {
|
||||
let theme = &settings.borrow().theme.project_panel;
|
||||
let this = handle.upgrade(cx).unwrap();
|
||||
this.update(cx.app, |this, cx| {
|
||||
this.append_visible_entries(range, items, cx, |entry, details, cx| {
|
||||
Self::render_entry(entry, details, theme, cx)
|
||||
});
|
||||
})
|
||||
},
|
||||
)
|
||||
.contained()
|
||||
.with_style(self.settings.borrow().theme.project_panel.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ProjectPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::test_app_state;
|
||||
use gpui::{TestAppContext, ViewHandle};
|
||||
use serde_json::json;
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_visible_list(mut cx: gpui::TestAppContext) {
|
||||
let app_state = cx.update(test_app_state);
|
||||
let settings = app_state.settings.clone();
|
||||
let fs = app_state.fs.as_fake();
|
||||
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
json!({
|
||||
".dockerignore": "",
|
||||
".git": {
|
||||
"HEAD": "",
|
||||
},
|
||||
"a": {
|
||||
"0": { "q": "", "r": "", "s": "" },
|
||||
"1": { "t": "", "u": "" },
|
||||
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||
},
|
||||
"b": {
|
||||
"3": { "Q": "" },
|
||||
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||
},
|
||||
"c": {
|
||||
"5": {},
|
||||
"6": { "V": "", "W": "" },
|
||||
"7": { "X": "" },
|
||||
"8": { "Y": {}, "Z": "" }
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = cx.add_model(|_| Project::new(&app_state));
|
||||
let worktree = project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.add_local_worktree("/root1".as_ref(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree
|
||||
.read_with(&cx, |t, _| t.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
let (_, panel) = cx.add_window(|cx| ProjectPanel::new(project, settings, cx));
|
||||
assert_eq!(
|
||||
visible_entry_details(&panel, 0..50, &mut cx),
|
||||
&[
|
||||
EntryDetails {
|
||||
filename: "root1".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: ".dockerignore".to_string(),
|
||||
depth: 1,
|
||||
is_dir: false,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "a".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "b".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "c".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
toggle_expand_dir(&panel, "root1/b", &mut cx);
|
||||
assert_eq!(
|
||||
visible_entry_details(&panel, 0..50, &mut cx),
|
||||
&[
|
||||
EntryDetails {
|
||||
filename: "root1".to_string(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: ".dockerignore".to_string(),
|
||||
depth: 1,
|
||||
is_dir: false,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "a".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "b".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: true,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "3".to_string(),
|
||||
depth: 2,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "4".to_string(),
|
||||
depth: 2,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
EntryDetails {
|
||||
filename: "c".to_string(),
|
||||
depth: 1,
|
||||
is_dir: true,
|
||||
is_expanded: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
fn toggle_expand_dir(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
path: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let path = path.as_ref();
|
||||
panel.update(cx, |panel, cx| {
|
||||
for (worktree_ix, worktree) in panel.project.read(cx).worktrees().iter().enumerate()
|
||||
{
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
panel.toggle_expanded(
|
||||
&ToggleExpanded(ProjectEntry {
|
||||
worktree_ix,
|
||||
entry_id,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("no worktree for path {:?}", path);
|
||||
});
|
||||
}
|
||||
|
||||
fn visible_entry_details(
|
||||
panel: &ViewHandle<ProjectPanel>,
|
||||
range: Range<usize>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Vec<EntryDetails> {
|
||||
let mut result = Vec::new();
|
||||
let mut project_entries = HashSet::new();
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.append_visible_entries(
|
||||
range,
|
||||
&mut result,
|
||||
cx,
|
||||
|project_entry, details, _| {
|
||||
assert!(
|
||||
project_entries.insert(project_entry),
|
||||
"duplicate project entry {:?} {:?}",
|
||||
project_entry,
|
||||
details
|
||||
);
|
||||
details
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+140
-59
@@ -1482,16 +1482,16 @@ impl Snapshot {
|
||||
self.entries_by_path.summary().visible_file_count
|
||||
}
|
||||
|
||||
pub fn files(&self, start: usize) -> EntryIter {
|
||||
EntryIter::files(self, start)
|
||||
pub fn files(&self, start: usize) -> EntryIter<FileCount> {
|
||||
EntryIter::new(self, start)
|
||||
}
|
||||
|
||||
pub fn visible_entries(&self, start: usize) -> EntryIter {
|
||||
EntryIter::visible(self, start)
|
||||
pub fn visible_entries(&self, start: usize) -> EntryIter<VisibleCountAndPath> {
|
||||
EntryIter::new(self, start)
|
||||
}
|
||||
|
||||
pub fn visible_files(&self, start: usize) -> EntryIter {
|
||||
EntryIter::visible_files(self, start)
|
||||
pub fn visible_files(&self, start: usize) -> EntryIter<VisibleFileCount> {
|
||||
EntryIter::new(self, start)
|
||||
}
|
||||
|
||||
pub fn paths(&self) -> impl Iterator<Item = &Arc<Path>> {
|
||||
@@ -1514,7 +1514,7 @@ impl Snapshot {
|
||||
&self.root_name
|
||||
}
|
||||
|
||||
fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
|
||||
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
|
||||
let mut cursor = self.entries_by_path.cursor::<_, ()>();
|
||||
if cursor.seek(&PathSearch::Exact(path.as_ref()), Bias::Left, &()) {
|
||||
cursor.item()
|
||||
@@ -2065,33 +2065,111 @@ impl<'a: 'b, 'b> sum_tree::Dimension<'a, EntrySummary> for PathSearch<'b> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct FileCount(usize);
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct VisibleFileCount(usize);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct VisibleCountAndPath<'a> {
|
||||
count: Option<usize>,
|
||||
path: PathSearch<'a>,
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, EntrySummary> for FileCount {
|
||||
fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
|
||||
self.0 += summary.file_count;
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleFileCount {
|
||||
fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
|
||||
self.0 += summary.visible_file_count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, EntrySummary> for VisibleCountAndPath<'a> {
|
||||
fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) {
|
||||
if let Some(count) = self.count.as_mut() {
|
||||
*count += summary.visible_count;
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
self.path = PathSearch::Exact(summary.max_path.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Ord for VisibleCountAndPath<'a> {
|
||||
fn cmp(&self, other: &Self) -> cmp::Ordering {
|
||||
if let Some(count) = self.count {
|
||||
count.cmp(&other.count.unwrap())
|
||||
} else {
|
||||
self.path.cmp(&other.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PartialOrd for VisibleCountAndPath<'a> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for FileCount {
|
||||
fn from(count: usize) -> Self {
|
||||
Self(count)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for VisibleFileCount {
|
||||
fn from(count: usize) -> Self {
|
||||
Self(count)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<usize> for VisibleCountAndPath<'a> {
|
||||
fn from(count: usize) -> Self {
|
||||
Self {
|
||||
count: Some(count),
|
||||
path: PathSearch::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FileCount {
|
||||
type Target = usize;
|
||||
|
||||
fn deref(&self) -> &usize {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for VisibleFileCount {
|
||||
type Target = usize;
|
||||
|
||||
fn deref(&self) -> &usize {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VisibleCountAndPath<'a> {
|
||||
type Target = usize;
|
||||
|
||||
fn deref(&self) -> &usize {
|
||||
self.count.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Default for VisibleCountAndPath<'a> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
count: Some(0),
|
||||
path: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BackgroundScanner {
|
||||
fs: Arc<dyn Fs>,
|
||||
snapshot: Arc<Mutex<Snapshot>>,
|
||||
@@ -2584,63 +2662,66 @@ impl WorktreeHandle for ModelHandle<Worktree> {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EntryIter<'a> {
|
||||
Files(Cursor<'a, Entry, FileCount, ()>),
|
||||
Visible(Cursor<'a, Entry, VisibleCount, ()>),
|
||||
VisibleFiles(Cursor<'a, Entry, VisibleFileCount, ()>),
|
||||
pub struct EntryIter<'a, Dim> {
|
||||
cursor: Cursor<'a, Entry, Dim, ()>,
|
||||
}
|
||||
|
||||
impl<'a> EntryIter<'a> {
|
||||
fn files(snapshot: &'a Snapshot, start: usize) -> Self {
|
||||
impl<'a, Dim> EntryIter<'a, Dim>
|
||||
where
|
||||
Dim: sum_tree::SeekDimension<'a, EntrySummary> + From<usize> + Deref<Target = usize>,
|
||||
{
|
||||
fn new(snapshot: &'a Snapshot, start: usize) -> Self {
|
||||
let mut cursor = snapshot.entries_by_path.cursor();
|
||||
cursor.seek(&FileCount(start), Bias::Right, &());
|
||||
Self::Files(cursor)
|
||||
cursor.seek(&Dim::from(start), Bias::Right, &());
|
||||
Self { cursor }
|
||||
}
|
||||
|
||||
fn visible(snapshot: &'a Snapshot, start: usize) -> Self {
|
||||
let mut cursor = snapshot.entries_by_path.cursor();
|
||||
cursor.seek(&VisibleCount(start), Bias::Right, &());
|
||||
Self::Visible(cursor)
|
||||
pub fn ix(&self) -> usize {
|
||||
*self.cursor.seek_start().deref()
|
||||
}
|
||||
|
||||
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)
|
||||
pub fn advance_to_ix(&mut self, ix: usize) {
|
||||
self.cursor.seek_forward(&Dim::from(ix), Bias::Right, &());
|
||||
}
|
||||
|
||||
fn next_internal(&mut self) {
|
||||
match self {
|
||||
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, &());
|
||||
}
|
||||
}
|
||||
pub fn advance(&mut self) {
|
||||
self.advance_to_ix(self.ix() + 1);
|
||||
}
|
||||
|
||||
fn item(&self) -> Option<&'a Entry> {
|
||||
match self {
|
||||
Self::Files(cursor) => cursor.item(),
|
||||
Self::Visible(cursor) => cursor.item(),
|
||||
Self::VisibleFiles(cursor) => cursor.item(),
|
||||
}
|
||||
pub fn item(&self) -> Option<&'a Entry> {
|
||||
self.cursor.item()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for EntryIter<'a> {
|
||||
impl<'a> EntryIter<'a, VisibleCountAndPath<'a>> {
|
||||
pub fn advance_sibling(&mut self) -> bool {
|
||||
let start_count = self.cursor.seek_start().count.unwrap();
|
||||
while let Some(item) = self.cursor.item() {
|
||||
self.cursor.seek_forward(
|
||||
&VisibleCountAndPath {
|
||||
count: None,
|
||||
path: PathSearch::Successor(item.path.as_ref()),
|
||||
},
|
||||
Bias::Right,
|
||||
&(),
|
||||
);
|
||||
if self.cursor.seek_start().count.unwrap() > start_count {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Dim> Iterator for EntryIter<'a, Dim>
|
||||
where
|
||||
Dim: sum_tree::SeekDimension<'a, EntrySummary> + From<usize> + Deref<Target = usize>,
|
||||
{
|
||||
type Item = &'a Entry;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(entry) = self.item() {
|
||||
self.next_internal();
|
||||
self.advance();
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user