feat: add floating and docked searchable file picker

This commit is contained in:
Valdemar Lorenzen
2026-10-08 14:25:54 +02:00
parent 5a67bf64a3
commit c60e625555
12 changed files with 1820 additions and 34 deletions
+671 -7
View File
@@ -18,7 +18,7 @@ use crossterm::{
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Constraint, Layout},
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::Line,
widgets::{Block, Borders, Clear, Paragraph},
@@ -28,6 +28,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
editor::{Editor, Input, Mode, Palette},
files::{DockSide, FilePicker, FilePresentation},
session::EditorSession,
};
@@ -219,12 +220,21 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
return;
}
let prompting = matches!(editor.mode(), Mode::Command | Mode::Search);
let [viewport, status_area, prompt_area] = Layout::vertical([
let dock_focused = editor
.file_picker()
.is_some_and(|picker| picker.focused && picker.presentation == FilePresentation::Docked);
let [workspace, status_area, prompt_area] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(u16::from(area.height >= 3)),
Constraint::Length(u16::from(area.height >= 2 || prompting)),
Constraint::Length(u16::from(area.height >= 2 || (prompting && !dock_focused))),
])
.areas(area);
let (viewport, dock) = match editor.file_picker() {
Some(picker) if picker.presentation == FilePresentation::Docked => {
dock_layout(workspace, editor.file_explorer_side(), picker.focused)
}
_ => (workspace, None),
};
let digits = editor.lines().len().ilog10() as usize + 1;
let gutter_width = (digits + 1) as u16;
let [gutter, viewport] = Layout::horizontal([
@@ -239,7 +249,7 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
let height = usize::from(viewport.height);
let width = usize::from(viewport.width);
let cursor = editor.cursor();
if height > 0 {
if height > 0 && width > 0 {
if cursor.row < *top {
*top = cursor.row;
} else if cursor.row >= top.saturating_add(height) {
@@ -372,7 +382,7 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
Paragraph::new(status).style(Style::default().fg(Color::Black).bg(Color::Cyan)),
status_area,
);
if prompting && prompt_area.height > 0 {
if prompting && prompt_area.height > 0 && prompt_area.width > 0 {
let prefix = if editor.mode() == Mode::Search {
'/'
} else {
@@ -381,7 +391,7 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
let command = format!("{prefix}{}", editor.command_line());
let command_width = display_column(&command, usize::MAX);
let prompt_width = usize::from(prompt_area.width);
let offset = command_width.saturating_sub(prompt_width - 1);
let offset = command_width.saturating_sub(prompt_width.saturating_sub(1));
frame.render_widget(
Paragraph::new(visible_line(&command, offset, prompt_width)),
prompt_area,
@@ -392,7 +402,7 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
));
} else {
frame.render_widget(Paragraph::new(editor.message()), prompt_area);
if height > 0 {
if height > 0 && width > 0 {
let column = display_column(&editor.lines()[cursor.row], cursor.column);
frame.set_cursor_position((
viewport.x + (column - *left) as u16,
@@ -400,11 +410,191 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
));
}
}
if let Some(picker) = editor.file_picker() {
if picker.presentation == FilePresentation::Floating {
let [_, column, _] = Layout::horizontal([
Constraint::Fill(1),
Constraint::Length(area.width.min(84)),
Constraint::Fill(1),
])
.areas(area);
let [_, panel, _] = Layout::vertical([
Constraint::Fill(1),
Constraint::Length(area.height.min(18)),
Constraint::Fill(1),
])
.areas(column);
frame.render_widget(Clear, panel);
draw_file_list(frame, picker, panel);
} else if let Some(panel) = dock {
draw_file_list(frame, picker, panel);
}
}
if let Some(palette) = editor.palette() {
draw_palette(frame, palette, editor.mode());
}
}
fn dock_layout(area: Rect, side: DockSide, focused: bool) -> (Rect, Option<Rect>) {
if area.width == 0 || area.height == 0 {
return (area, None);
}
let horizontal = matches!(side, DockSide::Left | DockSide::Right);
let available = if horizontal { area.width } else { area.height };
if !focused && available < if horizontal { 24 } else { 8 } {
return (area, None);
}
let extent = if available < if horizontal { 20 } else { 6 } {
available
} else if horizontal {
(available / 3).clamp(12, 38).min(available - 8)
} else {
(available / 3).clamp(4, 12).min(available - 2)
};
let leading = matches!(side, DockSide::Left | DockSide::Top);
let constraints = if leading {
[Constraint::Length(extent), Constraint::Fill(1)]
} else {
[Constraint::Fill(1), Constraint::Length(extent)]
};
let [first, second] = if horizontal {
Layout::horizontal(constraints).areas(area)
} else {
Layout::vertical(constraints).areas(area)
};
if leading {
(second, Some(first))
} else {
(first, Some(second))
}
}
fn display_text(text: &str) -> String {
text.chars()
.map(|ch| if ch.is_control() { '�' } else { ch })
.collect()
}
fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect) {
if panel.width == 0 || panel.height == 0 {
return;
}
let color = if picker.focused {
Color::Cyan
} else {
Color::DarkGray
};
let bordered = panel.width >= 4 && panel.height >= 4;
let title = match picker.presentation {
FilePresentation::Floating => " Files ",
FilePresentation::Docked => " Explorer ",
};
let block = Block::default()
.borders(if bordered {
Borders::ALL
} else {
Borders::NONE
})
.border_style(Style::default().fg(color))
.title(title);
let content = if bordered { block.inner(panel) } else { panel };
if bordered {
frame.render_widget(block, panel);
}
let show_root = content.height >= 4;
let show_error = picker.error.is_some() && content.height >= 2;
let show_help = content.height >= 5 + u16::from(show_error);
let [query_area, root_area, error_area, results_area, help_area] = Layout::vertical([
Constraint::Length(1),
Constraint::Length(u16::from(show_root)),
Constraint::Length(u16::from(show_error)),
Constraint::Fill(1),
Constraint::Length(u16::from(show_help)),
])
.areas(content);
let query = format!("> {}", display_text(&picker.query));
let query_width = display_column(&query, usize::MAX);
let width = usize::from(query_area.width);
let offset = query_width.saturating_sub(width.saturating_sub(1));
frame.render_widget(
Paragraph::new(visible_line(&query, offset, width)).style(Style::default().fg(
if picker.focused {
Color::Yellow
} else {
Color::DarkGray
},
)),
query_area,
);
if picker.focused && query_area.width > 0 && query_area.height > 0 {
frame.set_cursor_position((query_area.x + (query_width - offset) as u16, query_area.y));
}
if show_root {
frame.render_widget(
Paragraph::new(display_text(&picker.root.to_string_lossy()))
.style(Style::default().fg(Color::DarkGray)),
root_area,
);
}
if show_error {
frame.render_widget(
Paragraph::new(display_text(picker.error.as_deref().unwrap()))
.style(Style::default().fg(Color::Red)),
error_area,
);
}
let entries = picker.entries();
if entries.len() == 0 {
frame.render_widget(
Paragraph::new(if picker.query.is_empty() {
"No project files"
} else {
"No matching files"
}),
results_area,
);
} else if results_area.height > 0 {
let count = usize::from(results_area.height);
let first = picker.selected.saturating_sub(count - 1);
let lines: Vec<Line> = entries
.enumerate()
.skip(first)
.take(count)
.map(|(index, entry)| {
let selected = index == picker.selected;
let style = if selected {
Style::default()
.bg(if picker.focused {
Color::Blue
} else {
Color::DarkGray
})
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
Line::styled(
format!("{} {}", if selected { ">" } else { " " }, entry.label),
style,
)
})
.collect();
frame.render_widget(Paragraph::new(lines), results_area);
}
let help = if picker.focused {
match picker.presentation {
FilePresentation::Floating => "↑↓ Enter · Esc/^W close · ^R scan",
FilePresentation::Docked => "↑↓ Enter · Esc/Tab edit · ^W · ^R",
}
} else {
"Editor focus · invoke explorer to search"
};
frame.render_widget(
Paragraph::new(help).style(Style::default().fg(Color::DarkGray)),
help_area,
);
}
fn draw_palette(frame: &mut Frame, palette: &Palette, mode: Mode) {
let area = frame.area();
if area.width == 0 || area.height == 0 {
@@ -548,6 +738,480 @@ mod tests {
.join("\n")
}
#[test]
fn file_list_scrolls_selection_and_sanitizes_display_without_losing_paths() {
let directory = Directory::new();
for index in 0..18 {
directory.file(&format!("file-{index:02}"), "text");
}
let special = directory.file("界e\u{301} space\nname", "text");
let mut picker = FilePicker::scan(directory.0.clone(), FilePresentation::Floating).unwrap();
assert!(
picker
.entries()
.any(|entry| directory.0.join(&entry.path) == special)
);
let count = picker.entries().len();
picker.selected = count - 1;
let label = picker.entries().last().unwrap().label.clone();
assert_eq!(label, "界e\u{301} space�name");
picker.query = "long\t界e\u{301}z".into();
let area = Rect::new(3, 2, 24, 9);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(40, 15),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
terminal
.draw(|frame| draw_file_list(frame, &picker, area))
.unwrap();
let text = screen_text(&terminal);
assert!(!text.contains("file-00"), "{text}");
let cursor = terminal.get_cursor_position().unwrap();
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(area.x + 7, area.y + 1)].symbol(), "�");
assert_eq!(buffer[(area.x + 8, area.y + 1)].symbol(), "界");
assert_eq!(buffer[(area.x + 10, area.y + 1)].symbol(), "e\u{301}");
let selected_y = (area.y + 2..area.bottom() - 1)
.find(|&y| buffer[(area.x + 1, y)].symbol() == ">")
.unwrap();
assert_eq!(buffer[(area.x + 3, selected_y)].symbol(), "界");
assert_eq!(buffer[(area.x + 5, selected_y)].symbol(), "e\u{301}");
assert_eq!(buffer[(area.x + 1, selected_y)].bg, Color::Blue);
assert!(
buffer[(area.x + 1, selected_y)]
.modifier
.contains(Modifier::BOLD)
);
assert_eq!(cursor.y, area.y + 1);
assert_eq!(buffer[(0, 0)].symbol(), " ");
}
#[test]
fn file_list_renders_root_empty_filter_errors_and_idle_focus() {
let directory = Directory::new();
let mut picker = FilePicker::scan(directory.0.clone(), FilePresentation::Docked).unwrap();
let area = Rect::new(0, 0, 100, 12);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
terminal
.draw(|frame| draw_file_list(frame, &picker, area))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("Explorer"), "{text}");
assert!(
text.contains(&directory.0.to_string_lossy().to_string()),
"{text}"
);
assert!(text.contains("No project files"), "{text}");
assert!(text.contains("Esc/Tab"), "{text}");
picker.query = "missing".into();
picker.error = Some("Unreadable\npath\u{1b}".into());
picker.focused = false;
terminal
.draw(|frame| {
frame.set_cursor_position((99, 11));
draw_file_list(frame, &picker, area);
})
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("No matching files"), "{text}");
assert!(text.contains("Unreadable�path�"), "{text}");
assert!(text.contains("Editor focus"), "{text}");
assert_eq!(terminal.backend().buffer()[(1, 3)].fg, Color::Red);
assert_eq!(terminal.backend().buffer()[(0, 0)].fg, Color::DarkGray);
assert_eq!(
terminal.get_cursor_position().unwrap(),
ratatui::layout::Position::new(99, 11)
);
picker.presentation = FilePresentation::Floating;
picker.focused = true;
terminal
.draw(|frame| draw_file_list(frame, &picker, area))
.unwrap();
assert!(screen_text(&terminal).contains("Files"));
assert!(screen_text(&terminal).contains("Esc/^W"));
assert_eq!(terminal.backend().buffer()[(0, 0)].fg, Color::Cyan);
}
#[test]
fn file_query_cursor_and_dock_layout_survive_tiny_offset_resizes() {
let directory = Directory::new();
let mut picker = FilePicker::scan(directory.0.clone(), FilePresentation::Docked).unwrap();
picker.query = "long界e\u{301}z".into();
for (width, height) in [(12, 6), (8, 2), (1, 1), (1, 2), (8, 1), (12, 6)] {
let area = Rect::new(3, 2, width, height);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(24, 12),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
terminal
.draw(|frame| draw_file_list(frame, &picker, area))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert!(area.contains(cursor), "{area:?}: {cursor:?}");
let bordered = width >= 4 && height >= 4;
let query_x = area.x + u16::from(bordered);
let query_y = area.y + u16::from(bordered);
let query_width = width - 2 * u16::from(bordered);
assert_eq!(cursor.x, query_x + query_width - 1);
assert_eq!(cursor.y, query_y);
let query = format!("> {}", picker.query);
let offset =
display_column(&query, usize::MAX).saturating_sub(usize::from(query_width) - 1);
let mut expected =
ratatui::buffer::Buffer::empty(Rect::new(query_x, query_y, query_width, 1));
use ratatui::widgets::Widget;
Paragraph::new(visible_line(&query, offset, usize::from(query_width)))
.render(expected.area, &mut expected);
for x in query_x..query_x + query_width {
assert_eq!(
terminal.backend().buffer()[(x, query_y)].symbol(),
expected[(x, query_y)].symbol()
);
}
assert_eq!(terminal.backend().buffer()[(0, 0)].symbol(), " ");
for side in [
DockSide::Left,
DockSide::Right,
DockSide::Top,
DockSide::Bottom,
] {
for focused in [false, true] {
let (editor, panel) = dock_layout(area, side, focused);
for rect in [Some(editor), panel].into_iter().flatten() {
assert!(rect.x >= area.x && rect.y >= area.y);
assert!(rect.right() <= area.right() && rect.bottom() <= area.bottom());
}
if focused {
assert!(panel.is_some());
} else {
assert!(panel.is_none());
}
}
}
}
for (width, height) in [(0, 1), (1, 0), (0, 0)] {
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| draw_file_list(frame, &picker, frame.area()))
.unwrap();
assert!(
dock_layout(Rect::new(3, 2, width, height), DockSide::Right, true)
.1
.is_none()
);
}
}
#[test]
fn all_dock_sides_clip_editor_gutter_selection_carets_and_cursor() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace(
vec!["word ".repeat(40); 20].join("\n"),
)])
.unwrap();
editor.handle(Input::Char('v'));
editor.handle(Input::Ctrl('a'));
assert!(editor.selection_count() > 1);
let area = Rect::new(3, 2, 90, 26);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(100, 32),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
let (mut top, mut left) = (0, 0);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
assert!(editor.file_picker().unwrap().focused);
for (name, side) in [
("left", DockSide::Left),
("right", DockSide::Right),
("top", DockSide::Top),
("bottom", DockSide::Bottom),
] {
editor.set_file_explorer_side(name).unwrap();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let workspace = Rect::new(area.x, area.y, area.width, area.height - 2);
let (editor_area, panel) = dock_layout(workspace, side, true);
let panel = panel.unwrap();
assert_eq!(editor_area.area() + panel.area(), workspace.area());
assert_eq!(editor_area.intersection(panel).area(), 0);
match side {
DockSide::Left => assert_eq!(panel.right(), editor_area.x),
DockSide::Right => assert_eq!(editor_area.right(), panel.x),
DockSide::Top => assert_eq!(panel.bottom(), editor_area.y),
DockSide::Bottom => assert_eq!(editor_area.bottom(), panel.y),
}
assert!(panel.contains(terminal.get_cursor_position().unwrap()));
editor.handle(Input::Escape);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
assert!(editor_area.contains(terminal.get_cursor_position().unwrap()));
let buffer = terminal.backend().buffer();
for y in panel.y..panel.bottom() {
for x in panel.x..panel.right() {
assert!(
!matches!(
buffer[(x, y)].bg,
Color::Blue | Color::Magenta | Color::Yellow
),
"{side:?}: {x},{y}"
);
}
}
assert_eq!(buffer[(editor_area.x + 3, editor_area.y)].symbol(), "w");
assert!(matches!(
buffer[(editor_area.x + 3, editor_area.y)].bg,
Color::Blue | Color::Magenta
));
for y in editor_area.y..editor_area.bottom() {
for x in editor_area.x..editor_area.x + 3 {
assert_eq!(buffer[(x, y)].bg, Color::Reset);
}
}
for x in area.x..area.right() {
assert_eq!(buffer[(x, area.bottom() - 2)].bg, Color::Cyan);
}
assert_eq!(buffer[(0, 0)].symbol(), " ");
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
assert!(editor.file_picker().unwrap().focused);
}
}
#[test]
fn floating_picker_clears_a_centered_overlay_without_moving_editor_regions() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace(
vec!["x".repeat(120); 30].join("\n"),
)])
.unwrap();
let area = Rect::new(3, 2, 110, 26);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(120, 32),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
let (mut top, mut left) = (0, 0);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let before = (top, left);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let panel = Rect::new(area.x + 13, area.y + 4, 84, 18);
assert_eq!((top, left), before);
assert_eq!(
terminal.get_cursor_position().unwrap(),
ratatui::layout::Position::new(panel.x + 3, panel.y + 1)
);
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(panel.x, panel.y)].fg, Color::Cyan);
assert_eq!(buffer[(panel.right() - 2, panel.y + 1)].symbol(), " ");
assert_eq!(buffer[(area.x + 3, area.y)].symbol(), "x");
assert_eq!(buffer[(area.x, area.bottom() - 2)].bg, Color::Cyan);
assert_eq!(buffer[(0, 0)].symbol(), " ");
}
#[test]
fn picker_conversion_focus_palette_and_closure_preserve_buffer_viewport_cache() {
let directory = Directory::new();
let a = directory.file("a", &vec!["a".repeat(80); 30].join("\n"));
let b = directory.file("b", "other");
let mut editor = Editor::open(Some(a), vec![]).unwrap();
let a_id = editor.buffer_id();
for _ in 0..20 {
editor.handle(Input::Down);
}
editor.handle(Input::End);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(50, 10)).unwrap();
let mut viewports = BufferViewports::default();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let before = viewports.offsets[&a_id];
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(viewports.offsets[&a_id], before);
assert!(screen_text(&terminal).contains("Files"));
editor.handle(Input::Escape);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(viewports.offsets[&a_id], before);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert!(screen_text(&terminal).contains("Explorer"));
editor.handle(Input::Escape);
let docked = editor.file_picker().unwrap();
assert!(!docked.focused);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
let dock_offsets = viewports.offsets[&a_id];
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert!(screen_text(&terminal).contains("Commands"));
assert_eq!(terminal.get_cursor_position().unwrap().y, 1);
assert_eq!(viewports.offsets[&a_id], dock_offsets);
editor.handle(Input::Escape);
editor.execute(&format!("edit {}", b.display()));
let b_id = editor.buffer_id();
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert!(editor.file_picker().is_some());
assert_eq!(viewports.offsets[&b_id], (0, 0));
editor.execute(&format!("buffer {a_id}"));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert_eq!(viewports.offsets[&a_id], dock_offsets);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('o'));
assert_eq!(
editor.file_picker().unwrap().presentation,
FilePresentation::Floating
);
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert!(screen_text(&terminal).contains("Files"));
editor.handle(Input::Ctrl('w'));
terminal
.draw(|frame| viewports.draw(frame, &editor))
.unwrap();
assert!(editor.file_picker().is_none());
assert_eq!(viewports.offsets[&b_id], (0, 0));
assert!(screen_text(&terminal).contains("a"));
}
#[test]
fn docked_search_highlight_and_cursor_survive_focus_and_offset_resizes() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor
.apply(vec![crate::editor::Effect::Replace(
"0123456789界e\u{301}z\nsecond".into(),
)])
.unwrap();
editor.handle(Input::Char('/'));
editor.handle(Input::Char('界'));
editor.handle(Input::Enter);
assert!(editor.search_match().is_some());
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(100, 32),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(Rect::new(3, 2, 90, 26)),
},
)
.unwrap();
let (mut top, mut left) = (0, 0);
for (name, side) in [
("left", DockSide::Left),
("right", DockSide::Right),
("top", DockSide::Top),
("bottom", DockSide::Bottom),
] {
editor.set_file_explorer_side(name).unwrap();
for (width, height) in [
(90, 26),
(12, 6),
(8, 2),
(1, 1),
(1, 2),
(0, 1),
(1, 0),
(90, 26),
] {
let area = Rect::new(3, 2, width, height);
terminal.resize(area).unwrap();
let area = terminal.get_frame().area();
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
if width > 0 && height > 0 {
let workspace = Rect::new(
area.x,
area.y,
width,
height - u16::from(height >= 3) - u16::from(height >= 2),
);
let (_, panel) = dock_layout(workspace, side, true);
let cursor = terminal.get_cursor_position().unwrap();
assert!(
panel.unwrap().contains(cursor),
"{side:?} {area:?} {panel:?}: {cursor:?}"
);
}
editor.handle(Input::Escape);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
if width > 0 && height > 0 {
let cursor = terminal.get_cursor_position().unwrap();
assert!(area.contains(cursor), "{side:?} {area:?}: {cursor:?}");
let workspace = Rect::new(
area.x,
area.y,
width,
height - u16::from(height >= 3) - u16::from(height >= 2),
);
let (editor_area, panel) = dock_layout(workspace, side, false);
assert!(editor_area.contains(cursor), "{side:?}: {cursor:?}");
let buffer = terminal.backend().buffer();
if width == 90 {
let mut yellow = 0;
for y in editor_area.y..editor_area.bottom() {
for x in editor_area.x..editor_area.right() {
yellow += usize::from(buffer[(x, y)].bg == Color::Yellow);
}
}
assert!(yellow > 0);
}
if let Some(panel) = panel {
for y in panel.y..panel.bottom() {
for x in panel.x..panel.right() {
assert_ne!(buffer[(x, y)].bg, Color::Yellow);
}
}
}
assert_eq!(buffer[(0, 0)].symbol(), " ");
}
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
assert!(editor.file_picker().unwrap().focused);
}
}
}
#[test]
fn pending_leader_is_visible_before_long_paths_and_unknown_keys_do_not_edit() {
let directory = Directory::new();