feat: default explorer to compact directory tree

This commit is contained in:
Valdemar Lorenzen
2026-10-08 15:51:25 +02:00
parent 9ba8b38898
commit 6d7be0c169
9 changed files with 2036 additions and 145 deletions
+505 -32
View File
@@ -28,7 +28,7 @@ use unicode_width::UnicodeWidthStr;
use crate::{
editor::{Editor, Input, Mode, Palette},
files::{DockSide, FilePicker, FilePresentation},
files::{DockSide, FilePicker, FilePresentation, FileView},
session::EditorSession,
};
@@ -427,9 +427,9 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) {
])
.areas(column);
frame.render_widget(Clear, panel);
draw_file_list(frame, picker, panel, editor.selection_navigation_hint());
draw_file_list(frame, picker, panel, editor.file_navigation_hint());
} else if let Some(panel) = dock {
draw_file_list(frame, picker, panel, editor.selection_navigation_hint());
draw_file_list(frame, picker, panel, editor.file_navigation_hint());
}
}
if let Some(palette) = editor.palette() {
@@ -492,9 +492,14 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
Color::DarkGray
};
let bordered = panel.width >= 4 && panel.height >= 4;
let title = match picker.presentation {
FilePresentation::Floating => " Files ",
FilePresentation::Docked => " Explorer ",
let browsing = picker.view == FileView::Tree && !picker.searching;
let title = match (picker.presentation, picker.view, browsing) {
(FilePresentation::Floating, FileView::List, _) => " Files · List ",
(FilePresentation::Docked, FileView::List, _) => " Explorer · List ",
(FilePresentation::Floating, FileView::Tree, true) => " Files · Tree browse ",
(FilePresentation::Docked, FileView::Tree, true) => " Explorer · Tree browse ",
(FilePresentation::Floating, FileView::Tree, false) => " Files · Tree search ",
(FilePresentation::Docked, FileView::Tree, false) => " Explorer · Tree search ",
};
let block = Block::default()
.borders(if bordered {
@@ -519,10 +524,18 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
Constraint::Length(u16::from(show_help)),
])
.areas(content);
let query = format!("> {}", display_text(&picker.query));
let query = if browsing {
"browse · / search · ^T view".into()
} else {
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));
let offset = if browsing {
0
} else {
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 {
@@ -534,7 +547,8 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
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));
let cursor_x = if browsing { 0 } else { query_width - offset };
frame.set_cursor_position((query_area.x + cursor_x as u16, query_area.y));
}
if show_root {
frame.render_widget(
@@ -562,13 +576,14 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
);
} else if results_area.height > 0 {
let count = usize::from(results_area.height);
let first = picker.selected.saturating_sub(count - 1);
let selected_index = picker.selected.min(entries.len() - 1);
let first = selected_index.saturating_sub(count - 1);
let lines: Vec<Line> = entries
.enumerate()
.skip(first)
.take(count)
.map(|(index, entry)| {
let selected = index == picker.selected;
let selected = index == selected_index;
let style = if selected {
Style::default()
.bg(if picker.focused {
@@ -580,22 +595,47 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
} else {
Style::default()
};
Line::styled(
format!("{} {}", if selected { ">" } else { " " }, entry.label),
style,
)
let label = match picker.view {
FileView::List => {
format!("{} {}", if selected { ">" } else { " " }, entry.label)
}
FileView::Tree => format!(
"{} {:indent$}{} {}",
if selected { ">" } else { " " },
"",
if entry.is_dir {
if entry.expanded { "▾" } else { "▸" }
} else {
" "
},
entry.label,
indent = entry.depth * 2,
),
};
Line::styled(label, style)
})
.collect();
frame.render_widget(Paragraph::new(lines), results_area);
if browsing && picker.focused && results_area.width > 0 {
frame.set_cursor_position((
results_area.x,
results_area.y + (selected_index - first) as u16,
));
}
}
let help = if picker.focused {
let controls = match picker.presentation {
FilePresentation::Floating => "Enter · Esc/^W close · ^R scan",
FilePresentation::Docked => "Enter · Esc/Tab edit · ^W · ^R",
};
format!("{navigation_hint} · {controls}")
let view_controls = if picker.view == FileView::Tree {
"/ search · ^T view"
} else {
"^T view"
};
format!("{navigation_hint} · {view_controls} · {controls}")
} else {
"Editor focus · invoke explorer to search".into()
"Editor focus · invoke explorer to browse".into()
};
frame.render_widget(
Paragraph::new(help).style(Style::default().fg(Color::DarkGray)),
@@ -722,6 +762,7 @@ mod tests {
fn file(&self, name: &str, text: &str) -> PathBuf {
let path = self.0.join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, text).unwrap();
path
}
@@ -764,6 +805,21 @@ mod tests {
KeyModifiers::CONTROL,
Some(Input::Ctrl('k')),
),
(
KeyCode::Char('h'),
KeyModifiers::CONTROL,
Some(Input::Ctrl('h')),
),
(
KeyCode::Char('l'),
KeyModifiers::CONTROL,
Some(Input::Ctrl('l')),
),
(
KeyCode::Backspace,
KeyModifiers::NONE,
Some(Input::Backspace),
),
(KeyCode::Enter, KeyModifiers::NONE, Some(Input::Enter)),
(KeyCode::Enter, KeyModifiers::CONTROL, Some(Input::Enter)),
(
@@ -793,16 +849,11 @@ mod tests {
editor.handle(translate(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL)).unwrap());
assert_eq!(editor.file_picker().unwrap().selected, count - 2);
let picker = editor.file_picker().unwrap();
let label = picker.entries().nth(picker.selected).unwrap().label.clone();
let label = picker.entries().nth(picker.selected).unwrap().label;
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 9)).unwrap();
terminal
.draw(|frame| {
draw_file_list(
frame,
picker,
frame.area(),
editor.selection_navigation_hint(),
)
draw_file_list(frame, picker, frame.area(), editor.file_navigation_hint())
})
.unwrap();
let text = screen_text(&terminal);
@@ -815,6 +866,7 @@ mod tests {
#[test]
fn list_footers_show_remapped_and_disabled_selection_keys() {
let mut editor = Editor::open(None, vec![]).unwrap();
editor.set_file_explorer_view("list").unwrap();
for key in ["ctrl+j", "ctrl+n", "down", "ctrl+k", "up"] {
editor.bind("selection", key, None).unwrap();
}
@@ -854,15 +906,20 @@ mod tests {
}
let special = directory.file("界e\u{301} space\nname", "text");
let editor = Editor::open(None, vec![]).unwrap();
let mut picker = FilePicker::scan(directory.0.clone(), FilePresentation::Floating).unwrap();
let mut picker = FilePicker::scan(
directory.0.clone(),
FilePresentation::Floating,
FileView::List,
)
.unwrap();
assert!(
picker
.entries()
.any(|entry| directory.0.join(&entry.path) == special)
.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();
let label = picker.entries().last().unwrap().label;
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);
@@ -902,7 +959,12 @@ mod tests {
fn file_list_renders_root_empty_filter_errors_and_idle_focus() {
let directory = Directory::new();
let editor = Editor::open(None, vec![]).unwrap();
let mut picker = FilePicker::scan(directory.0.clone(), FilePresentation::Docked).unwrap();
let mut picker = FilePicker::scan(
directory.0.clone(),
FilePresentation::Docked,
FileView::List,
)
.unwrap();
let area = Rect::new(0, 0, 100, 12);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 12)).unwrap();
terminal
@@ -943,13 +1005,41 @@ mod tests {
assert!(screen_text(&terminal).contains("Files"));
assert!(screen_text(&terminal).contains("Esc/^W"));
assert_eq!(terminal.backend().buffer()[(0, 0)].fg, Color::Cyan);
picker.set_view(FileView::Tree);
picker.cancel_search();
terminal
.draw(|frame| draw_file_list(frame, &picker, area, editor.file_navigation_hint()))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("Tree browse"), "{text}");
assert!(text.contains("No project files"), "{text}");
assert_eq!(
terminal.get_cursor_position().unwrap(),
ratatui::layout::Position::new(1, 1)
);
picker.focused = false;
terminal
.draw(|frame| {
frame.set_cursor_position((99, 11));
draw_file_list(frame, &picker, area, editor.file_navigation_hint());
})
.unwrap();
assert_eq!(
terminal.get_cursor_position().unwrap(),
ratatui::layout::Position::new(99, 11)
);
}
#[test]
fn file_query_cursor_and_dock_layout_survive_tiny_offset_resizes() {
let directory = Directory::new();
let editor = Editor::open(None, vec![]).unwrap();
let mut picker = FilePicker::scan(directory.0.clone(), FilePresentation::Docked).unwrap();
let mut picker = FilePicker::scan(
directory.0.clone(),
FilePresentation::Docked,
FileView::List,
)
.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);
@@ -1029,6 +1119,378 @@ mod tests {
}
}
#[test]
fn tree_rows_render_directory_chains_hierarchy_and_basename_files_in_both_presentations() {
use std::path::Path;
let directory = Directory::new();
directory.file("chain/a/b/leaf.txt", "");
directory.file("mixed/nested/inner.txt", "");
directory.file("mixed/sibling.txt", "");
directory.file("single/only.txt", "");
directory.file("root.txt", "");
fs::create_dir(directory.0.join("empty")).unwrap();
let editor = Editor::open(None, vec![]).unwrap();
for presentation in [FilePresentation::Floating, FilePresentation::Docked] {
let mut picker =
FilePicker::scan(directory.0.clone(), presentation, FileView::Tree).unwrap();
assert_eq!(picker.entries().next().unwrap().label, "chain/a/b");
for path in ["chain/a/b", "mixed", "mixed/nested", "single"] {
let selected = picker
.entries()
.position(|row| row.path == Path::new(path))
.unwrap();
picker.selected = selected;
picker.right();
}
let area = Rect::new(3, 2, 100, 22);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(110, 28),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
terminal
.draw(|frame| draw_file_list(frame, &picker, area, editor.file_navigation_hint()))
.unwrap();
let text = screen_text(&terminal);
for label in [
" ▾ chain/a/b",
" leaf.txt",
" ▸ empty",
" ▾ mixed",
" ▾ nested",
" inner.txt",
" sibling.txt",
"> ▾ single",
" only.txt",
" root.txt",
"/ search · ^T view",
] {
assert!(text.contains(label), "{label}: {text}");
}
assert!(!text.contains("single/only.txt"), "{text}");
assert!(!text.contains("mixed/nested/inner.txt"), "{text}");
let cursor = terminal.get_cursor_position().unwrap();
assert_eq!(
terminal.backend().buffer()[(cursor.x, cursor.y)].symbol(),
">"
);
assert_eq!(
terminal.backend().buffer()[(cursor.x, cursor.y)].bg,
Color::Blue
);
picker.set_view(FileView::List);
terminal
.draw(|frame| draw_file_list(frame, &picker, area, editor.file_navigation_hint()))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("List"), "{text}");
assert!(text.contains("chain/a/b/leaf.txt"), "{text}");
assert!(text.contains("single/only.txt"), "{text}");
assert!(!text.contains("▸"), "{text}");
assert_eq!(terminal.get_cursor_position().unwrap().y, area.y + 1);
}
}
#[test]
fn tree_browse_cursor_tracks_window_and_bounds_empty_search_and_tiny_offset_panels() {
let directory = Directory::new();
for index in 0..18 {
directory.file(&format!("dir-{index:02}/file.txt"), "");
}
let editor = Editor::open(None, vec![]).unwrap();
let mut picker = FilePicker::scan(
directory.0.clone(),
FilePresentation::Docked,
FileView::Tree,
)
.unwrap();
for _ in 0..17 {
picker.navigate(true);
}
picker.right();
picker.right();
for (width, height) in [(100, 9), (12, 6), (8, 2), (1, 1), (1, 2), (8, 1), (100, 9)] {
let area = Rect::new(3, 2, width, height);
let mut terminal = Terminal::with_options(
ratatui::backend::TestBackend::new(110, 16),
ratatui::TerminalOptions {
viewport: ratatui::Viewport::Fixed(area),
},
)
.unwrap();
terminal
.draw(|frame| draw_file_list(frame, &picker, area, editor.file_navigation_hint()))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert!(area.contains(cursor), "{area:?}: {cursor:?}");
let has_results = height > if width >= 4 && height >= 4 { 3 } else { 1 };
if has_results {
assert_eq!(
terminal.backend().buffer()[(cursor.x, cursor.y)].symbol(),
">"
);
} else {
assert_eq!(cursor, ratatui::layout::Position::new(area.x, area.y));
}
if width == 100 {
let text = screen_text(&terminal);
assert!(text.contains("> file.txt"), "{text}");
assert!(!text.contains("dir-00"), "{text}");
}
picker.start_search();
picker.query = "missing".into();
picker.refilter();
terminal
.draw(|frame| draw_file_list(frame, &picker, area, editor.file_navigation_hint()))
.unwrap();
let cursor = terminal.get_cursor_position().unwrap();
assert!(area.contains(cursor), "{area:?}: {cursor:?}");
assert_eq!(cursor.y, area.y + u16::from(width >= 4 && height >= 4));
picker.cancel_search();
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
std::path::Path::new("dir-17/file.txt")
);
}
}
#[test]
fn default_explorer_tree_search_and_escape_restore_browse_rows_cursor_and_editor_state() {
let directory = Directory::new();
directory.file("src/inner/needle.txt", "");
directory.file("src/other.txt", "");
directory.file("root.txt", "");
let mut editor = Editor::open(None, vec![]).unwrap();
editor.set_project_root(directory.0.clone());
editor
.apply(vec![crate::editor::Effect::Replace("unchanged".into())])
.unwrap();
editor.handle(Input::Char('v'));
let mode = editor.mode();
let selections = editor.selections();
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
assert_eq!(editor.file_picker().unwrap().view, FileView::Tree);
editor.handle(Input::Char('z'));
assert_eq!(editor.file_picker().unwrap().query, "");
editor.handle(Input::Ctrl('l'));
editor.handle(Input::Ctrl('l'));
let picker = editor.file_picker().unwrap();
let selected = picker
.entries()
.nth(picker.selected)
.unwrap()
.path
.to_owned();
let before: Vec<_> = picker
.entries()
.map(|row| (row.path.to_owned(), row.expanded))
.collect();
assert_eq!(selected, std::path::Path::new("src/inner"));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
let (mut top, mut left) = (0, 0);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let browse_cursor = terminal.get_cursor_position().unwrap();
editor.handle(Input::Char('/'));
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "");
assert!(
editor
.file_picker()
.unwrap()
.entries()
.any(|row| row.path == std::path::Path::new("src/inner/needle.txt"))
);
editor.handle(Input::Ctrl('t'));
assert_eq!(editor.file_picker().unwrap().view, FileView::List);
editor.handle(Input::Ctrl('t'));
assert_eq!(editor.file_picker().unwrap().view, FileView::Tree);
assert!(editor.file_picker().unwrap().searching);
for ch in "src/inner/needle.txt".chars() {
editor.handle(Input::Char(ch));
}
editor.handle(Input::Ctrl('h'));
assert_eq!(editor.file_picker().unwrap().query, "src/inner/needle.txt");
let picker = editor.file_picker().unwrap();
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
std::path::Path::new("src/inner/needle.txt")
);
editor.handle(Input::Ctrl('j'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let text = screen_text(&terminal);
assert!(text.contains("Tree search"), "{text}");
assert!(text.contains("needle.txt"), "{text}");
assert_eq!(terminal.get_cursor_position().unwrap().y, 1);
editor.handle(Input::Escape);
let picker = editor.file_picker().unwrap();
assert!(picker.focused && !picker.searching && picker.query.is_empty());
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
selected.as_path()
);
assert_eq!(
picker
.entries()
.map(|row| (row.path.to_owned(), row.expanded))
.collect::<Vec<_>>(),
before
);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
assert_eq!(terminal.get_cursor_position().unwrap(), browse_cursor);
editor.handle(Input::Escape);
assert!(!editor.file_picker().unwrap().focused);
assert_eq!(editor.mode(), mode);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.lines(), ["unchanged"]);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
editor.handle(Input::Char('/'));
editor.handle(Input::Char('n'));
editor.handle(Input::Char('\t'));
assert!(!editor.file_picker().unwrap().focused);
editor.handle(Input::Char(' '));
editor.handle(Input::Char('e'));
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "n");
editor.handle(Input::Ctrl('t'));
assert_eq!(editor.file_picker().unwrap().view, FileView::List);
assert_eq!(editor.file_picker().unwrap().query, "n");
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
assert!(screen_text(&terminal).contains("List"));
assert_eq!(terminal.get_cursor_position().unwrap().y, 1);
editor.handle(Input::Ctrl('t'));
assert_eq!(editor.file_picker().unwrap().view, FileView::Tree);
assert!(editor.file_picker().unwrap().searching);
assert_eq!(editor.file_picker().unwrap().query, "n");
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
assert!(screen_text(&terminal).contains("Tree search"));
assert_eq!(terminal.get_cursor_position().unwrap().y, 1);
editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
assert!(!editor.file_picker().unwrap().searching);
}
#[test]
fn configured_views_and_view_toggle_render_in_both_presentations_with_scoped_hints() {
let directory = Directory::new();
directory.file("folder/file.txt", "");
let mut editor = Editor::open(None, vec![]).unwrap();
editor.set_project_root(directory.0.clone());
editor.set_file_picker_view("tree").unwrap();
editor.set_file_explorer_view("list").unwrap();
editor.set_file_explorer_side("bottom").unwrap();
editor.bind("tree", "ctrl+l", None).unwrap();
editor.bind("tree", "right", None).unwrap();
editor.bind("tree", "x", Some("core.right".into())).unwrap();
for key in ["ctrl+j", "ctrl+n", "down", "ctrl+k", "up"] {
editor.bind("selection", key, None).unwrap();
}
editor
.bind("selection", "j", Some("core.down".into()))
.unwrap();
editor
.bind("selection", "k", Some("core.up".into()))
.unwrap();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(200, 30)).unwrap();
let (mut top, mut left) = (0, 0);
for (key, expected) in [('o', FileView::Tree), ('e', FileView::List)] {
editor.handle(Input::Char(' '));
editor.handle(Input::Char(key));
assert_eq!(editor.file_picker().unwrap().view, expected);
for view in [
expected,
if expected == FileView::Tree {
FileView::List
} else {
FileView::Tree
},
] {
assert_eq!(editor.file_picker().unwrap().view, view);
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
let text = screen_text(&terminal);
assert!(
text.contains(if view == FileView::Tree {
"Tree browse"
} else {
"List"
}),
"{text}"
);
assert!(text.contains("^T view"), "{text}");
assert!(text.contains(editor.file_navigation_hint()), "{text}");
if view == FileView::Tree {
assert_ne!(
editor.file_navigation_hint(),
editor.selection_navigation_hint()
);
assert!(text.contains("/ search"), "{text}");
let cursor = terminal.get_cursor_position().unwrap();
assert_eq!(
terminal.backend().buffer()[(cursor.x, cursor.y)].symbol(),
">"
);
editor.handle(Input::Char('x'));
assert_eq!(editor.file_picker().unwrap().query, "");
assert!(
editor
.file_picker()
.unwrap()
.entries()
.next()
.unwrap()
.expanded
);
let picker = editor.file_picker().unwrap();
let was_file = !picker.entries().nth(picker.selected).unwrap().is_dir;
editor.handle(Input::Ctrl('h'));
if was_file {
let picker = editor.file_picker().unwrap();
assert!(picker.entries().nth(picker.selected).unwrap().is_dir);
assert!(picker.entries().next().unwrap().expanded);
editor.handle(Input::Ctrl('h'));
}
assert!(
!editor
.file_picker()
.unwrap()
.entries()
.next()
.unwrap()
.expanded
);
} else {
assert_eq!(
editor.file_navigation_hint(),
editor.selection_navigation_hint()
);
assert!(text.contains("folder/file.txt"), "{text}");
}
editor.handle(Input::Ctrl('t'));
}
editor.handle(Input::Escape);
}
editor.handle(Input::Ctrl('p'));
terminal
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
.unwrap();
assert!(screen_text(&terminal).contains(editor.selection_navigation_hint()));
}
#[test]
fn all_dock_sides_clip_editor_gutter_selection_carets_and_cursor() {
let mut editor = Editor::open(None, vec![]).unwrap();
@@ -2245,6 +2707,10 @@ mod tests {
Input::Ctrl('k'),
Input::Ctrl('j'),
Input::Ctrl('k'),
Input::Ctrl('h'),
Input::Ctrl('l'),
Input::Backspace,
Input::Backspace,
Input::Enter,
Input::Enter,
]
@@ -2257,6 +2723,10 @@ mod tests {
let input = translate(key).unwrap();
assert_eq!(input, expected);
if index == 4 {
for ch in "ab".chars() {
editor.handle(Input::Char(ch));
}
} else if index == 8 {
for ch in "q!".chars() {
editor.handle(Input::Char(ch));
}
@@ -2265,13 +2735,16 @@ mod tests {
match index {
0 | 2 => assert_eq!(editor.palette().unwrap().selected, 1),
1 | 3 => assert_eq!(editor.palette().unwrap().selected, 0),
4 => {
4 | 5 => assert_eq!(editor.palette().unwrap().query, "ab"),
6 => assert_eq!(editor.palette().unwrap().query, "a"),
7 => assert_eq!(editor.palette().unwrap().query, ""),
8 => {
assert!(editor.palette().is_none());
assert_eq!(editor.mode(), Mode::Command);
assert_eq!(editor.command_line(), "q! ");
assert!(!editor.should_quit());
}
5 => assert!(editor.should_quit()),
9 => assert!(editor.should_quit()),
_ => unreachable!(),
}
assert_eq!(editor.lines(), original);
@@ -2374,7 +2847,7 @@ PTY.spawn(*ARGV) do |output, input, pid|
STDOUT.write(chunk)
captured << chunk
if !sent_keys && captured.include?("READING_KEYS")
input.write("\e[106;5u\e[107;5u\n\v\e[13u\r")
input.write("\e[106;5u\e[107;5u\n\v\b\f\x7f\x7f\e[13u\r")
input.flush
sent_keys = true
end