feat: toggle diff explorer and show file change indicators
This commit is contained in:
+227
-26
@@ -861,9 +861,23 @@ fn draw(
|
||||
if picker.presentation == FilePresentation::Floating {
|
||||
let panel = floating_panel(area);
|
||||
frame.render_widget(Clear, panel);
|
||||
draw_file_list(frame, picker, panel, editor.file_navigation_hint(), panels);
|
||||
draw_file_list(
|
||||
frame,
|
||||
picker,
|
||||
panel,
|
||||
editor.file_navigation_hint(),
|
||||
panels,
|
||||
None,
|
||||
);
|
||||
} else if let Some(panel) = dock {
|
||||
draw_file_list(frame, picker, panel, editor.file_navigation_hint(), panels);
|
||||
draw_file_list(
|
||||
frame,
|
||||
picker,
|
||||
panel,
|
||||
editor.file_navigation_hint(),
|
||||
panels,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(palette) = editor.palette() {
|
||||
@@ -1140,7 +1154,7 @@ fn draw_diff(
|
||||
notice_area,
|
||||
);
|
||||
}
|
||||
let (panes, navigator) = if diff.files.is_some() {
|
||||
let (panes, navigator) = if diff.files_visible && diff.files.is_some() {
|
||||
dock_layout(workspace, DockSide::Right, diff.focus == Focus::Files)
|
||||
} else {
|
||||
(workspace, None)
|
||||
@@ -1150,7 +1164,14 @@ fn draw_diff(
|
||||
if let Some(picker) = &diff.files
|
||||
&& let Some(navigator) = navigator
|
||||
{
|
||||
draw_file_list(frame, picker, navigator, file_navigation_hint, panels);
|
||||
draw_file_list(
|
||||
frame,
|
||||
picker,
|
||||
navigator,
|
||||
file_navigation_hint,
|
||||
panels,
|
||||
comparison.map(|comparison| comparison.files.as_slice()),
|
||||
);
|
||||
}
|
||||
if let Some(file) = selected {
|
||||
draw_diff_pane(frame, diff, file, before_area, true);
|
||||
@@ -1167,7 +1188,7 @@ fn draw_diff(
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(
|
||||
"Tab focus · i filter · Enter open/toggle · ↑/↓ scroll/tree · ←/→ horizontal · Esc close",
|
||||
"^E explorer · Tab focus · i filter · Enter open/toggle · ↑/↓ scroll/tree · ←/→ horizontal · Esc close",
|
||||
)
|
||||
.style(Style::default().fg(Color::DarkGray)),
|
||||
help,
|
||||
@@ -1740,6 +1761,7 @@ fn draw_file_list(
|
||||
panel: Rect,
|
||||
navigation_hint: &str,
|
||||
panels: &mut WheelPanels,
|
||||
changed_files: Option<&[ChangedFile]>,
|
||||
) {
|
||||
if panel.width == 0 || panel.height == 0 {
|
||||
return;
|
||||
@@ -1862,20 +1884,46 @@ fn draw_file_list(
|
||||
})
|
||||
.add_modifier(Modifier::BOLD);
|
||||
}
|
||||
let label = match picker.view {
|
||||
FileView::List => {
|
||||
format!("{} {}", if selected { ">" } else { " " }, entry.label)
|
||||
}
|
||||
FileView::Tree => format!(
|
||||
"{} {:indent$}{}{}",
|
||||
if selected { ">" } else { " " },
|
||||
"",
|
||||
entry.label,
|
||||
if entry.is_dir { "/" } else { "" },
|
||||
indent = entry.depth * 2,
|
||||
),
|
||||
};
|
||||
Line::styled(label, style)
|
||||
let changed = entry
|
||||
.catalog_index
|
||||
.and_then(|index| changed_files.and_then(|files| files.get(index)));
|
||||
let prefix = format!(
|
||||
"{} {:indent$}",
|
||||
if selected { ">" } else { " " },
|
||||
"",
|
||||
indent = if picker.view == FileView::Tree {
|
||||
entry.depth * 2
|
||||
} else {
|
||||
0
|
||||
},
|
||||
);
|
||||
let mut spans = Vec::with_capacity(if changed.is_some() { 6 } else { 3 });
|
||||
spans.push(Span::raw(prefix));
|
||||
if let Some(file) = changed {
|
||||
let color = match file.status {
|
||||
FileStatus::Added => Color::Green,
|
||||
FileStatus::Removed => Color::Red,
|
||||
FileStatus::Modified => Color::Yellow,
|
||||
FileStatus::Renamed => Color::Cyan,
|
||||
};
|
||||
spans.push(Span::styled(
|
||||
diff_status(file.status),
|
||||
Style::default().fg(color),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
spans.push(Span::raw(entry.label));
|
||||
if entry.is_dir && picker.view == FileView::Tree {
|
||||
spans.push(Span::raw("/"));
|
||||
}
|
||||
if let Some(file) = changed
|
||||
&& file.status == FileStatus::Renamed
|
||||
&& let Some(old_path) = &file.old_path
|
||||
{
|
||||
spans.push(Span::raw(" ← "));
|
||||
spans.push(Span::raw(display_text(&old_path.to_string_lossy())));
|
||||
}
|
||||
Line::from(spans).style(style)
|
||||
})
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(lines), results_area);
|
||||
@@ -1891,7 +1939,9 @@ fn draw_file_list(
|
||||
));
|
||||
}
|
||||
}
|
||||
let help = if picker.focused {
|
||||
let help = if changed_files.is_some() {
|
||||
"A added · M modified · D deleted · R renamed · ^E hide".into()
|
||||
} else if picker.focused {
|
||||
let controls = if picker.root.as_os_str().is_empty() {
|
||||
if browsing {
|
||||
"Enter · i filter · Esc close · Tab panes · ^T view"
|
||||
@@ -2299,7 +2349,6 @@ mod tests {
|
||||
assert_eq!(buffer[(after.x + 2, y)].symbol(), "+");
|
||||
assert_eq!(buffer[(after.x + 4, y)].fg, Color::Green);
|
||||
}
|
||||
assert!(screen_text(&terminal).contains("> added.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2328,7 +2377,6 @@ mod tests {
|
||||
let mut panels = WheelPanels::default();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("> new-name.txt"));
|
||||
assert!(text.contains("Before · R · old-name.txt"));
|
||||
assert!(text.contains("After · R · new-name.txt"));
|
||||
let geometry = panels.diff.as_ref().unwrap();
|
||||
@@ -2417,6 +2465,130 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_file_markers_follow_catalog_indices_in_every_explorer_view() {
|
||||
let files: Vec<_> = [
|
||||
("z.txt", FileStatus::Added),
|
||||
("chain/a/m.txt", FileStatus::Modified),
|
||||
("d.txt", FileStatus::Removed),
|
||||
("r.txt", FileStatus::Renamed),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(path, status)| {
|
||||
let mut file = deletion_file();
|
||||
file.path = path.into();
|
||||
file.status = status;
|
||||
file.old_path = (status == FileStatus::Renamed).then(|| "old\nname.txt".into());
|
||||
file
|
||||
})
|
||||
.collect();
|
||||
let mut picker =
|
||||
FilePicker::from_paths(PathBuf::new(), files.iter().map(|file| file.path.clone()));
|
||||
picker.reveal_path(std::path::Path::new("chain/a/m.txt"));
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 16)).unwrap();
|
||||
for view in [FileView::Tree, FileView::List] {
|
||||
picker.set_view(view);
|
||||
for focused in [true, false] {
|
||||
picker.focused = focused;
|
||||
for index in 0..files.len() {
|
||||
picker.reveal_path(&files[index].path);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
Some(&files),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (file, color) in
|
||||
files
|
||||
.iter()
|
||||
.zip([Color::Green, Color::Yellow, Color::Red, Color::Cyan])
|
||||
{
|
||||
let path = if view == FileView::Tree {
|
||||
std::path::Path::new(file.path.file_name().unwrap())
|
||||
} else {
|
||||
file.path.as_path()
|
||||
};
|
||||
let expected =
|
||||
format!("{} {}", diff_status(file.status), path.to_string_lossy());
|
||||
let (x, y) = (0..16)
|
||||
.find_map(|y| {
|
||||
let row = (0..100)
|
||||
.map(|x| buffer[(x, y)].symbol())
|
||||
.collect::<String>();
|
||||
row.find(&expected).map(|offset| {
|
||||
(UnicodeWidthStr::width(&row[..offset]) as u16, y)
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| panic!("missing {expected}"));
|
||||
assert_eq!(buffer[(x, y)].fg, color);
|
||||
}
|
||||
assert!(screen_text(&terminal).contains("r.txt ← old�name.txt"));
|
||||
assert_eq!(picker.accept(), Some(files[index].path.clone()));
|
||||
assert!(picker.query.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
picker.query = "m.txt".into();
|
||||
picker.refilter();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
Some(&files),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("M chain/a/m.txt"));
|
||||
assert!(!screen_text(&terminal).contains("R r.txt"));
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert!(!screen_text(&terminal).contains("M chain/a/m.txt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hiding_diff_explorer_reclaims_workspace_and_restores_its_geometry() {
|
||||
let mut diff = diff_comparison(vec![deletion_file()]);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 18)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let original = panels.diff.as_ref().unwrap().regions;
|
||||
diff.handle(Input::Ctrl('e'));
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let hidden = panels.diff.as_ref().unwrap().regions;
|
||||
assert_eq!(hidden[0].0, Rect::default());
|
||||
assert!(panels.files.is_none());
|
||||
assert_eq!(hidden[1].0.x, 0);
|
||||
assert_eq!(hidden[1].0.right(), hidden[2].0.x);
|
||||
assert_eq!(hidden[2].0.right(), 120);
|
||||
assert!(hidden[1].0.width + hidden[2].0.width > original[1].0.width + original[2].0.width);
|
||||
assert!(!screen_text(&terminal).contains("Explorer"));
|
||||
diff.handle(Input::Ctrl('e'));
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
assert_eq!(panels.diff.as_ref().unwrap().regions, original);
|
||||
assert_eq!(panels.files.unwrap().0, original[0].0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_notices_empty_comparisons_and_tiny_layouts_remain_renderable() {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
|
||||
@@ -2443,7 +2615,6 @@ mod tests {
|
||||
}]);
|
||||
render_diff(&mut terminal, &binary, &mut panels);
|
||||
assert!(screen_text(&terminal).contains("Binary file"));
|
||||
assert!(screen_text(&terminal).contains("> image.bin"));
|
||||
let blank = diff_comparison(vec![ChangedFile {
|
||||
path: "empty.txt".into(),
|
||||
old_path: None,
|
||||
@@ -2613,6 +2784,20 @@ mod tests {
|
||||
assert_eq!(diff.focus, Focus::After);
|
||||
assert_eq!(diff.top, 0);
|
||||
assert_eq!(diff.file, 3);
|
||||
let selected = diff.files.as_ref().unwrap().selected;
|
||||
session.handle(Input::Ctrl('e'));
|
||||
terminal
|
||||
.draw(|frame| viewports.draw(frame, session.editor()))
|
||||
.unwrap();
|
||||
assert!(viewports.scroll(
|
||||
&mut session,
|
||||
document_wheel(MouseEventKind::ScrollDown, tree.x + 1, tree.y + 1),
|
||||
));
|
||||
let diff = session.editor().diff().unwrap();
|
||||
assert_eq!(diff.focus, Focus::After);
|
||||
assert_eq!(diff.files.as_ref().unwrap().selected, selected);
|
||||
assert!(!diff.files.as_ref().unwrap().focused);
|
||||
assert!(viewports.panels.files.is_none());
|
||||
assert_eq!(session.editor().lines(), original);
|
||||
assert_eq!(session.editor().cursor(), cursor);
|
||||
assert!(viewports.document.geometry.is_none());
|
||||
@@ -3892,7 +4077,7 @@ end
|
||||
let area = Rect::new(3, 2, 40, 6);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(60, 12)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw_file_list(frame, picker, area, "", &mut panels))
|
||||
.draw(|frame| draw_file_list(frame, picker, area, "", &mut panels, None))
|
||||
.unwrap();
|
||||
let mouse = MouseEvent {
|
||||
kind: MouseEventKind::ScrollDown,
|
||||
@@ -3919,6 +4104,7 @@ end
|
||||
area,
|
||||
"",
|
||||
&mut panels,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -3966,6 +4152,7 @@ end
|
||||
area,
|
||||
"",
|
||||
&mut panels,
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4010,7 +4197,7 @@ end
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.set_cursor_position((59, 11));
|
||||
draw_file_list(frame, &picker, area, "", &mut panels);
|
||||
draw_file_list(frame, &picker, area, "", &mut panels, None);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
@@ -4027,7 +4214,7 @@ end
|
||||
}
|
||||
picker.navigate(false);
|
||||
terminal
|
||||
.draw(|frame| draw_file_list(frame, &picker, area, "", &mut panels))
|
||||
.draw(|frame| draw_file_list(frame, &picker, area, "", &mut panels, None))
|
||||
.unwrap();
|
||||
assert!(panels.manual_files.is_none());
|
||||
assert!(screen_text(&terminal).contains("> file-10"));
|
||||
@@ -4060,6 +4247,7 @@ end
|
||||
frame.area(),
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4167,6 +4355,7 @@ end
|
||||
area,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4212,6 +4401,7 @@ end
|
||||
area,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4235,6 +4425,7 @@ end
|
||||
area,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4258,6 +4449,7 @@ end
|
||||
area,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4274,6 +4466,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4294,6 +4487,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4331,6 +4525,7 @@ end
|
||||
area,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4388,6 +4583,7 @@ end
|
||||
frame.area(),
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4438,6 +4634,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4489,6 +4686,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4536,6 +4734,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4567,6 +4766,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4584,6 +4784,7 @@ end
|
||||
area,
|
||||
editor.file_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user