feat: support configurable tabline status label
This commit is contained in:
+191
-4
@@ -1002,7 +1002,7 @@ fn diagnostic_on_row(diagnostic: &Diagnostic, row: usize) -> bool {
|
||||
}
|
||||
|
||||
fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
||||
if editor.tab_count() <= 1 || area.height == 0 {
|
||||
if (editor.tab_count() <= 1 && editor.tabline_label().is_empty()) || area.height == 0 {
|
||||
return area;
|
||||
}
|
||||
let bar = Rect::new(area.x, area.y, area.width, 1);
|
||||
@@ -1044,7 +1044,16 @@ fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
||||
let fade_width = if active == 0 { 3 } else { 6 };
|
||||
let fade = usize::from(bar.width) >= labels[active].width() + fade_width;
|
||||
let mut width = labels[active].width() + if fade { fade_width } else { 0 };
|
||||
while first > 0 && width + labels[first - 1].width() <= usize::from(bar.width) {
|
||||
let status = editor.tabline_label();
|
||||
let status_width = status.width() + 5; // Left fade and one space on each side.
|
||||
let show_status = !status.is_empty() && width + 1 + status_width <= usize::from(bar.width);
|
||||
let tabs_right = bar.right()
|
||||
- if show_status {
|
||||
status_width as u16 + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
while first > 0 && width + labels[first - 1].width() <= usize::from(tabs_right - bar.x) {
|
||||
first -= 1;
|
||||
width += labels[first].width();
|
||||
}
|
||||
@@ -1053,7 +1062,7 @@ fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
||||
for (index, label) in labels.iter().enumerate().skip(first) {
|
||||
let decorated = index == active && fade;
|
||||
let width = (label.width() + if decorated { fade_width } else { 0 })
|
||||
.min(usize::from(bar.right() - x)) as u16;
|
||||
.min(usize::from(tabs_right - x)) as u16;
|
||||
let highlight = Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::Cyan)
|
||||
@@ -1073,10 +1082,28 @@ fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
|
||||
};
|
||||
frame.render_widget(Paragraph::new(line), Rect::new(x, bar.y, width, 1));
|
||||
x += width;
|
||||
if x == bar.right() {
|
||||
if x == tabs_right {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if show_status {
|
||||
let highlight = Style::default().fg(Color::Black).bg(Color::Cyan);
|
||||
let edge = Style::default().fg(Color::Cyan);
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::styled("░▒▓", edge),
|
||||
Span::styled(" ", highlight),
|
||||
Span::styled(status, highlight),
|
||||
Span::styled(" ", highlight),
|
||||
])),
|
||||
Rect::new(
|
||||
bar.right() - status_width as u16,
|
||||
bar.y,
|
||||
status_width as u16,
|
||||
1,
|
||||
),
|
||||
);
|
||||
}
|
||||
workspace
|
||||
}
|
||||
|
||||
@@ -5249,6 +5276,7 @@ mod tests {
|
||||
"edit {}",
|
||||
directory.file("active.txt", "ACTIVE").display()
|
||||
));
|
||||
editor.set_tabline_label("status".into());
|
||||
let mut views = BufferViewports::default();
|
||||
for (width, height) in [(12, 9), (7, 9), (1, 9), (0, 9), (12, 1), (12, 0)] {
|
||||
let mut terminal =
|
||||
@@ -5259,6 +5287,7 @@ mod tests {
|
||||
if width > 0 && height > 0 {
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||
assert!(title.trim_start().starts_with('3'), "{title}");
|
||||
assert!(!title.contains("status"), "{title}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5273,6 +5302,164 @@ mod tests {
|
||||
assert!(titles.contains("2 [No Name]"), "{titles}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singleton_tabline_label_sanitizes_unicode_and_redraws_through_clear() {
|
||||
let directory = Directory::new();
|
||||
let mut editor =
|
||||
Editor::open(Some(directory.file("source.txt", "DOCUMENT")), vec![]).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let original = screen_text(&terminal);
|
||||
assert_eq!(views.document.geometry.unwrap().1.y, 0);
|
||||
|
||||
editor.set_tabline_label("電池 e\u{301}\t90%".into());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
|
||||
assert!(title.contains("1 source.txt"), "{title}");
|
||||
assert!(title.contains("電") && title.contains("池"), "{title}");
|
||||
assert!(title.contains("e\u{301}�90%"), "{title}");
|
||||
let start = 80 - (editor.tabline_label().width() + 5) as u16;
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (offset, glyph) in [(0, "░"), (1, "▒"), (2, "▓"), (3, " ")] {
|
||||
assert_eq!(buffer[(start + offset, 0)].symbol(), glyph);
|
||||
}
|
||||
assert_eq!(buffer[(78, 0)].symbol(), "%");
|
||||
assert_eq!(buffer[(79, 0)].symbol(), " ");
|
||||
assert_eq!(views.document.geometry.unwrap().1.y, 1);
|
||||
assert!(pane_screen(&terminal, views.rects[0].1).contains("DOCUMENT"));
|
||||
assert!(terminal.get_cursor_position().unwrap().y >= 1);
|
||||
|
||||
editor.set_tabline_label("live".into());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(pane_screen(&terminal, Rect::new(0, 0, 80, 1)).ends_with("░▒▓ live "));
|
||||
assert!(!screen_text(&terminal).contains("90%"));
|
||||
editor.set_tabline_label(String::new());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert_eq!(views.document.geometry.unwrap().1.y, 0);
|
||||
assert_eq!(screen_text(&terminal), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabline_label_keeps_a_gap_and_yields_to_the_full_active_title() {
|
||||
let directory = Directory::new();
|
||||
let mut editor = Editor::open(
|
||||
Some(directory.file("a-very-long-inactive-title.txt", "first")),
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
editor.execute("tab.new");
|
||||
editor.execute("tab.new");
|
||||
editor.execute(&format!(
|
||||
"edit {}",
|
||||
directory.file("active.txt", "ACTIVE").display()
|
||||
));
|
||||
editor.set_tabline_label("90%".into());
|
||||
for width in [28, 29, 80] {
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 2)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
assert_eq!(draw_tab_bar(frame, &editor, area).y, 1);
|
||||
})
|
||||
.unwrap();
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, width, 1));
|
||||
assert!(title.contains("3 active.txt"), "{title}");
|
||||
if width == 28 {
|
||||
assert!(!title.contains("90%"), "{title}");
|
||||
} else {
|
||||
assert!(title.ends_with(" ░▒▓ 90% "), "{title}");
|
||||
}
|
||||
}
|
||||
editor.set_tabline_label("界".repeat(1024));
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 2)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
let area = frame.area();
|
||||
draw_tab_bar(frame, &editor, area);
|
||||
})
|
||||
.unwrap();
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, 80, 1));
|
||||
assert!(title.contains("3 active.txt"), "{title}");
|
||||
assert!(!title.contains('界'), "{title}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tabline_label_stays_above_split_overlays_and_diff_geometry() {
|
||||
let directory = Directory::new();
|
||||
let mut editor =
|
||||
Editor::open(Some(directory.file("source.txt", "DOCUMENT")), vec![]).unwrap();
|
||||
editor.execute("pane.split_vertical");
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let original = views.rects.clone();
|
||||
editor.set_tabline_label("live".into());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
for ((id, before), (after_id, after)) in original.iter().zip(&views.rects) {
|
||||
assert_eq!(id, after_id);
|
||||
assert_eq!(
|
||||
*after,
|
||||
Rect::new(before.x, before.y + 1, before.width, before.height - 1)
|
||||
);
|
||||
}
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
|
||||
editor.handle(Input::Ctrl('p'));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("Commands"));
|
||||
assert_eq!(pane_screen(&terminal, Rect::new(0, 0, 120, 1)), title);
|
||||
|
||||
let mut editor = readonly_diff_editor(deletion_file());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let regions = views.panels.diff.as_ref().unwrap().regions;
|
||||
editor.set_tabline_label("live".into());
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
for ((before, focus), (after, after_focus)) in regions
|
||||
.into_iter()
|
||||
.zip(views.panels.diff.as_ref().unwrap().regions)
|
||||
{
|
||||
assert_eq!(focus, after_focus);
|
||||
if before == Rect::default() {
|
||||
assert_eq!(after, before);
|
||||
} else {
|
||||
assert_eq!(
|
||||
after,
|
||||
Rect::new(before.x, before.y + 1, before.width, before.height - 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(views.panels.files.unwrap().0.y, 2);
|
||||
let title = pane_screen(&terminal, Rect::new(0, 0, 120, 1));
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Char('o'));
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_switches_restore_independent_pane_viewports_and_retire_only_closed_panes() {
|
||||
let directory = Directory::new();
|
||||
|
||||
Reference in New Issue
Block a user