feat: default explorer to compact directory tree
This commit is contained in:
+505
-32
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user