find files via fuzzy finder (#890)

This commit is contained in:
Stephan Dilly 2021-09-04 10:50:03 +02:00 committed by GitHub
parent 3b5d43ecb2
commit fb2b990072
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 694 additions and 36 deletions

19
Cargo.lock generated
View file

@ -410,6 +410,15 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fuzzy-matcher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94"
dependencies = [
"thread_local",
]
[[package]]
name = "getrandom"
version = "0.2.3"
@ -491,6 +500,7 @@ dependencies = [
"dirs-next",
"easy-cast",
"filetreelist",
"fuzzy-matcher",
"gh-emoji",
"itertools",
"lazy_static",
@ -1422,6 +1432,15 @@ dependencies = [
"syn",
]
[[package]]
name = "thread_local"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.1.43"

View file

@ -48,6 +48,7 @@ bugreport = "0.4"
lazy_static = "1.4"
syntect = { version = "4.5", default-features = false, features = ["metadata", "default-fancy"]}
gh-emoji = "1.0.6"
fuzzy-matcher = "0.3"
[target.'cfg(all(target_family="unix",not(target_os="macos")))'.dependencies]
which = "4.1"

View file

@ -142,6 +142,25 @@ impl FileTree {
})
}
pub fn select_file(&mut self, path: &Path) -> bool {
let new_selection = self
.items
.tree_items
.iter()
.position(|item| item.info().full_path() == path);
if new_selection == self.selection {
return false;
}
self.selection = new_selection;
if let Some(selection) = self.selection {
self.items.show_element(selection);
}
self.visual_selection = self.calc_visual_selection();
true
}
fn visual_index_to_absolute(
&self,
visual_index: usize,

View file

@ -148,7 +148,7 @@ impl FileTreeItems {
if item_path.starts_with(&path) {
item.hide();
} else {
return;
break;
}
}
}
@ -188,6 +188,90 @@ impl FileTreeItems {
}
}
/// makes sure `index` is visible.
/// this expands all parents and shows all siblings
pub fn show_element(&mut self, index: usize) -> Option<usize> {
Some(
self.show_element_upward(index)?
+ self.show_element_downward(index)?,
)
}
fn show_element_upward(&mut self, index: usize) -> Option<usize> {
let mut shown = 0_usize;
let item = self.tree_items.get(index)?;
let mut current_folder: (PathBuf, u8) = (
item.info().full_path().parent()?.to_path_buf(),
item.info().indent(),
);
let item_count = self.tree_items.len();
for item in self
.tree_items
.iter_mut()
.rev()
.skip(item_count - index - 1)
{
if item.info().indent() == current_folder.1 {
item.show();
shown += 1;
} else if item.info().indent() == current_folder.1 - 1 {
// this must be our parent
item.expand_path();
if item.info().is_visible() {
// early out if parent already visible
break;
}
item.show();
shown += 1;
current_folder = (
item.info().full_path().parent()?.to_path_buf(),
item.info().indent(),
);
}
}
Some(shown)
}
fn show_element_downward(
&mut self,
index: usize,
) -> Option<usize> {
let mut shown = 0_usize;
let item = self.tree_items.get(index)?;
let mut current_folder: (PathBuf, u8) = (
item.info().full_path().parent()?.to_path_buf(),
item.info().indent(),
);
for item in self.tree_items.iter_mut().skip(index + 1) {
if item.info().indent() == current_folder.1 {
item.show();
shown += 1;
}
if item.info().indent() == current_folder.1 - 1 {
// this must be our parent
item.show();
shown += 1;
current_folder = (
item.info().full_path().parent()?.to_path_buf(),
item.info().indent(),
);
}
}
Some(shown)
}
fn update_visibility(
&mut self,
prefix: &Option<PathBuf>,
@ -687,6 +771,152 @@ mod tests {
]
);
}
#[test]
fn test_show_element() {
let items = vec![
Path::new("a/b/c"), //
Path::new("a/b2/d"), //
Path::new("a/b2/e"), //
];
//0 a/
//1 b/
//2 c
//3 b2/
//4 d
//5 e
let mut tree =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
tree.collapse(0, true);
let res = tree.show_element(5).unwrap();
assert_eq!(res, 4);
assert!(tree.tree_items[3].kind().is_path());
assert!(!tree.tree_items[3].kind().is_path_collapsed());
assert_eq!(
get_visibles(&tree),
vec![
true, //
true, //
false, //
true, //
true, //
true,
]
);
}
#[test]
fn test_show_element_later_elements() {
let items = vec![
Path::new("a/b"), //
Path::new("a/c"), //
];
//0 a/
//1 b
//2 c
let mut tree =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
tree.collapse(0, true);
assert_eq!(
get_visibles(&tree),
vec![
true, //
false, //
false, //
]
);
let res = tree.show_element(1).unwrap();
assert_eq!(res, 2);
assert_eq!(
get_visibles(&tree),
vec![
true, //
true, //
true, //
]
);
}
#[test]
fn test_show_element_downward_parent() {
let items = vec![
Path::new("a/b/c"), //
Path::new("a/d"), //
Path::new("a/e"), //
];
//0 a/
//1 b/
//2 c
//3 d
//4 e
let mut tree =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
tree.collapse(0, true);
let res = tree.show_element(2).unwrap();
assert_eq!(res, 4);
assert_eq!(
get_visibles(&tree),
vec![
true, //
true, //
true, //
true, //
true, //
]
);
}
#[test]
fn test_show_element_expand_visible_parent() {
let items = vec![
Path::new("a/b"), //
];
//0 a/
//1 b
let mut tree =
FileTreeItems::new(&items, &BTreeSet::new()).unwrap();
tree.collapse(0, true);
assert_eq!(
get_visibles(&tree),
vec![
true, //
false, //
]
);
let res = tree.show_element(1).unwrap();
assert_eq!(res, 1);
assert!(tree.tree_items[0].kind().is_path());
assert!(!tree.tree_items[0].kind().is_path_collapsed());
assert_eq!(
get_visibles(&tree),
vec![
true, //
true, //
]
);
}
}
#[cfg(test)]

