use crate::askpass_modal::AskPassModal;use crate::commit_modal::CommitModal;use crate::commit_tooltip::CommitTooltip;use crate::commit_view::CommitView;use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};use crate::remote_output::{self, RemoteAction, SuccessMessage};use crate::{branch_picker, picker_prompt, render_remote_button};use crate::{ file_history_view::FileHistoryView, git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,};use agent_settings::AgentSettings;use anyhow::Context as _;use askpass::AskPassDelegate;use cloud_llm_client::CompletionIntent;use collections::{BTreeMap, HashMap, HashSet};use db::kvp::KEY_VALUE_STORE;use editor::{ Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, actions::ExpandAllDiffHunks,};use editor::{EditorStyle, RewrapOptions};use file_icons::FileIcons;use futures::StreamExt as _;use git::commit::ParsedCommitMessage;use git::repository::{ Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer,};use git::stash::GitStash;use git::status::{DiffStat, StageStatus};use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus};use git::{ ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop, TrashUntrackedFiles, UnstageAll,};use gpui::{ Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, TextStyle, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, size, uniform_list,};use itertools::Itertools;use language::{Buffer, File};use language_model::{ ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,};use menu;use multi_buffer::ExcerptInfo;use notifications::status_toast::{StatusToast, ToastIcon};use panel::{PanelHeader, panel_button, panel_filled_button, panel_icon_button};use project::{ Fs, Project, ProjectPath, git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op}, project_settings::{GitPathStyle, ProjectSettings},};use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};use proto::RpcError;use serde::{Deserialize, Serialize};use settings::{Settings, SettingsStore, StatusStyle};use smallvec::SmallVec;use std::future::Future;use std::ops::Range;use std::path::Path;use std::{sync::Arc, time::Duration, usize};use strum::{IntoEnumIterator, VariantNames};use theme::ThemeSettings;use time::OffsetDateTime;use ui::{ ButtonLike, Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IndentGuideColors, PopoverMenu, RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*,};use util::paths::PathStyle;use util::{ResultExt, TryFutureExt, maybe, rel_path::RelPath};use workspace::SERIALIZATION_THROTTLE_TIME;use workspace::{ Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},}; actions!( git_panel, [ /// Closes the git panel. Close, /// Toggles the git panel. Toggle, /// Toggles focus on the git panel. ToggleFocus, /// Opens the git panel menu. OpenMenu, /// Focuses on the commit message editor. FocusEditor, /// Focuses on the changes list. FocusChanges, /// Select next git panel menu item, and show it in the diff view NextEntry, /// Select previous git panel menu item, and show it in the diff view PreviousEntry, /// Select first git panel menu item, and show it in the diff view FirstEntry, /// Select last git panel menu item, and show it in the diff view LastEntry, /// Toggles automatic co-author suggestions. ToggleFillCoAuthors, /// Toggles sorting entries by path vs status. ToggleSortByPath, /// Toggles showing entries in tree vs flat view. ToggleTreeView, /// Expands the selected entry to show its children. ExpandSelectedEntry, /// Collapses the selected entry to hide its children. CollapseSelectedEntry, ]); actions!( git_graph, [ /// Opens the Git Graph Tab. Open, ]); /// Opens the Git Graph Tab at a specific commit.#[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)]#[action(namespace = git_graph)]pub struct OpenAtCommit { pub sha: String,} fn prompt<T>( msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App,) -> Task<anyhow::Result<T>>where T: IntoEnumIterator + VariantNames + 'static,{ let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx); cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap()))} #[derive(strum::EnumIter, strum::VariantNames)]#[strum(serialize_all = "title_case")]enum TrashCancel { Trash, Cancel,} struct GitMenuState { has_tracked_changes: bool, has_staged_changes: bool, has_unstaged_changes: bool, has_new_changes: bool, sort_by_path: bool, has_stash_items: bool, tree_view: bool,} fn git_panel_context_menu( focus_handle: FocusHandle, state: GitMenuState, window: &mut Window, cx: &mut App,) -> Entity<ContextMenu> { ContextMenu::build(window, cx, move |context_menu, _, _| { context_menu .context(focus_handle) .action_disabled_when( !state.has_unstaged_changes, "Stage All", StageAll.boxed_clone(), ) .action_disabled_when( !state.has_staged_changes, "Unstage All", UnstageAll.boxed_clone(), ) .separator() .action_disabled_when( !(state.has_new_changes || state.has_tracked_changes), "Stash All", StashAll.boxed_clone(), ) .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone()) .action("View Stash", zed_actions::git::ViewStash.boxed_clone()) .separator() .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() .action_disabled_when( !state.has_tracked_changes, "Discard Tracked Changes", RestoreTrackedFiles.boxed_clone(), ) .action_disabled_when( !state.has_new_changes, "Trash Untracked Files", TrashUntrackedFiles.boxed_clone(), ) .separator() .entry( if state.tree_view { "Flat View" } else { "Tree View" }, Some(Box::new(ToggleTreeView)), move |window, cx| window.dispatch_action(Box::new(ToggleTreeView), cx), ) .when(!state.tree_view, |this| { this.entry( if state.sort_by_path { "Sort by Status" } else { "Sort by Path" }, Some(Box::new(ToggleSortByPath)), move |window, cx| window.dispatch_action(Box::new(ToggleSortByPath), cx), ) }) })} const GIT_PANEL_KEY: &str = "GitPanel"; const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);// TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panelconst TREE_INDENT: f32 = 16.0; pub fn register(workspace: &mut Workspace) { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::<GitPanel>(window, cx); }); workspace.register_action(|workspace, _: &Toggle, window, cx| { if !workspace.toggle_panel_focus::<GitPanel>(window, cx) { workspace.close_panel::<GitPanel>(window, cx); } }); workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| { CommitModal::toggle(workspace, None, window, cx) }); workspace.register_action(|workspace, _: &git::Init, window, cx| { if let Some(panel) = workspace.panel::<GitPanel>(cx) { panel.update(cx, |panel, cx| panel.git_init(window, cx)); } });} #[derive(Debug, Clone)]pub enum Event { Focus,} #[derive(Serialize, Deserialize)]struct SerializedGitPanel { width: Option<Pixels>, #[serde(default)] amend_pending: bool, #[serde(default)] signoff_enabled: bool,} #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]enum Section { Conflict, Tracked, New,} #[derive(Debug, PartialEq, Eq, Clone)]struct GitHeaderEntry { header: Section,} impl GitHeaderEntry { pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool { let this = &self.header; let status = status_entry.status; match this { Section::Conflict => { repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) } Section::Tracked => !status.is_created(), Section::New => status.is_created(), } } pub fn title(&self) -> &'static str { match self.header { Section::Conflict => "Conflicts", Section::Tracked => "Tracked", Section::New => "Untracked", } }} #[derive(Debug, PartialEq, Eq, Clone)]enum GitListEntry { Status(GitStatusEntry), TreeStatus(GitTreeStatusEntry), Directory(GitTreeDirEntry), Header(GitHeaderEntry),} impl GitListEntry { fn status_entry(&self) -> Option<&GitStatusEntry> { match self { GitListEntry::Status(entry) => Some(entry), GitListEntry::TreeStatus(entry) => Some(&entry.entry), _ => None, } } fn directory_entry(&self) -> Option<&GitTreeDirEntry> { match self { GitListEntry::Directory(entry) => Some(entry), _ => None, } } /// Returns the tree indentation depth for this entry. fn depth(&self) -> usize { match self { GitListEntry::Directory(dir) => dir.depth, GitListEntry::TreeStatus(status) => status.depth, _ => 0, } }} enum GitPanelViewMode { Flat, Tree(TreeViewState),} impl GitPanelViewMode { fn from_settings(cx: &App) -> Self { if GitPanelSettings::get_global(cx).tree_view { GitPanelViewMode::Tree(TreeViewState::default()) } else { GitPanelViewMode::Flat } } fn tree_state(&self) -> Option<&TreeViewState> { match self { GitPanelViewMode::Tree(state) => Some(state), GitPanelViewMode::Flat => None, } } fn tree_state_mut(&mut self) -> Option<&mut TreeViewState> { match self { GitPanelViewMode::Tree(state) => Some(state), GitPanelViewMode::Flat => None, } }} #[derive(Default)]struct TreeViewState { // Maps visible index to actual entry index. // Length equals the number of visible entries. // This is needed because some entries (like collapsed directories) may be hidden. logical_indices: Vec<usize>, expanded_dirs: HashMap<TreeKey, bool>, directory_descendants: HashMap<TreeKey, Vec<GitStatusEntry>>,} impl TreeViewState { fn build_tree_entries( &mut self, section: Section, mut entries: Vec<GitStatusEntry>, seen_directories: &mut HashSet<TreeKey>, ) -> Vec<(GitListEntry, bool)> { if entries.is_empty() { return Vec::new(); } entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path)); let mut root = TreeNode::default(); for entry in entries { let components: Vec<&str> = entry.repo_path.components().collect(); if components.is_empty() { root.files.push(entry); continue; } let mut current = &mut root; let mut current_path = String::new(); for (ix, component) in components.iter().enumerate() { if ix == components.len() - 1 { current.files.push(entry.clone()); } else { if !current_path.is_empty() { current_path.push('/'); } current_path.push_str(component); let dir_path = RepoPath::new(¤t_path) .expect("repo path from status entry component"); let component = SharedString::from(component.to_string()); current = current .children .entry(component.clone()) .or_insert_with(|| TreeNode { name: component, path: Some(dir_path), ..Default::default() }); } } } let (flattened, _) = self.flatten_tree(&root, section, 0, seen_directories); flattened } fn flatten_tree( &mut self, node: &TreeNode, section: Section, depth: usize, seen_directories: &mut HashSet<TreeKey>, ) -> (Vec<(GitListEntry, bool)>, Vec<GitStatusEntry>) { let mut all_statuses = Vec::new(); let mut flattened = Vec::new(); for child in node.children.values() { let (terminal, name) = Self::compact_directory_chain(child); let Some(path) = terminal.path.clone().or_else(|| child.path.clone()) else { continue; }; let (child_flattened, mut child_statuses) = self.flatten_tree(terminal, section, depth + 1, seen_directories); let key = TreeKey { section, path }; let expanded = *self.expanded_dirs.get(&key).unwrap_or(&true); self.expanded_dirs.entry(key.clone()).or_insert(true); seen_directories.insert(key.clone()); self.directory_descendants .insert(key.clone(), child_statuses.clone()); flattened.push(( GitListEntry::Directory(GitTreeDirEntry { key, name, depth, expanded, }), true, )); if expanded { flattened.extend(child_flattened); } else { flattened.extend(child_flattened.into_iter().map(|(child, _)| (child, false))); } all_statuses.append(&mut child_statuses); } for file in &node.files { all_statuses.push(file.clone()); flattened.push(( GitListEntry::TreeStatus(GitTreeStatusEntry { entry: file.clone(), depth, }), true, )); } (flattened, all_statuses) } fn compact_directory_chain(mut node: &TreeNode) -> (&TreeNode, SharedString) { let mut parts = vec![node.name.clone()]; while node.files.is_empty() && node.children.len() == 1 { let Some(child) = node.children.values().next() else { continue; }; if child.path.is_none() { break; } parts.push(child.name.clone()); node = child; } let name = parts.join("/"); (node, SharedString::from(name)) }}Lines 1–500 of 7853. Load more
use crate::askpass_modal::AskPassModal; use crate::commit_modal::CommitModal; use crate::commit_tooltip::CommitTooltip; use crate::commit_view::CommitView; use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff}; use crate::remote_output::{self, RemoteAction, SuccessMessage}; use crate::{branch_picker, picker_prompt, render_remote_button}; use crate::{ file_history_view::FileHistoryView, git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; use agent_settings::AgentSettings; use anyhow::Context as _; use askpass::AskPassDelegate; use cloud_llm_client::CompletionIntent; use collections::{BTreeMap, HashMap, HashSet}; use db::kvp::KEY_VALUE_STORE; use editor::{ Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, actions::ExpandAllDiffHunks, }; use editor::{EditorStyle, RewrapOptions}; use file_icons::FileIcons; use futures::StreamExt as _; use git::commit::ParsedCommitMessage; use git::repository::{ Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter, PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus, get_git_committer, }; use git::stash::GitStash; use git::status::{DiffStat, StageStatus}; use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus}; use git::{ ExpandCommitEditor, GitHostingProviderRegistry, RestoreTrackedFiles, StageAll, StashAll, StashApply, StashPop, TrashUntrackedFiles, UnstageAll, }; use gpui::{ Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, TextStyle, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, size, uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; use language_model::{ ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, }; use menu; use multi_buffer::ExcerptInfo; use notifications::status_toast::{StatusToast, ToastIcon}; use panel::{PanelHeader, panel_button, panel_filled_button, panel_icon_button}; use project::{ Fs, Project, ProjectPath, git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op}, project_settings::{GitPathStyle, ProjectSettings}, }; use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES}; use proto::RpcError; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore, StatusStyle}; use smallvec::SmallVec; use std::future::Future; use std::ops::Range; use std::path::Path; use std::{sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use theme::ThemeSettings; use time::OffsetDateTime; use ui::{ ButtonLike, Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IndentGuideColors, PopoverMenu, RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tooltip, WithScrollbar, prelude::*, }; use util::paths::PathStyle; use util::{ResultExt, TryFutureExt, maybe, rel_path::RelPath}; use workspace::SERIALIZATION_THROTTLE_TIME; use workspace::{ Workspace, dock::{DockPosition, Panel, PanelEvent}, notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt}, }; actions!( git_panel, [ /// Closes the git panel. Close, /// Toggles the git panel. Toggle, /// Toggles focus on the git panel. ToggleFocus, /// Opens the git panel menu. OpenMenu, /// Focuses on the commit message editor. FocusEditor, /// Focuses on the changes list. FocusChanges, /// Select next git panel menu item, and show it in the diff view NextEntry, /// Select previous git panel menu item, and show it in the diff view PreviousEntry, /// Select first git panel menu item, and show it in the diff view FirstEntry, /// Select last git panel menu item, and show it in the diff view LastEntry, /// Toggles automatic co-author suggestions. ToggleFillCoAuthors, /// Toggles sorting entries by path vs status. ToggleSortByPath, /// Toggles showing entries in tree vs flat view. ToggleTreeView, /// Expands the selected entry to show its children. ExpandSelectedEntry, /// Collapses the selected entry to hide its children. CollapseSelectedEntry, ] ); actions!( git_graph, [ /// Opens the Git Graph Tab. Open, ] ); /// Opens the Git Graph Tab at a specific commit. #[derive(Clone, PartialEq, serde::Deserialize, schemars::JsonSchema, gpui::Action)] #[action(namespace = git_graph)] pub struct OpenAtCommit { pub sha: String, } fn prompt<T>( msg: &str, detail: Option<&str>, window: &mut Window, cx: &mut App, ) -> Task<anyhow::Result<T>> where T: IntoEnumIterator + VariantNames + 'static, { let rx = window.prompt(PromptLevel::Info, msg, detail, T::VARIANTS, cx); cx.spawn(async move |_| Ok(T::iter().nth(rx.await?).unwrap())) } #[derive(strum::EnumIter, strum::VariantNames)] #[strum(serialize_all = "title_case")] enum TrashCancel { Trash, Cancel, } struct GitMenuState { has_tracked_changes: bool, has_staged_changes: bool, has_unstaged_changes: bool, has_new_changes: bool, sort_by_path: bool, has_stash_items: bool, tree_view: bool, } fn git_panel_context_menu( focus_handle: FocusHandle, state: GitMenuState, window: &mut Window, cx: &mut App, ) -> Entity<ContextMenu> { ContextMenu::build(window, cx, move |context_menu, _, _| { context_menu .context(focus_handle) .action_disabled_when( !state.has_unstaged_changes, "Stage All", StageAll.boxed_clone(), ) .action_disabled_when( !state.has_staged_changes, "Unstage All", UnstageAll.boxed_clone(), ) .separator() .action_disabled_when( !(state.has_new_changes || state.has_tracked_changes), "Stash All", StashAll.boxed_clone(), ) .action_disabled_when(!state.has_stash_items, "Stash Pop", StashPop.boxed_clone()) .action("View Stash", zed_actions::git::ViewStash.boxed_clone()) .separator() .action("Open Diff", project_diff::Diff.boxed_clone()) .separator() .action_disabled_when( !state.has_tracked_changes, "Discard Tracked Changes", RestoreTrackedFiles.boxed_clone(), ) .action_disabled_when( !state.has_new_changes, "Trash Untracked Files", TrashUntrackedFiles.boxed_clone(), ) .separator() .entry( if state.tree_view { "Flat View" } else { "Tree View" }, Some(Box::new(ToggleTreeView)), move |window, cx| window.dispatch_action(Box::new(ToggleTreeView), cx), ) .when(!state.tree_view, |this| { this.entry( if state.sort_by_path { "Sort by Status" } else { "Sort by Path" }, Some(Box::new(ToggleSortByPath)), move |window, cx| window.dispatch_action(Box::new(ToggleSortByPath), cx), ) }) }) } const GIT_PANEL_KEY: &str = "GitPanel"; const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); // TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel const TREE_INDENT: f32 = 16.0; pub fn register(workspace: &mut Workspace) { workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { workspace.toggle_panel_focus::<GitPanel>(window, cx); }); workspace.register_action(|workspace, _: &Toggle, window, cx| { if !workspace.toggle_panel_focus::<GitPanel>(window, cx) { workspace.close_panel::<GitPanel>(window, cx); } }); workspace.register_action(|workspace, _: &ExpandCommitEditor, window, cx| { CommitModal::toggle(workspace, None, window, cx) }); workspace.register_action(|workspace, _: &git::Init, window, cx| { if let Some(panel) = workspace.panel::<GitPanel>(cx) { panel.update(cx, |panel, cx| panel.git_init(window, cx)); } }); } #[derive(Debug, Clone)] pub enum Event { Focus, } #[derive(Serialize, Deserialize)] struct SerializedGitPanel { width: Option<Pixels>, #[serde(default)] amend_pending: bool, #[serde(default)] signoff_enabled: bool, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] enum Section { Conflict, Tracked, New, } #[derive(Debug, PartialEq, Eq, Clone)] struct GitHeaderEntry { header: Section, } impl GitHeaderEntry { pub fn contains(&self, status_entry: &GitStatusEntry, repo: &Repository) -> bool { let this = &self.header; let status = status_entry.status; match this { Section::Conflict => { repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) } Section::Tracked => !status.is_created(), Section::New => status.is_created(), } } pub fn title(&self) -> &'static str { match self.header { Section::Conflict => "Conflicts", Section::Tracked => "Tracked", Section::New => "Untracked", } } } #[derive(Debug, PartialEq, Eq, Clone)] enum GitListEntry { Status(GitStatusEntry), TreeStatus(GitTreeStatusEntry), Directory(GitTreeDirEntry), Header(GitHeaderEntry), } impl GitListEntry { fn status_entry(&self) -> Option<&GitStatusEntry> { match self { GitListEntry::Status(entry) => Some(entry), GitListEntry::TreeStatus(entry) => Some(&entry.entry), _ => None, } } fn directory_entry(&self) -> Option<&GitTreeDirEntry> { match self { GitListEntry::Directory(entry) => Some(entry), _ => None, } } /// Returns the tree indentation depth for this entry. fn depth(&self) -> usize { match self { GitListEntry::Directory(dir) => dir.depth, GitListEntry::TreeStatus(status) => status.depth, _ => 0, } } } enum GitPanelViewMode { Flat, Tree(TreeViewState), } impl GitPanelViewMode { fn from_settings(cx: &App) -> Self { if GitPanelSettings::get_global(cx).tree_view { GitPanelViewMode::Tree(TreeViewState::default()) } else { GitPanelViewMode::Flat } } fn tree_state(&self) -> Option<&TreeViewState> { match self { GitPanelViewMode::Tree(state) => Some(state), GitPanelViewMode::Flat => None, } } fn tree_state_mut(&mut self) -> Option<&mut TreeViewState> { match self { GitPanelViewMode::Tree(state) => Some(state), GitPanelViewMode::Flat => None, } } } #[derive(Default)] struct TreeViewState { // Maps visible index to actual entry index. // Length equals the number of visible entries. // This is needed because some entries (like collapsed directories) may be hidden. logical_indices: Vec<usize>, expanded_dirs: HashMap<TreeKey, bool>, directory_descendants: HashMap<TreeKey, Vec<GitStatusEntry>>, } impl TreeViewState { fn build_tree_entries( &mut self, section: Section, mut entries: Vec<GitStatusEntry>, seen_directories: &mut HashSet<TreeKey>, ) -> Vec<(GitListEntry, bool)> { if entries.is_empty() { return Vec::new(); } entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path)); let mut root = TreeNode::default(); for entry in entries { let components: Vec<&str> = entry.repo_path.components().collect(); if components.is_empty() { root.files.push(entry); continue; } let mut current = &mut root; let mut current_path = String::new(); for (ix, component) in components.iter().enumerate() { if ix == components.len() - 1 { current.files.push(entry.clone()); } else { if !current_path.is_empty() { current_path.push('/'); } current_path.push_str(component); let dir_path = RepoPath::new(¤t_path) .expect("repo path from status entry component"); let component = SharedString::from(component.to_string()); current = current .children .entry(component.clone()) .or_insert_with(|| TreeNode { name: component, path: Some(dir_path), ..Default::default() }); } } } let (flattened, _) = self.flatten_tree(&root, section, 0, seen_directories); flattened } fn flatten_tree( &mut self, node: &TreeNode, section: Section, depth: usize, seen_directories: &mut HashSet<TreeKey>, ) -> (Vec<(GitListEntry, bool)>, Vec<GitStatusEntry>) { let mut all_statuses = Vec::new(); let mut flattened = Vec::new(); for child in node.children.values() { let (terminal, name) = Self::compact_directory_chain(child); let Some(path) = terminal.path.clone().or_else(|| child.path.clone()) else { continue; }; let (child_flattened, mut child_statuses) = self.flatten_tree(terminal, section, depth + 1, seen_directories); let key = TreeKey { section, path }; let expanded = *self.expanded_dirs.get(&key).unwrap_or(&true); self.expanded_dirs.entry(key.clone()).or_insert(true); seen_directories.insert(key.clone()); self.directory_descendants .insert(key.clone(), child_statuses.clone()); flattened.push(( GitListEntry::Directory(GitTreeDirEntry { key, name, depth, expanded, }), true, )); if expanded { flattened.extend(child_flattened); } else { flattened.extend(child_flattened.into_iter().map(|(child, _)| (child, false))); } all_statuses.append(&mut child_statuses); } for file in &node.files { all_statuses.push(file.clone()); flattened.push(( GitListEntry::TreeStatus(GitTreeStatusEntry { entry: file.clone(), depth, }), true, )); } (flattened, all_statuses) } fn compact_directory_chain(mut node: &TreeNode) -> (&TreeNode, SharedString) { let mut parts = vec![node.name.clone()]; while node.files.is_empty() && node.children.len() == 1 { let Some(child) = node.children.values().next() else { continue; }; if child.path.is_none() { break; } parts.push(child.name.clone()); node = child; } let name = parts.join("/"); (node, SharedString::from(name)) } }