feat: open floating explorer as Tree in Insert mode

This commit is contained in:
Valdemar Lorenzen
2026-10-08 16:41:16 +02:00
parent 49242bb6e4
commit 86395cf5ce
8 changed files with 99 additions and 25 deletions
+6 -6
View File
@@ -20,7 +20,7 @@ Without a file, vamp opens an unnamed buffer. A nonexistent file opens an empty
| All modes | Ctrl+p opens the command palette; type to filter, Ctrl+j/k or arrows select, Enter accepts, Esc/Ctrl+p cancels | | All modes | Ctrl+p opens the command palette; type to filter, Ctrl+j/k or arrows select, Enter accepts, Esc/Ctrl+p cancels |
| Normal, Visual | Space Space opens the palette through the default leader | | Normal, Visual | Space Space opens the palette through the default leader |
| Normal | Space w saves; Space b n/p/l/d switches next/previous, lists, or closes buffers | | Normal | Space w saves; Space b n/p/l/d switches next/previous, lists, or closes buffers |
| Normal, Visual | Space o opens the floating file list; Space e opens/focuses the docked compact tree | | Normal, Visual | Space o opens the floating tree in INSERT/filter mode; Space e opens/focuses the docked compact tree |
| Normal | `i` insert before each caret; `a` insert after each caret; `o`/`O` open lines below/above | | Normal | `i` insert before each caret; `a` insert after each caret; `o`/`O` open lines below/above |
| Normal, Visual | `I` (Shift+i) / `A` (Shift+a) insert at first nonblank / line end; Visual splits each touched row into a caret first | | Normal, Visual | `I` (Shift+i) / `A` (Shift+a) insert at first nonblank / line end; Visual splits each touched row into a caret first |
| Normal | `h`/`j`/`k`/`l` or arrows move every caret; `0`/`$` or Home/End | | Normal | `h`/`j`/`k`/`l` or arrows move every caret; `0`/`$` or Home/End |
@@ -92,13 +92,13 @@ Switching from Insert commits its pending undo group and parks that buffer in No
### File picker and explorer ### File picker and explorer
Space o opens a floating file picker, defaulting to **List**; Space e opens or focuses the docked explorer, defaulting to **Tree**. Both use one catalog and instance. Invoking the other action applies that presentation's configured default while retaining query, selection identities, and surviving folder expansion. These actions are available as `config.core.file_picker` and `config.core.file_explorer` in Normal/Visual and in the command palette. Space o opens a floating **Tree** explorer directly in **INSERT/filter** mode, ready for typing; Space e opens or focuses the docked **Tree**, starting in **NORMAL/navigation** mode when new. Both use one catalog and instance. Invoking the other action applies that presentation's configured default while retaining query, selection identities, and surviving folder expansion. Floating Tree invocation enters filtering if not already filtering. These actions are available as `config.core.file_picker` and `config.core.file_explorer` in Normal/Visual and in the command palette.
The dock defaults to the right of the editor workspace. Set `config.file_explorer_side = "left"` (or `"right"`, `"top"`, `"bottom"`) to choose another side. Status and command/search prompts remain full-width; editor text, gutter, highlights, and cursors use the remaining rectangle. Focused panes prioritize their query on tiny terminals; unfocused docks shrink or hide to preserve editing space. The dock defaults to the right of the editor workspace. Set `config.file_explorer_side = "left"` (or `"right"`, `"top"`, `"bottom"`) to choose another side. Status and command/search prompts remain full-width; editor text, gutter, highlights, and cursors use the remaining rectangle. Focused panes prioritize their query on tiny terminals; unfocused docks shrink or hide to preserve editing space.
Choose either view independently with `config.file_picker_view = "list"` and `config.file_explorer_view = "tree"` (both accept `"list"` or `"tree"`). Ctrl+t toggles the current focused instance without rescanning. List retains searchable full relative paths. Tree shows folders first, two-cell indentation, and basename-only files. Folder names end in `/` and use cyan text; files use the normal foreground color. Fold-state arrows are omitted. Maximal single-child directory chains share one row, such as `src/client/http/`; a directory containing one file still gets its own row. Both view preferences default to `"tree"`: `config.file_picker_view` and `config.file_explorer_view` independently accept `"list"` or `"tree"`. Set the floating preference to `"list"` to retain the old flat picker. Ctrl+t toggles the current focused instance without rescanning. List retains searchable full relative paths. Tree shows folders first, two-cell indentation, and basename-only files. Folder names end in `/` and use cyan text; files use the normal foreground color. Fold-state arrows are omitted. Maximal single-child directory chains share one row, such as `src/client/http/`; a directory containing one file still gets its own row.
New trees start in explorer **NORMAL** mode for navigation. Press `i` to enter explorer **INSERT** mode and filter; the `i` itself is not added to the query. Typing and Backspace then filter full relative paths by case-insensitive whitespace tokens. These are explorer-local modes, not changes to the file buffer's mode. Matching nodes appear with their ancestor context, temporarily revealing descendants without clearing saved expansion. Query changes select a direct match, not merely a contextual ancestor. Escape in explorer INSERT clears the temporary filter and restores explorer NORMAL and its previous selection, retaining explorer focus. Escape from explorer NORMAL returns focus to the buffer (or closes floating search). List always accepts query typing immediately. Floating Tree opens in explorer **INSERT** mode; a new docked Tree opens in **NORMAL**. Press `i` in NORMAL to filter; the `i` itself is not added to the query. Typing and Backspace filter full relative paths by case-insensitive whitespace tokens. These are explorer-local modes, not changes to the file buffer's mode. Matching nodes appear with their ancestor context, temporarily revealing descendants without clearing saved expansion. Query changes select a direct match, not merely a contextual ancestor. Escape in explorer INSERT clears the temporary filter and restores explorer NORMAL and its previous selection, retaining explorer focus. Escape from explorer NORMAL returns focus to the buffer (or closes floating search). List always accepts query typing immediately.
| File-view input | Behavior | | File-view input | Behavior |
| --- | --- | | --- | --- |
@@ -201,7 +201,7 @@ Vamp.configure do |config|
config.leader = "space" config.leader = "space"
config.file_explorer_side = "right" config.file_explorer_side = "right"
config.file_explorer_view = "tree" config.file_explorer_view = "tree"
config.file_picker_view = "list" config.file_picker_view = "tree"
config.bind "ctrl+h", config.core.left, mode: config.modes.tree config.bind "ctrl+h", config.core.left, mode: config.modes.tree
config.bind "ctrl+l", config.core.right, mode: config.modes.tree config.bind "ctrl+l", config.core.right, mode: config.modes.tree
config.plugin "plugins/greeting.rb" config.plugin "plugins/greeting.rb"
@@ -220,7 +220,7 @@ end
``` ```
Plugin paths resolve relative to the config file, not the shell's working directory. `config.bind key, target, mode: config.modes.normal` overrides one key or leader-prefixed sequence; editing modes are `config.modes.normal`, `config.modes.insert`, `config.modes.command`, `config.modes.search`, and `config.modes.visual`. `config.modes.selection` configures shared list navigation; `config.modes.tree` overrides it in tree browsing. Omitting `mode` selects Normal. The leader defaults to `"space"` and is applied before configured bindings. Plugin paths resolve relative to the config file, not the shell's working directory. `config.bind key, target, mode: config.modes.normal` overrides one key or leader-prefixed sequence; editing modes are `config.modes.normal`, `config.modes.insert`, `config.modes.command`, `config.modes.search`, and `config.modes.visual`. `config.modes.selection` configures shared list navigation; `config.modes.tree` overrides it in tree browsing. Omitting `mode` selects Normal. The leader defaults to `"space"` and is applied before configured bindings.
`config.file_explorer_side` defaults to `"right"`; other choices are `"left"`, `"top"`, and `"bottom"`. Independent `file_explorer_view` / `file_picker_view` defaults are `"tree"` / `"list"` and both accept either value. Invalid preference strings report notices and retain defaults while valid bindings still install. Non-string/empty values disable the whole configuration like other Ruby type errors. `config.file_explorer_side` defaults to `"right"`; other choices are `"left"`, `"top"`, and `"bottom"`. Independent `file_explorer_view` / `file_picker_view` preferences both default to `"tree"` and accept `"list"` as an alternative. Invalid preference strings report notices and retain defaults while valid bindings still install. Non-string/empty values disable the whole configuration like other Ruby type errors.
`config.core` exposes the built-in actions below as immutable `Action` values; `config.modes` exposes immutable `Mode` values. Misspelled methods fail during configuration evaluation, and `bind` rejects the wrong value type. Built-in action strings and mode strings are not accepted. Registered Ex/plugin commands still use exact string names (`"w"`, `"greet"`, etc., without `:` or arguments) and can only be bound in Normal mode. Ruby provides runtime checks, not compile-time checking; the editor still validates action/mode compatibility. `config.core` exposes the built-in actions below as immutable `Action` values; `config.modes` exposes immutable `Mode` values. Misspelled methods fail during configuration evaluation, and `bind` rejects the wrong value type. Built-in action strings and mode strings are not accepted. Registered Ex/plugin commands still use exact string names (`"w"`, `"greet"`, etc., without `:` or arguments) and can only be bound in Normal mode. Ruby provides runtime checks, not compile-time checking; the editor still validates action/mode compatibility.
+1 -1
View File
@@ -2,7 +2,7 @@ Vamp.configure do |config|
config.leader = "space" config.leader = "space"
config.file_explorer_side = "right" config.file_explorer_side = "right"
config.file_explorer_view = "tree" config.file_explorer_view = "tree"
config.file_picker_view = "list" config.file_picker_view = "tree"
config.bind "ctrl+j", config.core.down, mode: config.modes.selection config.bind "ctrl+j", config.core.down, mode: config.modes.selection
config.bind "ctrl+k", config.core.up, mode: config.modes.selection config.bind "ctrl+k", config.core.up, mode: config.modes.selection
config.bind "h", config.core.left, mode: config.modes.tree config.bind "h", config.core.left, mode: config.modes.tree
+76 -6
View File
@@ -771,7 +771,7 @@ impl Editor {
project_root: std::env::current_dir()?, project_root: std::env::current_dir()?,
file_picker: None, file_picker: None,
file_explorer_side: DockSide::Right, file_explorer_side: DockSide::Right,
file_picker_view: FileView::List, file_picker_view: FileView::Tree,
file_explorer_view: FileView::Tree, file_explorer_view: FileView::Tree,
count: None, count: None,
}; };
@@ -1450,6 +1450,13 @@ impl Editor {
} }
} }
} }
if presentation == FilePresentation::Floating
&& view == FileView::Tree
&& let Some(picker) = &mut self.file_picker
&& !picker.searching
{
picker.start_search();
}
self.palette = None; self.palette = None;
self.count = None; self.count = None;
self.clear_leader(); self.clear_leader();
@@ -3340,6 +3347,50 @@ mod tests {
editor editor
} }
#[test]
fn floating_default_tree_starts_filtering_then_escape_navigates_before_close() {
let directory = Directory::new();
fs::create_dir(directory.path("dir")).unwrap();
fs::write(directory.path("dir/first.txt"), "first").unwrap();
fs::write(directory.path("dir/second.txt"), "second").unwrap();
let mut editor = project_editor(&directory);
type_text(&mut editor, " osecond.txt");
let picker = editor.file_picker().unwrap();
assert_eq!(picker.view, FileView::Tree);
assert!(picker.focused && picker.searching);
assert_eq!(picker.query, "second.txt");
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("dir/second.txt")
);
editor.handle(Input::Escape);
let picker = editor.file_picker().unwrap();
assert!(picker.focused && !picker.searching);
assert_eq!(
picker.entries().nth(picker.selected).unwrap().path,
Path::new("dir")
);
editor.handle(Input::Char('l'));
assert!(
editor
.file_picker()
.unwrap()
.entries()
.next()
.unwrap()
.expanded
);
editor.handle(Input::Escape);
assert!(editor.file_picker().is_none());
assert_eq!(editor.lines(), &[String::new()]);
editor.set_file_picker_view("list").unwrap();
type_text(&mut editor, " osecond.txt");
assert_eq!(editor.file_picker().unwrap().view, FileView::List);
editor.handle(Input::Enter);
assert_eq!(editor.lines(), &["second"]);
assert!(editor.file_picker().is_none());
}
#[test] #[test]
fn selection_navigation_wraps_without_touching_any_underlying_mode() { fn selection_navigation_wraps_without_touching_any_underlying_mode() {
let directory = Directory::new(); let directory = Directory::new();
@@ -3705,6 +3756,7 @@ mod tests {
fs::write(directory.path(name), "text").unwrap(); fs::write(directory.path(name), "text").unwrap();
} }
let mut editor = project_editor(&directory); let mut editor = project_editor(&directory);
editor.set_file_picker_view("list").unwrap();
type_text(&mut editor, " o"); type_text(&mut editor, " o");
editor.handle(Input::Ctrl('K')); editor.handle(Input::Ctrl('K'));
assert_eq!(editor.file_picker().unwrap().selected, 2); assert_eq!(editor.file_picker().unwrap().selected, 2);
@@ -3788,6 +3840,8 @@ mod tests {
type_text(&mut editor, " o"); type_text(&mut editor, " o");
assert_eq!(editor.file_picker().unwrap().query, ""); assert_eq!(editor.file_picker().unwrap().query, "");
editor.handle(Input::Escape); editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
editor.handle(Input::Escape);
assert!(editor.file_picker().is_none()); assert!(editor.file_picker().is_none());
} }
@@ -3867,6 +3921,7 @@ mod tests {
assert!(editor.file_picker().unwrap().error.is_none()); assert!(editor.file_picker().unwrap().error.is_none());
editor.handle(Input::Escape); editor.handle(Input::Escape);
editor.handle(Input::Escape); editor.handle(Input::Escape);
editor.handle(Input::Escape);
editor.handle(Input::Char('i')); editor.handle(Input::Char('i'));
type_text(&mut editor, "X"); type_text(&mut editor, "X");
editor.open_file_picker(FilePresentation::Docked); editor.open_file_picker(FilePresentation::Docked);
@@ -3970,6 +4025,8 @@ mod tests {
editor.open_file_picker(FilePresentation::Floating); editor.open_file_picker(FilePresentation::Floating);
editor.handle(Input::Paste("ignored".into())); editor.handle(Input::Paste("ignored".into()));
editor.handle(Input::Escape); editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Insert); assert_eq!(editor.mode(), Mode::Insert);
assert!(editor.active.insert_start.is_some()); assert!(editor.active.insert_start.is_some());
assert_eq!(editor.context().text, "X"); assert_eq!(editor.context().text, "X");
@@ -4023,8 +4080,6 @@ mod tests {
fs::create_dir(directory.path("folder")).unwrap(); fs::create_dir(directory.path("folder")).unwrap();
fs::write(directory.path("file.txt"), "text").unwrap(); fs::write(directory.path("file.txt"), "text").unwrap();
let mut editor = project_editor(&directory); let mut editor = project_editor(&directory);
assert_eq!(editor.file_picker_view(), FileView::List);
assert_eq!(editor.file_explorer_view(), FileView::Tree);
editor.open_file_picker(FilePresentation::Docked); editor.open_file_picker(FilePresentation::Docked);
assert_eq!(editor.file_picker().unwrap().view, FileView::Tree); assert_eq!(editor.file_picker().unwrap().view, FileView::Tree);
editor.set_file_picker_view("TREE").unwrap(); editor.set_file_picker_view("TREE").unwrap();
@@ -4066,6 +4121,9 @@ mod tests {
let mut editor = project_editor(&directory); let mut editor = project_editor(&directory);
editor.set_file_picker_view("tree").unwrap(); editor.set_file_picker_view("tree").unwrap();
editor.open_file_picker(presentation); editor.open_file_picker(presentation);
if presentation == FilePresentation::Floating {
editor.handle(Input::Escape);
}
let selected = |editor: &Editor| { let selected = |editor: &Editor| {
let picker = editor.file_picker().unwrap(); let picker = editor.file_picker().unwrap();
picker picker
@@ -4179,6 +4237,9 @@ mod tests {
let undo = editor.active.undo.len(); let undo = editor.active.undo.len();
editor.set_file_picker_view("tree").unwrap(); editor.set_file_picker_view("tree").unwrap();
editor.open_file_picker(presentation); editor.open_file_picker(presentation);
if presentation == FilePresentation::Floating {
editor.handle(Input::Escape);
}
type_text(&mut editor, "123 b xyz"); type_text(&mut editor, "123 b xyz");
editor.handle(Input::Paste("ignored".into())); editor.handle(Input::Paste("ignored".into()));
editor.handle(Input::Backspace); editor.handle(Input::Backspace);
@@ -4392,7 +4453,10 @@ mod tests {
let mut editor = project_editor(&directory); let mut editor = project_editor(&directory);
editor.set_file_picker_view("tree").unwrap(); editor.set_file_picker_view("tree").unwrap();
editor.open_file_picker(presentation); editor.open_file_picker(presentation);
type_text(&mut editor, "ineedle.txt"); if presentation == FilePresentation::Docked {
editor.handle(Input::Char('i'));
}
type_text(&mut editor, "needle.txt");
let picker = editor.file_picker().unwrap(); let picker = editor.file_picker().unwrap();
assert_eq!( assert_eq!(
picker.entries().nth(picker.selected).unwrap().path, picker.entries().nth(picker.selected).unwrap().path,
@@ -4407,7 +4471,10 @@ mod tests {
editor.open_file_picker(presentation); editor.open_file_picker(presentation);
editor.handle(Input::Ctrl('w')); editor.handle(Input::Ctrl('w'));
editor.open_file_picker(presentation); editor.open_file_picker(presentation);
type_text(&mut editor, "imodule"); if presentation == FilePresentation::Docked {
editor.handle(Input::Char('i'));
}
type_text(&mut editor, "module");
assert!( assert!(
editor editor
.file_picker() .file_picker()
@@ -4453,7 +4520,10 @@ mod tests {
editor.edit_buffer(b.clone(), false).unwrap(); editor.edit_buffer(b.clone(), false).unwrap();
fs::write(&a, b"\xff").unwrap(); fs::write(&a, b"\xff").unwrap();
editor.open_file_picker(presentation); editor.open_file_picker(presentation);
type_text(&mut editor, "isame"); if presentation == FilePresentation::Docked {
editor.handle(Input::Char('i'));
}
type_text(&mut editor, "same");
let picker = editor.file_picker.as_mut().unwrap(); let picker = editor.file_picker.as_mut().unwrap();
let labels = picker let labels = picker
.entries() .entries()
+1 -1
View File
@@ -10,7 +10,7 @@ Default keys (configurable with config.bind):
All modes: Ctrl+p command palette; type to filter, Ctrl+j/k or arrows select, Enter accept, Esc cancel All modes: Ctrl+p command palette; type to filter, Ctrl+j/k or arrows select, Enter accept, Esc cancel
Normal/Visual: Space is leader; Space Space opens palette; Esc cancels prefix Normal/Visual: Space is leader; Space Space opens palette; Esc cancels prefix
Normal leader: Space w saves; Space b n/p/l/d next/previous/list/close buffer Normal leader: Space w saves; Space b n/p/l/d next/previous/list/close buffer
Files: Space o floating list; Space e docked compact tree (right by default) Files: Space o floating tree INSERT; Space e docked tree NORMAL (right by default)
tree NORMAL: hjkl navigate; INSERT: type to filter, Ctrl+hjkl navigate; Enter toggles/opens tree NORMAL: hjkl navigate; INSERT: type to filter, Ctrl+hjkl navigate; Enter toggles/opens
i filters in tree INSERT; Esc returns NORMAL, then editor; Ctrl+t view, Ctrl+w close, Ctrl+r refresh i filters in tree INSERT; Esc returns NORMAL, then editor; Ctrl+t view, Ctrl+w close, Ctrl+r refresh
Normal: i/a insert, I/A insert at first nonblank/end, o/O new line Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
+1 -1
View File
@@ -30,7 +30,7 @@ impl Default for Config {
bindings: Vec::new(), bindings: Vec::new(),
leader: "space".into(), leader: "space".into(),
file_explorer_side: "right".into(), file_explorer_side: "right".into(),
file_picker_view: "list".into(), file_picker_view: "tree".into(),
file_explorer_view: "tree".into(), file_explorer_view: "tree".into(),
} }
} }
+1 -1
View File
@@ -53,7 +53,7 @@ module Vamp
@bindings = [] @bindings = []
@leader = "space" @leader = "space"
@file_explorer_side = "right" @file_explorer_side = "right"
@file_picker_view = "list" @file_picker_view = "tree"
@file_explorer_view = "tree" @file_explorer_view = "tree"
end end
+5 -9
View File
@@ -276,9 +276,12 @@ end
); );
assert_eq!( assert_eq!(
session.editor().file_picker().unwrap().view, session.editor().file_picker().unwrap().view,
crate::files::FileView::List crate::files::FileView::Tree
); );
assert_eq!(session.editor().file_picker().unwrap().query, ""); assert_eq!(session.editor().file_picker().unwrap().query, "");
assert!(session.editor().file_picker().unwrap().searching);
session.handle(Input::Escape);
assert!(session.editor().file_picker().unwrap().focused);
session.handle(Input::Escape); session.handle(Input::Escape);
assert!(session.editor().file_picker().is_none()); assert!(session.editor().file_picker().is_none());
session.handle(Input::Char('u')); session.handle(Input::Char('u'));
@@ -318,10 +321,7 @@ end
session.editor().file_picker().unwrap().view, session.editor().file_picker().unwrap().view,
crate::files::FileView::Tree crate::files::FileView::Tree
); );
assert!(!session.editor().file_picker().unwrap().searching); assert!(session.editor().file_picker().unwrap().searching);
session.handle(Input::Char('x'));
assert_eq!(session.editor().file_picker().unwrap().query, "");
session.handle(Input::Char('i'));
session.handle(Input::Char('x')); session.handle(Input::Char('x'));
session.handle(Input::Ctrl('h')); session.handle(Input::Ctrl('h'));
assert_eq!(session.editor().file_picker().unwrap().query, "x"); assert_eq!(session.editor().file_picker().unwrap().query, "x");
@@ -365,10 +365,6 @@ end
session.editor().file_explorer_view(), session.editor().file_explorer_view(),
crate::files::FileView::Tree crate::files::FileView::Tree
); );
assert_eq!(
session.editor().file_picker_view(),
crate::files::FileView::List
);
session.handle(Input::Char('n')); session.handle(Input::Char('n'));
assert_eq!( assert_eq!(
session.editor().cursor(), session.editor().cursor(),
+8
View File
@@ -833,6 +833,7 @@ mod tests {
#[test] #[test]
fn file_list_window_follows_control_j_k_selection() { fn file_list_window_follows_control_j_k_selection() {
let mut editor = Editor::open(None, vec![]).unwrap(); let mut editor = Editor::open(None, vec![]).unwrap();
editor.set_file_picker_view("list").unwrap();
editor.handle(Input::Char(' ')); editor.handle(Input::Char(' '));
editor.handle(Input::Char('o')); editor.handle(Input::Char('o'));
let count = editor.file_picker().unwrap().entries().len(); let count = editor.file_picker().unwrap().entries().len();
@@ -864,6 +865,7 @@ mod tests {
fn list_footers_show_remapped_and_disabled_selection_keys() { fn list_footers_show_remapped_and_disabled_selection_keys() {
let mut editor = Editor::open(None, vec![]).unwrap(); let mut editor = Editor::open(None, vec![]).unwrap();
editor.set_file_explorer_view("list").unwrap(); editor.set_file_explorer_view("list").unwrap();
editor.set_file_picker_view("list").unwrap();
for key in ["ctrl+j", "ctrl+n", "down", "ctrl+k", "up"] { for key in ["ctrl+j", "ctrl+n", "down", "ctrl+k", "up"] {
editor.bind("selection", key, None).unwrap(); editor.bind("selection", key, None).unwrap();
} }
@@ -1422,6 +1424,10 @@ mod tests {
editor.handle(Input::Char(' ')); editor.handle(Input::Char(' '));
editor.handle(Input::Char(key)); editor.handle(Input::Char(key));
assert_eq!(editor.file_picker().unwrap().view, expected); assert_eq!(editor.file_picker().unwrap().view, expected);
if expected == FileView::Tree {
assert!(editor.file_picker().unwrap().searching);
editor.handle(Input::Escape);
}
for view in [ for view in [
expected, expected,
if expected == FileView::Tree { if expected == FileView::Tree {
@@ -1640,6 +1646,8 @@ mod tests {
assert_eq!(viewports.offsets[&a_id], before); assert_eq!(viewports.offsets[&a_id], before);
assert!(screen_text(&terminal).contains("Files")); assert!(screen_text(&terminal).contains("Files"));
editor.handle(Input::Escape); editor.handle(Input::Escape);
assert!(editor.file_picker().unwrap().focused);
editor.handle(Input::Escape);
terminal terminal
.draw(|frame| viewports.draw(frame, &editor)) .draw(|frame| viewports.draw(frame, &editor))
.unwrap(); .unwrap();