From 7ec7e36a53fc20dc80e9b53a50790550f264f53d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Mon, 11 Sep 2023 18:49:26 +0800 Subject: [PATCH] feat: find (#104) --- Cargo.lock | 12 +-- app/src/app.rs | 8 +- app/src/executor.rs | 10 ++- app/src/manager/folder.rs | 61 ++++++++++--- 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 | 14 ++- config/src/keymap/control.rs | 7 +- config/src/keymap/exec.rs | 23 +++++ core/src/event.rs | 8 +- core/src/files/files.rs | 34 ++++--- core/src/files/op.rs | 8 +- core/src/manager/finder.rs | 130 +++++++++++++++++++++++++++ core/src/manager/folder.rs | 2 +- core/src/manager/mod.rs | 2 + core/src/manager/preview/preview.rs | 4 +- core/src/manager/preview/provider.rs | 5 +- core/src/manager/tab.rs | 64 +++++++++++-- core/src/which/which.rs | 4 +- shared/src/debounce.rs | 62 +++++++++++++ shared/src/lib.rs | 2 + 23 files changed, 403 insertions(+), 64 deletions(-) create mode 100644 core/src/manager/finder.rs create mode 100644 shared/src/debounce.rs diff --git a/Cargo.lock b/Cargo.lock index 79fe8430f..308784635 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,9 +177,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "bincode" @@ -1548,9 +1548,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2" dependencies = [ "itoa", "ryu", @@ -1880,9 +1880,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0a3ab2091e52d7299a39d098e200114a972df0a7724add02a273aa9aada592" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "indexmap 2.0.0", "serde", 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..68bafe865 100644 --- a/app/src/manager/folder.rs +++ b/app/src/manager/folder.rs @@ -1,7 +1,7 @@ use core::files::File; use config::{MANAGER, THEME}; -use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::{List, ListItem, Widget}}; +use ratatui::{buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{List, ListItem, Widget}}; use shared::short_path; use crate::Ctx; @@ -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.url(), 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; @@ -53,17 +72,10 @@ impl<'a> Widget for Folder<'a> { self.folder.window() }; - let items = window + let items: Vec<_> = window .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()); if (!self.is_selection && is_selected) || (self.is_selection && mode.pending(self.folder.offset() + i, is_selected)) @@ -87,16 +99,37 @@ 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(short_path(f.url(), &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(|&f| hovered && self.is_find && f.has_matched()) + .and_then(|finder| finder.matched_idx(f.url())) + { + 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::>(); + .collect(); List::new(items).render(area, buf); } 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..13c20b0f4 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)" }, @@ -106,7 +112,7 @@ keymap = [ { on = [ "g", "" ], exec = "cd --interactive", desc = "Go to a directory interactively" }, # Help - { on = [ "?" ], exec = "help", desc = "Open help" }, + { on = [ "~" ], exec = "help", desc = "Open help" }, ] [tasks] @@ -125,7 +131,7 @@ keymap = [ { on = [ "" ], exec = "inspect", desc = "Inspect the task" }, { on = [ "x" ], exec = "cancel", desc = "Cancel the task" }, - { on = [ "?" ], exec = "help", desc = "Open help" } + { on = [ "~" ], exec = "help", desc = "Open help" } ] [select] @@ -144,7 +150,7 @@ keymap = [ { on = [ "" ], exec = "arrow -1", desc = "Move cursor up" }, { on = [ "" ], exec = "arrow 1", desc = "Move cursor down" }, - { on = [ "?" ], exec = "help", desc = "Open help" } + { on = [ "~" ], exec = "help", desc = "Open help" } ] [input] @@ -190,7 +196,7 @@ keymap = [ { on = [ "" ], exec = "redo", desc = "Redo the last operation" }, # Help - { on = [ "?" ], exec = "help", desc = "Open help" } + { on = [ "~" ], exec = "help", desc = "Open help" } ] [help] 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..207088a56 100644 --- a/config/src/keymap/exec.rs +++ b/config/src/keymap/exec.rs @@ -86,3 +86,26 @@ impl Exec { deserializer.deserialize_any(ExecVisitor) } } + +impl Exec { + #[inline] + pub fn call(cwd: &str, args: Vec) -> Self { + Exec { cmd: cwd.to_owned(), args, named: Default::default() } + } + + #[inline] + pub fn call_named(cwd: &str, named: BTreeMap) -> Self { + Exec { cmd: cwd.to_owned(), args: Default::default(), named } + } + + #[inline] + pub fn vec(self) -> Vec { vec![self] } + + #[inline] + pub fn with_bool(mut self, name: &str, state: bool) -> Self { + if state { + self.named.insert(name.to_string(), "".to_string()); + } + self + } +} diff --git a/core/src/event.rs b/core/src/event.rs index 3460d2038..54063426a 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::{InputError, RoCell, Url}; use tokio::sync::{mpsc::{self, 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/files/files.rs b/core/src/files/files.rs index f17df4b41..03b5bd902 100644 --- a/core/src/files/files.rs +++ b/core/src/files/files.rs @@ -5,11 +5,12 @@ use config::manager::SortBy; use shared::Url; use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}}; -use super::{File, FilesSorter, FILES_VERSION}; +use super::{File, FilesSorter, FILES_TICKET}; pub struct Files { items: Vec, hidden: Vec, + ticket: u64, version: u64, sizes: BTreeMap, @@ -24,6 +25,7 @@ impl Default for Files { Self { items: Default::default(), hidden: Default::default(), + ticket: Default::default(), version: Default::default(), sizes: Default::default(), @@ -126,15 +128,16 @@ impl Files { if !self.show_hidden { (self.hidden, items) = items.into_iter().partition(|f| f.is_hidden); } + self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); self.sorter.sort(&mut items, &self.sizes); self.items = items; - self.version = FILES_VERSION.fetch_add(1, Ordering::Relaxed); + self.version += 1; true } pub fn update_part(&mut self, version: u64, items: Vec) -> bool { if !items.is_empty() { - if version != self.version { + if version != self.ticket { return false; } @@ -147,22 +150,26 @@ impl Files { } self.sorter.sort(&mut self.items, &self.sizes); + self.version += 1; return true; } - self.version = version; - if !self.items.is_empty() { - self.items.clear(); - self.hidden.clear(); - return true; + self.ticket = version; + if self.items.is_empty() && self.hidden.is_empty() { + return false; } - false + + self.items.clear(); + self.hidden.clear(); + self.version += 1; + true } pub fn update_size(&mut self, items: BTreeMap) -> bool { self.sizes.extend(items); if self.sorter.by == SortBy::Size { self.sorter.sort(&mut self.items, &self.sizes); + self.version += 1; } true } @@ -186,7 +193,11 @@ impl Files { #[inline] pub fn duplicate(&self, idx: usize) -> Option { self.items.get(idx).cloned() } - // --- Size + // --- Version + #[inline] + pub fn version(&self) -> u64 { self.version } + + // --- Sizes #[inline] pub fn size(&self, url: &Url) -> Option { self.sizes.get(url).copied() } @@ -230,6 +241,7 @@ impl Files { } // --- Sorter + #[inline] pub fn sorter(&self) -> &FilesSorter { &self.sorter } #[inline] @@ -238,6 +250,7 @@ impl Files { return false; } self.sorter = sorter; + self.version += 1; self.sorter.sort(&mut self.items, &self.sizes) } @@ -259,6 +272,7 @@ impl Files { } self.show_hidden = state; + self.version += 1; true } } diff --git a/core/src/files/op.rs b/core/src/files/op.rs index 346d880a6..92523434c 100644 --- a/core/src/files/op.rs +++ b/core/src/files/op.rs @@ -5,7 +5,7 @@ use shared::Url; use super::File; use crate::emit; -pub(super) static FILES_VERSION: AtomicU64 = AtomicU64::new(0); +pub(super) static FILES_TICKET: AtomicU64 = AtomicU64::new(0); #[derive(Debug)] pub enum FilesOp { @@ -28,8 +28,8 @@ impl FilesOp { #[inline] pub fn prepare(url: &Url) -> u64 { - let version = FILES_VERSION.fetch_add(1, Ordering::Relaxed); - emit!(Files(Self::Part(url.clone(), version, Vec::new()))); - version + let ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed); + emit!(Files(Self::Part(url.clone(), ticket, Vec::new()))); + ticket } } diff --git a/core/src/manager/finder.rs b/core/src/manager/finder.rs new file mode 100644 index 000000000..01a6b85a3 --- /dev/null +++ b/core/src/manager/finder.rs @@ -0,0 +1,130 @@ +use std::{collections::BTreeMap, ffi::OsStr}; + +use anyhow::Result; +use regex::bytes::Regex; +use shared::Url; + +use crate::files::Files; + +pub struct Finder { + query: Regex, + matched: BTreeMap, + version: u64, +} + +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.matches(n))) + .map(|(i, _)| -(i as isize) - 1) + } else { + files + .iter() + .skip(cursor + 1) + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.matches(n))) + .map(|(i, _)| i as isize + 1) + } + } + + pub(super) fn ring(&self, files: &Files, cursor: usize, prev: bool) -> Option { + if prev { + files + .iter() + .take(cursor + 1) + .rev() + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.matches(n))) + .map(|(i, _)| -(i as isize)) + .or_else(|| { + files + .iter() + .skip(cursor + 1) + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.matches(n))) + .map(|(i, _)| i as isize + 1) + }) + } else { + files + .iter() + .skip(cursor) + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.matches(n))) + .map(|(i, _)| i as isize) + .or_else(|| { + files + .iter() + .take(cursor) + .rev() + .enumerate() + .find(|(_, f)| f.name().map_or(false, |n| self.matches(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.matches(n)) != Some(true) { + continue; + } + + self.matched.insert(file.url_owned(), i); + if self.matched.len() > 99 { + break; + } + + i += 1; + } + + self.version = files.version(); + true + } + + #[inline] + fn matches(&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 has_matched(&self) -> bool { !self.matched.is_empty() } + + #[inline] + pub fn matched_idx(&self, url: &Url) -> Option { + if let Some((_, &idx)) = self.matched.iter().find(|(u, _)| *u == url) { + return Some(idx); + } + if url.file_name().map(|n| self.matches(n)) == Some(true) { + return Some(100); + } + None + } +} diff --git a/core/src/manager/folder.rs b/core/src/manager/folder.rs index 3f7654777..9d1a832ef 100644 --- a/core/src/manager/folder.rs +++ b/core/src/manager/folder.rs @@ -28,7 +28,7 @@ impl Folder { pub fn update(&mut self, op: FilesOp) -> bool { let b = match op { FilesOp::Full(_, items) => self.files.update_full(items), - FilesOp::Part(_, version, items) => self.files.update_part(version, items), + FilesOp::Part(_, ticket, items) => self.files.update_part(ticket, items), FilesOp::Size(_, items) => self.files.update_size(items), _ => unreachable!(), }; 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/manager/preview/preview.rs b/core/src/manager/preview/preview.rs index 7f0bc4bc7..1ff069468 100644 --- a/core/src/manager/preview/preview.rs +++ b/core/src/manager/preview/preview.rs @@ -97,9 +97,9 @@ impl Preview { let rx = UnboundedReceiverStream::new(rx).chunks_timeout(10000, Duration::from_millis(500)); pin!(rx); - let version = FilesOp::prepare(&url); + let ticket = FilesOp::prepare(&url); while let Some(chunk) = rx.next().await { - emit!(Files(FilesOp::Part(url.clone(), version, chunk))); + emit!(Files(FilesOp::Part(url.clone(), ticket, chunk))); } })); } diff --git a/core/src/manager/preview/provider.rs b/core/src/manager/preview/provider.rs index 45680eae5..cf54a127f 100644 --- a/core/src/manager/preview/provider.rs +++ b/core/src/manager/preview/provider.rs @@ -7,7 +7,6 @@ use shared::{MimeKind, PeekError}; use syntect::{easy::HighlightFile, util::as_24_bit_terminal_escaped}; use tokio::fs; - use super::PreviewData; use crate::{external, highlighter}; @@ -85,7 +84,7 @@ impl Provider { } pub(super) async fn highlight(path: &Path, skip: usize) -> Result { - let tick = INCR.load(Ordering::Relaxed); + let ticket = INCR.load(Ordering::Relaxed); let path = path.to_path_buf(); let spaces = " ".repeat(PREVIEW.tab_size as usize); @@ -98,7 +97,7 @@ impl Provider { let mut i = 0; let limit = MANAGER.layout.preview_height(); while h.reader.read_line(&mut line)? > 0 { - if tick != INCR.load(Ordering::Relaxed) { + if ticket != INCR.load(Ordering::Relaxed) { return Err("Highlighting cancelled".into()); } diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index cdf30220d..a41ab3c51 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -1,12 +1,12 @@ use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, time::Duration}; use anyhow::{bail, Error, Result}; -use config::open::Opener; -use shared::{Defer, Url}; +use config::{keymap::{Exec, KeymapLayer}, open::Opener}; +use shared::{Debounce, Defer, InputError, Url}; use tokio::{pin, task::JoinHandle}; use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt}; -use super::{Folder, Mode, Preview, PreviewLock}; +use super::{Finder, Folder, Mode, Preview, PreviewLock}; use crate::{emit, external::{self, FzfOpt, ZoxideOpt}, files::{File, FilesOp, FilesSorter}, input::InputOpt, Event, BLOCKER}; pub struct Tab { @@ -17,6 +17,7 @@ pub struct Tab { pub(super) history: BTreeMap, pub(super) preview: Preview, + finder: Option, search: Option>>, pub(super) show_hidden: bool, } @@ -33,6 +34,7 @@ impl From for Tab { history: Default::default(), preview: Default::default(), + finder: None, search: None, show_hidden: true, } @@ -45,6 +47,11 @@ impl From<&Url> for Tab { impl Tab { pub fn escape(&mut self) -> bool { + if self.finder.is_some() { + self.finder = None; + return true; + } + if let Some((_, indices)) = self.mode.visual() { self.current.files.select_index(indices, Some(self.mode.is_select())); self.mode = Mode::Normal; @@ -232,6 +239,49 @@ impl Tab { false } + pub fn find(&mut self, query: Option<&str>, prev: bool) -> bool { + if let Some(query) = query { + let Ok(finder) = Finder::new(query) else { + return false; + }; + + if let Some(step) = finder.ring(&self.current.files, self.current.cursor(), prev) { + self.arrow(step); + } + + self.finder = Some(finder); + return true; + } + + tokio::spawn(async move { + let rx = emit!(Input(InputOpt::top("Find:").with_realtime())); + + let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50)); + pin!(rx); + + while let Some(Ok(s)) | Some(Err(InputError::Typed(s))) = rx.next().await { + emit!(Call( + Exec::call("find", vec![s]).with_bool("previous", prev).vec(), + KeymapLayer::Manager + )); + } + }); + false + } + + pub fn find_arrow(&mut self, prev: bool) -> bool { + let Some(finder) = &mut self.finder else { + return false; + }; + + let mut b = finder.catchup(&self.current.files); + if let Some(step) = finder.arrow(&self.current.files, self.current.cursor(), prev) { + b |= self.arrow(step); + } + + b + } + pub fn search(&mut self, grep: bool) -> bool { if let Some(handle) = self.search.take() { handle.abort(); @@ -254,14 +304,14 @@ impl Tab { let rx = UnboundedReceiverStream::new(rx).chunks_timeout(1000, Duration::from_millis(300)); pin!(rx); - let version = FilesOp::prepare(&cwd); + let ticket = FilesOp::prepare(&cwd); let mut first = true; while let Some(chunk) = rx.next().await { if first { emit!(Cd(cwd.clone())); first = false; } - emit!(Files(FilesOp::Part(cwd.clone(), version, chunk))); + emit!(Files(FilesOp::Part(cwd.clone(), ticket, chunk))); } Ok(()) })); @@ -409,6 +459,10 @@ impl Tab { #[inline] pub fn preview_arrow(&mut self, step: isize) -> bool { self.preview.arrow(step) } + // --- Finder + #[inline] + pub fn finder(&self) -> Option<&Finder> { self.finder.as_ref() } + // --- Sorter pub fn set_sorter(&mut self, sorter: FilesSorter) -> bool { if !self.current.files.set_sorter(sorter) { 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; diff --git a/shared/src/debounce.rs b/shared/src/debounce.rs new file mode 100644 index 000000000..dce640a26 --- /dev/null +++ b/shared/src/debounce.rs @@ -0,0 +1,62 @@ +use std::{pin::Pin, task::{Context, Poll}, time::Duration}; + +use futures::{FutureExt, Stream, StreamExt}; +use tokio::time::{sleep, Instant, Sleep}; + +pub struct Debounce +where + S: Stream, +{ + stream: S, + interval: Duration, + + sleep: Sleep, + last: Option, +} + +impl Debounce +where + S: Stream + Unpin, +{ + pub fn new(stream: S, interval: Duration) -> Debounce { + Self { stream, interval, sleep: sleep(Duration::ZERO), last: None } + } +} + +impl Stream for Debounce +where + S: Stream + Unpin, +{ + type Item = S::Item; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let (mut stream, interval, mut sleep, last) = unsafe { + let me = self.get_unchecked_mut(); + (Pin::new(&mut me.stream), me.interval, Pin::new_unchecked(&mut me.sleep), &mut me.last) + }; + + if sleep.poll_unpin(cx).is_ready() { + if let Some(last) = last.take() { + return Poll::Ready(Some(last)); + } + } + + while let Poll::Ready(next) = stream.poll_next_unpin(cx) { + match next { + Some(next) => { + *last = Some(next); + } + None if last.is_none() => { + return Poll::Ready(None); + } + None => { + sleep.reset(Instant::now()); + return Poll::Pending; + } + } + } + + sleep.reset(Instant::now() + interval); + Poll::Pending + } +} diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 3b12b26f3..189b2c4fc 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -1,6 +1,7 @@ #![allow(clippy::option_map_unit_fn)] mod chars; +mod debounce; mod defer; mod errors; mod fns; @@ -13,6 +14,7 @@ mod time; mod url; pub use chars::*; +pub use debounce::*; pub use defer::*; pub use errors::*; pub use fns::*;