mirror of
https://github.com/sxyazi/yazi.git
synced 2026-05-13 08:16:40 +00:00
feat: improve path auto-completion results (#2765)
This commit is contained in:
parent
e257581fe7
commit
ffc635e434
11 changed files with 94 additions and 57 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3659,6 +3659,7 @@ dependencies = [
|
|||
"yazi-config",
|
||||
"yazi-macro",
|
||||
"yazi-plugin",
|
||||
"yazi-proxy",
|
||||
"yazi-shared",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
13
yazi-proxy/src/options/cmp.rs
Normal file
13
yazi-proxy/src/options/cmp.rs
Normal 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 { "" })
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(notify open plugin process search);
|
||||
yazi_macro::mod_flat!(cmp notify open plugin process search);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue