use anyhow::Result;
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use crate::cli::DiffRange;
use crate::diff::{DiffLine, LineKind};
use crate::state::App;
use crate::{git, highlight};
pub fn tick(app: &mut App) {
if let Some(max_lines) = app.pending_expand.take() {
let _ = try_expand_fold(app, max_lines);
app.ensure_cursor_visible();
}
if let Some(target) = app.pending_range_change.take() {
apply_range_change(app, target);
}
}
pub fn apply_range_change(app: &mut App, target: String) {
let new_range = DiffRange::UncommittedAgainst {
base: target.clone(),
};
match git::load_diff_simple(&new_range) {
Ok(mut files) => {
highlight::highlight_files(&mut files);
app.diff_fingerprint = crate::storage::diff_fingerprint(&files);
app.files = files;
app.range = new_range;
app.selected = 0;
app.cursor_line = app.first_navigable_line();
app.diff_scroll = 0;
app.comment_focus = None;
app.set_flash(format!("Switched to {}", target));
}
Err(e) => {
app.set_flash(format!("Load failed: {}", e));
}
}
}
fn try_expand_fold(app: &mut App, max_lines: usize) -> Result<()> {
let file = app
.files
.get(app.selected)
.ok_or(anyhow::anyhow!("no file"))?;
let line = file
.lines
.get(app.cursor_line)
.ok_or(anyhow::anyhow!("no line"))?;
match line.kind {
LineKind::FoldDown => expand_down(app, max_lines),
LineKind::FoldUp => expand_up(app, max_lines),
_ => Ok(()),
}
}
fn expand_down(app: &mut App, max_lines: usize) -> Result<()> {
let line = &app.files[app.selected].lines[app.cursor_line];
let new_start = line.new_lineno.unwrap_or(1);
let old_start = line.old_lineno.unwrap_or(1);
let total = line.fold_count as usize;
let expand = total.min(max_lines);
let path = app.files[app.selected].path.clone();
let hunk_header = line.hunk_header.clone();
let content = git::load_file_content(&app.range, &path)?;
let file_start = (new_start as usize).saturating_sub(1);
let mut new_lines: Vec<DiffLine> = Vec::with_capacity(expand + 1);
for (i, text) in content.lines().skip(file_start).take(expand).enumerate() {
new_lines.push(DiffLine::body(
LineKind::Context,
text.to_string(),
Some(old_start + i as u32),
Some(new_start + i as u32),
));
}
let remaining = total - expand;
if remaining > 0 {
let down_header = if remaining > 20 {
String::new()
} else {
hunk_header
};
new_lines.push(DiffLine::fold_down(
old_start + expand as u32,
new_start + expand as u32,
remaining as u32,
down_header,
));
}
let cursor = app.cursor_line;
let file = &mut app.files[app.selected];
file.lines.splice(cursor..=cursor, new_lines);
let fold_up_idx = cursor + expand + if remaining > 0 { 1 } else { 0 };
if fold_up_idx < file.lines.len() && file.lines[fold_up_idx].kind == LineKind::FoldUp {
if remaining > 20 {
update_fold(
&mut file.lines[fold_up_idx],
remaining as u32,
old_start + expand as u32,
new_start + expand as u32,
);
} else {
file.lines.remove(fold_up_idx);
if remaining == 0 {
remove_adjacent_hunk_header_if_merged(file, fold_up_idx);
}
}
}
highlight::highlight_files(&mut app.files[app.selected..=app.selected]);
Ok(())
}
fn expand_up(app: &mut App, max_lines: usize) -> Result<()> {
let line = &app.files[app.selected].lines[app.cursor_line];
let new_start = line.new_lineno.unwrap_or(1);
let old_start = line.old_lineno.unwrap_or(1);
let total = line.fold_count as usize;
let expand = total.min(max_lines);
let path = app.files[app.selected].path.clone();
let hunk_header = line.hunk_header.clone();
let content = git::load_file_content(&app.range, &path)?;
let file_start = (new_start as usize).saturating_sub(1);
let skip = file_start + total - expand;
let mut new_lines: Vec<DiffLine> = Vec::with_capacity(expand + 1);
let remaining = total - expand;
if remaining > 0 {
new_lines.push(DiffLine::fold_up(
old_start,
new_start,
remaining as u32,
hunk_header,
));
}
let offset = total - expand;
for (i, text) in content.lines().skip(skip).take(expand).enumerate() {
new_lines.push(DiffLine::body(
LineKind::Context,
text.to_string(),
Some(old_start + (offset + i) as u32),
Some(new_start + (offset + i) as u32),
));
}
let cursor = app.cursor_line;
let file = &mut app.files[app.selected];
let len_before = file.lines.len();
file.lines.splice(cursor..=cursor, new_lines);
let fold_down_idx = cursor.checked_sub(1);
if let Some(idx) = fold_down_idx {
if file.lines[idx].kind == LineKind::FoldDown {
if remaining > 20 {
update_fold(&mut file.lines[idx], remaining as u32, old_start, new_start);
} else if remaining > 0 {
file.lines.remove(idx);
if app.cursor_line > 0 {
app.cursor_line -= 1;
}
} else {
file.lines.remove(idx);
if app.cursor_line > 0 {
app.cursor_line -= 1;
}
remove_adjacent_hunk_header_if_merged(file, idx.saturating_sub(1));
}
}
}
let len_after = file.lines.len();
let delta = len_after.saturating_sub(len_before);
app.diff_scroll = app.diff_scroll.saturating_add(delta);
highlight::highlight_files(&mut app.files[app.selected..=app.selected]);
Ok(())
}
fn update_fold(line: &mut DiffLine, count: u32, old_start: u32, new_start: u32) {
line.fold_count = count;
line.old_lineno = Some(old_start);
line.new_lineno = Some(new_start);
let arrow = match line.kind {
LineKind::FoldDown => "▼",
LineKind::FoldUp => "▲",
_ => " ",
};
line.text = format!("{arrow} {count} lines");
}
pub fn handle_mouse(app: &mut App, mouse: MouseEvent) {
let col = mouse.column;
let row = mouse.row;
match mouse.kind {
MouseEventKind::Down(MouseButton::Left) => {
let fl = app.file_list_area;
let da = app.diff_area;
if col > fl.x && col < fl.x + fl.width - 1 && row > fl.y && row < fl.y + fl.height - 1 {
let clicked_row = (row - fl.y - 1) as usize;
let tree = crate::ui::file_tree_indices(
&app.files,
&app.comments,
&app.expanded_dirs,
app.file_filter.as_deref(),
);
if let Some(&Some(file_idx)) = tree.get(clicked_row) {
if file_idx < app.files.len() {
app.selected = file_idx;
app.cursor_line = app.first_navigable_line();
app.diff_scroll = 0;
app.file_tree_cursor = clicked_row;
}
}
app.focus = crate::state::Focus::Files;
} else if col > da.x
&& col < da.x + da.width - 1
&& row > da.y
&& row < da.y + da.height - 1
{
let visual_row = (row - da.y - 1) as usize;
if let Some(&target) = app.diff_row_map.get(visual_row) {
let (navigable, is_fold) = app
.current()
.and_then(|f| f.lines.get(target))
.map(|l| (l.kind.is_navigable(), l.kind.is_fold()))
.unwrap_or((false, false));
if navigable {
app.cursor_line = target;
}
if is_fold {
app.cursor_line = target;
app.pending_expand = Some(20);
}
}
app.focus = crate::state::Focus::Diff;
}
}
MouseEventKind::ScrollDown => {
if row >= app.diff_area.y && row < app.diff_area.y + app.diff_area.height {
app.move_cursor_down(3);
} else if row >= app.file_list_area.y
&& row < app.file_list_area.y + app.file_list_area.height
{
app.select_next_file();
}
}
MouseEventKind::ScrollUp => {
if row >= app.diff_area.y && row < app.diff_area.y + app.diff_area.height {
app.move_cursor_up(3);
} else if row >= app.file_list_area.y
&& row < app.file_list_area.y + app.file_list_area.height
{
app.select_prev_file();
}
}
_ => {}
}
}
fn remove_adjacent_hunk_header_if_merged(file: &mut crate::diff::FileDiff, check_idx: usize) {
if check_idx >= file.lines.len() {
return;
}
if file.lines[check_idx].kind == LineKind::HunkHeader {
let before_is_context = check_idx > 0 && file.lines[check_idx - 1].kind.is_code();
let after_is_context =
check_idx + 1 < file.lines.len() && file.lines[check_idx + 1].kind.is_code();
if before_is_context && after_is_context {
file.lines.remove(check_idx);
}
}
}