feat: impl pull request file tree
This commit is contained in:
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
pull_request_query::PullRequestQueryNode,
|
pull_request_query::PullRequestQueryNode,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
query,
|
query, util,
|
||||||
};
|
};
|
||||||
|
|
||||||
type DateTime = String;
|
type DateTime = String;
|
||||||
@@ -447,7 +447,7 @@ pub(crate) struct FetchPullRequestFileTree {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl query::QueryFn for FetchPullRequestFileTree {
|
impl query::QueryFn for FetchPullRequestFileTree {
|
||||||
type Data = Vec<ChangedFile>;
|
type Data = util::file::SortedByPath<ChangedFile>;
|
||||||
type Error = api::Error;
|
type Error = api::Error;
|
||||||
type Context = api::QueryContext;
|
type Context = api::QueryContext;
|
||||||
|
|
||||||
@@ -513,9 +513,7 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
|||||||
edge.node.map(|node| ChangedFile {
|
edge.node.map(|node| ChangedFile {
|
||||||
cursor,
|
cursor,
|
||||||
change_type: match node.change_type {
|
change_type: match node.change_type {
|
||||||
| pull_request_file_tree_query::PatchStatus::ADDED => {
|
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added,
|
||||||
ChangeType::Added
|
|
||||||
}
|
|
||||||
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
||||||
ChangeType::Modified
|
ChangeType::Modified
|
||||||
}
|
}
|
||||||
@@ -541,6 +539,7 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
|
.map(|files| util::file::sort_by_path(files, |f| &f.path))
|
||||||
.unwrap_or_default())
|
.unwrap_or_default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/asset/font_icon/chevron_right.svg
Normal file
1
src/asset/font_icon/chevron_right.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-right-icon lucide-chevron-right"><path d="m9 18 6-6-6-6"/></svg>
|
||||||
|
After Width: | Height: | Size: 275 B |
1
src/asset/font_icon/folder_closed.svg
Normal file
1
src/asset/font_icon/folder_closed.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-closed-icon lucide-folder-closed"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/><path d="M2 10h20"/></svg>
|
||||||
|
After Width: | Height: | Size: 400 B |
1
src/asset/font_icon/folder_open.svg
Normal file
1
src/asset/font_icon/folder_open.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-open-icon lucide-folder-open"><path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"/></svg>
|
||||||
|
After Width: | Height: | Size: 435 B |
176
src/component/file_tree.rs
Normal file
176
src/component/file_tree.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
InteractiveElement, IntoElement, ParentElement, StatefulInteractiveElement, Styled, div, list,
|
||||||
|
prelude::FluentBuilder, px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app,
|
||||||
|
component::{
|
||||||
|
font_icon::{FontIcon, font_icon},
|
||||||
|
text::text,
|
||||||
|
},
|
||||||
|
util::{
|
||||||
|
self,
|
||||||
|
file::{FileTreeItem, FileTreeItemKind},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(gpui::IntoElement)]
|
||||||
|
pub(crate) struct FileTree {
|
||||||
|
state: FileTreeState,
|
||||||
|
item: Box<dyn Fn(usize, &gpui::Window, &mut gpui::App) -> util::file::FileTreeItem>,
|
||||||
|
on_item_click: Option<Rc<dyn Fn(&usize, &mut gpui::Window, &mut gpui::App)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(gpui::IntoElement)]
|
||||||
|
pub(crate) struct Item {
|
||||||
|
item: util::file::FileTreeItem,
|
||||||
|
is_expanded: bool,
|
||||||
|
on_click: Box<dyn Fn(&mut gpui::Window, &mut gpui::App) + 'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct FileTreeState(Rc<RefCell<FileTreeStateInner>>);
|
||||||
|
|
||||||
|
struct FileTreeStateInner {
|
||||||
|
list_state: gpui::ListState,
|
||||||
|
collapsed_dirs: HashSet<Arc<str>>,
|
||||||
|
visible_items: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn file_tree(
|
||||||
|
state: FileTreeState,
|
||||||
|
item: impl Fn(usize, &gpui::Window, &mut gpui::App) -> util::file::FileTreeItem + 'static,
|
||||||
|
) -> FileTree {
|
||||||
|
FileTree {
|
||||||
|
state,
|
||||||
|
item: Box::new(item),
|
||||||
|
on_item_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTreeState {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self(Rc::new(RefCell::new(FileTreeStateInner {
|
||||||
|
list_state: gpui::ListState::new(0, gpui::ListAlignment::Top, px(50.)),
|
||||||
|
collapsed_dirs: HashSet::new(),
|
||||||
|
visible_items: Vec::new(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn reset(&self, items: &[util::file::FileTreeItem]) {
|
||||||
|
let mut state = self.0.borrow_mut();
|
||||||
|
state.list_state.reset(items.len());
|
||||||
|
state.visible_items = (0..items.len()).collect::<Vec<_>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn toggle_directory(
|
||||||
|
&mut self,
|
||||||
|
dir_path: &Arc<str>,
|
||||||
|
items: &[util::file::FileTreeItem],
|
||||||
|
) {
|
||||||
|
let mut state = self.0.borrow_mut();
|
||||||
|
if !state.collapsed_dirs.remove(dir_path) {
|
||||||
|
state.collapsed_dirs.insert(Arc::clone(dir_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.visible_items.clear();
|
||||||
|
|
||||||
|
let mut hidden_after_level: usize = usize::MAX;
|
||||||
|
for (i, item) in items.iter().enumerate() {
|
||||||
|
if item.level > hidden_after_level {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hidden_after_level = usize::MAX;
|
||||||
|
|
||||||
|
if item.kind == FileTreeItemKind::Directory
|
||||||
|
&& state.collapsed_dirs.contains(&item.full_path)
|
||||||
|
{
|
||||||
|
hidden_after_level = item.level;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.visible_items.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.list_state.reset(state.visible_items.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileTree {
|
||||||
|
pub(crate) fn on_item_click(
|
||||||
|
mut self,
|
||||||
|
f: impl Fn(&usize, &mut gpui::Window, &mut gpui::App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_item_click = Some(Rc::new(f));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::RenderOnce for FileTree {
|
||||||
|
fn render(
|
||||||
|
self,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
_cx: &mut gpui::App,
|
||||||
|
) -> impl gpui::prelude::IntoElement {
|
||||||
|
let list_state = self.state.0.borrow().list_state.clone();
|
||||||
|
|
||||||
|
list(list_state, move |i, window, cx| {
|
||||||
|
let state = self.state.0.borrow();
|
||||||
|
let item_index = state.visible_items[i];
|
||||||
|
let on_item_click = self.on_item_click.as_ref().map(Rc::clone);
|
||||||
|
|
||||||
|
let item = (self.item)(item_index, window, cx);
|
||||||
|
let item = Item {
|
||||||
|
is_expanded: !state.collapsed_dirs.contains(&item.full_path),
|
||||||
|
item,
|
||||||
|
on_click: Box::new(move |window, cx| {
|
||||||
|
if let Some(f) = &on_item_click {
|
||||||
|
f(&i, window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
item.into_any_element()
|
||||||
|
})
|
||||||
|
.size_full()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::RenderOnce for Item {
|
||||||
|
fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||||
|
let theme = app::current_theme(cx);
|
||||||
|
div()
|
||||||
|
.id(gpui::SharedString::new(self.item.full_path))
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.gap_1p5()
|
||||||
|
.pr_2()
|
||||||
|
.py_0p5()
|
||||||
|
.pl(px(8. + (8 * self.item.level) as f32))
|
||||||
|
.rounded_sm()
|
||||||
|
.hover(|it| it.bg(theme.colors.surface_hover))
|
||||||
|
.on_click(move |_, window, cx| (self.on_click)(window, cx))
|
||||||
|
.when(
|
||||||
|
matches!(self.item.kind, FileTreeItemKind::Directory),
|
||||||
|
|it| {
|
||||||
|
it.child(
|
||||||
|
font_icon(if self.is_expanded {
|
||||||
|
FontIcon::FolderOpen
|
||||||
|
} else {
|
||||||
|
FontIcon::FolderClosed
|
||||||
|
})
|
||||||
|
.size_3(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.when(matches!(self.item.kind, FileTreeItemKind::File), |it| {
|
||||||
|
it.child(font_icon(FontIcon::FileBracesCorner).size_3())
|
||||||
|
})
|
||||||
|
.child(text(gpui::SharedString::new(self.item.name)).text_sm())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ macro_rules! define_font_icons {
|
|||||||
|
|
||||||
define_font_icons!(
|
define_font_icons!(
|
||||||
Check,
|
Check,
|
||||||
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
FolderGit,
|
FolderGit,
|
||||||
Github,
|
Github,
|
||||||
@@ -45,6 +46,8 @@ define_font_icons!(
|
|||||||
Star,
|
Star,
|
||||||
MessageCircleMore,
|
MessageCircleMore,
|
||||||
FileBracesCorner,
|
FileBracesCorner,
|
||||||
|
FolderClosed,
|
||||||
|
FolderOpen,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(gpui::IntoElement)]
|
#[derive(gpui::IntoElement)]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub(crate) mod button;
|
pub(crate) mod button;
|
||||||
pub(crate) mod code_view;
|
pub(crate) mod code_view;
|
||||||
pub(crate) mod diff_view;
|
pub(crate) mod diff_view;
|
||||||
|
pub(crate) mod file_tree;
|
||||||
pub(crate) mod font_icon;
|
pub(crate) mod font_icon;
|
||||||
pub(crate) mod markdown;
|
pub(crate) mod markdown;
|
||||||
pub(crate) mod segmented_control;
|
pub(crate) mod segmented_control;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod issue_list;
|
mod issue_list;
|
||||||
mod pull_request_diff_view;
|
mod pull_request_diff_view;
|
||||||
|
mod pull_request_file_tree;
|
||||||
mod pull_request_view;
|
mod pull_request_view;
|
||||||
mod screen;
|
mod screen;
|
||||||
mod sidebar;
|
mod sidebar;
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use crate::{
|
|||||||
text::text,
|
text::text,
|
||||||
},
|
},
|
||||||
query::{self, QueryStatus, read_query, use_query, watch_query},
|
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||||
util,
|
screen::dashboard::pull_request_file_tree::{self, PullRequestFileTree},
|
||||||
|
util::{self},
|
||||||
};
|
};
|
||||||
use gpui::{AppContext, ParentElement, Styled, div};
|
use gpui::{AppContext, ParentElement, Styled, div};
|
||||||
|
|
||||||
@@ -20,6 +21,8 @@ pub(crate) struct PullRequestDiffView {
|
|||||||
|
|
||||||
diff_view_state: DiffViewState,
|
diff_view_state: DiffViewState,
|
||||||
diff_view_content: Option<DiffViewContent>,
|
diff_view_content: Option<DiffViewContent>,
|
||||||
|
|
||||||
|
file_tree: gpui::Entity<PullRequestFileTree>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
@@ -31,7 +34,7 @@ pub(crate) fn new(
|
|||||||
pr_query: use_query(api::issues::FetchPullRequest { id: pr_id.clone() }, cx),
|
pr_query: use_query(api::issues::FetchPullRequest { id: pr_id.clone() }, cx),
|
||||||
file_tree_query: use_query(
|
file_tree_query: use_query(
|
||||||
api::issues::FetchPullRequestFileTree {
|
api::issues::FetchPullRequestFileTree {
|
||||||
id: pr_id,
|
id: pr_id.clone(),
|
||||||
first: 100,
|
first: 100,
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
@@ -39,6 +42,8 @@ pub(crate) fn new(
|
|||||||
content_diff_query: None,
|
content_diff_query: None,
|
||||||
diff_view_state: DiffViewState::new(),
|
diff_view_state: DiffViewState::new(),
|
||||||
diff_view_content: None,
|
diff_view_content: None,
|
||||||
|
|
||||||
|
file_tree: cx.new(|cx| pull_request_file_tree::new(pr_id, cx)),
|
||||||
};
|
};
|
||||||
view.on_create(cx);
|
view.on_create(cx);
|
||||||
view
|
view
|
||||||
@@ -193,6 +198,18 @@ impl gpui::Render for PullRequestDiffView {
|
|||||||
| QueryStatus::Loaded(_) => match &self.diff_view_content {
|
| QueryStatus::Loaded(_) => match &self.diff_view_content {
|
||||||
| Some(content) => div()
|
| Some(content) => div()
|
||||||
.size_full()
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.w_80()
|
||||||
|
.h_full()
|
||||||
|
.border_r_1()
|
||||||
|
.border_color(theme.colors.border_muted)
|
||||||
|
.p_1()
|
||||||
|
.child(self.file_tree.clone()),
|
||||||
|
)
|
||||||
.child(diff_view(self.diff_view_state.clone(), content.clone())),
|
.child(diff_view(self.diff_view_state.clone(), content.clone())),
|
||||||
| None => div(),
|
| None => div(),
|
||||||
},
|
},
|
||||||
|
|||||||
81
src/screen/dashboard/pull_request_file_tree.rs
Normal file
81
src/screen/dashboard/pull_request_file_tree.rs
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
use crate::{
|
||||||
|
api,
|
||||||
|
component::file_tree::{FileTree, FileTreeState, file_tree},
|
||||||
|
query::{self, QueryStatus, read_query, use_query, watch_query},
|
||||||
|
util::{
|
||||||
|
self,
|
||||||
|
file::{FileTreeItem, FileTreeItemKind},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct PullRequestFileTree {
|
||||||
|
file_tree_query: query::Entity<api::issues::FetchPullRequestFileTree>,
|
||||||
|
file_tree_state: FileTreeState,
|
||||||
|
file_tree_items: Vec<FileTreeItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new(
|
||||||
|
pr_id: api::issues::Id,
|
||||||
|
cx: &mut gpui::Context<PullRequestFileTree>,
|
||||||
|
) -> PullRequestFileTree {
|
||||||
|
let mut v = PullRequestFileTree {
|
||||||
|
file_tree_query: use_query(
|
||||||
|
api::issues::FetchPullRequestFileTree {
|
||||||
|
id: pr_id,
|
||||||
|
first: 100,
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
file_tree_state: FileTreeState::new(),
|
||||||
|
file_tree_items: Vec::new(),
|
||||||
|
};
|
||||||
|
v.on_create(cx);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PullRequestFileTree {
|
||||||
|
fn on_create(&mut self, cx: &mut gpui::Context<PullRequestFileTree>) {
|
||||||
|
watch_query(
|
||||||
|
&self.file_tree_query,
|
||||||
|
|this, query, cx| {
|
||||||
|
if let QueryStatus::Loaded(changed_files) = read_query(query, cx) {
|
||||||
|
let tree = util::file::build_file_tree(changed_files, |f| &f.path);
|
||||||
|
this.file_tree_state.reset(&tree);
|
||||||
|
this.file_tree_items = tree;
|
||||||
|
cx.notify();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_file_tree_item_click(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
cx: &mut gpui::Context<PullRequestFileTree>,
|
||||||
|
) {
|
||||||
|
let item = &self.file_tree_items[i];
|
||||||
|
if item.kind == FileTreeItemKind::Directory {
|
||||||
|
self.file_tree_state
|
||||||
|
.toggle_directory(&item.full_path, &self.file_tree_items);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl gpui::Render for PullRequestFileTree {
|
||||||
|
fn render(
|
||||||
|
&mut self,
|
||||||
|
_window: &mut gpui::Window,
|
||||||
|
cx: &mut gpui::prelude::Context<Self>,
|
||||||
|
) -> impl gpui::prelude::IntoElement {
|
||||||
|
let weak = cx.entity();
|
||||||
|
file_tree(self.file_tree_state.clone(), move |i, _, cx| {
|
||||||
|
weak.read(cx).file_tree_items[i].clone()
|
||||||
|
})
|
||||||
|
.on_item_click(cx.listener(|this, i, _, cx| {
|
||||||
|
this.handle_file_tree_item_click(*i, cx);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,12 +17,20 @@ pub(crate) enum FileType {
|
|||||||
|
|
||||||
pub(crate) struct SortedByPath<T>(Vec<T>);
|
pub(crate) struct SortedByPath<T>(Vec<T>);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct FileTreeItem {
|
pub(crate) struct FileTreeItem {
|
||||||
|
pub(crate) kind: FileTreeItemKind,
|
||||||
pub(crate) full_path: Arc<str>,
|
pub(crate) full_path: Arc<str>,
|
||||||
pub(crate) name: Arc<str>,
|
pub(crate) name: Arc<str>,
|
||||||
pub(crate) level: usize,
|
pub(crate) level: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum FileTreeItemKind {
|
||||||
|
File,
|
||||||
|
Directory,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||||
if content.is_empty() {
|
if content.is_empty() {
|
||||||
ContentType::Text
|
ContentType::Text
|
||||||
@@ -98,12 +106,17 @@ pub(crate) fn sort_by_path<T>(mut items: Vec<T>, key: impl Fn(&T) -> &str) -> So
|
|||||||
| _ => {}
|
| _ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut a_parts = a_path.split('/');
|
let mut a_parts = a_path.split('/').peekable();
|
||||||
let mut b_parts = b_path.split('/');
|
let mut b_parts = b_path.split('/').peekable();
|
||||||
loop {
|
loop {
|
||||||
match (a_parts.next(), b_parts.next()) {
|
match (a_parts.next(), b_parts.next()) {
|
||||||
| (Some(a), Some(b)) => {
|
| (Some(a), Some(b)) => {
|
||||||
if a != b {
|
if a != b {
|
||||||
|
match (a_parts.peek().is_some(), b_parts.peek().is_some()) {
|
||||||
|
| (true, false) => return std::cmp::Ordering::Less,
|
||||||
|
| (false, true) => return std::cmp::Ordering::Greater,
|
||||||
|
| _ => {}
|
||||||
|
}
|
||||||
return a.cmp(b);
|
return a.cmp(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,10 +129,10 @@ pub(crate) fn sort_by_path<T>(mut items: Vec<T>, key: impl Fn(&T) -> &str) -> So
|
|||||||
SortedByPath(items)
|
SortedByPath(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build_file_tree_from_sorted_paths<T>(paths: &SortedByPath<T>) -> Vec<FileTreeItem>
|
pub(crate) fn build_file_tree<T>(
|
||||||
where
|
paths: &SortedByPath<T>,
|
||||||
T: AsRef<str>,
|
key: impl Fn(&T) -> &str,
|
||||||
{
|
) -> Vec<FileTreeItem> {
|
||||||
let mut stack: Vec<&str> = Vec::with_capacity(50);
|
let mut stack: Vec<&str> = Vec::with_capacity(50);
|
||||||
let mut leafs: Vec<&str> = Vec::with_capacity(50);
|
let mut leafs: Vec<&str> = Vec::with_capacity(50);
|
||||||
|
|
||||||
@@ -127,7 +140,7 @@ where
|
|||||||
|
|
||||||
fn strip_path_prefix<'a>(path: &'a str, prefix: &str) -> &'a str {
|
fn strip_path_prefix<'a>(path: &'a str, prefix: &str) -> &'a str {
|
||||||
path.strip_prefix(prefix)
|
path.strip_prefix(prefix)
|
||||||
.and_then(|it| it.strip_prefix('/'))
|
.map(|it| it.strip_prefix('/').unwrap_or(it))
|
||||||
.unwrap_or(path)
|
.unwrap_or(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,66 +150,84 @@ where
|
|||||||
items: &mut Vec<FileTreeItem>,
|
items: &mut Vec<FileTreeItem>,
|
||||||
emitted_depth: usize,
|
emitted_depth: usize,
|
||||||
base_depth: usize,
|
base_depth: usize,
|
||||||
) {
|
) -> bool {
|
||||||
|
let mut base_dir_created = false;
|
||||||
|
|
||||||
if leafs.is_empty() && stack.is_empty() {
|
if leafs.is_empty() && stack.is_empty() {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stack_dir_path = Arc::<str>::from(stack.join("/"));
|
let stack_dir_path = Arc::<str>::from(stack.join("/"));
|
||||||
|
|
||||||
let (common_dir_path, stack_dir_name) =
|
let (common_dir_path, stack_dir_name) =
|
||||||
if (base_depth == 0 || base_depth == stack.len()) && emitted_depth == 0 {
|
if (base_depth == 0 || base_depth == stack.len()) && emitted_depth == 0 {
|
||||||
(None, Arc::clone(&stack_dir_path))
|
(None, Some(Arc::clone(&stack_dir_path)))
|
||||||
} else {
|
} else {
|
||||||
let common_dir_path = if base_depth == stack.len() {
|
let common_dir_path = if base_depth == stack.len() || emitted_depth == stack.len() {
|
||||||
Arc::<str>::from(stack[..emitted_depth].join("/"))
|
Arc::<str>::from(stack[..emitted_depth].join("/"))
|
||||||
} else {
|
} else {
|
||||||
Arc::<str>::from(stack[..base_depth].join("/"))
|
Arc::<str>::from(stack[..base_depth].join("/"))
|
||||||
};
|
};
|
||||||
let stack_dir_name =
|
let stack_dir_name =
|
||||||
Arc::<str>::from(strip_path_prefix(&stack_dir_path, &common_dir_path));
|
Arc::<str>::from(strip_path_prefix(&stack_dir_path, &common_dir_path));
|
||||||
(Some(common_dir_path), stack_dir_name)
|
(
|
||||||
|
Some(common_dir_path),
|
||||||
|
if stack_dir_name.len() == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(stack_dir_name)
|
||||||
|
},
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let stack_dir_depth = if let Some(common_dir_path) = common_dir_path
|
let stack_dir_depth = if let Some(common_dir_path) = common_dir_path
|
||||||
&& emitted_depth == 0
|
&& emitted_depth == 0
|
||||||
{
|
{
|
||||||
items.push(FileTreeItem {
|
items.push(FileTreeItem {
|
||||||
|
kind: FileTreeItemKind::Directory,
|
||||||
full_path: Arc::clone(&common_dir_path),
|
full_path: Arc::clone(&common_dir_path),
|
||||||
name: common_dir_path,
|
name: common_dir_path,
|
||||||
level: base_depth.saturating_sub(1),
|
level: base_depth.saturating_sub(1),
|
||||||
});
|
});
|
||||||
|
base_dir_created = true;
|
||||||
base_depth
|
base_depth
|
||||||
} else {
|
} else {
|
||||||
emitted_depth
|
emitted_depth
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(stack_dir_name) = stack_dir_name {
|
||||||
items.push(FileTreeItem {
|
items.push(FileTreeItem {
|
||||||
|
kind: FileTreeItemKind::Directory,
|
||||||
full_path: Arc::clone(&stack_dir_path),
|
full_path: Arc::clone(&stack_dir_path),
|
||||||
name: stack_dir_name,
|
name: stack_dir_name,
|
||||||
level: stack_dir_depth,
|
level: stack_dir_depth,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for leaf in leafs.drain(..) {
|
for leaf in leafs.drain(..) {
|
||||||
items.push(FileTreeItem {
|
items.push(FileTreeItem {
|
||||||
|
kind: FileTreeItemKind::File,
|
||||||
full_path: Arc::<str>::from(leaf),
|
full_path: Arc::<str>::from(leaf),
|
||||||
name: strip_path_prefix(&leaf, &stack_dir_path).into(),
|
name: strip_path_prefix(&leaf, &stack_dir_path).into(),
|
||||||
level: stack.len(),
|
level: stack.len(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
base_dir_created
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut base_depth = 0;
|
let mut base_depth = 0;
|
||||||
let mut emitted_depth = 0;
|
let mut emitted_depth = 0;
|
||||||
|
|
||||||
for path in paths.0.iter() {
|
for path in paths.0.iter() {
|
||||||
let path = path.as_ref();
|
let path = key(path);
|
||||||
match path.rsplit_once('/') {
|
match path.rsplit_once('/') {
|
||||||
| None => {
|
| None => {
|
||||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, base_depth);
|
||||||
stack.clear();
|
stack.clear();
|
||||||
// top level file
|
// top level file
|
||||||
items.push(FileTreeItem {
|
items.push(FileTreeItem {
|
||||||
|
kind: FileTreeItemKind::File,
|
||||||
full_path: path.into(),
|
full_path: path.into(),
|
||||||
name: path.into(),
|
name: path.into(),
|
||||||
level: 0,
|
level: 0,
|
||||||
@@ -232,11 +263,17 @@ where
|
|||||||
// also push stack dir a/b/c to items but strip a from name so that it becomes "b/c" with level equal to common_count
|
// also push stack dir a/b/c to items but strip a from name so that it becomes "b/c" with level equal to common_count
|
||||||
// finally push any leaf under a/b/c
|
// finally push any leaf under a/b/c
|
||||||
|
|
||||||
|
let base_dir_created =
|
||||||
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth);
|
flush_leafs(&mut leafs, &stack, &mut items, emitted_depth, common_depth);
|
||||||
|
|
||||||
// pop top of stack minus common dir
|
// pop top of stack minus common dir
|
||||||
stack.truncate(common_depth);
|
stack.truncate(common_depth);
|
||||||
|
|
||||||
|
if base_dir_created {
|
||||||
emitted_depth = common_depth;
|
emitted_depth = common_depth;
|
||||||
|
} else {
|
||||||
|
emitted_depth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
for seg in parent.split('/').skip(common_depth) {
|
for seg in parent.split('/').skip(common_depth) {
|
||||||
stack.push(seg);
|
stack.push(seg);
|
||||||
@@ -253,6 +290,12 @@ where
|
|||||||
items
|
items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<T> Default for SortedByPath<T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Deref for SortedByPath<T> {
|
impl<T> Deref for SortedByPath<T> {
|
||||||
type Target = [T];
|
type Target = [T];
|
||||||
fn deref(&self) -> &[T] {
|
fn deref(&self) -> &[T] {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
fn assert_tree(paths: &[&str], expected: &[(&str, &str, usize)]) {
|
fn assert_tree(paths: &[&str], expected: &[(&str, &str, usize)]) {
|
||||||
let sorted_paths = sort_by_path(paths.to_vec(), |path| *path);
|
let sorted_paths = sort_by_path(paths.to_vec(), |path| *path);
|
||||||
@@ -8,7 +9,7 @@ fn assert_tree(paths: &[&str], expected: &[(&str, &str, usize)]) {
|
|||||||
"test inputs must already be sorted by sort_by_path",
|
"test inputs must already be sorted by sort_by_path",
|
||||||
);
|
);
|
||||||
|
|
||||||
let actual = build_file_tree_from_sorted_paths(&sorted_paths)
|
let actual = build_file_tree(&sorted_paths, |path| *path)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
(
|
(
|
||||||
@@ -54,6 +55,19 @@ fn sorts_paths_by_components_with_root_files_at_bottom() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sorts_directory_branches_before_sibling_files() {
|
||||||
|
let sorted_paths = sort_by_path(
|
||||||
|
vec!["src/query.rs", "src/screen/dashboard/issue_list.rs"],
|
||||||
|
|path| *path,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
sorted_paths.0,
|
||||||
|
vec!["src/screen/dashboard/issue_list.rs", "src/query.rs",],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn builds_empty_tree_for_empty_paths() {
|
fn builds_empty_tree_for_empty_paths() {
|
||||||
assert_tree(&[], &[]);
|
assert_tree(&[], &[]);
|
||||||
@@ -138,3 +152,58 @@ fn keeps_emitted_parent_for_mixed_multi_file_and_singleton_branches() {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn builds_tree_for_pull_request_fixture_with_root_and_nested_file() {
|
||||||
|
let fixture: Value = serde_json::from_str(include_str!(
|
||||||
|
"../../fixtures/github/issues.pull_request_file_tree.PR_kwDONovem84.json"
|
||||||
|
))
|
||||||
|
.expect("fixture json should parse");
|
||||||
|
|
||||||
|
let paths = fixture
|
||||||
|
.get("node")
|
||||||
|
.and_then(|node| node.get("files"))
|
||||||
|
.and_then(|files| files.get("edges"))
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.expect("fixture should contain file edges")
|
||||||
|
.iter()
|
||||||
|
.filter_map(|edge| edge.get("node"))
|
||||||
|
.filter_map(|node| node.get("path"))
|
||||||
|
.filter_map(Value::as_str)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let sorted_paths = sort_by_path(paths, |path| *path);
|
||||||
|
assert_eq!(
|
||||||
|
sorted_paths.0.as_slice(),
|
||||||
|
&["src/screen/dashboard/issue_list.rs", "src/query.rs"],
|
||||||
|
);
|
||||||
|
|
||||||
|
let actual = build_file_tree(&sorted_paths, |path| *path)
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| {
|
||||||
|
(
|
||||||
|
item.full_path.to_string(),
|
||||||
|
item.name.to_string(),
|
||||||
|
item.level,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
actual,
|
||||||
|
vec![
|
||||||
|
("src".to_string(), "src".to_string(), 0),
|
||||||
|
(
|
||||||
|
"src/screen/dashboard".to_string(),
|
||||||
|
"screen/dashboard".to_string(),
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"src/screen/dashboard/issue_list.rs".to_string(),
|
||||||
|
"issue_list.rs".to_string(),
|
||||||
|
3,
|
||||||
|
),
|
||||||
|
("src/query.rs".to_string(), "query.rs".to_string(), 1),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user