use std::collections::{BTreeSet, HashMap, HashSet};
use diffler_core::classify::Kind;
use diffler_core::model::DiffModel;
use diffler_core::review::Review;
use diffler_core::session::Session;
use super::DiffView;
use super::slide::stop_file_index;
use crate::app::{App, Flow};
use crate::config::FileLayout;
use crate::tree::{self, Bucket, TreeNode, TreeRow};
#[derive(Debug, Clone)]
pub(crate) struct BucketFolds(BTreeSet<Bucket>);
impl Default for BucketFolds {
fn default() -> Self {
Self(
[
Bucket::Viewed,
Bucket::Kind(Kind::Generated),
Bucket::Kind(Kind::Assets),
]
.into(),
)
}
}
impl BucketFolds {
pub(super) fn is_folded(&self, bucket: Bucket) -> bool {
self.0.contains(&bucket)
}
pub(super) fn toggle_fold(&mut self, bucket: Bucket) {
if !self.0.remove(&bucket) {
self.0.insert(bucket);
}
}
pub(super) fn unfold(&mut self, bucket: Bucket) -> bool {
self.0.remove(&bucket)
}
}
impl DiffView {
pub(crate) fn tree_rows(&self, model: &DiffModel, session: &Session) -> Vec<TreeRow> {
match self.layout {
FileLayout::Review => self.section_rows(model, Self::review_groups(model, session)),
FileLayout::Kinds => self.section_rows(model, self.kind_groups(model, session)),
FileLayout::Walkthrough => match self.active_walkthrough(session) {
Some(walkthrough) => walkthrough
.summary
.is_some()
.then_some(TreeRow {
depth: 0,
node: TreeNode::WalkthroughSummary,
})
.into_iter()
.chain((0..walkthrough.stops.len()).map(|index| TreeRow {
depth: 0,
node: TreeNode::Stop { index },
}))
.collect(),
None => {
tree::visible_rows_promoting(&Self::paths(model), &self.folded_dirs, &|index| {
Self::is_viewed(model, session, index)
})
}
},
FileLayout::Tree | FileLayout::List => {
tree::visible_rows_promoting(&Self::paths(model), &self.folded_dirs, &|index| {
Self::is_viewed(model, session, index)
})
}
}
}
pub(crate) fn display_order(&self, model: &DiffModel, session: &Session) -> Vec<usize> {
match self.layout {
FileLayout::Review => Self::review_groups(model, session),
FileLayout::Kinds => self.kind_groups(model, session),
FileLayout::Walkthrough if self.active_walkthrough(session).is_some() => {
let stops = self
.active_walkthrough(session)
.into_iter()
.flat_map(|walkthrough| &walkthrough.stops);
let mut seen = HashSet::new();
return stops
.filter_map(|id| stop_file_index(model, session, id))
.filter(|index| seen.insert(*index))
.collect();
}
FileLayout::Walkthrough | FileLayout::Tree | FileLayout::List => {
let rows =
tree::visible_rows_promoting(&Self::paths(model), &BTreeSet::new(), &|index| {
Self::is_viewed(model, session, index)
});
return rows.iter().filter_map(row_file_index).collect();
}
}
.into_iter()
.flat_map(|(_, indices)| indices)
.collect()
}
fn paths(model: &DiffModel) -> Vec<&str> {
model.files.iter().map(|file| file.path.as_str()).collect()
}
fn is_viewed(model: &DiffModel, session: &Session, index: usize) -> bool {
model
.files
.get(index)
.is_some_and(|file| session.is_viewed(&file.path, &file.content_hash()))
}
fn viewed_first(model: &DiffModel, session: &Session, indices: &mut [usize]) {
indices.sort_by_key(|&index| !Self::is_viewed(model, session, index));
}
fn section_rows(
&self,
model: &DiffModel,
groups: impl IntoIterator<Item = (Bucket, Vec<usize>)>,
) -> Vec<TreeRow> {
let mut rows = Vec::new();
for (bucket, indices) in groups {
let folded = self.bucket_folds.is_folded(bucket);
rows.push(TreeRow {
depth: 0,
node: TreeNode::Section {
bucket,
count: indices.len(),
folded,
},
});
if folded {
continue;
}
rows.extend(indices.into_iter().filter_map(|index| {
let file = model.files.get(index)?;
Some(TreeRow {
depth: 1,
node: TreeNode::File {
index,
name: file.path.clone(),
},
})
}));
}
rows
}
fn review_groups(model: &DiffModel, session: &Session) -> Vec<(Bucket, Vec<usize>)> {
let (mut fresh, mut viewed) = (Vec::new(), Vec::new());
for index in 0..model.files.len() {
if Self::is_viewed(model, session, index) {
viewed.push(index);
} else {
fresh.push(index);
}
}
vec![(Bucket::ToReview, fresh), (Bucket::Viewed, viewed)]
}
fn kind_groups(&self, model: &DiffModel, session: &Session) -> Vec<(Bucket, Vec<usize>)> {
let mut grouped: HashMap<Kind, Vec<usize>> = HashMap::new();
for (index, file) in model.files.iter().enumerate() {
grouped
.entry(self.kind_of(&file.path))
.or_default()
.push(index);
}
Kind::ALL
.into_iter()
.filter_map(|kind| {
let mut indices = grouped.remove(&kind)?;
Self::viewed_first(model, session, &mut indices);
Some((Bucket::Kind(kind), indices))
})
.collect()
}
pub(crate) fn bucket_files(
&self,
model: &DiffModel,
session: &Session,
bucket: Bucket,
) -> Vec<usize> {
let groups = match bucket {
Bucket::Kind(_) => self.kind_groups(model, session),
Bucket::ToReview | Bucket::Viewed => Self::review_groups(model, session),
};
groups
.into_iter()
.find(|(known, _)| *known == bucket)
.map_or_else(Vec::new, |(_, indices)| indices)
}
pub(crate) fn kind_of(&self, path: &str) -> Kind {
self.rules.kind(path, self.declared.get(path).copied())
}
fn declared_targets(&self, review: &Review) -> Vec<String> {
if self.layout != FileLayout::Kinds {
return Vec::new();
}
self.model(review)
.files
.iter()
.map(|file| file.path.clone())
.collect()
}
pub(crate) fn cycle_layout(&mut self, has_walkthrough: bool, empty: bool) -> FileLayout {
self.layout = match self.layout {
FileLayout::Review => FileLayout::Kinds,
FileLayout::Kinds if has_walkthrough => FileLayout::Walkthrough,
FileLayout::Walkthrough if empty && has_walkthrough => FileLayout::Walkthrough,
FileLayout::Kinds | FileLayout::Walkthrough => FileLayout::Tree,
FileLayout::Tree | FileLayout::List => FileLayout::Review,
};
self.layout
}
#[cfg(test)]
pub(crate) fn sidebar_tree(&self, review: &Review) -> Vec<TreeRow> {
sidebar_rows(self, review)
}
pub(crate) fn reseat_tree_cursor(&mut self, rows: &[TreeRow]) {
self.tree_cursor = tree_position_of_file(rows, self.selected)
.unwrap_or_else(|| self.tree_cursor.min(rows.len().saturating_sub(1)));
}
}
pub(super) fn sidebar_rows(diff: &DiffView, review: &Review) -> Vec<TreeRow> {
diff.tree_rows(diff.model(review), review.session_for(&diff.source))
}
pub(super) fn next_unviewed_index(diff: &DiffView, review: &Review, wrap: bool) -> Option<usize> {
let model = diff.model(review);
let session = review.session_for(&diff.source);
let order = diff.display_order(model, session);
let count = order.len();
let at = order.iter().position(|&index| index == diff.selected)?;
(1..=count)
.map(|step| at + step)
.take_while(|&position| wrap || position < count)
.filter_map(|position| order.get(position % count).copied())
.find(|&index| !DiffView::is_viewed(model, session, index))
}
pub(super) fn row_file_index(row: &TreeRow) -> Option<usize> {
match row.node {
TreeNode::File { index, .. } => Some(index),
TreeNode::Dir { .. }
| TreeNode::Section { .. }
| TreeNode::Stop { .. }
| TreeNode::WalkthroughSummary => None,
}
}
pub(super) fn tree_position_of_file(rows: &[TreeRow], file_index: usize) -> Option<usize> {
rows.iter()
.position(|row| row_file_index(row) == Some(file_index))
}
pub(super) fn step_file_row(rows: &[TreeRow], at: usize, forward: bool) -> Option<usize> {
crate::app::step_to(rows, at, forward, |row| {
row_file_index(row).is_some() || matches!(row.node, TreeNode::Stop { .. })
})
}
#[derive(Debug, Clone)]
pub struct DeclaredRequest {
pub paths: Vec<String>,
pub token: u64,
}
impl App {
pub(crate) fn queue_declared(&mut self) {
let paths = self
.diff
.as_ref()
.map(|diff| diff.declared_targets(&self.review))
.unwrap_or_default();
self.declared_token += 1;
let token = self.declared_token;
self.pending_declared = (!paths.is_empty()).then_some(DeclaredRequest { paths, token });
}
pub(crate) fn on_declared_kinds(&mut self, kinds: HashMap<String, Kind>, token: u64) -> Flow {
let Some(diff) = self.diff.as_mut().filter(|_| token == self.declared_token) else {
return Flow::Idle;
};
if diff.declared == kinds {
return Flow::Idle;
}
diff.declared = kinds;
diff.invalidate();
diff.ensure_rows(&self.review);
Flow::Continue
}
}