Fix performance of GitPanel::update_visible_entries (#25504)

Co-authored-by: Anthony Eid <hello@anthonyeid.me>

Closes #19022

Release Notes:

- Fixes pessimal performance with the new git panel when a very large
number of files are untracked

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
Conrad Irwin 2025-02-24 15:55:44 -07:00 committed by GitHub
parent def342e35c
commit 53a5145dc8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -5,7 +5,6 @@ use crate::{
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
use crate::{picker_prompt, project_diff, ProjectDiff};
use collections::HashMap;
use db::kvp::KEY_VALUE_STORE;
use editor::commit_tooltip::CommitTooltip;
use editor::{
@ -30,7 +29,7 @@ use settings::Settings as _;
use std::cell::RefCell;
use std::future::Future;
use std::rc::Rc;
use std::{collections::HashSet, path::PathBuf, sync::Arc, time::Duration, usize};
use std::{collections::HashSet, sync::Arc, time::Duration, usize};
use strum::{IntoEnumIterator, VariantNames};
use time::OffsetDateTime;
use ui::{
@ -155,8 +154,6 @@ impl GitListEntry {
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GitStatusEntry {
pub(crate) depth: usize,
pub(crate) display_name: String,
pub(crate) repo_path: RepoPath,
pub(crate) status: FileStatus,
pub(crate) is_staged: Option<bool>,
@ -190,7 +187,6 @@ pub struct GitPanel {
current_modifiers: Modifiers,
add_coauthors: bool,
entries: Vec<GitListEntry>,
entries_by_path: collections::HashMap<RepoPath, usize>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
hide_scrollbar_task: Option<Task<()>>,
@ -315,7 +311,6 @@ impl GitPanel {
current_modifiers: window.modifiers(),
add_coauthors: true,
entries: Vec::new(),
entries_by_path: HashMap::default(),
focus_handle: cx.focus_handle(),
fs,
hide_scrollbar_task: None,
@ -344,6 +339,80 @@ impl GitPanel {
})
}
pub fn entry_by_path(&self, path: &RepoPath) -> Option<usize> {
fn binary_search<F>(mut low: usize, mut high: usize, is_target: F) -> Option<usize>
where
F: Fn(usize) -> std::cmp::Ordering,
{
while low < high {
let mid = low + (high - low) / 2;
match is_target(mid) {
std::cmp::Ordering::Equal => return Some(mid),
std::cmp::Ordering::Less => low = mid + 1,
std::cmp::Ordering::Greater => high = mid,
}
}
None
}
if self.conflicted_count > 0 {
let conflicted_start = 1;
if let Some(ix) = binary_search(
conflicted_start,
conflicted_start + self.conflicted_count,
|ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
},
) {
return Some(ix);
}
}
if self.tracked_count > 0 {
let tracked_start = if self.conflicted_count > 0 {
1 + self.conflicted_count
} else {
0
} + 1;
if let Some(ix) =
binary_search(tracked_start, tracked_start + self.tracked_count, |ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
})
{
return Some(ix);
}
}
if self.new_count > 0 {
let untracked_start = if self.conflicted_count > 0 {
1 + self.conflicted_count
} else {
0
} + if self.tracked_count > 0 {
1 + self.tracked_count
} else {
0
} + 1;
if let Some(ix) =
binary_search(untracked_start, untracked_start + self.new_count, |ix| {
self.entries[ix]
.status_entry()
.unwrap()
.repo_path
.cmp(&path)
})
{
return Some(ix);
}
}
None
}
pub fn select_entry_by_path(
&mut self,
path: ProjectPath,
@ -356,10 +425,10 @@ impl GitPanel {
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
return;
};
let Some(ix) = self.entries_by_path.get(&repo_path) else {
let Some(ix) = self.entry_by_path(&repo_path) else {
return;
};
self.selected_entry = Some(*ix);
self.selected_entry = Some(ix);
cx.notify();
}
@ -483,31 +552,6 @@ impl GitPanel {
cx.notify();
}
fn calculate_depth_and_difference(
repo_path: &RepoPath,
visible_entries: &HashSet<RepoPath>,
) -> (usize, usize) {
let ancestors = repo_path.ancestors().skip(1);
for ancestor in ancestors {
if let Some(parent_entry) = visible_entries.get(ancestor) {
let entry_component_count = repo_path.components().count();
let parent_component_count = parent_entry.components().count();
let difference = entry_component_count - parent_component_count;
let parent_depth = parent_entry
.ancestors()
.skip(1) // Skip the parent itself
.filter(|ancestor| visible_entries.contains(*ancestor))
.count();
return (parent_depth + 1, difference);
}
}
(0, 0)
}
fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) {
if let Some(selected_entry) = self.selected_entry {
self.scroll_handle
@ -1548,7 +1592,6 @@ impl GitPanel {
fn update_visible_entries(&mut self, cx: &mut Context<Self>) {
self.entries.clear();
self.entries_by_path.clear();
let mut changed_entries = Vec::new();
let mut new_entries = Vec::new();
let mut conflict_entries = Vec::new();
@ -1561,13 +1604,9 @@ impl GitPanel {
// First pass - collect all paths
let repo = repo.read(cx);
let path_set = HashSet::from_iter(repo.status().map(|entry| entry.repo_path));
// Second pass - create entries with proper depth calculation
for entry in repo.status() {
let (depth, difference) =
Self::calculate_depth_and_difference(&entry.repo_path, &path_set);
let is_conflict = repo.has_conflict(&entry.repo_path);
let is_new = entry.status.is_created();
let is_staged = entry.status.is_staged();
@ -1580,28 +1619,7 @@ impl GitPanel {
continue;
}
let display_name = if difference > 1 {
// Show partial path for deeply nested files
entry
.repo_path
.as_ref()
.iter()
.skip(entry.repo_path.components().count() - difference)
.collect::<PathBuf>()
.to_string_lossy()
.into_owned()
} else {
// Just show filename
entry
.repo_path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default()
};
let entry = GitStatusEntry {
depth,
display_name,
repo_path: entry.repo_path.clone(),
status: entry.status,
is_staged,
@ -1616,11 +1634,6 @@ impl GitPanel {
}
}
// Sort entries by path to maintain consistent order
conflict_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
changed_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
new_entries.sort_by(|a, b| a.repo_path.cmp(&b.repo_path));
if conflict_entries.len() > 0 {
self.entries.push(GitListEntry::Header(GitHeaderEntry {
header: Section::Conflict,
@ -1650,12 +1663,6 @@ impl GitPanel {
.extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry));
}
for (ix, entry) in self.entries.iter().enumerate() {
if let Some(status_entry) = entry.status_entry() {
self.entries_by_path
.insert(status_entry.repo_path.clone(), ix);
}
}
self.update_counts(repo);
self.select_first_entry_if_none(cx);
@ -2211,8 +2218,8 @@ impl GitPanel {
) -> Option<AnyElement> {
let repo = self.active_repository.as_ref()?.read(cx);
let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
let ix = self.entries_by_path.get(&repo_path)?;
let entry = self.entries.get(*ix)?;
let ix = self.entry_by_path(&repo_path)?;
let entry = self.entries.get(ix)?;
let is_staged = self.entry_is_staged(entry.status_entry()?);