feat: out-of-box experience #2
Generated
+1
@@ -10785,6 +10785,7 @@ version = "1.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assets",
|
"assets",
|
||||||
|
"gix",
|
||||||
"gpui",
|
"gpui",
|
||||||
"gpui-component",
|
"gpui-component",
|
||||||
"signed_core",
|
"signed_core",
|
||||||
|
|||||||
@@ -240,17 +240,24 @@ fn open_with_cache(workdir: &Path) -> Result<gix::Repository> {
|
|||||||
Ok(repo)
|
Ok(repo)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [`FileCommit`] from a walk commit: author, message title and shortened id.
|
/// A [`FileCommit`] from a walk commit: author, message title and shortened
|
||||||
fn file_commit(commit: &gix::Commit<'_>) -> Result<FileCommit> {
|
/// id. `include_description` controls whether the message body is copied;
|
||||||
|
/// history lists never display it, so skipping it saves a string allocation
|
||||||
|
/// per listed commit (the diff panel fetches the full commit on demand).
|
||||||
|
fn file_commit(commit: &gix::Commit<'_>, include_description: bool) -> Result<FileCommit> {
|
||||||
let author = commit.author()?;
|
let author = commit.author()?;
|
||||||
let message = commit.message()?;
|
let message = commit.message()?;
|
||||||
Ok(FileCommit {
|
Ok(FileCommit {
|
||||||
id: commit.id().shorten_or_id().to_string(),
|
id: commit.id().shorten_or_id().to_string(),
|
||||||
summary: String::from_utf8_lossy(message.title).trim().to_string(),
|
summary: String::from_utf8_lossy(message.title).trim().to_string(),
|
||||||
description: message
|
description: if include_description {
|
||||||
.body
|
message
|
||||||
.map(|body| String::from_utf8_lossy(body).trim().to_string())
|
.body
|
||||||
.filter(|body| !body.is_empty()),
|
.map(|body| String::from_utf8_lossy(body).trim().to_string())
|
||||||
|
.filter(|body| !body.is_empty())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
author: String::from_utf8_lossy(author.name).trim().to_string(),
|
author: String::from_utf8_lossy(author.name).trim().to_string(),
|
||||||
time: author.time()?.seconds,
|
time: author.time()?.seconds,
|
||||||
})
|
})
|
||||||
@@ -332,7 +339,7 @@ fn last_commits(repo: &gix::Repository, rels: &[PathBuf]) -> Result<Vec<(PathBuf
|
|||||||
|
|
||||||
if blob.map(|entry| entry.id().detach()) != parent_blob.map(|entry| entry.id().detach())
|
if blob.map(|entry| entry.id().detach()) != parent_blob.map(|entry| entry.id().detach())
|
||||||
{
|
{
|
||||||
found.push((rel.clone(), file_commit(&commit)?));
|
found.push((rel.clone(), file_commit(&commit, true)?));
|
||||||
pending.swap_remove(ix);
|
pending.swap_remove(ix);
|
||||||
} else {
|
} else {
|
||||||
ix += 1;
|
ix += 1;
|
||||||
@@ -381,7 +388,7 @@ pub fn all_commits(repo: &gix::Repository) -> Result<CommitList> {
|
|||||||
let info = info?;
|
let info = info?;
|
||||||
total += 1;
|
total += 1;
|
||||||
if commits.len() < MAX_LISTED_COMMITS {
|
if commits.len() < MAX_LISTED_COMMITS {
|
||||||
commits.push(file_commit(&info.object()?)?);
|
commits.push(file_commit(&info.object()?, false)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(CommitList { total, commits })
|
Ok(CommitList { total, commits })
|
||||||
@@ -681,23 +688,29 @@ pub fn head_commit(repo: &gix::Repository) -> Result<Option<FileCommit>> {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
let commit = head.object()?.into_commit();
|
let commit = head.object()?.into_commit();
|
||||||
let author = commit.author()?;
|
Ok(Some(file_commit(&commit, true)?))
|
||||||
let message = commit.message()?;
|
|
||||||
Ok(Some(FileCommit {
|
|
||||||
id: commit.id().shorten_or_id().to_string(),
|
|
||||||
summary: String::from_utf8_lossy(message.title).trim().to_string(),
|
|
||||||
description: message
|
|
||||||
.body
|
|
||||||
.map(|body| String::from_utf8_lossy(body).trim().to_string())
|
|
||||||
.filter(|body| !body.is_empty()),
|
|
||||||
author: String::from_utf8_lossy(author.name).trim().to_string(),
|
|
||||||
time: author.time()?.seconds,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Short names of local branches (`refs/heads/*`), sorted alphabetically.
|
/// Full metadata of the commit `id` (short or full) in the repository at
|
||||||
pub fn worktree_branches(workdir: &Path) -> Result<Vec<String>> {
|
/// `workdir`, like [`head_commit`] for an arbitrary commit. Returns
|
||||||
|
/// `Ok(None)` when the id cannot be resolved.
|
||||||
|
///
|
||||||
|
/// The commit list ([`all_commits`]) omits message bodies to keep the walk
|
||||||
|
/// cheap; the diff panel uses this to fetch the full commit on demand.
|
||||||
|
pub fn worktree_commit(workdir: &Path, id: &str) -> Result<Option<FileCommit>> {
|
||||||
let repo = open_with_cache(workdir)?;
|
let repo = open_with_cache(workdir)?;
|
||||||
|
match repo.rev_parse_single(id.as_bytes()) {
|
||||||
|
Ok(commit_id) => {
|
||||||
|
let commit = commit_id.object()?.into_commit();
|
||||||
|
Ok(Some(file_commit(&commit, true)?))
|
||||||
|
}
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short names of local branches (`refs/heads/*`) of `repo`, sorted
|
||||||
|
/// alphabetically.
|
||||||
|
pub fn repo_branches(repo: &gix::Repository) -> Result<Vec<String>> {
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
for reference in repo.references()?.local_branches()? {
|
for reference in repo.references()?.local_branches()? {
|
||||||
let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?;
|
let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||||
@@ -707,9 +720,8 @@ pub fn worktree_branches(workdir: &Path) -> Result<Vec<String>> {
|
|||||||
Ok(names)
|
Ok(names)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Short names of tags (`refs/tags/*`), sorted alphabetically.
|
/// Short names of tags (`refs/tags/*`) of `repo`, sorted alphabetically.
|
||||||
pub fn worktree_tags(workdir: &Path) -> Result<Vec<String>> {
|
pub fn repo_tags(repo: &gix::Repository) -> Result<Vec<String>> {
|
||||||
let repo = open_with_cache(workdir)?;
|
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
for reference in repo.references()?.tags()? {
|
for reference in repo.references()?.tags()? {
|
||||||
let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?;
|
let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||||
@@ -719,6 +731,16 @@ pub fn worktree_tags(workdir: &Path) -> Result<Vec<String>> {
|
|||||||
Ok(names)
|
Ok(names)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Short names of local branches (`refs/heads/*`), sorted alphabetically.
|
||||||
|
pub fn worktree_branches(workdir: &Path) -> Result<Vec<String>> {
|
||||||
|
repo_branches(&open_with_cache(workdir)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Short names of tags (`refs/tags/*`), sorted alphabetically.
|
||||||
|
pub fn worktree_tags(workdir: &Path) -> Result<Vec<String>> {
|
||||||
|
repo_tags(&open_with_cache(workdir)?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Short name of the branch HEAD points to, or `None` when detached (e.g.
|
/// Short name of the branch HEAD points to, or `None` when detached (e.g.
|
||||||
/// after checking out a tag or a commit directly).
|
/// after checking out a tag or a commit directly).
|
||||||
pub fn current_branch(repo: &gix::Repository) -> Result<Option<String>> {
|
pub fn current_branch(repo: &gix::Repository) -> Result<Option<String>> {
|
||||||
|
|||||||
@@ -13,5 +13,6 @@ utils = { path = "../utils" }
|
|||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
gpui-component.workspace = true
|
gpui-component.workspace = true
|
||||||
|
gix.workspace = true
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ use gpui_component::list::ListItem;
|
|||||||
use gpui_component::spinner::Spinner;
|
use gpui_component::spinner::Spinner;
|
||||||
use gpui_component::text::{TextView, TextViewState};
|
use gpui_component::text::{TextView, TextViewState};
|
||||||
use gpui_component::tree::{TreeEntry, TreeState, tree};
|
use gpui_component::tree::{TreeEntry, TreeState, tree};
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
use gpui_component::{ActiveTheme, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
use super::RepoDetailView;
|
use super::RepoDetailView;
|
||||||
use super::helpers::{code_language, is_markdown_path, placeholder};
|
use super::helpers::{code_language, is_markdown_path, placeholder, tree_row};
|
||||||
|
|
||||||
/// Width of the file explorer column.
|
/// Width of the file explorer column.
|
||||||
const TREE_WIDTH: f32 = 240.;
|
const TREE_WIDTH: f32 = 240.;
|
||||||
@@ -82,46 +82,18 @@ impl RepoDetailView {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
view: &WeakEntity<Self>,
|
view: &WeakEntity<Self>,
|
||||||
) -> ListItem {
|
) -> ListItem {
|
||||||
let item = entry.item();
|
|
||||||
let id = item.id.clone();
|
|
||||||
let is_folder = entry.is_folder();
|
|
||||||
|
|
||||||
let icon = if is_folder {
|
|
||||||
if entry.is_expanded() {
|
|
||||||
IconName::FolderOpen
|
|
||||||
} else {
|
|
||||||
IconName::FolderClosed
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
IconName::File
|
|
||||||
};
|
|
||||||
|
|
||||||
let view = view.clone();
|
let view = view.clone();
|
||||||
|
let id = entry.item().id.clone();
|
||||||
|
|
||||||
ListItem::new(ix)
|
tree_row(ix, entry, selected, move |window, cx| {
|
||||||
.pl(px(8.) + px(14.) * entry.depth() as f32)
|
if let Some(view) = view.upgrade() {
|
||||||
.selected(selected)
|
view.update(cx, |this, cx| this.open_file(&id, window, cx));
|
||||||
.child(
|
}
|
||||||
h_flex()
|
})
|
||||||
.gap_2()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(Icon::new(icon).small())
|
|
||||||
.child(div().text_sm().text_ellipsis().child(item.label.clone())),
|
|
||||||
)
|
|
||||||
.on_click(move |_event, window, cx| {
|
|
||||||
// Folders expand/collapse via the tree itself.
|
|
||||||
if is_folder {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(view) = view.upgrade() {
|
|
||||||
view.update(cx, |this, cx| this.open_file(&id, window, cx));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Left column: the file tree.
|
/// Left column: the file tree.
|
||||||
pub(super) fn render_tree_column(
|
pub(super) fn render_tree_column(
|
||||||
&mut self,
|
|
||||||
tree_state: Entity<TreeState>,
|
tree_state: Entity<TreeState>,
|
||||||
view: WeakEntity<Self>,
|
view: WeakEntity<Self>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@@ -143,7 +115,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// Right column: README, selected file preview, or status text.
|
/// Right column: README, selected file preview, or status text.
|
||||||
pub(super) fn render_content_column(
|
pub(super) fn render_content_column(
|
||||||
&mut self,
|
&self,
|
||||||
pane_title: SharedString,
|
pane_title: SharedString,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
@@ -190,12 +162,7 @@ impl RepoDetailView {
|
|||||||
Some(FileContent::Binary) => placeholder("Binary file — preview not supported", cx),
|
Some(FileContent::Binary) => placeholder("Binary file — preview not supported", cx),
|
||||||
Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx),
|
Some(FileContent::TooLarge) => placeholder("File is too large to preview", cx),
|
||||||
Some(FileContent::Failed(message)) => placeholder(message, cx),
|
Some(FileContent::Failed(message)) => placeholder(message, cx),
|
||||||
None => v_flex()
|
None => preview_spinner(),
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(Spinner::new().small())
|
|
||||||
.into_any_element(),
|
|
||||||
}
|
}
|
||||||
} else if self.readme_name.is_some() {
|
} else if self.readme_name.is_some() {
|
||||||
self.markdown_element(None, cx)
|
self.markdown_element(None, cx)
|
||||||
@@ -206,19 +173,13 @@ impl RepoDetailView {
|
|||||||
// Latest commit for the current pane: the selected file, or the README
|
// Latest commit for the current pane: the selected file, or the README
|
||||||
// while nothing is selected. Computed after the body above, which
|
// while nothing is selected. Computed after the body above, which
|
||||||
// needs `&mut self`.
|
// needs `&mut self`.
|
||||||
let commit = self
|
let commit = match &self.selected_file {
|
||||||
.selected_file
|
Some(path) => self.commits.get(path.as_ref()),
|
||||||
.as_ref()
|
None => self
|
||||||
.and_then(|path| self.commits.get(path.as_ref()))
|
.readme_name
|
||||||
.or_else(|| {
|
.as_ref()
|
||||||
if self.selected_file.is_none() {
|
.and_then(|name| self.commits.get(name.as_ref())),
|
||||||
self.readme_name
|
};
|
||||||
.as_ref()
|
|
||||||
.and_then(|name| self.commits.get(name.as_ref()))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
@@ -285,7 +246,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// The persistent markdown TextView for `path` (`None` = README), or a
|
/// The persistent markdown TextView for `path` (`None` = README), or a
|
||||||
/// spinner while the document is being loaded/parsed.
|
/// spinner while the document is being loaded/parsed.
|
||||||
fn markdown_element(&mut self, path: Option<&str>, _cx: &mut Context<Self>) -> AnyElement {
|
fn markdown_element(&self, path: Option<&str>, _cx: &mut Context<Self>) -> AnyElement {
|
||||||
let Some(md) = &self.md else {
|
let Some(md) = &self.md else {
|
||||||
return preview_spinner();
|
return preview_spinner();
|
||||||
};
|
};
|
||||||
@@ -325,7 +286,7 @@ impl RepoDetailView {
|
|||||||
|
|
||||||
/// The persistent code editor for `path`, or a spinner while the file is
|
/// The persistent code editor for `path`, or a spinner while the file is
|
||||||
/// being loaded/parsed.
|
/// being loaded/parsed.
|
||||||
fn code_element(&mut self, path: &str, _cx: &mut Context<Self>) -> AnyElement {
|
fn code_element(&self, path: &str, _cx: &mut Context<Self>) -> AnyElement {
|
||||||
let Some(code) = &self.code else {
|
let Some(code) = &self.code else {
|
||||||
return preview_spinner();
|
return preview_spinner();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ fn commit_row(
|
|||||||
cx: &App,
|
cx: &App,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let view = view.clone();
|
let view = view.clone();
|
||||||
let commit = commit.clone();
|
// Only the id is needed by the click handler: the diff panel fetches
|
||||||
|
// the full commit itself.
|
||||||
|
let id = commit.id.clone();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(ix)
|
.id(ix)
|
||||||
@@ -76,7 +78,7 @@ fn commit_row(
|
|||||||
)
|
)
|
||||||
.on_click(move |_event, window, cx| {
|
.on_click(move |_event, window, cx| {
|
||||||
if let Some(view) = view.upgrade() {
|
if let Some(view) = view.upgrade() {
|
||||||
view.update(cx, |this, cx| this.open_commit_diff(&commit, window, cx));
|
view.update(cx, |this, cx| this.open_commit_diff(&id, window, cx));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -85,7 +87,7 @@ fn commit_row(
|
|||||||
impl RepoDetailView {
|
impl RepoDetailView {
|
||||||
/// Full-height body of the Commits tab: all commits in a virtual
|
/// Full-height body of the Commits tab: all commits in a virtual
|
||||||
/// list, or a status message while loading / when there are none.
|
/// list, or a status message while loading / when there are none.
|
||||||
pub(super) fn render_commits_tab(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
pub(super) fn render_commits_tab(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let Some(list) = self.all_commits.as_ref() else {
|
let Some(list) = self.all_commits.as_ref() else {
|
||||||
return if self.loading_all_commits {
|
return if self.loading_all_commits {
|
||||||
v_flex()
|
v_flex()
|
||||||
|
|||||||
@@ -4,29 +4,48 @@
|
|||||||
//! commit in the Commits tab or the latest-commit button in the header.
|
//! commit in the Commits tab or the latest-commit button in the header.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString,
|
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render,
|
||||||
WeakEntity, Window, div, px,
|
ScrollStrategy, SharedString, Size, WeakEntity, Window, div, px, size,
|
||||||
};
|
};
|
||||||
use gpui_component::clipboard::Clipboard;
|
use gpui_component::clipboard::Clipboard;
|
||||||
use gpui_component::dock::{Panel, PanelEvent};
|
use gpui_component::dock::{Panel, PanelEvent};
|
||||||
use gpui_component::list::ListItem;
|
use gpui_component::list::ListItem;
|
||||||
use gpui_component::resizable::{resizable_panel, v_resizable};
|
use gpui_component::resizable::{resizable_panel, v_resizable};
|
||||||
|
use gpui_component::scroll::{ScrollableElement, Scrollbar};
|
||||||
use gpui_component::spinner::Spinner;
|
use gpui_component::spinner::Spinner;
|
||||||
use gpui_component::tag::Tag;
|
use gpui_component::tag::Tag;
|
||||||
use gpui_component::tree::{TreeEntry, TreeItem, TreeState, tree};
|
use gpui_component::tree::{TreeEntry, TreeItem, TreeState, tree};
|
||||||
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
use gpui_component::{
|
||||||
|
ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list,
|
||||||
|
};
|
||||||
use signed_git::{CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff};
|
use signed_git::{CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff};
|
||||||
use utils::relative_time_secs;
|
use utils::relative_time_secs;
|
||||||
|
|
||||||
use super::helpers::{build_tree_items, placeholder, tree_items};
|
use super::helpers::{build_tree_items, placeholder, track, tree_items, tree_row};
|
||||||
|
|
||||||
/// Width of the changed-files column.
|
/// Width of the changed-files column.
|
||||||
const TREE_WIDTH: f32 = 260.;
|
const TREE_WIDTH: f32 = 260.;
|
||||||
/// Width of one line-number gutter in a diff row.
|
/// Width of one line-number gutter in a diff row.
|
||||||
const GUTTER_WIDTH: f32 = 44.;
|
const GUTTER_WIDTH: f32 = 44.;
|
||||||
|
/// Height of one row in the virtual diff list.
|
||||||
|
const DIFF_ROW_HEIGHT: f32 = 20.;
|
||||||
|
|
||||||
|
/// One row of the virtual diff list: a hunk header, or a line of a hunk.
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
enum DiffRow {
|
||||||
|
Hunk {
|
||||||
|
old_start: u32,
|
||||||
|
old_lines: u32,
|
||||||
|
new_start: u32,
|
||||||
|
new_lines: u32,
|
||||||
|
},
|
||||||
|
/// Line `line` of hunk `hunk` of the selected file's diff.
|
||||||
|
Line { hunk: usize, line: usize },
|
||||||
|
}
|
||||||
|
|
||||||
/// Detail panel showing the diff of one commit.
|
/// Detail panel showing the diff of one commit.
|
||||||
pub struct CommitDiffView {
|
pub struct CommitDiffView {
|
||||||
@@ -35,7 +54,9 @@ pub struct CommitDiffView {
|
|||||||
worktree: PathBuf,
|
worktree: PathBuf,
|
||||||
/// Display name of the repository the commit belongs to.
|
/// Display name of the repository the commit belongs to.
|
||||||
repo_name: SharedString,
|
repo_name: SharedString,
|
||||||
/// The commit being shown (header and tab title).
|
/// The commit being shown (header and tab title). Starts as an id-only
|
||||||
|
/// stub; [`Self::load`] replaces it with the full metadata, which the
|
||||||
|
/// history list intentionally omits.
|
||||||
commit: FileCommit,
|
commit: FileCommit,
|
||||||
/// Loaded diff; `None` while loading or after a failure.
|
/// Loaded diff; `None` while loading or after a failure.
|
||||||
diff: Option<CommitDiff>,
|
diff: Option<CommitDiff>,
|
||||||
@@ -46,7 +67,14 @@ pub struct CommitDiffView {
|
|||||||
tree_state: Entity<TreeState>,
|
tree_state: Entity<TreeState>,
|
||||||
/// Path of the file whose diff is shown in the detail column.
|
/// Path of the file whose diff is shown in the detail column.
|
||||||
selected_file: Option<SharedString>,
|
selected_file: Option<SharedString>,
|
||||||
/// In-flight tasks; pruned on every push (see [`Self::track`]).
|
/// Rows of the selected file's diff (hunk headers + lines), backing the
|
||||||
|
/// virtual list in the detail column.
|
||||||
|
rows: Vec<DiffRow>,
|
||||||
|
/// Per-row heights of [`Self::rows`].
|
||||||
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
|
/// Virtual list state of the diff rows.
|
||||||
|
scroll_handle: VirtualListScrollHandle,
|
||||||
|
/// In-flight tasks; pruned on every push (see [`helpers::track`]).
|
||||||
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
tasks: Vec<gpui::Task<Result<(), anyhow::Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +82,7 @@ impl CommitDiffView {
|
|||||||
pub fn new(
|
pub fn new(
|
||||||
worktree: PathBuf,
|
worktree: PathBuf,
|
||||||
repo_name: SharedString,
|
repo_name: SharedString,
|
||||||
commit: FileCommit,
|
commit_id: String,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -69,17 +97,27 @@ impl CommitDiffView {
|
|||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
worktree,
|
worktree,
|
||||||
repo_name,
|
repo_name,
|
||||||
commit,
|
commit: FileCommit {
|
||||||
|
id: commit_id,
|
||||||
|
summary: String::new(),
|
||||||
|
description: None,
|
||||||
|
author: String::new(),
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
diff: None,
|
diff: None,
|
||||||
loading: true,
|
loading: true,
|
||||||
error: None,
|
error: None,
|
||||||
tree_state,
|
tree_state,
|
||||||
selected_file: None,
|
selected_file: None,
|
||||||
|
rows: Vec::new(),
|
||||||
|
item_sizes: Rc::new(Vec::new()),
|
||||||
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
tasks: Vec::new(),
|
tasks: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the commit diff on a background task and populate the tree.
|
/// Load the commit diff (and the full commit metadata) on a background
|
||||||
|
/// task and populate the tree.
|
||||||
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.loading = true;
|
self.loading = true;
|
||||||
self.error = None;
|
self.error = None;
|
||||||
@@ -89,13 +127,27 @@ impl CommitDiffView {
|
|||||||
let id = self.commit.id.clone();
|
let id = self.commit.id.clone();
|
||||||
|
|
||||||
let task = cx.spawn_in(window, async move |this, cx| {
|
let task = cx.spawn_in(window, async move |this, cx| {
|
||||||
let result = cx
|
let commit = cx
|
||||||
.background_spawn(async move { signed_git::worktree_commit_diff(&worktree, &id) })
|
.background_spawn({
|
||||||
|
let worktree = worktree.clone();
|
||||||
|
let id = id.clone();
|
||||||
|
async move { signed_git::worktree_commit(&worktree, &id) }
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let diff = cx
|
||||||
|
.background_spawn({
|
||||||
|
let worktree = worktree.clone();
|
||||||
|
let id = id.clone();
|
||||||
|
async move { signed_git::worktree_commit_diff(&worktree, &id) }
|
||||||
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
this.update_in(cx, |this, _window, cx| {
|
this.update_in(cx, |this, _window, cx| {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
match result {
|
if let Ok(Some(commit)) = commit {
|
||||||
|
this.commit = commit;
|
||||||
|
}
|
||||||
|
match diff {
|
||||||
Ok(diff) => {
|
Ok(diff) => {
|
||||||
let mut paths: Vec<PathBuf> = diff
|
let mut paths: Vec<PathBuf> = diff
|
||||||
.files
|
.files
|
||||||
@@ -113,8 +165,11 @@ impl CommitDiffView {
|
|||||||
let item = find_item(&items, first.as_deref());
|
let item = find_item(&items, first.as_deref());
|
||||||
state.set_selected_item(item, cx);
|
state.set_selected_item(item, cx);
|
||||||
});
|
});
|
||||||
this.selected_file = first;
|
this.selected_file = first.clone();
|
||||||
this.diff = Some(diff);
|
this.diff = Some(diff);
|
||||||
|
if let Some(path) = first {
|
||||||
|
this.set_diff_rows(path.as_ref());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
this.error = Some(error.to_string().into());
|
this.error = Some(error.to_string().into());
|
||||||
@@ -126,20 +181,28 @@ impl CommitDiffView {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
self.track(task);
|
track(&mut self.tasks, task);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show the diff of the file at `path` (selected in the tree).
|
/// Show the diff of the file at `path` (selected in the tree).
|
||||||
fn select_file(&mut self, path: &str, cx: &mut Context<Self>) {
|
fn select_file(&mut self, path: &str, cx: &mut Context<Self>) {
|
||||||
self.selected_file = Some(path.into());
|
self.selected_file = Some(path.into());
|
||||||
|
self.set_diff_rows(path);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Track `task` until it completes; finished tasks are pruned on every
|
/// Rebuild the virtual list state for the file at `path` and scroll back
|
||||||
/// push so the vec stays bounded by the number of in-flight loads.
|
/// to the top.
|
||||||
fn track(&mut self, task: gpui::Task<Result<(), anyhow::Error>>) {
|
fn set_diff_rows(&mut self, path: &str) {
|
||||||
self.tasks.retain(|task| !task.is_ready());
|
let Some(diff) = self.diff.as_ref() else {
|
||||||
self.tasks.push(task);
|
return;
|
||||||
|
};
|
||||||
|
let Some(file) = diff.files.iter().find(|file| file.path == path) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.rows = diff_rows(file);
|
||||||
|
self.item_sizes = Rc::new(vec![size(px(0.), px(DIFF_ROW_HEIGHT)); self.rows.len()]);
|
||||||
|
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One row of the changed-files tree: icon + name, indented by depth.
|
/// One row of the changed-files tree: icon + name, indented by depth.
|
||||||
@@ -149,45 +212,18 @@ impl CommitDiffView {
|
|||||||
selected: bool,
|
selected: bool,
|
||||||
view: &WeakEntity<Self>,
|
view: &WeakEntity<Self>,
|
||||||
) -> ListItem {
|
) -> ListItem {
|
||||||
let item = entry.item();
|
|
||||||
let id = item.id.clone();
|
|
||||||
let is_folder = entry.is_folder();
|
|
||||||
|
|
||||||
let icon = if is_folder {
|
|
||||||
if entry.is_expanded() {
|
|
||||||
IconName::FolderOpen
|
|
||||||
} else {
|
|
||||||
IconName::FolderClosed
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
IconName::File
|
|
||||||
};
|
|
||||||
|
|
||||||
let view = view.clone();
|
let view = view.clone();
|
||||||
|
let id = entry.item().id.clone();
|
||||||
|
|
||||||
ListItem::new(ix)
|
tree_row(ix, entry, selected, move |_window, cx| {
|
||||||
.pl(px(8.) + px(14.) * entry.depth() as f32)
|
if let Some(view) = view.upgrade() {
|
||||||
.selected(selected)
|
view.update(cx, |this, cx| this.select_file(&id, cx));
|
||||||
.child(
|
}
|
||||||
h_flex()
|
})
|
||||||
.gap_2()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(Icon::new(icon).small())
|
|
||||||
.child(div().text_sm().text_ellipsis().child(item.label.clone())),
|
|
||||||
)
|
|
||||||
.on_click(move |_event, _window, cx| {
|
|
||||||
// Folders expand/collapse via the tree itself.
|
|
||||||
if is_folder {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(view) = view.upgrade() {
|
|
||||||
view.update(cx, |this, cx| this.select_file(&id, cx));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Left column: the changed-files tree.
|
/// Left column: the changed-files tree.
|
||||||
fn render_tree_column(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_tree_column(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let tree_state = self.tree_state.clone();
|
let tree_state = self.tree_state.clone();
|
||||||
let view = cx.entity().downgrade();
|
let view = cx.entity().downgrade();
|
||||||
|
|
||||||
@@ -210,26 +246,14 @@ impl CommitDiffView {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(self.diff.is_none() && !self.loading, |this| {
|
.when(self.diff.is_none() && !self.loading, |this| {
|
||||||
this.child(
|
this.child(placeholder("Failed to load diff", cx))
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_4()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().muted_foreground)
|
|
||||||
.child("Failed to load diff"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Right column: header of the selected file plus its diff.
|
/// Right column: header of the selected file plus its diff.
|
||||||
fn render_detail_column(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_detail_column(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
if self.loading {
|
if self.loading {
|
||||||
return v_flex()
|
return v_flex()
|
||||||
.size_full()
|
.size_full()
|
||||||
@@ -254,11 +278,12 @@ impl CommitDiffView {
|
|||||||
let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else {
|
let Some(file) = diff.files.iter().find(|file| file.path == path.as_ref()) else {
|
||||||
return placeholder("File not found", cx);
|
return placeholder("File not found", cx);
|
||||||
};
|
};
|
||||||
self.render_file_diff(file, cx)
|
self.render_file_diff(file, cx.entity(), cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The diff of one file: a header with status and stats, then the hunks.
|
/// The diff of one file: a header with status and stats, then the hunks
|
||||||
fn render_file_diff(&self, file: &FileDiff, cx: &App) -> AnyElement {
|
/// in a virtual list (a large diff is never materialized per frame).
|
||||||
|
fn render_file_diff(&self, file: &FileDiff, view: Entity<Self>, cx: &App) -> AnyElement {
|
||||||
let status_label = match file.status {
|
let status_label = match file.status {
|
||||||
DiffStatus::Added => "A",
|
DiffStatus::Added => "A",
|
||||||
DiffStatus::Modified => "M",
|
DiffStatus::Modified => "M",
|
||||||
@@ -282,11 +307,44 @@ impl CommitDiffView {
|
|||||||
} else if file.hunks.is_empty() {
|
} else if file.hunks.is_empty() {
|
||||||
placeholder("No content changes", cx)
|
placeholder("No content changes", cx)
|
||||||
} else {
|
} else {
|
||||||
|
let sizes = self.item_sizes.clone();
|
||||||
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.size_full()
|
||||||
.font_family(cx.theme().mono_font_family.clone())
|
.relative()
|
||||||
.text_xs()
|
.child(
|
||||||
.children(file.hunks.iter().map(|hunk| Self::render_hunk(hunk, cx)))
|
v_virtual_list(
|
||||||
|
view,
|
||||||
|
"commit-diff-rows",
|
||||||
|
sizes,
|
||||||
|
move |this, range, _window, cx| {
|
||||||
|
let Some(diff) = this.diff.as_ref() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(path) = this.selected_file.as_deref() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let Some(file) = diff.files.iter().find(|file| file.path == path)
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
range
|
||||||
|
.map(|ix| Self::render_diff_row(&file.hunks, this.rows[ix], cx))
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.track_scroll(&scroll_handle)
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.top_0()
|
||||||
|
.left_0()
|
||||||
|
.right_0()
|
||||||
|
.bottom_0()
|
||||||
|
.child(Scrollbar::vertical(&scroll_handle)),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -335,41 +393,35 @@ impl CommitDiffView {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(div().id("commit-diff-body").flex_1().min_h_0().child(body))
|
||||||
div()
|
|
||||||
.id("commit-diff-body")
|
|
||||||
.flex_1()
|
|
||||||
.min_h_0()
|
|
||||||
.overflow_scroll()
|
|
||||||
.child(body),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One hunk: the `@@ -a,b +c,d @@` header row followed by its lines.
|
/// One row of the virtual diff list: a hunk header or a single line.
|
||||||
fn render_hunk(hunk: &DiffHunk, cx: &App) -> AnyElement {
|
fn render_diff_row(hunks: &[DiffHunk], row: DiffRow, cx: &App) -> AnyElement {
|
||||||
v_flex()
|
match row {
|
||||||
.w_full()
|
DiffRow::Hunk {
|
||||||
.child(
|
old_start,
|
||||||
div()
|
old_lines,
|
||||||
.px_2()
|
new_start,
|
||||||
.py_0p5()
|
new_lines,
|
||||||
.w_full()
|
} => div()
|
||||||
.bg(cx.theme().muted)
|
.px_2()
|
||||||
.border_y(px(1.))
|
.w_full()
|
||||||
.border_color(cx.theme().border)
|
.h(px(DIFF_ROW_HEIGHT))
|
||||||
.text_color(cx.theme().muted_foreground)
|
.font_family(cx.theme().mono_font_family.clone())
|
||||||
.child(SharedString::from(format!(
|
.text_xs()
|
||||||
"@@ -{},{} +{},{} @@",
|
.bg(cx.theme().muted)
|
||||||
hunk.old_start, hunk.old_lines, hunk.new_start, hunk.new_lines
|
.border_y(px(1.))
|
||||||
))),
|
.border_color(cx.theme().border)
|
||||||
)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.children(
|
.child(SharedString::from(format!(
|
||||||
hunk.lines
|
"@@ -{},{} +{},{} @@",
|
||||||
.iter()
|
old_start, old_lines, new_start, new_lines
|
||||||
.map(|line| Self::render_diff_line(line, cx)),
|
)))
|
||||||
)
|
.into_any_element(),
|
||||||
.into_any_element()
|
DiffRow::Line { hunk, line } => Self::render_diff_line(&hunks[hunk].lines[line], cx),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One diff line: old and new line numbers in gutters, then the content,
|
/// One diff line: old and new line numbers in gutters, then the content,
|
||||||
@@ -382,8 +434,14 @@ impl CommitDiffView {
|
|||||||
};
|
};
|
||||||
let gutter = cx.theme().muted_foreground;
|
let gutter = cx.theme().muted_foreground;
|
||||||
|
|
||||||
|
// Fixed height and nowrap: the virtual list assumes every row has
|
||||||
|
// the same height, so long lines are clipped instead of wrapped.
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
.h(px(DIFF_ROW_HEIGHT))
|
||||||
|
.items_center()
|
||||||
|
.font_family(cx.theme().mono_font_family.clone())
|
||||||
|
.text_xs()
|
||||||
.when_some(bg, |this, bg| this.bg(bg))
|
.when_some(bg, |this, bg| this.bg(bg))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -407,6 +465,8 @@ impl CommitDiffView {
|
|||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_w_0()
|
.min_w_0()
|
||||||
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
.text_color(cx.theme().foreground)
|
.text_color(cx.theme().foreground)
|
||||||
.child(line.text.clone()),
|
.child(line.text.clone()),
|
||||||
)
|
)
|
||||||
@@ -414,7 +474,7 @@ impl CommitDiffView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Header: commit id, summary, author/time and overall change stats.
|
/// Header: commit id, summary, author/time and overall change stats.
|
||||||
fn render_header(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let commit = &self.commit;
|
let commit = &self.commit;
|
||||||
let (files, insertions, deletions) = self.diff.as_ref().map_or((0, 0, 0), |diff| {
|
let (files, insertions, deletions) = self.diff.as_ref().map_or((0, 0, 0), |diff| {
|
||||||
(
|
(
|
||||||
@@ -484,10 +544,11 @@ impl CommitDiffView {
|
|||||||
Tag::danger()
|
Tag::danger()
|
||||||
.outline()
|
.outline()
|
||||||
.small()
|
.small()
|
||||||
.child(format!("- {insertions}")),
|
.child(format!("- {deletions}")),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.overflow_y_scrollbar()
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,6 +565,24 @@ fn find_item<'a>(items: &'a [TreeItem], id: Option<&str>) -> Option<&'a TreeItem
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The rows of `file`'s diff: one header row per hunk, then its lines.
|
||||||
|
fn diff_rows(file: &FileDiff) -> Vec<DiffRow> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
for (hunk_ix, hunk) in file.hunks.iter().enumerate() {
|
||||||
|
rows.push(DiffRow::Hunk {
|
||||||
|
old_start: hunk.old_start,
|
||||||
|
old_lines: hunk.old_lines,
|
||||||
|
new_start: hunk.new_start,
|
||||||
|
new_lines: hunk.new_lines,
|
||||||
|
});
|
||||||
|
rows.extend((0..hunk.lines.len()).map(|line| DiffRow::Line {
|
||||||
|
hunk: hunk_ix,
|
||||||
|
line,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
impl Panel for CommitDiffView {
|
impl Panel for CommitDiffView {
|
||||||
fn panel_name(&self) -> &'static str {
|
fn panel_name(&self) -> &'static str {
|
||||||
"commit_diff"
|
"commit_diff"
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::Error;
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{AnyElement, App, div};
|
use gpui::{AnyElement, App, Task, Window, div, px};
|
||||||
use gpui_component::tree::TreeItem;
|
use gpui_component::list::ListItem;
|
||||||
use gpui_component::{ActiveTheme, v_flex};
|
use gpui_component::tree::{TreeEntry, TreeItem};
|
||||||
|
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex};
|
||||||
|
|
||||||
/// A `Send` file-tree node: the tree is built on a background thread and
|
/// A `Send` file-tree node: the tree is built on a background thread and
|
||||||
/// converted into [`TreeItem`]s (which hold `Rc` state, so they cannot
|
/// converted into [`TreeItem`]s (which hold `Rc` state, so they cannot
|
||||||
@@ -20,14 +22,6 @@ pub(super) struct TreeItemSeed {
|
|||||||
children: Vec<TreeItemSeed>,
|
children: Vec<TreeItemSeed>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TreeItemSeed> for TreeItem {
|
|
||||||
fn from(seed: TreeItemSeed) -> Self {
|
|
||||||
let mut item = TreeItem::new(seed.id, seed.label);
|
|
||||||
item.children = seed.children.into_iter().map(Into::into).collect();
|
|
||||||
item
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert tree seeds into [`TreeItem`]s, expanding every folder when
|
/// Convert tree seeds into [`TreeItem`]s, expanding every folder when
|
||||||
/// `expand_folders` is set.
|
/// `expand_folders` is set.
|
||||||
///
|
///
|
||||||
@@ -54,6 +48,51 @@ pub(super) fn tree_items(seeds: Vec<TreeItemSeed>, expand_folders: bool) -> Vec<
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One row of a file tree: icon + name, indented by depth. Clicking a file
|
||||||
|
/// runs `on_click`; folders expand/collapse via the tree itself.
|
||||||
|
pub(super) fn tree_row<F>(ix: usize, entry: &TreeEntry, selected: bool, on_click: F) -> ListItem
|
||||||
|
where
|
||||||
|
F: Fn(&mut Window, &mut App) + 'static,
|
||||||
|
{
|
||||||
|
let item = entry.item();
|
||||||
|
let is_folder = entry.is_folder();
|
||||||
|
|
||||||
|
let icon = if is_folder {
|
||||||
|
if entry.is_expanded() {
|
||||||
|
IconName::FolderOpen
|
||||||
|
} else {
|
||||||
|
IconName::FolderClosed
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IconName::File
|
||||||
|
};
|
||||||
|
|
||||||
|
ListItem::new(ix)
|
||||||
|
.pl(px(8.) + px(14.) * entry.depth() as f32)
|
||||||
|
.selected(selected)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(Icon::new(icon).small())
|
||||||
|
.child(div().text_sm().text_ellipsis().child(item.label.clone())),
|
||||||
|
)
|
||||||
|
.on_click(move |_event, window, cx| {
|
||||||
|
// Folders expand/collapse via the tree itself.
|
||||||
|
if is_folder {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
on_click(window, cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track `task` until it completes; finished tasks are pruned on every push
|
||||||
|
/// so the vec stays bounded by the number of in-flight loads.
|
||||||
|
pub(super) fn track(tasks: &mut Vec<Task<Result<(), Error>>>, task: Task<Result<(), Error>>) {
|
||||||
|
tasks.retain(|task| !task.is_ready());
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
/// Build nested tree items from a flat, sorted (dirs-first) entry list.
|
/// Build nested tree items from a flat, sorted (dirs-first) entry list.
|
||||||
///
|
///
|
||||||
/// Returns [`TreeItemSeed`]s so the build can run off the main thread; a
|
/// Returns [`TreeItemSeed`]s so the build can run off the main thread; a
|
||||||
@@ -259,10 +298,7 @@ mod tests {
|
|||||||
PathBuf::from("README.md"),
|
PathBuf::from("README.md"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let items: Vec<TreeItem> = build_tree_items(&entries)
|
let items: Vec<TreeItem> = tree_items(build_tree_items(&entries), false);
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
|
||||||
.collect();
|
|
||||||
assert_eq!(items.len(), 2);
|
assert_eq!(items.len(), 2);
|
||||||
assert_eq!(items[0].label, "src");
|
assert_eq!(items[0].label, "src");
|
||||||
assert_eq!(items[0].children.len(), 1);
|
assert_eq!(items[0].children.len(), 1);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user