Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions yazi-core/src/cmp/cmp.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::{collections::HashMap, path::PathBuf};

use yazi_proxy::options::CmpItem;
use yazi_shared::Id;

#[derive(Default)]
pub struct Cmp {
pub(super) caches: HashMap<PathBuf, Vec<String>>,
pub(super) cands: Vec<String>,
pub(super) caches: HashMap<PathBuf, Vec<CmpItem>>,
pub(super) cands: Vec<CmpItem>,
pub(super) offset: usize,
pub cursor: usize,

Expand All @@ -16,7 +17,7 @@ pub struct Cmp {
impl Cmp {
// --- Cands
#[inline]
pub fn window(&self) -> &[String] {
pub fn window(&self) -> &[CmpItem] {
let end = (self.offset + self.limit()).min(self.cands.len());
&self.cands[self.offset..end]
}
Expand All @@ -25,7 +26,7 @@ impl Cmp {
pub fn limit(&self) -> usize { self.cands.len().min(10) }

#[inline]
pub fn selected(&self) -> Option<&String> { self.cands.get(self.cursor) }
pub fn selected(&self) -> Option<&CmpItem> { self.cands.get(self.cursor) }

// --- Cursor
#[inline]
Expand Down
43 changes: 19 additions & 24 deletions yazi-core/src/cmp/commands/show.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::{borrow::Cow, mem, ops::ControlFlow, path::PathBuf};

use yazi_macro::render;
use yazi_shared::{Id, event::{Cmd, CmdCow, Data}};
use yazi_proxy::options::CmpItem;
use yazi_shared::{Id, event::{Cmd, CmdCow, Data}, osstr_contains, osstr_starts_with};

use crate::cmp::Cmp;

const LIMIT: usize = 30;

struct Opt {
cache: Vec<String>,
cache: Vec<CmpItem>,
cache_name: PathBuf,
word: Cow<'static, str>,
ticket: Id,
Expand Down Expand Up @@ -55,34 +56,28 @@ impl Cmp {
render!();
}

fn match_candidates(word: &str, cache: &[String]) -> Vec<String> {
fn match_candidates(word: &str, cache: &[CmpItem]) -> Vec<CmpItem> {
let smart = !word.bytes().any(|c| c.is_ascii_uppercase());

let flow = cache.iter().try_fold(
(Vec::with_capacity(LIMIT), Vec::with_capacity(LIMIT)),
|(mut prefixed, mut fuzzy), s| {
if (smart && s.to_lowercase().starts_with(word)) || (!smart && s.starts_with(word)) {
if s != word {
prefixed.push(s);
if prefixed.len() >= LIMIT {
return ControlFlow::Break((prefixed, fuzzy));
}
}
} else if fuzzy.len() < LIMIT - prefixed.len() && s.contains(word) {
// here we don't break the control flow, since we want more exact matching.
fuzzy.push(s)
let flow = cache.iter().try_fold((Vec::new(), Vec::new()), |(mut exact, mut fuzzy), item| {
if osstr_starts_with(&item.name, word, smart) {
exact.push(item);
if exact.len() >= LIMIT {
return ControlFlow::Break((exact, fuzzy));
}
ControlFlow::Continue((prefixed, fuzzy))
},
);
} else if fuzzy.len() < LIMIT - exact.len() && osstr_contains(&item.name, word) {
// Here we don't break the control flow, since we want more exact matching.
fuzzy.push(item)
}
ControlFlow::Continue((exact, fuzzy))
});

let (mut prefixed, fuzzy) = match flow {
let (exact, fuzzy) = match flow {
ControlFlow::Continue(v) => v,
ControlFlow::Break(v) => v,
};
if prefixed.len() < LIMIT {
prefixed.extend(fuzzy.into_iter().take(LIMIT - prefixed.len()))
}
prefixed.into_iter().map(ToOwned::to_owned).collect()

let it = fuzzy.into_iter().take(LIMIT - exact.len());
exact.into_iter().chain(it).cloned().collect()
}
}
18 changes: 9 additions & 9 deletions yazi-core/src/cmp/commands/trigger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use std::{borrow::Cow, mem, path::{MAIN_SEPARATOR_STR, PathBuf}};
use tokio::fs;
use yazi_fs::{CWD, expand_path};
use yazi_macro::{emit, render};
use yazi_shared::{Id, event::{Cmd, CmdCow, Data}};
use yazi_proxy::options::CmpItem;
use yazi_shared::{Id, event::{Cmd, CmdCow, Data}, natsort};

use crate::cmp::Cmp;

Expand Down Expand Up @@ -43,17 +44,16 @@ impl Cmp {
tokio::spawn(async move {
let mut dir = fs::read_dir(&parent).await?;
let mut cache = vec![];
while let Ok(Some(f)) = dir.next_entry().await {
let Ok(meta) = f.metadata().await else { continue };

cache.push(format!(
"{}{}",
f.file_name().to_string_lossy(),
if meta.is_dir() { MAIN_SEPARATOR_STR } else { "" },
));
while let Ok(Some(ent)) = dir.next_entry().await {
if let Ok(ft) = ent.file_type().await {
cache.push(CmpItem { name: ent.file_name(), is_dir: ft.is_dir() });
}
}

if !cache.is_empty() {
cache.sort_unstable_by(|a, b| {
natsort(a.name.as_encoded_bytes(), b.name.as_encoded_bytes(), false)
});
emit!(Call(
Cmd::new("cmp:show")
.with_any("cache", cache)
Expand Down
8 changes: 4 additions & 4 deletions yazi-fm/src/cmp/cmp.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::MAIN_SEPARATOR;
use std::path::MAIN_SEPARATOR_STR;

use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, ListItem, Widget}};
use yazi_adapter::Dimension;
Expand All @@ -23,10 +23,10 @@ impl Widget for Cmp<'_> {
.iter()
.enumerate()
.map(|(i, x)| {
let icon =
if x.ends_with(MAIN_SEPARATOR) { &THEME.cmp.icon_folder } else { &THEME.cmp.icon_file };
let icon = if x.is_dir { &THEME.cmp.icon_folder } else { &THEME.cmp.icon_file };
let slash = if x.is_dir { MAIN_SEPARATOR_STR } else { "" };

let mut item = ListItem::new(format!(" {icon} {x}"));
let mut item = ListItem::new(format!(" {icon} {}{slash}", x.name.display()));
if i == self.cx.cmp.rel_cursor() {
item = item.style(THEME.cmp.active);
} else {
Expand Down
6 changes: 4 additions & 2 deletions yazi-proxy/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use yazi_config::popup::InputCfg;
use yazi_macro::emit;
use yazi_shared::{Id, errors::InputError, event::Cmd};

use crate::options::CmpItem;

pub struct InputProxy;

impl InputProxy {
Expand All @@ -14,7 +16,7 @@ impl InputProxy {
}

#[inline]
pub fn complete(word: &str, ticket: Id) {
emit!(Call(Cmd::args("input:complete", &[word]).with("ticket", ticket)));
pub fn complete(item: &CmpItem, ticket: Id) {
emit!(Call(Cmd::new("input:complete").with_any("item", item.clone()).with("ticket", ticket)));
}
}
13 changes: 13 additions & 0 deletions yazi-proxy/src/options/cmp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use std::{ffi::OsString, path::MAIN_SEPARATOR_STR};

#[derive(Debug, Clone)]
pub struct CmpItem {
pub name: OsString,
pub is_dir: bool,
}

impl CmpItem {
pub fn completable(&self) -> String {
format!("{}{}", self.name.to_string_lossy(), if self.is_dir { MAIN_SEPARATOR_STR } else { "" })
}
}
2 changes: 1 addition & 1 deletion yazi-proxy/src/options/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
yazi_macro::mod_flat!(notify open plugin process search);
yazi_macro::mod_flat!(cmp notify open plugin process search);
22 changes: 21 additions & 1 deletion yazi-shared/src/chars.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use core::str;
use std::borrow::Cow;
use std::{borrow::Cow, ffi::OsStr};

pub const MIME_DIR: &str = "inode/directory";

Expand Down Expand Up @@ -106,3 +106,23 @@ pub fn replace_to_printable(s: &[String], tab_size: u8) -> String {
}
unsafe { String::from_utf8_unchecked(buf) }
}

pub fn osstr_contains(s: impl AsRef<OsStr>, needle: impl AsRef<OsStr>) -> bool {
memchr::memmem::find(s.as_ref().as_encoded_bytes(), needle.as_ref().as_encoded_bytes()).is_some()
}

pub fn osstr_starts_with(
s: impl AsRef<OsStr>,
prefix: impl AsRef<OsStr>,
insensitive: bool,
) -> bool {
let (s, prefix) = (s.as_ref().as_encoded_bytes(), prefix.as_ref().as_encoded_bytes());
if s.len() < prefix.len() {
return false;
}
if insensitive {
s[..prefix.len()].eq_ignore_ascii_case(prefix)
} else {
s[..prefix.len()] == *prefix
}
}
1 change: 1 addition & 0 deletions yazi-widgets/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ yazi-codegen = { path = "../yazi-codegen", version = "25.5.14" }
yazi-config = { path = "../yazi-config", version = "25.5.14" }
yazi-macro = { path = "../yazi-macro", version = "25.5.14" }
yazi-plugin = { path = "../yazi-plugin", version = "25.5.14" }
yazi-proxy = { path = "../yazi-proxy", version = "25.5.14" }
yazi-shared = { path = "../yazi-shared", version = "25.5.14" }

# External dependencies
Expand Down
28 changes: 16 additions & 12 deletions yazi-widgets/src/input/commands/complete.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{borrow::Cow, path::MAIN_SEPARATOR_STR};
use std::path::MAIN_SEPARATOR_STR;

use yazi_macro::render;
use yazi_proxy::options::CmpItem;
use yazi_shared::{Id, event::{CmdCow, Data}};

use crate::input::Input;
Expand All @@ -11,28 +12,31 @@ const SEPARATOR: [char; 2] = ['/', '\\'];
#[cfg(not(windows))]
const SEPARATOR: char = std::path::MAIN_SEPARATOR;

struct Opt {
word: Cow<'static, str>,
pub struct Opt {
item: CmpItem,
_ticket: Id, // FIXME: not used
}

impl From<CmdCow> for Opt {
fn from(mut c: CmdCow) -> Self {
Self {
word: c.take_first_str().unwrap_or_default(),
impl TryFrom<CmdCow> for Opt {
type Error = ();

fn try_from(mut c: CmdCow) -> Result<Self, Self::Error> {
Ok(Self {
item: c.take_any("item").ok_or(())?,
_ticket: c.get("ticket").and_then(Data::as_id).unwrap_or_default(),
}
})
}
}

impl Input {
#[yazi_codegen::command]
pub fn complete(&mut self, opt: Opt) {
pub fn complete(&mut self, opt: impl TryInto<Opt>) {
let Ok(opt): Result<Opt, _> = opt.try_into() else { return };

let (before, after) = self.partition();
let new = if let Some((prefix, _)) = before.rsplit_once(SEPARATOR) {
format!("{prefix}/{}{after}", opt.word).replace(SEPARATOR, MAIN_SEPARATOR_STR)
format!("{prefix}/{}{after}", opt.item.completable()).replace(SEPARATOR, MAIN_SEPARATOR_STR)
} else {
format!("{}{after}", opt.word).replace(SEPARATOR, MAIN_SEPARATOR_STR)
format!("{}{after}", opt.item.completable()).replace(SEPARATOR, MAIN_SEPARATOR_STR)
};

let snap = self.snap_mut();
Expand Down
Loading