mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
feat: filter files in real-time (#454)
This commit is contained in:
parent
82bab0f24a
commit
d2599b80b0
15 changed files with 260 additions and 82 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{env, path::Path, sync::{atomic::Ordering, Arc}};
|
use std::{env, path::Path, sync::Arc};
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use ratatui::prelude::Rect;
|
use ratatui::prelude::Rect;
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,9 @@ keymap = [
|
||||||
{ on = [ "c", "f" ], exec = "copy filename", desc = "Copy the name of the file" },
|
{ 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" },
|
{ on = [ "c", "n" ], exec = "copy name_without_ext", desc = "Copy the name of the file without the extension" },
|
||||||
|
|
||||||
|
# Filter
|
||||||
|
{ on = [ "f" ], exec = "filter" },
|
||||||
|
|
||||||
# Find
|
# Find
|
||||||
{ on = [ "/" ], exec = "find --smart" },
|
{ on = [ "/" ], exec = "find --smart" },
|
||||||
{ on = [ "?" ], exec = "find --previous --smart" },
|
{ on = [ "?" ], exec = "find --previous --smart" },
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,11 @@ delete_title = "Delete {n} selected file{s} permanently? (y/N)"
|
||||||
delete_origin = "top-center"
|
delete_origin = "top-center"
|
||||||
delete_offset = [ 0, 2, 50, 3 ]
|
delete_offset = [ 0, 2, 50, 3 ]
|
||||||
|
|
||||||
|
# filter
|
||||||
|
filter_title = "Filter:"
|
||||||
|
filter_origin = "top-center"
|
||||||
|
filter_offset = [ 0, 2, 50, 3 ]
|
||||||
|
|
||||||
# find
|
# find
|
||||||
find_title = [ "Find next:", "Find previous:" ]
|
find_title = [ "Find next:", "Find previous:" ]
|
||||||
find_origin = "top-center"
|
find_origin = "top-center"
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ pub struct Input {
|
||||||
pub delete_origin: Origin,
|
pub delete_origin: Origin,
|
||||||
pub delete_offset: Offset,
|
pub delete_offset: Offset,
|
||||||
|
|
||||||
|
// filter
|
||||||
|
pub filter_title: String,
|
||||||
|
pub filter_origin: Origin,
|
||||||
|
pub filter_offset: Offset,
|
||||||
|
|
||||||
// find
|
// find
|
||||||
pub find_title: [String; 2],
|
pub find_title: [String; 2],
|
||||||
pub find_origin: Origin,
|
pub find_origin: Origin,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,16 @@ impl InputCfg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn filter() -> Self {
|
||||||
|
Self {
|
||||||
|
title: INPUT.filter_title.to_owned(),
|
||||||
|
position: Position::new(INPUT.filter_origin, INPUT.filter_offset),
|
||||||
|
realtime: true,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn find(prev: bool) -> Self {
|
pub fn find(prev: bool) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use tokio::{fs, select, sync::mpsc::{self, UnboundedReceiver}};
|
||||||
use yazi_config::{manager::SortBy, MANAGER};
|
use yazi_config::{manager::SortBy, MANAGER};
|
||||||
use yazi_shared::fs::{File, Url, FILES_TICKET};
|
use yazi_shared::fs::{File, Url, FILES_TICKET};
|
||||||
|
|
||||||
use super::FilesSorter;
|
use super::{FilesSorter, Filter};
|
||||||
|
|
||||||
pub struct Files {
|
pub struct Files {
|
||||||
hidden: Vec<File>,
|
hidden: Vec<File>,
|
||||||
|
|
@ -18,6 +18,7 @@ pub struct Files {
|
||||||
selected: BTreeSet<Url>,
|
selected: BTreeSet<Url>,
|
||||||
|
|
||||||
sorter: FilesSorter,
|
sorter: FilesSorter,
|
||||||
|
filter: Option<Filter>,
|
||||||
show_hidden: bool,
|
show_hidden: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,6 +35,7 @@ impl Default for Files {
|
||||||
selected: Default::default(),
|
selected: Default::default(),
|
||||||
|
|
||||||
sorter: Default::default(),
|
sorter: Default::default(),
|
||||||
|
filter: Default::default(),
|
||||||
show_hidden: MANAGER.show_hidden,
|
show_hidden: MANAGER.show_hidden,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,11 +138,7 @@ impl Files {
|
||||||
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
|
self.ticket = FILES_TICKET.fetch_add(1, Ordering::Relaxed);
|
||||||
self.revision += 1;
|
self.revision += 1;
|
||||||
|
|
||||||
(self.hidden, self.items) = if self.show_hidden {
|
(self.hidden, self.items) = self.split_files(files);
|
||||||
(vec![], files)
|
|
||||||
} else {
|
|
||||||
files.into_iter().partition(|f| f.is_hidden())
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_part(&mut self, files: Vec<File>, ticket: u64) {
|
pub fn update_part(&mut self, files: Vec<File>, ticket: u64) {
|
||||||
|
|
@ -150,14 +148,9 @@ impl Files {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.revision += 1;
|
self.revision += 1;
|
||||||
if self.show_hidden {
|
let (hidden, items) = self.split_files(files);
|
||||||
self.hidden.clear();
|
self.hidden.extend(hidden);
|
||||||
self.items.extend(files);
|
self.items.extend(items);
|
||||||
} else {
|
|
||||||
let (hidden, items): (Vec<_>, Vec<_>) = files.into_iter().partition(|f| f.is_hidden());
|
|
||||||
self.hidden.extend(hidden);
|
|
||||||
self.items.extend(items);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -200,12 +193,7 @@ impl Files {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let (hidden, items) = if self.show_hidden {
|
let (hidden, items) = self.split_files(files);
|
||||||
(vec![], files)
|
|
||||||
} else {
|
|
||||||
files.into_iter().partition(|f| f.is_hidden())
|
|
||||||
};
|
|
||||||
|
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
go!(self.items, items);
|
go!(self.items, items);
|
||||||
}
|
}
|
||||||
|
|
@ -231,8 +219,15 @@ impl Files {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let (hidden, items) =
|
let (hidden, items) = if let Some(filter) = &self.filter {
|
||||||
if self.show_hidden { (vec![], urls) } else { urls.into_iter().partition(|u| u.is_hidden()) };
|
urls.into_iter().partition(|u| {
|
||||||
|
(!self.show_hidden && u.is_hidden()) || !u.file_name().is_some_and(|s| filter.matches(s))
|
||||||
|
})
|
||||||
|
} else if self.show_hidden {
|
||||||
|
(vec![], urls)
|
||||||
|
} else {
|
||||||
|
urls.into_iter().partition(|u| u.is_hidden())
|
||||||
|
};
|
||||||
|
|
||||||
if !items.is_empty() {
|
if !items.is_empty() {
|
||||||
go!(self.items, items);
|
go!(self.items, items);
|
||||||
|
|
@ -242,7 +237,10 @@ impl Files {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_updating(&mut self, files: BTreeMap<Url, File>) -> [BTreeMap<Url, File>; 2] {
|
pub fn update_updating(
|
||||||
|
&mut self,
|
||||||
|
files: BTreeMap<Url, File>,
|
||||||
|
) -> (BTreeMap<Url, File>, BTreeMap<Url, File>) {
|
||||||
if files.is_empty() {
|
if files.is_empty() {
|
||||||
return Default::default();
|
return Default::default();
|
||||||
}
|
}
|
||||||
|
|
@ -264,7 +262,12 @@ impl Files {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut hidden, mut items) = if self.show_hidden {
|
let (mut hidden, mut items) = if let Some(filter) = &self.filter {
|
||||||
|
files.into_iter().partition(|(_, f)| {
|
||||||
|
(f.is_hidden() && !self.show_hidden)
|
||||||
|
|| !f.url.file_name().is_some_and(|s| filter.matches(s))
|
||||||
|
})
|
||||||
|
} else if self.show_hidden {
|
||||||
(BTreeMap::new(), files)
|
(BTreeMap::new(), files)
|
||||||
} else {
|
} else {
|
||||||
files.into_iter().partition(|(_, f)| f.is_hidden())
|
files.into_iter().partition(|(_, f)| f.is_hidden())
|
||||||
|
|
@ -276,7 +279,7 @@ impl Files {
|
||||||
if !hidden.is_empty() {
|
if !hidden.is_empty() {
|
||||||
go!(self.hidden, hidden);
|
go!(self.hidden, hidden);
|
||||||
}
|
}
|
||||||
[hidden, items]
|
(hidden, items)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_upserting(&mut self, files: BTreeMap<Url, File>) {
|
pub fn update_upserting(&mut self, files: BTreeMap<Url, File>) {
|
||||||
|
|
@ -284,7 +287,7 @@ impl Files {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let [hidden, items] = self.update_updating(files);
|
let (hidden, items) = self.update_updating(files);
|
||||||
if hidden.is_empty() && items.is_empty() {
|
if hidden.is_empty() && items.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +310,19 @@ impl Files {
|
||||||
self.sorter.sort(&mut self.items, &self.sizes);
|
self.sorter.sort(&mut self.items, &self.sizes);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn split_files(&self, files: impl IntoIterator<Item = File>) -> (Vec<File>, Vec<File>) {
|
||||||
|
if let Some(filter) = &self.filter {
|
||||||
|
files.into_iter().partition(|f| {
|
||||||
|
(f.is_hidden() && !self.show_hidden)
|
||||||
|
|| !f.url.file_name().is_some_and(|s| filter.matches(s))
|
||||||
|
})
|
||||||
|
} else if self.show_hidden {
|
||||||
|
(vec![], files.into_iter().collect())
|
||||||
|
} else {
|
||||||
|
files.into_iter().partition(|f| f.is_hidden())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Files {
|
impl Files {
|
||||||
|
|
@ -374,6 +390,31 @@ impl Files {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Filter
|
||||||
|
pub fn set_filter(&mut self, filter: Option<Filter>) -> bool {
|
||||||
|
if self.filter == filter {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.filter = filter;
|
||||||
|
if self.filter.is_none() {
|
||||||
|
let take = mem::take(&mut self.hidden);
|
||||||
|
let (hidden, items) = self.split_files(take);
|
||||||
|
|
||||||
|
self.hidden = hidden;
|
||||||
|
if !items.is_empty() {
|
||||||
|
self.items.extend(items);
|
||||||
|
self.sorter.sort(&mut self.items, &self.sizes);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let it = mem::take(&mut self.items).into_iter().chain(mem::take(&mut self.hidden));
|
||||||
|
(self.hidden, self.items) = self.split_files(it);
|
||||||
|
self.sorter.sort(&mut self.items, &self.sizes);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
// --- Show hidden
|
// --- Show hidden
|
||||||
pub fn set_show_hidden(&mut self, state: bool) {
|
pub fn set_show_hidden(&mut self, state: bool) {
|
||||||
if self.show_hidden == state {
|
if self.show_hidden == state {
|
||||||
|
|
@ -387,12 +428,12 @@ impl Files {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let take =
|
||||||
|
if self.show_hidden { mem::take(&mut self.hidden) } else { mem::take(&mut self.items) };
|
||||||
|
let (hidden, items) = self.split_files(take);
|
||||||
|
|
||||||
self.revision += 1;
|
self.revision += 1;
|
||||||
if self.show_hidden {
|
self.hidden.extend(hidden);
|
||||||
self.items.append(&mut self.hidden);
|
self.items.extend(items);
|
||||||
} else {
|
|
||||||
let items = mem::take(&mut self.items);
|
|
||||||
(self.hidden, self.items) = items.into_iter().partition(|f| f.is_hidden());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
yazi-core/src/folder/filter.rs
Normal file
54
yazi-core/src/folder/filter.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
use std::{ffi::OsStr, ops::Range};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use regex::bytes::{Regex, RegexBuilder};
|
||||||
|
use yazi_shared::event::Exec;
|
||||||
|
|
||||||
|
pub struct Filter {
|
||||||
|
raw: String,
|
||||||
|
regex: Regex,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Filter {
|
||||||
|
fn eq(&self, other: &Self) -> bool { self.raw == other.raw }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
pub fn new(s: &str, case: FilterCase) -> Result<Self> {
|
||||||
|
let regex = match case {
|
||||||
|
FilterCase::Smart => {
|
||||||
|
let uppercase = s.chars().any(|c| c.is_uppercase());
|
||||||
|
RegexBuilder::new(s).case_insensitive(!uppercase).build()?
|
||||||
|
}
|
||||||
|
FilterCase::Sensitive => Regex::new(s)?,
|
||||||
|
FilterCase::Insensitive => RegexBuilder::new(s).case_insensitive(true).build()?,
|
||||||
|
};
|
||||||
|
Ok(Self { raw: s.to_owned(), regex })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn matches(&self, name: &OsStr) -> bool { self.regex.is_match(name.as_encoded_bytes()) }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn highlighted(&self, name: &OsStr) -> Option<Vec<Range<usize>>> {
|
||||||
|
self.regex.find(name.as_encoded_bytes()).map(|m| vec![m.range()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Eq)]
|
||||||
|
pub enum FilterCase {
|
||||||
|
Smart,
|
||||||
|
#[default]
|
||||||
|
Sensitive,
|
||||||
|
Insensitive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Exec> for FilterCase {
|
||||||
|
fn from(e: &Exec) -> Self {
|
||||||
|
match (e.named.contains_key("smart"), e.named.contains_key("insensitive")) {
|
||||||
|
(true, _) => Self::Smart,
|
||||||
|
(_, false) => Self::Sensitive,
|
||||||
|
(_, true) => Self::Insensitive,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
mod files;
|
mod files;
|
||||||
|
mod filter;
|
||||||
mod folder;
|
mod folder;
|
||||||
mod sorter;
|
mod sorter;
|
||||||
|
|
||||||
pub use files::*;
|
pub use files::*;
|
||||||
|
pub use filter::*;
|
||||||
pub use folder::*;
|
pub use folder::*;
|
||||||
pub use sorter::*;
|
pub use sorter::*;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ use crate::tab::{Mode, Tab};
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
pub struct Opt: u8 {
|
pub struct Opt: u8 {
|
||||||
const FIND = 0b0001;
|
const FIND = 0b00001;
|
||||||
const VISUAL = 0b0010;
|
const VISUAL = 0b00010;
|
||||||
const SELECT = 0b0100;
|
const SELECT = 0b00100;
|
||||||
const SEARCH = 0b1000;
|
const FILTER = 0b01000;
|
||||||
|
const SEARCH = 0b10000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,6 +20,7 @@ impl From<&Exec> for Opt {
|
||||||
b"find" => acc | Self::FIND,
|
b"find" => acc | Self::FIND,
|
||||||
b"visual" => acc | Self::VISUAL,
|
b"visual" => acc | Self::VISUAL,
|
||||||
b"select" => acc | Self::SELECT,
|
b"select" => acc | Self::SELECT,
|
||||||
|
b"filter" => acc | Self::FILTER,
|
||||||
b"search" => acc | Self::SEARCH,
|
b"search" => acc | Self::SEARCH,
|
||||||
_ => acc,
|
_ => acc,
|
||||||
})
|
})
|
||||||
|
|
@ -42,6 +44,11 @@ impl Tab {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn escape_select(&mut self) -> bool { self.select_all(Some(false)) }
|
fn escape_select(&mut self) -> bool { self.select_all(Some(false)) }
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn escape_filter(&mut self) -> bool {
|
||||||
|
self.filter_do(super::filter::Opt { query: "", ..Default::default() })
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn escape_search(&mut self) -> bool { self.search_stop() }
|
fn escape_search(&mut self) -> bool { self.search_stop() }
|
||||||
|
|
||||||
|
|
@ -51,6 +58,7 @@ impl Tab {
|
||||||
return self.escape_find()
|
return self.escape_find()
|
||||||
|| self.escape_visual()
|
|| self.escape_visual()
|
||||||
|| self.escape_select()
|
|| self.escape_select()
|
||||||
|
|| self.escape_filter()
|
||||||
|| self.escape_search();
|
|| self.escape_search();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +72,9 @@ impl Tab {
|
||||||
if opt.contains(Opt::SELECT) {
|
if opt.contains(Opt::SELECT) {
|
||||||
b |= self.escape_select();
|
b |= self.escape_select();
|
||||||
}
|
}
|
||||||
|
if opt.contains(Opt::FILTER) {
|
||||||
|
b |= self.escape_filter();
|
||||||
|
}
|
||||||
if opt.contains(Opt::SEARCH) {
|
if opt.contains(Opt::SEARCH) {
|
||||||
b |= self.escape_search();
|
b |= self.escape_search();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
65
yazi-core/src/tab/commands/filter.rs
Normal file
65
yazi-core/src/tab/commands/filter.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tokio::pin;
|
||||||
|
use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||||
|
use yazi_config::popup::InputCfg;
|
||||||
|
use yazi_shared::{emit, event::Exec, Debounce, InputError, Layer};
|
||||||
|
|
||||||
|
use crate::{folder::{Filter, FilterCase}, input::Input, manager::Manager, tab::Tab};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Opt<'a> {
|
||||||
|
pub query: &'a str,
|
||||||
|
pub case: FilterCase,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Exec> for Opt<'a> {
|
||||||
|
fn from(e: &'a Exec) -> Self {
|
||||||
|
Self { query: e.args.first().map(|s| s.as_str()).unwrap_or_default(), case: e.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tab {
|
||||||
|
pub fn filter<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool {
|
||||||
|
let opt = opt.into() as Opt;
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let rx = Input::_show(InputCfg::filter());
|
||||||
|
|
||||||
|
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("filter_do", vec![s])
|
||||||
|
.with_bool("smart", opt.case == FilterCase::Smart)
|
||||||
|
.with_bool("insensitive", opt.case == FilterCase::Insensitive)
|
||||||
|
.vec(),
|
||||||
|
Layer::Manager
|
||||||
|
));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn filter_do<'a>(&mut self, opt: impl Into<Opt<'a>>) -> bool {
|
||||||
|
let opt = opt.into() as Opt;
|
||||||
|
|
||||||
|
let filter = if opt.query.is_empty() {
|
||||||
|
None
|
||||||
|
} else if let Ok(f) = Filter::new(opt.query, opt.case) {
|
||||||
|
Some(f)
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let hovered = self.current.hovered().map(|f| f.url());
|
||||||
|
if !self.current.files.set_filter(filter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current.repos(hovered) {
|
||||||
|
Manager::_hover(None);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,12 +5,12 @@ use tokio_stream::{wrappers::UnboundedReceiverStream, StreamExt};
|
||||||
use yazi_config::popup::InputCfg;
|
use yazi_config::popup::InputCfg;
|
||||||
use yazi_shared::{emit, event::Exec, Debounce, InputError, Layer};
|
use yazi_shared::{emit, event::Exec, Debounce, InputError, Layer};
|
||||||
|
|
||||||
use crate::{input::Input, tab::{Finder, FinderCase, Tab}};
|
use crate::{folder::FilterCase, input::Input, tab::{Finder, Tab}};
|
||||||
|
|
||||||
pub struct Opt<'a> {
|
pub struct Opt<'a> {
|
||||||
query: Option<&'a str>,
|
query: Option<&'a str>,
|
||||||
prev: bool,
|
prev: bool,
|
||||||
case: FinderCase,
|
case: FilterCase,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> From<&'a Exec> for Opt<'a> {
|
impl<'a> From<&'a Exec> for Opt<'a> {
|
||||||
|
|
@ -18,11 +18,7 @@ impl<'a> From<&'a Exec> for Opt<'a> {
|
||||||
Self {
|
Self {
|
||||||
query: e.args.first().map(|s| s.as_str()),
|
query: e.args.first().map(|s| s.as_str()),
|
||||||
prev: e.named.contains_key("previous"),
|
prev: e.named.contains_key("previous"),
|
||||||
case: match (e.named.contains_key("smart"), e.named.contains_key("insensitive")) {
|
case: e.into(),
|
||||||
(true, _) => FinderCase::Smart,
|
|
||||||
(_, false) => FinderCase::Sensitive,
|
|
||||||
(_, true) => FinderCase::Insensitive,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -48,8 +44,8 @@ impl Tab {
|
||||||
emit!(Call(
|
emit!(Call(
|
||||||
Exec::call("find_do", vec![s])
|
Exec::call("find_do", vec![s])
|
||||||
.with_bool("previous", opt.prev)
|
.with_bool("previous", opt.prev)
|
||||||
.with_bool("smart", opt.case == FinderCase::Smart)
|
.with_bool("smart", opt.case == FilterCase::Smart)
|
||||||
.with_bool("insensitive", opt.case == FinderCase::Insensitive)
|
.with_bool("insensitive", opt.case == FilterCase::Insensitive)
|
||||||
.vec(),
|
.vec(),
|
||||||
Layer::Manager
|
Layer::Manager
|
||||||
));
|
));
|
||||||
|
|
@ -63,10 +59,16 @@ impl Tab {
|
||||||
let Some(query) = opt.query else {
|
let Some(query) = opt.query else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if query.is_empty() {
|
||||||
|
return self.escape(super::escape::Opt::FIND);
|
||||||
|
}
|
||||||
|
|
||||||
let Ok(finder) = Finder::new(query, opt.case) else {
|
let Ok(finder) = Finder::new(query, opt.case) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if matches!(&self.finder, Some(f) if f.filter == finder.filter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let step = if opt.prev {
|
let step = if opt.prev {
|
||||||
finder.prev(&self.current.files, self.current.cursor, true)
|
finder.prev(&self.current.files, self.current.cursor, true)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ mod cd;
|
||||||
mod copy;
|
mod copy;
|
||||||
mod enter;
|
mod enter;
|
||||||
mod escape;
|
mod escape;
|
||||||
|
mod filter;
|
||||||
mod find;
|
mod find;
|
||||||
mod hidden;
|
mod hidden;
|
||||||
mod jump;
|
mod jump;
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,25 @@
|
||||||
use std::{collections::BTreeMap, ffi::OsStr, ops::Range};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use regex::bytes::{Regex, RegexBuilder};
|
|
||||||
use yazi_shared::fs::Url;
|
use yazi_shared::fs::Url;
|
||||||
|
|
||||||
use crate::folder::Files;
|
use crate::folder::{Files, Filter, FilterCase};
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
|
||||||
pub enum FinderCase {
|
|
||||||
Smart,
|
|
||||||
Sensitive,
|
|
||||||
Insensitive,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Finder {
|
pub struct Finder {
|
||||||
query: Regex,
|
pub filter: Filter,
|
||||||
matched: BTreeMap<Url, u8>,
|
matched: BTreeMap<Url, u8>,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Finder {
|
impl Finder {
|
||||||
pub(super) fn new(s: &str, case: FinderCase) -> Result<Self> {
|
pub(super) fn new(s: &str, case: FilterCase) -> Result<Self> {
|
||||||
let query = match case {
|
Ok(Self { filter: Filter::new(s, case)?, matched: Default::default(), revision: 0 })
|
||||||
FinderCase::Smart => {
|
|
||||||
let uppercase = s.chars().any(|c| c.is_uppercase());
|
|
||||||
RegexBuilder::new(s).case_insensitive(!uppercase).build()?
|
|
||||||
}
|
|
||||||
FinderCase::Sensitive => Regex::new(s)?,
|
|
||||||
FinderCase::Insensitive => RegexBuilder::new(s).case_insensitive(true).build()?,
|
|
||||||
};
|
|
||||||
Ok(Self { query, matched: Default::default(), revision: 0 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
pub(super) fn prev(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
||||||
for i in !include as usize..files.len() {
|
for i in !include as usize..files.len() {
|
||||||
let idx = (cursor + files.len() - i) % files.len();
|
let idx = (cursor + files.len() - i) % files.len();
|
||||||
if files[idx].name().is_some_and(|n| self.matches(n)) {
|
if files[idx].name().is_some_and(|n| self.filter.matches(n)) {
|
||||||
return Some(idx as isize - cursor as isize);
|
return Some(idx as isize - cursor as isize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +29,7 @@ impl Finder {
|
||||||
pub(super) fn next(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
pub(super) fn next(&self, files: &Files, cursor: usize, include: bool) -> Option<isize> {
|
||||||
for i in !include as usize..files.len() {
|
for i in !include as usize..files.len() {
|
||||||
let idx = (cursor + i) % files.len();
|
let idx = (cursor + i) % files.len();
|
||||||
if files[idx].name().is_some_and(|n| self.matches(n)) {
|
if files[idx].name().is_some_and(|n| self.filter.matches(n)) {
|
||||||
return Some(idx as isize - cursor as isize);
|
return Some(idx as isize - cursor as isize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +44,7 @@ impl Finder {
|
||||||
|
|
||||||
let mut i = 0u8;
|
let mut i = 0u8;
|
||||||
for file in files.iter() {
|
for file in files.iter() {
|
||||||
if file.name().map(|n| self.matches(n)) != Some(true) {
|
if file.name().map(|n| self.filter.matches(n)) != Some(true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,15 +59,6 @@ impl Finder {
|
||||||
self.revision = files.revision;
|
self.revision = files.revision;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn matches(&self, name: &OsStr) -> bool { self.query.is_match(name.as_encoded_bytes()) }
|
|
||||||
|
|
||||||
/// Explode the name into three parts: head, body, tail.
|
|
||||||
#[inline]
|
|
||||||
pub fn highlighted(&self, name: &OsStr) -> Option<Vec<Range<usize>>> {
|
|
||||||
self.query.find(name.as_encoded_bytes()).map(|m| vec![m.range()])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Finder {
|
impl Finder {
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,10 @@ impl<'a> Executor<'a> {
|
||||||
on!(ACTIVE, search);
|
on!(ACTIVE, search);
|
||||||
on!(ACTIVE, jump);
|
on!(ACTIVE, jump);
|
||||||
|
|
||||||
|
// Filter
|
||||||
|
on!(ACTIVE, filter);
|
||||||
|
on!(ACTIVE, filter_do);
|
||||||
|
|
||||||
// Find
|
// Find
|
||||||
on!(ACTIVE, find);
|
on!(ACTIVE, find);
|
||||||
on!(ACTIVE, find_do);
|
on!(ACTIVE, find_do);
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ impl<'a, 'b> Folder<'a, 'b> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = me.borrow::<yazi_shared::fs::File>()?;
|
let file = me.borrow::<yazi_shared::fs::File>()?;
|
||||||
let Some(h) = file.name().and_then(|n| finder.highlighted(n)) else {
|
let Some(h) = file.name().and_then(|n| finder.filter.highlighted(n)) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue