use std::collections::HashMap;
use std::collections::HashSet;
use std::ops::Index;
use std::path::Path;
use indexmap::IndexMap;
use indexmap::map::Values;
use indexmap::map::ValuesMut;
use super::app::CiRunDisplayMode;
use super::app::CleanSelection;
use super::app::FinderState;
use super::app::SelectionPaths;
use super::app::SelectionSync;
use super::columns::ProjectListWidths;
use super::pane::DismissTarget;
use super::state::Ci;
use super::state::CiStatusLookup;
use crate::ci;
use crate::ci::CiRun;
use crate::ci::CiStatus;
use crate::ci::OwnerRepo;
use crate::constants::IN_SYNC;
use crate::constants::NO_REMOTE_SYNC;
use crate::constants::SYNC_DOWN;
use crate::constants::SYNC_UP;
use crate::lint::LintRuns;
use crate::project;
use crate::project::AbsolutePath;
use crate::project::Cargo;
use crate::project::CheckoutInfo;
use crate::project::DisplayPath;
use crate::project::GitHubInfo;
use crate::project::GitRepo;
use crate::project::GitStatus;
use crate::project::LanguageStats;
use crate::project::ProjectCiData;
use crate::project::ProjectCiInfo;
use crate::project::ProjectEntry;
use crate::project::ProjectFields;
use crate::project::ProjectInfo;
use crate::project::ProjectPrData;
use crate::project::ProjectPrInfo;
use crate::project::RepoInfo;
use crate::project::RootItem;
use crate::project::RustInfo;
use crate::project::RustProject;
use crate::project::TestCounts;
use crate::project::VendoredPackage;
use crate::project::Visibility;
use crate::project::WorkspaceMetadata;
use crate::project::WorktreeGroup;
use crate::project::WorktreeStatus;
mod grouping;
mod selection;
mod visible_rows;
use grouping::find_matching_worktree_container;
use grouping::linked_worktree_identity;
use grouping::regroup_workspace;
use grouping::shortest_unique_suffixes;
use grouping::try_attach_worktree;
use grouping::try_insert_member;
pub(super) use selection::SelectionMutation;
pub(super) use visible_rows::ExpandKey;
pub(super) use visible_rows::LegacyRootExpansion;
pub(super) use visible_rows::VisibleRow;
use visible_rows::worst_git_status;
pub(super) enum SyncResolution {
Unresolved,
Resolved(Option<(usize, usize)>),
}
fn project_lint_is_running(project: &RustProject) -> bool {
project.visibility() == Visibility::Visible
&& matches!(
project.lint_at_path(project.path()).map(LintRuns::status),
Some(crate::lint::LintStatus::Running(_))
)
}
#[derive(Default)]
pub(crate) struct ProjectList {
roots: IndexMap<AbsolutePath, ProjectEntry>,
pub(super) paths: SelectionPaths,
sync: SelectionSync,
pub(super) expanded: HashSet<ExpandKey>,
pub(super) finder: FinderState,
cached_visible_rows: Vec<VisibleRow>,
pub(super) cached_root_sorted: Vec<u64>,
pub(super) cached_child_sorted: HashMap<usize, Vec<u64>>,
pub(super) cached_fit_widths: ProjectListWidths,
cursor: usize,
}
impl ProjectList {
pub(super) fn new(items: Vec<RootItem>) -> Self {
Self {
roots: items
.into_iter()
.map(|item| {
let entry = ProjectEntry::new(item);
(entry.item.path().clone(), entry)
})
.collect(),
..Self::default()
}
}
pub(super) fn init_runtime_state(&mut self, lint_enabled: bool) {
self.paths = SelectionPaths::new();
self.cached_fit_widths = ProjectListWidths::new(lint_enabled);
}
pub(super) fn len(&self) -> usize { self.roots.len() }
pub(super) fn is_empty(&self) -> bool { self.roots.is_empty() }
pub(super) fn iter(&self) -> Values<'_, AbsolutePath, ProjectEntry> { self.roots.values() }
pub(super) fn replace_roots_from(&mut self, replacement: Self) {
self.roots = replacement.roots;
}
pub(super) fn iter_with_expanded_mut(
&mut self,
) -> (
Values<'_, AbsolutePath, ProjectEntry>,
&mut HashSet<ExpandKey>,
) {
let Self {
roots, expanded, ..
} = self;
(roots.values(), expanded)
}
#[cfg(test)]
pub(super) fn first(&self) -> Option<&ProjectEntry> {
self.roots.first().map(|(_, entry)| entry)
}
pub(super) fn get(&self, index: usize) -> Option<&ProjectEntry> {
self.roots.get_index(index).map(|(_, entry)| entry)
}
pub(super) fn resolved_root_labels(&self, include_non_rust: bool) -> Vec<String> {
let mut labels: Vec<String> = self
.roots
.values()
.map(|entry| entry.item.root_directory_name().into_string())
.collect();
let mut collision_sets: HashMap<String, Vec<usize>> = HashMap::new();
for (index, entry) in self.roots.values().enumerate() {
if matches!(entry.item.visibility(), Visibility::Dismissed) {
continue;
}
if !include_non_rust && !entry.item.is_rust() {
continue;
}
collision_sets
.entry(entry.item.root_directory_name().into_string())
.or_default()
.push(index);
}
for indices in collision_sets
.into_values()
.filter(|indices| indices.len() > 1)
{
let suffixes = shortest_unique_suffixes(
&indices
.iter()
.map(|&index| self.roots[index].item.display_path().into_string())
.collect::<Vec<_>>(),
);
for (index, suffix) in indices.into_iter().zip(suffixes) {
labels[index] = format!("{} [{suffix}]", labels[index]);
}
}
for (label, entry) in labels.iter_mut().zip(self.roots.values()) {
if let Some(suffix) = entry.item.worktree_badge_suffix() {
label.push_str(&suffix);
}
}
labels
}
pub(super) fn git_directories(&self) -> Vec<AbsolutePath> {
self.roots
.values()
.filter_map(|entry| entry.item.git_directory())
.collect()
}
pub(super) fn for_each_leaf(&self, mut f: impl FnMut(&ProjectEntry)) {
for entry in self.roots.values() {
match &entry.item {
RootItem::Worktrees(group) => {
for project in group.iter_entries() {
f(&ProjectEntry::with_repo(
RootItem::Rust(project.clone()),
entry.git_repo.clone(),
));
}
},
_ => f(entry),
}
}
}
pub(super) fn for_each_leaf_path(&self, mut f: impl FnMut(&Path, bool)) {
for entry in self.roots.values() {
match &entry.item {
RootItem::Worktrees(group) => {
for project in group.iter_entries() {
f(project.path(), true);
}
},
other => f(other.path(), other.is_rust()),
}
}
}
pub(super) fn at_path(&self, target: &Path) -> Option<&ProjectInfo> {
if let Some(entry) = self.roots.get(target) {
return entry.item.at_path(target);
}
self.roots
.values()
.find_map(|entry| entry.item.at_path(target))
}
pub(super) fn at_path_mut(&mut self, target: &Path) -> Option<&mut ProjectInfo> {
if self.roots.contains_key(target) {
return self
.roots
.get_mut(target)
.and_then(|entry| entry.item.at_path_mut(target));
}
self.roots
.values_mut()
.find_map(|entry| entry.item.at_path_mut(target))
}
pub(super) fn is_submodule_path(&self, target: &Path) -> bool {
self.roots.values().any(|entry| {
entry
.item
.submodules()
.iter()
.any(|s| s.path.as_path() == target)
})
}
pub(super) fn rust_info_at_path(&self, target: &Path) -> Option<&RustInfo> {
self.roots
.values()
.find_map(|entry| entry.item.rust_info_at_path(target))
}
pub(super) fn rust_info_at_path_mut(&mut self, target: &Path) -> Option<&mut RustInfo> {
self.roots
.values_mut()
.find_map(|entry| entry.item.rust_info_at_path_mut(target))
}
pub(super) fn vendored_at_path(&self, target: &Path) -> Option<&VendoredPackage> {
self.roots
.values()
.find_map(|entry| entry.item.vendored_at_path(target))
}
pub(super) fn vendored_at_path_mut(&mut self, target: &Path) -> Option<&mut VendoredPackage> {
self.roots
.values_mut()
.find_map(|entry| entry.item.vendored_at_path_mut(target))
}
pub(super) fn vendored_owner_lint(&self, target: &Path) -> Option<&LintRuns> {
self.roots
.values()
.find_map(|entry| entry.item.vendored_owner_lint(target))
}
pub(super) fn lint_at_path(&self, target: &Path) -> Option<&LintRuns> {
self.roots
.values()
.find_map(|entry| entry.item.lint_at_path(target))
}
pub(super) fn lint_at_path_mut(&mut self, target: &Path) -> Option<&mut LintRuns> {
self.roots
.values_mut()
.find_map(|entry| entry.item.lint_at_path_mut(target))
}
pub(super) fn lint_owner_path(&self, target: &Path) -> Option<AbsolutePath> {
self.roots
.values()
.find_map(|entry| entry.item.lint_owner_path(target))
.cloned()
}
pub(super) fn checkout_root_for(&self, target: &Path) -> Option<AbsolutePath> {
self.roots
.values()
.find_map(|entry| entry.item.checkout_root_for(target))
.cloned()
}
pub(super) fn has_running_lints(&self) -> bool {
self.roots.values().any(|entry| match &entry.item {
RootItem::Rust(project) => project_lint_is_running(project),
RootItem::Worktrees(group) => group.iter_entries().any(project_lint_is_running),
RootItem::NonRust(_) => false,
})
}
pub(super) fn running_lint_paths(&self) -> Vec<AbsolutePath> {
let mut paths = Vec::new();
for entry in self.roots.values() {
match &entry.item {
RootItem::Rust(project) => {
if project_lint_is_running(project) {
paths.push(project.path().clone());
}
},
RootItem::Worktrees(group) => {
paths.extend(group.iter_entries().filter_map(|project| {
if project_lint_is_running(project) {
Some(project.path().clone())
} else {
None
}
}));
},
RootItem::NonRust(_) => {},
}
}
paths
}
pub(super) fn entry_containing(&self, target: &Path) -> Option<&ProjectEntry> {
self.roots
.values()
.find(|entry| project::entry_contains(entry, target))
}
pub(super) fn worktree_status_for(&self, path: &Path) -> Option<&WorktreeStatus> {
self.entry_containing(path)?.item.worktree_status_at(path)
}
pub(super) fn entry_containing_mut(&mut self, target: &Path) -> Option<&mut ProjectEntry> {
self.roots
.values_mut()
.find(|entry| project::entry_contains(entry, target))
}
pub(super) fn replace_ci_data_for_path(&mut self, path: &Path, ci_data: ProjectCiData) {
if let Some(repo) = self.git_repo_for_mut(path) {
repo.ci_data = ci_data;
}
}
pub(super) fn replace_pr_data_for_repo(
&mut self,
owner_repo: &OwnerRepo,
data: &ProjectPrData,
) {
let targets: Vec<AbsolutePath> = self
.roots
.values()
.filter_map(|entry| {
let url = self.fetch_url_for(entry.item.path())?;
(ci::parse_owner_repo(&url).as_ref() == Some(owner_repo))
.then(|| entry.item.path().clone())
})
.collect();
for path in targets {
if let Some(repo) = self.git_repo_for_mut(path.as_path()) {
repo.pr_data = data.clone();
}
}
}
pub(super) fn pr_info_for_repo(&self, owner_repo: &OwnerRepo) -> Option<&ProjectPrInfo> {
self.roots.values().find_map(|entry| {
let url = self.fetch_url_for(entry.item.path())?;
(ci::parse_owner_repo(&url).as_ref() == Some(owner_repo))
.then(|| entry.git_repo.as_ref()?.pr_data.info())
.flatten()
})
}
pub(super) fn git_info_for(&self, path: &Path) -> Option<&CheckoutInfo> {
self.at_path(path)
.and_then(|project| project.local_git_state.info())
}
pub(super) fn repo_info_for(&self, path: &Path) -> Option<&RepoInfo> {
self.git_repo_for(path)
.and_then(|repo| repo.repo_info.as_ref())
}
pub(super) fn git_repo_for(&self, path: &Path) -> Option<&GitRepo> {
for entry in self.roots.values() {
if entry.item.path() == path {
return entry.git_repo.as_ref();
}
}
for entry in self.roots.values() {
if let Some(submodule) = entry.item.find_submodule(path) {
return submodule.git_repo.as_ref();
}
}
self.entry_containing(path)
.and_then(|entry| entry.git_repo.as_ref())
}
pub(super) fn git_repo_for_mut(&mut self, path: &Path) -> Option<&mut GitRepo> {
match GitRepoLookup::find(self, path)? {
GitRepoLookup::Submodule { entry, submodule } => self
.roots
.get_mut(&entry)?
.item
.find_submodule_mut(submodule.as_path())?
.git_repo
.as_mut(),
GitRepoLookup::Direct(key) | GitRepoLookup::Containing(key) => {
self.roots.get_mut(&key)?.git_repo.as_mut()
},
}
}
pub(super) fn ensure_git_repo_for(&mut self, path: &Path) -> Option<&mut GitRepo> {
match GitRepoLookup::find(self, path)? {
GitRepoLookup::Direct(key) | GitRepoLookup::Containing(key) => Some(
self.roots
.get_mut(&key)?
.git_repo
.get_or_insert_with(Default::default),
),
GitRepoLookup::Submodule { entry, submodule } => Some(
self.roots
.get_mut(&entry)?
.item
.find_submodule_mut(submodule.as_path())?
.git_repo
.get_or_insert_with(Default::default),
),
}
}
pub(super) fn primary_url_for(&self, path: &Path) -> Option<&str> {
let checkout = self.git_info_for(path)?;
let repo = self.repo_info_for(path)?;
checkout.primary_url(repo)
}
pub(super) fn primary_ahead_behind_for(&self, path: &Path) -> Option<(usize, usize)> {
let checkout = self.git_info_for(path)?;
let repo = self.repo_info_for(path)?;
checkout.primary_ahead_behind(repo)
}
pub(super) fn primary_sync_resolution(&self, path: &Path) -> SyncResolution {
let (Some(checkout), Some(repo)) = (self.git_info_for(path), self.repo_info_for(path))
else {
return SyncResolution::Unresolved;
};
SyncResolution::Resolved(checkout.primary_ahead_behind(repo))
}
pub(super) fn fetch_url_for(&self, path: &Path) -> Option<String> {
let repo = self.repo_info_for(path)?;
let parseable = |name: &str| {
repo.remotes
.iter()
.find(|r| r.name == name)
.and_then(|r| r.url.as_deref())
.filter(|url| ci::parse_owner_repo(url).is_some())
};
parseable("upstream")
.or_else(|| parseable("origin"))
.or_else(|| {
repo.remotes.iter().find_map(|r| {
let url = r.url.as_deref()?;
ci::parse_owner_repo(url).map(|_| url)
})
})
.map(String::from)
}
pub(super) fn git_status_for(&self, path: &Path) -> Option<GitStatus> {
self.git_info_for(path).map(|info| info.status)
}
pub(super) fn git_status_for_item(&self, item: &RootItem) -> Option<GitStatus> {
match item {
RootItem::Worktrees(g) => worst_git_status(
std::iter::once(self.git_status_for(g.primary.path())).chain(
g.linked
.iter()
.filter(|l| l.visibility() == Visibility::Visible)
.map(|l| self.git_status_for(l.path())),
),
),
_ => self.git_status_for(item.path()),
}
}
pub(super) fn git_sync(&self, path: &Path) -> String {
let Some(info) = self.git_info_for(path) else {
return String::new();
};
if matches!(info.status, GitStatus::Untracked | GitStatus::Ignored) {
return String::new();
}
match self.primary_ahead_behind_for(path) {
Some((0, 0)) => IN_SYNC.to_string(),
Some((a, 0)) => format!("{SYNC_UP}{a}"),
Some((0, b)) => format!("{SYNC_DOWN}{b}"),
Some((a, b)) => format!("{SYNC_UP}{a}{SYNC_DOWN}{b}"),
None => NO_REMOTE_SYNC.to_string(),
}
}
pub(super) fn ci_data_for(&self, path: &Path) -> Option<&ProjectCiData> {
self.entry_containing(path)
.and_then(|entry| entry.git_repo.as_ref())
.map(|repo| &repo.ci_data)
}
pub(super) fn ci_info_for(&self, path: &Path) -> Option<&ProjectCiInfo> {
self.ci_data_for(path).and_then(ProjectCiData::info)
}
pub(super) fn unpublished_ci_branch_name(&self, path: &Path) -> Option<String> {
let owner = self.ci_branch_owner_path(path);
let git = self.git_info_for(owner.as_path())?;
let default_branch = self
.repo_info_for(owner.as_path())
.and_then(|repo| repo.default_branch.as_deref());
(git.primary_tracked_ref().is_none() && git.head.branch_name() != default_branch)
.then(|| git.head.branch_name().map(str::to_string))
.flatten()
}
pub(super) fn ci_status_using_lookup(
&self,
path: &Path,
lookup: &CiStatusLookup,
) -> Option<CiStatus> {
if self.unpublished_ci_branch_name(path).is_some() {
return None;
}
let display_mode = lookup.display_mode_for(self.ci_branch_owner_path(path).as_path());
let info = self.ci_info_for(path)?;
let runs = info.runs.as_slice();
let latest = match self.current_branch_for(path) {
None => runs.first(),
Some(_) if display_mode == CiRunDisplayMode::All => runs.first(),
Some(branch) => runs.iter().find(|run| run.branch == branch),
};
latest.map(|run| run.ci_status)
}
pub(super) fn ci_status_for_root_item_using_lookup(
&self,
item: &RootItem,
lookup: &CiStatusLookup,
) -> Option<CiStatus> {
item.ci_status(|p| self.ci_status_using_lookup(p, lookup))
}
pub(super) fn is_deleted(&self, path: &Path) -> bool {
self.at_path(path)
.is_some_and(|project| project.visibility == Visibility::Deleted)
}
pub(super) fn is_rust_at_path(&self, path: &Path) -> bool {
self.iter().any(|item| {
if item
.submodules()
.iter()
.any(|submodule| submodule.path.as_path() == path)
{
return false;
}
(item.path() == path || item.at_path(path).is_some()) && item.is_rust()
})
}
pub(super) fn is_vendored_path(&self, path: &Path) -> bool {
self.iter()
.any(|item| item.item.vendored_at_path(path).is_some())
}
pub(super) fn is_workspace_member_path(&self, path: &Path) -> bool {
self.iter().any(|item| match &item.item {
RootItem::Rust(RustProject::Workspace(ws)) => ws
.groups()
.iter()
.any(|g| g.members().iter().any(|m| m.path() == path)),
RootItem::Worktrees(group) => group.iter_entries().any(|entry| {
if let RustProject::Workspace(ws) = entry {
ws.groups()
.iter()
.any(|g| g.members().iter().any(|m| m.path() == path))
} else {
false
}
}),
_ => false,
})
}
pub(super) fn git_main(&self, path: &Path) -> String {
let Some(info) = self.git_info_for(path) else {
return String::new();
};
if matches!(info.status, GitStatus::Untracked | GitStatus::Ignored) {
return String::new();
}
match info.ahead_behind_local {
Some((0, 0)) => IN_SYNC.to_string(),
Some((a, 0)) => format!("{SYNC_UP}{a}"),
Some((0, b)) => format!("{SYNC_DOWN}{b}"),
Some((a, b)) => format!("{SYNC_UP}{a}{SYNC_DOWN}{b}"),
None => String::new(),
}
}
pub(super) fn replace_leaf_by_path(
&mut self,
path: &Path,
mut replacement: RootItem,
) -> Option<RootItem> {
for entry in self.roots.values_mut() {
match &mut entry.item {
item @ (RootItem::Rust(_) | RootItem::NonRust(_)) => {
if item.path() == path {
debug_assert_eq!(
replacement.path().as_path(),
path,
"replacement path must match target path"
);
std::mem::swap(item, &mut replacement);
return Some(replacement);
}
},
RootItem::Worktrees(group) => {
if group.primary.path() == path
&& let RootItem::Rust(rp) = replacement
{
let old = std::mem::replace(&mut group.primary, rp);
return Some(RootItem::Rust(old));
}
for l in &mut group.linked {
if l.path() == path
&& let RootItem::Rust(rp) = replacement
{
let old = std::mem::replace(l, rp);
return Some(RootItem::Rust(old));
}
}
},
}
}
None
}
#[expect(
dead_code,
reason = "kept for use by upcoming worktree promotion sites"
)]
pub(super) fn promote_to_worktree_group(&mut self, path: &Path, group: WorktreeGroup) -> bool {
let Some(entry) = self.roots.get_mut(path) else {
return false;
};
debug_assert_eq!(
group.primary_path().as_path(),
path,
"promoted group primary must retain the same root path"
);
entry.item = RootItem::Worktrees(group);
true
}
pub(super) fn insert_into_hierarchy(&mut self, item: RootItem) -> bool {
let item_path = item.path().to_path_buf();
for entry in self.roots.values_mut() {
if try_attach_worktree(&mut entry.item, &item) {
return false;
}
let inserted = match &mut entry.item {
RootItem::Rust(RustProject::Workspace(ws)) => {
try_insert_member(ws, &item_path, &item)
},
RootItem::Worktrees(group) => std::iter::once(&mut group.primary)
.chain(group.linked.iter_mut())
.any(|entry| {
if let RustProject::Workspace(ws) = entry {
try_insert_member(ws, &item_path, &item)
} else {
false
}
}),
_ => false,
};
if inserted {
return true;
}
}
let insert_index = self
.roots
.keys()
.position(|existing| existing.as_path() > item_path.as_path())
.unwrap_or(self.roots.len());
let key = item.path().clone();
self.roots
.shift_insert(insert_index, key, ProjectEntry::new(item));
false
}
pub(super) fn regroup_members(&mut self, inline_dirs: &[String]) {
for entry in self.roots.values_mut() {
match &mut entry.item {
RootItem::Rust(RustProject::Workspace(ws)) => {
regroup_workspace(ws, inline_dirs);
},
RootItem::Worktrees(group) => {
for entry in std::iter::once(&mut group.primary).chain(group.linked.iter_mut())
{
if let RustProject::Workspace(ws) = entry {
regroup_workspace(ws, inline_dirs);
}
}
},
_ => {},
}
}
}
pub(super) fn regroup_top_level_worktrees(&mut self) {
let mut index = 0;
while index < self.roots.len() {
let Some(identity) = linked_worktree_identity(&self.roots[index].item).cloned() else {
index += 1;
continue;
};
let Some(mut target_index) =
find_matching_worktree_container(&self.roots, index, &identity)
else {
index += 1;
continue;
};
let Some((_key, linked_entry)) = self.roots.shift_remove_index(index) else {
index += 1;
continue;
};
if target_index > index {
target_index -= 1;
}
let attached =
try_attach_worktree(&mut self.roots[target_index].item, &linked_entry.item);
debug_assert!(
attached,
"linked worktree regroup should attach after container match"
);
if target_index >= index {
index += 1;
}
}
}
pub(super) fn clear(&mut self) { self.roots.clear(); }
#[cfg(test)]
pub(super) fn push(&mut self, item: RootItem) {
let key = item.path().clone();
self.roots.insert(key, ProjectEntry::new(item));
}
}
impl Index<usize> for ProjectList {
type Output = ProjectEntry;
fn index(&self, index: usize) -> &ProjectEntry { &self.roots[index] }
}
impl<'a> IntoIterator for &'a ProjectList {
type IntoIter = Values<'a, AbsolutePath, ProjectEntry>;
type Item = &'a ProjectEntry;
fn into_iter(self) -> Self::IntoIter { self.roots.values() }
}
impl<'a> IntoIterator for &'a mut ProjectList {
type IntoIter = ValuesMut<'a, AbsolutePath, ProjectEntry>;
type Item = &'a mut ProjectEntry;
fn into_iter(self) -> Self::IntoIter { self.roots.values_mut() }
}
impl ProjectList {
pub(super) const fn cursor(&self) -> usize { self.cursor }
pub(super) const fn set_cursor(&mut self, cursor: usize) { self.cursor = cursor; }
pub(super) const fn sync(&self) -> SelectionSync { self.sync }
pub(super) const fn mark_sync_changed(&mut self) { self.sync = SelectionSync::Changed; }
pub(super) const fn mark_sync_stable(&mut self) { self.sync = SelectionSync::Stable; }
pub(super) fn visible_rows(&self) -> &[VisibleRow] { &self.cached_visible_rows }
pub(super) const fn row_count(&self) -> usize { self.cached_visible_rows.len() }
pub(super) const fn move_up(&mut self) {
let count = self.row_count();
if count == 0 {
return;
}
let current = self.cursor;
if current > 0 {
self.cursor = current - 1;
}
}
pub(super) const fn move_down(&mut self) {
let count = self.row_count();
if count == 0 {
return;
}
let current = self.cursor;
if current < count - 1 {
self.cursor = current + 1;
}
}
pub(super) const fn move_up_by(&mut self, step: usize) {
self.cursor = self.cursor.saturating_sub(step);
}
pub(super) fn move_down_by(&mut self, step: usize) {
let count = self.row_count();
if count == 0 {
return;
}
self.cursor = self.cursor.saturating_add(step).min(count - 1);
}
pub(super) const fn move_to_top(&mut self) {
if self.row_count() > 0 {
self.cursor = 0;
}
}
pub(super) const fn move_to_bottom(&mut self) {
let count = self.row_count();
if count > 0 {
self.cursor = count - 1;
}
}
pub(super) fn recompute_visibility(&mut self, include_non_rust: bool) {
self.cached_visible_rows = self.compute_visible_rows(&self.expanded, include_non_rust);
let len = self.cached_visible_rows.len();
if len == 0 {
self.cursor = 0;
} else if self.cursor >= len {
self.cursor = len - 1;
}
}
pub(super) fn set_disk_caches(
&mut self,
root_sorted: Vec<u64>,
child_sorted: HashMap<usize, Vec<u64>>,
) {
self.cached_root_sorted = root_sorted;
self.cached_child_sorted = child_sorted;
}
#[cfg(test)]
pub(super) const fn fit_widths_mut(&mut self) -> &mut ProjectListWidths {
&mut self.cached_fit_widths
}
pub(super) fn set_fit_widths(&mut self, widths: ProjectListWidths) {
self.cached_fit_widths = widths;
}
pub(super) fn reset_fit_widths(&mut self, lint_enabled: bool) {
self.cached_fit_widths = ProjectListWidths::new(lint_enabled);
}
#[allow(
dead_code,
reason = "tui::app::navigation (try_expand / try_collapse) still calls \
expanded_mut directly because it recomputes via a separate \
ensure_visible_rows_cached() call in the same code path."
)]
pub(super) const fn mutate(&mut self, include_non_rust: bool) -> SelectionMutation<'_> {
SelectionMutation {
project_list: self,
include_non_rust,
}
}
}
impl ProjectList {
pub(super) fn selected_row(&self) -> Option<VisibleRow> {
let rows = self.visible_rows();
let selected = self.cursor();
rows.get(selected).copied()
}
pub(super) fn selected_project_path(&self) -> Option<&Path> {
let row = self.selected_row()?;
self.path_for_row(row)
}
pub(super) fn selected_worktree_group_checkout_paths(&self) -> Option<Vec<AbsolutePath>> {
let VisibleRow::Root { node_index } = self.selected_row()? else {
return None;
};
let RootItem::Worktrees(group) = &self.get(node_index)?.item else {
return None;
};
if !group.renders_as_group() {
return None;
}
Some(
group
.iter_entries()
.filter(|entry| entry.visibility() == Visibility::Visible)
.map(|entry| entry.path().clone())
.collect(),
)
}
pub(super) fn path_for_row(&self, row: VisibleRow) -> Option<&Path> {
match row {
VisibleRow::Root { node_index } | VisibleRow::GroupHeader { node_index, .. } => {
Some(self.get(node_index)?.path().as_path())
},
VisibleRow::Member {
node_index,
group_index,
member_index,
} => self
.get(node_index)?
.item
.member_path_ref(group_index, member_index),
VisibleRow::MemberVendored {
node_index,
group_index,
member_index,
vendored_index,
} => self
.get(node_index)?
.item
.resolve_member_vendored(group_index, member_index, vendored_index)
.map(|vendored| vendored.path().as_path()),
VisibleRow::Vendored {
node_index,
vendored_index,
} => self.get(node_index)?.item.vendored_path_ref(vendored_index),
VisibleRow::WorktreeEntry {
node_index,
worktree_index,
}
| VisibleRow::WorktreeGroupHeader {
node_index,
worktree_index,
..
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => wtg.worktree_path_ref(worktree_index),
_ => None,
},
VisibleRow::WorktreeMember {
node_index,
worktree_index,
group_index,
member_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_member_path_ref(worktree_index, group_index, member_index)
},
_ => None,
},
VisibleRow::WorktreeMemberVendored {
node_index,
worktree_index,
group_index,
member_index,
vendored_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => wtg
.member_vendored_ref(worktree_index, group_index, member_index, vendored_index)
.map(|vendored| vendored.path().as_path()),
_ => None,
},
VisibleRow::WorktreeVendored {
node_index,
worktree_index,
vendored_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_vendored_path_ref(worktree_index, vendored_index)
},
_ => None,
},
VisibleRow::Submodule {
node_index,
submodule_index,
} => self
.get(node_index)?
.submodules()
.get(submodule_index)
.map(|s| s.path.as_path()),
}
}
pub(super) fn display_path_for_row(&self, row: VisibleRow) -> Option<DisplayPath> {
match row {
VisibleRow::Root { node_index } | VisibleRow::GroupHeader { node_index, .. } => {
let item = self.get(node_index)?;
Some(item.display_path())
},
VisibleRow::Member {
node_index,
group_index,
member_index,
} => self
.get(node_index)?
.item
.resolve_member(group_index, member_index)
.map(ProjectFields::display_path),
VisibleRow::MemberVendored {
node_index,
group_index,
member_index,
vendored_index,
} => self
.get(node_index)?
.item
.resolve_member_vendored(group_index, member_index, vendored_index)
.map(ProjectFields::display_path),
VisibleRow::Vendored {
node_index,
vendored_index,
} => self
.get(node_index)?
.item
.resolve_vendored(vendored_index)
.map(ProjectFields::display_path),
VisibleRow::WorktreeEntry {
node_index,
worktree_index,
}
| VisibleRow::WorktreeGroupHeader {
node_index,
worktree_index,
..
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => wtg.worktree_display_path(worktree_index),
_ => None,
},
VisibleRow::WorktreeMember {
node_index,
worktree_index,
group_index,
member_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_member_display_path(worktree_index, group_index, member_index)
},
_ => None,
},
VisibleRow::WorktreeMemberVendored {
node_index,
worktree_index,
group_index,
member_index,
vendored_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => wtg
.member_vendored_ref(worktree_index, group_index, member_index, vendored_index)
.map(ProjectFields::display_path),
_ => None,
},
VisibleRow::WorktreeVendored {
node_index,
worktree_index,
vendored_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_vendored_display_path(worktree_index, vendored_index)
},
_ => None,
},
VisibleRow::Submodule {
node_index,
submodule_index,
} => {
let item = self.get(node_index)?;
let submodule = item.submodules().get(submodule_index)?;
Some(DisplayPath::new(project::home_relative_path(
&submodule.path,
)))
},
}
}
pub(super) fn abs_path_for_row(&self, row: VisibleRow) -> Option<AbsolutePath> {
match row {
VisibleRow::Root { node_index } | VisibleRow::GroupHeader { node_index, .. } => {
let item = self.get(node_index)?;
Some(item.path().clone())
},
VisibleRow::Member {
node_index,
group_index,
member_index,
} => self
.get(node_index)?
.item
.resolve_member(group_index, member_index)
.map(|p| p.path().clone()),
VisibleRow::MemberVendored {
node_index,
group_index,
member_index,
vendored_index,
} => self
.get(node_index)?
.item
.resolve_member_vendored(group_index, member_index, vendored_index)
.map(|p| p.path().clone()),
VisibleRow::Vendored {
node_index,
vendored_index,
} => self
.get(node_index)?
.item
.resolve_vendored(vendored_index)
.map(|p| p.path().clone()),
VisibleRow::WorktreeEntry {
node_index,
worktree_index,
}
| VisibleRow::WorktreeGroupHeader {
node_index,
worktree_index,
..
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => wtg.worktree_abs_path(worktree_index),
_ => None,
},
VisibleRow::WorktreeMember {
node_index,
worktree_index,
group_index,
member_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_member_abs_path(worktree_index, group_index, member_index)
},
_ => None,
},
VisibleRow::WorktreeMemberVendored {
node_index,
worktree_index,
group_index,
member_index,
vendored_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => wtg
.member_vendored_ref(worktree_index, group_index, member_index, vendored_index)
.map(|p| p.path().clone()),
_ => None,
},
VisibleRow::WorktreeVendored {
node_index,
worktree_index,
vendored_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_vendored_abs_path(worktree_index, vendored_index)
},
_ => None,
},
VisibleRow::Submodule {
node_index,
submodule_index,
} => {
let item = self.get(node_index)?;
item.submodules()
.get(submodule_index)
.map(|s| s.path.clone())
},
}
}
pub(super) fn expand_key_for_row(&self, row: VisibleRow) -> Option<ExpandKey> {
match row {
VisibleRow::Root { node_index } => self
.get(node_index)?
.has_children()
.then_some(ExpandKey::Node(node_index)),
VisibleRow::GroupHeader {
node_index,
group_index,
} => Some(ExpandKey::Group(node_index, group_index)),
VisibleRow::WorktreeEntry {
node_index,
worktree_index,
} => {
let item = self.get(node_index)?;
match &item.item {
RootItem::Worktrees(group) => match group.entry(worktree_index)? {
RustProject::Workspace(ws) => ws
.has_members()
.then_some(ExpandKey::Worktree(node_index, worktree_index)),
RustProject::Package(_) => None,
},
_ => None,
}
},
VisibleRow::WorktreeGroupHeader {
node_index,
worktree_index,
group_index,
} => Some(ExpandKey::WorktreeGroup(
node_index,
worktree_index,
group_index,
)),
VisibleRow::Member { .. }
| VisibleRow::MemberVendored { .. }
| VisibleRow::Vendored { .. }
| VisibleRow::Submodule { .. }
| VisibleRow::WorktreeMember { .. }
| VisibleRow::WorktreeMemberVendored { .. }
| VisibleRow::WorktreeVendored { .. } => None,
}
}
pub(super) fn try_collapse(&mut self, key: &ExpandKey) -> bool { self.expanded.remove(key) }
pub(super) fn dismiss_target_for_row_inner(&self, row: VisibleRow) -> Option<DismissTarget> {
let dismiss_path = match row {
VisibleRow::Root { node_index } | VisibleRow::GroupHeader { node_index, .. } => {
self.get(node_index).map(|item| item.path().clone())
},
VisibleRow::Member { node_index, .. }
| VisibleRow::MemberVendored { node_index, .. }
| VisibleRow::Vendored { node_index, .. }
| VisibleRow::Submodule { node_index, .. } => {
self.get(node_index).map(|item| item.path().clone())
},
VisibleRow::WorktreeEntry {
node_index,
worktree_index,
}
| VisibleRow::WorktreeGroupHeader {
node_index,
worktree_index,
..
}
| VisibleRow::WorktreeMember {
node_index,
worktree_index,
..
}
| VisibleRow::WorktreeMemberVendored {
node_index,
worktree_index,
..
}
| VisibleRow::WorktreeVendored {
node_index,
worktree_index,
..
} => match &self.get(node_index)?.item {
RootItem::Worktrees(group) => group.entry(worktree_index).map(|p| p.path().clone()),
_ => None,
},
}?;
if self.is_deleted(&dismiss_path) {
Some(DismissTarget::DeletedProject(dismiss_path))
} else {
None
}
}
pub(super) fn worktree_parent_node_index(&self, path: &Path) -> Option<usize> {
self.iter()
.enumerate()
.find_map(|(ni, item)| match &item.item {
RootItem::Worktrees(group) => {
group.iter_entries().any(|p| p.path() == path).then_some(ni)
},
_ => None,
})
}
pub(super) fn row_matches_project_path(&self, row: VisibleRow, target_path: &Path) -> bool {
self.path_for_row(row)
.is_some_and(|path| path == target_path)
}
pub(super) const fn last_selected_path(&self) -> Option<&AbsolutePath> {
self.paths.last_selected.as_ref()
}
pub(super) fn ci_branch_owner_path(&self, path: &Path) -> AbsolutePath {
self.checkout_root_for(path)
.unwrap_or_else(|| AbsolutePath::from(path))
}
pub(super) fn current_branch_for(&self, path: &Path) -> Option<&str> {
let owner = self.ci_branch_owner_path(path);
self.git_info_for(owner.as_path())?.head.branch_name()
}
pub(super) fn ci_toggle_available_for_inner(&self, path: &Path) -> bool {
self.current_branch_for(path).is_some() && self.unpublished_ci_branch_name(path).is_none()
}
pub(super) fn owner_repo_for_path_inner(&self, path: &Path) -> Option<OwnerRepo> {
let entry_path = self.entry_containing(path)?.item.path().clone();
self.fetch_url_for(entry_path.as_path())
.as_deref()
.and_then(ci::parse_owner_repo)
}
}
impl ProjectList {
pub(super) fn expand_all(&mut self, include_non_rust: bool) {
let selected_path = self
.paths
.collapsed_selected
.take()
.or_else(|| self.selected_project_path().map(AbsolutePath::from));
self.paths.collapsed_anchor = None;
let (roots, expanded) = self.iter_with_expanded_mut();
for (ni, entry) in roots.enumerate() {
if entry.item.has_children() {
expanded.insert(ExpandKey::Node(ni));
}
match &entry.item {
RootItem::Rust(RustProject::Workspace(ws)) => {
for (gi, group) in ws.groups().iter().enumerate() {
if group.is_named() {
expanded.insert(ExpandKey::Group(ni, gi));
}
}
},
RootItem::Worktrees(group) => {
for (wi, entry) in group.iter_entries().enumerate() {
if let RustProject::Workspace(ws) = entry {
if ws.has_members() {
expanded.insert(ExpandKey::Worktree(ni, wi));
}
for (gi, g) in ws.groups().iter().enumerate() {
if g.is_named() {
expanded.insert(ExpandKey::WorktreeGroup(ni, wi, gi));
}
}
}
}
},
_ => {},
}
}
if let Some(path) = selected_path {
self.select_project_in_tree(path.as_path(), include_non_rust);
}
}
pub(super) fn collapse_all(&mut self, include_non_rust: bool) {
let selected_path = self.selected_project_path().map(AbsolutePath::from);
let anchor = self.selected_row().map(VisibleRow::collapse_anchor);
self.expanded.clear();
self.recompute_visibility(include_non_rust);
if let Some(anchor) = anchor
&& let Some(pos) = self.visible_rows().iter().position(|row| *row == anchor)
{
self.set_cursor(pos);
}
let anchor_path = self.selected_project_path().map(AbsolutePath::from);
if selected_path == anchor_path {
self.paths.collapsed_selected = None;
self.paths.collapsed_anchor = None;
} else {
self.paths.collapsed_selected = selected_path;
self.paths.collapsed_anchor = anchor_path;
}
}
pub(super) fn expand_path_in_tree(&mut self, target_path: &Path) {
let (roots, expanded) = self.iter_with_expanded_mut();
for (ni, entry) in roots.enumerate() {
match &entry.item {
RootItem::Rust(RustProject::Workspace(ws)) => {
for (gi, group) in ws.groups().iter().enumerate() {
for member in group.members() {
if member.path() == target_path {
expanded.insert(ExpandKey::Node(ni));
if group.is_named() {
expanded.insert(ExpandKey::Group(ni, gi));
}
}
if member
.vendored()
.iter()
.any(|vendored| vendored.path() == target_path)
{
expanded.insert(ExpandKey::Node(ni));
if group.is_named() {
expanded.insert(ExpandKey::Group(ni, gi));
}
}
}
}
for vendored in ws.vendored() {
if vendored.path() == target_path {
expanded.insert(ExpandKey::Node(ni));
}
}
},
RootItem::Rust(RustProject::Package(pkg)) => {
for vendored in pkg.vendored() {
if vendored.path() == target_path {
expanded.insert(ExpandKey::Node(ni));
}
}
},
RootItem::NonRust(_) => {},
RootItem::Worktrees(group) => {
for (wi, entry) in group.iter_entries().enumerate() {
if entry.path() == target_path {
expanded.insert(ExpandKey::Node(ni));
}
if let RustProject::Workspace(ws) = entry {
for (gi, g) in ws.groups().iter().enumerate() {
for member in g.members() {
if member.path() == target_path {
expanded.insert(ExpandKey::Node(ni));
expanded.insert(ExpandKey::Worktree(ni, wi));
if g.is_named() {
expanded.insert(ExpandKey::WorktreeGroup(ni, wi, gi));
}
}
if member
.vendored()
.iter()
.any(|vendored| vendored.path() == target_path)
{
expanded.insert(ExpandKey::Node(ni));
expanded.insert(ExpandKey::Worktree(ni, wi));
if g.is_named() {
expanded.insert(ExpandKey::WorktreeGroup(ni, wi, gi));
}
}
}
}
}
for vendored in entry.rust_info().vendored() {
if vendored.path() == target_path {
expanded.insert(ExpandKey::Node(ni));
expanded.insert(ExpandKey::Worktree(ni, wi));
}
}
}
},
}
}
}
pub(super) fn select_matching_visible_row(
&mut self,
target_path: &Path,
include_non_rust: bool,
) {
self.recompute_visibility(include_non_rust);
let selected_index = self
.visible_rows()
.iter()
.position(|row| self.row_matches_project_path(*row, target_path));
if let Some(selected_index) = selected_index {
self.set_cursor(selected_index);
}
}
pub(super) fn select_project_in_tree(&mut self, target_path: &Path, include_non_rust: bool) {
self.expand_path_in_tree(target_path);
self.select_matching_visible_row(target_path, include_non_rust);
}
pub(super) fn collapse_to(
&mut self,
key: &ExpandKey,
target: VisibleRow,
include_non_rust: bool,
) {
self.expanded.remove(key);
self.recompute_visibility(include_non_rust);
if let Some(pos) = self.visible_rows().iter().position(|r| *r == target) {
self.set_cursor(pos);
}
}
pub(super) fn collapse_row(&mut self, row: VisibleRow, include_non_rust: bool) {
match row {
VisibleRow::Root { node_index: ni } => {
self.try_collapse(&ExpandKey::Node(ni));
},
VisibleRow::GroupHeader {
node_index: ni,
group_index: gi,
} => {
if !self.try_collapse(&ExpandKey::Group(ni, gi)) {
self.collapse_to_root(ni, include_non_rust);
}
},
VisibleRow::Member {
node_index: ni,
group_index: gi,
..
}
| VisibleRow::MemberVendored {
node_index: ni,
group_index: gi,
..
} => {
if self.is_inline_group(ni, gi) {
self.collapse_to_root(ni, include_non_rust);
} else {
self.collapse_to(
&ExpandKey::Group(ni, gi),
VisibleRow::GroupHeader {
node_index: ni,
group_index: gi,
},
include_non_rust,
);
}
},
VisibleRow::Vendored { node_index: ni, .. }
| VisibleRow::Submodule { node_index: ni, .. } => {
self.collapse_to_root(ni, include_non_rust);
},
VisibleRow::WorktreeEntry {
node_index: ni,
worktree_index: wi,
} => {
if !self.try_collapse(&ExpandKey::Worktree(ni, wi)) {
self.collapse_to_root(ni, include_non_rust);
}
},
VisibleRow::WorktreeGroupHeader {
node_index: ni,
worktree_index: wi,
group_index: gi,
} => {
if !self.try_collapse(&ExpandKey::WorktreeGroup(ni, wi, gi)) {
self.collapse_to_worktree_entry(ni, wi, include_non_rust);
}
},
VisibleRow::WorktreeMember {
node_index: ni,
worktree_index: wi,
group_index: gi,
..
}
| VisibleRow::WorktreeMemberVendored {
node_index: ni,
worktree_index: wi,
group_index: gi,
..
} => {
if self.is_worktree_inline_group(ni, wi, gi) {
self.collapse_to_worktree_entry(ni, wi, include_non_rust);
} else {
self.collapse_to(
&ExpandKey::WorktreeGroup(ni, wi, gi),
VisibleRow::WorktreeGroupHeader {
node_index: ni,
worktree_index: wi,
group_index: gi,
},
include_non_rust,
);
}
},
VisibleRow::WorktreeVendored {
node_index: ni,
worktree_index: wi,
..
} => {
self.collapse_to_worktree_entry(ni, wi, include_non_rust);
},
}
}
fn collapse_to_root(&mut self, ni: usize, include_non_rust: bool) {
self.collapse_to(
&ExpandKey::Node(ni),
VisibleRow::Root { node_index: ni },
include_non_rust,
);
}
fn collapse_to_worktree_entry(&mut self, ni: usize, wi: usize, include_non_rust: bool) {
self.collapse_to(
&ExpandKey::Worktree(ni, wi),
VisibleRow::WorktreeEntry {
node_index: ni,
worktree_index: wi,
},
include_non_rust,
);
}
pub(super) fn collapse(&mut self, include_non_rust: bool) -> bool {
let selected = self.cursor();
let Some(row) = self.visible_rows().get(selected).copied() else {
return false;
};
let expanded_before = self.expanded.len();
let selected_before = self.cursor();
self.collapse_row(row, include_non_rust);
self.expanded.len() != expanded_before || self.cursor() != selected_before
}
pub(super) fn is_inline_group(&self, ni: usize, gi: usize) -> bool {
let Some(item) = self.get(ni) else {
return true;
};
match &item.item {
RootItem::Rust(RustProject::Workspace(ws)) => {
ws.groups().get(gi).is_some_and(|g| !g.is_named())
},
_ => true,
}
}
pub(super) fn is_worktree_inline_group(&self, ni: usize, wi: usize, gi: usize) -> bool {
let Some(item) = self.get(ni) else {
return true;
};
match &item.item {
RootItem::Worktrees(group) => match group.entry(wi) {
Some(RustProject::Workspace(ws)) => {
ws.groups().get(gi).is_some_and(|g| !g.is_named())
},
_ => true,
},
_ => true,
}
}
pub(super) fn clean_selection(&self) -> Option<CleanSelection> {
let row = self.selected_row()?;
match row {
VisibleRow::Root { node_index } => {
let entry = self.get(node_index)?;
match &entry.item {
RootItem::Rust(rust) => Some(CleanSelection::Project {
root: rust.path().clone(),
}),
RootItem::Worktrees(group) => Some(worktree_group_selection(group)),
RootItem::NonRust(_) => None,
}
},
VisibleRow::WorktreeEntry {
node_index,
worktree_index,
} => match &self.get(node_index)?.item {
RootItem::Worktrees(wtg) => {
wtg.worktree_path_ref(worktree_index)
.map(|path| CleanSelection::Project {
root: AbsolutePath::from(path),
})
},
_ => None,
},
_ => None,
}
}
pub(super) fn select_root_row(&mut self, node_index: usize) {
if let Some(pos) = self
.visible_rows()
.iter()
.position(|row| matches!(row, VisibleRow::Root { node_index: ni } if *ni == node_index))
{
self.set_cursor(pos);
}
}
pub(super) fn capture_legacy_root_expansions(&self) -> Vec<LegacyRootExpansion> {
self.iter()
.enumerate()
.filter_map(|(ni, entry)| {
if !self.expanded.contains(&ExpandKey::Node(ni)) {
return None;
}
match &entry.item {
RootItem::Rust(RustProject::Workspace(ws)) => Some(LegacyRootExpansion {
root_path: ws.path().clone(),
old_node_index: ni,
had_children: ws.has_members() || !ws.vendored().is_empty(),
named_groups: ws
.groups()
.iter()
.enumerate()
.filter_map(|(gi, group)| {
group
.is_named()
.then(|| self.expanded.contains(&ExpandKey::Group(ni, gi)))
.filter(|expanded| *expanded)
.map(|_| gi)
})
.collect(),
}),
RootItem::Rust(RustProject::Package(pkg)) => Some(LegacyRootExpansion {
root_path: pkg.path().clone(),
old_node_index: ni,
had_children: !pkg.vendored().is_empty(),
named_groups: Vec::new(),
}),
_ => None,
}
})
.collect()
}
pub(super) fn migrate_legacy_root_expansions(&mut self, legacy: &[LegacyRootExpansion]) {
let (roots, expanded) = self.iter_with_expanded_mut();
let entries: Vec<(usize, &RootItem)> = roots
.enumerate()
.map(|(idx, entry)| (idx, &entry.item))
.collect();
for legacy_root in legacy {
let Some((current_index, item)) = entries
.iter()
.find(|(_, item)| item.path() == legacy_root.root_path.as_path())
.map(|(idx, item)| (*idx, *item))
else {
continue;
};
match item {
RootItem::Worktrees(group) if group.renders_as_group() => {
expanded.insert(ExpandKey::Node(current_index));
if legacy_root.had_children {
expanded.insert(ExpandKey::Worktree(current_index, 0));
}
if let RustProject::Workspace(ws) = &group.primary {
for &group_index in &legacy_root.named_groups {
if ws.groups().get(group_index).is_some() {
expanded.insert(ExpandKey::WorktreeGroup(
current_index,
0,
group_index,
));
}
expanded
.remove(&ExpandKey::Group(legacy_root.old_node_index, group_index));
}
}
},
_ => {},
}
}
}
pub(super) fn apply_cargo_fields_from_workspace_metadata(
&mut self,
metadata: &WorkspaceMetadata,
) {
for record in metadata.packages.values() {
let Some(manifest_dir) = record.manifest_path.as_path().parent() else {
continue;
};
let cargo = Cargo::from_package_record(record);
if let Some(rust_info) = self.rust_info_at_path_mut(manifest_dir) {
rust_info.cargo = cargo.clone();
}
if let Some(vendored) = self.vendored_at_path_mut(manifest_dir) {
vendored.cargo = cargo;
}
}
}
pub(super) fn handle_language_stats_batch(
&mut self,
entries: Vec<(AbsolutePath, LanguageStats)>,
) {
for (path, stats) in entries {
if let Some(project) = self.at_path_mut(path.as_path()) {
project.language_stats = Some(stats);
}
}
}
pub(super) fn handle_test_counts_batch(&mut self, entries: Vec<(AbsolutePath, TestCounts)>) {
for (path, counts) in entries {
if let Some(project) = self.at_path_mut(path.as_path()) {
project.test_counts = Some(counts);
}
}
}
pub(super) fn handle_crates_io_version_msg(
&mut self,
path: &Path,
version: String,
prerelease: Option<String>,
downloads: u64,
) {
if let Some(rust_info) = self.rust_info_at_path_mut(path) {
rust_info.set_crates_io(version, prerelease, downloads);
} else if let Some(vendored) = self.vendored_at_path_mut(path) {
vendored.set_crates_io(version, prerelease, downloads);
}
}
pub(super) fn handle_repo_meta(
&mut self,
path: &Path,
stars: u64,
description: Option<String>,
) {
if let Some(repo) = self.ensure_git_repo_for(path) {
repo.github_info = Some(GitHubInfo { stars, description });
}
}
pub(super) fn lint_runtime_root_entries(&self) -> Vec<(AbsolutePath, bool)> {
let mut seen = HashSet::new();
let mut entries = Vec::new();
for entry in self {
let items: Vec<(&AbsolutePath, bool)> = match &entry.item {
RootItem::Worktrees(group) => {
group.iter_entries().map(|p| (p.path(), true)).collect()
},
_ => vec![(entry.item.path(), entry.item.is_rust())],
};
for (path, is_rust) in items {
let owned = path.clone();
if seen.insert(owned.clone()) {
entries.push((owned, is_rust));
}
}
}
entries
}
pub(super) fn has_cached_non_rust_projects(&self) -> bool {
let mut found = false;
self.for_each_leaf(|item| {
if !item.is_rust() {
found = true;
}
});
found
}
pub(super) fn selected_project_is_deleted(&self) -> bool {
self.selected_project_path()
.is_some_and(|path| self.is_deleted(path))
}
pub(super) fn selected_ci_path(&self) -> Option<AbsolutePath> {
let path = self.selected_project_path()?;
let entry = self.entry_containing(path)?;
Some(entry.item.path().clone())
}
pub(super) fn ci_runs_for_ci_pane(&self, path: &Path, ci: &Ci) -> Vec<CiRun> {
let Some(info) = self.ci_info_for(path) else {
return Vec::new();
};
if !self.ci_toggle_available_for_inner(path) {
return info.runs.clone();
}
let Some(branch) = self.current_branch_for(path) else {
return info.runs.clone();
};
if ci.display_mode_for(self.ci_branch_owner_path(path).as_path()) == CiRunDisplayMode::All {
return info.runs.clone();
}
info.runs
.iter()
.filter(|run| run.branch == branch)
.cloned()
.collect()
}
}
enum GitRepoLookup {
Direct(AbsolutePath),
Submodule {
entry: AbsolutePath,
submodule: AbsolutePath,
},
Containing(AbsolutePath),
}
impl GitRepoLookup {
fn find(list: &ProjectList, path: &Path) -> Option<Self> {
for (key, entry) in &list.roots {
if entry.item.path() == path {
return Some(Self::Direct(key.clone()));
}
}
for (key, entry) in &list.roots {
if let Some(submodule) = entry.item.find_submodule(path) {
return Some(Self::Submodule {
entry: key.clone(),
submodule: submodule.path.clone(),
});
}
}
for (key, entry) in &list.roots {
if project::entry_contains(entry, path) {
return Some(Self::Containing(key.clone()));
}
}
None
}
}
fn worktree_group_selection(group: &WorktreeGroup) -> CleanSelection {
CleanSelection::WorktreeGroup {
primary: group.primary.path().clone(),
linked: group.linked.iter().map(|p| p.path().clone()).collect(),
}
}