View file

@ -164,13 +164,17 @@ impl FileTreeItem {
&self.kind
}
///
/// # Panics
/// panics if self is not a path
pub fn collapse_path(&mut self) {
assert!(self.kind.is_path());
self.kind = FileTreeItemKind::Path(PathCollapsed(true));
}
///
/// # Panics
/// panics if self is not a path
pub fn expand_path(&mut self) {
assert!(self.kind.is_path());
self.kind = FileTreeItemKind::Path(PathCollapsed(false));
}
@ -178,6 +182,11 @@ impl FileTreeItem {
pub fn hide(&mut self) {
self.info.visible = false;
}
///
pub fn show(&mut self) {
self.info.visible = true;
}
}
impl Eq for FileTreeItem {}

View file

@ -6,7 +6,7 @@ use crate::{
BranchListComponent, CommandBlocking, CommandInfo,
CommitComponent, CompareCommitsComponent, Component,
ConfirmComponent, CreateBranchComponent, DrawableComponent,
ExternalEditorComponent, HelpComponent,
ExternalEditorComponent, FileFindComponent, HelpComponent,
InspectCommitComponent, MsgComponent, OptionsPopupComponent,
PullComponent, PushComponent, PushTagsComponent,
RenameBranchComponent, RevisionFilesPopup, SharedOptions,
@ -51,6 +51,7 @@ pub struct App {
compare_commits_popup: CompareCommitsComponent,
external_editor_popup: ExternalEditorComponent,
revision_files_popup: RevisionFilesPopup,
find_file_popup: FileFindComponent,
push_popup: PushComponent,
push_tags_popup: PushTagsComponent,
pull_popup: PullComponent,
@ -189,6 +190,11 @@ impl App {
key_config.clone(),
options.clone(),
),
find_file_popup: FileFindComponent::new(
&queue,
theme.clone(),
key_config.clone(),
),
do_quit: false,
cmdbar: RefCell::new(CommandBar::new(
theme.clone(),
@ -448,6 +454,7 @@ impl App {
rename_branch_popup,
select_branch_popup,
revision_files_popup,
find_file_popup,
tags_popup,
options_popup,
help,
@ -475,6 +482,7 @@ impl App {
create_branch_popup,
rename_branch_popup,
revision_files_popup,
find_file_popup,
push_popup,
push_tags_popup,
pull_popup,
@ -693,6 +701,11 @@ impl App {
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OpenFileFinder(files) => {
self.find_file_popup.open(&files)?;
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::OptionSwitched(o) => {
match o {
AppOption::StatusShowUntracked => {
@ -712,6 +725,11 @@ impl App {
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
InternalEvent::FileFinderChanged(file) => {
self.files_tab.file_finder_update(file);
flags
.insert(NeedsUpdate::ALL | NeedsUpdate::COMMANDS);
}
};
Ok(flags)

View file

@ -3,11 +3,10 @@ use super::{
Direction, DrawableComponent, ScrollType,
};
use crate::{
components::{
tabs_to_spaces, CommandInfo, Component, EventState,
},
components::{CommandInfo, Component, EventState},
keys::SharedKeyConfig,
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
string_utils::tabs_to_spaces,
strings, try_or_popup,
ui::style::SharedTheme,
};

271
src/components/file_find.rs Normal file
View file

@ -0,0 +1,271 @@
use super::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState, TextInputComponent,
};
use crate::{
keys::SharedKeyConfig,
queue::{InternalEvent, Queue},
string_utils::trim_length_left,
strings,
ui::{self, style::SharedTheme},
};
use anyhow::Result;
use asyncgit::sync::TreeFile;
use crossterm::event::Event;
use fuzzy_matcher::FuzzyMatcher;
use std::borrow::Cow;
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Margin, Rect},
text::Span,
widgets::{Block, Borders, Clear},
Frame,
};
pub struct FileFindComponent {
queue: Queue,
visible: bool,
find_text: TextInputComponent,
query: Option<String>,
theme: SharedTheme,
files: Vec<TreeFile>,
selection: Option<usize>,
files_filtered: Vec<usize>,
key_config: SharedKeyConfig,
}
impl FileFindComponent {
///
pub fn new(
queue: &Queue,
theme: SharedTheme,
key_config: SharedKeyConfig,
) -> Self {
let mut find_text = TextInputComponent::new(
theme.clone(),
key_config.clone(),
"",
"start typing..",
false,
);
find_text.embed();
Self {
queue: queue.clone(),
visible: false,
query: None,
find_text,
theme,
files: Vec::new(),
files_filtered: Vec::new(),
key_config,
selection: None,
}
}
fn update_query(&mut self) {
if self.find_text.get_text().is_empty() {
self.set_query(None);
} else if self
.query
.as_ref()
.map_or(true, |q| q != self.find_text.get_text())
{
self.set_query(Some(
self.find_text.get_text().to_string(),
));
}
}
fn set_query(&mut self, query: Option<String>) {
self.query = query;
self.files_filtered.clear();
if let Some(q) = &self.query {
let matcher =
fuzzy_matcher::skim::SkimMatcherV2::default();
self.files_filtered.extend(
self.files.iter().enumerate().filter_map(|a| {
a.1.path.to_str().and_then(|path| {
//TODO: use fuzzy_indices and highlight hits
matcher.fuzzy_match(path, q).map(|_| a.0)
})
}),
);
self.refresh_selection();
} else {
self.files_filtered
.extend(self.files.iter().enumerate().map(|a| a.0));
}
}
fn refresh_selection(&mut self) {
let selection = self.files_filtered.first().copied();
if self.selection != selection {
self.selection = selection;
let file = self
.selection
.and_then(|index| self.files.get(index))
.map(|f| f.path.clone());
self.queue.push(InternalEvent::FileFinderChanged(file));
}
}
pub fn open(&mut self, files: &[TreeFile]) -> Result<()> {
self.show()?;
self.find_text.show()?;
self.find_text.set_text(String::new());
self.query = None;
if self.files != *files {
self.files = files.to_owned();
}
self.update_query();
Ok(())
}
}
impl DrawableComponent for FileFindComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
area: Rect,
) -> Result<()> {
if self.is_visible() {
const SIZE: (u16, u16) = (50, 25);
let area =
ui::centered_rect_absolute(SIZE.0, SIZE.1, area);
f.render_widget(Clear, area);
f.render_widget(
Block::default()
.borders(Borders::all())
.style(self.theme.title(true))
.title(Span::styled(
//TODO: strings
"Fuzzy find",
self.theme.title(true),
)),
area,
);
let area = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(1),
Constraint::Percentage(100),
]
.as_ref(),
)
.split(area.inner(&Margin {
horizontal: 1,
vertical: 1,
}));
self.find_text.draw(f, area[0])?;
let height = usize::from(area[1].height);
let width = usize::from(area[1].width);
let items =
self.files_filtered.iter().take(height).map(|idx| {
let selected = self
.selection
.map_or(false, |selection| selection == *idx);
Span::styled(
Cow::from(trim_length_left(
self.files[*idx]
.path
.to_str()
.unwrap_or_default(),
width,
)),
self.theme.text(selected, false),
)
});
let title = format!(
"Hits: {}/{}",
height.min(self.files_filtered.len()),
self.files_filtered.len()
);
ui::draw_list_block(
f,
area[1],
Block::default()
.title(Span::styled(
title,
self.theme.title(true),
))
.borders(Borders::TOP),
items,
);
}
Ok(())
}
}
impl Component for FileFindComponent {
fn commands(
&self,
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if self.is_visible() || force_all {
out.push(
CommandInfo::new(
strings::commands::close_popup(&self.key_config),
true,
true,
)
.order(1),
);
}
visibility_blocking(self)
}
fn event(
&mut self,
event: crossterm::event::Event,
) -> Result<EventState> {
if self.is_visible() {
if let Event::Key(key) = &event {
if *key == self.key_config.exit_popup
|| *key == self.key_config.enter
{
self.hide();
}
}
if self.find_text.event(event)?.is_consumed() {
self.update_query();
}
return Ok(EventState::Consumed);
}
Ok(EventState::NotConsumed)
}
fn is_visible(&self) -> bool {
self.visible
}
fn hide(&mut self) {
self.visible = false;
}
fn show(&mut self) -> Result<()> {
self.visible = true;
Ok(())
}
}

View file

@ -10,6 +10,7 @@ mod create_branch;
mod cred;
mod diff;
mod externaleditor;
mod file_find;
mod filetree;
mod help;
mod inspect_commit;
@ -41,6 +42,7 @@ pub use compare_commits::CompareCommitsComponent;
pub use create_branch::CreateBranchComponent;
pub use diff::DiffComponent;
pub use externaleditor::ExternalEditorComponent;
pub use file_find::FileFindComponent;
pub use help::HelpComponent;
pub use inspect_commit::InspectCommitComponent;
pub use msg::MsgComponent;
@ -297,27 +299,24 @@ fn popup_paragraph<'a, T>(
content: T,
theme: &Theme,
focused: bool,
block: bool,
) -> Paragraph<'a>
where
T: Into<Text<'a>>,
{
Paragraph::new(content.into())
.block(
let paragraph = Paragraph::new(content.into())
.alignment(Alignment::Left)
.wrap(Wrap { trim: true });
if block {
paragraph.block(
Block::default()
.title(Span::styled(title, theme.title(focused)))
.borders(Borders::ALL)
.border_type(BorderType::Thick)
.border_style(theme.block(focused)),
)
.alignment(Alignment::Left)
.wrap(Wrap { trim: true })
}
//TODO: allow customize tabsize
pub fn tabs_to_spaces(input: String) -> String {
if input.contains('\t') {
input.replace("\t", " ")
} else {
input
paragraph
}
}

View file

@ -41,7 +41,7 @@ impl DrawableComponent for ConfirmComponent {
let area = ui::centered_rect(50, 20, f.size());
f.render_widget(Clear, area);
f.render_widget(
popup_paragraph(&title, txt, &self.theme, true),
popup_paragraph(&title, txt, &self.theme, true, true),
area,
);
}

View file

@ -18,7 +18,11 @@ use asyncgit::{
use crossbeam_channel::Sender;
use crossterm::event::Event;
use filetreelist::{FileTree, FileTreeItem};
use std::{collections::BTreeSet, convert::From, path::Path};
use std::{
collections::BTreeSet,
convert::From,
path::{Path, PathBuf},
};
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
@ -137,6 +141,20 @@ impl RevisionFilesComponent {
})
}
fn open_finder(&self) {
self.queue
.push(InternalEvent::OpenFileFinder(self.files.clone()));
}
pub fn find_file(&mut self, file: Option<PathBuf>) {
if let Some(file) = file {
self.tree.collapse_but_root();
if self.tree.select_file(&file) {
self.selection_changed();
}
}
}
fn selection_changed(&mut self) {
//TODO: retrieve TreeFile from tree datastructure
if let Some(file) = self
@ -144,6 +162,7 @@ impl RevisionFilesComponent {
.selected_file()
.map(|file| file.full_path_str().to_string())
{
log::info!("selected: {:?}", file);
let path = Path::new(&file);
if let Some(item) =
self.files.iter().find(|f| f.path == path)
@ -188,7 +207,7 @@ impl RevisionFilesComponent {
"Files at [{}]",
self.revision
.map(|c| c.get_short_string())
.unwrap_or_default()
.unwrap_or_default(),
);
ui::draw_list_block(
f,
@ -241,7 +260,9 @@ impl Component for RevisionFilesComponent {
out: &mut Vec<CommandInfo>,
force_all: bool,
) -> CommandBlocking {
if matches!(self.focus, Focus::Tree) || force_all {
let is_tree_focused = matches!(self.focus, Focus::Tree);
if is_tree_focused || force_all {
out.push(
CommandInfo::new(
strings::commands::blame_file(&self.key_config),
@ -288,6 +309,11 @@ impl Component for RevisionFilesComponent {
self.focus(false);
return Ok(EventState::Consumed);
}
} else if key == self.key_config.file_find {
if is_tree_focused {
self.open_finder();
return Ok(EventState::Consumed);
}
} else if !is_tree_focused {
return self.current_file.event(event);
}

View file

@ -1,9 +1,10 @@
use super::{
tabs_to_spaces, CommandBlocking, CommandInfo, Component,
DrawableComponent, EventState,
CommandBlocking, CommandInfo, Component, DrawableComponent,
EventState,
};
use crate::{
keys::SharedKeyConfig,
string_utils::tabs_to_spaces,
strings,
ui::{
self, common_nav, style::SharedTheme, AsyncSyntaxJob,

View file

@ -41,6 +41,7 @@ pub struct TextInputComponent {
cursor_position: usize,
input_type: InputType,
current_area: Cell<Rect>,
embed: bool,
}
impl TextInputComponent {
@ -63,6 +64,7 @@ impl TextInputComponent {
cursor_position: 0,
input_type: InputType::Multiline,
current_area: Cell::new(Rect::default()),
embed: false,
}
}
@ -90,6 +92,11 @@ impl TextInputComponent {
self.current_area.get()
}
/// embed into parent draw area
pub fn embed(&mut self) {
self.embed = true;
}
/// Move the cursor right one char.
fn incr_cursor(&mut self) {
if let Some(pos) = self.next_char_position() {
@ -267,7 +274,7 @@ impl DrawableComponent for TextInputComponent {
fn draw<B: Backend>(
&self,
f: &mut Frame<B>,
_rect: Rect,
rect: Rect,
) -> Result<()> {
if self.visible {
let txt = if self.msg.is_empty() {
@ -279,16 +286,21 @@ impl DrawableComponent for TextInputComponent {
self.get_draw_text()
};
let area = match self.input_type {
InputType::Multiline => {
let area = ui::centered_rect(60, 20, f.size());
ui::rect_inside(
Size::new(10, 3),
f.size().into(),
area,
)
let area = if self.embed {
rect
} else {
match self.input_type {
InputType::Multiline => {
let area =
ui::centered_rect(60, 20, f.size());
ui::rect_inside(
Size::new(10, 3),
f.size().into(),
area,
)
}
_ => ui::centered_rect_absolute(32, 3, f.size()),
}
_ => ui::centered_rect_absolute(32, 3, f.size()),
};
f.render_widget(Clear, area);
@ -298,6 +310,7 @@ impl DrawableComponent for TextInputComponent {
txt,
&self.theme,
true,
!self.embed,
),
area,
);

View file

@ -83,6 +83,7 @@ pub struct KeyConfig {
pub select_tag: KeyEvent,
pub push: KeyEvent,
pub open_file_tree: KeyEvent,
pub file_find: KeyEvent,
pub force_push: KeyEvent,
pub pull: KeyEvent,
pub abort_merge: KeyEvent,
@ -159,6 +160,7 @@ impl Default for KeyConfig {
pull: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
abort_merge: KeyEvent { code: KeyCode::Char('M'), modifiers: KeyModifiers::SHIFT},
open_file_tree: KeyEvent { code: KeyCode::Char('F'), modifiers: KeyModifiers::SHIFT},
file_find: KeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty()},
}
}
}

View file

@ -30,6 +30,7 @@ mod notify_mutex;
mod profiler;
mod queue;
mod spinner;
mod string_utils;
mod strings;
mod tabs;
mod ui;

View file

@ -1,7 +1,11 @@
use crate::{components::AppOption, tabs::StashingOptions};
use asyncgit::sync::{diff::DiffLinePosition, CommitId, CommitTags};
use asyncgit::sync::{
diff::DiffLinePosition, CommitId, CommitTags, TreeFile,
};
use bitflags::bitflags;
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use std::{
cell::RefCell, collections::VecDeque, path::PathBuf, rc::Rc,
};
bitflags! {
/// flags defining what part of the app need to update
@ -87,6 +91,10 @@ pub enum InternalEvent {
OpenFileTree(CommitId),
///
OptionSwitched(AppOption),
///
OpenFileFinder(Vec<TreeFile>),
///
FileFinderChanged(Option<PathBuf>),
}
/// single threaded simple queue for components to communicate with each other

35
src/string_utils.rs Normal file
View file

@ -0,0 +1,35 @@
///
pub fn trim_length_left(s: &str, width: usize) -> &str {
let len = s.len();
if len > width {
for i in len - width..len {
if s.is_char_boundary(i) {
return &s[i..];
}
}
}
s
}
//TODO: allow customize tabsize
pub fn tabs_to_spaces(input: String) -> String {
if input.contains('\t') {
input.replace("\t", " ")
} else {
input
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use crate::string_utils::trim_length_left;
#[test]
fn test_trim() {
assert_eq!(trim_length_left("👍foo", 3), "foo");
assert_eq!(trim_length_left("👍foo", 4), "foo");
}
}

View file

@ -4,6 +4,8 @@
clippy::unused_self
)]
use std::path::PathBuf;
use crate::{
components::{
visibility_blocking, CommandBlocking, CommandInfo, Component,
@ -68,6 +70,10 @@ impl FilesTab {
self.files.update(ev);
}
}
pub fn file_finder_update(&mut self, file: Option<PathBuf>) {
self.files.find_file(file);
}
}
impl DrawableComponent for FilesTab {

View file

@ -98,6 +98,7 @@
pull: ( code: Char('f'), modifiers: ( bits: 0,),),
open_file_tree: ( code: Char('F'), modifiers: ( bits: 1,),),
file_find: ( code: Char('f'), modifiers: ( bits: 0,),),
//removed in 0.11
//tab_toggle_reverse_windows: ( code: BackTab, modifiers: ( bits: 1,),),