feat: improve path auto-completion results (#2765)

This commit is contained in:
三咲雅 · Misaki Masa 2025-05-16 16:37:04 +08:00 committed by GitHub
parent e257581fe7
commit ffc635e434
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 94 additions and 57 deletions

1
Cargo.lock generated
View file

@ -3659,6 +3659,7 @@ dependencies = [
"yazi-config",
"yazi-macro",
"yazi-plugin",
"yazi-proxy",
"yazi-shared",
]

View file

@ -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,
@ -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]
}
@ -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]

View file

@ -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,
@ -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()
}
}

View file

@ -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;
@ -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)

View file

@ -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;
@ -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 {

View file

@ -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 {
@ -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)));
}
}

View file

@ -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 { "" })
}
}

View file

@ -1 +1 @@
yazi_macro::mod_flat!(notify open plugin process search);
yazi_macro::mod_flat!(cmp notify open plugin process search);

View file

@ -1,5 +1,5 @@
use core::str;
use std::borrow::Cow;
use std::{borrow::Cow, ffi::OsStr};
pub const MIME_DIR: &str = "inode/directory";
@ -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
}
}

View file

@ -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

View file

@ -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;
@ -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();