From 90df0a2b5aaeb96e3ada3f9d1f13914d5379555b Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 1 May 2024 14:38:17 +0530 Subject: [PATCH] vimlike_scrolling -> paginated_scrolling Inspired by @ElSamhaa 's PR https://github.com/sayanarijit/xplr/pull/704 --- benches/criterion.rs | 3 +- docs/en/src/upgrade-guide.md | 6 +- src/app.rs | 1 - src/config.rs | 5 +- src/directory_buffer.rs | 226 ----------------------------------- src/explorer.rs | 51 ++++---- src/init.lua | 10 +- src/runner.rs | 3 - src/ui.rs | 40 ++++++- 9 files changed, 76 insertions(+), 269 deletions(-) diff --git a/benches/criterion.rs b/benches/criterion.rs index f9bddee..cff705b 100644 --- a/benches/criterion.rs +++ b/benches/criterion.rs @@ -98,6 +98,7 @@ fn draw_benchmark(c: &mut Criterion) { }); let lua = mlua::Lua::new(); + let mut ui = ui::UI::new(&lua); let mut app = app::App::create("xplr".into(), None, PWD.into(), &lua, None, [].into()) .expect("failed to create app"); @@ -121,7 +122,7 @@ fn draw_benchmark(c: &mut Criterion) { c.bench_function("draw on terminal", |b| { b.iter(|| { - terminal.draw(|f| ui::draw(f, &mut app, &lua)).unwrap(); + terminal.draw(|f| ui.draw(f, &app)).unwrap(); }) }); diff --git a/docs/en/src/upgrade-guide.md b/docs/en/src/upgrade-guide.md index 38d5347..af56522 100644 --- a/docs/en/src/upgrade-guide.md +++ b/docs/en/src/upgrade-guide.md @@ -128,8 +128,10 @@ compatibility. and move focused or selected files, without having to change directory. - Use `xplr.util.debug()` to debug lua values. - Since v0.21.8: - - You can set `xplr.config.general.vimlike_scrolling = true` to enable - vim-like scrolling. + - Scroll behavior will default to vim-like continuous scrolling. You can set + `xplr.config.general.paginated_scrolling = true` to revert back to the + paginated scrolling. + - Set `xplr.config.general.scroll_padding` to customize the scroll padding. Thanks to @noahmayr for contributing to a major part of this release. diff --git a/src/app.rs b/src/app.rs index dd521d2..ea6384e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -753,7 +753,6 @@ impl App { self.pwd.clone().into(), focus.as_ref().map(PathBuf::from), self.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), - self.config.general.vimlike_scrolling, ) { Ok(dir) => self.set_directory(dir), Err(e) => { diff --git a/src/config.rs b/src/config.rs index 64663c5..27ceeaf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -355,7 +355,10 @@ pub struct GeneralConfig { pub global_key_bindings: KeyBindings, #[serde(default)] - pub vimlike_scrolling: bool, + pub paginated_scrolling: bool, + + #[serde(default)] + pub scroll_padding: usize, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index a625b40..c6e755d 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -1,121 +1,7 @@ use crate::node::Node; use serde::{Deserialize, Serialize}; -use std::cmp::Ordering; use time::OffsetDateTime; -#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] -pub struct ScrollState { - pub current_focus: usize, - pub last_focus: Option, - pub skipped_rows: usize, - /* The number of visible next lines when scrolling towards either ends of the view port */ - pub initial_preview_cushion: usize, - - pub vimlike_scrolling: bool, -} - -impl ScrollState { - pub fn new(current_focus: usize, total: usize, vimlike_scrolling: bool) -> Self { - let initial_preview_cushion = 5; - Self { - current_focus, - last_focus: None, - skipped_rows: 0, - initial_preview_cushion, - vimlike_scrolling, - } - .update_skipped_rows(initial_preview_cushion + 1, total) - } - - pub fn set_focus(mut self, current_focus: usize) -> Self { - self.last_focus = Some(self.current_focus); - self.current_focus = current_focus; - self - } - - pub fn update_skipped_rows(self, height: usize, total: usize) -> Self { - if self.vimlike_scrolling { - self.update_skipped_rows_vimlike(height, total) - } else { - self.update_skipped_rows_paginated(height) - } - } - - pub fn update_skipped_rows_paginated(mut self, height: usize) -> Self { - self.skipped_rows = height * (self.current_focus / height.max(1)); - self - } - - pub fn update_skipped_rows_vimlike(mut self, height: usize, total: usize) -> Self { - let preview_cushion = if height >= self.initial_preview_cushion * 3 { - self.initial_preview_cushion - } else if height >= 9 { - 3 - } else if height >= 3 { - 1 - } else { - 0 - }; - - let current_focus = self.current_focus; - let last_focus = self.last_focus; - let first_visible_row = self.skipped_rows; - - // Calculate the cushion rows at the start and end of the view port - let start_cushion_row = first_visible_row + preview_cushion; - let end_cushion_row = (first_visible_row + height) - .saturating_sub(preview_cushion + 1) - .min(total.saturating_sub(preview_cushion + 1)); - - self.skipped_rows = if current_focus == 0 { - // When focus goes to first node - 0 - } else if current_focus == total.saturating_sub(1) { - // When focus goes to last node - total.saturating_sub(height) - } else if current_focus > start_cushion_row && current_focus <= end_cushion_row { - // If within cushioned area; do nothing - first_visible_row - } else if let Some(last_focus) = last_focus { - match current_focus.cmp(&last_focus) { - Ordering::Greater => { - // When scrolling down the cushioned area - if current_focus > total.saturating_sub(preview_cushion + 1) { - // When focusing the last nodes; always view the full last page - total.saturating_sub(height) - } else { - // When scrolling down the cushioned area without reaching the last nodes - current_focus - .saturating_sub(height.saturating_sub(preview_cushion + 1)) - } - } - - Ordering::Less => { - // When scrolling up the cushioned area - if current_focus < preview_cushion { - // When focusing the first nodes; always view the full first page - 0 - } else if current_focus > end_cushion_row { - // When scrolling up from the last rows; do nothing - first_visible_row - } else { - // When scrolling up the cushioned area without reaching the first nodes - current_focus.saturating_sub(preview_cushion) - } - } - Ordering::Equal => { - // Do nothing - first_visible_row - } - } - } else { - // Just entered dir - first_visible_row - }; - self - } -} - #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct DirectoryBuffer { pub parent: String, @@ -149,115 +35,3 @@ fn now() -> OffsetDateTime { .ok() .unwrap_or_else(OffsetDateTime::now_utc) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_update_skipped_rows_paginated() { - let state = ScrollState { - current_focus: 10, - last_focus: Some(8), - skipped_rows: 0, - initial_preview_cushion: 5, - vimlike_scrolling: false, - }; - - let height = 5; - let total = 100; - - let state = state.update_skipped_rows(height, total); - assert_eq!( - state.skipped_rows, - height * (state.current_focus / height.max(1)) - ); - } - - #[test] - fn test_update_skipped_rows_entered_directory() { - let state = ScrollState { - current_focus: 100, - last_focus: None, - skipped_rows: 0, - initial_preview_cushion: 5, - vimlike_scrolling: true, - }; - - let height = 5; - let total = 200; - - let result = state.update_skipped_rows(height, total).skipped_rows; - assert_eq!(result, 0); - } - - #[test] - fn test_calc_skipped_rows_top_of_directory() { - let state = ScrollState { - current_focus: 0, - last_focus: Some(8), - skipped_rows: 5, - initial_preview_cushion: 5, - vimlike_scrolling: true, - }; - - let height = 5; - let total = 20; - - let result = state.update_skipped_rows(height, total).skipped_rows; - assert_eq!(result, 0); - } - - #[test] - fn test_calc_skipped_rows_bottom_of_directory() { - let state = ScrollState { - current_focus: 19, - last_focus: Some(18), - skipped_rows: 15, - initial_preview_cushion: 5, - vimlike_scrolling: true, - }; - - let height = 5; - let total = 20; - - let result = state.update_skipped_rows(height, total).skipped_rows; - assert_eq!(result, 15); - } - - #[test] - fn test_calc_skipped_rows_scrolling_down() { - let state = ScrollState { - current_focus: 12, - last_focus: Some(10), - skipped_rows: 10, - initial_preview_cushion: 5, - vimlike_scrolling: true, - }; - - let height = 5; - let total = 20; - - let result = state.update_skipped_rows(height, total).skipped_rows; - assert_eq!(result, 10); - } - - #[test] - fn test_calc_skipped_rows_scrolling_up() { - let state = ScrollState { - current_focus: 8, - last_focus: Some(10), - skipped_rows: 10, - initial_preview_cushion: 5, - vimlike_scrolling: true, - }; - - let height = 5; - let total = 20; - - let result = state.update_skipped_rows(height, total).skipped_rows; - assert_eq!(result, 7); - } - - // Add more tests for other scenarios... -} diff --git a/src/explorer.rs b/src/explorer.rs index 2ab45dc..1f66d56 100644 --- a/src/explorer.rs +++ b/src/explorer.rs @@ -45,7 +45,6 @@ pub(crate) fn explore_sync( parent: PathBuf, focused_path: Option, fallback_focus: usize, - vimlike_scrolling: bool, ) -> Result { let nodes = explore(&parent, &config)?; let focus_index = if config.searcher.is_some() { @@ -74,33 +73,26 @@ pub(crate) fn explore_async( parent: PathBuf, focused_path: Option, fallback_focus: usize, - vimlike_scrolling: bool, tx_msg_in: Sender, ) { thread::spawn(move || { - explore_sync( - config, - parent.clone(), - focused_path, - fallback_focus, - vimlike_scrolling, - ) - .and_then(|buf| { - tx_msg_in - .send(Task::new( - MsgIn::Internal(InternalMsg::SetDirectory(buf)), - None, - )) - .map_err(Error::new) - }) - .unwrap_or_else(|e| { - tx_msg_in - .send(Task::new( - MsgIn::External(ExternalMsg::LogError(e.to_string())), - None, - )) - .unwrap_or_default(); // Let's not panic if xplr closes. - }) + explore_sync(config, parent.clone(), focused_path, fallback_focus) + .and_then(|buf| { + tx_msg_in + .send(Task::new( + MsgIn::Internal(InternalMsg::SetDirectory(buf)), + None, + )) + .map_err(Error::new) + }) + .unwrap_or_else(|e| { + tx_msg_in + .send(Task::new( + MsgIn::External(ExternalMsg::LogError(e.to_string())), + None, + )) + .unwrap_or_default(); // Let's not panic if xplr closes. + }) }); } @@ -109,7 +101,6 @@ pub(crate) fn explore_recursive_async( parent: PathBuf, focused_path: Option, fallback_focus: usize, - vimlike_scrolling: bool, tx_msg_in: Sender, ) { explore_async( @@ -117,7 +108,6 @@ pub(crate) fn explore_recursive_async( parent.clone(), focused_path, fallback_focus, - vimlike_scrolling, tx_msg_in.clone(), ); if let Some(grand_parent) = parent.parent() { @@ -126,7 +116,6 @@ pub(crate) fn explore_recursive_async( grand_parent.into(), parent.file_name().map(|p| p.into()), 0, - vimlike_scrolling, tx_msg_in, ); } @@ -141,7 +130,7 @@ mod tests { let config = ExplorerConfig::default(); let path = PathBuf::from("."); - let r = explore_sync(config, path, None, 0, false); + let r = explore_sync(config, path, None, 0); assert!(r.is_ok()); } @@ -151,7 +140,7 @@ mod tests { let config = ExplorerConfig::default(); let path = PathBuf::from("/there/is/no/path"); - let r = explore_sync(config, path, None, 0, false); + let r = explore_sync(config, path, None, 0); assert!(r.is_err()); } @@ -180,7 +169,7 @@ mod tests { let path = PathBuf::from("."); let (tx_msg_in, rx_msg_in) = mpsc::channel(); - explore_async(config, path, None, 0, false, tx_msg_in.clone()); + explore_async(config, path, None, 0, tx_msg_in.clone()); let task = rx_msg_in.recv().unwrap(); let dbuf = extract_dirbuf_from_msg(task.msg); diff --git a/src/init.lua b/src/init.lua index 903a651..0e2ed3c 100644 --- a/src/init.lua +++ b/src/init.lua @@ -91,10 +91,16 @@ xplr.config.general.enable_recover_mode = false -- Type: boolean xplr.config.general.hide_remaps_in_help_menu = false --- Set it to `true` if you want vim-like scrolling. +-- Set it to `true` if you want paginated scrolling. -- -- Type: boolean -xplr.config.general.vimlike_scrolling = false +xplr.config.general.paginated_scrolling = false + +-- Set the padding value to the scroll area. Only applicable when +-- `xplr.config.general.paginated_scrolling` is set to `false`. +-- +-- Type: boolean +xplr.config.general.scroll_padding = 5 -- Set it to `true` if you want the cursor to stay in the same position when -- the focus is on the first path and you navigate to the previous path diff --git a/src/runner.rs b/src/runner.rs index a3c900e..c5e2c1e 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -281,7 +281,6 @@ impl Runner { app.pwd.clone().into(), self.focused_path, app.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), - app.config.general.vimlike_scrolling, tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; @@ -437,7 +436,6 @@ impl Runner { .as_ref() .map(|d| d.focus) .unwrap_or(0), - app.config.general.vimlike_scrolling, tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; @@ -453,7 +451,6 @@ impl Runner { .as_ref() .map(|d| d.focus) .unwrap_or(0), - app.config.general.vimlike_scrolling, tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; diff --git a/src/ui.rs b/src/ui.rs index d1813c8..dfebd7c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -743,12 +743,18 @@ pub fn block<'a>(config: PanelUiConfig, default_title: String) -> Block<'a> { pub struct UI<'lua> { pub lua: &'lua Lua, pub screen_size: TuiRect, + pub scrolltop: usize, } impl<'lua> UI<'lua> { pub fn new(lua: &'lua Lua) -> Self { let screen_size = Default::default(); - Self { lua, screen_size } + let scrolltop = 0; + Self { + lua, + scrolltop, + screen_size, + } } } @@ -766,10 +772,40 @@ impl UI<'_> { .directory_buffer .as_ref() .map(|dir| { + // Scroll + if app.config.general.paginated_scrolling { + // Paginated scrolling + self.scrolltop = height * (dir.focus / height.max(1)) + } else { + // vim-like-scrolling + self.scrolltop = match dir.focus.cmp(&self.scrolltop) { + Ordering::Greater => { + // Scrolling down + if dir.focus >= self.scrolltop + height { + dir.focus - height + 1 + } else { + self.scrolltop + } + } + Ordering::Less => dir.focus, + Ordering::Equal => self.scrolltop, + }; + + // Add padding if possible + let padding = app.config.general.scroll_padding; + if padding != 0 { + if dir.focus < self.scrolltop + padding { + self.scrolltop = dir.focus.saturating_sub(padding); + } else if dir.focus >= self.scrolltop + height - padding { + self.scrolltop = dir.focus + padding - height + 1; + }; + } + }; + dir.nodes .iter() .enumerate() - .skip(height * (dir.focus / height.max(1))) + .skip(self.scrolltop) .take(height) .map(|(index, node)| { let is_focused = dir.focus == index;