mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 14:51:03 +00:00
feat: H/M/L Vim-like motion for moving cursor relative to viewport (#3970)
Co-authored-by: sxyazi <sxyazi@gmail.com>
This commit is contained in:
parent
581886a355
commit
b1ee2e72ff
7 changed files with 47 additions and 15 deletions
|
|
@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
- Drag and drop ([#4005])
|
||||
- Bulk create ([#3793])
|
||||
- Make help menu a command palette ([#4074])
|
||||
- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970])
|
||||
- Dynamic keymap Lua API ([#4031])
|
||||
- New `ui.Input` element ([#4040])
|
||||
- Image preview with Überzug++ on Niri ([#3990])
|
||||
|
|
@ -1749,6 +1750,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
|
|||
[#3906]: https://github.com/sxyazi/yazi/pull/3906
|
||||
[#3934]: https://github.com/sxyazi/yazi/pull/3934
|
||||
[#3943]: https://github.com/sxyazi/yazi/pull/3943
|
||||
[#3970]: https://github.com/sxyazi/yazi/pull/3970
|
||||
[#3989]: https://github.com/sxyazi/yazi/pull/3989
|
||||
[#3990]: https://github.com/sxyazi/yazi/pull/3990
|
||||
[#4005]: https://github.com/sxyazi/yazi/pull/4005
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ impl Actor for Arrow {
|
|||
let len = confirm.list.line_count(area.width);
|
||||
|
||||
let old = confirm.offset;
|
||||
confirm.offset = form.step.add(confirm.offset, len, area.height as _);
|
||||
confirm.offset = form.step.add(confirm.offset, len, area.height as _, 0, 0);
|
||||
|
||||
succ!(render!(old != confirm.offset));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ impl Actor for TabSwap {
|
|||
fn act(cx: &mut Ctx, form: Self::Form) -> Result<Data> {
|
||||
let tabs = cx.tabs_mut();
|
||||
|
||||
let new = form.step.add(tabs.cursor, tabs.len(), 0);
|
||||
let new = form.step.add(tabs.cursor, tabs.len(), 0, 0, 0);
|
||||
if new == tabs.cursor {
|
||||
succ!();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ impl Actor for Arrow {
|
|||
let spot = &mut cx.tab_mut().spot;
|
||||
let Some(lock) = &mut spot.lock else { succ!() };
|
||||
|
||||
let new = form.step.add(spot.skip, lock.len().unwrap_or(u16::MAX as _), 0);
|
||||
let new = form.step.add(spot.skip, lock.len().unwrap_or(u16::MAX as _), 0, 0, 0);
|
||||
let Some(old) = lock.selected() else {
|
||||
return act!(mgr:spot, cx, new);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ impl Actor for Arrow {
|
|||
let tasks = &mut cx.tasks;
|
||||
|
||||
let old = tasks.cursor;
|
||||
tasks.cursor = form.step.add(tasks.cursor, tasks.snaps.len(), Tasks::limit());
|
||||
tasks.cursor = form.step.add(tasks.cursor, tasks.snaps.len(), Tasks::limit(), 0, 0);
|
||||
|
||||
succ!(render!(tasks.cursor != old));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,19 +7,35 @@ pub trait Scrollable {
|
|||
fn cursor_mut(&mut self) -> &mut usize;
|
||||
fn offset_mut(&mut self) -> &mut usize;
|
||||
|
||||
/// End of the visible window (exclusive)
|
||||
fn visible_end(&mut self) -> usize { self.total().min(*self.offset_mut() + self.limit()) }
|
||||
|
||||
/// Cursor is past the bottom safe zone
|
||||
fn needs_scroll_down(&mut self, cursor: usize) -> bool {
|
||||
cursor >= self.visible_end().saturating_sub(self.scrolloff())
|
||||
}
|
||||
|
||||
/// Cursor is in the top scrolloff zone
|
||||
fn needs_scroll_up(&mut self, cursor: usize) -> bool {
|
||||
cursor < *self.offset_mut() + self.scrolloff()
|
||||
}
|
||||
|
||||
fn scroll(&mut self, step: impl Into<Step>) -> bool {
|
||||
let new = step.into().add(*self.cursor_mut(), self.total(), self.limit());
|
||||
if new > *self.cursor_mut() { self.next(new) } else { self.prev(new) }
|
||||
let old = *self.cursor_mut();
|
||||
let new =
|
||||
step.into().add(old, self.total(), self.limit(), *self.offset_mut(), self.scrolloff());
|
||||
|
||||
if new > old { self.next(new) } else { self.prev(new) }
|
||||
}
|
||||
|
||||
fn next(&mut self, n_cur: usize) -> bool {
|
||||
let (o_cur, o_off) = (*self.cursor_mut(), *self.offset_mut());
|
||||
let (total, limit, scrolloff) = (self.total(), self.limit(), self.scrolloff());
|
||||
let (total, limit) = (self.total(), self.limit());
|
||||
|
||||
let n_off = if n_cur < total.min(o_off + limit).saturating_sub(scrolloff) {
|
||||
o_off.min(total.saturating_sub(1))
|
||||
} else {
|
||||
let n_off = if self.needs_scroll_down(n_cur) {
|
||||
total.saturating_sub(limit).min(o_off + n_cur - o_cur)
|
||||
} else {
|
||||
o_off.min(total.saturating_sub(1))
|
||||
};
|
||||
|
||||
*self.cursor_mut() = n_cur;
|
||||
|
|
@ -30,7 +46,7 @@ pub trait Scrollable {
|
|||
fn prev(&mut self, n_cur: usize) -> bool {
|
||||
let (o_cur, o_off) = (*self.cursor_mut(), *self.offset_mut());
|
||||
|
||||
let n_off = if n_cur < o_off + self.scrolloff() {
|
||||
let n_off = if self.needs_scroll_up(n_cur) {
|
||||
o_off.saturating_sub(o_cur - n_cur)
|
||||
} else {
|
||||
self.total().saturating_sub(1).min(o_off)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ pub enum Step {
|
|||
Next,
|
||||
Offset(isize),
|
||||
Percent(i8),
|
||||
Vp(i8),
|
||||
}
|
||||
|
||||
impl Default for Step {
|
||||
|
|
@ -30,6 +31,7 @@ impl FromStr for Step {
|
|||
"prev" => Self::Prev,
|
||||
"next" => Self::Next,
|
||||
s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?),
|
||||
s if s.ends_with("vp") => Self::Vp(s[..s.len() - 2].parse()?),
|
||||
s => Self::Offset(s.parse()?),
|
||||
})
|
||||
}
|
||||
|
|
@ -76,7 +78,7 @@ impl<'de> Deserialize<'de> for Step {
|
|||
}
|
||||
|
||||
impl Step {
|
||||
pub fn add(self, pos: usize, len: usize, limit: usize) -> usize {
|
||||
pub fn add(self, pos: usize, len: usize, limit: usize, offset: usize, scrolloff: usize) -> usize {
|
||||
if len == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -84,6 +86,20 @@ impl Step {
|
|||
let off = match self {
|
||||
Self::Top => return 0,
|
||||
Self::Bot => return len - 1,
|
||||
Self::Vp(n) if limit == 0 => n as isize * len as isize / 100,
|
||||
Self::Vp(n) => {
|
||||
let end = len.min(offset + limit);
|
||||
let Some(count) = end.checked_sub(offset + 1) else { return 0 };
|
||||
let scrolloff = scrolloff.min(count / 2);
|
||||
|
||||
// Clamp relative position in window to not reach into any scrolloff region.
|
||||
// Still allow reaching the real list start and end if already visible.
|
||||
let target = offset.saturating_add_signed(n as isize * count as isize / 100);
|
||||
let min = if offset == 0 { 0 } else { offset + scrolloff };
|
||||
let max = end - 1 - if end == len { 0 } else { scrolloff };
|
||||
|
||||
target.clamp(min, max) as isize - pos as isize
|
||||
}
|
||||
Self::Prev => -1,
|
||||
Self::Next => 1,
|
||||
Self::Offset(n) => n,
|
||||
|
|
@ -93,10 +109,8 @@ impl Step {
|
|||
|
||||
if matches!(self, Self::Prev | Self::Next) {
|
||||
off.saturating_add_unsigned(pos).rem_euclid(len as _) as _
|
||||
} else if off >= 0 {
|
||||
pos.saturating_add_signed(off)
|
||||
} else {
|
||||
pos.saturating_sub(off.unsigned_abs())
|
||||
pos.saturating_add_signed(off)
|
||||
}
|
||||
.min(len - 1)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue