refactor: simplify cursor repositioning (#3040)

This commit is contained in:
三咲雅 misaki masa 2025-08-08 02:41:41 +08:00 committed by GitHub
parent 41b00c8d4e
commit 292d37ccde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 209 additions and 164 deletions

View file

@ -88,6 +88,11 @@ impl<'a> Ctx<'a> {
#[inline]
pub fn hovered_folder(&self) -> Option<&Folder> { self.tab().hovered_folder() }
#[inline]
pub fn hovered_folder_mut(&mut self) -> Option<&mut Folder> {
self.tab_mut().hovered_folder_mut()
}
#[inline]
pub fn tasks(&self) -> &Tasks { &self.tasks }

View file

@ -24,7 +24,7 @@ impl Actor for Arrow {
*items = (start.min(end)..=end.max(start)).collect();
}
act!(mgr:hover, cx, None)?;
act!(mgr:hover, cx)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;

View file

@ -59,6 +59,8 @@ impl Actor for Cd {
}
err!(Pubsub::pub_after_cd(tab.id, tab.cwd()));
act!(mgr:hidden, cx)?;
act!(mgr:sort, cx)?;
act!(mgr:hover, cx)?;
act!(mgr:refresh, cx)?;
succ!(render!());

View file

@ -17,12 +17,10 @@ impl Actor for FilterDo {
let filter = if opt.query.is_empty() { None } else { Some(Filter::new(&opt.query, opt.case)?) };
let hovered = cx.hovered().map(|f| f.urn_owned());
if cx.current_mut().files.set_filter(filter) {
cx.current_mut().repos(hovered.as_ref());
}
cx.current_mut().files.set_filter(filter);
if cx.hovered().map(|f| f.urn()) != hovered.as_ref().map(|u| u.as_urn()) {
act!(mgr:hover, cx)?;
if cx.hovered().map(|f| f.urn()) != hovered.as_deref() {
act!(mgr:hover, cx, hovered)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}

View file

@ -1,5 +1,7 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_core::tab::Folder;
use yazi_fs::FolderStage;
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::mgr::HiddenOpt;
use yazi_shared::event::Data;
@ -13,21 +15,39 @@ impl Actor for Hidden {
const NAME: &str = "hidden";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let tab = cx.tab_mut();
tab.pref.show_hidden = opt.state.unwrap_or(!tab.pref.show_hidden);
let state = opt.state.bool(cx.tab().pref.show_hidden);
cx.tab_mut().pref.show_hidden = state;
let hovered = tab.hovered().map(|f| f.url_owned());
tab.apply_files_attrs();
let hovered = cx.hovered().map(|f| f.urn_owned());
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();
false
} else {
f.files.set_show_hidden(state);
render_and!(f.files.catchup_revision())
}
};
if hovered.as_ref() != tab.hovered().map(|f| &f.url) {
act!(mgr:hover, cx, hovered)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
} else if tab.hovered().is_some_and(|f| f.is_dir()) {
act!(mgr:peek, cx, true)?;
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
}
act!(mgr:update_paged, cx)?;
succ!();
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut()
&& apply(h)
{
render!(h.repos(None));
act!(mgr:peek, cx, true)?;
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
succ!()
}
}

View file

@ -1,7 +1,7 @@
use anyhow::Result;
use yazi_dds::Pubsub;
use yazi_macro::{act, err, render, succ};
use yazi_parser::mgr::{HoverDoOpt, HoverOpt};
use yazi_macro::{err, render, succ, tab};
use yazi_parser::mgr::HoverOpt;
use yazi_shared::event::Data;
use crate::{Actor, Ctx};
@ -14,39 +14,27 @@ impl Actor for Hover {
const NAME: &str = "hover";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
if let Some(u) = opt.url {
act!(mgr:hover_do, cx, u)?;
} else {
cx.current_mut().arrow(0);
let tab = tab!(cx);
// Parent should always track CWD
if let Some(p) = &mut tab.parent {
render!(p.repos(tab.current.url.strip_prefix(&p.url)));
}
// Repos CWD
tab.current.repos(opt.urn.as_deref());
// Turn on tracing
if let (Some(h), Some(u)) = (tab.hovered(), opt.urn)
&& *h.urn() == u
{
// `hover(Some)` occurs after user actions, such as create, rename, reveal, etc.
// At this point, it's intuitive to track the location of the file regardless.
tab.current.trace = Some(u.to_owned());
}
// Publish through DDS
let tab = cx.tab();
err!(Pubsub::pub_after_hover(tab.id, tab.hovered().map(|h| &h.url)));
succ!();
}
}
// --- Do
pub struct HoverDo;
impl Actor for HoverDo {
type Options = HoverDoOpt;
const NAME: &str = "hover_do";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
// Hover on the file
if let Some(u) = opt.url.strip_prefix(cx.cwd()) {
render!(cx.current_mut().hover(u));
}
// Turn on tracing
if cx.hovered().is_some_and(|h| h.url == opt.url) {
// `hover(Some)` occurs after user actions, such as create, rename, reveal, etc.
// At this point, it's intuitive to track the location of the file regardless.
cx.current_mut().trace = Some(opt.url.urn_owned());
}
succ!();
}
}

View file

@ -21,15 +21,13 @@ impl Actor for Refresh {
execute!(TTY.writer(), SetTitle(s)).ok();
}
cx.tab_mut().apply_files_attrs();
if let Some(p) = cx.parent() {
cx.mgr.watcher.trigger_dirs(&[cx.current(), p]);
} else {
cx.mgr.watcher.trigger_dirs(&[cx.current()]);
}
act!(mgr:peek, cx, false)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?;

View file

@ -26,12 +26,12 @@ impl Actor for Reveal {
// If the child is not hovered, which means it doesn't exist,
// create a dummy file
if !opt.no_dummy && tab.hovered().is_none_or(|f| &child != f.urn()) {
let op = FilesOp::Creating(parent, vec![File::from_dummy(opt.target.clone(), None)]);
let op = FilesOp::Creating(parent, vec![File::from_dummy(opt.target, None)]);
tab.current.update_pub(tab.id, op);
}
// Now, we can safely hover on the target
act!(mgr:hover, cx, Some(opt.target))?;
act!(mgr:hover, cx, Some(child))?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;

View file

@ -107,11 +107,15 @@ impl Actor for SearchStop {
handle.abort();
}
if tab.cwd().is_search() {
let rep = tab.history.remove_or(&tab.cwd().to_regular());
drop(mem::replace(&mut tab.current, rep));
act!(mgr:refresh, cx)?;
if !tab.cwd().is_search() {
succ!();
}
succ!();
let rep = tab.history.remove_or(&tab.cwd().to_regular());
drop(mem::replace(&mut tab.current, rep));
act!(mgr:hidden, cx)?;
act!(mgr:sort, cx)?;
act!(mgr:refresh, cx)
}
}

View file

@ -1,5 +1,7 @@
use anyhow::Result;
use yazi_macro::{act, succ};
use yazi_core::tab::Folder;
use yazi_fs::{FilesSorter, FolderStage};
use yazi_macro::{act, render, render_and, succ};
use yazi_parser::mgr::SortOpt;
use yazi_shared::event::Data;
@ -13,25 +15,44 @@ impl Actor for Sort {
const NAME: &str = "sort";
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
let mut new = cx.tab().pref.clone();
new.sort_by = opt.by.unwrap_or(new.sort_by);
new.sort_reverse = opt.reverse.unwrap_or(new.sort_reverse);
new.sort_dir_first = opt.dir_first.unwrap_or(new.sort_dir_first);
new.sort_sensitive = opt.sensitive.unwrap_or(new.sort_sensitive);
new.sort_translit = opt.translit.unwrap_or(new.sort_translit);
let pref = &mut cx.tab_mut().pref;
pref.sort_by = opt.by.unwrap_or(pref.sort_by);
pref.sort_reverse = opt.reverse.unwrap_or(pref.sort_reverse);
pref.sort_dir_first = opt.dir_first.unwrap_or(pref.sort_dir_first);
pref.sort_sensitive = opt.sensitive.unwrap_or(pref.sort_sensitive);
pref.sort_translit = opt.translit.unwrap_or(pref.sort_translit);
if new == cx.tab().pref {
succ!();
let sorter = FilesSorter::from(&*pref);
let hovered = cx.hovered().map(|f| f.urn_owned());
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
render!();
false
} else {
f.files.set_sorter(sorter);
render_and!(f.files.catchup_revision())
}
};
// Apply to CWD and parent
if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply))
&& (a | b)
{
act!(mgr:hover, cx)?;
act!(mgr:update_paged, cx)?;
cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.files);
}
cx.tab_mut().pref = new;
cx.tab_mut().apply_files_attrs();
act!(mgr:hover, cx)?;
cx.tasks.prework_sorted(&cx.mgr.tabs[cx.tab].current.files);
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?;
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut()
&& apply(h)
{
render!(h.repos(None));
act!(mgr:peek, cx, true)?;
} else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) {
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
}
succ!();
}

