From ccafcdd1879ca6d599062b3bae6edccbc60b7a5f Mon Sep 17 00:00:00 2001 From: sxyazi Date: Mon, 4 Sep 2023 01:34:07 +0800 Subject: [PATCH] feat: find --- app/src/app.rs | 8 ++-- app/src/executor.rs | 10 +++- app/src/manager/folder.rs | 57 +++++++++++++++++----- app/src/manager/layout.rs | 1 + app/src/status/left.rs | 2 +- app/src/status/right.rs | 2 +- app/src/which/side.rs | 2 +- config/preset/keymap.toml | 6 +++ config/src/keymap/control.rs | 7 ++- config/src/keymap/exec.rs | 12 +++++ core/src/event.rs | 8 ++-- core/src/manager/finder.rs | 91 ++++++++++++++++++++++++++++++++++++ core/src/manager/mod.rs | 2 + core/src/which/which.rs | 4 +- 14 files changed, 185 insertions(+), 27 deletions(-) create mode 100644 core/src/manager/finder.rs diff --git a/app/src/app.rs b/app/src/app.rs index d513509df..761d09171 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -2,7 +2,7 @@ use core::{emit, files::FilesOp, input::InputMode, Event}; use std::ffi::OsString; use anyhow::{Ok, Result}; -use config::{keymap::{Control, Key, KeymapLayer}, BOOT}; +use config::{keymap::{Exec, Key, KeymapLayer}, BOOT}; use crossterm::event::KeyEvent; use shared::{expand_url, Term}; use tokio::sync::oneshot; @@ -34,7 +34,7 @@ impl App { Event::Render(_) => app.dispatch_render(), Event::Resize(..) => app.dispatch_resize(), Event::Stop(state, tx) => app.dispatch_stop(state, tx), - Event::Ctrl(ctrl, layer) => app.dispatch_ctrl(ctrl, layer), + Event::Call(exec, layer) => app.dispatch_call(exec, layer), event => app.dispatch_module(event), } } @@ -109,8 +109,8 @@ impl App { } #[inline] - fn dispatch_ctrl(&mut self, ctrl: Control, layer: KeymapLayer) { - if Executor::dispatch(&mut self.cx, &ctrl.exec, layer) { + fn dispatch_call(&mut self, exec: Vec, layer: KeymapLayer) { + if Executor::dispatch(&mut self.cx, &exec, layer) { emit!(Render); } } diff --git a/app/src/executor.rs b/app/src/executor.rs index a7399b9f4..6c7e41ae2 100644 --- a/app/src/executor.rs +++ b/app/src/executor.rs @@ -37,7 +37,7 @@ impl Executor { } #[inline] - pub(super) fn dispatch(cx: &mut Ctx, exec: &Vec, layer: KeymapLayer) -> bool { + pub(super) fn dispatch(cx: &mut Ctx, exec: &[Exec], layer: KeymapLayer) -> bool { let mut render = false; for e in exec { render |= match layer { @@ -138,6 +138,14 @@ impl Executor { _ => false, }, + // Find + "find" => { + let query = exec.args.get(0).map(|s| s.as_str()); + let prev = exec.named.contains_key("previous"); + cx.manager.active_mut().find(query, prev) + } + "find_arrow" => cx.manager.active_mut().find_arrow(exec.named.contains_key("previous")), + // Sorting "sort" => { let b = cx.manager.active_mut().set_sorter(FilesSorter { diff --git a/app/src/manager/folder.rs b/app/src/manager/folder.rs index 582f93245..9be38b690 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -11,11 +11,12 @@ pub(super) struct Folder<'a> { folder: &'a core::manager::Folder, is_preview: bool, is_selection: bool, + is_find: bool, } impl<'a> Folder<'a> { pub(super) fn new(cx: &'a Ctx, folder: &'a core::manager::Folder) -> Self { - Self { cx, folder, is_preview: false, is_selection: false } + Self { cx, folder, is_preview: false, is_selection: false, is_find: false } } #[inline] @@ -30,6 +31,24 @@ impl<'a> Folder<'a> { self } + #[inline] + pub(super) fn with_find(mut self, state: bool) -> Self { + self.is_find = state; + self + } +} + +impl<'a> Folder<'a> { + #[inline] + fn icon(file: &File) -> &'static str { + THEME + .icons + .iter() + .find(|x| x.name.match_path(file.path(), Some(file.is_dir()))) + .map(|x| x.display.as_ref()) + .unwrap_or("") + } + #[inline] fn file_style(&self, file: &File) -> Style { let mimetype = &self.cx.manager.mimetype; @@ -57,14 +76,7 @@ impl<'a> Widget for Folder<'a> { .iter() .enumerate() .map(|(i, f)| { - let icon = THEME - .icons - .iter() - .find(|x| x.name.match_path(f.url(), Some(f.is_dir()))) - .map(|x| x.display.as_ref()) - .unwrap_or(""); - - let is_selected = self.folder.files.is_selected(f.url()); + let is_selected = self.folder.files.is_selected(f.path()); if (!self.is_selection && is_selected) || (self.is_selection && mode.pending(self.folder.offset() + i, is_selected)) { @@ -87,14 +99,35 @@ impl<'a> Widget for Folder<'a> { self.file_style(f) }; - let mut path = format!(" {icon} {}", short_path(f.url(), &self.folder.cwd)); + let mut spans = Vec::with_capacity(10); + + spans.push(Span::raw(format!(" {} ", Self::icon(f)))); + spans.push(Span::raw(readable_path(f.path(), &self.folder.cwd))); + if let Some(link_to) = f.link_to() { if MANAGER.show_symlink { - path.push_str(&format!(" -> {}", link_to.display())); + spans.push(Span::raw(format!(" -> {}", link_to.display()))); } } - ListItem::new(path).style(style) + if let Some(idx) = active + .finder() + .filter(|_| hovered && self.is_find) + .and_then(|finder| finder.matched_idx(f.path())) + { + let len = active.finder().unwrap().matched().len(); + let style = Style::new().fg(Color::Rgb(255, 255, 50)).add_modifier(Modifier::ITALIC); + spans.push(Span::styled( + format!( + " [{}/{}]", + if idx > 99 { ">99".to_string() } else { (idx + 1).to_string() }, + if len > 99 { ">99".to_string() } else { len.to_string() } + ), + style, + )); + } + + ListItem::new(Line::from(spans)).style(style) }) .collect::>(); diff --git a/app/src/manager/layout.rs b/app/src/manager/layout.rs index 2bb888177..8aeb6f134 100644 --- a/app/src/manager/layout.rs +++ b/app/src/manager/layout.rs @@ -39,6 +39,7 @@ impl<'a> Widget for Layout<'a> { // Current Folder::new(self.cx, manager.current()) .with_selection(manager.active().mode().is_visual()) + .with_find(manager.active().finder().is_some()) .render(chunks[1], buf); // Preview diff --git a/app/src/status/left.rs b/app/src/status/left.rs index 6ab724fbf..10f84a939 100644 --- a/app/src/status/left.rs +++ b/app/src/status/left.rs @@ -26,7 +26,7 @@ impl<'a> Widget for Left<'a> { let separator = &THEME.status.separator; // Mode - let mut spans = vec![]; + let mut spans = Vec::with_capacity(5); spans.push(Span::styled(&separator.opening, primary.fg())); spans.push(Span::styled( format!(" {mode} "), diff --git a/app/src/status/right.rs b/app/src/status/right.rs index a0425f7b5..45e00da00 100644 --- a/app/src/status/right.rs +++ b/app/src/status/right.rs @@ -65,7 +65,7 @@ impl<'a> Right<'a> { impl Widget for Right<'_> { fn render(self, area: Rect, buf: &mut Buffer) { let manager = self.cx.manager.current(); - let mut spans = vec![]; + let mut spans = Vec::with_capacity(20); // Permissions #[cfg(not(target_os = "windows"))] diff --git a/app/src/which/side.rs b/app/src/which/side.rs index a0c10a69d..da14f5c20 100644 --- a/app/src/which/side.rs +++ b/app/src/which/side.rs @@ -16,7 +16,7 @@ impl Widget for Side<'_> { .cands .into_iter() .map(|c| { - let mut spans = vec![]; + let mut spans = Vec::with_capacity(10); // Keys let keys = c.on[self.times..].iter().map(ToString::to_string).collect::>(); diff --git a/config/preset/keymap.toml b/config/preset/keymap.toml index e1736b177..83743427c 100644 --- a/config/preset/keymap.toml +++ b/config/preset/keymap.toml @@ -64,6 +64,12 @@ keymap = [ { on = [ "c", "f" ], exec = "copy filename", desc = "Copy the name of the file" }, { on = [ "c", "n" ], exec = "copy name_without_ext", desc = "Copy the name of the file without the extension" }, + # Find + { on = [ "/" ], exec = "find" }, + { on = [ "?" ], exec = "find --previous" }, + { on = [ "-" ], exec = "find_arrow" }, + { on = [ "=" ], exec = "find_arrow --previous" }, + # Sorting { on = [ ",", "a" ], exec = "sort alphabetical --dir_first", desc = "Sort alphabetically, directories first" }, { on = [ ",", "A" ], exec = "sort alphabetical --reverse --dir_first", desc = "Sort alphabetically, directories first (reverse)" }, diff --git a/config/src/keymap/control.rs b/config/src/keymap/control.rs index afe50ca5a..65b707071 100644 --- a/config/src/keymap/control.rs +++ b/config/src/keymap/control.rs @@ -12,6 +12,11 @@ pub struct Control { pub desc: Option, } +impl Control { + #[inline] + pub fn to_call(&self) -> Vec { self.exec.clone() } +} + impl Control { #[inline] pub fn on(&self) -> String { self.on.iter().map(ToString::to_string).collect() } @@ -28,7 +33,7 @@ impl Control { #[inline] pub fn contains(&self, s: &str) -> bool { - self.desc.as_ref().map(|d| d.contains(s)).unwrap_or(false) + self.desc.as_ref().map(|d| d.contains(s)) == Some(true) || self.exec().contains(s) || self.on().contains(s) } diff --git a/config/src/keymap/exec.rs b/config/src/keymap/exec.rs index 3180567f0..f946ae5df 100644 --- a/config/src/keymap/exec.rs +++ b/config/src/keymap/exec.rs @@ -86,3 +86,15 @@ impl Exec { deserializer.deserialize_any(ExecVisitor) } } + +impl Exec { + #[inline] + pub fn call(cwd: &str, args: Vec) -> Vec { + vec![Exec { cmd: cwd.to_owned(), args, named: Default::default() }] + } + + #[inline] + pub fn call_named(cwd: &str, named: BTreeMap) -> Vec { + vec![Exec { cmd: cwd.to_owned(), args: Default::default(), named }] + } +} diff --git a/core/src/event.rs b/core/src/event.rs index 1b7236c74..9d50a473f 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -1,7 +1,7 @@ use std::{collections::BTreeMap, ffi::OsString}; use anyhow::Result; -use config::{keymap::{Control, KeymapLayer}, open::Opener}; +use config::{keymap::{Exec, KeymapLayer}, open::Opener}; use crossterm::event::KeyEvent; use shared::{RoCell, Url}; use tokio::sync::{mpsc::UnboundedSender, oneshot}; @@ -18,7 +18,7 @@ pub enum Event { Render(String), Resize(u16, u16), Stop(bool, Option>), - Ctrl(Control, KeymapLayer), + Call(Vec, KeymapLayer), // Manager Cd(Url), @@ -67,8 +67,8 @@ macro_rules! emit { let (tx, rx) = tokio::sync::oneshot::channel(); $crate::Event::Stop($state, Some(tx)).wait(rx) }}; - (Ctrl($exec:expr, $layer:expr)) => { - $crate::Event::Ctrl($exec, $layer).emit(); + (Call($exec:expr, $layer:expr)) => { + $crate::Event::Call($exec, $layer).emit(); }; (Cd($url:expr)) => { diff --git a/core/src/manager/finder.rs b/core/src/manager/finder.rs new file mode 100644 index 000000000..cc47ec8cf --- /dev/null +++ b/core/src/manager/finder.rs @@ -0,0 +1,91 @@ +use std::{collections::BTreeMap, ffi::OsStr, path::{Path, PathBuf}}; + +use anyhow::Result; +use regex::bytes::Regex; + +use crate::files::Files; + +pub struct Finder { + query: Regex, + matched: BTreeMap, + version: usize, +} + +impl Finder { + pub(super) fn new(s: &str) -> Result { + Ok(Self { query: Regex::new(s)?, matched: Default::default(), version: 0 }) + } + + pub(super) fn arrow(&self, files: &Files, cursor: usize, prev: bool) -> Option { + if prev { + files + .iter() + .take(cursor) + .rev() + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.is_match(n))) + .map(|(i, _)| -(i as isize) - 1) + } else { + files + .iter() + .skip(cursor + 1) + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.is_match(n))) + .map(|(i, _)| i as isize + 1) + } + } + + pub(super) fn catchup(&mut self, files: &Files) -> bool { + if self.version == files.version() { + return false; + } + + self.matched.clear(); + + let mut i = 0u8; + for file in files.iter() { + if file.name().map(|n| self.is_match(n)) != Some(true) { + continue; + } + + self.matched.insert(file.path_owned(), i); + if self.matched.len() > 99 { + break; + } + + i += 1; + } + + self.version = files.version(); + true + } + + #[inline] + fn is_match(&self, name: &OsStr) -> bool { + #[cfg(target_os = "windows")] + { + self.query.is_match(name.to_string_lossy().as_bytes()) + } + #[cfg(not(target_os = "windows"))] + { + use std::os::unix::ffi::OsStrExt; + self.query.is_match(name.as_bytes()) + } + } +} + +impl Finder { + #[inline] + pub fn matched(&self) -> &BTreeMap { &self.matched } + + #[inline] + pub fn matched_idx(&self, path: &Path) -> Option { + if let Some((_, &idx)) = self.matched.iter().find(|(p, _)| *p == path) { + return Some(idx); + } + if path.file_name().map(|n| self.is_match(n)) == Some(true) { + return Some(100); + } + None + } +} diff --git a/core/src/manager/mod.rs b/core/src/manager/mod.rs index 751ed8779..afa145490 100644 --- a/core/src/manager/mod.rs +++ b/core/src/manager/mod.rs @@ -1,3 +1,4 @@ +mod finder; mod folder; mod manager; mod mode; @@ -6,6 +7,7 @@ mod tab; mod tabs; mod watcher; +pub use finder::*; pub use folder::*; pub use manager::*; pub use mode::*; diff --git a/core/src/which/which.rs b/core/src/which/which.rs index 21dd4a3a5..36ec00528 100644 --- a/core/src/which/which.rs +++ b/core/src/which/which.rs @@ -38,10 +38,10 @@ impl Which { self.switch(false); } else if self.cands.len() == 1 { self.switch(false); - emit!(Ctrl(self.cands.remove(0), self.layer)); + emit!(Call(self.cands[0].to_call(), self.layer)); } else if let Some(i) = self.cands.iter().position(|c| c.on.len() == self.times + 1) { self.switch(false); - emit!(Ctrl(self.cands.remove(i), self.layer)); + emit!(Call(self.cands[i].to_call(), self.layer)); } self.times += 1;