From e834242f5d98bd0049b580088dfed1652abf8347 Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Sun, 7 Apr 2024 23:56:14 +0300 Subject: [PATCH 01/14] Adds vim-like scrolling --- benches/criterion.rs | 2 +- src/app.rs | 45 +++++++++++++++----------- src/directory_buffer.rs | 70 ++++++++++++++++++++++++++++++++++++++--- src/runner.rs | 13 +++++--- src/ui.rs | 27 +++++++++------- 5 files changed, 118 insertions(+), 39 deletions(-) diff --git a/benches/criterion.rs b/benches/criterion.rs index 04d31a4..f9bddee 100644 --- a/benches/criterion.rs +++ b/benches/criterion.rs @@ -121,7 +121,7 @@ fn draw_benchmark(c: &mut Criterion) { c.bench_function("draw on terminal", |b| { b.iter(|| { - terminal.draw(|f| ui::draw(f, &app, &lua)).unwrap(); + terminal.draw(|f| ui::draw(f, &mut app, &lua)).unwrap(); }) }); diff --git a/src/app.rs b/src/app.rs index 49f3b33..3a32cee 100644 --- a/src/app.rs +++ b/src/app.rs @@ -752,7 +752,10 @@ impl App { self.explorer_config.clone(), self.pwd.clone().into(), focus.as_ref().map(PathBuf::from), - self.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), + self.directory_buffer + .as_ref() + .map(|d| d.scroll_state.current_focus) + .unwrap_or(0), ) { Ok(dir) => self.set_directory(dir), Err(e) => { @@ -791,7 +794,7 @@ impl App { } } - dir.focus = 0; + dir.scroll_state.set_focus(0); if save_history { if let Some(n) = self.focused_node() { @@ -809,7 +812,7 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.focus = dir.total.saturating_sub(1); + dir.scroll_state.set_focus(dir.total.saturating_sub(1)); if let Some(n) = dir.focused_node() { self.history = history.push(n.absolute_path.clone()); @@ -822,14 +825,15 @@ impl App { let bounded = self.config.general.enforce_bounded_index_navigation; if let Some(dir) = self.directory_buffer_mut() { - dir.focus = if dir.focus == 0 { + if dir.scroll_state.current_focus == 0 { if bounded { - dir.focus + dir.scroll_state.set_focus(dir.scroll_state.current_focus); } else { - dir.total.saturating_sub(1) + dir.scroll_state.set_focus(dir.total.saturating_sub(1)); } } else { - dir.focus.saturating_sub(1) + dir.scroll_state + .set_focus(dir.scroll_state.current_focus.saturating_sub(1)); }; }; Ok(self) @@ -882,7 +886,8 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.focus = dir.focus.saturating_sub(index); + dir.scroll_state + .set_focus(dir.scroll_state.current_focus.saturating_sub(index)); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -907,14 +912,15 @@ impl App { let bounded = self.config.general.enforce_bounded_index_navigation; if let Some(dir) = self.directory_buffer_mut() { - dir.focus = if (dir.focus + 1) == dir.total { + if (dir.scroll_state.current_focus + 1) == dir.total { if bounded { - dir.focus + dir.scroll_state.set_focus(dir.scroll_state.current_focus); } else { - 0 + dir.scroll_state.set_focus(0); } } else { - dir.focus + 1 + dir.scroll_state + .set_focus(dir.scroll_state.current_focus + 1); } }; Ok(self) @@ -967,10 +973,12 @@ impl App { history = history.push(n.absolute_path.clone()); } - dir.focus = dir - .focus - .saturating_add(index) - .min(dir.total.saturating_sub(1)); + dir.scroll_state.set_focus( + dir.scroll_state + .current_focus + .saturating_add(index) + .min(dir.total.saturating_sub(1)), + ); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); @@ -1238,7 +1246,8 @@ impl App { fn focus_by_index(mut self, index: usize) -> Result { let history = self.history.clone(); if let Some(dir) = self.directory_buffer_mut() { - dir.focus = index.min(dir.total.saturating_sub(1)); + dir.scroll_state + .set_focus(index.min(dir.total.saturating_sub(1))); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -1275,7 +1284,7 @@ impl App { history = history.push(n.absolute_path.clone()); } } - dir_buf.focus = focus; + dir_buf.scroll_state.set_focus(focus); if save_history { if let Some(n) = dir_buf.focused_node() { self.history = history.push(n.absolute_path.clone()); diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index c6e755d..cf6a5c1 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -2,31 +2,93 @@ use crate::node::Node; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct ScrollState { + pub current_focus: usize, + pub last_focus: Option, + pub skipped_rows: usize, +} + +impl ScrollState { + /* The number of visible next lines when scrolling towards either ends of the view port */ + pub const PREVIEW_CUSHION: usize = 3; + + pub fn set_focus(&mut self, current_focus: usize) { + self.last_focus = Some(self.current_focus); + self.current_focus = current_focus; + } + + pub fn calc_skipped_rows(&mut self, height: usize, total: usize) -> usize { + let current_focus = self.current_focus; + let last_focus = self.last_focus; + let first_visible_row = self.skipped_rows; + let start_cushion_row = first_visible_row + ScrollState::PREVIEW_CUSHION; + let end_cushion_row = (first_visible_row + height) + .saturating_sub(ScrollState::PREVIEW_CUSHION + 1); + + let vim_scrolling_enabled = true; + + if !vim_scrolling_enabled { + height * (self.current_focus / height.max(1)) + } else if last_focus == None { + // Just entered the directory + 0 + } else if current_focus == 0 { + 0 + } else if current_focus == total.saturating_sub(1) { + total.saturating_sub(height) + } else if current_focus > last_focus.unwrap() { + // Scrolling down + if current_focus <= end_cushion_row { + first_visible_row + } else if total <= (current_focus + ScrollState::PREVIEW_CUSHION) { + first_visible_row + } else { + (self.current_focus + ScrollState::PREVIEW_CUSHION + 1) + .saturating_sub(height) + } + } else { + // Scrolling up + if current_focus >= start_cushion_row { + first_visible_row + } else if current_focus <= ScrollState::PREVIEW_CUSHION { + 0 + } else { + current_focus.saturating_sub(ScrollState::PREVIEW_CUSHION) + } + } + } +} + #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct DirectoryBuffer { pub parent: String, pub nodes: Vec, pub total: usize, - pub focus: usize, + pub scroll_state: ScrollState, #[serde(skip, default = "now")] pub explored_at: OffsetDateTime, } impl DirectoryBuffer { - pub fn new(parent: String, nodes: Vec, focus: usize) -> Self { + pub fn new(parent: String, nodes: Vec, current_focus: usize) -> Self { let total = nodes.len(); Self { parent, nodes, total, - focus, + scroll_state: ScrollState { + current_focus, + last_focus: None, + skipped_rows: 0, + }, explored_at: now(), } } pub fn focused_node(&self) -> Option<&Node> { - self.nodes.get(self.focus) + self.nodes.get(self.scroll_state.current_focus) } } diff --git a/src/runner.rs b/src/runner.rs index 4b80274..7c15bfc 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -89,7 +89,7 @@ fn call( let focus_index = app .directory_buffer .as_ref() - .map(|d| d.focus) + .map(|d| d.scroll_state.current_focus) .unwrap_or_default() .to_string(); @@ -279,7 +279,10 @@ impl Runner { app.explorer_config.clone(), app.pwd.clone().into(), self.focused_path, - app.directory_buffer.as_ref().map(|d| d.focus).unwrap_or(0), + app.directory_buffer + .as_ref() + .map(|d| d.scroll_state.current_focus) + .unwrap_or(0), tx_msg_in.clone(), ); tx_pwd_watcher.send(app.pwd.clone())?; @@ -430,7 +433,7 @@ impl Runner { .map(|n| n.relative_path.clone().into()), app.directory_buffer .as_ref() - .map(|d| d.focus) + .map(|d| d.scroll_state.current_focus) .unwrap_or(0), tx_msg_in.clone(), ); @@ -445,7 +448,7 @@ impl Runner { .map(|n| n.relative_path.clone().into()), app.directory_buffer .as_ref() - .map(|d| d.focus) + .map(|d| d.scroll_state.current_focus) .unwrap_or(0), tx_msg_in.clone(), ); @@ -493,7 +496,7 @@ impl Runner { } // UI - terminal.draw(|f| ui::draw(f, &app, &lua))?; + terminal.draw(|f| ui::draw(f, &mut app, &lua))?; } EnableMouse => { diff --git a/src/ui.rs b/src/ui.rs index 0d3dda0..838b1ef 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -722,7 +722,7 @@ fn draw_table( f: &mut Frame, screen_size: TuiRect, layout_size: TuiRect, - app: &app::App, + app: &mut app::App, lua: &Lua, ) { let panel_config = &app.config.general.panel_ui; @@ -735,15 +735,16 @@ fn draw_table( let rows = app .directory_buffer - .as_ref() + .as_mut() .map(|dir| { + dir.scroll_state.skipped_rows = dir.scroll_state.calc_skipped_rows(height, dir.total); dir.nodes .iter() .enumerate() - .skip(height * (dir.focus / height.max(1))) + .skip(dir.scroll_state.skipped_rows) .take(height) .map(|(index, node)| { - let is_focused = dir.focus == index; + let is_focused = dir.scroll_state.current_focus == index; let is_selected = app .selection @@ -772,9 +773,13 @@ fn draw_table( let node_type = app_config.node_types.get(node); let (relative_index, is_before_focus, is_after_focus) = - match dir.focus.cmp(&index) { - Ordering::Greater => (dir.focus - index, true, false), - Ordering::Less => (index - dir.focus, false, true), + match dir.scroll_state.current_focus.cmp(&index) { + Ordering::Greater => { + (dir.scroll_state.current_focus - index, true, false) + } + Ordering::Less => { + (index - dir.scroll_state.current_focus, false, true) + } Ordering::Equal => (0, false, false), }; @@ -1284,7 +1289,7 @@ pub fn draw_dynamic( f: &mut Frame, screen_size: TuiRect, layout_size: TuiRect, - app: &app::App, + app: &mut app::App, func: &str, lua: &Lua, ) { @@ -1308,7 +1313,7 @@ pub fn draw_static( f: &mut Frame, screen_size: TuiRect, layout_size: TuiRect, - app: &app::App, + app: &mut app::App, panel: CustomPanel, _lua: &Lua, ) { @@ -1402,7 +1407,7 @@ pub fn draw_layout( f: &mut Frame, screen_size: TuiRect, layout_size: TuiRect, - app: &app::App, + app: &mut app::App, lua: &Lua, ) { match layout { @@ -1493,7 +1498,7 @@ pub fn draw_layout( } } -pub fn draw(f: &mut Frame, app: &app::App, lua: &Lua) { +pub fn draw(f: &mut Frame, app: &mut app::App, lua: &Lua) { let screen_size = f.size(); let layout = app.mode.layout.as_ref().unwrap_or(&app.layout).to_owned(); From 01606e0e6006a9d6ef3492975d573565121d306c Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 00:14:29 +0300 Subject: [PATCH 02/14] Adds corresponding config setting for vimlike_scrolling --- src/config.rs | 3 +++ src/directory_buffer.rs | 11 +++++++---- src/init.lua | 5 +++++ src/ui.rs | 6 +++++- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/config.rs b/src/config.rs index db08f86..05f9998 100644 --- a/src/config.rs +++ b/src/config.rs @@ -353,6 +353,9 @@ pub struct GeneralConfig { #[serde(default)] pub global_key_bindings: KeyBindings, + + #[serde(default)] + pub vimlike_scrolling: bool, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index cf6a5c1..b25798a 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -18,7 +18,12 @@ impl ScrollState { self.current_focus = current_focus; } - pub fn calc_skipped_rows(&mut self, height: usize, total: usize) -> usize { + pub fn calc_skipped_rows( + &mut self, + height: usize, + total: usize, + vimlike_scrolling: bool, + ) -> usize { let current_focus = self.current_focus; let last_focus = self.last_focus; let first_visible_row = self.skipped_rows; @@ -26,9 +31,7 @@ impl ScrollState { let end_cushion_row = (first_visible_row + height) .saturating_sub(ScrollState::PREVIEW_CUSHION + 1); - let vim_scrolling_enabled = true; - - if !vim_scrolling_enabled { + if !vimlike_scrolling { height * (self.current_focus / height.max(1)) } else if last_focus == None { // Just entered the directory diff --git a/src/init.lua b/src/init.lua index a36ddb7..48a5f54 100644 --- a/src/init.lua +++ b/src/init.lua @@ -91,6 +91,11 @@ 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. +-- +-- Type: boolean +xplr.config.general.vimlike_scrolling = false + -- 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 -- (by pressing `up`/`k`), or when the focus is on the last path and you diff --git a/src/ui.rs b/src/ui.rs index 838b1ef..2f92c6e 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -737,7 +737,11 @@ fn draw_table( .directory_buffer .as_mut() .map(|dir| { - dir.scroll_state.skipped_rows = dir.scroll_state.calc_skipped_rows(height, dir.total); + dir.scroll_state.skipped_rows = dir.scroll_state.calc_skipped_rows( + height, + dir.total, + app.config.general.vimlike_scrolling, + ); dir.nodes .iter() .enumerate() From 4aa367ca7c33229c314bd2b62a91280384e01a2f Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 00:26:33 +0300 Subject: [PATCH 03/14] Makes the current_focus field private to limit usage to its setters and getters --- src/app.rs | 18 +++++++++--------- src/directory_buffer.rs | 6 +++++- src/runner.rs | 8 ++++---- src/ui.rs | 8 ++++---- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3a32cee..7c95c1c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -754,7 +754,7 @@ impl App { focus.as_ref().map(PathBuf::from), self.directory_buffer .as_ref() - .map(|d| d.scroll_state.current_focus) + .map(|d| d.scroll_state.get_focus()) .unwrap_or(0), ) { Ok(dir) => self.set_directory(dir), @@ -825,15 +825,15 @@ impl App { let bounded = self.config.general.enforce_bounded_index_navigation; if let Some(dir) = self.directory_buffer_mut() { - if dir.scroll_state.current_focus == 0 { + if dir.scroll_state.get_focus() == 0 { if bounded { - dir.scroll_state.set_focus(dir.scroll_state.current_focus); + dir.scroll_state.set_focus(dir.scroll_state.get_focus()); } else { dir.scroll_state.set_focus(dir.total.saturating_sub(1)); } } else { dir.scroll_state - .set_focus(dir.scroll_state.current_focus.saturating_sub(1)); + .set_focus(dir.scroll_state.get_focus().saturating_sub(1)); }; }; Ok(self) @@ -887,7 +887,7 @@ impl App { } dir.scroll_state - .set_focus(dir.scroll_state.current_focus.saturating_sub(index)); + .set_focus(dir.scroll_state.get_focus().saturating_sub(index)); if let Some(n) = self.focused_node() { self.history = history.push(n.absolute_path.clone()); } @@ -912,15 +912,15 @@ impl App { let bounded = self.config.general.enforce_bounded_index_navigation; if let Some(dir) = self.directory_buffer_mut() { - if (dir.scroll_state.current_focus + 1) == dir.total { + if (dir.scroll_state.get_focus() + 1) == dir.total { if bounded { - dir.scroll_state.set_focus(dir.scroll_state.current_focus); + dir.scroll_state.set_focus(dir.scroll_state.get_focus()); } else { dir.scroll_state.set_focus(0); } } else { dir.scroll_state - .set_focus(dir.scroll_state.current_focus + 1); + .set_focus(dir.scroll_state.get_focus() + 1); } }; Ok(self) @@ -975,7 +975,7 @@ impl App { dir.scroll_state.set_focus( dir.scroll_state - .current_focus + .get_focus() .saturating_add(index) .min(dir.total.saturating_sub(1)), ); diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index b25798a..5d73ec9 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -4,7 +4,7 @@ use time::OffsetDateTime; #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub struct ScrollState { - pub current_focus: usize, + current_focus: usize, pub last_focus: Option, pub skipped_rows: usize, } @@ -18,6 +18,10 @@ impl ScrollState { self.current_focus = current_focus; } + pub fn get_focus(&self) -> usize { + self.current_focus + } + pub fn calc_skipped_rows( &mut self, height: usize, diff --git a/src/runner.rs b/src/runner.rs index 7c15bfc..8a96599 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -89,7 +89,7 @@ fn call( let focus_index = app .directory_buffer .as_ref() - .map(|d| d.scroll_state.current_focus) + .map(|d| d.scroll_state.get_focus()) .unwrap_or_default() .to_string(); @@ -281,7 +281,7 @@ impl Runner { self.focused_path, app.directory_buffer .as_ref() - .map(|d| d.scroll_state.current_focus) + .map(|d| d.scroll_state.get_focus()) .unwrap_or(0), tx_msg_in.clone(), ); @@ -433,7 +433,7 @@ impl Runner { .map(|n| n.relative_path.clone().into()), app.directory_buffer .as_ref() - .map(|d| d.scroll_state.current_focus) + .map(|d| d.scroll_state.get_focus()) .unwrap_or(0), tx_msg_in.clone(), ); @@ -448,7 +448,7 @@ impl Runner { .map(|n| n.relative_path.clone().into()), app.directory_buffer .as_ref() - .map(|d| d.scroll_state.current_focus) + .map(|d| d.scroll_state.get_focus()) .unwrap_or(0), tx_msg_in.clone(), ); diff --git a/src/ui.rs b/src/ui.rs index 2f92c6e..2f62610 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -748,7 +748,7 @@ fn draw_table( .skip(dir.scroll_state.skipped_rows) .take(height) .map(|(index, node)| { - let is_focused = dir.scroll_state.current_focus == index; + let is_focused = dir.scroll_state.get_focus() == index; let is_selected = app .selection @@ -777,12 +777,12 @@ fn draw_table( let node_type = app_config.node_types.get(node); let (relative_index, is_before_focus, is_after_focus) = - match dir.scroll_state.current_focus.cmp(&index) { + match dir.scroll_state.get_focus().cmp(&index) { Ordering::Greater => { - (dir.scroll_state.current_focus - index, true, false) + (dir.scroll_state.get_focus() - index, true, false) } Ordering::Less => { - (index - dir.scroll_state.current_focus, false, true) + (index - dir.scroll_state.get_focus(), false, true) } Ordering::Equal => (0, false, false), }; From 87805509c56a9552efefdb9ee94ee3118f5f172a Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 00:34:57 +0300 Subject: [PATCH 04/14] Refactors the calc_skipped_rows function to make it more readable --- src/directory_buffer.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index 5d73ec9..e1e3c56 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -31,18 +31,22 @@ impl ScrollState { 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 + ScrollState::PREVIEW_CUSHION; let end_cushion_row = (first_visible_row + height) .saturating_sub(ScrollState::PREVIEW_CUSHION + 1); - if !vimlike_scrolling { + let new_skipped_rows = if !vimlike_scrolling { height * (self.current_focus / height.max(1)) } else if last_focus == None { // Just entered the directory 0 } else if current_focus == 0 { + // Focus on first node 0 } else if current_focus == total.saturating_sub(1) { + // Focus on last node total.saturating_sub(height) } else if current_focus > last_focus.unwrap() { // Scrolling down @@ -63,7 +67,9 @@ impl ScrollState { } else { current_focus.saturating_sub(ScrollState::PREVIEW_CUSHION) } - } + }; + + new_skipped_rows } } From fd40de26e7b295e4a1270e8475e21f25a4f625df Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 00:55:52 +0300 Subject: [PATCH 05/14] Adds tests for the ScrollState calc_skipped_rows fn --- src/directory_buffer.rs | 103 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index e1e3c56..5bc006a 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -110,3 +110,106 @@ fn now() -> OffsetDateTime { .ok() .unwrap_or_else(OffsetDateTime::now_utc) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calc_skipped_rows_non_vimlike_scrolling() { + let mut state = ScrollState { + current_focus: 10, + last_focus: Some(8), + skipped_rows: 0, + }; + + let height = 5; + let total = 20; + let vimlike_scrolling = false; + + let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + assert_eq!(result, height * (state.current_focus / height.max(1))); + } + + #[test] + fn test_calc_skipped_rows_entered_directory() { + let mut state = ScrollState { + current_focus: 10, + last_focus: None, + skipped_rows: 0, + }; + + let height = 5; + let total = 20; + let vimlike_scrolling = true; + + let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + assert_eq!(result, 0); + } + + #[test] + fn test_calc_skipped_rows_top_of_directory() { + let mut state = ScrollState { + current_focus: 0, + last_focus: Some(8), + skipped_rows: 5, + }; + + let height = 5; + let total = 20; + let vimlike_scrolling = true; + + let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + assert_eq!(result, 0); + } + + #[test] + fn test_calc_skipped_rows_bottom_of_directory() { + let mut state = ScrollState { + current_focus: 19, + last_focus: Some(18), + skipped_rows: 15, + }; + + let height = 5; + let total = 20; + let vimlike_scrolling = true; + + let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + assert_eq!(result, 15); + } + + #[test] + fn test_calc_skipped_rows_scrolling_down() { + let mut state = ScrollState { + current_focus: 12, + last_focus: Some(10), + skipped_rows: 10, + }; + + let height = 5; + let total = 20; + let vimlike_scrolling = true; + + let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + assert_eq!(result, 11); + } + + #[test] + fn test_calc_skipped_rows_scrolling_up() { + let mut state = ScrollState { + current_focus: 8, + last_focus: Some(10), + skipped_rows: 10, + }; + + let height = 5; + let total = 20; + let vimlike_scrolling = true; + + let result = state.calc_skipped_rows(height, total, vimlike_scrolling); + assert_eq!(result, 5); + } + + // Add more tests for other scenarios... +} From a6fb695ff95f2434556f8902e22fc16bc69a7abd Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 01:59:54 +0300 Subject: [PATCH 06/14] Refactors the calc_skipped_rows function to make it even more readable --- src/directory_buffer.rs | 41 ++++++++++++++++++++++++++--------------- src/runner.rs | 4 ++-- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index 5bc006a..3f5fc92 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -1,3 +1,5 @@ +use std::thread::current; + use crate::node::Node; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; @@ -35,7 +37,8 @@ impl ScrollState { // Calculate the cushion rows at the start and end of the view port let start_cushion_row = first_visible_row + ScrollState::PREVIEW_CUSHION; let end_cushion_row = (first_visible_row + height) - .saturating_sub(ScrollState::PREVIEW_CUSHION + 1); + .saturating_sub(ScrollState::PREVIEW_CUSHION + 1) + .min(total.saturating_sub(ScrollState::PREVIEW_CUSHION + 1)); let new_skipped_rows = if !vimlike_scrolling { height * (self.current_focus / height.max(1)) @@ -43,30 +46,38 @@ impl ScrollState { // Just entered the directory 0 } else if current_focus == 0 { - // Focus on first node + // When focus goes to first node 0 } else if current_focus == total.saturating_sub(1) { - // Focus on last node + // When focus goes to last node total.saturating_sub(height) + } else if (start_cushion_row..=end_cushion_row).contains(¤t_focus) { + // IF within cushioned area; do nothing + first_visible_row } else if current_focus > last_focus.unwrap() { - // Scrolling down - if current_focus <= end_cushion_row { - first_visible_row - } else if total <= (current_focus + ScrollState::PREVIEW_CUSHION) { - first_visible_row + // When scrolling down outside the view port + if current_focus > total.saturating_sub(ScrollState::PREVIEW_CUSHION + 1) { + // When focusing the last nodes; always view the full last page + total.saturating_sub(height) } else { - (self.current_focus + ScrollState::PREVIEW_CUSHION + 1) - .saturating_sub(height) + // When scrolling down the view port without reaching the last nodes + current_focus.saturating_sub(height - 1 - ScrollState::PREVIEW_CUSHION) } - } else { - // Scrolling up - if current_focus >= start_cushion_row { - first_visible_row - } else if current_focus <= ScrollState::PREVIEW_CUSHION { + } else if current_focus < last_focus.unwrap() { + // When scrolling up outside the view port + if current_focus < ScrollState::PREVIEW_CUSHION { + // When focusing the first nodes; always view the full first page 0 + } else if current_focus > end_cushion_row { + // When scrolling up around from the last rows; do nothing + first_visible_row } else { + // When scrolling up the view port without reaching the first nodes current_focus.saturating_sub(ScrollState::PREVIEW_CUSHION) } + } else { + // If nothing matches; do nothing + first_visible_row }; new_skipped_rows diff --git a/src/runner.rs b/src/runner.rs index 8a96599..8130bf2 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -27,8 +27,8 @@ use tui::Terminal; use tui_input::Input; pub fn get_tty() -> Result { - let tty = "/dev/tty"; - match fs::OpenOptions::new().read(true).write(true).open(tty) { + let tty = "/dev/stdout"; + match fs::File::create(tty) { Ok(f) => Ok(f), Err(e) => { bail!(format!("could not open {tty}. {e}")) From 5240b3904b87baada756cec3fa5c5a4e58cc89bc Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 02:05:06 +0300 Subject: [PATCH 07/14] Rolls back changes to the open terminal file --- src/directory_buffer.rs | 4 +--- src/runner.rs | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index 3f5fc92..fd06a81 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -1,5 +1,3 @@ -use std::thread::current; - use crate::node::Node; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; @@ -52,7 +50,7 @@ impl ScrollState { // When focus goes to last node total.saturating_sub(height) } else if (start_cushion_row..=end_cushion_row).contains(¤t_focus) { - // IF within cushioned area; do nothing + // If within cushioned area; do nothing first_visible_row } else if current_focus > last_focus.unwrap() { // When scrolling down outside the view port diff --git a/src/runner.rs b/src/runner.rs index 8130bf2..8a96599 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -27,8 +27,8 @@ use tui::Terminal; use tui_input::Input; pub fn get_tty() -> Result { - let tty = "/dev/stdout"; - match fs::File::create(tty) { + let tty = "/dev/tty"; + match fs::OpenOptions::new().read(true).write(true).open(tty) { Ok(f) => Ok(f), Err(e) => { bail!(format!("could not open {tty}. {e}")) From 95621af9eb28154f08a9a2218d700fa4b0353b5d Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 03:16:43 +0300 Subject: [PATCH 08/14] Increases the preview_cushion to 5 like in vim --- src/directory_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index fd06a81..39d89f5 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -11,7 +11,7 @@ pub struct ScrollState { impl ScrollState { /* The number of visible next lines when scrolling towards either ends of the view port */ - pub const PREVIEW_CUSHION: usize = 3; + pub const PREVIEW_CUSHION: usize = 5; pub fn set_focus(&mut self, current_focus: usize) { self.last_focus = Some(self.current_focus); From 00bd54abe93049e97170097b5109fb423662a4d5 Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 19:36:17 +0300 Subject: [PATCH 09/14] Removes unnecessary mut from the calc_skipped_rows fn --- src/directory_buffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index 39d89f5..29aa149 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -23,7 +23,7 @@ impl ScrollState { } pub fn calc_skipped_rows( - &mut self, + &self, height: usize, total: usize, vimlike_scrolling: bool, From 91276f6871eef94fe64a29515dc2e937c2ff62b3 Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 19:44:18 +0300 Subject: [PATCH 10/14] Removes an unnecessary condition --- src/app.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7c95c1c..ec3cd0f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -826,9 +826,7 @@ impl App { if let Some(dir) = self.directory_buffer_mut() { if dir.scroll_state.get_focus() == 0 { - if bounded { - dir.scroll_state.set_focus(dir.scroll_state.get_focus()); - } else { + if !bounded { dir.scroll_state.set_focus(dir.total.saturating_sub(1)); } } else { @@ -913,9 +911,7 @@ impl App { if let Some(dir) = self.directory_buffer_mut() { if (dir.scroll_state.get_focus() + 1) == dir.total { - if bounded { - dir.scroll_state.set_focus(dir.scroll_state.get_focus()); - } else { + if !bounded { dir.scroll_state.set_focus(0); } } else { From 2a3d056bf1a268c1c5214a36b1b998f8e946b439 Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 20:04:51 +0300 Subject: [PATCH 11/14] Clarifies some comments --- src/directory_buffer.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index 29aa149..31cbbe8 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -53,24 +53,24 @@ impl ScrollState { // If within cushioned area; do nothing first_visible_row } else if current_focus > last_focus.unwrap() { - // When scrolling down outside the view port + // When scrolling down the cushioned area if current_focus > total.saturating_sub(ScrollState::PREVIEW_CUSHION + 1) { // When focusing the last nodes; always view the full last page total.saturating_sub(height) } else { - // When scrolling down the view port without reaching the last nodes + // When scrolling down the cushioned area without reaching the last nodes current_focus.saturating_sub(height - 1 - ScrollState::PREVIEW_CUSHION) } } else if current_focus < last_focus.unwrap() { - // When scrolling up outside the view port + // When scrolling up the cushioned area if current_focus < ScrollState::PREVIEW_CUSHION { // When focusing the first nodes; always view the full first page 0 } else if current_focus > end_cushion_row { - // When scrolling up around from the last rows; do nothing + // When scrolling up from the last rows; do nothing first_visible_row } else { - // When scrolling up the view port without reaching the first nodes + // When scrolling up the cushioned area without reaching the first nodes current_focus.saturating_sub(ScrollState::PREVIEW_CUSHION) } } else { From 1600ad9a9c2119490c286c4bfeef6ec8e99112a7 Mon Sep 17 00:00:00 2001 From: Ahmed ElSamhaa Date: Mon, 8 Apr 2024 22:19:34 +0300 Subject: [PATCH 12/14] Makes the preview cushion dynamic now, and sets an initial value 5 for it --- src/directory_buffer.rs | 51 +++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index 31cbbe8..b9764bf 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -7,11 +7,11 @@ pub struct ScrollState { 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, } impl ScrollState { - /* The number of visible next lines when scrolling towards either ends of the view port */ - pub const PREVIEW_CUSHION: usize = 5; pub fn set_focus(&mut self, current_focus: usize) { self.last_focus = Some(self.current_focus); @@ -28,15 +28,25 @@ impl ScrollState { total: usize, vimlike_scrolling: bool, ) -> usize { + 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 + ScrollState::PREVIEW_CUSHION; + let start_cushion_row = first_visible_row + preview_cushion; let end_cushion_row = (first_visible_row + height) - .saturating_sub(ScrollState::PREVIEW_CUSHION + 1) - .min(total.saturating_sub(ScrollState::PREVIEW_CUSHION + 1)); + .saturating_sub(preview_cushion + 1) + .min(total.saturating_sub(preview_cushion + 1)); let new_skipped_rows = if !vimlike_scrolling { height * (self.current_focus / height.max(1)) @@ -54,16 +64,16 @@ impl ScrollState { first_visible_row } else if current_focus > last_focus.unwrap() { // When scrolling down the cushioned area - if current_focus > total.saturating_sub(ScrollState::PREVIEW_CUSHION + 1) { + 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 - 1 - ScrollState::PREVIEW_CUSHION) + current_focus.saturating_sub(height.saturating_sub(preview_cushion + 1)) } } else if current_focus < last_focus.unwrap() { // When scrolling up the cushioned area - if current_focus < ScrollState::PREVIEW_CUSHION { + if current_focus < preview_cushion { // When focusing the first nodes; always view the full first page 0 } else if current_focus > end_cushion_row { @@ -71,7 +81,7 @@ impl ScrollState { first_visible_row } else { // When scrolling up the cushioned area without reaching the first nodes - current_focus.saturating_sub(ScrollState::PREVIEW_CUSHION) + current_focus.saturating_sub(preview_cushion) } } else { // If nothing matches; do nothing @@ -104,6 +114,7 @@ impl DirectoryBuffer { current_focus, last_focus: None, skipped_rows: 0, + initial_preview_cushion: 5, }, explored_at: now(), } @@ -126,10 +137,11 @@ mod tests { #[test] fn test_calc_skipped_rows_non_vimlike_scrolling() { - let mut state = ScrollState { + let state = ScrollState { current_focus: 10, last_focus: Some(8), skipped_rows: 0, + initial_preview_cushion: 5, }; let height = 5; @@ -142,10 +154,11 @@ mod tests { #[test] fn test_calc_skipped_rows_entered_directory() { - let mut state = ScrollState { + let state = ScrollState { current_focus: 10, last_focus: None, skipped_rows: 0, + initial_preview_cushion: 5, }; let height = 5; @@ -158,10 +171,11 @@ mod tests { #[test] fn test_calc_skipped_rows_top_of_directory() { - let mut state = ScrollState { + let state = ScrollState { current_focus: 0, last_focus: Some(8), skipped_rows: 5, + initial_preview_cushion: 5, }; let height = 5; @@ -174,10 +188,11 @@ mod tests { #[test] fn test_calc_skipped_rows_bottom_of_directory() { - let mut state = ScrollState { + let state = ScrollState { current_focus: 19, last_focus: Some(18), skipped_rows: 15, + initial_preview_cushion: 5, }; let height = 5; @@ -190,10 +205,11 @@ mod tests { #[test] fn test_calc_skipped_rows_scrolling_down() { - let mut state = ScrollState { + let state = ScrollState { current_focus: 12, last_focus: Some(10), skipped_rows: 10, + initial_preview_cushion: 5, }; let height = 5; @@ -201,15 +217,16 @@ mod tests { let vimlike_scrolling = true; let result = state.calc_skipped_rows(height, total, vimlike_scrolling); - assert_eq!(result, 11); + assert_eq!(result, 10); } #[test] fn test_calc_skipped_rows_scrolling_up() { - let mut state = ScrollState { + let state = ScrollState { current_focus: 8, last_focus: Some(10), skipped_rows: 10, + initial_preview_cushion: 5, }; let height = 5; @@ -217,7 +234,7 @@ mod tests { let vimlike_scrolling = true; let result = state.calc_skipped_rows(height, total, vimlike_scrolling); - assert_eq!(result, 5); + assert_eq!(result, 7); } // Add more tests for other scenarios... From 96da7e1da81211a07ad4c44be6a1c07c1a893700 Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 10 Apr 2024 13:02:05 +0530 Subject: [PATCH 13/14] Fix linting --- src/app.rs | 3 +-- src/directory_buffer.rs | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/app.rs b/src/app.rs index ec3cd0f..19e3268 100644 --- a/src/app.rs +++ b/src/app.rs @@ -915,8 +915,7 @@ impl App { dir.scroll_state.set_focus(0); } } else { - dir.scroll_state - .set_focus(dir.scroll_state.get_focus() + 1); + dir.scroll_state.set_focus(dir.scroll_state.get_focus() + 1); } }; Ok(self) diff --git a/src/directory_buffer.rs b/src/directory_buffer.rs index b9764bf..aa60eeb 100644 --- a/src/directory_buffer.rs +++ b/src/directory_buffer.rs @@ -12,7 +12,6 @@ pub struct ScrollState { } impl ScrollState { - pub fn set_focus(&mut self, current_focus: usize) { self.last_focus = Some(self.current_focus); self.current_focus = current_focus; @@ -48,9 +47,9 @@ impl ScrollState { .saturating_sub(preview_cushion + 1) .min(total.saturating_sub(preview_cushion + 1)); - let new_skipped_rows = if !vimlike_scrolling { + if !vimlike_scrolling { height * (self.current_focus / height.max(1)) - } else if last_focus == None { + } else if last_focus.is_none() { // Just entered the directory 0 } else if current_focus == 0 { @@ -86,9 +85,7 @@ impl ScrollState { } else { // If nothing matches; do nothing first_visible_row - }; - - new_skipped_rows + } } } From 976530ba708abd7038de4fd113b3ec1c8bec9d6f Mon Sep 17 00:00:00 2001 From: Arijit Basu Date: Wed, 10 Apr 2024 13:02:49 +0530 Subject: [PATCH 14/14] Gen docs --- docs/en/src/general-config.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/src/general-config.md b/docs/en/src/general-config.md index 75fa861..a986348 100644 --- a/docs/en/src/general-config.md +++ b/docs/en/src/general-config.md @@ -42,6 +42,12 @@ Set it to `true` if you want to hide all remaps in the help menu. Type: boolean +#### xplr.config.general.vimlike_scrolling + +Set it to `true` if you want vim-like scrolling. + +Type: boolean + #### xplr.config.general.enforce_bounded_index_navigation Set it to `true` if you want the cursor to stay in the same position when