From f6e7473db9076e70a1ca18b16b1d9986d246ad2d Mon Sep 17 00:00:00 2001 From: nabsei Date: Fri, 17 Jul 2026 00:20:44 +0200 Subject: [PATCH] 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 everywhere else (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 Co-authored-by: Claude Signed-off-by: nabsei --- yazi-core/src/mgr/snap.rs | 59 +++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/yazi-core/src/mgr/snap.rs b/yazi-core/src/mgr/snap.rs index fecb4de4..194f296e 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,15 @@ 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, same + // as `Tab::selected_or_hovered_urls()` used for file operations elsewhere. + 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 +55,43 @@ impl Splatable for MgrSnap { .map(AsUrl::as_url) } } + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::*; + + fn url(s: &str) -> UrlBuf { UrlBuf::from(PathBuf::from(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_nothing_selected() { + let s = snap(&[], Some("hovered/file")); + let urls: Vec<_> = s.selected(1, None).collect(); + assert_eq!(urls, [url("hovered/file")]); + } + + #[test] + fn selected_prefers_actual_selection_over_hovered() { + let s = snap(&["a", "b"], Some("hovered/file")); + let urls: Vec<_> = s.selected(1, None).collect(); + assert_eq!(urls, [url("a"), url("b")]); + } + + #[test] + fn selected_is_empty_when_nothing_selected_or_hovered() { + let s = snap(&[], None); + assert_eq!(s.selected(1, None).count(), 0); + } +}