feat: use hjkl for explorer Normal navigation
This commit is contained in:
@@ -102,10 +102,10 @@ New trees start in explorer **NORMAL** mode for navigation. Press `i` to enter e
|
|||||||
|
|
||||||
| File-view input | Behavior |
|
| File-view input | Behavior |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Type / Backspace | Filter in List/explorer INSERT / remove one whole grapheme; ignored in explorer NORMAL |
|
| Type / Backspace | Type query text in List/explorer INSERT / remove one whole grapheme; explorer NORMAL uses hjkl for navigation |
|
||||||
| Ctrl+j/Down/Ctrl+n or Ctrl+k/Up | Wrap forward/backward through visible rows |
|
| j/k (NORMAL); Ctrl+j/k or Up/Down | Wrap forward/backward through visible rows |
|
||||||
| Ctrl+l/Right (explorer NORMAL) | Expand a closed folder; select its first child when already open |
|
| l (NORMAL); Ctrl+l/Right | NORMAL expands/enters a folder; INSERT selects its visible child without changing the filter |
|
||||||
| Ctrl+h/Left (explorer NORMAL) | Collapse an open folder; otherwise select its visible parent |
|
| h (NORMAL); Ctrl+h/Left | NORMAL collapses/selects parent; INSERT selects its visible parent without changing the filter |
|
||||||
| Enter | Toggle a folder, or open the selected file using existing buffer deduplication |
|
| Enter | Toggle a folder, or open the selected file using existing buffer deduplication |
|
||||||
| `i` (explorer NORMAL) | Enter explorer INSERT/filter mode without inserting `i` |
|
| `i` (explorer NORMAL) | Enter explorer INSERT/filter mode without inserting `i` |
|
||||||
| Esc | INSERT → explorer NORMAL, retaining focus; NORMAL → buffer focus or floating close |
|
| Esc | INSERT → explorer NORMAL, retaining focus; NORMAL → buffer focus or floating close |
|
||||||
@@ -141,14 +141,14 @@ config.bind "ctrl+j", nil, mode: config.modes.selection
|
|||||||
|
|
||||||
Selection bindings take precedence while a file or command list is focused. Unbound printable keys filter in List/explorer INSERT, but not explorer NORMAL; a disabled key is consumed without filtering or moving. Only `up`, `down`, and `nil` are accepted—no leader sequences, edits, or plugin commands. Invalid declarations leave existing routes unchanged. Navigation does not change text, carets, history, or the buffer mode. Both list footers show effective keys, including remaps and disabled directions.
|
Selection bindings take precedence while a file or command list is focused. Unbound printable keys filter in List/explorer INSERT, but not explorer NORMAL; a disabled key is consumed without filtering or moving. Only `up`, `down`, and `nil` are accepted—no leader sequences, edits, or plugin commands. Invalid declarations leave existing routes unchanged. Navigation does not change text, carets, history, or the buffer mode. Both list footers show effective keys, including remaps and disabled directions.
|
||||||
|
|
||||||
Explorer NORMAL additionally uses `config.modes.tree`, accepting `up`, `down`, `left`, `right`, or `nil`. Its single-key routes override shared selection routes only during tree navigation; unmatched keys inherit the shared selection bindings. Defaults are Ctrl+h/Left for collapse/parent and Ctrl+l/Right for expand/child. For example:
|
Tree views use `config.modes.tree`, accepting `up`, `down`, `left`, `right`, or `nil`. Explorer NORMAL defaults to h/j/k/l for parent/next/previous/child, with the existing control/arrow routes retained. Tree routes override shared selection routes. In explorer INSERT, printable tree keys are not intercepted: h/j/k/l become query text, while control/special-key tree routes still navigate. Ctrl+h/l select visible parents/children in the filtered projection without changing the query or saved folder expansion. Unmatched keys inherit shared selection bindings. Cached hints prioritize plain keys in NORMAL and omit them in INSERT. For example:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
config.bind "ctrl+f", config.core.right, mode: config.modes.tree
|
config.bind "ctrl+f", config.core.right, mode: config.modes.tree
|
||||||
config.bind "ctrl+h", nil, mode: config.modes.tree
|
config.bind "ctrl+h", nil, mode: config.modes.tree
|
||||||
```
|
```
|
||||||
|
|
||||||
Ctrl+h is **not** a Backspace alias or default deletion key in any mode. Raw Ctrl+h (0x08) and Backspace (DEL/0x7f) are distinct, as are CSI-u events. It navigates only through the tree browsing binding context; it does not delete a query or Insert text. Explicit `config.core.backspace` bindings remain available if desired.
|
Ctrl+h is **not** a Backspace alias or default deletion key in any mode. Raw Ctrl+h (0x08) and Backspace (DEL/0x7f) are distinct, as are CSI-u events. In the tree it navigates in NORMAL and INSERT without deleting query text; it never deletes file-buffer Insert text by default. Explicit `config.core.backspace` bindings remain available if desired.
|
||||||
|
|
||||||
Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside lists, existing editing bindings are unchanged: Ctrl+k still clones above in Normal/Visual, and explicit Ctrl+j defaults insert a newline in Insert or submit Command/Search prompts. Enter and Ctrl+j can now be rebound independently.
|
Ctrl+j is distinct from Enter: in the TUI's raw mode, LF is Ctrl+j and CR is Enter; CSI-u control events also work. No enhanced-terminal negotiation is required. Ctrl+m remains an Enter alias. Outside lists, existing editing bindings are unchanged: Ctrl+k still clones above in Normal/Visual, and explicit Ctrl+j defaults insert a newline in Insert or submit Command/Search prompts. Enter and Ctrl+j can now be rebound independently.
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ Vamp.configure do |config|
|
|||||||
config.file_picker_view = "list"
|
config.file_picker_view = "list"
|
||||||
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 "j", config.core.down, mode: config.modes.tree
|
||||||
|
config.bind "k", config.core.up, mode: config.modes.tree
|
||||||
|
config.bind "l", config.core.right, mode: config.modes.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"
|
||||||
|
|||||||
+149
-28
@@ -709,6 +709,7 @@ pub struct Editor {
|
|||||||
selection_navigation_hint: String,
|
selection_navigation_hint: String,
|
||||||
tree_bindings: HashMap<Input, Binding>,
|
tree_bindings: HashMap<Input, Binding>,
|
||||||
tree_navigation_hint: String,
|
tree_navigation_hint: String,
|
||||||
|
tree_filter_navigation_hint: String,
|
||||||
leader: Input,
|
leader: Input,
|
||||||
leader_bindings: HashMap<Mode, HashMap<Vec<Input>, Binding>>,
|
leader_bindings: HashMap<Mode, HashMap<Vec<Input>, Binding>>,
|
||||||
leader_suffix: Vec<Input>,
|
leader_suffix: Vec<Input>,
|
||||||
@@ -752,11 +753,16 @@ impl Editor {
|
|||||||
(Input::Ctrl('h'), Action::Left),
|
(Input::Ctrl('h'), Action::Left),
|
||||||
(Input::Right, Action::Right),
|
(Input::Right, Action::Right),
|
||||||
(Input::Ctrl('l'), Action::Right),
|
(Input::Ctrl('l'), Action::Right),
|
||||||
|
(Input::Char('h'), Action::Left),
|
||||||
|
(Input::Char('j'), Action::Down),
|
||||||
|
(Input::Char('k'), Action::Up),
|
||||||
|
(Input::Char('l'), Action::Right),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(key, action)| (key, Binding::Action(action)))
|
.map(|(key, action)| (key, Binding::Action(action)))
|
||||||
.collect(),
|
.collect(),
|
||||||
tree_navigation_hint: String::new(),
|
tree_navigation_hint: String::new(),
|
||||||
|
tree_filter_navigation_hint: String::new(),
|
||||||
leader: Input::Char(' '),
|
leader: Input::Char(' '),
|
||||||
leader_bindings: HashMap::new(),
|
leader_bindings: HashMap::new(),
|
||||||
leader_suffix: Vec::new(),
|
leader_suffix: Vec::new(),
|
||||||
@@ -1178,47 +1184,70 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn file_navigation_hint(&self) -> &str {
|
pub fn file_navigation_hint(&self) -> &str {
|
||||||
if self
|
if let Some(picker) = &self.file_picker
|
||||||
.file_picker
|
&& picker.view == FileView::Tree
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|picker| picker.view == FileView::Tree && !picker.searching)
|
|
||||||
{
|
{
|
||||||
&self.tree_navigation_hint
|
if picker.searching {
|
||||||
|
&self.tree_filter_navigation_hint
|
||||||
|
} else {
|
||||||
|
&self.tree_navigation_hint
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
&self.selection_navigation_hint
|
&self.selection_navigation_hint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_selection_navigation_hint(&mut self) {
|
fn refresh_selection_navigation_hint(&mut self) {
|
||||||
let keys = |action, tree: bool| {
|
let keys = |action, tree: bool, filtering: bool| {
|
||||||
|
let applies = |input: &Input| tree && (!filtering || !matches!(input, Input::Char(_)));
|
||||||
let mut keys: Vec<_> = self
|
let mut keys: Vec<_> = self
|
||||||
.selection_bindings
|
.selection_bindings
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(key, _)| !tree || !self.tree_bindings.contains_key(*key))
|
.filter(|(key, _)| !applies(key) || !self.tree_bindings.contains_key(*key))
|
||||||
.chain(self.tree_bindings.iter().filter(|_| tree))
|
.chain(self.tree_bindings.iter().filter(|(key, _)| applies(key)))
|
||||||
.filter(
|
.filter(
|
||||||
|(_, binding)| matches!(binding, Binding::Action(bound) if *bound == action),
|
|(_, binding)| matches!(binding, Binding::Action(bound) if *bound == action),
|
||||||
)
|
)
|
||||||
.map(|(input, _)| display_key(input))
|
.map(|(input, _)| {
|
||||||
|
let priority =
|
||||||
|
usize::from(!(tree && !filtering && matches!(input, Input::Char(_))));
|
||||||
|
(priority, display_key(input))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
keys.sort();
|
keys.sort();
|
||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
"unbound".into()
|
"unbound".into()
|
||||||
} else {
|
} else {
|
||||||
keys.join("/")
|
let capacity =
|
||||||
|
keys.iter().map(|(_, key)| key.len()).sum::<usize>() + keys.len() - 1;
|
||||||
|
let mut hint = String::with_capacity(capacity);
|
||||||
|
for (_, key) in keys {
|
||||||
|
if !hint.is_empty() {
|
||||||
|
hint.push('/');
|
||||||
|
}
|
||||||
|
hint.push_str(&key);
|
||||||
|
}
|
||||||
|
hint
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
self.selection_navigation_hint = format!(
|
self.selection_navigation_hint = format!(
|
||||||
"{} next · {} previous",
|
"{} next · {} previous",
|
||||||
keys(Action::Down, false),
|
keys(Action::Down, false, false),
|
||||||
keys(Action::Up, false)
|
keys(Action::Up, false, false)
|
||||||
);
|
);
|
||||||
self.tree_navigation_hint = format!(
|
self.tree_navigation_hint = format!(
|
||||||
"{} next · {} previous · {} parent/close · {} open/child",
|
"{} next · {} previous · {} parent/close · {} open/child",
|
||||||
keys(Action::Down, true),
|
keys(Action::Down, true, false),
|
||||||
keys(Action::Up, true),
|
keys(Action::Up, true, false),
|
||||||
keys(Action::Left, true),
|
keys(Action::Left, true, false),
|
||||||
keys(Action::Right, true)
|
keys(Action::Right, true, false)
|
||||||
|
);
|
||||||
|
self.tree_filter_navigation_hint = format!(
|
||||||
|
"{} next · {} previous · {} parent · {} child",
|
||||||
|
keys(Action::Down, true, true),
|
||||||
|
keys(Action::Up, true, true),
|
||||||
|
keys(Action::Left, true, true),
|
||||||
|
keys(Action::Right, true, true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,10 +1255,13 @@ impl Editor {
|
|||||||
let Some(picker) = self
|
let Some(picker) = self
|
||||||
.file_picker
|
.file_picker
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.filter(|picker| picker.focused && picker.view == FileView::Tree && !picker.searching)
|
.filter(|picker| picker.focused && picker.view == FileView::Tree)
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if picker.searching && matches!(input, Input::Char(_)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
match self.tree_bindings.get(input) {
|
match self.tree_bindings.get(input) {
|
||||||
Some(Binding::Action(Action::Down)) => picker.navigate(true),
|
Some(Binding::Action(Action::Down)) => picker.navigate(true),
|
||||||
Some(Binding::Action(Action::Up)) => picker.navigate(false),
|
Some(Binding::Action(Action::Up)) => picker.navigate(false),
|
||||||
@@ -4024,6 +4056,103 @@ mod tests {
|
|||||||
assert_eq!(editor.file_picker().unwrap().entries().len(), 1);
|
assert_eq!(editor.file_picker().unwrap().entries().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tree_hjkl_navigates_normal_and_control_hjkl_navigates_filter_without_changing_text() {
|
||||||
|
let directory = Directory::new();
|
||||||
|
fs::create_dir(directory.path("dir")).unwrap();
|
||||||
|
fs::write(directory.path("dir/hjkl-a.txt"), "a").unwrap();
|
||||||
|
fs::write(directory.path("dir/hjkl-b.txt"), "b").unwrap();
|
||||||
|
for presentation in [FilePresentation::Floating, FilePresentation::Docked] {
|
||||||
|
let mut editor = project_editor(&directory);
|
||||||
|
editor.set_file_picker_view("tree").unwrap();
|
||||||
|
editor.open_file_picker(presentation);
|
||||||
|
let selected = |editor: &Editor| {
|
||||||
|
let picker = editor.file_picker().unwrap();
|
||||||
|
picker
|
||||||
|
.entries()
|
||||||
|
.nth(picker.selected)
|
||||||
|
.unwrap()
|
||||||
|
.path
|
||||||
|
.to_owned()
|
||||||
|
};
|
||||||
|
editor.handle(Input::Char('l'));
|
||||||
|
assert!(
|
||||||
|
editor
|
||||||
|
.file_picker()
|
||||||
|
.unwrap()
|
||||||
|
.entries()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.expanded
|
||||||
|
);
|
||||||
|
editor.handle(Input::Char('l'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
|
||||||
|
editor.handle(Input::Char('j'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-b.txt"));
|
||||||
|
editor.handle(Input::Char('k'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
|
||||||
|
editor.handle(Input::Char('h'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir"));
|
||||||
|
assert!(
|
||||||
|
editor
|
||||||
|
.file_picker()
|
||||||
|
.unwrap()
|
||||||
|
.entries()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.expanded
|
||||||
|
);
|
||||||
|
editor.handle(Input::Char('h'));
|
||||||
|
assert!(
|
||||||
|
!editor
|
||||||
|
.file_picker()
|
||||||
|
.unwrap()
|
||||||
|
.entries()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.expanded
|
||||||
|
);
|
||||||
|
editor.handle(Input::Char('i'));
|
||||||
|
type_text(&mut editor, "hjkl");
|
||||||
|
assert_eq!(editor.file_picker().unwrap().query, "hjkl");
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
|
||||||
|
editor.handle(Input::Ctrl('j'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-b.txt"));
|
||||||
|
editor.handle(Input::Ctrl('k'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
|
||||||
|
editor.handle(Input::Ctrl('h'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir"));
|
||||||
|
editor.handle(Input::Ctrl('l'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-a.txt"));
|
||||||
|
assert_eq!(editor.file_picker().unwrap().query, "hjkl");
|
||||||
|
editor.handle(Input::Escape);
|
||||||
|
assert!(editor.file_picker().unwrap().focused);
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir"));
|
||||||
|
assert!(
|
||||||
|
!editor
|
||||||
|
.file_picker()
|
||||||
|
.unwrap()
|
||||||
|
.entries()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.expanded
|
||||||
|
);
|
||||||
|
editor.bind("tree", "j", Some("core.up".into())).unwrap();
|
||||||
|
editor.bind("tree", "k", None).unwrap();
|
||||||
|
editor.handle(Input::Char('l'));
|
||||||
|
editor.handle(Input::Char('j'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-b.txt"));
|
||||||
|
editor.handle(Input::Char('k'));
|
||||||
|
assert_eq!(selected(&editor), Path::new("dir/hjkl-b.txt"));
|
||||||
|
editor.handle(Input::Char('i'));
|
||||||
|
type_text(&mut editor, "jk");
|
||||||
|
assert_eq!(editor.file_picker().unwrap().query, "jk");
|
||||||
|
assert_eq!(editor.lines(), &[String::new()]);
|
||||||
|
assert_eq!(editor.mode(), Mode::Normal);
|
||||||
|
assert_eq!(editor.cursor(), Cursor::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tree_browse_and_search_have_separate_input_and_two_stage_escape() {
|
fn tree_browse_and_search_have_separate_input_and_two_stage_escape() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
@@ -4119,7 +4248,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tree_bindings_override_selection_only_in_browse_and_hints_are_effective() {
|
fn tree_binding_precedence_is_modal_and_navigation_preferences_are_atomic() {
|
||||||
let directory = Directory::new();
|
let directory = Directory::new();
|
||||||
fs::create_dir(directory.path("folder")).unwrap();
|
fs::create_dir(directory.path("folder")).unwrap();
|
||||||
fs::write(directory.path("folder/a.txt"), "a").unwrap();
|
fs::write(directory.path("folder/a.txt"), "a").unwrap();
|
||||||
@@ -4151,10 +4280,6 @@ mod tests {
|
|||||||
assert_eq!(editor.leader_pending(), Some(pending.as_str()));
|
assert_eq!(editor.leader_pending(), Some(pending.as_str()));
|
||||||
}
|
}
|
||||||
editor.open_file_picker(FilePresentation::Docked);
|
editor.open_file_picker(FilePresentation::Docked);
|
||||||
assert_eq!(
|
|
||||||
editor.file_navigation_hint(),
|
|
||||||
"Ctrl+n/Down next · Up previous · Ctrl+h/Ctrl+j/Left parent/close · Ctrl+l/Right/x open/child"
|
|
||||||
);
|
|
||||||
editor.handle(Input::Char('x'));
|
editor.handle(Input::Char('x'));
|
||||||
assert_eq!(editor.file_picker().unwrap().selected, 0);
|
assert_eq!(editor.file_picker().unwrap().selected, 0);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -4173,17 +4298,13 @@ mod tests {
|
|||||||
editor.handle(Input::Ctrl('j'));
|
editor.handle(Input::Ctrl('j'));
|
||||||
assert_eq!(editor.file_picker().unwrap().selected, 0);
|
assert_eq!(editor.file_picker().unwrap().selected, 0);
|
||||||
editor.handle(Input::Char('i'));
|
editor.handle(Input::Char('i'));
|
||||||
assert_eq!(
|
|
||||||
editor.file_navigation_hint(),
|
|
||||||
editor.selection_navigation_hint()
|
|
||||||
);
|
|
||||||
editor.handle(Input::Char('x'));
|
editor.handle(Input::Char('x'));
|
||||||
assert_eq!(editor.file_picker().unwrap().selected, 1);
|
assert_eq!(editor.file_picker().unwrap().selected, 1);
|
||||||
assert_eq!(editor.file_picker().unwrap().query, "");
|
assert_eq!(editor.file_picker().unwrap().query, "");
|
||||||
editor.handle(Input::Ctrl('j'));
|
editor.handle(Input::Ctrl('j'));
|
||||||
assert_eq!(editor.file_picker().unwrap().selected, 2);
|
assert_eq!(editor.file_picker().unwrap().selected, 0);
|
||||||
editor.handle(Input::Ctrl('h'));
|
editor.handle(Input::Ctrl('h'));
|
||||||
assert_eq!(editor.file_picker().unwrap().selected, 2);
|
assert_eq!(editor.file_picker().unwrap().selected, 0);
|
||||||
editor.handle(Input::Ctrl('t'));
|
editor.handle(Input::Ctrl('t'));
|
||||||
assert_eq!(editor.file_picker().unwrap().view, FileView::List);
|
assert_eq!(editor.file_picker().unwrap().view, FileView::List);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -507,6 +507,18 @@ impl FilePicker {
|
|||||||
let Some(row) = self.rows.get(self.selected) else {
|
let Some(row) = self.rows.get(self.selected) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if self.searching {
|
||||||
|
if self
|
||||||
|
.rows
|
||||||
|
.get(self.selected + 1)
|
||||||
|
.is_some_and(|child| child.depth > row.depth)
|
||||||
|
{
|
||||||
|
self.selected += 1;
|
||||||
|
}
|
||||||
|
self.filtered_selection[self.view.index()] = self.selected_node();
|
||||||
|
self.clear_error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if !self.nodes[row.node].is_dir {
|
if !self.nodes[row.node].is_dir {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -532,6 +544,17 @@ impl FilePicker {
|
|||||||
let Some(row) = self.rows.get(self.selected) else {
|
let Some(row) = self.rows.get(self.selected) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if self.searching {
|
||||||
|
if let Some(parent) = row.tree.and_then(|tree| self.tree[tree].parent) {
|
||||||
|
let node = self.tree[parent].node;
|
||||||
|
if let Some(index) = self.rows.iter().position(|row| row.node == node) {
|
||||||
|
self.selected = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.filtered_selection[self.view.index()] = self.selected_node();
|
||||||
|
self.clear_error();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if row.expanded {
|
if row.expanded {
|
||||||
let node = row.node;
|
let node = row.node;
|
||||||
let tree = row.tree.expect("directory navigation is tree-only");
|
let tree = row.tree.expect("directory navigation is tree-only");
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ All modes: Ctrl+p command palette; type to filter, Ctrl+j/k or arrows select, En
|
|||||||
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 list; Space e docked compact tree (right by default)
|
||||||
Ctrl+j/k select; tree Ctrl+l/h expand/parent, 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
|
||||||
h/j/k/l move, 0/$ line ends, x delete, J join lines
|
h/j/k/l move, 0/$ line ends, x delete, J join lines
|
||||||
|
|||||||
@@ -1323,10 +1323,17 @@ mod tests {
|
|||||||
editor.handle(Input::Ctrl('h'));
|
editor.handle(Input::Ctrl('h'));
|
||||||
assert_eq!(editor.file_picker().unwrap().query, "src/inner/needle.txt");
|
assert_eq!(editor.file_picker().unwrap().query, "src/inner/needle.txt");
|
||||||
let picker = editor.file_picker().unwrap();
|
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!(
|
assert_eq!(
|
||||||
picker.entries().nth(picker.selected).unwrap().path,
|
picker.entries().nth(picker.selected).unwrap().path,
|
||||||
std::path::Path::new("src/inner/needle.txt")
|
std::path::Path::new("src/inner/needle.txt")
|
||||||
);
|
);
|
||||||
|
assert_eq!(picker.query, "src/inner/needle.txt");
|
||||||
editor.handle(Input::Ctrl('j'));
|
editor.handle(Input::Ctrl('j'));
|
||||||
terminal
|
terminal
|
||||||
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
.draw(|frame| draw(frame, &editor, &mut top, &mut left))
|
||||||
|
|||||||
Reference in New Issue
Block a user