diff --git a/src/ui/details_actions.rs b/src/ui/details_actions.rs index cb6a982..c9ced37 100644 --- a/src/ui/details_actions.rs +++ b/src/ui/details_actions.rs @@ -1,38 +1,91 @@ +use std::cmp::Ordering; + use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, - text::{Line, Span}, + text::{Line, Span, ToSpan}, widgets::{Block, Borders, Padding, Paragraph, Wrap}, Frame, }; use crate::app::{screens::details_actions::PatchsetAction, App}; -fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, actions_chunk: Rect) { +/// Get list of indexes representing the patches that were replied w/ the +/// "Reviewed-by:" tag, or that are staged to. +/// +/// # Arguments +/// +/// * `app`: Immutable reference to `patch-hub` model +/// +/// # Returns +/// +/// An owned `Vec` that is an ordered comma-separated list of the indexes. +/// Staged indexes are appended w/ `*` and colored `Color::DarkGrey`. If there +/// are no patches that were neither replied or staged w/ the tag, the return +/// string will be empty. +fn reviewed_by_list(app: &App) -> Vec { let patchset_details_and_actions = app.patchset_details_and_actions_state.as_ref().unwrap(); - let mut patches_to_reply = String::new(); + let number_offset = if patchset_details_and_actions.has_cover_letter { + 0 + } else { + 1 + }; + + let mut replied_inds: Vec = Vec::new(); + if let Some(already_reviewed_by) = app.reviewed_patchsets.get( + &patchset_details_and_actions + .representative_patch + .message_id() + .href, + ) { + replied_inds = already_reviewed_by + .iter() + .cloned() + .map(|i| i + number_offset) + .collect(); + replied_inds.sort_unstable(); + } + + let mut staged_inds: Vec = Vec::new(); if let Some(true) = patchset_details_and_actions .patchset_actions .get(&PatchsetAction::ReplyWithReviewedBy) { - patches_to_reply.push('('); - let number_offset = if patchset_details_and_actions.has_cover_letter { - 0 - } else { - 1 - }; - let patches_to_reply_numbers: Vec = patchset_details_and_actions + staged_inds = patchset_details_and_actions .patches_to_reply .iter() .enumerate() .filter_map(|(i, &val)| if val { Some(i + number_offset) } else { None }) .collect(); - for number in patches_to_reply_numbers { - patches_to_reply.push_str(&format!("{number},")); + staged_inds.sort_unstable(); + } + + let reviewed_by_inds = merge_reviewed_by_inds(replied_inds, staged_inds); + + let mut reviewed_by_list = Vec::new(); + // let mut reviewed_by_list = String::new(); + + for (index, is_replied) in reviewed_by_inds { + let mut entry = index.to_string(); + let mut entry_color = Style::default().fg(Color::White); + if !is_replied { + entry.push('*'); + entry_color = entry_color.fg(Color::DarkGray); } - patches_to_reply = format!("{})", &patches_to_reply[..patches_to_reply.len() - 1]); + reviewed_by_list.push(Span::styled(entry, entry_color)); + reviewed_by_list.push(Span::styled( + ", ".to_string(), + Style::default().fg(Color::White), + )); } + reviewed_by_list.pop(); + + reviewed_by_list +} + +fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, actions_chunk: Rect) { + let patchset_details_and_actions = app.patchset_details_and_actions_state.as_ref().unwrap(); let patchset_details = &patchset_details_and_actions.representative_patch; let mut patchset_details = vec![ @@ -72,11 +125,15 @@ fn render_details_and_actions(f: &mut Frame, app: &App, details_chunk: Rect, act ), ]), ]; - if !patches_to_reply.is_empty() { - patchset_details.push(Line::from(vec![ - Span::styled("Reviewed-by*: ", Style::default().fg(Color::Cyan)), - Span::styled(patches_to_reply, Style::default().fg(Color::DarkGray)), - ])); + let mut reviewed_by = reviewed_by_list(app); + if !reviewed_by.is_empty() { + let mut reviewed_by_line = vec![ + Span::styled("Reviewed-by: ", Style::default().fg(Color::Cyan)), + '('.to_span(), + ]; + reviewed_by_line.append(&mut reviewed_by); + reviewed_by_line.push(')'.to_span()); + patchset_details.push(Line::from(reviewed_by_line)); } let patchset_details = Paragraph::new(patchset_details) @@ -224,3 +281,64 @@ pub fn keys_hint() -> Span<'static> { Style::default().fg(Color::Red), ) } + +/// Receives two `usize` slices representing the patch indexes tagged w/ +/// "Reviewed-by", preceding those that were replied over those that are only +/// staged. +/// +/// # Arguments +/// +/// * `replied_inds`: Ordered slice of patch indexes that were replied +/// * `staged_inds`: Ordered slice of patch indexes that staged to be replied +/// +/// # Returns +/// +/// This function always returns a `Vec<(usize, bool)>` (empty or not). The +/// tuples `(index, is_replied)` encode the precedence, in which `is_replied == +/// true` means that patch of number `index` was replied w/ the tag. +fn merge_reviewed_by_inds( + mut replied_inds: Vec, + mut staged_inds: Vec, +) -> Vec<(usize, bool)> { + let mut reviewed_by_inds = Vec::new(); + let mut i = 0; + let mut j = 0; + + // Don't assume the caller ordered the vectors beforehand + replied_inds.sort(); + staged_inds.sort(); + + while (i != replied_inds.len()) && (j != staged_inds.len()) { + match replied_inds[i].cmp(&staged_inds[j]) { + Ordering::Less => { + reviewed_by_inds.push((replied_inds[i], true)); + i += 1; + } + Ordering::Equal => { + reviewed_by_inds.push((replied_inds[i], true)); + i += 1; + j += 1; + } + Ordering::Greater => { + reviewed_by_inds.push((staged_inds[j], false)); + j += 1; + } + } + } + + let mut remaining_inds: Vec<(usize, bool)>; + if i != replied_inds.len() { + remaining_inds = replied_inds[i..] + .iter() + .map(|&index| (index, true)) + .collect(); + } else { + remaining_inds = staged_inds[j..] + .iter() + .map(|&index| (index, false)) + .collect(); + } + reviewed_by_inds.append(&mut remaining_inds); + + reviewed_by_inds +}