use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use super::statusline::path::compact_cwd;
use crate::session::{is_cross_project, SessionSummary, Workspace};
#[derive(Debug)]
pub(super) struct DirectoryGroup {
pub(super) cwd: PathBuf,
pub(super) display: String,
pub(super) sessions: Vec<SessionSummary>,
}
#[derive(Debug)]
pub(super) struct Worktree {
pub(super) name: String,
pub(super) directory: DirectoryGroup,
}
#[derive(Debug)]
pub(super) enum HubGroup {
Directory(DirectoryGroup),
Repo {
display: String,
worktrees: Vec<Worktree>,
},
Missing(Vec<DirectoryGroup>),
}
pub(super) fn find_directory<'a>(groups: &'a [HubGroup], cwd: &Path) -> Option<&'a DirectoryGroup> {
groups.iter().find_map(|group| match group {
HubGroup::Directory(directory) => (directory.cwd == cwd).then_some(directory),
HubGroup::Repo { worktrees, .. } => worktrees
.iter()
.map(|worktree| &worktree.directory)
.find(|directory| directory.cwd == cwd),
HubGroup::Missing(directories) => directories.iter().find(|directory| directory.cwd == cwd),
})
}
pub(super) enum Placement {
Repo(Workspace),
Lone,
Missing,
}
pub(super) fn directory_groups(
sessions: Vec<SessionSummary>,
current_cwd: &Path,
) -> Vec<DirectoryGroup> {
let mut groups: Vec<DirectoryGroup> = Vec::new();
let mut indexes = HashMap::<PathBuf, usize>::new();
for session in sessions {
if let Some(index) = indexes.get(&session.cwd).copied() {
groups[index].sessions.push(session);
continue;
}
let index = groups.len();
indexes.insert(session.cwd.clone(), index);
groups.push(DirectoryGroup {
display: compact_cwd(&session.cwd),
cwd: session.cwd.clone(),
sessions: vec![session],
});
}
if let Some(position) = groups
.iter()
.position(|group| !is_cross_project(&group.cwd, current_cwd))
{
let current = groups.remove(position);
groups.insert(0, current);
}
groups
}
enum Slot {
Directory(DirectoryGroup),
Repo(usize),
}
pub(super) fn hub_groups(
sessions: Vec<SessionSummary>,
current_cwd: &Path,
place: impl Fn(&Path) -> Placement,
) -> Vec<HubGroup> {
let mut slots = Vec::new();
let mut repos: Vec<Vec<(DirectoryGroup, Workspace)>> = Vec::new();
let mut repo_indexes = HashMap::<PathBuf, usize>::new();
let mut missing = Vec::new();
for directory in directory_groups(sessions, current_cwd) {
match place(&directory.cwd) {
Placement::Missing => missing.push(directory),
Placement::Lone => slots.push(Slot::Directory(directory)),
Placement::Repo(workspace) => {
let index = *repo_indexes
.entry(workspace.repo.clone())
.or_insert_with(|| {
slots.push(Slot::Repo(repos.len()));
repos.push(Vec::new());
repos.len() - 1
});
repos[index].push((directory, workspace));
}
}
}
let mut groups = slots
.into_iter()
.map(|slot| match slot {
Slot::Directory(directory) => HubGroup::Directory(directory),
Slot::Repo(index) => repo_group(std::mem::take(&mut repos[index])),
})
.collect::<Vec<_>>();
if !missing.is_empty() {
groups.push(HubGroup::Missing(missing));
}
groups
}
fn repo_group(mut members: Vec<(DirectoryGroup, Workspace)>) -> HubGroup {
if members.len() == 1 {
return HubGroup::Directory(members.remove(0).0);
}
HubGroup::Repo {
display: compact_cwd(repo_checkout(&members[0].1.repo)),
worktrees: members
.into_iter()
.map(|(directory, workspace)| Worktree {
name: name_in_worktree(&directory.cwd, &workspace.worktree),
directory,
})
.collect(),
}
}
fn repo_checkout(repo: &Path) -> &Path {
match (repo.file_name(), repo.parent()) {
(Some(name), Some(parent)) if name == ".git" => parent,
_ => repo,
}
}
fn name_in_worktree(cwd: &Path, worktree: &Path) -> String {
let root = worktree.file_name().map_or_else(
|| compact_cwd(worktree),
|name| name.to_string_lossy().into_owned(),
);
match cwd.strip_prefix(worktree) {
Ok(rest) if !rest.as_os_str().is_empty() => format!("{root}/{}", rest.display()),
_ => root,
}
}
#[cfg(test)]
#[path = "sessions_hub_groups_tests.rs"]
mod tests;