use std::borrow::Cow;
use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
};
use crate::app::{App, DiffSource, FocusedPanel, InputMode, Message, MessageType};
use crate::theme::Theme;
use crate::ui::commit_row::CURSOR_GLYPH;
use crate::ui::styles;
const COMMAND_COMPLETION_MAX_ROWS: usize = 7;
pub fn build_message_span(message: Option<&Message>, theme: &Theme) -> (Span<'static>, usize) {
if let Some(msg) = message {
let (fg, bg) = match msg.message_type {
MessageType::Info => (theme.message_info_fg, theme.message_info_bg),
MessageType::Warning => (theme.message_warning_fg, theme.message_warning_bg),
MessageType::Error => (theme.message_error_fg, theme.message_error_bg),
};
let content = format!(" {} ", msg.content);
let width = content.len();
(
Span::styled(
content,
Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
),
width,
)
} else {
(Span::raw(""), 0)
}
}
pub fn build_right_aligned_spans<'a>(
mut left_spans: Vec<Span<'a>>,
message_span: Span<'a>,
message_width: usize,
total_width: usize,
) -> Vec<Span<'a>> {
let left_width: usize = left_spans.iter().map(|s| s.content.len()).sum();
let padding_width = total_width.saturating_sub(left_width + message_width);
let padding = Span::raw(" ".repeat(padding_width));
left_spans.push(padding);
if message_width > 0 {
left_spans.push(message_span);
}
left_spans
}
pub fn render_header(frame: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let in_pr_mode = matches!(app.diff_source, DiffSource::PullRequest(_));
let brand = Span::styled(
" tuicr ",
Style::default()
.fg(theme.fg_primary)
.add_modifier(Modifier::BOLD),
);
let mut chunks: Vec<String> = Vec::new();
if in_pr_mode {
chunks.push("PR Mode".to_string());
} else {
let vcs_type = &app.vcs_info.vcs_type;
let branch = app.vcs_info.branch_name.as_deref().unwrap_or("detached");
chunks.push(format!("{vcs_type}:{branch}"));
}
if let Some(source) = header_source_chunk(app) {
chunks.push(source);
}
if app.is_single_file_view {
chunks.push("FOCUS".to_string());
}
if let Some(slug) = app.session_slug() {
chunks.push(slug);
}
if app.is_pristine_mode {
let head_label = app
.vcs_info
.head_commit
.strip_prefix("pristine:")
.and_then(|rest| rest.split(':').next())
.map(|raw| if raw == "none" { "uncommitted" } else { raw })
.unwrap_or("?");
chunks.push(format!(
"PRISTINE \u{00b7} {} \u{00b7} {} files",
head_label,
app.diff_files.len()
));
}
let source_text = if chunks.is_empty() {
String::new()
} else {
format!(" {} ", chunks.join(" \u{00b7} "))
};
let source_width = source_text.chars().count();
let source_span = Span::styled(source_text, Style::default().fg(theme.fg_secondary));
let (update_span, update_width) = match app.update_info.as_ref() {
Some(info) if info.update_available => {
let text = format!(" v{} available ", info.latest_version);
let width = text.chars().count();
(
Span::styled(
text,
Style::default()
.fg(theme.update_badge_fg)
.bg(theme.update_badge_bg)
.add_modifier(Modifier::BOLD),
),
width,
)
}
Some(info) if info.is_ahead => {
let text = format!(" unreleased v{} ", info.current_version);
let width = text.chars().count();
(
Span::styled(
text,
Style::default()
.fg(theme.update_badge_fg)
.bg(theme.update_badge_bg)
.add_modifier(Modifier::BOLD),
),
width,
)
}
_ => (Span::raw(""), 0),
};
let total_width = area.width as usize;
let brand_width = brand.content.chars().count();
let right_width = source_width + update_width;
let pad_width = total_width.saturating_sub(brand_width + right_width);
let mut spans = vec![brand, Span::raw(" ".repeat(pad_width)), source_span];
if update_width > 0 {
spans.push(update_span);
}
frame.render_widget(
Paragraph::new(Line::from(spans)).style(styles::status_bar_style(theme)),
area,
);
}
fn head_commit_label(app: &App) -> Option<String> {
if app.is_pristine_mode {
return None;
}
let head = app.vcs_info.head_commit.as_str();
(!head.is_empty()).then(|| head[..7.min(head.len())].to_string())
}
fn with_head_commit(label: &str, app: &App) -> String {
match head_commit_label(app) {
Some(head) => format!("{label} \u{00b7} commit {head}"),
None => label.to_string(),
}
}
fn header_source_chunk(app: &App) -> Option<String> {
match &app.diff_source {
DiffSource::WorkingTree => head_commit_label(app).map(|head| format!("commit {head}")),
DiffSource::Staged => Some(with_head_commit("staged", app)),
DiffSource::Unstaged => Some(with_head_commit("unstaged", app)),
DiffSource::StagedAndUnstaged => Some(with_head_commit("staged + unstaged", app)),
DiffSource::CommitRange(commits) => {
if commits.len() == 1 {
Some(format!("commit {}", &commits[0][..7.min(commits[0].len())]))
} else {
Some(
app.commit_selection_summary()
.unwrap_or_else(|| format!("{} commits", commits.len())),
)
}
}
DiffSource::StagedUnstagedAndCommits(commits) => {
if commits.len() == 1 {
Some(format!(
"staged + unstaged + commit {}",
&commits[0][..7.min(commits[0].len())]
))
} else {
Some(format!("staged + unstaged + {} commits", commits.len()))
}
}
DiffSource::PullRequest(pr) => {
let slug = pr.key.repository.display_name();
let trimmed_title = if pr.title.chars().count() > 60 {
let truncated: String = pr.title.chars().take(59).collect();
format!("{truncated}\u{2026}")
} else {
pr.title.clone()
};
let mut s = format!(
"{slug}#{number} \u{00b7} {trimmed_title}",
number = pr.key.number
);
if app.pr_commits.len() > 1
&& let Some(summary) = app.commit_selection_summary()
{
s.push_str(&format!(" \u{00b7} {summary}"));
}
Some(s)
}
}
}
pub fn render_status_bar(frame: &mut Frame, app: &App, area: Rect) {
let theme = &app.theme;
let left_spans = if matches!(app.input_mode, InputMode::Command | InputMode::Search) {
let prefix = if app.input_mode == InputMode::Command {
":"
} else {
"/"
};
let buffer = if app.input_mode == InputMode::Command {
&app.command_buffer
} else {
&app.search_buffer
};
let command_text = format!("{prefix}{buffer}");
vec![Span::styled(
command_text,
Style::default().fg(theme.fg_primary),
)]
} else {
let mode_str = match app.input_mode {
InputMode::Normal => {
if let Some(count) = app.pending_count {
format!(" NORMAL {count} ")
} else {
" NORMAL ".to_string()
}
}
InputMode::Command => " COMMAND ".to_string(),
InputMode::Search => " SEARCH ".to_string(),
InputMode::Comment => " COMMENT ".to_string(),
InputMode::Help => " HELP ".to_string(),
InputMode::Confirm => " CONFIRM ".to_string(),
InputMode::CommitSelect => " SELECT ".to_string(),
InputMode::VisualSelect => {
if let Some((range, _)) = app.visual_selection_line_range() {
if range.is_single() {
format!(" VISUAL L{} ", range.start)
} else {
format!(" VISUAL L{}-L{} ", range.start, range.end)
}
} else {
" VISUAL ".to_string()
}
}
InputMode::SubmitResolver => " RESOLVE ".to_string(),
InputMode::SubmitConfirm => " SUBMIT ".to_string(),
InputMode::SubmitActionPicker => " SUBMIT ".to_string(),
};
let mode_span = Span::styled(mode_str, styles::mode_style(theme));
let hints: Cow<'static, str> = if app.message.is_some() {
Cow::Borrowed("")
} else if app.file_tree_prompt_editing() {
Cow::Borrowed(" \u{21b5} apply \u{00b7} esc cancel")
} else {
match app.input_mode {
InputMode::Normal if app.focused_panel == FocusedPanel::FileList => Cow::Borrowed(
" j/k move \u{00b7} \u{21b5} open \u{00b7} i/e filter \u{00b7} I/E clear \u{00b7} / search \u{00b7} r reviewed",
),
InputMode::Normal => Cow::Borrowed(
" j/k scroll \u{00b7} {/} file \u{00b7} m/M comment \u{00b7} r file \u{00b7} R hunk \u{00b7} c comment \u{00b7} ? help",
),
InputMode::Command => {
Cow::Borrowed(" tab complete \u{00b7} \u{21b5} execute \u{00b7} esc cancel")
}
InputMode::Search => Cow::Borrowed(" \u{21b5} search \u{00b7} esc cancel"),
InputMode::Comment => Cow::Borrowed(" ctrl-s save \u{00b7} esc cancel"),
InputMode::Help => Cow::Borrowed(" / search · n/N match · q/?/esc close"),
InputMode::Confirm => Cow::Borrowed(" y yes \u{00b7} n no"),
InputMode::CommitSelect => Cow::Borrowed(
" j/k navigate \u{00b7} space select \u{00b7} \u{21b5} confirm \u{00b7} esc back",
),
InputMode::VisualSelect => Cow::Borrowed(
" j/k extend \u{00b7} c/\u{21b5} comment \u{00b7} y yank \u{00b7} esc/V cancel",
),
InputMode::SubmitResolver => Cow::Borrowed(
" j/k move \u{00b7} \u{21b5} toggle \u{00b7} s submit \u{00b7} esc cancel",
),
InputMode::SubmitConfirm => {
Cow::Borrowed(" y submit \u{00b7} n cancel \u{00b7} esc cancel")
}
InputMode::SubmitActionPicker => {
Cow::Borrowed(" j/k move \u{00b7} \u{21b5} submit \u{00b7} esc cancel")
}
}
};
let hints_span = Span::styled(hints, Style::default().fg(theme.fg_secondary));
vec![mode_span, hints_span]
};
let (right_span, right_width) = if app.message.is_some() {
build_message_span(app.message.as_ref(), theme)
} else if let Some(submit) = app.pr_submit_state.as_ref() {
use crate::forge::submit::SubmitEvent;
let glyph = crate::ui::selector::pr_open_spinner_glyph(submit.started_at.elapsed());
let label = match submit.event {
SubmitEvent::Draft => "Pushing pending review…",
_ => "Submitting review…",
};
let content = format!(" {glyph} {label} ");
let width = content.chars().count();
(
Span::styled(
content,
Style::default()
.fg(theme.message_info_fg)
.bg(theme.message_info_bg)
.add_modifier(Modifier::BOLD),
),
width,
)
} else if let Some(reload) = app.pr_reload_state.as_ref() {
let glyph = crate::ui::selector::pr_open_spinner_glyph(reload.started_at.elapsed());
let content = format!(" {glyph} Reloading PR… ");
let width = content.chars().count();
(
Span::styled(
content,
Style::default()
.fg(theme.message_info_fg)
.bg(theme.message_info_bg)
.add_modifier(Modifier::BOLD),
),
width,
)
} else if let Some(range) = app.pr_range_reload_state.as_ref() {
let glyph = crate::ui::selector::pr_open_spinner_glyph(range.started_at.elapsed());
let content = format!(" {glyph} Loading range diff… ");
let width = content.chars().count();
(
Span::styled(
content,
Style::default()
.fg(theme.message_info_fg)
.bg(theme.message_info_bg)
.add_modifier(Modifier::BOLD),
),
width,
)
} else if app.forge_review_threads_loading {
let content = " loading remote comments\u{2026} ".to_string();
let width = content.chars().count();
(
Span::styled(content, Style::default().fg(theme.fg_dim)),
width,
)
} else if app.dirty {
let content = " \u{2022} modified ".to_string();
let width = content.chars().count();
(
Span::styled(content, Style::default().fg(theme.pending)),
width,
)
} else {
(Span::raw(""), 0)
};
let total_width = area.width as usize;
let spans = build_right_aligned_spans(left_spans, right_span, right_width, total_width);
let line = Line::from(spans);
let status = Paragraph::new(line)
.style(styles::status_bar_style(theme))
.block(Block::default());
frame.render_widget(status, area);
}
pub fn render_command_completion_popup(frame: &mut Frame, app: &App, status_area: Rect) {
if app.input_mode != InputMode::Command {
return;
}
let Some(completion) = app.command_completion.as_ref() else {
return;
};
if completion.matches.is_empty() || status_area.y < 3 || status_area.width < 4 {
return;
}
let available_rows = status_area.y.saturating_sub(2) as usize;
let visible_rows = completion
.matches
.len()
.min(COMMAND_COMPLETION_MAX_ROWS)
.min(available_rows);
if visible_rows == 0 {
return;
}
let selected = completion
.selected
.min(completion.matches.len().saturating_sub(1));
let start = completion_window_start(selected, completion.matches.len(), visible_rows);
let end = start + visible_rows;
let visible_matches = &completion.matches[start..end];
let content_width = visible_matches
.iter()
.map(|command| command.chars().count())
.max()
.unwrap_or(0)
+ 2;
let popup_width = (content_width as u16 + 2).min(status_area.width);
if popup_width == 0 {
return;
}
let popup_height = visible_rows as u16 + 2;
let popup_area = Rect {
x: status_area.x,
y: status_area.y.saturating_sub(popup_height),
width: popup_width,
height: popup_height,
};
let rows: Vec<Line<'_>> = visible_matches
.iter()
.enumerate()
.map(|(offset, command)| {
let idx = start + offset;
let marker = if idx == selected { CURSOR_GLYPH } else { " " };
let style = if idx == selected {
styles::selected_style(&app.theme)
} else {
Style::default().fg(app.theme.fg_secondary)
};
Line::from(Span::styled(format!("{marker} {command}"), style))
})
.collect();
frame.render_widget(Clear, popup_area);
frame.render_widget(
Paragraph::new(rows)
.style(styles::panel_style(&app.theme))
.block(Block::default().borders(Borders::ALL)),
popup_area,
);
}
fn completion_window_start(selected: usize, match_count: usize, visible_rows: usize) -> usize {
if visible_rows >= match_count {
return 0;
}
let half = visible_rows / 2;
selected
.saturating_sub(half)
.min(match_count.saturating_sub(visible_rows))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_message(message_type: MessageType) -> Message {
Message {
content: "hello".to_string(),
message_type,
expires_at: None,
}
}
#[test]
fn should_style_info_message_using_theme_fields() {
let theme = Theme::dark();
let (span, width) = build_message_span(Some(&test_message(MessageType::Info)), &theme);
assert_eq!(span.style.fg, Some(theme.message_info_fg));
assert_eq!(span.style.bg, Some(theme.message_info_bg));
assert_eq!(width, " hello ".len());
}
#[test]
fn should_return_empty_span_when_message_is_none() {
let theme = Theme::dark();
let (span, width) = build_message_span(None, &theme);
assert_eq!(span.content.as_ref(), "");
assert_eq!(width, 0);
}
#[test]
fn should_center_completion_window_when_possible() {
assert_eq!(completion_window_start(5, 12, 7), 2);
}
#[test]
fn should_pin_completion_window_to_top_near_start() {
assert_eq!(completion_window_start(1, 12, 7), 0);
}
#[test]
fn should_pin_completion_window_to_bottom_near_end() {
assert_eq!(completion_window_start(10, 12, 7), 5);
}
#[test]
fn should_show_all_completion_rows_when_they_fit() {
assert_eq!(completion_window_start(3, 5, 7), 0);
}
#[test]
fn should_style_warning_message_using_theme_fields() {
let theme = Theme::dark();
let (span, _) = build_message_span(Some(&test_message(MessageType::Warning)), &theme);
assert_eq!(span.style.fg, Some(theme.message_warning_fg));
assert_eq!(span.style.bg, Some(theme.message_warning_bg));
}
#[test]
fn should_style_error_message_using_theme_fields() {
let theme = Theme::dark();
let (span, _) = build_message_span(Some(&test_message(MessageType::Error)), &theme);
assert_eq!(span.style.fg, Some(theme.message_error_fg));
assert_eq!(span.style.bg, Some(theme.message_error_bg));
}
}
#[cfg(test)]
mod header_snapshot_tests {
use crate::app::{App, DiffSource, InputMode, PullRequestDiffSource};
use crate::error::Result as TuicrResult;
use crate::error::TuicrError;
use crate::forge::traits::{ForgeRepository, PrSessionKey};
use crate::model::{DiffFile, DiffLine, FileStatus, ReviewSession, SessionDiffSource};
use crate::syntax::SyntaxHighlighter;
use crate::theme::Theme;
use crate::vcs::traits::{VcsBackend, VcsInfo, VcsType};
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use ratatui::buffer::Buffer;
use std::path::{Path, PathBuf};
struct NoopVcs {
info: VcsInfo,
}
impl VcsBackend for NoopVcs {
fn info(&self) -> &VcsInfo {
&self.info
}
fn get_working_tree_diff(
&self,
_highlighter: &SyntaxHighlighter,
) -> TuicrResult<Vec<DiffFile>> {
Err(TuicrError::NoChanges)
}
fn fetch_context_lines(
&self,
_file_path: &Path,
_file_status: FileStatus,
_ref_commit: Option<&str>,
_start_line: u32,
_end_line: u32,
) -> TuicrResult<Vec<DiffLine>> {
Ok(Vec::new())
}
fn file_line_count(
&self,
_file_path: &Path,
_file_status: FileStatus,
_ref_commit: Option<&str>,
) -> TuicrResult<u32> {
Ok(0)
}
}
fn pr_source(closed: bool, merged: bool) -> PullRequestDiffSource {
PullRequestDiffSource {
key: PrSessionKey::new(
ForgeRepository::github("github.com", "agavra", "tuicr"),
125,
"abcdef0123456789".to_string(),
),
base_sha: "1234567890abcdef".to_string(),
title: "Add forge-backed PR review".to_string(),
url: "https://github.com/agavra/tuicr/pull/125".to_string(),
head_ref_name: "reviews".to_string(),
base_ref_name: "main".to_string(),
state: if closed { "CLOSED" } else { "OPEN" }.to_string(),
closed,
merged,
}
}
fn build_pr_app(pr: PullRequestDiffSource) -> App {
let vcs_info = VcsInfo {
root_path: PathBuf::from("forge:github.com/agavra/tuicr"),
head_commit: pr.key.head_sha.clone(),
branch_name: Some(pr.head_ref_name.clone()),
vcs_type: VcsType::File,
};
let mut session = ReviewSession::new(
vcs_info.root_path.clone(),
pr.key.head_sha.clone(),
Some(pr.head_ref_name.clone()),
SessionDiffSource::PullRequest,
);
session.pr_session_key = Some(pr.key.clone());
App::build(
Box::new(NoopVcs {
info: vcs_info.clone(),
}),
vcs_info,
Theme::dark(),
None,
false,
Vec::new(),
session,
DiffSource::PullRequest(Box::new(pr)),
InputMode::Normal,
Vec::new(),
None,
None,
)
.expect("build pr app")
}
fn draw_header(app: &App) -> Buffer {
let backend = TestBackend::new(140, 3);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| {
let area = frame.area();
super::render_header(frame, app, area);
})
.expect("draw frame");
terminal.backend().buffer().clone()
}
fn row_text(buffer: &Buffer, y: u16) -> String {
(0..buffer.area.width)
.map(|x| buffer[(x, y)].symbol().to_string())
.collect()
}
#[test]
fn should_render_pr_mode_header_with_slug_number_and_title() {
let app = build_pr_app(pr_source(false, false));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("tuicr"), "got: {line:?}");
assert!(line.contains("PR Mode"), "got: {line:?}");
assert!(line.contains("agavra/tuicr#125"), "got: {line:?}");
assert!(line.contains("Add forge-backed PR review"), "got: {line:?}");
}
#[test]
fn should_not_show_read_only_badge_for_closed_pr() {
let app = build_pr_app(pr_source(true, false));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("PR Mode"), "got: {line:?}");
assert!(!line.contains("read only"), "got: {line:?}");
}
#[test]
fn should_not_show_read_only_badge_for_merged_pr() {
let app = build_pr_app(pr_source(false, true));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("PR Mode"), "got: {line:?}");
assert!(!line.contains("read only"), "got: {line:?}");
}
#[test]
fn should_omit_read_only_badge_for_open_pr() {
let app = build_pr_app(pr_source(false, false));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(!line.contains("read only"), "got: {line:?}");
}
fn fake_pr_commit(oid: &str, summary: &str) -> crate::forge::traits::PullRequestCommit {
crate::forge::traits::PullRequestCommit {
oid: oid.to_string(),
short_oid: oid[..7.min(oid.len())].to_string(),
summary: summary.to_string(),
author: "Alice".to_string(),
timestamp: None,
}
}
#[test]
fn should_render_n_of_m_commits_when_subset_selected() {
let mut app = build_pr_app(pr_source(false, false));
app.pr_commits = vec![
fake_pr_commit("aaaaaaa1", "third"),
fake_pr_commit("bbbbbbb2", "second"),
fake_pr_commit("ccccccc3", "first"),
];
app.review_commits = app
.pr_commits
.iter()
.map(crate::app::pr_commit_to_commit_info)
.collect();
app.commit_selection_range = Some((0, 1));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("2 of 3 commits"), "got: {line:?}");
}
#[test]
fn should_render_commit_position_when_single_commit_selected() {
let mut app = build_pr_app(pr_source(false, false));
app.pr_commits = vec![
fake_pr_commit("aaaaaaa1", "third"),
fake_pr_commit("bbbbbbb2", "second"),
fake_pr_commit("ccccccc3", "first"),
];
app.review_commits = app
.pr_commits
.iter()
.map(crate::app::pr_commit_to_commit_info)
.collect();
app.commit_selection_range = Some((1, 1));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("commit 2/3"), "got: {line:?}");
}
#[test]
fn should_omit_commits_label_when_full_range_selected() {
let mut app = build_pr_app(pr_source(false, false));
app.pr_commits = vec![
fake_pr_commit("a", "third"),
fake_pr_commit("b", "second"),
fake_pr_commit("c", "first"),
];
app.commit_selection_range = Some((0, 2));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(!line.contains(" of 3 commits"), "got: {line:?}");
}
#[test]
fn should_omit_commits_label_for_single_commit_pr() {
let mut app = build_pr_app(pr_source(false, false));
app.pr_commits = vec![fake_pr_commit("a", "only commit")];
app.commit_selection_range = Some((0, 0));
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(!line.contains(" of "), "got: {line:?}");
}
#[test]
fn should_not_panic_on_long_multibyte_title() {
let mut pr = pr_source(false, false);
pr.title =
"プルリクエストのタイトルが非常に長くて六十文字を超える場合でも表示が壊れずに正しく切り詰められることを確認するためのテストです"
.to_string();
let app = build_pr_app(pr);
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("agavra/tuicr#125"), "got: {line:?}");
}
fn build_local_app(diff_source: DiffSource, head_commit: &str) -> App {
let vcs_info = VcsInfo {
root_path: PathBuf::from("/repo"),
head_commit: head_commit.to_string(),
branch_name: Some("main".to_string()),
vcs_type: VcsType::Git,
};
let session = ReviewSession::new(
vcs_info.root_path.clone(),
vcs_info.head_commit.clone(),
vcs_info.branch_name.clone(),
SessionDiffSource::WorkingTree,
);
App::build(
Box::new(NoopVcs {
info: vcs_info.clone(),
}),
vcs_info,
Theme::dark(),
None,
false,
Vec::new(),
session,
diff_source,
InputMode::Normal,
Vec::new(),
None,
None,
)
.expect("build local app")
}
#[test]
fn should_show_head_commit_when_reviewing_the_working_tree() {
let app = build_local_app(DiffSource::WorkingTree, "abcdef0123456789");
assert_eq!(
super::header_source_chunk(&app),
Some("commit abcdef0".to_string())
);
}
#[test]
fn should_show_head_commit_alongside_staged_labels() {
let staged = build_local_app(DiffSource::Staged, "abcdef0123456789");
assert_eq!(
super::header_source_chunk(&staged),
Some("staged \u{00b7} commit abcdef0".to_string())
);
let both = build_local_app(DiffSource::StagedAndUnstaged, "abcdef0123456789");
assert_eq!(
super::header_source_chunk(&both),
Some("staged + unstaged \u{00b7} commit abcdef0".to_string())
);
}
#[test]
fn should_omit_head_commit_when_repository_has_no_head() {
let app = build_local_app(DiffSource::WorkingTree, "");
assert_eq!(super::header_source_chunk(&app), None);
let staged = build_local_app(DiffSource::Staged, "");
assert_eq!(
super::header_source_chunk(&staged),
Some("staged".to_string())
);
}
#[test]
fn should_not_duplicate_commit_for_a_revision_review() {
let app = build_local_app(
DiffSource::CommitRange(vec!["fedcba9876543210".to_string()]),
"abcdef0123456789",
);
assert_eq!(
super::header_source_chunk(&app),
Some("commit fedcba9".to_string())
);
}
#[test]
fn should_render_head_commit_in_the_header() {
let app = build_local_app(DiffSource::WorkingTree, "abcdef0123456789");
let buffer = draw_header(&app);
let line = row_text(&buffer, 0);
assert!(line.contains("commit abcdef0"), "got: {line:?}");
}
}