View file

@ -29,11 +29,9 @@ impl Actor for TabCreate {
(true, wd)
} else if let Some(h) = cx.hovered() {
tab.pref = cx.tab().pref.clone();
tab.apply_files_attrs();
(false, h.url.to_regular())
} else {
tab.pref = cx.tab().pref.clone();
tab.apply_files_attrs();
(true, cx.cwd().to_regular())
};

View file

@ -1,5 +1,6 @@
use anyhow::Result;
use yazi_macro::{act, render, succ};
use yazi_dds::Pubsub;
use yazi_macro::{err, render, succ};
use yazi_parser::ArrowOpt;
use yazi_shared::event::Data;
@ -21,12 +22,9 @@ impl Actor for TabSwap {
}
tabs.items.swap(tabs.cursor, new);
tabs.set_idx(new);
let cx = &mut Ctx::renew(cx);
act!(mgr:refresh, cx)?;
act!(mgr:peek, cx, true)?;
tabs.cursor = new;
err!(Pubsub::pub_after_tab(cx.active().id));
succ!(render!());
}
}

View file

@ -23,11 +23,12 @@ impl Actor for UpdateFiles {
}
render!(cx.mgr.yanked.catchup_revision(false));
cx.tab_mut().apply_files_attrs();
act!(mgr:hidden, cx)?;
act!(mgr:sort, cx)?;
if revision != cx.current().files.revision {
act!(mgr:hover, cx)?;
act!(mgr:peek, cx, false)?;
act!(mgr:peek, cx)?;
act!(mgr:watch, cx)?;
act!(mgr:update_paged, cx)?;
}

