style: move diff metadata into pane borders

This commit is contained in:
Valdemar Lorenzen
2026-10-12 15:08:41 +02:00
parent 054225686e
commit 9f1ea04f4d
3 changed files with 349 additions and 122 deletions
+2
View File
@@ -63,6 +63,8 @@ Redraws stage Ratatui's buffer diff, cursor placement, and DEC synchronized-upda
The TUI allocates a flexible buffer viewport and optional one-row top/bottom bars, with no reserved prompt/message footer. Both bars derive visibility from configured content: empty content takes no row, and tabs-only content is hidden with a single tab in either position. Both render across ordinary, split, diff, and selector views. Ordered left/center/right groups use full-row centering, active-tab collision priority, internal padding, and independent styled edges. Floating Command/Search input owns the hardware cursor; non-focusable operation/diagnostic notifications anchor bottom-right inside the chrome-free workspace and hide obscured document carets. Higher-priority modal overlays suppress them without changing input state. Wheel hit regions clear each frame and consume reports over these popups. Zero-sized terminals render nothing; minimal-height input never reserves another row. Layout and terminal-cell text mapping remain frontend-owned.
Comparison views reserve no separate metadata header or native-buffer help footer. Revision labels render bottom-left in pane borders (combined on single-pane views), with per-side editability top-right and focus indicated by border styling. Dock and document geometry use the reclaimed workspace rows; conditional loading/error/file notices remain separate from the border metadata.
Mouse wheel input uses the last rendered document/panel rectangles for hit-testing. Document offsets and explorer scroll state remain frontend-owned; three-row wheel steps never become document cursor motions. Manual document views persist until logical caret-reveal, caret/selection, mode, revision, or buffer changes; offscreen primary and browsing-selection cursors are not placed. Hover wheel input reuses the existing editor-owned popup scrolling.
Already-ready mouse reports share a redraw: at most 64 reports are applied in their original order before painting, without dropping or summing away wheel steps. Readiness checks do not wait. The first non-mouse event is retained for the next iteration after rendering, so keys, paste, and resizes cannot reuse geometry from before their layout changes. The batch bound keeps continuous mouse input from starving drawing or background polling.
+1 -1
View File
@@ -305,7 +305,7 @@ Comparison discovery builds the changed-path/status catalog first. File text, no
The changed-file explorer is **docked on the right**. Modified files show Before on the left and After on the right. Added files show only After, using the full text workspace; deleted files show only Before. Rename-only files show one empty pane, with the old and new paths in the explorer; renames with text or final-newline changes retain the split view. Before and historical/remote After snapshots are read-only. Selecting **Working checkout** as After binds eligible regular UTF-8 files to normal editor buffers, reusing already-open dirty buffers and their undo history. Editable After supports native edits, completion, undo/redo, and `:w`. Removed files, rename-only files, binary files, symlinks, and submodules remain read-only. Closing the diff keeps its buffers and unsaved edits.
When a diff occupies the sole workspace pane, its top bar spans the full editor width. The right-docked changed-file explorer starts below that bar rather than covering the header; closing the dock preserves the full-width bar.
Diff metadata lives in the text-pane borders instead of a separate info row: split panes show their own Before/After revision labels at the bottom-left; a single visible pane shows the combined `before → after` label there. Each pane shows its read-only/editable state at the top-right, independently of keyboard focus. Before is always read-only; After is editable only for an eligible Working-checkout file. The changed-file explorer and panes start directly below configured chrome, and there is no native-buffer help footer. Loading/error/file notices remain visible when applicable. Narrow borders prioritize state and clip titles without painting over the gutter or source text.
Text panes use normal buffer interaction regardless of writability: motions and counts, search, Visual/Visual Line selections, multiple carets, yank, remapped bindings, and non-mutating actions work in Before and After. Read-only snapshots reject insert/delete/change/paste, repeat edits, undo/redo, mutating plugin effects, and saves, including saves to another path. Their text retains the revision's final newline. Snapshots use separate virtual LSP document identities and do not appear in ordinary buffer lists or dirty-buffer comparisons. Closing the diff disposes unreferenced snapshots and restores the live or original buffer in its workspace pane; snapshots displayed by other panes or tabs remain read-only until closed. Explicit buffer commands and Markdown/plugin navigation retain their ordinary behavior; native LSP definitions use the routing below.
+346 -121
View File
@@ -409,17 +409,6 @@ impl BufferViewports {
let syntax = self.syntax.entry(editor.buffer_id()).or_default();
let full_workspace = draw_bars(frame, editor, frame.area(), syntax);
let multiple = editor.pane_layout().len() > 1;
let diff_header = editor
.diff()
.filter(|diff| !multiple && diff.phase == Phase::View)
.map(|_| {
Rect::new(
full_workspace.x,
full_workspace.y,
full_workspace.width,
full_workspace.height.min(1),
)
});
let (workspace, dock) = match editor.file_picker() {
Some(picker) if picker.presentation == FilePresentation::Docked => dock_layout(
full_workspace,
@@ -432,13 +421,6 @@ impl BufferViewports {
),
_ => (full_workspace, None),
};
let dock = dock.map(|mut panel| {
if let Some(header) = diff_header {
panel.y = panel.y.saturating_add(header.height);
panel.height = panel.height.saturating_sub(header.height);
}
panel
});
editor.set_pane_bounds(pane_rect(workspace));
self.rects.clear();
editor
@@ -504,11 +486,6 @@ impl BufferViewports {
self.panels.begin_frame();
self.document.geometry = None;
}
if let Some(header) = diff_header
&& let Some(diff) = editor.diff()
{
draw_diff_header(frame, diff, Some(editor), header);
}
if let Some(panel) = dock
&& let Some(picker) = editor.file_picker()
{
@@ -2035,10 +2012,24 @@ struct DiffGeometry {
wrapping: bool,
}
fn diff_panel(frame: &mut Frame, area: Rect, title: &str, focused: bool) -> Rect {
fn diff_panel(
frame: &mut Frame,
area: Rect,
title: &str,
state: &str,
revision: &str,
focused: bool,
) -> Rect {
if area.width < 4 || area.height < 4 {
return area;
}
let title_width = usize::from(area.width - 2)
.saturating_sub(UnicodeWidthStr::width(state) + usize::from(!state.is_empty()));
let title = if UnicodeWidthStr::width(title) > title_width {
Cow::Owned(visible_line(title, 0, title_width))
} else {
Cow::Borrowed(title)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(if focused {
@@ -2046,7 +2037,9 @@ fn diff_panel(frame: &mut Frame, area: Rect, title: &str, focused: bool) -> Rect
} else {
Color::DarkGray
}))
.title(title);
.title_top(Line::from(title).left_aligned())
.title_top(Line::from(state).right_aligned())
.title_bottom(Line::from(revision).left_aligned());
let inner = block.inner(area);
frame.render_widget(block, area);
inner
@@ -2296,46 +2289,6 @@ fn draw_diff_popups(
}
}
fn draw_diff_header(frame: &mut Frame, diff: &DiffState, editor: Option<&Editor>, header: Rect) {
if header.width == 0 || header.height == 0 {
return;
}
let (before, after) = diff.comparison.as_ref().map_or(("", ""), |comparison| {
(comparison.before.as_str(), comparison.after.as_str())
});
frame.render_widget(
Paragraph::new(format!(
"Diff · {} → {} · {:?} focus · {}",
display_text(before),
display_text(after),
diff.focus,
editor
.filter(|editor| editor.diff_interacting())
.map_or_else(
|| "read-only".into(),
|editor| format!(
"{} · {}{} · {}:{}",
if editor.diff_editing() {
"editable"
} else {
"read-only"
},
if editor.visual_linewise() {
"VISUAL LINE".into()
} else {
editor.mode().to_string()
},
if editor.dirty() { " [+]" } else { "" },
editor.cursor().row + 1,
editor.cursor().column + 1,
),
)
))
.style(Style::default().fg(Color::Black).bg(Color::Cyan)),
header,
);
}
fn draw_diff(
frame: &mut Frame,
diff: &DiffState,
@@ -2362,7 +2315,7 @@ fn draw_diff(
"After"
};
let title = format!(" Diff · Choose {stage} ");
let content = diff_panel(frame, panel, &title, true);
let content = diff_panel(frame, panel, &title, "", "", true);
let notice = diff.error.as_deref().or_else(|| {
if diff.loading {
Some(if diff.after.is_some() {
@@ -2526,7 +2479,7 @@ fn draw_diff(
}
frame.render_widget(
Paragraph::new(format!(
"{navigation_hint} · Tab categories · type search · Enter select · Esc back/clear · :diff.close"
"{navigation_hint} · Tab categories · type search · Enter select · Esc close · Ctrl+p actions"
))
.style(Style::default().fg(Color::DarkGray)),
help,
@@ -2559,16 +2512,11 @@ fn draw_diff(
syntax_error.or_else(|| selected.and_then(|file| file.notice.as_deref()))
}
});
let [header, notice_area, workspace, help] = Layout::vertical([
Constraint::Length(1),
let [notice_area, workspace] = Layout::vertical([
Constraint::Length(u16::from(notice.is_some() && area.height >= 3)),
Constraint::Fill(1),
Constraint::Length(u16::from(area.height >= 3)),
])
.areas(area);
if !panels.workspace_managed || editor.is_none_or(|editor| editor.pane_layout().len() > 1) {
draw_diff_header(frame, diff, editor, header);
}
if let Some(notice) = notice {
frame.render_widget(
Paragraph::new(display_text(notice)).style(Style::default().fg(
@@ -2740,7 +2688,7 @@ fn draw_diff(
&& let Some(syntax) = &panels.diff_syntax
{
if pane_layout == PaneLayout::Empty {
diff_panel(frame, before_area, "", diff.focus == Focus::Before);
diff_pane_panel(frame, diff, Some(file), before_area, true);
} else {
if before_area != Rect::default() {
let text_area = draw_diff_pane(
@@ -2810,6 +2758,7 @@ fn draw_diff(
}
}
} else {
let content = diff_pane_panel(frame, diff, selected, panes, true);
frame.render_widget(
Paragraph::new(if diff.loading {
"Loading file…"
@@ -2818,16 +2767,9 @@ fn draw_diff(
} else {
"No changed files"
}),
panes,
content,
);
}
frame.render_widget(
Paragraph::new(
"Native buffer bindings · Tab focus · [/] expand above/below in Normal · :diff.close",
)
.style(Style::default().fg(Color::DarkGray)),
help,
);
panels.diff = Some(DiffGeometry {
regions: [
(navigator.unwrap_or_default(), Focus::Files),
@@ -2854,6 +2796,62 @@ fn diff_status(status: FileStatus) -> &'static str {
}
}
fn diff_pane_panel(
frame: &mut Frame,
diff: &DiffState,
file: Option<&ChangedFile>,
area: Rect,
before: bool,
) -> Rect {
if area.width < 4 || area.height < 4 {
return area;
}
let title = file.map_or_else(
|| " Diff ".into(),
|file| {
let label = if before { "Before" } else { "After" };
let path = if before {
file.old_path.as_ref().unwrap_or(&file.path)
} else {
&file.path
};
format!(
" {label} · {} · {} ",
diff_status(file.status),
display_text(&path.to_string_lossy())
)
},
);
let revision = diff
.comparison
.as_ref()
.map_or_else(String::new, |comparison| {
if diff.pane_layout() == PaneLayout::Split {
format!(
" {} ",
display_text(if before {
&comparison.before
} else {
&comparison.after
})
)
} else {
format!(
" {} → {} ",
display_text(&comparison.before),
display_text(&comparison.after)
)
}
});
let state = if !before && diff.after_editable() {
" editable "
} else {
" read-only "
};
let focus = if before { Focus::Before } else { Focus::After };
diff_panel(frame, area, &title, state, &revision, diff.focus == focus)
}
#[allow(clippy::too_many_arguments)]
fn draw_diff_pane(
frame: &mut Frame,
@@ -2870,19 +2868,8 @@ fn draw_diff_pane(
if area.width == 0 || area.height == 0 {
return area;
}
let label = if before { "Before" } else { "After" };
let path = if before {
file.old_path.as_ref().unwrap_or(&file.path)
} else {
&file.path
};
let title = format!(
" {label} · {} · {} ",
diff_status(file.status),
display_text(&path.to_string_lossy())
);
let focus = if before { Focus::Before } else { Focus::After };
let content = diff_panel(frame, area, &title, diff.focus == focus);
let content = diff_pane_panel(frame, diff, Some(file), area, before);
if diff.display_rows.is_empty() {
frame.render_widget(
Paragraph::new(file.notice.as_deref().map(display_text).unwrap_or_else(|| {
@@ -4592,6 +4579,178 @@ mod tests {
notice: None,
}
}
#[test]
fn diff_borders_keep_full_revisions_and_each_panes_own_editability() {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(180, 12)).unwrap();
let mut panels = WheelPanels::default();
for case in ["working", "unlisted", "historical", "binary"] {
let mut file = syntax_diff_file("new.rs", "fn old() {}", "fn new() {}");
file.status = FileStatus::Renamed;
file.old_path = Some("old.rs".into());
if case == "binary" {
file.notice = Some("Binary or non-UTF-8 file; text comparison unavailable".into());
}
let mut diff = diff_comparison(vec![file]);
diff.files_visible = false;
let comparison = diff.comparison.as_mut().unwrap();
comparison.before = "abcdef0123456789 (baseline)".into();
comparison.after = "fedcba9876543210 (destination)".into();
if case != "unlisted" {
comparison.editable_paths.insert("new.rs".into());
}
if case == "historical" {
diff.after.as_mut().unwrap().target =
crate::diff::RevisionTarget::Commit("fedcba9876543210".into());
}
let writable = case == "working";
assert_eq!(diff.after_editable(), writable, "{case}");
for focus in [Focus::Before, Focus::After] {
diff.focus = focus;
render_diff(&mut terminal, &diff, &mut panels);
let regions = panels.diff.as_ref().unwrap().regions;
for (index, label, path, state) in [
(1, "abcdef0123456789 (baseline)", "old.rs", "read-only"),
(
2,
"fedcba9876543210 (destination)",
"new.rs",
if writable { "editable" } else { "read-only" },
),
] {
let pane = regions[index].0;
let top = pane_screen(&terminal, Rect::new(pane.x, pane.y, pane.width, 1));
let bottom = pane_screen(
&terminal,
Rect::new(pane.x, pane.bottom() - 1, pane.width, 1),
);
assert!(top.contains(path), "{case}: {top}");
assert!(
pane_screen(&terminal, Rect::new(pane.right() - 13, pane.y, 12, 1),)
.contains(state),
"{case}: {top}",
);
assert!(
bottom.find(label).is_some_and(|offset| {
UnicodeWidthStr::width(&bottom[..offset]) <= 2
}),
"{bottom}",
);
assert!(!top.contains(label));
assert!(!bottom.contains('→'));
assert_eq!(screen_text(&terminal).matches(label).count(), 1);
assert_eq!(
terminal.backend().buffer()[(pane.x, pane.y)].fg,
if regions[index].1 == focus {
Color::Cyan
} else {
Color::DarkGray
},
);
}
assert!(!screen_text(&terminal).contains("Native buffer bindings"));
assert!(!screen_text(&terminal).contains("focus ·"));
}
}
}
#[test]
fn diff_workspace_reclaims_header_and_footer_without_hiding_notices() {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 14)).unwrap();
let mut panels = WheelPanels::default();
let mut diff = diff_comparison(vec![deletion_file()]);
for notice in [None, Some("comparison failed"), Some("loading")] {
diff.error = notice
.filter(|notice| *notice != "loading")
.map(str::to_owned);
diff.loading = notice == Some("loading");
render_diff(&mut terminal, &diff, &mut panels);
let area = terminal.backend().buffer().area;
let regions = panels.diff.as_ref().unwrap().regions;
for (pane, _) in regions {
assert_eq!(pane.y, area.y + u16::from(notice.is_some()));
assert_eq!(pane.bottom(), area.bottom());
}
if let Some(notice) = notice {
let row = pane_screen(&terminal, Rect::new(area.x, area.y, area.width, 1));
assert!(row.contains(if notice == "loading" {
"Loading file"
} else {
notice
}));
}
let [_, before, after] = regions.map(|(pane, _)| pane);
assert_eq!(before.right(), after.x);
assert_eq!(
diff_inner(before).height,
area.height - 2 - u16::from(notice.is_some()),
);
}
}
#[test]
fn diff_border_titles_clip_unicode_without_overwriting_state_or_text() {
let mut file = syntax_diff_file(
&format!("界e\u{301}👩‍🚀\n\t{}", "long-path".repeat(12)),
"old",
"new",
);
file.old_path = Some(format!("e\u{301}界👩‍🚀\u{1b}{}", "old-path".repeat(12)).into());
let mut diff = diff_comparison(vec![file]);
diff.files_visible = false;
let comparison = diff.comparison.as_mut().unwrap();
comparison.before = "before 界e\u{301}👩‍🚀\n\t\u{1b}".into();
comparison.after = "after e\u{301}界👩‍🚀\r\u{7f}".into();
let mut panels = WheelPanels::default();
for (width, height) in [(180, 10), (64, 8), (36, 6), (8, 4), (3, 3), (1, 1)] {
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
render_diff(&mut terminal, &diff, &mut panels);
for (pane, _) in panels.diff.as_ref().unwrap().regions.into_iter().skip(1) {
assert!(pane.right() <= width && pane.bottom() <= height);
let buffer = terminal.backend().buffer();
for y in pane.y..pane.bottom() {
for x in pane.x..pane.right() {
let symbol = buffer[(x, y)].symbol();
assert!(!symbol.chars().any(char::is_control), "{symbol:?}");
assert!(!symbol.starts_with('\u{301}'), "{symbol:?}");
assert!(!symbol.starts_with('\u{200d}'), "{symbol:?}");
if symbol.contains('👩') || symbol.contains('🚀') {
assert_eq!(symbol, "👩‍🚀");
}
}
}
if width >= 36 {
assert!(
pane_screen(&terminal, Rect::new(pane.right() - 13, pane.y, 12, 1))
.contains("read-only")
);
let inner = diff_inner(pane);
assert_eq!(buffer[(inner.x, inner.y)].symbol(), "1");
assert_eq!(
buffer[(inner.x + 3, inner.y)].symbol(),
if pane.x == 0 { "o" } else { "n" },
);
}
if width == 180 {
let bottom = pane_screen(
&terminal,
Rect::new(pane.x, pane.bottom() - 1, pane.width, 1),
);
for grapheme in [
if pane.x == 0 { "before" } else { "after" },
"界",
"e\u{301}",
"👩‍🚀",
] {
assert!(bottom.contains(grapheme), "{bottom}");
}
assert_eq!(bottom.matches('�').count(), if pane.x == 0 { 3 } else { 2 });
}
}
}
}
#[test]
fn diff_status_layouts_hide_absent_panes_and_leave_unchanged_renames_blank() {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 12)).unwrap();
@@ -4621,6 +4780,11 @@ mod tests {
}];
let mut diff = diff_comparison(vec![file]);
diff.files_visible = false;
diff.comparison
.as_mut()
.unwrap()
.editable_paths
.insert("deleted-lines.txt".into());
diff.focus = if added { Focus::After } else { Focus::Before };
diff.left_after = 2;
render_diff(&mut terminal, &diff, &mut panels);
@@ -4653,6 +4817,17 @@ mod tests {
}));
assert!(text.contains(if added { "After · A" } else { "Before · D" }));
assert!(!text.contains(if added { "Before ·" } else { "After ·" }));
let bottom = pane_screen(
&terminal,
Rect::new(present.x, present.bottom() - 1, present.width, 1),
);
assert!(bottom.contains("old revision → Working checkout"));
assert_eq!(text.matches("old revision").count(), 1);
assert_eq!(text.matches("Working checkout").count(), 1);
assert!(
pane_screen(&terminal, Rect::new(present.right() - 13, present.y, 12, 1),)
.contains(if added { "editable" } else { "read-only" })
);
assert!(!text.contains('╱'));
}
let mut empty_added = deletion_file();
@@ -4669,6 +4844,14 @@ mod tests {
assert_eq!(geometry.regions[1].0, Rect::default());
assert_eq!(geometry.regions[2].0.width, 160);
assert!(screen_text(&terminal).contains("metadata-only addition"));
let pane = geometry.regions[2].0;
assert!(
pane_screen(
&terminal,
Rect::new(pane.x, pane.bottom() - 1, pane.width, 1),
)
.contains("old revision → Working checkout")
);
let mut renamed = deletion_file();
renamed.status = FileStatus::Renamed;
@@ -4699,8 +4882,13 @@ mod tests {
}
}
let text = screen_text(&terminal);
assert!(!text.contains("Before ·"));
assert!(!text.contains("After ·"));
assert!(
pane_screen(
&terminal,
Rect::new(blank.x, blank.bottom() - 1, blank.width, 1),
)
.contains("old revision → Working checkout")
);
assert!(!text.contains("unchanged rename content"));
assert!(!text.contains('╱'));
@@ -4842,6 +5030,8 @@ mod tests {
render_diff(&mut terminal, &diff, &mut panels);
let text = screen_text(&terminal);
assert!(text.contains("Choose Before"));
assert!(text.contains("Tab categories"));
assert!(text.contains("Enter select"));
assert!(text.contains("Working checkout · pinned"));
assert!(text.contains("> commit-19"));
assert!(!text.contains("commit-00"));
@@ -4857,6 +5047,8 @@ mod tests {
diff.handle(Input::Enter);
render_diff(&mut terminal, &diff, &mut panels);
assert!(screen_text(&terminal).contains("Choose After · Before: commit-19"));
assert!(screen_text(&terminal).contains("Tab categories"));
assert!(screen_text(&terminal).contains("Enter select"));
diff.handle(Input::Ctrl('u'));
diff.handle(Input::Char('\t'));
render_diff(&mut terminal, &diff, &mut panels);
@@ -4977,7 +5169,7 @@ mod tests {
}
assert_eq!(buffer[(before.x, before.y)].fg, Color::Cyan);
assert_eq!(buffer[(after.x, after.y)].fg, Color::DarkGray);
assert!(screen_text(&terminal).contains("Before focus"));
assert_eq!(diff.focus, Focus::Before);
}
#[test]
@@ -5149,9 +5341,6 @@ mod tests {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
let text = screen_text(&terminal);
assert!(text.contains("Before · R · old-name.txt"));
assert!(text.contains("After · R · new-name.txt"));
let geometry = panels.diff.as_ref().unwrap();
let before = geometry.regions[1].0;
let after = geometry.regions[2].0;
@@ -5163,7 +5352,7 @@ mod tests {
assert_eq!(buffer[(after.x + 8, after.y + 1)].fg, Color::Reset);
diff.handle(Input::Char('\t'));
render_diff(&mut terminal, &diff, &mut panels);
assert!(screen_text(&terminal).contains("After focus"));
assert_eq!(diff.focus, Focus::After);
assert_eq!(
terminal.backend().buffer()[(after.x, after.y)].fg,
Color::Cyan
@@ -5184,7 +5373,7 @@ mod tests {
let navigator = regions[0].0;
let before = regions[1].0;
let after = regions[2].0;
let workspace = Rect::new(0, 1, 120, 16);
let workspace = terminal.backend().buffer().area;
let (panes, dock) = dock_layout(workspace, DockSide::Right, true);
assert_eq!(Some(navigator), dock);
assert_eq!(before.x, panes.x);
@@ -5424,10 +5613,40 @@ mod tests {
}]);
render_diff(&mut terminal, &blank, &mut panels);
assert!(screen_text(&terminal).contains("Empty file"));
let mut unloaded_file = deletion_file();
unloaded_file.loaded = false;
let mut unloaded = diff_comparison(vec![unloaded_file]);
unloaded.loading = false;
render_diff(&mut terminal, &unloaded, &mut panels);
assert!(screen_text(&terminal).contains("File details unavailable"));
for state in [&empty, &binary, &blank, &unloaded] {
render_diff(&mut terminal, state, &mut panels);
let pane = panels
.diff
.as_ref()
.unwrap()
.regions
.into_iter()
.skip(1)
.find(|(pane, _)| *pane != Rect::default())
.unwrap()
.0;
assert!(
pane_screen(
&terminal,
Rect::new(pane.x, pane.bottom() - 1, pane.width, 1),
)
.contains("old revision → Working checkout")
);
assert!(
pane_screen(&terminal, Rect::new(pane.right() - 13, pane.y, 12, 1),)
.contains("read-only")
);
}
for (width, height) in [(1, 1), (2, 2), (3, 3), (8, 4), (39, 5)] {
let mut tiny =
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
for state in [&diff, &empty, &binary, &blank] {
for state in [&diff, &empty, &binary, &blank, &unloaded] {
render_diff(&mut tiny, state, &mut panels);
if let Some(geometry) = &panels.diff {
for (rect, _) in geometry.regions {
@@ -7118,7 +7337,7 @@ mod tests {
);
}
}
assert_eq!(views.panels.files.unwrap().0.y, 2);
assert_eq!(views.panels.files.unwrap().0.y, views.rects[0].1.y);
let title = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
@@ -7242,8 +7461,6 @@ mod tests {
.draw(|frame| views.draw_session(frame, &mut session))
.unwrap();
assert!(views.diff_visible);
assert_eq!(terminal.backend().buffer()[(0, 1)].bg, Color::Cyan);
assert_eq!(views.panels.files.unwrap().0.y, 2);
session.execute("diff");
terminal
@@ -7279,22 +7496,30 @@ mod tests {
}
#[test]
fn native_diff_dock_stays_below_the_full_width_header() {
let mut editor = readonly_diff_editor(deletion_file());
let mut views = BufferViewports::default();
fn native_diff_dock_and_panes_fill_the_workspace_below_only_configured_chrome() {
for (width, height) in [(120, 30), (32, 9)] {
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let dock = views.panels.files.unwrap().0;
let before = views.panels.diff.as_ref().unwrap().regions[1].0;
assert_eq!(dock.y, before.y);
assert_eq!(dock.y, 1);
assert_eq!(dock.right(), width);
for x in 0..width {
assert_eq!(terminal.backend().buffer()[(x, 0)].bg, Color::Cyan);
for chrome in [false, true] {
let mut editor = readonly_diff_editor(deletion_file());
if chrome {
editor.set_label("battery".into(), "live".into()).unwrap();
}
let mut views = BufferViewports::default();
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let dock = views.panels.files.unwrap().0;
let workspace = views.rects[0].1;
let regions = views.panels.diff.as_ref().unwrap().regions;
assert_eq!(dock.y, workspace.y);
assert_eq!(dock.right(), width);
assert_eq!(dock.bottom(), workspace.bottom());
for (pane, _) in regions.into_iter().skip(1) {
assert_eq!(pane.y, workspace.y);
assert_eq!(pane.bottom(), workspace.bottom());
assert_eq!(pane.y, u16::from(chrome));
}
}
}
}