feat: toggle diff explorer and show file change indicators
This commit is contained in:
+162
-3
@@ -148,6 +148,7 @@ pub struct DiffState {
|
||||
pub error: Option<String>,
|
||||
pub comparison: Option<Comparison>,
|
||||
pub files: Option<FilePicker>,
|
||||
pub files_visible: bool,
|
||||
pub file: usize,
|
||||
pub focus: Focus,
|
||||
pub top: usize,
|
||||
@@ -177,6 +178,7 @@ impl DiffState {
|
||||
error: None,
|
||||
comparison: None,
|
||||
files: None,
|
||||
files_visible: true,
|
||||
file: 0,
|
||||
focus: Focus::Files,
|
||||
top: 0,
|
||||
@@ -434,6 +436,18 @@ impl DiffState {
|
||||
}
|
||||
|
||||
fn handle_view(&mut self, input: Input) {
|
||||
if input == Input::Ctrl('e') {
|
||||
self.files_visible = !self.files_visible;
|
||||
if self.files_visible {
|
||||
self.focus = Focus::Files;
|
||||
} else if self.focus == Focus::Files {
|
||||
self.focus = Focus::Before;
|
||||
}
|
||||
if let Some(files) = &mut self.files {
|
||||
files.focused = self.files_visible && self.focus == Focus::Files;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.focus == Focus::Files {
|
||||
let files = self.files.as_mut().expect("comparison file picker");
|
||||
match input {
|
||||
@@ -491,14 +505,16 @@ impl DiffState {
|
||||
self.focus = match self.focus {
|
||||
Focus::Files => Focus::Before,
|
||||
Focus::Before => Focus::After,
|
||||
Focus::After => Focus::Files,
|
||||
Focus::After if self.files_visible => Focus::Files,
|
||||
Focus::After => Focus::Before,
|
||||
}
|
||||
}
|
||||
Input::BackTab => {
|
||||
self.focus = match self.focus {
|
||||
Focus::Files => Focus::After,
|
||||
Focus::After => Focus::Before,
|
||||
Focus::Before => Focus::Files,
|
||||
Focus::Before if self.files_visible => Focus::Files,
|
||||
Focus::Before => Focus::After,
|
||||
}
|
||||
}
|
||||
Input::Up | Input::Ctrl('k') => self.navigate(false),
|
||||
@@ -527,7 +543,7 @@ impl DiffState {
|
||||
_ => {}
|
||||
}
|
||||
if let Some(files) = &mut self.files {
|
||||
files.focused = self.focus == Focus::Files;
|
||||
files.focused = self.files_visible && self.focus == Focus::Files;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,6 +1024,149 @@ mod tests {
|
||||
assert!(editor.diff().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explorer_toggle_preserves_filtered_tree_review_and_source_state() {
|
||||
let mut editor = editor();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("unsaved source".into())])
|
||||
.unwrap();
|
||||
let original = editor.context().text;
|
||||
let carets = editor.carets();
|
||||
let revision = editor.text_revision();
|
||||
open(&mut editor);
|
||||
let id = choose_pair(&mut editor);
|
||||
let mut changes = comparison();
|
||||
changes.files[0].path = "src/deep/renamed.txt".into();
|
||||
changes.files[1].path = "src/deep/added.txt".into();
|
||||
editor.apply_diff_result(id, Ok(Outcome::Comparison(changes)));
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Right);
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.handle(Input::Right);
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.handle(Input::Char('i'));
|
||||
text(&mut editor, "src");
|
||||
editor.handle(Input::Down);
|
||||
let snapshot = |state: &DiffState| {
|
||||
let files = state.files.as_ref().unwrap();
|
||||
(
|
||||
files.query.clone(),
|
||||
files.selected,
|
||||
files.view,
|
||||
files.searching,
|
||||
files
|
||||
.entries()
|
||||
.map(|row| (row.path.to_owned(), row.expanded))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
let tree = snapshot(editor.diff().unwrap());
|
||||
let rows = editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| (row.before, row.after, row.kind))
|
||||
.collect::<Vec<_>>();
|
||||
editor
|
||||
.bind("tree", "ctrl+e", Some("core.down".into()))
|
||||
.unwrap();
|
||||
editor
|
||||
.bind("selection", "ctrl+e", Some("core.up".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
let state = editor.diff().unwrap();
|
||||
assert!(!state.files_visible);
|
||||
assert_eq!(state.focus, Focus::Before);
|
||||
assert!(!state.files.as_ref().unwrap().focused);
|
||||
assert_eq!(
|
||||
(state.file, state.top, state.left_before, state.left_after),
|
||||
(0, 1, 1, 1)
|
||||
);
|
||||
assert_eq!(snapshot(state), tree);
|
||||
for (input, focus) in [
|
||||
(Input::Char('\t'), Focus::After),
|
||||
(Input::Char('\t'), Focus::Before),
|
||||
(Input::BackTab, Focus::After),
|
||||
(Input::BackTab, Focus::Before),
|
||||
] {
|
||||
editor.handle(input);
|
||||
let state = editor.diff().unwrap();
|
||||
assert_eq!(state.focus, focus);
|
||||
assert!(!state.files.as_ref().unwrap().focused);
|
||||
}
|
||||
for input in [
|
||||
Input::Char('z'),
|
||||
Input::Ctrl('t'),
|
||||
Input::Backspace,
|
||||
Input::Delete,
|
||||
Input::Paste("not source".into()),
|
||||
Input::Enter,
|
||||
] {
|
||||
editor.handle(input);
|
||||
}
|
||||
assert_eq!(snapshot(editor.diff().unwrap()), tree);
|
||||
editor.handle(Input::Down);
|
||||
assert_eq!(editor.diff().unwrap().top, 2);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
let state = editor.diff().unwrap();
|
||||
assert!(state.files_visible && state.files.as_ref().unwrap().focused);
|
||||
assert_eq!(state.focus, Focus::Files);
|
||||
assert_eq!(snapshot(state), tree);
|
||||
assert_eq!(
|
||||
(state.file, state.top, state.left_before, state.left_after),
|
||||
(0, 2, 1, 1)
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| (row.before, row.after, row.kind))
|
||||
.collect::<Vec<_>>(),
|
||||
rows
|
||||
);
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
editor.handle(Input::BackTab);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
assert!(!editor.diff().unwrap().files.as_ref().unwrap().focused);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Files);
|
||||
assert_eq!(snapshot(editor.diff().unwrap()), tree);
|
||||
assert_eq!(editor.context().text, original);
|
||||
assert_eq!(editor.carets(), carets);
|
||||
assert_eq!(editor.text_revision(), revision);
|
||||
editor.handle(Input::Escape);
|
||||
assert!(!editor.diff().unwrap().files.as_ref().unwrap().searching);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
editor.handle(Input::Escape);
|
||||
assert!(editor.diff().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explorer_toggle_is_not_a_revision_selector_control() {
|
||||
let mut editor = editor();
|
||||
open(&mut editor);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
assert_eq!(editor.diff().unwrap().selected, 0);
|
||||
editor
|
||||
.bind("selection", "ctrl+e", Some("core.down".into()))
|
||||
.unwrap();
|
||||
for phase in [Phase::Before, Phase::After] {
|
||||
assert_eq!(editor.diff().unwrap().phase, phase);
|
||||
editor.handle(Input::Ctrl('e'));
|
||||
let state = editor.diff().unwrap();
|
||||
assert!(state.files_visible);
|
||||
assert_eq!(state.selected, 1);
|
||||
assert_eq!(state.phase, phase);
|
||||
editor.handle(Input::Enter);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aligned_hunks_navigation_shared_top_independent_horizontal_and_file_reset() {
|
||||
let mut editor = editor();
|
||||
|
||||
+8
-3
@@ -1279,8 +1279,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn handle_diff(&mut self, input: Input) {
|
||||
// Escape and Tab always retain their modal close/back/focus semantics.
|
||||
if !matches!(input, Input::Escape | Input::Char('\t') | Input::BackTab) {
|
||||
// Modal controls take precedence over configured browsing bindings.
|
||||
let toggle_files = input == Input::Ctrl('e')
|
||||
&& self
|
||||
.diff
|
||||
.as_ref()
|
||||
.is_some_and(|diff| diff.phase == Phase::View);
|
||||
if !toggle_files && !matches!(input, Input::Escape | Input::Char('\t') | Input::BackTab) {
|
||||
if self.handle_tree_binding(&input) {
|
||||
return;
|
||||
}
|
||||
@@ -2620,7 +2625,7 @@ impl Editor {
|
||||
|
||||
fn handle_tree_binding(&mut self, input: &Input) -> bool {
|
||||
let picker = if let Some(diff) = &mut self.diff {
|
||||
diff.files.as_mut()
|
||||
diff.files.as_mut().filter(|_| diff.files_visible)
|
||||
} else {
|
||||
self.file_picker.as_mut()
|
||||
};
|
||||
|
||||
+47
-5
@@ -38,6 +38,8 @@ pub struct FileRow<'a> {
|
||||
/// The actual project-relative OS path, never reconstructed from the label.
|
||||
pub path: &'a Path,
|
||||
pub label: &'a str,
|
||||
/// Original virtual-catalog input index; absent for scanned entries and directories.
|
||||
pub catalog_index: Option<usize>,
|
||||
pub is_dir: bool,
|
||||
pub depth: usize,
|
||||
pub expanded: bool,
|
||||
@@ -48,6 +50,7 @@ struct Node {
|
||||
path: PathBuf,
|
||||
label: String,
|
||||
search_label: String,
|
||||
catalog_index: Option<usize>,
|
||||
is_dir: bool,
|
||||
parent: Option<usize>,
|
||||
children: Vec<usize>,
|
||||
@@ -153,6 +156,7 @@ impl FilePicker {
|
||||
path,
|
||||
search_label: label.to_lowercase(),
|
||||
label,
|
||||
catalog_index: None,
|
||||
is_dir: entry.file_type().unwrap().is_dir(),
|
||||
parent: None,
|
||||
children: Vec::new(),
|
||||
@@ -163,25 +167,26 @@ impl FilePicker {
|
||||
|
||||
/// Builds the changed-path catalog without consulting the working tree.
|
||||
pub fn from_paths(root: PathBuf, paths: impl IntoIterator<Item = PathBuf>) -> Self {
|
||||
let mut catalog = HashSet::new();
|
||||
for path in paths {
|
||||
let mut catalog = HashMap::new();
|
||||
for (index, path) in paths.into_iter().enumerate() {
|
||||
for parent in path
|
||||
.ancestors()
|
||||
.skip(1)
|
||||
.filter(|path| !path.as_os_str().is_empty())
|
||||
{
|
||||
catalog.insert((parent.to_owned(), true));
|
||||
catalog.insert((parent.to_owned(), true), None);
|
||||
}
|
||||
catalog.insert((path, false));
|
||||
catalog.entry((path, false)).or_insert(Some(index));
|
||||
}
|
||||
let nodes = catalog
|
||||
.into_iter()
|
||||
.map(|(path, is_dir)| {
|
||||
.map(|((path, is_dir), catalog_index)| {
|
||||
let label = label(&path);
|
||||
Node {
|
||||
path,
|
||||
search_label: label.to_lowercase(),
|
||||
label,
|
||||
catalog_index,
|
||||
is_dir,
|
||||
parent: None,
|
||||
children: Vec::new(),
|
||||
@@ -310,6 +315,7 @@ impl FilePicker {
|
||||
.tree
|
||||
.map_or(node.label.as_str(), |tree| self.tree[tree].label.as_str()),
|
||||
is_dir: node.is_dir,
|
||||
catalog_index: node.catalog_index,
|
||||
depth: row.depth,
|
||||
expanded: row.expanded,
|
||||
}
|
||||
@@ -797,6 +803,30 @@ mod tests {
|
||||
picker.reveal_path(Path::new("src/deep/deleted.rs"));
|
||||
assert_eq!(picker.accept(), Some(PathBuf::from("src/deep/deleted.rs")));
|
||||
assert_eq!(picker.entries().filter(|row| !row.is_dir).count(), 3);
|
||||
let expected_index = |row: &FileRow<'_>| {
|
||||
if row.is_dir {
|
||||
None
|
||||
} else {
|
||||
Some(match row.path.to_str().unwrap() {
|
||||
"x" => 0,
|
||||
"x/y" => 1,
|
||||
"src/deep/deleted.rs" => 2,
|
||||
path => panic!("unexpected path {path}"),
|
||||
})
|
||||
}
|
||||
};
|
||||
for view in [FileView::List, FileView::Tree] {
|
||||
picker.set_view(view);
|
||||
for query in ["", "x", "deleted"] {
|
||||
picker.query = query.into();
|
||||
picker.refilter();
|
||||
let rows: Vec<_> = picker.entries().collect();
|
||||
assert!(!rows.is_empty());
|
||||
for row in rows {
|
||||
assert_eq!(row.catalog_index, expected_index(&row));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
@@ -831,6 +861,18 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scanned_files_and_directories_have_no_catalog_index() {
|
||||
let directory = Directory::new();
|
||||
directory.put("nested/file.txt", "");
|
||||
let mut picker = directory.scan();
|
||||
for view in [FileView::List, FileView::Tree] {
|
||||
picker.set_view(view);
|
||||
picker.reveal_path(Path::new("nested/file.txt"));
|
||||
assert!(picker.entries().all(|row| row.catalog_index.is_none()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_ignore_negation_hidden_files_and_vcs_metadata() {
|
||||
let directory = Directory::new();
|
||||
|
||||
@@ -43,6 +43,7 @@ Markdown links: :follow_link (primary caret; local files only)
|
||||
Diff: :diff (Normal); choose Before then After; Working checkout means disk, not unsaved buffers
|
||||
Tab/Shift+Tab categories/focus; type to search IDs/refs; arrows navigate/scroll; Esc back/close
|
||||
Remote refs/history load into temporary comparison repos without changing active refs/checkout
|
||||
Changed-file tree: Ctrl+e hides/shows; A added, M modified, D removed, R renamed (old path shown)
|
||||
|
||||
Ruby config: $XDG_CONFIG_HOME/vamp/config.rb or ~/.config/vamp/config.rb
|
||||
Set VAMP_RUBY to choose the Ruby executable.
|
||||
|
||||
+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