From 324a439c73a956dfa20aabdc9c77e2fef48509da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Sat, 15 Mar 2025 17:44:42 +0800 Subject: [PATCH] feat: file navigation wraparound with new `arrow prev` and `arrow next` commands (#2485) --- Cargo.lock | 28 ++++++------ Cargo.toml | 6 +-- yazi-codegen/Cargo.toml | 2 +- yazi-config/preset/keymap-default.toml | 62 +++++++++++++------------- yazi-config/src/layout.rs | 2 + yazi-config/src/lib.rs | 12 ----- yazi-core/src/tab/commands/arrow.rs | 12 ----- yazi-core/src/tab/folder.rs | 26 +++++------ yazi-fs/src/step.rs | 33 +++++++++----- yazi-plugin/src/isolate/preload.rs | 22 +-------- yazi-plugin/src/utils/utils.rs | 1 - 11 files changed, 87 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37aa8560..fee83de1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -999,9 +999,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" dependencies = [ "cfg-if", "crunchy", @@ -1373,9 +1373,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.170" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libfuzzer-sys" @@ -1767,9 +1767,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.0" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde51589ab56b20a6f686b2c68f7a0bd6add753d697abf720d63f8db3ab7b1ad" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "onig" @@ -1990,9 +1990,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] @@ -2632,9 +2632,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.0" +version = "1.44.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", "bytes", @@ -2672,9 +2672,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" dependencies = [ "bytes", "futures-core", @@ -3288,9 +3288,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" +checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 5205919e..49551b05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ foldhash = "0.1.4" futures = "0.3.31" globset = "0.4.16" indexmap = { version = "2.8.0", features = [ "serde" ] } -libc = "0.2.170" +libc = "0.2.171" lru = "0.13.0" md-5 = "0.10.6" mlua = { version = "0.10.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serialize" ] } @@ -37,9 +37,9 @@ regex = "1.11.1" scopeguard = "1.2.0" serde = { version = "1.0.219", features = [ "derive" ] } serde_json = "1.0.140" -tokio = { version = "1.44.0", features = [ "full" ] } +tokio = { version = "1.44.1", features = [ "full" ] } tokio-stream = "0.1.17" -tokio-util = "0.7.13" +tokio-util = "0.7.14" toml = { version = "0.8.20" } tracing = { version = "0.1.41", features = [ "max_level_debug", "release_max_level_debug" ] } twox-hash = { version = "2.1.0", default-features = false, features = [ "std", "random", "xxhash3_128" ] } diff --git a/yazi-codegen/Cargo.toml b/yazi-codegen/Cargo.toml index 0e632caf..22e9f85f 100644 --- a/yazi-codegen/Cargo.toml +++ b/yazi-codegen/Cargo.toml @@ -14,4 +14,4 @@ proc-macro = true [dependencies] # External dependencies syn = { version = "2.0.100", features = [ "full" ] } -quote = "1.0.39" +quote = "1.0.40" diff --git a/yazi-config/preset/keymap-default.toml b/yazi-config/preset/keymap-default.toml index 90f7144c..e9f00f81 100644 --- a/yazi-config/preset/keymap-default.toml +++ b/yazi-config/preset/keymap-default.toml @@ -5,19 +5,19 @@ [manager] keymap = [ - { on = "", run = "escape", desc = "Exit visual mode, clear selected, or cancel search" }, - { on = "", run = "escape", desc = "Exit visual mode, clear selected, or cancel search" }, + { on = "", run = "escape", desc = "Exit visual mode, clear selection, or cancel search" }, + { on = "", run = "escape", desc = "Exit visual mode, clear selection, or cancel search" }, { on = "q", run = "quit", desc = "Quit the process" }, - { on = "Q", run = "quit --no-cwd-file", desc = "Quit the process without outputting cwd-file" }, + { on = "Q", run = "quit --no-cwd-file", desc = "Quit without outputting cwd-file" }, { on = "", run = "close", desc = "Close the current tab, or quit if it's last" }, { on = "", run = "suspend", desc = "Suspend the process" }, # Hopping - { on = "k", run = "arrow -1", desc = "Move cursor up" }, - { on = "j", run = "arrow 1", desc = "Move cursor down" }, + { on = "k", run = "arrow prev", desc = "Go to previous file" }, + { on = "j", run = "arrow next", desc = "Go to next file" }, - { on = "", run = "arrow -1", desc = "Move cursor up" }, - { on = "", run = "arrow 1", desc = "Move cursor down" }, + { on = "", run = "arrow prev", desc = "Go to previous file" }, + { on = "", run = "arrow next", desc = "Go to next file" }, { on = "", run = "arrow -50%", desc = "Move cursor up half page" }, { on = "", run = "arrow 50%", desc = "Move cursor down half page" }, @@ -39,13 +39,13 @@ keymap = [ { on = "", run = "leave", desc = "Go back to the parent directory" }, { on = "", run = "enter", desc = "Enter the child directory" }, - { on = "H", run = "back", desc = "Go back to the previous directory" }, - { on = "L", run = "forward", desc = "Go forward to the next directory" }, + { on = "H", run = "back", desc = "Back to previous directory" }, + { on = "L", run = "forward", desc = "Forward to next directory" }, # Toggle - { on = "", run = [ "toggle", "arrow 1" ], desc = "Toggle the current selection state" }, - { on = "", run = "toggle_all --state=on", desc = "Select all files" }, - { on = "", run = "toggle_all", desc = "Invert selection of all files" }, + { on = "", run = [ "toggle", "arrow next" ], desc = "Toggle the current selection state" }, + { on = "", run = "toggle_all --state=on", desc = "Select all files" }, + { on = "", run = "toggle_all", desc = "Invert selection of all files" }, # Visual mode { on = "v", run = "visual_mode", desc = "Enter visual mode (selection mode)" }, @@ -105,8 +105,8 @@ keymap = [ # Find { on = "/", run = "find --smart", desc = "Find next file" }, { on = "?", run = "find --previous --smart", desc = "Find previous file" }, - { on = "n", run = "find_arrow", desc = "Goto the next found" }, - { on = "N", run = "find_arrow --previous", desc = "Goto the previous found" }, + { on = "n", run = "find_arrow", desc = "Go to next found" }, + { on = "N", run = "find_arrow --previous", desc = "Go to previous found" }, # Sorting { on = [ ",", "m" ], run = [ "sort mtime --reverse=no", "linemode mtime" ], desc = "Sort by modified time" }, @@ -125,25 +125,25 @@ keymap = [ # Goto { on = [ "g", "h" ], run = "cd ~", desc = "Go home" }, - { on = [ "g", "c" ], run = "cd ~/.config", desc = "Goto ~/.config" }, - { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Goto ~/Downloads" }, + { on = [ "g", "c" ], run = "cd ~/.config", desc = "Go ~/.config" }, + { on = [ "g", "d" ], run = "cd ~/Downloads", desc = "Go ~/Downloads" }, { on = [ "g", "" ], run = "cd --interactive", desc = "Jump interactively" }, # Tabs { on = "t", run = "tab_create --current", desc = "Create a new tab with CWD" }, - { on = "1", run = "tab_switch 0", desc = "Switch to the first tab" }, - { on = "2", run = "tab_switch 1", desc = "Switch to the second tab" }, - { on = "3", run = "tab_switch 2", desc = "Switch to the third tab" }, - { on = "4", run = "tab_switch 3", desc = "Switch to the fourth tab" }, - { on = "5", run = "tab_switch 4", desc = "Switch to the fifth tab" }, - { on = "6", run = "tab_switch 5", desc = "Switch to the sixth tab" }, - { on = "7", run = "tab_switch 6", desc = "Switch to the seventh tab" }, - { on = "8", run = "tab_switch 7", desc = "Switch to the eighth tab" }, - { on = "9", run = "tab_switch 8", desc = "Switch to the ninth tab" }, + { on = "1", run = "tab_switch 0", desc = "Switch to first tab" }, + { on = "2", run = "tab_switch 1", desc = "Switch to second tab" }, + { on = "3", run = "tab_switch 2", desc = "Switch to third tab" }, + { on = "4", run = "tab_switch 3", desc = "Switch to fourth tab" }, + { on = "5", run = "tab_switch 4", desc = "Switch to fifth tab" }, + { on = "6", run = "tab_switch 5", desc = "Switch to sixth tab" }, + { on = "7", run = "tab_switch 6", desc = "Switch to seventh tab" }, + { on = "8", run = "tab_switch 7", desc = "Switch to eighth tab" }, + { on = "9", run = "tab_switch 8", desc = "Switch to ninth tab" }, - { on = "[", run = "tab_switch -1 --relative", desc = "Switch to the previous tab" }, - { on = "]", run = "tab_switch 1 --relative", desc = "Switch to the next tab" }, + { on = "[", run = "tab_switch -1 --relative", desc = "Switch to previous tab" }, + { on = "]", run = "tab_switch 1 --relative", desc = "Switch to next tab" }, { on = "{", run = "tab_swap -1", desc = "Swap current tab with previous tab" }, { on = "}", run = "tab_swap 1", desc = "Swap current tab with next tab" }, @@ -188,13 +188,13 @@ keymap = [ { on = "k", run = "arrow -1", desc = "Move cursor up" }, { on = "j", run = "arrow 1", desc = "Move cursor down" }, - { on = "h", run = "swipe -1", desc = "Swipe to the previous file" }, - { on = "l", run = "swipe 1", desc = "Swipe to the next file" }, + { on = "h", run = "swipe -1", desc = "Swipe to previous file" }, + { on = "l", run = "swipe 1", desc = "Swipe to next file" }, { on = "", run = "arrow -1", desc = "Move cursor up" }, { on = "", run = "arrow 1", desc = "Move cursor down" }, - { on = "", run = "swipe -1", desc = "Swipe to the next file" }, - { on = "", run = "swipe 1", desc = "Swipe to the previous file" }, + { on = "", run = "swipe -1", desc = "Swipe to next file" }, + { on = "", run = "swipe 1", desc = "Swipe to previous file" }, # Copy { on = [ "c", "c" ], run = "copy cell", desc = "Copy selected cell" }, diff --git a/yazi-config/src/layout.rs b/yazi-config/src/layout.rs index 071c25b5..b2f220b7 100644 --- a/yazi-config/src/layout.rs +++ b/yazi-config/src/layout.rs @@ -11,4 +11,6 @@ impl Layout { pub const fn default() -> Self { Self { current: Rect::ZERO, preview: Rect::ZERO, progress: Rect::ZERO } } + + pub const fn limit(&self) -> usize { self.current.height as _ } } diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index 7cb515aa..1f87e7ca 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -27,18 +27,6 @@ pub fn init() -> anyhow::Result<()> { wait_for_key(e)?; try_init(false)?; } - - // TODO: remove this - for c in KEYMAP.mgr.iter().flat_map(|c| c.run.iter()) { - if c.name == "arrow" - && c.first_str().unwrap_or_default().parse::().is_ok_and(|n| n <= -999 || n >= 999) - { - eprintln!("Deprecated command: `arrow -99999999` and `arrow 99999999` have been deprecated, please use `arrow top` and `arrow bot` instead, in your `keymap.toml`. - -See #2294 for more details: https://github.com/sxyazi/yazi/pull/2294"); - } - } - Ok(()) } diff --git a/yazi-core/src/tab/commands/arrow.rs b/yazi-core/src/tab/commands/arrow.rs index e11b52a4..7aaac9a1 100644 --- a/yazi-core/src/tab/commands/arrow.rs +++ b/yazi-core/src/tab/commands/arrow.rs @@ -22,18 +22,6 @@ impl From for Opt { impl Tab { #[yazi_codegen::command] pub fn arrow(&mut self, opt: Opt) { - // TODO: remove this - if let Step::Fixed(n) = opt.step { - if n <= -999999 || n >= 999999 { - yazi_proxy::AppProxy::notify_warn( - "Deprecated command", - "`arrow -99999999` and `arrow 99999999` have been deprecated, please use `arrow top` and `arrow bot` instead. - -See #2294 for more details: https://github.com/sxyazi/yazi/pull/2294", - ); - } - } - if !self.current.arrow(opt.step) { return; } diff --git a/yazi-core/src/tab/folder.rs b/yazi-core/src/tab/folder.rs index 459e5b8c..ccfaf0e7 100644 --- a/yazi-core/src/tab/folder.rs +++ b/yazi-core/src/tab/folder.rs @@ -93,13 +93,13 @@ impl Folder { } pub fn arrow(&mut self, step: impl Into) -> bool { - let step = step.into() as Step; + let new = (step.into() as Step).add(self.cursor, self.files.len(), LAYOUT.get().limit()); let mut b = if self.files.is_empty() { (mem::take(&mut self.cursor), mem::take(&mut self.offset)) != (0, 0) - } else if step.is_positive() { - self.next(step) + } else if new > self.cursor { + self.next(new) } else { - self.prev(step) + self.prev(new) }; self.trace = self.hovered().filter(|_| b).map(|h| h.urn_owned()).or(self.trace.take()); @@ -124,7 +124,7 @@ impl Folder { } pub fn sync_page(&mut self, force: bool) { - let limit = LAYOUT.get().current.height as usize; + let limit = LAYOUT.get().limit(); if limit == 0 { return; } @@ -135,14 +135,14 @@ impl Folder { } } - fn next(&mut self, step: Step) -> bool { + fn next(&mut self, new: usize) -> bool { let old = (self.cursor, self.offset); let len = self.files.len(); - let limit = LAYOUT.get().current.height as usize; + let limit = LAYOUT.get().limit(); let scrolloff = (limit / 2).min(MGR.scrolloff as usize); - self.cursor = step.add(self.cursor, limit).min(len.saturating_sub(1)); + self.cursor = new.min(len.saturating_sub(1)); self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) { self.offset.min(len.saturating_sub(1)) } else { @@ -152,14 +152,14 @@ impl Folder { old != (self.cursor, self.offset) } - fn prev(&mut self, step: Step) -> bool { + fn prev(&mut self, new: usize) -> bool { let old = (self.cursor, self.offset); let max = self.files.len().saturating_sub(1); - let limit = LAYOUT.get().current.height as usize; + let limit = LAYOUT.get().limit(); let scrolloff = (limit / 2).min(MGR.scrolloff as usize); - self.cursor = step.add(self.cursor, limit).min(max); + self.cursor = new.min(max); self.offset = if self.cursor < self.offset + scrolloff { self.offset.saturating_sub(old.0 - self.cursor) } else { @@ -173,7 +173,7 @@ impl Folder { let old = self.offset; let len = self.files.len(); - let limit = LAYOUT.get().current.height as usize; + let limit = LAYOUT.get().limit(); let scrolloff = (limit / 2).min(MGR.scrolloff as usize); self.offset = if self.cursor < (self.offset + limit).min(len).saturating_sub(scrolloff) { @@ -192,7 +192,7 @@ impl Folder { pub fn paginate(&self, page: usize) -> &[File] { let len = self.files.len(); - let limit = LAYOUT.get().current.height as usize; + let limit = LAYOUT.get().limit(); let start = (page.saturating_sub(1) * limit).min(len.saturating_sub(1)); let end = ((page + 2) * limit).min(len); diff --git a/yazi-fs/src/step.rs b/yazi-fs/src/step.rs index 25f8c6fe..976ffcc4 100644 --- a/yazi-fs/src/step.rs +++ b/yazi-fs/src/step.rs @@ -2,10 +2,12 @@ use std::{num::ParseIntError, str::FromStr}; use yazi_shared::event::Data; -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] pub enum Step { Top, Bot, + Prev, + Next, Fixed(isize), Percent(i8), } @@ -25,6 +27,8 @@ impl FromStr for Step { Ok(match s { "top" => Self::Top, "bot" => Self::Bot, + "prev" => Self::Prev, + "next" => Self::Next, s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?), s => Self::Fixed(s.parse()?), }) @@ -44,24 +48,29 @@ impl TryFrom<&Data> for Step { } impl Step { - #[inline] - pub fn add(self, pos: usize, limit: usize) -> usize { + pub fn add(self, pos: usize, len: usize, limit: usize) -> usize { + if len == 0 { + return 0; + } + let fixed = match self { Self::Top => return 0, - Self::Bot => return usize::MAX, + Self::Bot => return len - 1, + Self::Prev => -1, + Self::Next => 1, Self::Fixed(n) => n, Self::Percent(0) => 0, Self::Percent(n) => n as isize * limit as isize / 100, }; - if fixed > 0 { pos + fixed as usize } else { pos.saturating_sub(fixed.unsigned_abs()) } - } - #[inline] - pub fn is_positive(self) -> bool { - match self { - Self::Top | Self::Bot => false, - Self::Fixed(n) => n > 0, - Self::Percent(n) => n > 0, + if fixed == 0 { + pos + } else if matches!(self, Self::Prev | Self::Next) { + fixed.saturating_add_unsigned(pos).rem_euclid(len as _) as _ + } else if fixed > 0 { + pos + fixed as usize + } else { + pos.saturating_sub(fixed.unsigned_abs()) } } } diff --git a/yazi-plugin/src/isolate/preload.rs b/yazi-plugin/src/isolate/preload.rs index e28fd813..4461a365 100644 --- a/yazi-plugin/src/isolate/preload.rs +++ b/yazi-plugin/src/isolate/preload.rs @@ -1,4 +1,4 @@ -use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table, Value}; +use mlua::{ExternalError, ExternalResult, IntoLua, ObjectLike, Table}; use tokio::runtime::Handle; use yazi_config::LAYOUT; use yazi_dds::Sendable; @@ -28,25 +28,7 @@ pub async fn preload( ("skip", 0.into_lua(&lua)?), ])?; - let (ok, mut err): (Value, Option) = - Handle::current().block_on(plugin.call_async_method("preload", job))?; - - // TODO: remove this - let ok = match ok { - Value::Boolean(b) => b, - Value::Integer(n) => { - crate::deprecate!(lua, "The integer return value of `preload()` has been deprecated since 25.01.27, please use the new `(bool, error)` instead, in your {}. - -See #2253 for more information: https://github.com/sxyazi/yazi/pull/2253"); - if n as u8 & 1 == 0 { - err = Some(Error::Custom(format!("Returned {n} when running the preloader"))); - } - n as u8 & 1 == 1 - }, - _ => Err("The first return value of `preload()` must be a bool".into_lua_err())?, - }; - - Ok((ok, err)) + Handle::current().block_on(plugin.call_async_method("preload", job)) }) .await .into_lua_err()? diff --git a/yazi-plugin/src/utils/utils.rs b/yazi-plugin/src/utils/utils.rs index ff27922f..839e3e46 100644 --- a/yazi-plugin/src/utils/utils.rs +++ b/yazi-plugin/src/utils/utils.rs @@ -60,7 +60,6 @@ pub fn compose(lua: &Lua, isolate: bool) -> mlua::Result { b"target_family" => Utils::target_family(lua)?, // Text - b"md5" => Utils::hash(lua, true)?, // TODO: deprecate this in the future b"hash" => Utils::hash(lua, false)?, b"quote" => Utils::quote(lua)?, b"truncate" => Utils::truncate(lua)?,