use super::{
apply_line_bg, apply_spans_bg, clear_leading_ws_bg, diff_line_bg, expand_tabs_in_spans,
pad_spans_bg, pending_tail_text, render_empty_state, render_review_note_avatar,
review_note_block, review_note_lines, slice_spans, spans_to_text, spans_width, truncate_text,
view_spans_to_text, wrap_count_for_spans, wrap_count_for_text, TAB_WIDTH,
};
use crate::app::{is_conflict_marker, is_fold_line, AnimationPhase, App};
use crate::color;
use crate::config::{DiffForegroundMode, DiffHighlightMode};
use crate::syntax::SyntaxSide;
use oyo_core::{
AnimationFrame, ChangeKind, LineKind, StepDirection, ViewLine, ViewSpan, ViewSpanKind,
};
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Paragraph, Wrap},
Frame,
};
fn split_old_bg_kind(kind: LineKind) -> LineKind {
match kind {
LineKind::Modified | LineKind::PendingModify => LineKind::Deleted,
_ => kind,
}
}
fn split_new_bg_kind(kind: LineKind) -> LineKind {
match kind {
LineKind::Modified | LineKind::PendingModify => LineKind::Inserted,
_ => kind,
}
}
fn line_num_style_for_kind(kind: LineKind, app: &App) -> Style {
let insert_base = color::gradient_color(&app.theme.insert, 0.5);
let delete_base = color::gradient_color(&app.theme.delete, 0.5);
let modify_base = color::gradient_color(&app.theme.modify, 0.5);
match kind {
LineKind::Inserted | LineKind::PendingInsert => {
Style::default().fg(Color::Rgb(insert_base.r, insert_base.g, insert_base.b))
}
LineKind::Deleted | LineKind::PendingDelete => {
Style::default().fg(Color::Rgb(delete_base.r, delete_base.g, delete_base.b))
}
LineKind::Modified | LineKind::PendingModify => {
Style::default().fg(Color::Rgb(modify_base.r, modify_base.g, modify_base.b))
}
LineKind::Context => Style::default().fg(app.theme.diff_line_number),
}
}
fn align_fill_color(app: &App) -> Color {
color::dim_color(app.theme.text_muted)
}
fn align_fill_span(app: &App, width: usize) -> Span<'static> {
if width == 0 || app.split_align_fill.is_empty() {
return Span::raw("");
}
let full_len = if app.line_wrap {
width
} else {
width.saturating_add(app.horizontal_scroll)
};
let mut out = String::with_capacity(full_len);
for ch in app.split_align_fill.chars().cycle().take(full_len) {
out.push(ch);
}
let text = if app.line_wrap {
out
} else {
out.chars()
.skip(app.horizontal_scroll)
.take(width)
.collect()
};
Span::styled(
text,
Style::default()
.fg(align_fill_color(app))
.add_modifier(Modifier::DIM),
)
}
fn align_fill_gutter_span(app: &App, width: usize) -> Span<'static> {
if width == 0 || app.split_align_fill.is_empty() {
return Span::raw(" ".repeat(width));
}
let mut out = String::with_capacity(width);
for ch in app.split_align_fill.chars().cycle().take(width) {
out.push(ch);
}
Span::styled(
out,
Style::default()
.fg(align_fill_color(app))
.add_modifier(Modifier::DIM),
)
}
#[allow(clippy::too_many_arguments)]
fn push_virtual_line_old(
text: &str,
app: &App,
line_number_width: u16,
visible_width: usize,
max_line_width: &mut usize,
content_lines: &mut Vec<Line>,
gutter_lines: &mut Vec<Line>,
bg_lines: Option<&mut Vec<Line<'static>>>,
) -> usize {
let virtual_style = Style::default()
.fg(app.theme.text_muted)
.add_modifier(Modifier::ITALIC);
let mut virtual_spans = vec![Span::styled(text.to_string(), virtual_style)];
virtual_spans = expand_tabs_in_spans(&virtual_spans, TAB_WIDTH);
let virtual_width = spans_width(&virtual_spans);
*max_line_width = (*max_line_width).max(virtual_width);
let virtual_wrap = if app.line_wrap {
wrap_count_for_spans(&virtual_spans, visible_width)
} else {
1
};
let mut display_virtual = virtual_spans;
if !app.line_wrap {
display_virtual = slice_spans(&display_virtual, app.horizontal_scroll, visible_width);
}
if let Some(bg_lines) = bg_lines {
super::push_wrapped_bg_line(bg_lines, visible_width, virtual_wrap, None);
}
content_lines.push(Line::from(display_virtual));
gutter_lines.push(blank_gutter_line(line_number_width));
if app.line_wrap && virtual_wrap > 1 {
for _ in 1..virtual_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
}
}
virtual_wrap
}
#[allow(clippy::too_many_arguments)]
fn push_virtual_line_new(
text: &str,
app: &App,
line_number_width: u16,
visible_width: usize,
max_line_width: &mut usize,
content_lines: &mut Vec<Line>,
gutter_lines: &mut Vec<Line>,
marker_lines: &mut Vec<Line>,
bg_lines: Option<&mut Vec<Line<'static>>>,
) -> usize {
let virtual_style = Style::default()
.fg(app.theme.text_muted)
.add_modifier(Modifier::ITALIC);
let mut virtual_spans = vec![Span::styled(text.to_string(), virtual_style)];
virtual_spans = expand_tabs_in_spans(&virtual_spans, TAB_WIDTH);
let virtual_width = spans_width(&virtual_spans);
*max_line_width = (*max_line_width).max(virtual_width);
let virtual_wrap = if app.line_wrap {
wrap_count_for_spans(&virtual_spans, visible_width)
} else {
1
};
let mut display_virtual = virtual_spans;
if !app.line_wrap {
display_virtual = slice_spans(&display_virtual, app.horizontal_scroll, visible_width);
}
if let Some(bg_lines) = bg_lines {
super::push_wrapped_bg_line(bg_lines, visible_width, virtual_wrap, None);
}
content_lines.push(Line::from(display_virtual));
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
if app.line_wrap && virtual_wrap > 1 {
for _ in 1..virtual_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
}
}
virtual_wrap
}
#[allow(clippy::too_many_arguments)]
fn push_review_note_lines_old(
note_lines: &[Line<'static>],
_anchor_key: &str,
app: &App,
line_number_width: u16,
visible_width: usize,
max_line_width: &mut usize,
content_lines: &mut Vec<Line>,
gutter_lines: &mut Vec<Line>,
mut bg_lines: Option<&mut Vec<Line<'static>>>,
) -> usize {
let mut row_span = 0usize;
let gutter_width = split_gutter_width(line_number_width) as usize;
let total_width = visible_width.saturating_add(gutter_width);
for note_line in note_lines {
let mut spans = vec![Span::raw(" ")];
spans.extend(note_line.spans.clone());
let mut spans = expand_tabs_in_spans(&spans, TAB_WIDTH);
if app.line_wrap {
spans = super::fit_review_note_footer(spans, total_width);
}
*max_line_width = (*max_line_width).max(spans_width(&spans).saturating_sub(gutter_width));
let wrap = if app.line_wrap {
super::review_note_wrap_count(&spans, total_width)
} else {
1
};
row_span = row_span.saturating_add(wrap);
if let Some(lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(lines, visible_width, wrap, None);
}
let gutter_spans = slice_spans(&spans, 0, gutter_width);
let content_spans = slice_spans(&spans, gutter_width, visible_width);
content_lines.push(Line::from(content_spans));
gutter_lines.push(Line::from(gutter_spans));
if app.line_wrap && wrap > 1 {
for _ in 1..wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
}
}
}
row_span
}
#[allow(clippy::too_many_arguments)]
fn push_review_note_lines_new(
note_lines: &[Line<'static>],
_anchor_key: &str,
app: &App,
line_number_width: u16,
visible_width: usize,
max_line_width: &mut usize,
content_lines: &mut Vec<Line>,
gutter_lines: &mut Vec<Line>,
marker_lines: &mut Vec<Line>,
mut bg_lines: Option<&mut Vec<Line<'static>>>,
) -> usize {
let mut row_span = 0usize;
let gutter_width = split_gutter_width(line_number_width) as usize;
let total_width = visible_width.saturating_add(gutter_width);
for note_line in note_lines {
let mut spans = vec![Span::raw(" ")];
spans.extend(note_line.spans.clone());
let mut spans = expand_tabs_in_spans(&spans, TAB_WIDTH);
if app.line_wrap {
spans = super::fit_review_note_footer(spans, total_width);
}
*max_line_width = (*max_line_width).max(spans_width(&spans).saturating_sub(gutter_width));
let wrap = if app.line_wrap {
super::review_note_wrap_count(&spans, total_width)
} else {
1
};
row_span = row_span.saturating_add(wrap);
if let Some(lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(lines, visible_width, wrap, None);
}
let gutter_spans = slice_spans(&spans, 0, gutter_width);
let content_spans = slice_spans(&spans, gutter_width, visible_width);
content_lines.push(Line::from(content_spans));
gutter_lines.push(Line::from(gutter_spans));
marker_lines.push(Line::from(Span::raw(" ")));
if app.line_wrap && wrap > 1 {
for _ in 1..wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
}
}
}
row_span
}
const MIN_LINE_NUMBER_WIDTH: u16 = 4;
const OLD_BORDER_WIDTH: u16 = 1;
const NEW_MARKER_WIDTH: u16 = 1;
fn split_line_number_width(view_lines: &[ViewLine]) -> u16 {
let max_line = view_lines
.iter()
.filter_map(|line| line.old_line.max(line.new_line))
.max()
.unwrap_or(0);
decimal_width(max_line).max(MIN_LINE_NUMBER_WIDTH)
}
fn decimal_width(value: usize) -> u16 {
if value == 0 {
1
} else {
value.ilog10() as u16 + 1
}
}
fn split_gutter_width(line_number_width: u16) -> u16 {
line_number_width.saturating_add(2)
}
fn blank_line_number(line_number_width: u16) -> String {
" ".repeat(line_number_width as usize)
}
fn format_line_number(line_number: usize, line_number_width: u16) -> String {
if line_number == 0 {
blank_line_number(line_number_width)
} else {
format!(
"{:>width$}",
line_number,
width = line_number_width as usize
)
}
}
fn blank_gutter_line(line_number_width: u16) -> Line<'static> {
Line::from(vec![
Span::raw(" "),
Span::raw(blank_line_number(line_number_width)),
Span::raw(" "),
])
}
fn render_split_divider(frame: &mut Frame, app: &App, area: Rect) {
if area.width == 0 {
return;
}
let bg = app.theme.background;
let fg = bg
.and_then(|bg| color::blend_colors(bg, app.theme.border_subtle, 0.12))
.unwrap_or(app.theme.text_muted);
let mut style = Style::default().fg(fg).add_modifier(Modifier::DIM);
if let Some(bg) = bg {
style = style.bg(bg);
}
let buffer = frame.buffer_mut();
for row in area.y..area.y.saturating_add(area.height) {
if let Some(cell) = buffer.cell_mut((area.x, row)) {
cell.set_symbol("â–•").set_style(style);
}
}
}
fn add_review_preview_boxes_for_rows(
app: &mut App,
content_area: Rect,
scroll_offset: usize,
rows: &[super::ReviewPreviewRow],
) {
if rows.is_empty() || content_area.width == 0 || content_area.height == 0 {
return;
}
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
let viewport_end = viewport_start.saturating_add(content_area.height as usize);
for row in rows {
let start = row.row_idx;
let end = start.saturating_add(row.row_span.max(1));
let visible_start = start.max(viewport_start);
let visible_end = end.min(viewport_end);
if visible_start >= visible_end {
continue;
}
let local_row = visible_start.saturating_sub(viewport_start);
let height = (visible_end.saturating_sub(visible_start)) as u16;
if height == 0 {
continue;
}
app.add_review_comment_preview_box(
content_area.x,
content_area.y.saturating_add(local_row as u16),
content_area.width,
height,
row.id,
row.anchor_key.clone(),
);
let action_x = |x| {
content_area
.x
.saturating_add(1)
.saturating_add(row.indent)
.saturating_add(x)
};
if let Some((offset, x, width)) = row.actions.edit {
let action_row = start.saturating_add(offset);
if action_row >= viewport_start && action_row < viewport_end {
app.add_review_preview_edit_box(
action_x(x),
content_area
.y
.saturating_add((action_row - viewport_start) as u16),
width,
1,
row.id,
row.anchor_key.clone(),
);
}
}
if let Some((offset, x, width)) = row.actions.reply {
let action_row = start.saturating_add(offset);
if action_row >= viewport_start && action_row < viewport_end {
app.add_review_preview_reply_box(
action_x(x),
content_area
.y
.saturating_add((action_row - viewport_start) as u16),
width,
1,
row.id,
row.anchor_key.clone(),
);
}
}
if let Some((offset, x, width)) = row.actions.resolve {
let action_row = start.saturating_add(offset);
if action_row >= viewport_start && action_row < viewport_end {
app.add_review_preview_resolve_box(
action_x(x),
content_area
.y
.saturating_add((action_row - viewport_start) as u16),
width,
1,
row.id,
row.anchor_key.clone(),
);
}
}
if let Some((offset, x, width)) = row.actions.delete {
let action_row = start.saturating_add(offset);
if action_row >= viewport_start && action_row < viewport_end {
app.add_review_preview_delete_box(
action_x(x),
content_area
.y
.saturating_add((action_row - viewport_start) as u16),
width,
1,
row.id,
row.anchor_key.clone(),
);
}
}
if let Some((offset, x, width)) = row.actions.overflow {
let action_row = start.saturating_add(offset);
if action_row >= viewport_start && action_row < viewport_end {
app.add_review_preview_overflow_box(
action_x(x),
content_area
.y
.saturating_add((action_row - viewport_start) as u16),
width,
1,
row.id,
row.anchor_key.clone(),
);
}
}
}
}
pub fn render_split(frame: &mut Frame, app: &mut App, area: Rect) {
let (area, scrollbar_area) = super::reserve_diff_scrollbar_lane(app, area);
let visible_height = area.height as usize;
if app.current_file_is_binary() {
super::render_binary_empty_state(frame, app, area);
return;
}
let show_extent = app.stepping && !app.multi_diff.current_navigator().state().is_at_start();
app.multi_diff
.current_navigator()
.set_show_hunk_extent_while_stepping(show_extent);
let view_lines = app.current_view_with_frame(AnimationFrame::Idle);
let line_number_width = split_line_number_width(&view_lines);
let gutter_width = split_gutter_width(line_number_width);
let step_direction = app.multi_diff.current_step_direction();
let preview_hunk = app.multi_diff.current_navigator().state().current_hunk;
let debug_enabled = super::view_debug_enabled();
if debug_enabled {
crate::syntax::syntax_debug_reset();
}
app.begin_syntax_warmup_frame();
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
let old_width = chunks[0]
.width
.saturating_sub(gutter_width + OLD_BORDER_WIDTH) as usize;
let new_width = chunks[1]
.width
.saturating_sub(gutter_width + NEW_MARKER_WIDTH) as usize;
let search_width = old_width.min(new_width);
if app.line_wrap {
app.handle_search_scroll_if_needed(visible_height, search_width);
} else {
app.ensure_active_visible_if_needed(visible_height, search_width);
}
let mut scroll_offset = app.render_scroll_offset();
let debug_extra = if debug_enabled {
Some(format!(
"split old_width={} new_width={} align_lines={}",
old_width, new_width, app.split_align_lines
))
} else {
None
};
let review_note_rows = if app.review_mode() {
app.review_comment_overlays_for_current_file()
.into_iter()
.map(|overlay| {
let width = if overlay.prefer_right {
new_width
} else {
old_width
};
review_note_lines(
app,
&overlay,
width
.saturating_add(gutter_width as usize)
.saturating_sub(1),
)
.len()
})
.sum::<usize>()
} else {
0
};
let total_len = if app.line_wrap {
let (display_len, active_idx) = split_wrap_display_metrics(
app,
&view_lines,
old_width,
new_width,
scroll_offset,
step_direction,
app.split_align_lines,
);
let display_len = display_len.saturating_add(review_note_rows);
app.ensure_active_visible_if_needed_wrapped(visible_height, display_len, active_idx);
app.render_total_lines(display_len)
} else {
let (display_len, _) = crate::app::display_metrics(
&view_lines,
app.view_mode,
app.animation_phase,
scroll_offset,
step_direction,
app.split_align_lines,
);
app.render_total_lines(display_len.saturating_add(review_note_rows))
};
let scroll_before = app.scroll_offset;
app.clamp_scroll(total_len, visible_height, app.allow_overscroll());
if app.scroll_offset != scroll_before {
scroll_offset = app.render_scroll_offset();
}
let hunk_overflow = if app.line_wrap {
split_hunk_overflow_wrapped(
app,
&view_lines,
preview_hunk,
scroll_offset,
visible_height,
old_width,
new_width,
)
} else {
app.hunk_hint_overflow(preview_hunk, visible_height)
};
if !app.line_wrap {
app.clamp_horizontal_scroll_cached(old_width.min(new_width));
}
app.reset_current_max_line_width();
let mut active_old = false;
let mut active_new = false;
if let Some(primary) = view_lines.iter().find(|line| line.is_primary_active) {
let fold_line = is_fold_line(primary);
let old_present = primary.old_line.is_some() || fold_line;
let new_present = (primary.new_line.is_some()
&& !matches!(primary.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
active_old = old_present;
active_new = new_present;
}
let (show_virtual_old, show_virtual_new) = match (active_old, active_new) {
(true, false) => (true, false),
(false, true) => (false, true),
(true, true) => (
step_direction == StepDirection::Backward,
step_direction != StepDirection::Backward,
),
_ => (true, true),
};
render_old_pane(
frame,
app,
chunks[0],
hunk_overflow,
show_virtual_old,
scroll_offset,
line_number_width,
);
render_new_pane(
frame,
app,
chunks[1],
hunk_overflow,
show_virtual_new,
scroll_offset,
line_number_width,
);
super::render_diff_scrollbar(
frame,
app,
scrollbar_area,
total_len,
visible_height,
scroll_offset,
);
app.commit_syntax_warmup_frame();
if debug_enabled {
let extra = super::merge_debug_extra(debug_extra, super::syntax_debug_extra());
super::maybe_log_view_debug(
app,
view_lines.as_ref(),
"split",
visible_height,
area.width as usize,
scroll_offset,
extra,
);
}
}
fn render_old_pane(
frame: &mut Frame,
app: &mut App,
area: Rect,
hunk_overflow: Option<(bool, bool)>,
show_virtual_pane: bool,
scroll_offset: usize,
line_number_width: u16,
) {
let primary_marker = app.primary_marker.clone();
let extent_marker = app.extent_marker.clone();
let extent_marker_deleted = app.extent_marker_deleted.clone();
let view_lines = app.current_view_with_frame(AnimationFrame::Idle);
let visible_height = area.height as usize;
let gutter_width = split_gutter_width(line_number_width);
let visible_width = area.width.saturating_sub(gutter_width + OLD_BORDER_WIDTH) as usize;
let syntax_window = if app.line_wrap {
Some(super::syntax_highlight_window(
scroll_offset,
visible_height,
))
} else {
None
};
let warmup_window = super::syntax_highlight_window(scroll_offset, visible_height);
let debug_target = app.syntax_scope_target(&view_lines);
let mut bg_lines: Option<Vec<Line<'static>>> =
if app.line_wrap && (app.diff_bg || app.fold_context.is_enabled()) {
Some(Vec::new())
} else {
None
};
let (preview_mode, preview_hunk) = {
let state = app.multi_diff.current_navigator().state();
(state.hunk_preview_mode, state.current_hunk)
};
let pending_insert_only = if app.stepping {
app.pending_insert_only_in_current_hunk()
} else {
0
};
let show_virtual = show_virtual_pane && app.allow_virtual_lines();
let pending_text = if show_virtual && pending_insert_only > 0 {
Some(pending_tail_text(pending_insert_only))
} else {
None
};
let mut parts: Vec<(String, bool)> = Vec::new();
if let Some(pending) = pending_text {
parts.push((pending, true));
}
if let Some(hint) = app.last_step_hint_text() {
parts.push((hint.to_string(), true));
}
if let Some(hint) = app.hunk_edge_hint_text() {
parts.push((hint.to_string(), true));
}
if let Some(hint) = app.blame_hunk_hint_text() {
parts.push((hint.to_string(), false));
}
let virtual_text = if show_virtual && !parts.is_empty() {
Some(
parts
.into_iter()
.map(|(text, _)| text)
.collect::<Vec<_>>()
.join(" • "),
)
} else {
None
};
let (overflow_above, overflow_below) = if virtual_text.is_some() {
hunk_overflow.unwrap_or((false, false))
} else {
(false, false)
};
let old_visible = |line: &ViewLine| -> bool {
let fold_line = is_fold_line(line);
let old_present = line.old_line.is_some() || fold_line;
let new_present = (line.new_line.is_some()
&& !matches!(line.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
old_present || (app.split_align_lines && new_present)
};
let cursor_in_target = view_lines
.iter()
.any(|line| line.is_primary_active && line.hunk_index == Some(preview_hunk));
let cursor_visible = if app.line_wrap {
cursor_in_target
} else {
let mut display_idx = 0usize;
let mut cursor_display = None;
for line in view_lines.iter() {
if !old_visible(line) {
continue;
}
if line.is_primary_active {
cursor_display = Some(display_idx);
break;
}
display_idx += 1;
}
cursor_display
.map(|idx| idx >= scroll_offset && idx < scroll_offset.saturating_add(visible_height))
.unwrap_or(false)
};
let visible_indices: Vec<usize> = view_lines
.iter()
.enumerate()
.filter_map(|(idx, line)| if old_visible(line) { Some(idx) } else { None })
.collect();
let mut next_visible_hunk: Vec<Option<usize>> = vec![None; view_lines.len()];
let mut next_hunk: Option<usize> = None;
for idx in visible_indices.iter().rev() {
next_visible_hunk[*idx] = next_hunk;
if view_lines[*idx].hunk_index.is_some() {
next_hunk = view_lines[*idx].hunk_index;
}
}
let mut prev_visible_hunk_map: Vec<Option<usize>> = vec![None; view_lines.len()];
let mut prev_hunk: Option<usize> = None;
for idx in visible_indices.iter() {
prev_visible_hunk_map[*idx] = prev_hunk;
if view_lines[*idx].hunk_index.is_some() {
prev_hunk = view_lines[*idx].hunk_index;
}
}
let cursor_idx = visible_indices.iter().copied().find(|idx| {
let line = &view_lines[*idx];
line.is_primary_active && line.hunk_index == Some(preview_hunk)
});
let cursor_at_first = cursor_idx
.map(|idx| prev_visible_hunk_map[idx] != Some(preview_hunk))
.unwrap_or(false);
let cursor_at_last = cursor_idx
.map(|idx| next_visible_hunk[idx] != Some(preview_hunk))
.unwrap_or(false);
let fully_visible = !overflow_above && !overflow_below;
let mut force_top = overflow_below && !overflow_above;
let mut force_bottom = overflow_above && !overflow_below;
if fully_visible {
if cursor_at_first {
force_top = true;
} else if cursor_at_last {
force_bottom = true;
}
}
let mut prefer_cursor =
cursor_in_target && cursor_visible && !force_top && !force_bottom && !fully_visible;
if pending_insert_only > 0 && cursor_in_target && cursor_visible {
prefer_cursor = true;
}
let mut prev_visible_hunk: Option<usize> = None;
let mut virtual_inserted = false;
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(gutter_width),
Constraint::Min(0),
Constraint::Length(1), ])
.split(area);
let gutter_area = chunks[0];
let content_area = chunks[1];
let border_area = chunks[2];
let mut gutter_lines: Vec<Line> = Vec::new();
let mut content_lines: Vec<Line> = Vec::new();
let mut review_preview_rows: Vec<super::ReviewPreviewRow> = Vec::new();
let mut review_avatar_rows = Vec::new();
let mut fold_context_rows = Vec::new();
let mut visible_fold_index = 0;
let mut line_idx = 0;
let mut display_row = 0usize;
let query = app.search_query().trim().to_ascii_lowercase();
let has_query = !query.is_empty();
let mut max_line_width: usize = 0;
let mut review_preview_before_idx: std::collections::HashMap<
usize,
Vec<(
String,
super::ReviewNoteBlock,
crate::app::review::ReviewCommentOverlay,
)>,
> = std::collections::HashMap::new();
let mut review_preview_after_idx: std::collections::HashMap<
usize,
Vec<(
String,
super::ReviewNoteBlock,
crate::app::review::ReviewCommentOverlay,
)>,
> = std::collections::HashMap::new();
if app.review_mode() && app.view_mode == crate::app::ViewMode::Split {
for overlay in app.review_comment_overlays_for_current_file() {
if overlay.prefer_right {
continue;
}
let block = review_note_block(
app,
&overlay,
visible_width
.saturating_add(split_gutter_width(line_number_width) as usize)
.saturating_sub(1),
);
if overlay.is_hunk {
review_preview_before_idx
.entry(overlay.display_idx)
.or_default()
.push((overlay.anchor_key.clone(), block, overlay));
} else {
review_preview_after_idx
.entry(overlay.display_idx)
.or_default()
.push((overlay.anchor_key.clone(), block, overlay));
}
}
}
for (idx, view_line) in view_lines.iter().enumerate() {
let fold_line = is_fold_line(view_line);
let old_present = view_line.old_line.is_some() || fold_line;
let new_present = (view_line.new_line.is_some()
&& !matches!(view_line.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
if !(old_present || (app.split_align_lines && new_present)) {
continue;
}
if !app.line_wrap && line_idx < scroll_offset {
line_idx += 1;
continue;
}
if !app.line_wrap && gutter_lines.len() >= visible_height {
break;
}
let line_hunk = view_line.hunk_index;
let is_first_in_hunk = line_hunk.is_some() && prev_visible_hunk != line_hunk;
let is_last_in_hunk = line_hunk.is_some() && next_visible_hunk[idx] != line_hunk;
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& !prefer_cursor
&& force_top
&& line_hunk == Some(preview_hunk)
&& is_first_in_hunk
{
let virtual_rows = push_virtual_line_old(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
if let Some(previews) = review_preview_before_idx.get(&idx) {
for (anchor_key, block, overlay) in previews {
let row_idx = display_row;
let row_span = push_review_note_lines_old(
&block.lines,
anchor_key,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
if let Some(avatar) = &block.avatar {
review_avatar_rows
.push((row_idx.saturating_add(avatar.row_offset), avatar.clone()));
}
review_preview_rows.push(super::review_preview_row(
row_idx,
row_span,
anchor_key.clone(),
overlay,
));
display_row = display_row.saturating_add(row_span);
}
}
if !old_present {
let wrap_count = if app.line_wrap {
split_new_line_wrap_count(app, view_line, visible_width)
} else {
1
};
let fill_span = align_fill_span(app, visible_width);
let marker_fill = Span::raw(" ");
let gutter_fill = align_fill_gutter_span(app, line_number_width as usize);
let sign_fill = align_fill_gutter_span(app, 1);
gutter_lines.push(Line::from(vec![marker_fill, gutter_fill, sign_fill]));
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, 1, None);
}
content_lines.push(Line::from(fill_span.clone()));
display_row = display_row.saturating_add(1);
if app.line_wrap && wrap_count > 1 {
for _ in 1..wrap_count {
let marker_fill = Span::raw(" ");
let gutter_fill = align_fill_gutter_span(app, line_number_width as usize);
let sign_fill = align_fill_gutter_span(app, 1);
gutter_lines.push(Line::from(vec![marker_fill, gutter_fill, sign_fill]));
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, 1, None);
}
content_lines.push(Line::from(fill_span.clone()));
display_row = display_row.saturating_add(1);
}
}
if let Some(previews) = review_preview_after_idx.get(&idx) {
for (anchor_key, block, overlay) in previews {
let row_idx = display_row;
let row_span = push_review_note_lines_old(
&block.lines,
anchor_key,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
if let Some(avatar) = &block.avatar {
review_avatar_rows
.push((row_idx.saturating_add(avatar.row_offset), avatar.clone()));
}
review_preview_rows.push(super::review_preview_row(
row_idx,
row_span,
anchor_key.clone(),
overlay,
));
display_row = display_row.saturating_add(row_span);
}
}
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& !force_top
&& line_hunk == Some(preview_hunk)
&& is_last_in_hunk
{
let virtual_rows = push_virtual_line_old(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
prev_visible_hunk = line_hunk;
line_idx += 1;
continue;
}
let fold_line = is_fold_line(view_line);
let fold_bg = fold_line.then(|| super::fold_context_background(app));
let old_line_num = view_line
.old_line
.or(if fold_line { Some(0) } else { None });
if let Some(old_line_num) = old_line_num {
let line_num_str = if old_line_num == 0 {
blank_line_number(line_number_width)
} else {
format_line_number(old_line_num, line_number_width)
};
let bg_kind = split_old_bg_kind(view_line.kind);
let line_num_style = line_num_style_for_kind(bg_kind, app);
let line_bg_gutter = fold_bg.or_else(|| {
if app.diff_bg {
diff_line_bg(bg_kind, &app.theme)
} else {
None
}
});
let show_extent = super::show_extent_marker(app, view_line);
let (active_marker, active_style) = if view_line.is_primary_active {
(
primary_marker.as_str(),
Style::default()
.fg(app.theme.primary)
.add_modifier(Modifier::BOLD),
)
} else if show_extent {
(
super::extent_marker_text(
extent_marker.as_str(),
extent_marker_deleted.as_str(),
view_line,
),
super::extent_marker_style(
app,
view_line.kind,
view_line.has_changes,
view_line.old_line,
view_line.new_line,
),
)
} else {
(" ", Style::default())
};
let mut gutter_spans = vec![
Span::styled(active_marker, active_style),
Span::styled(line_num_str, line_num_style),
Span::styled(" ", Style::default()),
];
if let Some(bg) = line_bg_gutter {
gutter_spans = gutter_spans
.into_iter()
.enumerate()
.map(|(idx, span)| {
if idx == 0 && !fold_line {
span
} else {
Span::styled(span.content, span.style.bg(bg))
}
})
.collect();
}
gutter_lines.push(Line::from(gutter_spans));
let display_idx = line_idx;
let fold_row = if app.line_wrap {
display_row
} else {
content_lines.len()
};
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
let fold_action_idx = (app.fold_context.is_enabled()
&& fold_line
&& fold_row >= viewport_start
&& fold_row < viewport_start.saturating_add(visible_height))
.then(|| {
let idx = visible_fold_index;
visible_fold_index += 1;
idx
});
let fold_band =
super::fold_context_band(app, view_line, visible_width, fold_action_idx);
if let Some(band) = fold_band.as_ref() {
fold_context_rows.push(crate::app::FoldContextRenderRow {
row: fold_row,
key: band.key,
top_x: band.top_x,
top_width: band.top_width,
bottom_x: band.bottom_x,
bottom_width: band.bottom_width,
});
}
let syntax_line_num = if old_line_num == 0 {
None
} else {
Some(old_line_num)
};
let (line_display_start, line_display_end) = if app.line_wrap {
let wrap_hint = split_old_line_wrap_count(app, view_line, visible_width).max(1);
let line_display_start = content_lines.len();
let line_display_end =
line_display_start.saturating_add(wrap_hint.saturating_sub(1));
(line_display_start, line_display_end)
} else {
(line_idx, line_idx)
};
let in_syntax_window = if app.line_wrap {
super::in_syntax_window(syntax_window, line_display_start, line_display_end)
} else {
true
};
let in_warmup_window = if app.line_wrap {
super::in_syntax_window(Some(warmup_window), line_display_start, line_display_end)
} else {
true
};
let mut content_spans: Vec<Span<'static>> = Vec::new();
let mut used_syntax = false;
if let Some(band) = fold_band.as_ref() {
content_spans = band.spans.clone();
used_syntax = true;
} else if fold_line {
content_spans.push(Span::styled(
view_line.content.clone(),
Style::default().fg(app.theme.text_muted),
));
used_syntax = true;
} else {
let pure_context = matches!(view_line.kind, LineKind::Context)
&& !view_line.has_changes
&& !view_line.is_active_change
&& view_line
.spans
.iter()
.all(|span| matches!(span.kind, ViewSpanKind::Equal));
let wants_diff_syntax =
app.diff_fg == DiffForegroundMode::Syntax && app.syntax_enabled();
let in_preview_hunk =
preview_mode && view_line.hunk_index == Some(preview_hunk) && wants_diff_syntax;
let preview_modified = in_preview_hunk
&& matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify);
let highlight_inline = matches!(
app.diff_highlight,
DiffHighlightMode::Text | DiffHighlightMode::Word
);
let modified_line =
matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify);
let can_use_diff_syntax = wants_diff_syntax && !modified_line;
if in_syntax_window
&& app.syntax_enabled()
&& !preview_modified
&& !view_line.is_active_change
&& (pure_context || can_use_diff_syntax || in_preview_hunk)
{
if in_warmup_window {
if let Some(line_num) = syntax_line_num {
app.record_syntax_warmup_line(SyntaxSide::Old, line_num);
}
}
if let Some(spans) = app.syntax_spans_for_line(SyntaxSide::Old, syntax_line_num)
{
content_spans = spans;
used_syntax = true;
}
}
if !used_syntax {
let mut rebuilt_spans: Vec<ViewSpan> = Vec::new();
let is_applied = app
.multi_diff
.current_navigator()
.state()
.is_applied(view_line.change_id);
let show_inline = view_line.old_line.is_some()
&& view_line.new_line.is_some()
&& (view_line.is_active
|| is_applied
|| (highlight_inline && modified_line));
let spans = if show_inline {
if let Some(change) = app
.multi_diff
.current_navigator()
.diff()
.changes
.get(view_line.change_id)
{
for span in &change.spans {
match span.kind {
ChangeKind::Equal => rebuilt_spans.push(ViewSpan {
text: span.text.clone(),
kind: ViewSpanKind::Equal,
}),
ChangeKind::Delete | ChangeKind::Replace => {
rebuilt_spans.push(ViewSpan {
text: span.text.clone(),
kind: ViewSpanKind::Deleted,
});
}
ChangeKind::Insert => {}
}
}
}
if rebuilt_spans.is_empty() {
&view_line.spans
} else {
&rebuilt_spans
}
} else {
&view_line.spans
};
for view_span in spans {
let highlight_allowed =
matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify)
|| !view_line.is_active
|| (view_line.is_active
&& !matches!(app.diff_highlight, DiffHighlightMode::None)
&& (!app.diff_bg || app.diff_fg == DiffForegroundMode::Theme));
let style = get_old_span_style(
view_span.kind,
view_line.kind,
view_line.is_active,
app,
highlight_allowed,
);
if app.strikethrough_deletions
&& matches!(
view_span.kind,
ViewSpanKind::Deleted | ViewSpanKind::PendingDelete
)
{
let text = &view_span.text;
let trimmed = text.trim_start();
let leading_ws_len = text.len() - trimmed.len();
if leading_ws_len > 0 && !trimmed.is_empty() {
let ws_style = style.remove_modifier(Modifier::CROSSED_OUT);
content_spans.push(Span::styled(
text[..leading_ws_len].to_string(),
ws_style,
));
content_spans.push(Span::styled(trimmed.to_string(), style));
} else {
content_spans.push(Span::styled(view_span.text.clone(), style));
}
} else {
content_spans.push(Span::styled(view_span.text.clone(), style));
}
}
}
}
let line_bg_line = fold_bg.or_else(|| {
if app.diff_bg {
diff_line_bg(bg_kind, &app.theme)
} else {
None
}
});
if let Some(bg) = line_bg_line {
content_spans = apply_line_bg(content_spans, bg, visible_width, app.line_wrap);
}
let highlight_allowed =
matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify)
|| !view_line.is_active
|| (view_line.is_active
&& !matches!(app.diff_highlight, DiffHighlightMode::None)
&& (!app.diff_bg || app.diff_fg == DiffForegroundMode::Theme));
if highlight_allowed
&& !app.diff_bg
&& matches!(
app.diff_highlight,
DiffHighlightMode::Text | DiffHighlightMode::Word
)
&& used_syntax
{
if let Some(bg) = diff_line_bg(bg_kind, &app.theme) {
content_spans = apply_spans_bg(content_spans, bg);
}
}
if highlight_allowed {
if !app.diff_bg {
if app.diff_highlight == DiffHighlightMode::Text {
if !view_line.is_active {
content_spans =
clear_leading_ws_bg(content_spans, Some(app.theme.diff_context));
}
} else if app.diff_highlight == DiffHighlightMode::Word {
content_spans = clear_leading_ws_bg(content_spans, None);
}
} else if app.diff_highlight == DiffHighlightMode::Word {
content_spans = super::replace_leading_ws_bg(content_spans, None, line_bg_line);
}
}
let mut italic_line = false;
if app.syntax_enabled() {
if used_syntax {
italic_line = super::line_is_italic(&content_spans);
} else if in_syntax_window {
if let Some(spans) = app.syntax_spans_for_line(SyntaxSide::Old, syntax_line_num)
{
italic_line = super::line_is_italic(&spans);
}
}
}
let line_text = spans_to_text(&content_spans);
let is_active_match = app.search_target() == Some(display_idx)
&& has_query
&& line_text.to_ascii_lowercase().contains(&query);
content_spans = app.highlight_search_spans(content_spans, &line_text, is_active_match);
if italic_line {
content_spans = super::apply_italic_spans(content_spans);
}
if is_conflict_marker(view_line) {
content_spans = content_spans
.into_iter()
.map(|span| {
let mut style = span.style;
style = style.fg(app.theme.warning).add_modifier(Modifier::BOLD);
Span::styled(span.content, style)
})
.collect();
}
content_spans = expand_tabs_in_spans(&content_spans, TAB_WIDTH);
let line_width = spans_width(&content_spans);
max_line_width = max_line_width.max(line_width);
let wrap_count = if app.line_wrap {
wrap_count_for_spans(&content_spans, visible_width)
} else {
1
};
let mut display_spans = content_spans;
if !app.line_wrap {
if !fold_line {
display_spans =
slice_spans(&display_spans, app.horizontal_scroll, visible_width);
}
if app.diff_bg {
if let Some(bg) = diff_line_bg(bg_kind, &app.theme) {
display_spans = pad_spans_bg(display_spans, bg, visible_width);
}
}
}
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, wrap_count, line_bg_line);
}
content_lines.push(Line::from(display_spans));
display_row = display_row.saturating_add(wrap_count);
if app.line_wrap && wrap_count > 1 {
let (wrap_marker, wrap_style) = if show_extent {
(
super::extent_marker_text(
extent_marker.as_str(),
extent_marker_deleted.as_str(),
view_line,
),
super::extent_marker_style(
app,
view_line.kind,
view_line.has_changes,
view_line.old_line,
view_line.new_line,
),
)
} else {
(" ", Style::default())
};
for _ in 1..wrap_count {
if let Some(bg) = line_bg_gutter {
let pad = " ".repeat(
split_gutter_width(line_number_width).saturating_sub(1) as usize,
);
gutter_lines.push(Line::from(vec![
Span::styled(wrap_marker, wrap_style),
Span::styled(pad, Style::default().bg(bg)),
]));
} else {
gutter_lines.push(Line::from(Span::styled(wrap_marker, wrap_style)));
}
}
}
if let Some(previews) = review_preview_after_idx.get(&idx) {
for (anchor_key, block, overlay) in previews {
let row_idx = display_row;
let row_span = push_review_note_lines_old(
&block.lines,
anchor_key,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
if let Some(avatar) = &block.avatar {
review_avatar_rows
.push((row_idx.saturating_add(avatar.row_offset), avatar.clone()));
}
review_preview_rows.push(super::review_preview_row(
row_idx,
row_span,
anchor_key.clone(),
overlay,
));
display_row = display_row.saturating_add(row_span);
}
}
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& prefer_cursor
&& line_hunk == Some(preview_hunk)
&& view_line.is_primary_active
{
let virtual_rows = push_virtual_line_old(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& !prefer_cursor
&& !force_top
&& line_hunk == Some(preview_hunk)
&& is_last_in_hunk
{
let virtual_rows = push_virtual_line_old(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
if let Some(hint_text) = app.step_edge_hint_for_change(view_line.change_id) {
let virtual_style = Style::default()
.fg(app.theme.text_muted)
.add_modifier(Modifier::ITALIC);
let mut virtual_spans = vec![Span::styled(hint_text.to_string(), virtual_style)];
virtual_spans = expand_tabs_in_spans(&virtual_spans, TAB_WIDTH);
let virtual_width = spans_width(&virtual_spans);
max_line_width = max_line_width.max(virtual_width);
let virtual_wrap = if app.line_wrap {
wrap_count_for_spans(&virtual_spans, visible_width)
} else {
1
};
let mut display_virtual = virtual_spans;
if !app.line_wrap {
display_virtual =
slice_spans(&display_virtual, app.horizontal_scroll, visible_width);
}
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, virtual_wrap, None);
}
content_lines.push(Line::from(display_virtual));
display_row = display_row.saturating_add(virtual_wrap);
gutter_lines.push(blank_gutter_line(line_number_width));
if app.line_wrap && virtual_wrap > 1 {
for _ in 1..virtual_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
}
}
}
if let Some(hint_text) = app.blame_step_hint_for_change(view_line.change_id) {
let virtual_style = Style::default()
.fg(app.theme.text_muted)
.add_modifier(Modifier::ITALIC);
let mut virtual_spans = vec![Span::styled(hint_text.to_string(), virtual_style)];
virtual_spans = expand_tabs_in_spans(&virtual_spans, TAB_WIDTH);
let virtual_width = spans_width(&virtual_spans);
max_line_width = max_line_width.max(virtual_width);
let virtual_wrap = if app.line_wrap {
wrap_count_for_spans(&virtual_spans, visible_width)
} else {
1
};
let mut display_virtual = virtual_spans;
if !app.line_wrap {
display_virtual =
slice_spans(&display_virtual, app.horizontal_scroll, visible_width);
}
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, virtual_wrap, None);
}
content_lines.push(Line::from(display_virtual));
display_row = display_row.saturating_add(virtual_wrap);
gutter_lines.push(blank_gutter_line(line_number_width));
if app.line_wrap && virtual_wrap > 1 {
for _ in 1..virtual_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
}
}
}
prev_visible_hunk = line_hunk;
line_idx += 1;
if let Some((debug_idx, _)) = debug_target {
if debug_idx == display_idx {
let debug_wrap = if app.line_wrap {
wrap_count_for_text("", visible_width)
} else {
1
};
gutter_lines.push(blank_gutter_line(line_number_width));
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, debug_wrap, None);
}
content_lines.push(Line::from(Span::raw("")));
display_row = display_row.saturating_add(debug_wrap);
if app.line_wrap && debug_wrap > 1 {
for _ in 1..debug_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
}
}
}
}
}
}
app.clamp_horizontal_scroll(max_line_width, visible_width);
let bg_style = app.theme.background.map(|bg| Style::default().bg(bg));
let mut gutter_paragraph = if app.line_wrap {
Paragraph::new(gutter_lines).scroll((scroll_offset as u16, 0))
} else {
Paragraph::new(gutter_lines)
};
if let Some(style) = bg_style {
gutter_paragraph = gutter_paragraph.style(style);
}
frame.render_widget(gutter_paragraph, gutter_area);
if content_lines.is_empty() {
let has_changes = !app
.multi_diff
.current_navigator()
.diff()
.significant_changes
.is_empty();
render_empty_state(
frame,
content_area,
&app.theme,
has_changes,
app.current_file_is_binary(),
);
} else {
let mut content_paragraph = if app.line_wrap {
Paragraph::new(content_lines)
.wrap(Wrap { trim: false })
.scroll((scroll_offset as u16, 0))
} else {
Paragraph::new(content_lines)
};
let has_bg_overlay = bg_lines.is_some();
if let Some(bg_lines) = bg_lines {
let mut bg_paragraph = Paragraph::new(bg_lines).scroll((scroll_offset as u16, 0));
if let Some(style) = bg_style {
bg_paragraph = bg_paragraph.style(style);
}
frame.render_widget(bg_paragraph, content_area);
}
if !has_bg_overlay {
if let Some(style) = bg_style {
content_paragraph = content_paragraph.style(style);
}
}
frame.render_widget(content_paragraph, content_area);
}
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
app.add_fold_context_render_rows(content_area, &fold_context_rows, viewport_start);
if app.review_mode() && app.view_mode == crate::app::ViewMode::Split {
let note_area = Rect::new(
gutter_area.x,
content_area.y,
gutter_area.width.saturating_add(content_area.width),
content_area.height,
);
add_review_preview_boxes_for_rows(app, note_area, scroll_offset, &review_preview_rows);
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
let viewport_end = viewport_start.saturating_add(note_area.height as usize);
for (row_idx, avatar) in &review_avatar_rows {
if *row_idx < viewport_start || *row_idx >= viewport_end {
continue;
}
let avatar_area = Rect::new(
note_area.x.saturating_add(1),
note_area.y,
note_area.width.saturating_sub(1),
note_area.height,
);
render_review_note_avatar(
frame,
app,
avatar_area,
row_idx.saturating_sub(viewport_start),
avatar,
);
}
}
render_split_divider(frame, app, border_area);
app.update_current_max_line_width(max_line_width);
}
fn render_new_pane(
frame: &mut Frame,
app: &mut App,
area: Rect,
hunk_overflow: Option<(bool, bool)>,
show_virtual_pane: bool,
scroll_offset: usize,
line_number_width: u16,
) {
let primary_marker_right = app.primary_marker_right.clone();
let extent_marker_right = app.extent_marker_right.clone();
let extent_marker_deleted = app.extent_marker_deleted.clone();
let animation_frame = app.animation_frame();
let view_lines = app.current_view_with_frame(animation_frame);
let visible_height = area.height as usize;
let syntax_window = if app.line_wrap {
Some(super::syntax_highlight_window(
scroll_offset,
visible_height,
))
} else {
None
};
let warmup_window = super::syntax_highlight_window(scroll_offset, visible_height);
let debug_target = app.syntax_scope_target(&view_lines);
let mut bg_lines: Option<Vec<Line<'static>>> =
if app.line_wrap && (app.diff_bg || app.fold_context.is_enabled()) {
Some(Vec::new())
} else {
None
};
let (preview_mode, preview_hunk) = {
let state = app.multi_diff.current_navigator().state();
(state.hunk_preview_mode, state.current_hunk)
};
let pending_insert_only = if app.stepping {
app.pending_insert_only_in_current_hunk()
} else {
0
};
let show_virtual = show_virtual_pane && app.allow_virtual_lines();
let pending_text = if show_virtual && pending_insert_only > 0 {
Some(pending_tail_text(pending_insert_only))
} else {
None
};
let mut parts: Vec<(String, bool)> = Vec::new();
if let Some(pending) = pending_text {
parts.push((pending, true));
}
if let Some(hint) = app.last_step_hint_text() {
parts.push((hint.to_string(), true));
}
if let Some(hint) = app.hunk_edge_hint_text() {
parts.push((hint.to_string(), true));
}
if let Some(hint) = app.blame_hunk_hint_text() {
parts.push((hint.to_string(), false));
}
let virtual_text = if show_virtual && !parts.is_empty() {
Some(
parts
.into_iter()
.map(|(text, _)| text)
.collect::<Vec<_>>()
.join(" • "),
)
} else {
None
};
let (overflow_above, overflow_below) = if virtual_text.is_some() {
hunk_overflow.unwrap_or((false, false))
} else {
(false, false)
};
let new_visible = |line: &ViewLine| -> bool {
let fold_line = is_fold_line(line);
let old_present = line.old_line.is_some() || fold_line;
let new_present = (line.new_line.is_some()
&& !matches!(line.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
new_present || (app.split_align_lines && old_present)
};
let cursor_in_target = view_lines
.iter()
.any(|line| line.is_primary_active && line.hunk_index == Some(preview_hunk));
let cursor_visible = if app.line_wrap {
cursor_in_target
} else {
let mut display_idx = 0usize;
let mut cursor_display = None;
for line in view_lines.iter() {
if !new_visible(line) {
continue;
}
if line.is_primary_active {
cursor_display = Some(display_idx);
break;
}
display_idx += 1;
}
cursor_display
.map(|idx| idx >= scroll_offset && idx < scroll_offset.saturating_add(visible_height))
.unwrap_or(false)
};
let visible_indices: Vec<usize> = view_lines
.iter()
.enumerate()
.filter_map(|(idx, line)| if new_visible(line) { Some(idx) } else { None })
.collect();
let mut next_visible_hunk: Vec<Option<usize>> = vec![None; view_lines.len()];
let mut next_hunk: Option<usize> = None;
for idx in visible_indices.iter().rev() {
next_visible_hunk[*idx] = next_hunk;
if view_lines[*idx].hunk_index.is_some() {
next_hunk = view_lines[*idx].hunk_index;
}
}
let mut prev_visible_hunk_map: Vec<Option<usize>> = vec![None; view_lines.len()];
let mut prev_hunk: Option<usize> = None;
for idx in visible_indices.iter() {
prev_visible_hunk_map[*idx] = prev_hunk;
if view_lines[*idx].hunk_index.is_some() {
prev_hunk = view_lines[*idx].hunk_index;
}
}
let cursor_idx = visible_indices.iter().copied().find(|idx| {
let line = &view_lines[*idx];
line.is_primary_active && line.hunk_index == Some(preview_hunk)
});
let cursor_at_first = cursor_idx
.map(|idx| prev_visible_hunk_map[idx] != Some(preview_hunk))
.unwrap_or(false);
let cursor_at_last = cursor_idx
.map(|idx| next_visible_hunk[idx] != Some(preview_hunk))
.unwrap_or(false);
let fully_visible = !overflow_above && !overflow_below;
let mut force_top = overflow_below && !overflow_above;
let mut force_bottom = overflow_above && !overflow_below;
if fully_visible {
if cursor_at_first {
force_top = true;
} else if cursor_at_last {
force_bottom = true;
}
}
let mut prefer_cursor =
cursor_in_target && cursor_visible && !force_top && !force_bottom && !fully_visible;
if pending_insert_only > 0 && cursor_in_target && cursor_visible {
prefer_cursor = true;
}
let mut prev_visible_hunk: Option<usize> = None;
let mut virtual_inserted = false;
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(split_gutter_width(line_number_width)),
Constraint::Min(0),
Constraint::Length(1), ])
.split(area);
let gutter_area = chunks[0];
let content_area = chunks[1];
let marker_area = chunks[2];
let visible_width = content_area.width as usize;
let mut gutter_lines: Vec<Line> = Vec::new();
let mut content_lines: Vec<Line> = Vec::new();
let mut marker_lines: Vec<Line> = Vec::new();
let mut review_preview_rows: Vec<super::ReviewPreviewRow> = Vec::new();
let mut review_avatar_rows = Vec::new();
let mut fold_context_rows = Vec::new();
let mut visible_fold_index = 0;
let mut line_idx = 0;
let mut display_row = 0usize;
let query = app.search_query().trim().to_ascii_lowercase();
let has_query = !query.is_empty();
let mut max_line_width: usize = 0;
let mut review_preview_before_idx: std::collections::HashMap<
usize,
Vec<(
String,
super::ReviewNoteBlock,
crate::app::review::ReviewCommentOverlay,
)>,
> = std::collections::HashMap::new();
let mut review_preview_after_idx: std::collections::HashMap<
usize,
Vec<(
String,
super::ReviewNoteBlock,
crate::app::review::ReviewCommentOverlay,
)>,
> = std::collections::HashMap::new();
if app.review_mode() && app.view_mode == crate::app::ViewMode::Split {
for overlay in app.review_comment_overlays_for_current_file() {
if !overlay.prefer_right {
continue;
}
let block = review_note_block(
app,
&overlay,
visible_width
.saturating_add(split_gutter_width(line_number_width) as usize)
.saturating_sub(1),
);
if overlay.is_hunk {
review_preview_before_idx
.entry(overlay.display_idx)
.or_default()
.push((overlay.anchor_key.clone(), block, overlay));
} else {
review_preview_after_idx
.entry(overlay.display_idx)
.or_default()
.push((overlay.anchor_key.clone(), block, overlay));
}
}
}
for (idx, view_line) in view_lines.iter().enumerate() {
let fold_line = is_fold_line(view_line);
let old_present = view_line.old_line.is_some() || fold_line;
let new_present = (view_line.new_line.is_some()
&& !matches!(view_line.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
if !(new_present || (app.split_align_lines && old_present)) {
continue;
}
if !app.line_wrap && line_idx < scroll_offset {
line_idx += 1;
continue;
}
if !app.line_wrap && gutter_lines.len() >= visible_height {
break;
}
let line_hunk = view_line.hunk_index;
let is_first_in_hunk = line_hunk.is_some() && prev_visible_hunk != line_hunk;
let is_last_in_hunk = line_hunk.is_some() && next_visible_hunk[idx] != line_hunk;
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& !prefer_cursor
&& force_top
&& line_hunk == Some(preview_hunk)
&& is_first_in_hunk
{
let virtual_rows = push_virtual_line_new(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
if let Some(previews) = review_preview_before_idx.get(&idx) {
for (anchor_key, block, overlay) in previews {
let row_idx = display_row;
let row_span = push_review_note_lines_new(
&block.lines,
anchor_key,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
if let Some(avatar) = &block.avatar {
review_avatar_rows
.push((row_idx.saturating_add(avatar.row_offset), avatar.clone()));
}
review_preview_rows.push(super::review_preview_row(
row_idx,
row_span,
anchor_key.clone(),
overlay,
));
display_row = display_row.saturating_add(row_span);
}
}
if !new_present {
let wrap_count = if app.line_wrap {
split_old_line_wrap_count(app, view_line, visible_width)
} else {
1
};
let fill_span = align_fill_span(app, visible_width);
let gutter_fill = align_fill_gutter_span(app, line_number_width as usize);
let sign_fill = align_fill_gutter_span(app, 1);
gutter_lines.push(Line::from(vec![Span::raw(" "), gutter_fill, sign_fill]));
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, 1, None);
}
content_lines.push(Line::from(fill_span.clone()));
display_row = display_row.saturating_add(1);
marker_lines.push(Line::from(Span::raw(" ")));
if app.line_wrap && wrap_count > 1 {
for _ in 1..wrap_count {
let gutter_fill = align_fill_gutter_span(app, line_number_width as usize);
let sign_fill = align_fill_gutter_span(app, 1);
gutter_lines.push(Line::from(vec![Span::raw(" "), gutter_fill, sign_fill]));
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, 1, None);
}
content_lines.push(Line::from(fill_span.clone()));
display_row = display_row.saturating_add(1);
marker_lines.push(Line::from(Span::raw(" ")));
}
}
if let Some(previews) = review_preview_after_idx.get(&idx) {
for (anchor_key, block, overlay) in previews {
let row_idx = display_row;
let row_span = push_review_note_lines_new(
&block.lines,
anchor_key,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
if let Some(avatar) = &block.avatar {
review_avatar_rows
.push((row_idx.saturating_add(avatar.row_offset), avatar.clone()));
}
review_preview_rows.push(super::review_preview_row(
row_idx,
row_span,
anchor_key.clone(),
overlay,
));
display_row = display_row.saturating_add(row_span);
}
}
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& !force_top
&& line_hunk == Some(preview_hunk)
&& is_last_in_hunk
{
let virtual_rows = push_virtual_line_new(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
prev_visible_hunk = line_hunk;
line_idx += 1;
continue;
}
let fold_line = is_fold_line(view_line);
let fold_bg = fold_line.then(|| super::fold_context_background(app));
let new_line_num = view_line
.new_line
.or(if fold_line { Some(0) } else { None });
if let Some(new_line_num) = new_line_num {
let line_num_str = if new_line_num == 0 {
blank_line_number(line_number_width)
} else {
format_line_number(new_line_num, line_number_width)
};
let bg_kind = split_new_bg_kind(view_line.kind);
let line_num_style = line_num_style_for_kind(bg_kind, app);
let line_bg_gutter = fold_bg.or_else(|| {
if app.diff_bg {
diff_line_bg(bg_kind, &app.theme)
} else {
None
}
});
let show_extent = super::show_extent_marker(app, view_line);
let (active_marker, active_style) = if view_line.is_primary_active {
(
primary_marker_right.as_str(),
Style::default()
.fg(app.theme.primary)
.add_modifier(Modifier::BOLD),
)
} else if show_extent {
(
super::extent_marker_text(
extent_marker_right.as_str(),
extent_marker_deleted.as_str(),
view_line,
),
super::extent_marker_style(
app,
view_line.kind,
view_line.has_changes,
view_line.old_line,
view_line.new_line,
),
)
} else {
(" ", Style::default())
};
let mut gutter_spans = vec![
Span::styled(" ", Style::default()),
Span::styled(line_num_str, line_num_style),
Span::styled(" ", Style::default()),
];
if let Some(bg) = line_bg_gutter {
gutter_spans = gutter_spans
.into_iter()
.map(|span| Span::styled(span.content, span.style.bg(bg)))
.collect();
}
gutter_lines.push(Line::from(gutter_spans));
let display_idx = line_idx;
let fold_row = if app.line_wrap {
display_row
} else {
content_lines.len()
};
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
let fold_action_idx = (app.fold_context.is_enabled()
&& fold_line
&& fold_row >= viewport_start
&& fold_row < viewport_start.saturating_add(visible_height))
.then(|| {
let idx = visible_fold_index;
visible_fold_index += 1;
idx
});
let fold_band =
super::fold_context_band(app, view_line, visible_width, fold_action_idx);
if let Some(band) = fold_band.as_ref() {
fold_context_rows.push(crate::app::FoldContextRenderRow {
row: fold_row,
key: band.key,
top_x: band.top_x,
top_width: band.top_width,
bottom_x: band.bottom_x,
bottom_width: band.bottom_width,
});
}
let syntax_line_num = if new_line_num == 0 {
None
} else {
Some(new_line_num)
};
let (line_display_start, line_display_end) = if app.line_wrap {
let wrap_hint = split_new_line_wrap_count(app, view_line, visible_width).max(1);
let line_display_start = content_lines.len();
let line_display_end =
line_display_start.saturating_add(wrap_hint.saturating_sub(1));
(line_display_start, line_display_end)
} else {
(line_idx, line_idx)
};
let in_syntax_window = if app.line_wrap {
super::in_syntax_window(syntax_window, line_display_start, line_display_end)
} else {
true
};
let in_warmup_window = if app.line_wrap {
super::in_syntax_window(Some(warmup_window), line_display_start, line_display_end)
} else {
true
};
let mut content_spans: Vec<Span<'static>> = Vec::new();
let mut used_syntax = false;
if let Some(band) = fold_band.as_ref() {
content_spans = band.spans.clone();
used_syntax = true;
} else if fold_line {
content_spans.push(Span::styled(
view_line.content.clone(),
Style::default().fg(app.theme.text_muted),
));
used_syntax = true;
} else {
let pure_context = matches!(view_line.kind, LineKind::Context)
&& !view_line.has_changes
&& !view_line.is_active_change
&& view_line
.spans
.iter()
.all(|span| matches!(span.kind, ViewSpanKind::Equal));
let wants_diff_syntax =
app.diff_fg == DiffForegroundMode::Syntax && app.syntax_enabled();
let in_preview_hunk =
preview_mode && view_line.hunk_index == Some(preview_hunk) && wants_diff_syntax;
let preview_modified = in_preview_hunk
&& matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify);
let highlight_inline = matches!(
app.diff_highlight,
DiffHighlightMode::Text | DiffHighlightMode::Word
);
let modified_line =
matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify);
let can_use_diff_syntax = wants_diff_syntax && !modified_line;
if in_syntax_window
&& app.syntax_enabled()
&& !preview_modified
&& !view_line.is_active_change
&& (pure_context || can_use_diff_syntax || in_preview_hunk)
{
let use_old = view_line.kind == LineKind::Context && view_line.has_changes;
let side = if use_old {
SyntaxSide::Old
} else {
SyntaxSide::New
};
let line_num = if use_old {
view_line.old_line.or(syntax_line_num)
} else {
syntax_line_num
};
if in_warmup_window {
if let Some(line_num) = line_num {
app.record_syntax_warmup_line(side, line_num);
}
}
if let Some(spans) = app.syntax_spans_for_line(side, line_num) {
content_spans = spans;
used_syntax = true;
}
}
if !used_syntax {
let mut rebuilt_spans: Vec<ViewSpan> = Vec::new();
let is_applied = app
.multi_diff
.current_navigator()
.state()
.is_applied(view_line.change_id);
let show_inline = view_line.old_line.is_some()
&& view_line.new_line.is_some()
&& (view_line.is_active
|| is_applied
|| (highlight_inline && modified_line));
let spans = if show_inline {
if let Some(change) = app
.multi_diff
.current_navigator()
.diff()
.changes
.get(view_line.change_id)
{
for span in &change.spans {
match span.kind {
ChangeKind::Equal => rebuilt_spans.push(ViewSpan {
text: span.text.clone(),
kind: ViewSpanKind::Equal,
}),
ChangeKind::Insert => rebuilt_spans.push(ViewSpan {
text: span.text.clone(),
kind: if view_line.is_active {
ViewSpanKind::PendingInsert
} else {
ViewSpanKind::Inserted
},
}),
ChangeKind::Replace => rebuilt_spans.push(ViewSpan {
text: span
.new_text
.clone()
.unwrap_or_else(|| span.text.clone()),
kind: if view_line.is_active {
ViewSpanKind::PendingInsert
} else {
ViewSpanKind::Inserted
},
}),
ChangeKind::Delete => {}
}
}
}
if rebuilt_spans.is_empty() {
&view_line.spans
} else {
&rebuilt_spans
}
} else {
&view_line.spans
};
for view_span in spans {
let highlight_allowed =
matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify)
|| !view_line.is_active
|| (view_line.is_active
&& !matches!(app.diff_highlight, DiffHighlightMode::None)
&& (!app.diff_bg || app.diff_fg == DiffForegroundMode::Theme));
let style = get_new_span_style(
view_span.kind,
view_line.kind,
view_line.is_active,
app,
highlight_allowed,
);
content_spans.push(Span::styled(view_span.text.clone(), style));
}
}
}
let line_bg_line = fold_bg.or_else(|| {
if app.diff_bg {
diff_line_bg(bg_kind, &app.theme)
} else {
None
}
});
if let Some(bg) = line_bg_line {
content_spans = apply_line_bg(content_spans, bg, visible_width, app.line_wrap);
}
let highlight_allowed =
matches!(view_line.kind, LineKind::Modified | LineKind::PendingModify)
|| !view_line.is_active
|| (view_line.is_active
&& !matches!(app.diff_highlight, DiffHighlightMode::None)
&& (!app.diff_bg || app.diff_fg == DiffForegroundMode::Theme));
if highlight_allowed
&& !app.diff_bg
&& matches!(
app.diff_highlight,
DiffHighlightMode::Text | DiffHighlightMode::Word
)
&& used_syntax
{
if let Some(bg) = diff_line_bg(bg_kind, &app.theme) {
content_spans = apply_spans_bg(content_spans, bg);
}
}
if highlight_allowed {
if !app.diff_bg {
if app.diff_highlight == DiffHighlightMode::Text {
if !view_line.is_active {
content_spans =
clear_leading_ws_bg(content_spans, Some(app.theme.diff_context));
}
} else if app.diff_highlight == DiffHighlightMode::Word {
content_spans = clear_leading_ws_bg(content_spans, None);
}
} else if app.diff_highlight == DiffHighlightMode::Word {
content_spans = super::replace_leading_ws_bg(content_spans, None, line_bg_line);
}
}
let mut italic_line = false;
if app.syntax_enabled() {
if used_syntax {
italic_line = super::line_is_italic(&content_spans);
} else if in_syntax_window {
let use_old = view_line.kind == LineKind::Context && view_line.has_changes;
let side = if use_old {
SyntaxSide::Old
} else {
SyntaxSide::New
};
let line_num = if use_old {
view_line.old_line.or(syntax_line_num)
} else {
syntax_line_num
};
if let Some(spans) = app.syntax_spans_for_line(side, line_num) {
italic_line = super::line_is_italic(&spans);
}
}
}
let line_text = spans_to_text(&content_spans);
let is_active_match = app.search_target() == Some(display_idx)
&& has_query
&& line_text.to_ascii_lowercase().contains(&query);
content_spans = app.highlight_search_spans(content_spans, &line_text, is_active_match);
if italic_line {
content_spans = super::apply_italic_spans(content_spans);
}
if is_conflict_marker(view_line) {
content_spans = content_spans
.into_iter()
.map(|span| {
let mut style = span.style;
style = style.fg(app.theme.warning).add_modifier(Modifier::BOLD);
Span::styled(span.content, style)
})
.collect();
}
content_spans = expand_tabs_in_spans(&content_spans, TAB_WIDTH);
let line_width = spans_width(&content_spans);
max_line_width = max_line_width.max(line_width);
let wrap_count = if app.line_wrap {
wrap_count_for_spans(&content_spans, visible_width)
} else {
1
};
let mut display_spans = content_spans;
if !app.line_wrap {
if !fold_line {
display_spans =
slice_spans(&display_spans, app.horizontal_scroll, visible_width);
}
if app.diff_bg {
if let Some(bg) = diff_line_bg(bg_kind, &app.theme) {
display_spans = pad_spans_bg(display_spans, bg, visible_width);
}
}
}
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, wrap_count, line_bg_line);
}
content_lines.push(Line::from(display_spans));
display_row = display_row.saturating_add(wrap_count);
let marker_style = fold_bg.map_or(active_style, |bg| active_style.bg(bg));
marker_lines.push(Line::from(Span::styled(active_marker, marker_style)));
if app.line_wrap && wrap_count > 1 {
let (wrap_marker, wrap_style) = if show_extent {
(
super::extent_marker_text(
extent_marker_right.as_str(),
extent_marker_deleted.as_str(),
view_line,
),
super::extent_marker_style(
app,
view_line.kind,
view_line.has_changes,
view_line.old_line,
view_line.new_line,
),
)
} else {
(" ", Style::default())
};
for _ in 1..wrap_count {
if let Some(bg) = line_bg_gutter {
gutter_lines.push(Line::from(Span::styled(
" ".repeat(split_gutter_width(line_number_width) as usize),
Style::default().bg(bg),
)));
} else {
gutter_lines.push(blank_gutter_line(line_number_width));
}
marker_lines.push(Line::from(Span::styled(wrap_marker, wrap_style)));
}
}
if let Some(previews) = review_preview_after_idx.get(&idx) {
for (anchor_key, block, overlay) in previews {
let row_idx = display_row;
let row_span = push_review_note_lines_new(
&block.lines,
anchor_key,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
if let Some(avatar) = &block.avatar {
review_avatar_rows
.push((row_idx.saturating_add(avatar.row_offset), avatar.clone()));
}
review_preview_rows.push(super::review_preview_row(
row_idx,
row_span,
anchor_key.clone(),
overlay,
));
display_row = display_row.saturating_add(row_span);
}
}
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& prefer_cursor
&& line_hunk == Some(preview_hunk)
&& view_line.is_primary_active
{
let virtual_rows = push_virtual_line_new(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
if let Some(text) = virtual_text.as_ref() {
if !virtual_inserted
&& !prefer_cursor
&& !force_top
&& line_hunk == Some(preview_hunk)
&& is_last_in_hunk
{
let virtual_rows = push_virtual_line_new(
text,
app,
line_number_width,
visible_width,
&mut max_line_width,
&mut content_lines,
&mut gutter_lines,
&mut marker_lines,
bg_lines.as_mut(),
);
display_row = display_row.saturating_add(virtual_rows);
virtual_inserted = true;
}
}
if let Some(hint_text) = app.step_edge_hint_for_change(view_line.change_id) {
let virtual_style = Style::default()
.fg(app.theme.text_muted)
.add_modifier(Modifier::ITALIC);
let mut virtual_spans = vec![Span::styled(hint_text.to_string(), virtual_style)];
virtual_spans = expand_tabs_in_spans(&virtual_spans, TAB_WIDTH);
let virtual_width = spans_width(&virtual_spans);
max_line_width = max_line_width.max(virtual_width);
let virtual_wrap = if app.line_wrap {
wrap_count_for_spans(&virtual_spans, visible_width)
} else {
1
};
let mut display_virtual = virtual_spans;
if !app.line_wrap {
display_virtual =
slice_spans(&display_virtual, app.horizontal_scroll, visible_width);
}
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, virtual_wrap, None);
}
content_lines.push(Line::from(display_virtual));
display_row = display_row.saturating_add(virtual_wrap);
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
if app.line_wrap && virtual_wrap > 1 {
for _ in 1..virtual_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
}
}
}
if let Some(hint_text) = app.blame_step_hint_for_change(view_line.change_id) {
let virtual_style = Style::default()
.fg(app.theme.text_muted)
.add_modifier(Modifier::ITALIC);
let mut virtual_spans = vec![Span::styled(hint_text.to_string(), virtual_style)];
virtual_spans = expand_tabs_in_spans(&virtual_spans, TAB_WIDTH);
let virtual_width = spans_width(&virtual_spans);
max_line_width = max_line_width.max(virtual_width);
let virtual_wrap = if app.line_wrap {
wrap_count_for_spans(&virtual_spans, visible_width)
} else {
1
};
let mut display_virtual = virtual_spans;
if !app.line_wrap {
display_virtual =
slice_spans(&display_virtual, app.horizontal_scroll, visible_width);
}
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, virtual_wrap, None);
}
content_lines.push(Line::from(display_virtual));
display_row = display_row.saturating_add(virtual_wrap);
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
if app.line_wrap && virtual_wrap > 1 {
for _ in 1..virtual_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
}
}
}
prev_visible_hunk = line_hunk;
line_idx += 1;
if let Some((debug_idx, ref label)) = debug_target {
if debug_idx == display_idx {
let debug_text = truncate_text(&format!(" {}", label), visible_width);
let debug_style = Style::default().fg(app.theme.text_muted);
let debug_wrap = if app.line_wrap {
wrap_count_for_text(&debug_text, visible_width)
} else {
1
};
gutter_lines.push(blank_gutter_line(line_number_width));
if let Some(bg_lines) = bg_lines.as_mut() {
super::push_wrapped_bg_line(bg_lines, visible_width, debug_wrap, None);
}
content_lines.push(Line::from(Span::styled(debug_text, debug_style)));
display_row = display_row.saturating_add(debug_wrap);
marker_lines.push(Line::from(Span::raw(" ")));
if app.line_wrap && debug_wrap > 1 {
for _ in 1..debug_wrap {
gutter_lines.push(blank_gutter_line(line_number_width));
marker_lines.push(Line::from(Span::raw(" ")));
}
}
}
}
}
}
let bg_style = app.theme.background.map(|bg| Style::default().bg(bg));
let mut gutter_paragraph = if app.line_wrap {
Paragraph::new(gutter_lines).scroll((scroll_offset as u16, 0))
} else {
Paragraph::new(gutter_lines)
};
if let Some(style) = bg_style {
gutter_paragraph = gutter_paragraph.style(style);
}
frame.render_widget(gutter_paragraph, gutter_area);
if content_lines.is_empty() {
let has_changes = !app
.multi_diff
.current_navigator()
.diff()
.significant_changes
.is_empty();
render_empty_state(
frame,
content_area,
&app.theme,
has_changes,
app.current_file_is_binary(),
);
} else {
let mut content_paragraph = if app.line_wrap {
Paragraph::new(content_lines)
.wrap(Wrap { trim: false })
.scroll((scroll_offset as u16, 0))
} else {
Paragraph::new(content_lines)
};
let has_bg_overlay = bg_lines.is_some();
if let Some(bg_lines) = bg_lines {
let mut bg_paragraph = Paragraph::new(bg_lines).scroll((scroll_offset as u16, 0));
if let Some(style) = bg_style {
bg_paragraph = bg_paragraph.style(style);
}
frame.render_widget(bg_paragraph, content_area);
}
if !has_bg_overlay {
if let Some(style) = bg_style {
content_paragraph = content_paragraph.style(style);
}
}
frame.render_widget(content_paragraph, content_area);
}
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
app.add_fold_context_render_rows(content_area, &fold_context_rows, viewport_start);
if app.review_mode() && app.view_mode == crate::app::ViewMode::Split {
let note_area = Rect::new(
gutter_area.x,
content_area.y,
gutter_area.width.saturating_add(content_area.width),
content_area.height,
);
add_review_preview_boxes_for_rows(app, note_area, scroll_offset, &review_preview_rows);
let viewport_start = if app.line_wrap { scroll_offset } else { 0 };
let viewport_end = viewport_start.saturating_add(note_area.height as usize);
for (row_idx, avatar) in &review_avatar_rows {
if *row_idx < viewport_start || *row_idx >= viewport_end {
continue;
}
let avatar_area = Rect::new(
note_area.x.saturating_add(1),
note_area.y,
note_area.width.saturating_sub(1),
note_area.height,
);
render_review_note_avatar(
frame,
app,
avatar_area,
row_idx.saturating_sub(viewport_start),
avatar,
);
}
}
let mut marker_paragraph = if app.line_wrap {
Paragraph::new(marker_lines).scroll((scroll_offset as u16, 0))
} else {
Paragraph::new(marker_lines)
};
if let Some(style) = bg_style {
marker_paragraph = marker_paragraph.style(style);
}
frame.render_widget(marker_paragraph, marker_area);
app.update_current_max_line_width(max_line_width);
}
fn split_wrap_display_metrics(
app: &mut App,
view: &[ViewLine],
old_width: usize,
new_width: usize,
scroll_offset: usize,
step_direction: StepDirection,
align_lines: bool,
) -> (usize, Option<usize>) {
let mut old_len = 0usize;
let mut new_len = 0usize;
let mut old_primary_idx: Option<usize> = None;
let mut new_primary_idx: Option<usize> = None;
let mut old_fallback_idx: Option<usize> = None;
let mut new_fallback_idx: Option<usize> = None;
for line in view {
let fold_line = is_fold_line(line);
let old_present = line.old_line.is_some() || fold_line;
let new_present = (line.new_line.is_some()
&& !matches!(line.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
let old_wrap = if old_present {
split_old_line_wrap_count(app, line, old_width)
} else if align_lines && new_present {
split_new_line_wrap_count(app, line, old_width)
} else {
0
};
let new_wrap = if new_present {
split_new_line_wrap_count(app, line, new_width)
} else if align_lines && old_present {
split_old_line_wrap_count(app, line, new_width)
} else {
0
};
if old_wrap > 0 {
if old_present {
if line.is_primary_active {
old_primary_idx = Some(old_len);
} else if line.is_active && old_fallback_idx.is_none() {
old_fallback_idx = Some(old_len);
}
}
old_len += old_wrap;
}
if new_wrap > 0 {
if new_present {
if line.is_primary_active {
new_primary_idx = Some(new_len);
} else if line.is_active && new_fallback_idx.is_none() {
new_fallback_idx = Some(new_len);
}
}
new_len += new_wrap;
}
}
let display_len = old_len.max(new_len);
let (old_idx, new_idx) = if old_primary_idx.is_some() || new_primary_idx.is_some() {
(old_primary_idx, new_primary_idx)
} else {
(old_fallback_idx, new_fallback_idx)
};
let active_idx = match (old_idx, new_idx) {
(Some(old), Some(new)) => {
let old_dist = (old as isize - scroll_offset as isize).abs();
let new_dist = (new as isize - scroll_offset as isize).abs();
if old_dist < new_dist {
Some(old)
} else if new_dist < old_dist {
Some(new)
} else {
match step_direction {
StepDirection::Forward | StepDirection::None => Some(new),
StepDirection::Backward => Some(old),
}
}
}
(Some(old), None) => Some(old),
(None, Some(new)) => Some(new),
(None, None) => None,
};
(display_len, active_idx)
}
fn split_hunk_overflow_wrapped(
app: &mut App,
view: &[ViewLine],
hunk_idx: usize,
scroll_offset: usize,
viewport_height: usize,
old_width: usize,
new_width: usize,
) -> Option<(bool, bool)> {
let mut old_idx = 0usize;
let mut new_idx = 0usize;
let mut old_start: Option<usize> = None;
let mut old_end: Option<usize> = None;
let mut new_start: Option<usize> = None;
let mut new_end: Option<usize> = None;
for line in view {
let fold_line = is_fold_line(line);
let old_present = line.old_line.is_some() || fold_line;
let new_present = (line.new_line.is_some()
&& !matches!(line.kind, LineKind::Deleted | LineKind::PendingDelete))
|| fold_line;
let old_wrap = if old_present {
split_old_line_wrap_count(app, line, old_width)
} else if app.split_align_lines && new_present {
split_new_line_wrap_count(app, line, old_width)
} else {
0
};
let new_wrap = if new_present {
split_new_line_wrap_count(app, line, new_width)
} else if app.split_align_lines && old_present {
split_old_line_wrap_count(app, line, new_width)
} else {
0
};
if old_wrap > 0 {
if line.hunk_index == Some(hunk_idx) {
if old_start.is_none() {
old_start = Some(old_idx);
}
old_end = Some(old_idx.saturating_add(old_wrap.saturating_sub(1)));
}
old_idx = old_idx.saturating_add(old_wrap);
}
if new_wrap > 0 {
if line.hunk_index == Some(hunk_idx) {
if new_start.is_none() {
new_start = Some(new_idx);
}
new_end = Some(new_idx.saturating_add(new_wrap.saturating_sub(1)));
}
new_idx = new_idx.saturating_add(new_wrap);
}
}
let old_bounds = old_start.zip(old_end);
let new_bounds = new_start.zip(new_end);
let (start, end) = match (old_bounds, new_bounds) {
(Some(old), Some(new)) => {
let old_dist = (old.0 as isize - scroll_offset as isize).abs();
let new_dist = (new.0 as isize - scroll_offset as isize).abs();
if old_dist < new_dist {
old
} else {
new
}
}
(Some(old), None) => old,
(None, Some(new)) => new,
(None, None) => return None,
};
let visible_start = scroll_offset;
let visible_end = scroll_offset.saturating_add(viewport_height.saturating_sub(1));
Some((start < visible_start, end > visible_end))
}
fn split_old_line_wrap_count(app: &mut App, line: &ViewLine, wrap_width: usize) -> usize {
if matches!(line.kind, LineKind::Modified | LineKind::PendingModify) {
if let Some(change) = app
.multi_diff
.current_navigator()
.diff()
.changes
.get(line.change_id)
{
let mut text = String::new();
for span in &change.spans {
match span.kind {
ChangeKind::Equal | ChangeKind::Delete | ChangeKind::Replace => {
text.push_str(&span.text);
}
ChangeKind::Insert => {}
}
}
if !text.is_empty() {
return wrap_count_for_text(&text, wrap_width);
}
}
}
let text = view_spans_to_text(&line.spans);
wrap_count_for_text(&text, wrap_width)
}
fn split_new_line_wrap_count(app: &mut App, line: &ViewLine, wrap_width: usize) -> usize {
if matches!(line.kind, LineKind::Modified | LineKind::PendingModify) {
if let Some(change) = app
.multi_diff
.current_navigator()
.diff()
.changes
.get(line.change_id)
{
let mut text = String::new();
for span in &change.spans {
match span.kind {
ChangeKind::Equal | ChangeKind::Insert => {
text.push_str(&span.text);
}
ChangeKind::Replace => {
let new_text = span.new_text.as_ref().unwrap_or(&span.text);
text.push_str(new_text);
}
ChangeKind::Delete => {}
}
}
if !text.is_empty() {
return wrap_count_for_text(&text, wrap_width);
}
}
}
let text = view_spans_to_text(&line.spans);
wrap_count_for_text(&text, wrap_width)
}
fn get_old_span_style(
kind: ViewSpanKind,
_line_kind: LineKind,
is_active: bool,
app: &App,
highlight_allowed: bool,
) -> Style {
let theme = &app.theme;
let use_bg = highlight_allowed
&& matches!(
app.diff_highlight,
DiffHighlightMode::Text | DiffHighlightMode::Word
);
let removed_bg = if use_bg {
super::boost_inline_bg(app, theme.diff_removed_bg, theme.delete_base())
} else {
None
};
match kind {
ViewSpanKind::Equal => Style::default().fg(theme.diff_context),
ViewSpanKind::Deleted => {
let mut style = if is_active {
super::delete_style(
app.animation_phase,
app.animation_progress,
app.is_backward_animation(),
app.strikethrough_deletions,
theme.delete_base(),
theme.diff_context,
removed_bg,
)
} else {
super::delete_style(
AnimationPhase::Idle,
0.0,
false,
false,
theme.delete_base(),
theme.delete_dim(),
removed_bg,
)
};
if app.strikethrough_deletions {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
style
}
ViewSpanKind::Inserted => {
Style::default().fg(theme.text_muted)
}
ViewSpanKind::PendingDelete => {
if is_active {
super::delete_style(
app.animation_phase,
app.animation_progress,
app.is_backward_animation(),
app.strikethrough_deletions,
theme.delete_base(),
theme.diff_context,
removed_bg,
)
} else {
let mut style = super::delete_style(
AnimationPhase::Idle,
0.0,
false,
false,
theme.delete_base(),
theme.delete_dim(),
removed_bg,
);
if app.strikethrough_deletions {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
style
}
}
ViewSpanKind::PendingInsert => Style::default()
.fg(theme.text_muted)
.add_modifier(Modifier::DIM),
}
}
fn get_new_span_style(
kind: ViewSpanKind,
_line_kind: LineKind,
is_active: bool,
app: &App,
highlight_allowed: bool,
) -> Style {
let theme = &app.theme;
let use_bg = highlight_allowed
&& matches!(
app.diff_highlight,
DiffHighlightMode::Text | DiffHighlightMode::Word
);
let added_bg = if use_bg {
super::boost_inline_bg(app, theme.diff_added_bg, theme.insert_base())
} else {
None
};
match kind {
ViewSpanKind::Equal => Style::default().fg(theme.diff_context),
ViewSpanKind::Inserted => {
super::insert_style(
AnimationPhase::Idle,
0.0,
false,
theme.insert_base(),
theme.insert_dim(),
added_bg,
)
}
ViewSpanKind::Deleted => {
Style::default().fg(theme.text_muted)
}
ViewSpanKind::PendingInsert => {
if is_active {
super::insert_style(
app.animation_phase,
app.animation_progress,
app.is_backward_animation(),
theme.insert_base(),
theme.insert_dim(),
added_bg,
)
} else {
let mut style = Style::default().fg(theme.insert_dim());
if let Some(bg) = added_bg {
style = style.bg(bg);
}
style
}
}
ViewSpanKind::PendingDelete => {
let mut style = Style::default().fg(theme.delete_dim());
let removed_bg = if use_bg { theme.diff_removed_bg } else { None };
if let Some(bg) = removed_bg {
style = style.bg(bg);
}
style
}
}
}
#[cfg(test)]
mod tests {
use super::{format_line_number, split_gutter_width, split_line_number_width};
use oyo_core::{LineKind, ViewLine};
fn view_line(old_line: Option<usize>, new_line: Option<usize>) -> ViewLine {
ViewLine {
content: String::new(),
spans: Vec::new(),
kind: LineKind::Context,
old_line,
new_line,
is_active: false,
is_active_change: false,
is_primary_active: false,
show_hunk_extent: false,
change_id: 0,
hunk_index: None,
has_changes: false,
}
}
#[test]
fn split_line_number_width_grows_for_large_files() {
let lines = [view_line(Some(9), Some(103474))];
assert_eq!(split_line_number_width(&lines), 6);
assert_eq!(split_gutter_width(6), 8);
assert_eq!(format_line_number(2039, 6), " 2039");
assert_eq!(format_line_number(103474, 6), "103474");
}
}