feat: configure interchangeable top and bottom bars

This commit is contained in:
Valdemar Lorenzen
2026-10-12 11:47:31 +02:00
parent 4cfd068f1c
commit 0d3fbc01f8
12 changed files with 776 additions and 340 deletions
+478 -135
View File
@@ -406,15 +406,9 @@ impl BufferViewports {
self.parked
.retain(|id, _| editor.pane_buffer_id(*id).is_some());
self.syntax.retain(|id, _| editor.has_buffer(*id));
let area = frame.area();
let area = draw_tab_bar(frame, editor, area);
let syntax = self.syntax.entry(editor.buffer_id()).or_default();
let [full_workspace, prompt] = draw_bars(frame, editor, frame.area(), syntax);
let multiple = editor.pane_layout().len() > 1;
let chrome = if !multiple && editor.diff().is_some() {
None
} else {
Some(document_layout(area, editor))
};
let full_workspace = chrome.map_or(area, |areas| areas[0]);
let diff_header = editor
.diff()
.filter(|diff| !multiple && diff.phase == Phase::View)
@@ -452,6 +446,10 @@ impl BufferViewports {
.visit_rects(pane_rect(workspace), |id, area| {
self.rects.push((id, terminal_rect(area)));
});
// An active prompt still needs its focused render pass when no document row fits.
if self.rects.is_empty() && prompt.height > 0 {
self.rects.push((focused, workspace));
}
// Paint focused overlays last, so an adjacent leaf cannot erase them.
for active_pass in [false, true] {
for index in 0..self.rects.len() {
@@ -474,12 +472,11 @@ impl BufferViewports {
frame.render_widget(block, leaf);
leaf = inner;
}
let status = chrome
.filter(|_| id == focused)
.map_or(Rect::default(), |a| a[1]);
let prompt = chrome
.filter(|_| id == focused)
.map_or(Rect::default(), |a| a[2]);
let prompt = if id == focused {
prompt
} else {
Rect::default()
};
editor.with_render_pane(id, |view| {
self.activate_document(view.buffer_id());
self.documents.retain(|id, _| view.has_buffer(*id));
@@ -492,8 +489,8 @@ impl BufferViewports {
draw_document(
frame,
view,
[leaf, status, prompt],
area,
[leaf, prompt],
full_workspace,
top,
left,
syntax,
@@ -544,14 +541,20 @@ impl BufferViewports {
draw_diff(
frame,
diff,
area,
full_workspace,
editor.selection_navigation_hint(),
editor.file_navigation_hint(),
&mut self.panels,
Some(editor),
);
} else {
draw_overlays(frame, editor, area, self.panels.caret, &mut self.panels);
draw_overlays(
frame,
editor,
full_workspace,
self.panels.caret,
&mut self.panels,
);
}
self.diff_visible = self
.panels
@@ -1071,7 +1074,7 @@ fn chrome_items<'a>(
editor: &'a Editor,
line: &'a ChromeLine,
names: &[String],
syntax: Option<&SyntaxCache>,
syntax: &SyntaxCache,
) -> Vec<ChromeItem<'a>> {
let mut items = Vec::new();
for name in names {
@@ -1100,31 +1103,28 @@ fn chrome_items<'a>(
continue;
}
let mut text = match name.as_str() {
"mode" if syntax.is_some() => Cow::Owned(if editor.visual_linewise() {
"mode" => Cow::Owned(if editor.visual_linewise() {
"VISUAL LINE".into()
} else {
editor.mode().to_string()
}),
"path" if syntax.is_some() => Cow::Owned(chrome_title(
"path" => Cow::Owned(chrome_title(
"{path}{modified}",
editor.tab_index() + 1,
editor.path(),
editor.dirty(),
)),
"position" if syntax.is_some() => Cow::Owned(format!(
"position" => Cow::Owned(format!(
"{}:{}",
editor.cursor().row + 1,
editor.cursor().column + 1
)),
"language" if syntax.is_some() => {
let syntax = syntax.unwrap();
Cow::Owned(if let Some(error) = syntax.error() {
format!("syntax: {error}")
} else {
syntax.language().unwrap_or_default().to_owned()
})
}
"diagnostics" if syntax.is_some() => {
"language" => Cow::Owned(if let Some(error) = syntax.error() {
format!("syntax: {error}")
} else {
syntax.language().unwrap_or_default().to_owned()
}),
"diagnostics" => {
let mut counts = [0; 4];
for diagnostic in editor.diagnostics() {
counts[diagnostic_severity(diagnostic.severity).0] += 1;
@@ -1140,18 +1140,18 @@ fn chrome_items<'a>(
}
Cow::Owned(text)
}
"selections" if syntax.is_some() => Cow::Owned(if editor.selection_count() > 1 {
"selections" => Cow::Owned(if editor.selection_count() > 1 {
format!("{} selections", editor.selection_count())
} else {
String::new()
}),
"count" if syntax.is_some() => Cow::Owned(
"count" => Cow::Owned(
editor
.count()
.map(|count| format!("count: {count}"))
.unwrap_or_default(),
),
"pending" if syntax.is_some() => {
"pending" => {
let mut text = editor.sequence_pending().unwrap_or_default().to_owned();
for pending in [
editor.g_pending().then_some('g'),
@@ -1168,7 +1168,7 @@ fn chrome_items<'a>(
}
Cow::Owned(text)
}
"buffer" if syntax.is_some() => Cow::Owned(if editor.buffer_count() > 1 {
"buffer" => Cow::Owned(if editor.buffer_count() > 1 {
format!("[buffer {}/{}]", editor.buffer_id(), editor.buffer_count())
} else {
String::new()
@@ -1348,47 +1348,69 @@ fn draw_chrome(frame: &mut Frame, mut groups: [Vec<ChromeItem<'_>>; 3], area: Re
}
}
fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
if area.height == 0 {
return area;
}
let line = editor.tabline_config();
let anchors = [&line.left, &line.center, &line.right];
if !anchors.iter().flat_map(|names| names.iter()).any(|name| {
if name == "tabs" {
editor.tab_count() > 1
} else {
!editor.label(name).is_empty()
}
}) {
return area;
}
let groups = anchors.map(|names| chrome_items(editor, line, names, None));
if !groups.iter().flatten().any(|item| item.tab.is_none())
&& !(editor.tab_count() > 1 && groups.iter().any(|group| !group.is_empty()))
{
return area;
}
draw_chrome(frame, groups, Rect::new(area.x, area.y, area.width, 1));
Rect::new(
area.x,
area.y.saturating_add(1),
area.width,
area.height - 1,
)
fn chrome_groups<'a>(
editor: &'a Editor,
line: &'a ChromeLine,
syntax: &SyntaxCache,
) -> [Vec<ChromeItem<'a>>; 3] {
[&line.left, &line.center, &line.right].map(|names| chrome_items(editor, line, names, syntax))
}
fn document_layout(area: Rect, editor: &Editor) -> [Rect; 3] {
fn chrome_visible(editor: &Editor, groups: &[Vec<ChromeItem<'_>>; 3]) -> bool {
groups.iter().flatten().any(|item| item.tab.is_none())
|| (editor.tab_count() > 1 && groups.iter().any(|group| !group.is_empty()))
}
fn document_layout(area: Rect, editor: &Editor, visible: [bool; 2]) -> [Rect; 4] {
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
let dock_focused = editor
.file_picker()
.is_some_and(|picker| picker.focused && picker.presentation == FilePresentation::Docked);
Layout::vertical([
Constraint::Fill(1),
Constraint::Length(u16::from(area.height >= 3)),
Constraint::Length(u16::from(area.height >= 2 || (prompting && !dock_focused))),
])
.areas(area)
let prompt =
u16::from(editor.diff().is_none() && (area.height >= 2 || (prompting && !dock_focused)))
.min(area.height);
// Both bars yield to the separate prompt and the last document row.
let available = area.height.saturating_sub(prompt + 1);
let top = u16::from(visible[0]).min(available);
let bottom = u16::from(visible[1]).min(available - top);
let workspace = Rect::new(
area.x,
area.y.saturating_add(top),
area.width,
area.height - top - bottom - prompt,
);
[
Rect::new(area.x, area.y, area.width, top),
workspace,
Rect::new(area.x, workspace.bottom(), area.width, bottom),
Rect::new(area.x, area.bottom() - prompt, area.width, prompt),
]
}
fn draw_bars(
frame: &mut Frame,
editor: &Editor,
area: Rect,
syntax: &mut SyntaxCache,
) -> [Rect; 2] {
if area.width == 0 || area.height == 0 {
let [_, workspace, _, prompt] = document_layout(area, editor, [false; 2]);
return [workspace, prompt];
}
// Resolve the active buffer's language before computing either bar's visibility.
syntax.update_visible(editor.lines(), editor.path(), editor.text_revision(), &[]);
let groups = [
chrome_groups(editor, editor.top_bar_config(), syntax),
chrome_groups(editor, editor.bottom_bar_config(), syntax),
];
let visible = groups
.each_ref()
.map(|groups| chrome_visible(editor, groups));
let [top, workspace, bottom, prompt] = document_layout(area, editor, visible);
for (groups, area) in groups.into_iter().zip([top, bottom]) {
draw_chrome(frame, groups, area);
}
[workspace, prompt]
}
#[cfg(test)]
@@ -1402,14 +1424,9 @@ fn draw(
panels: &mut WheelPanels,
) {
let area = frame.area();
let area = draw_tab_bar(frame, editor, area);
let areas = if editor.diff().is_some() {
[area, Rect::default(), Rect::default()]
} else {
document_layout(area, editor)
};
let areas = draw_bars(frame, editor, area, syntax);
draw_document(
frame, editor, areas, area, top, left, syntax, document, panels,
frame, editor, areas, areas[0], top, left, syntax, document, panels,
);
}
@@ -1417,7 +1434,7 @@ fn draw(
fn draw_document(
frame: &mut Frame,
editor: &Editor,
[workspace, status_area, prompt_area]: [Rect; 3],
[workspace, prompt_area]: [Rect; 2],
overlay_area: Rect,
top: &mut usize,
left: &mut usize,
@@ -1430,7 +1447,6 @@ fn draw_document(
let area = workspace;
let focused = editor.render_pane_focused();
if let Some(diff) = editor.diff() {
draw_status(frame, editor, syntax, status_area);
if panels.workspace_managed && diff.phase != Phase::View {
return;
}
@@ -1446,7 +1462,7 @@ fn draw_document(
return;
}
panels.diff_syntax = None;
if overlay_area.width == 0 || overlay_area.height == 0 {
if overlay_area.width == 0 || (overlay_area.height == 0 && prompt_area.height == 0) {
return;
}
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
@@ -1759,7 +1775,6 @@ fn draw_document(
);
}
}
draw_status(frame, editor, syntax, status_area);
if prompting && prompt_area.height > 0 && prompt_area.width > 0 {
let prefix = if editor.mode() == Mode::Search {
'/'
@@ -1895,13 +1910,6 @@ fn draw_overlays(
}
}
fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_area: Rect) {
let line = editor.statusline_config();
let groups = [&line.left, &line.center, &line.right]
.map(|names| chrome_items(editor, line, names, Some(syntax)));
draw_chrome(frame, groups, status_area);
}
struct DiffGeometry {
regions: [(Rect, Focus); 3],
viewport: Option<(u64, usize, usize, usize, usize)>,
@@ -5447,8 +5455,8 @@ mod tests {
editor: &mut Editor,
update: impl FnOnce(&mut ChromeLine, &mut ChromeLine),
) {
let mut top = editor.tabline_config().clone();
let mut bottom = editor.statusline_config().clone();
let mut top = editor.top_bar_config().clone();
let mut bottom = editor.bottom_bar_config().clone();
update(&mut top, &mut bottom);
editor
.set_chrome_config(top, bottom, BTreeMap::new())
@@ -5490,7 +5498,11 @@ mod tests {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(160, 3)).unwrap();
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
frame.area(),
);
})
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
@@ -5508,7 +5520,11 @@ mod tests {
});
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
frame.area(),
);
})
.unwrap();
let titles = pane_screen(&terminal, Rect::new(0, 0, 160, 1));
@@ -5554,8 +5570,16 @@ mod tests {
let syntax = SyntaxCache::new();
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
draw_status(frame, &editor, &syntax, Rect::new(3, 2, 45, 1));
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
frame.area(),
);
draw_chrome(
frame,
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
Rect::new(3, 2, 45, 1),
);
})
.unwrap();
let buffer = terminal.backend().buffer();
@@ -5570,8 +5594,16 @@ mod tests {
editor.set_label("left".into(), "L".repeat(30)).unwrap();
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
draw_status(frame, &editor, &syntax, Rect::new(3, 2, 45, 1));
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
frame.area(),
);
draw_chrome(
frame,
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
Rect::new(3, 2, 45, 1),
);
})
.unwrap();
for (y, x, width) in [(0, 0, 51), (2, 3, 45)] {
@@ -5585,7 +5617,7 @@ mod tests {
fn active_tabs_keep_their_anchor_and_text_when_optional_groups_collide() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor.execute("tab.new");
configure_chrome(&mut editor, |top, _| {
configure_chrome(&mut editor, |top, bottom| {
top.left = vec!["prefix".into()];
top.center = vec!["tabs".into()];
top.right = vec!["suffix".into()];
@@ -5594,6 +5626,7 @@ mod tests {
tabs.padding = 64;
tabs.leading_edge = "界".repeat(8);
tabs.trailing_edge = "界".repeat(8);
*bottom = top.clone();
});
editor
.set_label("prefix".into(), "LEFT".repeat(32))
@@ -5605,29 +5638,42 @@ mod tests {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 2)).unwrap();
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
for (y, line) in [editor.top_bar_config(), editor.bottom_bar_config()]
.into_iter()
.enumerate()
{
draw_chrome(
frame,
chrome_groups(&editor, line, &SyntaxCache::new()),
Rect::new(0, y as u16, width, 1),
);
}
})
.unwrap();
if width > 0 {
let row = pane_screen(&terminal, Rect::new(0, 0, width, 1));
for y in 0..2 {
if width == 0 {
continue;
}
let row = pane_screen(&terminal, Rect::new(0, y, width, 1));
assert!(
row.trim()
.starts_with(if width == 1 { "2" } else { "2:active" }),
"{row}"
);
assert_eq!(
terminal.backend().buffer()[((width - width.min(8)) / 2, 0)].symbol(),
terminal.backend().buffer()[((width - width.min(8)) / 2, y)].symbol(),
"2"
);
assert!(!row.contains("LEFT") && !row.contains("RIGHT"), "{row}");
assert!(!row.contains('界'), "{row}");
}
}
configure_chrome(&mut editor, |top, _| {
configure_chrome(&mut editor, |top, bottom| {
let tabs = top.segments.get_mut("tabs").unwrap();
tabs.padding = 0;
tabs.leading_edge.clear();
tabs.trailing_edge.clear();
*bottom = top.clone();
});
editor
.set_label("prefix".into(), "LEFT".repeat(32))
@@ -5638,11 +5684,22 @@ mod tests {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 2)).unwrap();
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
for (y, line) in [editor.top_bar_config(), editor.bottom_bar_config()]
.into_iter()
.enumerate()
{
draw_chrome(
frame,
chrome_groups(&editor, line, &SyntaxCache::new()),
Rect::new(0, y as u16, 40, 1),
);
}
})
.unwrap();
let row = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
assert_eq!(&row[16..24], "2:active");
for y in 0..2 {
let row = pane_screen(&terminal, Rect::new(0, y, 40, 1));
assert_eq!(&row[16..24], "2:active");
}
}
#[test]
@@ -5683,8 +5740,16 @@ mod tests {
let syntax = SyntaxCache::new();
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
draw_status(frame, &editor, &syntax, Rect::new(0, 2, 40, 1));
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
frame.area(),
);
draw_chrome(
frame,
chrome_groups(&editor, editor.bottom_bar_config(), &syntax),
Rect::new(0, 2, 40, 1),
);
})
.unwrap();
let buffer = terminal.backend().buffer();
@@ -5726,7 +5791,11 @@ mod tests {
editor.execute("tab.previous");
terminal
.draw(|frame| {
draw_tab_bar(frame, &editor, frame.area());
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
frame.area(),
);
})
.unwrap();
let buffer = terminal.backend().buffer();
@@ -5738,7 +5807,7 @@ mod tests {
}
#[test]
fn empty_configured_chrome_does_not_allocate_a_tab_row() {
fn empty_bar_content_reclaims_both_rows_without_hiding_the_prompt() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor.execute("tab.new");
configure_chrome(&mut editor, |top, bottom| {
@@ -5755,28 +5824,292 @@ mod tests {
first_leading_edge: None,
},
);
bottom.left.clear();
bottom.center.clear();
bottom.right.clear();
*bottom = top.clone();
});
let mut syntax = SyntaxCache::new();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 5)).unwrap();
terminal
.draw(|frame| {
let area = frame.area();
assert_eq!(draw_tab_bar(frame, &editor, area), area);
assert_eq!(document_layout(area, &editor)[1].height, 1);
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
assert_eq!(workspace, Rect::new(0, 0, 40, 4));
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
})
.unwrap();
editor.set_label("empty".into(), "present".into()).unwrap();
terminal
.draw(|frame| {
let area = frame.area();
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
assert_eq!(workspace, Rect::new(0, 1, 40, 2));
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
})
.unwrap();
let title = pane_screen(&terminal, Rect::new(0, 0, 40, 1));
assert!(title.contains("Not visible without a value"), "{title}");
assert!(!title.contains('▓'), "{title}");
for y in [0, 3] {
let title = pane_screen(&terminal, Rect::new(0, y, 40, 1));
assert!(title.contains("Not visible without a value"), "{title}");
assert!(!title.contains('▓'), "{title}");
}
editor.handle(Input::Char(':'));
terminal
.draw(|frame| {
let [workspace, prompt] = draw_bars(frame, &editor, frame.area(), &mut syntax);
assert_eq!(workspace.height, 2);
assert_eq!(prompt, Rect::new(0, 4, 40, 1));
})
.unwrap();
}
#[test]
fn tiny_bars_yield_to_document_and_prompt_identically_in_either_position() {
let directory = Directory::new();
let path = directory.file("source.txt", "DOCUMENT");
for segment in ["tabs", "mode"] {
for at_top in [false, true] {
for prompting in [false, true] {
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
editor.execute("tab.new");
configure_chrome(&mut editor, |top, bottom| {
for line in [&mut *top, &mut *bottom] {
line.left.clear();
line.center.clear();
line.right.clear();
}
let line = if at_top { top } else { bottom };
line.left = vec![segment.into()];
});
if prompting {
editor.handle(Input::Char(':'));
}
for height in 1..=4 {
let area = Rect::new(3, 2, 80, height);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(100, 10),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let bar_text = match segment {
"tabs" => "2 source.txt",
_ if prompting => "COMMAND",
_ => "NORMAL",
};
let visible = height >= 3;
assert_eq!(screen_text(&terminal).contains(bar_text), visible);
let cursor = terminal.get_cursor_position().unwrap();
assert!(
area.contains(cursor),
"{segment} {at_top} prompting={prompting} {area:?}: {cursor:?}"
);
if prompting {
assert_eq!(cursor.y, area.bottom() - 1);
assert_eq!(
terminal.backend().buffer()[(area.x, cursor.y)].symbol(),
":",
);
}
if !prompting || height > 1 {
let viewport = views.document.geometry.unwrap().1;
assert!(viewport.height > 0);
assert!(pane_screen(&terminal, viewport).contains("DOCUMENT"));
}
if visible {
let y = if at_top { area.y } else { area.bottom() - 2 };
assert!(
pane_screen(&terminal, Rect::new(area.x, y, area.width, 1))
.contains(bar_text)
);
}
}
}
}
}
}
#[test]
fn tabs_only_singletons_hide_in_either_bar_and_reappear_with_another_tab() {
let mut editor = Editor::open(None, vec![]).unwrap();
configure_chrome(&mut editor, |top, bottom| *bottom = top.clone());
let mut views = BufferViewports::default();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(views.rects[0].1, Rect::new(0, 0, 80, 11));
assert!(views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, 0),));
editor.execute("tab.new");
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(views.rects[0].1, Rect::new(0, 1, 80, 9));
for y in [0, 10] {
assert!(
!views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, y),)
);
}
let caret = terminal.get_cursor_position().unwrap();
assert!(caret.y >= 1 && caret.y < 10);
for y in [0, 10] {
let bar = pane_screen(&terminal, Rect::new(0, y, 80, 1));
assert!(
bar.contains("1 [No Name]") && bar.contains("2 [No Name]"),
"{bar}"
);
}
editor.execute("tab.close");
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(views.rects[0].1, Rect::new(0, 0, 80, 11));
assert!(!screen_text(&terminal).contains("[No Name]"));
for y in [0, 10] {
assert!(
views.scroll_document(&editor, document_wheel(MouseEventKind::ScrollDown, 2, y),)
);
}
}
#[test]
fn swapped_bars_render_active_metadata_and_language_on_the_first_paint() {
let directory = Directory::new();
let path = directory.file("active.rs", "fn main() {}");
let mut editor = Editor::open(Some(path.clone()), vec![]).unwrap();
editor.execute("tab.new");
editor.handle(Input::Char('i'));
editor.handle(Input::Paste("// changed\n".into()));
editor.handle(Input::Escape);
configure_chrome(&mut editor, std::mem::swap);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(240, 12)).unwrap();
let mut views = BufferViewports::default();
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let top = pane_screen(&terminal, Rect::new(0, 0, 240, 1));
assert!(top.contains("NORMAL"), "{top}");
assert!(top.contains(&path.display().to_string()), "{top}");
assert!(top.contains("[+]") && top.contains("Rust"), "{top}");
let cursor = editor.cursor();
assert!(
top.contains(&format!("{}:{}", cursor.row + 1, cursor.column + 1)),
"{top}"
);
let bottom = pane_screen(&terminal, Rect::new(0, 10, 240, 1));
assert!(
bottom.contains("1 active.rs [+]") && bottom.contains("2 active.rs [+]"),
"{bottom}"
);
assert_eq!(views.document.geometry.unwrap().1.y, 1);
let before = views.rects[0].1;
editor.handle(Input::Char(':'));
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(views.rects[0].1, before);
assert!(pane_screen(&terminal, Rect::new(0, 11, 240, 1)).starts_with(':'));
assert_eq!(terminal.get_cursor_position().unwrap().y, 11);
assert_eq!(pane_screen(&terminal, Rect::new(0, 10, 240, 1)), bottom);
}
#[test]
fn both_bars_bound_palettes_diff_selectors_docks_and_diff_prompts() {
let directory = Directory::new();
let mut editor =
Editor::open(Some(directory.file("source.rs", "fn main() {}")), vec![]).unwrap();
editor.execute("tab.new");
configure_chrome(&mut editor, |top, bottom| {
std::mem::swap(top, bottom);
top.left = vec!["mode".into(), "language".into()];
top.center = vec!["workspace".into()];
top.right = vec!["position".into()];
bottom.right = vec!["battery".into()];
});
editor
.set_label("workspace".into(), "WORKSPACE".into())
.unwrap();
editor.set_label("battery".into(), "BOTTOM".into()).unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
let mut views = BufferViewports::default();
let assert_bars = |terminal: &Terminal<ratatui::backend::TestBackend>,
views: &BufferViewports| {
assert!(pane_screen(terminal, Rect::new(0, 0, 120, 1)).contains("WORKSPACE"));
let bottom_y = if views.panels.diff.is_some() { 29 } else { 28 };
assert!(pane_screen(terminal, Rect::new(0, bottom_y, 120, 1)).contains("BOTTOM"));
for (_, rect) in &views.rects {
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
}
if let Some(geometry) = &views.panels.diff {
for (rect, _) in geometry.regions {
if rect.height > 0 {
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
}
}
}
if let Some((rect, _, _)) = views.panels.files {
assert!(rect.y >= 1 && rect.bottom() <= bottom_y);
}
};
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)).contains("Rust"));
assert_bars(&terminal, &views);
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert!(screen_text(&terminal).contains("Commands"));
assert_bars(&terminal, &views);
editor.handle(Input::Escape);
editor.execute("diff");
let (id, _) = editor.take_diff_request().unwrap();
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert!(screen_text(&terminal).contains("Choose Before"));
assert!(views.document.geometry.is_none());
assert_bars(&terminal, &views);
editor.handle(Input::Enter);
editor.handle(Input::Enter);
let (id, _) = editor.take_diff_request().unwrap();
editor.apply_diff_result(
id,
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
root: directory.0.clone(),
editable_paths: Default::default(),
before: "baseline".into(),
after: "Working checkout".into(),
files: vec![deletion_file()],
})),
);
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert!(views.panels.files.is_some());
assert_bars(&terminal, &views);
editor.handle(Input::Char(':'));
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert!(Rect::new(0, 1, 120, 28).contains(cursor), "{cursor:?}");
assert_bars(&terminal, &views);
editor.handle(Input::Escape);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(
editor.file_picker().unwrap().presentation,
FilePresentation::Floating
);
assert_bars(&terminal, &views);
}
#[test]
@@ -5802,11 +6135,16 @@ mod tests {
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
if width > 0 && height > 0 {
if width > 0 && height >= 3 {
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
assert!(title.trim_start().starts_with('3'), "{title}");
assert!(!title.contains("status"), "{title}");
}
if width > 0 && height == 1 {
let (_, viewport) = views.document.geometry.unwrap();
assert!(viewport.contains(terminal.get_cursor_position().unwrap()));
assert!(pane_screen(&terminal, viewport).contains("ACTIVE"));
}
}
let mut editor = Editor::open(None, vec![]).unwrap();
@@ -5890,7 +6228,11 @@ mod tests {
terminal
.draw(|frame| {
let area = frame.area();
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
area,
);
})
.unwrap();
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
@@ -5908,7 +6250,11 @@ mod tests {
terminal
.draw(|frame| {
let area = frame.area();
draw_tab_bar(frame, &editor, area);
draw_chrome(
frame,
chrome_groups(&editor, editor.top_bar_config(), &SyntaxCache::new()),
area,
);
})
.unwrap();
let title = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
@@ -5979,10 +6325,6 @@ mod tests {
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(
views.panels.files.unwrap().0,
floating_panel(Rect::new(0, 1, 120, 29))
);
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)), title);
}
@@ -6185,10 +6527,10 @@ mod tests {
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(
views.panels.files.unwrap().0,
floating_panel(Rect::new(0, 0, 120, 30))
);
let panel = views.panels.files.unwrap().0;
assert!(panel.x < 60 && panel.right() > 60);
assert!(panel.y >= views.rects[0].1.y);
assert!(panel.bottom() <= views.rects[0].1.bottom());
}
#[test]
@@ -6764,7 +7106,6 @@ mod tests {
editor.handle(Input::Char(ch));
}
render_native_diff(&mut terminal, &mut views, &mut editor);
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
editor.handle(Input::Enter);
let searched = render_native_diff(&mut terminal, &mut views, &mut editor);
assert!(before.contains(searched));
@@ -7071,7 +7412,6 @@ mod tests {
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
assert!(screen_text(&terminal).contains(":diff.close"));
editor.handle(Input::Enter);
assert!(editor.diff().is_none());
@@ -9989,6 +10329,7 @@ end
let before = (top, left);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
let mut panels = WheelPanels::default();
terminal
.draw(|frame| {
draw(
@@ -9998,11 +10339,13 @@ end
&mut left,
&mut syntax,
&mut DocumentViewport::default(),
&mut WheelPanels::default(),
&mut panels,
)
})
.unwrap();
let panel = Rect::new(area.x + 5, area.y + 1, 100, 24);
let panel = panels.files.unwrap().0;
assert_eq!(panel.x - area.x, area.right() - panel.right());
assert!(panel.y >= area.y && panel.bottom() <= area.bottom() - 2);
assert_eq!((top, left), before);
assert_eq!(
terminal.get_cursor_position().unwrap(),