feat: configure half-page viewport scroll distance

This commit is contained in:
Valdemar Lorenzen
2026-10-16 10:25:27 +02:00
parent 5a8c37b2f2
commit 284ddd3713
11 changed files with 96 additions and 7 deletions
+12 -2
View File
@@ -13,7 +13,7 @@ use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::fmt;
use std::fs::{self, OpenOptions};
use std::io::{self, Read, Write};
use std::num::NonZeroU8;
use std::num::{NonZeroU8, NonZeroUsize};
use std::ops::Range;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
@@ -2009,6 +2009,7 @@ pub struct Editor {
popup: PopupConfig,
labels: BTreeMap<String, String>,
scroll_distance: Option<usize>,
scroll_half_page_lines: Option<NonZeroUsize>,
highlight_trailing_whitespace: bool,
whitespace_characters: BTreeMap<char, char>,
whitespace_scope: WhitespaceScope,
@@ -2119,6 +2120,7 @@ impl Editor {
popup: PopupConfig::default(),
labels: BTreeMap::new(),
scroll_distance: Some(0),
scroll_half_page_lines: None,
highlight_trailing_whitespace: false,
whitespace_characters: BTreeMap::new(),
whitespace_scope: WhitespaceScope::All,
@@ -4116,6 +4118,10 @@ impl Editor {
self.scroll_distance = distance;
}
pub(crate) fn set_scroll_half_page_lines(&mut self, lines: Option<NonZeroUsize>) {
self.scroll_half_page_lines = lines;
}
pub(crate) fn highlight_trailing_whitespace(&self) -> bool {
self.highlight_trailing_whitespace
}
@@ -5829,7 +5835,11 @@ impl Editor {
pub(crate) fn take_viewport_scroll(&self, height: usize) -> isize {
let (rows, halves) = self.active.view.viewport_scroll.take();
rows.saturating_add(halves.saturating_mul((height / 2).max(1) as isize))
let lines = self
.scroll_half_page_lines
.map_or((height / 2).max(1), NonZeroUsize::get);
let lines = isize::try_from(lines).unwrap_or(isize::MAX);
rows.saturating_add(halves.saturating_mul(lines))
}
fn reveal_caret(&mut self) {
+1 -1
View File
@@ -28,7 +28,7 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
_ splits above/below, | left/right; Option/Alt+h/j/k/l focuses neighboring panes
Ctrl+Option/Alt+h/l previous/next tab; :tab.new opens current buffer in another tab
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
Normal/Visual: Ctrl+u/d scroll half a viewport up/down without moving carets; counts supported
Normal/Visual: Ctrl+u/d scroll up/down without moving carets; configurable distance (default half viewport), counts supported
u undo, U redo, : command, / search, * search word, n/N next/previous match
yy copies whole lines; dd deletes them; cc changes them and enters Insert; counts: 3yy, 2dd, 2cc
dw/db cut forward/backward words on the current line; cw/cb cut the same spans and enter Insert
+50 -1
View File
@@ -3,7 +3,7 @@ use crate::editor::{Context, Effect, Feature};
use crate::plugin::{DocumentEvent, PluginEvent, PluginUpdate, UpdateEffect};
use serde::{Deserialize, Serialize};
use std::io::{self, BufRead, BufReader, Write};
use std::num::NonZeroU8;
use std::num::{NonZeroU8, NonZeroUsize};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender};
@@ -46,6 +46,7 @@ pub struct Config {
pub popup: PopupConfig,
pub labels: BTreeMap<String, String>,
pub scroll_distance: Option<usize>,
pub scroll_half_page_lines: Option<NonZeroUsize>,
pub highlight_trailing_whitespace: bool,
pub whitespace_characters: BTreeMap<char, char>,
pub whitespace_scope: crate::editor::WhitespaceScope,
@@ -97,6 +98,7 @@ impl Default for Config {
popup: PopupConfig::default(),
labels: BTreeMap::new(),
scroll_distance: Some(0),
scroll_half_page_lines: None,
highlight_trailing_whitespace: false,
whitespace_characters: BTreeMap::new(),
whitespace_scope: crate::editor::WhitespaceScope::All,
@@ -135,6 +137,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
popup,
labels,
scroll_distance,
scroll_half_page_lines,
highlight_trailing_whitespace,
whitespace_characters,
whitespace_scope,
@@ -174,6 +177,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
config.popup = popup;
config.labels = labels;
config.scroll_distance = scroll_distance;
config.scroll_half_page_lines = scroll_half_page_lines;
config.highlight_trailing_whitespace = highlight_trailing_whitespace;
config.whitespace_characters = whitespace_characters;
config.whitespace_scope = whitespace_scope;
@@ -512,6 +516,7 @@ struct ConfigFrame {
labels: BTreeMap<String, String>,
#[serde(deserialize_with = "scroll_distance")]
scroll_distance: Option<usize>,
scroll_half_page_lines: Option<NonZeroUsize>,
highlight_trailing_whitespace: bool,
#[serde(deserialize_with = "whitespace_characters")]
whitespace_characters: BTreeMap<char, char>,
@@ -1929,6 +1934,50 @@ end
}
}
#[test]
fn config_scroll_half_page_lines_controls_counted_scrolling_and_rejects_invalid_values() {
let fixture = Fixture::new();
for (value, expected) in [
("nil", 30),
("1", 3),
("7", 21),
("18446744073709551615", isize::MAX),
] {
let path = fixture.file(
"config.rb",
&format!("Vamp.configure {{ |config| config.scroll_half_page_lines = {value} }}"),
);
let mut session =
crate::session::EditorSession::open(None, vec![], Some(&path)).unwrap();
session.handle(crate::editor::Input::Char('3'));
session.handle(crate::editor::Input::Ctrl('d'));
assert_eq!(
session.editor().take_viewport_scroll(20),
expected,
"{value}"
);
}
for value in [
"0",
"-1",
"1.5",
"\"3\"",
"true",
"false",
"18446744073709551616",
] {
let path = fixture.file(
"config.rb",
&format!("Vamp.configure {{ |config| config.scroll_half_page_lines = {value} }}"),
);
let error = load_config(&path).unwrap_err();
assert!(
error.contains("scroll_half_page_lines") && error.contains("must be"),
"{value}: {error}"
);
}
}
#[test]
fn config_scroll_distance_accepts_nil_and_counts_and_rejects_invalid_values() {
let fixture = Fixture::new();
+8
View File
@@ -145,6 +145,7 @@ module Vamp
@popup = PopupConfig.new
@labels = {}
@scroll_distance = 0
@scroll_half_page_lines = nil
@highlight_trailing_whitespace = false
@whitespace_characters = {}
@whitespace_scope = "all"
@@ -229,6 +230,12 @@ module Vamp
@scroll_distance = distance.nil? ? nil : diff_line_count!(distance, "scroll_distance", 0)
end
# @param lines [Integer, nil] positive screen rows, or nil for half the viewport
# @return [Integer, nil]
def scroll_half_page_lines=(lines)
@scroll_half_page_lines = lines.nil? ? nil : diff_line_count!(lines, "scroll_half_page_lines", 1)
end
# @param enabled [Boolean]
# @return [Boolean]
def highlight_trailing_whitespace=(enabled)
@@ -343,6 +350,7 @@ module Vamp
"git_blame" => @git_blame,
"git_gutter" => @git_gutter,
"scroll_distance" => @scroll_distance,
"scroll_half_page_lines" => @scroll_half_page_lines,
"highlight_trailing_whitespace" => @highlight_trailing_whitespace,
"whitespace_characters" => @whitespace_characters,
"whitespace_scope" => @whitespace_scope,
+1
View File
@@ -512,6 +512,7 @@ impl EditorSession {
editor.set_git_blame(config.git_blame);
editor.set_gutter_style(config.git_gutter);
editor.set_scroll_distance(config.scroll_distance);
editor.set_scroll_half_page_lines(config.scroll_half_page_lines);
editor.set_highlight_trailing_whitespace(config.highlight_trailing_whitespace);
editor.set_whitespace_characters(config.whitespace_characters);
editor.set_whitespace_scope(config.whitespace_scope);
+13
View File
@@ -12617,6 +12617,19 @@ end
editor.handle(Input::Ctrl('u'));
render(&mut terminal, &mut views, &mut editor);
assert_eq!(offset(&views), initial);
editor.set_scroll_half_page_lines(std::num::NonZeroUsize::new(3));
editor.handle(Input::Char('2'));
editor.handle(Input::Ctrl('d'));
render(&mut terminal, &mut views, &mut editor);
if !wrapped {
assert_eq!(offset(&views), (initial.0 + 6, 0));
}
assert_eq!(editor.carets(), carets);
assert_eq!(editor.selections(), selections);
editor.handle(Input::Char('2'));
editor.handle(Input::Ctrl('u'));
render(&mut terminal, &mut views, &mut editor);
assert_eq!(offset(&views), initial);
if !visual {
editor.handle(Input::Char('3'));
editor.handle(Input::Alt('j'));