feat: retain explorer filters in navigation mode
This commit is contained in:
+241
-112
@@ -524,8 +524,10 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
|
||||
Constraint::Length(u16::from(show_help)),
|
||||
])
|
||||
.areas(content);
|
||||
let query = if browsing {
|
||||
"NORMAL · i filter · ^T view".into()
|
||||
let query = if browsing && picker.query.is_empty() {
|
||||
"NORMAL · i filter · ^U clear · ^T view".into()
|
||||
} else if browsing {
|
||||
format!("filter: {} · ^U clear", display_text(&picker.query))
|
||||
} else {
|
||||
format!("> {}", display_text(&picker.query))
|
||||
};
|
||||
@@ -630,7 +632,7 @@ fn draw_file_list(frame: &mut Frame, picker: &FilePicker, panel: Rect, navigatio
|
||||
(FilePresentation::Floating, false) => "Enter · Esc/^W close · ^R · ^T",
|
||||
(FilePresentation::Docked, false) => "Enter · Esc/Tab edit · ^W · ^R · ^T",
|
||||
};
|
||||
format!("{navigation_hint} · {controls}")
|
||||
format!("^U clear · {navigation_hint} · {controls}")
|
||||
} else {
|
||||
"Editor focus · invoke explorer to browse".into()
|
||||
};
|
||||
@@ -1005,12 +1007,13 @@ mod tests {
|
||||
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();
|
||||
picker.finish_search();
|
||||
terminal
|
||||
.draw(|frame| draw_file_list(frame, &picker, area, editor.file_navigation_hint()))
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("No project files"), "{text}");
|
||||
assert!(text.contains("No matching files"), "{text}");
|
||||
assert!(text.contains("filter: missing"), "{text}");
|
||||
assert_eq!(
|
||||
terminal.get_cursor_position().unwrap(),
|
||||
ratatui::layout::Position::new(1, 1)
|
||||
@@ -1220,6 +1223,7 @@ mod tests {
|
||||
}
|
||||
picker.right();
|
||||
picker.right();
|
||||
let long_query = "long界e\u{301}z".repeat(20);
|
||||
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(
|
||||
@@ -1248,124 +1252,235 @@ mod tests {
|
||||
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")
|
||||
);
|
||||
for query in ["missing", long_query.as_str()] {
|
||||
picker.start_search();
|
||||
picker.query = query.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.finish_search();
|
||||
assert_eq!(picker.query, query);
|
||||
assert_eq!(picker.entries().len(), 0);
|
||||
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 query_x = area.x + u16::from(width >= 4 && height >= 4);
|
||||
assert_eq!(cursor.x, query_x);
|
||||
let query_y = area.y + u16::from(width >= 4 && height >= 4);
|
||||
let query_width = width - 2 * u16::from(width >= 4 && height >= 4);
|
||||
let header = format!("filter: {query} · ^U clear");
|
||||
let mut expected =
|
||||
ratatui::buffer::Buffer::empty(Rect::new(query_x, query_y, query_width, 1));
|
||||
use ratatui::widgets::Widget;
|
||||
Paragraph::new(visible_line(&header, 0, usize::from(query_width)))
|
||||
.render(expected.area, &mut expected);
|
||||
let mut continuation = 0;
|
||||
for x in query_x..query_x + query_width {
|
||||
if continuation > 0 {
|
||||
continuation -= 1;
|
||||
continue;
|
||||
}
|
||||
continuation = expected[(x, query_y)].symbol().width().saturating_sub(1);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(x, query_y)].symbol(),
|
||||
expected[(x, query_y)].symbol()
|
||||
);
|
||||
}
|
||||
if width == 100 {
|
||||
assert!(screen_text(&terminal).contains("filter: "), "{query}");
|
||||
assert!(
|
||||
screen_text(&terminal).contains("No matching files"),
|
||||
"{query}"
|
||||
);
|
||||
}
|
||||
picker.clear_filter();
|
||||
assert!(!picker.searching);
|
||||
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() {
|
||||
fn tree_filter_escape_resume_and_clear_keep_selection_and_cursor_ownership() {
|
||||
use std::path::Path;
|
||||
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('i'));
|
||||
assert!(editor.file_picker().unwrap().searching);
|
||||
assert_eq!(editor.file_picker().unwrap().query, "");
|
||||
assert!(
|
||||
for presentation in [FilePresentation::Floating, FilePresentation::Docked] {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_project_root(directory.0.clone());
|
||||
editor.set_file_picker_view("tree").unwrap();
|
||||
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")
|
||||
);
|
||||
editor.handle(Input::Ctrl('l'));
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
std::path::Path::new("src/inner/needle.txt")
|
||||
);
|
||||
assert_eq!(picker.query, "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("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
|
||||
.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(if presentation == FilePresentation::Floating {
|
||||
'o'
|
||||
} else {
|
||||
'e'
|
||||
}));
|
||||
if !editor.file_picker().unwrap().searching {
|
||||
editor.handle(Input::Char('i'));
|
||||
}
|
||||
let query = "src/inner/needle.txt";
|
||||
for ch in query.chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
editor.handle(Input::Ctrl('h'));
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert_eq!(picker.query, query);
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
Path::new("src/inner")
|
||||
);
|
||||
editor.handle(Input::Ctrl('l'));
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
Path::new(query)
|
||||
);
|
||||
let filtered: Vec<_> = 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"]);
|
||||
.collect();
|
||||
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 query_cursor = terminal.get_cursor_position().unwrap();
|
||||
assert!(screen_text(&terminal).contains(&format!("> {query}")));
|
||||
assert!(screen_text(&terminal).contains("^U clear"));
|
||||
editor.handle(Input::Escape);
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert!(picker.focused && !picker.searching);
|
||||
assert_eq!(picker.query, query);
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
Path::new(query)
|
||||
);
|
||||
assert_eq!(
|
||||
picker
|
||||
.entries()
|
||||
.map(|row| (row.path.to_owned(), row.expanded))
|
||||
.collect::<Vec<_>>(),
|
||||
filtered
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains(&format!("filter: {query}")), "{text}");
|
||||
assert!(
|
||||
text.contains("needle.txt") && !text.contains("other.txt"),
|
||||
"{text}"
|
||||
);
|
||||
let selected_cursor = terminal.get_cursor_position().unwrap();
|
||||
assert_ne!(selected_cursor.y, query_cursor.y);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(selected_cursor.x, selected_cursor.y)].symbol(),
|
||||
">"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(selected_cursor.x, selected_cursor.y)].bg,
|
||||
Color::Blue
|
||||
);
|
||||
editor.handle(Input::Char('i'));
|
||||
assert_eq!(editor.file_picker().unwrap().query, query);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap(), query_cursor);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert!(picker.focused && !picker.searching && picker.query.is_empty());
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
Path::new(query)
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(cursor.x, cursor.y)].symbol(),
|
||||
">"
|
||||
);
|
||||
assert!(screen_text(&terminal).contains("other.txt"));
|
||||
editor.handle(Input::Char('i'));
|
||||
for ch in query.chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert!(picker.focused && picker.searching && picker.query.is_empty());
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
Path::new(query)
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert_eq!(cursor.y, query_cursor.y);
|
||||
assert_eq!(cursor.x, query_cursor.x - query.len() as u16);
|
||||
editor.handle(Input::Ctrl('t'));
|
||||
for ch in query.chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
editor.handle(Input::Ctrl('u'));
|
||||
let picker = editor.file_picker().unwrap();
|
||||
assert_eq!(picker.view, FileView::List);
|
||||
assert!(picker.focused && picker.query.is_empty());
|
||||
assert_eq!(
|
||||
picker.entries().nth(picker.selected).unwrap().path,
|
||||
Path::new(query)
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||
.unwrap();
|
||||
assert_eq!(terminal.get_cursor_position().unwrap(), cursor);
|
||||
editor.handle(Input::Ctrl('t'));
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Escape);
|
||||
if presentation == FilePresentation::Floating {
|
||||
assert!(editor.file_picker().is_none());
|
||||
} else {
|
||||
assert!(!editor.file_picker().unwrap().focused);
|
||||
}
|
||||
assert_eq!(editor.mode(), mode);
|
||||
assert_eq!(editor.selections(), selections);
|
||||
assert_eq!(editor.lines(), ["unchanged"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explorer_retains_insert_filter_through_focus_and_view_switches() {
|
||||
let directory = Directory::new();
|
||||
directory.file("src/inner/needle.txt", "");
|
||||
directory.file("src/other.txt", "");
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_project_root(directory.0.clone());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 24)).unwrap();
|
||||
let (mut top, mut left) = (0, 0);
|
||||
editor.handle(Input::Char(' '));
|
||||
editor.handle(Input::Char('e'));
|
||||
editor.handle(Input::Char('i'));
|
||||
@@ -1395,6 +1510,20 @@ mod tests {
|
||||
editor.handle(Input::Escape);
|
||||
assert!(editor.file_picker().unwrap().focused);
|
||||
assert!(!editor.file_picker().unwrap().searching);
|
||||
assert_eq!(editor.file_picker().unwrap().query, "n");
|
||||
editor.handle(Input::Ctrl('t'));
|
||||
editor.handle(Input::Ctrl('t'));
|
||||
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("filter: n"));
|
||||
let cursor = terminal.get_cursor_position().unwrap();
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(cursor.x, cursor.y)].symbol(),
|
||||
">"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user