View file

@ -47,7 +47,7 @@ impl Actor for UpdateMimes {
cx.mgr.mimetype.extend(updates);
if repeek {
act!(mgr:peek, cx, false)?;
act!(mgr:peek, cx)?;
}
cx.tasks.fetch_paged(&affected, &cx.mgr.mimetype);
cx.tasks.preload_paged(&affected, &cx.mgr.mimetype);

View file

@ -81,7 +81,7 @@ impl Folder {
}
self.trace = self.trace.take_if(|_| !self.files.is_empty() || self.stage.is_loading());
self.repos(self.trace.clone());
self.repos(None);
(stage, revision) != (self.stage, self.files.revision)
}
@ -120,8 +120,14 @@ impl Folder {
}
#[inline]
pub fn repos(&mut self, urn: Option<impl AsRef<Urn>>) -> bool {
if let Some(u) = urn { self.hover(u.as_ref()) } else { self.arrow(0) }
pub fn repos(&mut self, urn: Option<&Urn>) -> bool {
if let Some(u) = urn {
self.hover(u)
} else if let Some(u) = &self.trace {
self.hover(u.clone().as_ref())
} else {
self.arrow(0)
}
}
pub fn sync_page(&mut self, force: bool) {
@ -157,6 +163,9 @@ impl Folder {
#[inline]
pub fn hovered(&self) -> Option<&File> { self.files.get(self.cursor) }
#[inline]
pub fn hovered_mut(&mut self) -> Option<&mut File> { self.files.get_mut(self.cursor) }
pub fn paginate(&self, page: usize) -> &[File] {
let len = self.files.len();
let limit = LAYOUT.get().limit();

View file

@ -5,9 +5,8 @@ use ratatui::layout::Rect;
use tokio::task::JoinHandle;
use yazi_adapter::Dimension;
use yazi_config::{LAYOUT, popup::{Origin, Position}};
use yazi_fs::{File, FolderStage};
use yazi_macro::render;
use yazi_shared::{Id, Ids, url::{Url, Urn}};
use yazi_fs::File;
use yazi_shared::{Id, Ids, url::Url};
use super::{Backstack, Finder, Folder, History, Mode, Preference, Preview};
use crate::{spot::Spot, tab::Selected};
@ -67,6 +66,9 @@ impl Tab {
#[inline]
pub fn hovered(&self) -> Option<&File> { self.current.hovered() }
#[inline]
pub fn hovered_mut(&mut self) -> Option<&mut File> { self.current.hovered_mut() }
pub fn hovered_rect(&self) -> Option<Rect> {
let y = self.current.files.position(self.hovered()?.urn())? - self.current.offset;
@ -108,33 +110,8 @@ impl Tab {
self.hovered().filter(|&h| h.is_dir()).and_then(|h| self.history.get(&h.url))
}
pub fn apply_files_attrs(&mut self) {
let apply = |f: &mut Folder| {
if f.stage == FolderStage::Loading {
return render!();
}
f.files.set_show_hidden(self.pref.show_hidden);
f.files.set_sorter(<_>::from(&self.pref));
render!(f.files.catchup_revision());
render!(f.repos(f.trace.clone()));
};
apply(&mut self.current);
if let Some(parent) = &mut self.parent {
apply(parent);
// The parent should always track the CWD
parent.hover(self.current.url.strip_prefix(&parent.url).unwrap_or(Urn::new("")));
}
self
.current
.hovered()
.filter(|h| h.is_dir())
.and_then(|h| self.history.get_mut(&h.url))
.map(apply);
#[inline]
pub fn hovered_folder_mut(&mut self) -> Option<&mut Folder> {
self.current.hovered_mut().filter(|h| h.is_dir()).and_then(|h| self.history.get_mut(&h.url))
}
}

View file

@ -32,7 +32,6 @@ pub enum Spark<'a> {
Hardlink(yazi_parser::mgr::HardlinkOpt),
Hidden(yazi_parser::mgr::HiddenOpt),
Hover(yazi_parser::mgr::HoverOpt),
HoverDo(yazi_parser::mgr::HoverDoOpt),
Leave(yazi_parser::VoidOpt),
Linemode(yazi_parser::mgr::LinemodeOpt),
Link(yazi_parser::mgr::LinkOpt),
@ -155,7 +154,6 @@ impl<'a> IntoLua for Spark<'a> {
Self::Hardlink(b) => b.into_lua(lua),
Self::Hidden(b) => b.into_lua(lua),
Self::Hover(b) => b.into_lua(lua),
Self::HoverDo(b) => b.into_lua(lua),
Self::Leave(b) => b.into_lua(lua),
Self::Linemode(b) => b.into_lua(lua),
Self::Link(b) => b.into_lua(lua),
@ -289,7 +287,6 @@ try_from_spark!(mgr::FindDoOpt, mgr:find_do);
try_from_spark!(mgr::FindOpt, mgr:find);
try_from_spark!(mgr::HardlinkOpt, mgr:hardlink);
try_from_spark!(mgr::HiddenOpt, mgr:hidden);
try_from_spark!(mgr::HoverDoOpt, mgr:hover_do);
try_from_spark!(mgr::HoverOpt, mgr:hover);
try_from_spark!(mgr::LinemodeOpt, mgr:linemode);
try_from_spark!(mgr::LinkOpt, mgr:link);

View file

@ -1,4 +1,4 @@
use std::{collections::{HashMap, HashSet}, mem, ops::{Deref, Not}};
use std::{collections::{HashMap, HashSet}, mem, ops::{Deref, DerefMut, Not}};
use tokio::{select, sync::mpsc::{self, UnboundedReceiver}};
use yazi_shared::{Id, url::{Url, Urn, UrnBuf}};
@ -27,6 +27,10 @@ impl Deref for Files {
fn deref(&self) -> &Self::Target { &self.items }
}
impl DerefMut for Files {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.items }
}
impl Files {
pub fn new(show_hidden: bool) -> Self { Self { show_hidden, ..Default::default() } }

View file

@ -4,7 +4,7 @@ use yazi_shared::{LcgRng, natsort, translit::Transliterator, url::UrnBuf};
use crate::{File, SortBy};
#[derive(Clone, Copy, Default, PartialEq)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct FilesSorter {
pub by: SortBy,
pub sensitive: bool,

View file

@ -0,0 +1,7 @@
#[macro_export]
macro_rules! tab {
($cx:ident) => {{
let tab = $cx.tab;
&mut $cx.core.mgr.tabs.items[tab]
}};
}

View file

@ -1,5 +1,6 @@
mod actor;
mod asset;
mod context;
mod event;
mod fmt;
mod log;

View file

@ -1,20 +1,19 @@
use std::str::FromStr;
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use serde::{Deserialize, Serialize};
use yazi_shared::event::CmdCow;
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct HiddenOpt {
pub state: Option<bool>,
pub state: HiddenOptState,
}
impl From<CmdCow> for HiddenOpt {
fn from(c: CmdCow) -> Self {
let state = match c.first_str() {
Some("show") => Some(true),
Some("hide") => Some(false),
_ => None,
};
impl TryFrom<CmdCow> for HiddenOpt {
type Error = anyhow::Error;
Self { state }
fn try_from(c: CmdCow) -> Result<Self, Self::Error> {
Ok(Self { state: c.first_str().map(FromStr::from_str).transpose()?.unwrap_or_default() })
}
}
@ -25,3 +24,33 @@ impl FromLua for HiddenOpt {
impl IntoLua for HiddenOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}
// --- State
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum HiddenOptState {
#[default]
None,
Show,
Hide,
Toggle,
}
impl FromStr for HiddenOptState {
type Err = serde::de::value::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::deserialize(serde::de::value::StrDeserializer::new(s))
}
}
impl HiddenOptState {
pub fn bool(self, old: bool) -> bool {
match self {
Self::None => old,
Self::Show => true,
Self::Hide => false,
Self::Toggle => !old,
}
}
}

View file

@ -1,17 +1,13 @@
use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_shared::{event::CmdCow, url::Url};
use yazi_shared::url::UrnBuf;
#[derive(Debug, Default)]
pub struct HoverOpt {
pub url: Option<Url>,
pub urn: Option<UrnBuf>,
}
impl From<CmdCow> for HoverOpt {
fn from(mut c: CmdCow) -> Self { Self { url: c.take_first_url() } }
}
impl From<Option<Url>> for HoverOpt {
fn from(url: Option<Url>) -> Self { Self { url } }
impl From<Option<UrnBuf>> for HoverOpt {
fn from(urn: Option<UrnBuf>) -> Self { Self { urn } }
}
impl FromLua for HoverOpt {
@ -21,21 +17,3 @@ impl FromLua for HoverOpt {
impl IntoLua for HoverOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}
// --- Do
#[derive(Debug)]
pub struct HoverDoOpt {
pub url: Url,
}
impl From<Url> for HoverDoOpt {
fn from(url: Url) -> Self { Self { url } }
}
impl FromLua for HoverDoOpt {
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
}
impl IntoLua for HoverDoOpt {
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
}

View file

@ -4,7 +4,7 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
use yazi_fs::SortBy;
use yazi_shared::event::CmdCow;
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct SortOpt {
pub by: Option<SortBy>,
pub reverse: Option<bool>,

View file

@ -61,6 +61,12 @@ impl PartialEq<Cow<'_, OsStr>> for &Urn {
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct UrnBuf(PathBuf);
impl Deref for UrnBuf {
type Target = Urn;
fn deref(&self) -> &Self::Target { self.borrow() }
}
impl Borrow<Urn> for UrnBuf {
fn borrow(&self) -> &Urn { Urn::new(&self.0) }
}
@ -77,6 +83,10 @@ impl PartialEq<Urn> for UrnBuf {
fn eq(&self, other: &Urn) -> bool { self.0 == other.0 }
}
impl PartialEq<UrnBuf> for Urn {
fn eq(&self, other: &UrnBuf) -> bool { self.0 == other.0 }
}
impl<T: Into<PathBuf>> From<T> for UrnBuf {
fn from(p: T) -> Self { Self(p.into()) }
}