From 2e60b1741b0f49a6a14dd6d49f4ee6a19ebe670d Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Mon, 20 Jul 2026 07:29:53 +0700 Subject: [PATCH 1/3] fix(mgr): restore hovered fallback for %s/%d shell-formatting when nothing is selected The Splatable impl for Mgr used to fall back to the hovered item via Tab::selected_or_hovered_urls() when the selection was empty, the same convention used elsewhere (yank, copy, remove, open). PR #4108 rewrote this into MgrSnap/TabSnap but only ported the plain selected list, dropping the hovered fallback. This made %s and %d expand to nothing when hovering a file without an explicit selection. Restore the fallback in MgrSnap::selected(), which both %s/%S and %d/%D read from. Resolves #4132 --- yazi-core/src/mgr/snap.rs | 62 ++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index fecb4de4..5e4b659b 100644 --- a/yazi-core/src/mgr/snap.rs +++ b/yazi-core/src/mgr/snap.rs @@ -1,3 +1,5 @@ +use std::iter; + use yazi_fs::Splatable; use yazi_shared::url::{AsUrl, Url, UrlBuf}; @@ -24,14 +26,17 @@ impl Splatable for MgrSnap { fn selected(&self, tab: usize, mut idx: Option) -> impl Iterator> { idx = idx.and_then(|i| i.checked_sub(1)); - tab - .checked_sub(1) - .and_then(|tab| self.tabs.get(tab)) - .map_or_else(|| &[][..], |s| &s.selected) - .iter() - .skip(idx.unwrap_or(0)) - .take(if idx.is_some() { 1 } else { usize::MAX }) - .map(AsUrl::as_url) + + // Fall back to the hovered item when nothing is explicitly selected, the + // same convention `Tab::selected_or_hovered_urls()` used before #4108. + let urls: Box> = + match tab.checked_sub(1).and_then(|tab| self.tabs.get(tab)) { + Some(s) if !s.selected.is_empty() => Box::new(s.selected.iter()), + Some(s) => Box::new(s.hovered.iter()), + None => Box::new(iter::empty()), + }; + + urls.skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }).map(AsUrl::as_url) } fn hovered(&self, tab: usize) -> Option> { @@ -52,3 +57,44 @@ impl Splatable for MgrSnap { .map(AsUrl::as_url) } } + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + + fn url(s: &str) -> UrlBuf { UrlBuf::from(Path::new(s)) } + + fn snap(selected: &[&str], hovered: Option<&str>) -> MgrSnap { + MgrSnap { + tab: 0, + tabs: vec![TabSnap { + hovered: hovered.map(url), + selected: selected.iter().map(|s| url(s)).collect(), + }], + yanked: vec![], + } + } + + #[test] + fn selected_falls_back_to_hovered_when_empty() { + let s = snap(&[], Some("hovered")); + + assert_eq!(s.selected(1, None).collect::>(), [url("hovered").as_url()]); + } + + #[test] + fn selected_prefers_explicit_selection_over_hovered() { + let s = snap(&["a", "b"], Some("hovered")); + + assert_eq!(s.selected(1, None).collect::>(), [url("a").as_url(), url("b").as_url()]); + } + + #[test] + fn selected_empty_when_nothing_selected_or_hovered() { + let s = snap(&[], None); + + assert_eq!(s.selected(1, None).count(), 0); + } +} From 4531842b404a6f399841c0bcbc32e600ac896fed Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:55:46 +0700 Subject: [PATCH 2/3] fix(mgr): avoid Box allocation in MgrSnap::selected Iterate a &[UrlBuf] slice instead of boxing dyn Iterator. --- yazi-core/src/mgr/snap.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index 5e4b659b..df93a2ae 100644 --- a/yazi-core/src/mgr/snap.rs +++ b/yazi-core/src/mgr/snap.rs @@ -1,5 +1,3 @@ -use std::iter; - use yazi_fs::Splatable; use yazi_shared::url::{AsUrl, Url, UrlBuf}; @@ -29,14 +27,13 @@ impl Splatable for MgrSnap { // Fall back to the hovered item when nothing is explicitly selected, the // same convention `Tab::selected_or_hovered_urls()` used before #4108. - let urls: Box> = - match tab.checked_sub(1).and_then(|tab| self.tabs.get(tab)) { - Some(s) if !s.selected.is_empty() => Box::new(s.selected.iter()), - Some(s) => Box::new(s.hovered.iter()), - None => Box::new(iter::empty()), - }; + let urls: &[UrlBuf] = match tab.checked_sub(1).and_then(|tab| self.tabs.get(tab)) { + Some(s) if !s.selected.is_empty() => &s.selected, + Some(s) => s.hovered.as_slice(), + None => &[], + }; - urls.skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }).map(AsUrl::as_url) + urls.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }).map(AsUrl::as_url) } fn hovered(&self, tab: usize) -> Option> { From 751d5729bcb0789d90b3635cea5654f1401f47fb Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:14:40 +0700 Subject: [PATCH 3/3] style(mgr): rustfmt MgrSnap::selected chain for nightly CI Co-authored-by: Cursor --- yazi-core/src/mgr/snap.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index df93a2ae..8d79ce00 100644 --- a/yazi-core/src/mgr/snap.rs +++ b/yazi-core/src/mgr/snap.rs @@ -33,7 +33,11 @@ impl Splatable for MgrSnap { None => &[], }; - urls.iter().skip(idx.unwrap_or(0)).take(if idx.is_some() { 1 } else { usize::MAX }).map(AsUrl::as_url) + urls + .iter() + .skip(idx.unwrap_or(0)) + .take(if idx.is_some() { 1 } else { usize::MAX }) + .map(AsUrl::as_url) } fn hovered(&self, tab: usize) -> Option> {