feat: configure cursor distance from viewport edges
This commit is contained in:
@@ -1762,6 +1762,7 @@ pub struct Editor {
|
||||
bottom_bar: ChromeLine,
|
||||
popup: PopupConfig,
|
||||
labels: BTreeMap<String, String>,
|
||||
scroll_distance: Option<usize>,
|
||||
battery_charge: Option<u8>,
|
||||
battery_label: String,
|
||||
git_blame: bool,
|
||||
@@ -1863,6 +1864,7 @@ impl Editor {
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
labels: BTreeMap::new(),
|
||||
scroll_distance: Some(0),
|
||||
battery_charge: None,
|
||||
battery_label: String::new(),
|
||||
git_blame: true,
|
||||
@@ -2601,6 +2603,14 @@ impl Editor {
|
||||
.map_or(self.active.view.word_wrap, |diff| diff.word_wrap)
|
||||
}
|
||||
|
||||
pub fn scroll_distance(&self) -> Option<usize> {
|
||||
self.scroll_distance
|
||||
}
|
||||
|
||||
pub(crate) fn set_scroll_distance(&mut self, distance: Option<usize>) {
|
||||
self.scroll_distance = distance;
|
||||
}
|
||||
|
||||
pub(crate) fn set_diff_context_options(&mut self, context_lines: usize, expand_lines: usize) {
|
||||
self.diff_context_options = (context_lines, expand_lines);
|
||||
if let Some(diff) = &mut self.diff {
|
||||
|
||||
@@ -93,6 +93,7 @@ pub struct FilePicker {
|
||||
tree_for_node: Vec<usize>,
|
||||
tree_roots: Vec<usize>,
|
||||
rows: Vec<VisibleRow>,
|
||||
viewport_revision: u64,
|
||||
expanded: HashSet<usize>,
|
||||
filter_collapsed: HashSet<usize>,
|
||||
saved_selection: [Option<usize>; 2],
|
||||
@@ -298,6 +299,7 @@ impl FilePicker {
|
||||
tree: Vec::new(),
|
||||
tree_roots: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
viewport_revision: 0,
|
||||
expanded: HashSet::new(),
|
||||
filter_collapsed: HashSet::new(),
|
||||
saved_selection: [None, None],
|
||||
@@ -346,6 +348,10 @@ impl FilePicker {
|
||||
tree
|
||||
}
|
||||
|
||||
pub(crate) fn viewport_revision(&self) -> u64 {
|
||||
self.viewport_revision
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> impl ExactSizeIterator<Item = FileRow<'_>> + '_ {
|
||||
self.rows.iter().map(|row| {
|
||||
let node = &self.nodes[row.node];
|
||||
@@ -542,6 +548,8 @@ impl FilePicker {
|
||||
}
|
||||
|
||||
fn project(&mut self, selected: Option<usize>, select_match: bool) {
|
||||
static NEXT_REVISION: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
self.viewport_revision = NEXT_REVISION.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let query = self.query.to_lowercase();
|
||||
let tokens: Vec<_> = query.split_whitespace().collect();
|
||||
let matches = |node: &Node| tokens.iter().all(|token| node.search_label.contains(token));
|
||||
|
||||
+81
-3
@@ -44,6 +44,7 @@ pub struct Config {
|
||||
pub bottom_bar: ChromeLine,
|
||||
pub popup: PopupConfig,
|
||||
pub labels: BTreeMap<String, String>,
|
||||
pub scroll_distance: Option<usize>,
|
||||
pub diff_context_lines: usize,
|
||||
pub diff_expand_lines: usize,
|
||||
pub diff_view_mode: crate::diff::ViewMode,
|
||||
@@ -89,6 +90,7 @@ impl Default for Config {
|
||||
bottom_bar: ChromeLine::bottom(),
|
||||
popup: PopupConfig::default(),
|
||||
labels: BTreeMap::new(),
|
||||
scroll_distance: Some(0),
|
||||
diff_context_lines: 10,
|
||||
diff_expand_lines: 15,
|
||||
diff_view_mode: crate::diff::ViewMode::Split,
|
||||
@@ -121,6 +123,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
bottom_bar,
|
||||
popup,
|
||||
labels,
|
||||
scroll_distance,
|
||||
diff_context_lines,
|
||||
diff_expand_lines,
|
||||
diff_view_mode,
|
||||
@@ -154,6 +157,7 @@ pub fn load_config(path: &Path) -> Result<Config, String> {
|
||||
config.bottom_bar = bottom_bar;
|
||||
config.popup = popup;
|
||||
config.labels = labels;
|
||||
config.scroll_distance = scroll_distance;
|
||||
config.diff_context_lines = diff_context_lines;
|
||||
config.diff_expand_lines = diff_expand_lines;
|
||||
config.diff_view_mode = diff_view_mode;
|
||||
@@ -450,6 +454,17 @@ fn diff_expand_lines<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Resul
|
||||
diff_line_count(deserializer, "diff_expand_lines", 1)
|
||||
}
|
||||
|
||||
fn scroll_distance<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Option<usize>, D::Error> {
|
||||
Option::<usize>::deserialize(deserializer).map_err(|error| {
|
||||
serde::de::Error::custom(format!(
|
||||
"scroll_distance must be nil or an Integer between 0 and {}: {error}",
|
||||
usize::MAX
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
// Startup-only configuration does not inflate every queued plugin response.
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -474,6 +489,8 @@ struct ConfigFrame {
|
||||
popup: PopupConfig,
|
||||
#[serde(deserialize_with = "crate::chrome::labels")]
|
||||
labels: BTreeMap<String, String>,
|
||||
#[serde(deserialize_with = "scroll_distance")]
|
||||
scroll_distance: Option<usize>,
|
||||
#[serde(deserialize_with = "diff_context_lines")]
|
||||
diff_context_lines: usize,
|
||||
#[serde(deserialize_with = "diff_expand_lines")]
|
||||
@@ -1217,9 +1234,9 @@ end
|
||||
assert!(load_config(&path).unwrap_err().contains(error), "{value}");
|
||||
}
|
||||
for frame in [
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":null,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
r#"{"type":"config","version":1,"plugins":[],"lsp":{},"bindings":[],"leader":7,"file_explorer_side":"right","file_picker_view":"list","file_explorer_view":"tree","clipboard_mode":"system","git_blame":true,"git_gutter":"bar","scroll_distance":0,"top_bar":{},"bottom_bar":{},"popup":{"command":{},"search":{}},"labels":{},"diff_context_lines":10,"diff_expand_lines":15,"diff_view_mode":"split","diff_split_orientation":"vertical","diff_follow_working_copy":true}"#,
|
||||
] {
|
||||
assert!(serde_json::from_str::<Response>(frame).is_err(), "{frame}");
|
||||
}
|
||||
@@ -1295,6 +1312,7 @@ end
|
||||
"leader": "space", "file_explorer_side": "right", "file_picker_view": "tree",
|
||||
"file_explorer_view": "tree", "clipboard_mode": "system", "git_blame": true,
|
||||
"git_gutter": "bar",
|
||||
"scroll_distance": 0,
|
||||
"top_bar": {}, "bottom_bar": {}, "popup": {"command": {}, "search": {}},
|
||||
"labels": {}, "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
@@ -1545,6 +1563,7 @@ end
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||
"scroll_distance": 0,
|
||||
"popup": {"command": {}, "search": {}},
|
||||
"clipboard_mode": "system", "diff_context_lines": 0, "diff_expand_lines": 7,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
@@ -1585,6 +1604,64 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_scroll_distance_accepts_nil_and_counts_and_rejects_invalid_values() {
|
||||
let fixture = Fixture::new();
|
||||
let frame = serde_json::json!({
|
||||
"version": 1, "type": "config", "plugins": [], "lsp": {},
|
||||
"bindings": [], "leader": "space", "file_explorer_side": "right",
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"clipboard_mode": "system", "git_blame": true, "git_gutter": "bar",
|
||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||
"popup": {"command": {}, "search": {}},
|
||||
"diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
"diff_view_mode": "split", "diff_split_orientation": "vertical",
|
||||
"diff_follow_working_copy": true, "scroll_distance": 0
|
||||
});
|
||||
for distance in [None, Some(0), Some(7), Some(usize::MAX)] {
|
||||
let value = distance.map_or_else(|| "nil".into(), |value| value.to_string());
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.scroll_distance = {value} }}"),
|
||||
);
|
||||
assert_eq!(load_config(&path).unwrap().scroll_distance, distance);
|
||||
let mut valid_frame = frame.clone();
|
||||
valid_frame["scroll_distance"] = serde_json::json!(distance);
|
||||
let response = serde_json::from_value::<Response>(valid_frame).unwrap();
|
||||
let Response::Config(config) = response else {
|
||||
panic!("expected a configuration frame");
|
||||
};
|
||||
assert_eq!(config.scroll_distance, distance);
|
||||
}
|
||||
for value in [
|
||||
"-1",
|
||||
"1.5",
|
||||
"\"3\"",
|
||||
"true",
|
||||
"false",
|
||||
"18446744073709551616",
|
||||
] {
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
&format!("Vamp.configure {{ |config| config.scroll_distance = {value} }}"),
|
||||
);
|
||||
let error = load_config(&path).unwrap_err();
|
||||
assert!(
|
||||
error.contains("scroll_distance") && error.contains("must be"),
|
||||
"{value}: {error}"
|
||||
);
|
||||
let mut invalid_frame = frame.clone();
|
||||
invalid_frame["scroll_distance"] = serde_json::from_str(value).unwrap();
|
||||
let error = serde_json::from_value::<Response>(invalid_frame)
|
||||
.err()
|
||||
.unwrap();
|
||||
assert!(
|
||||
error.to_string().contains("scroll_distance"),
|
||||
"{value}: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_configuration_is_disabled_unless_explicitly_enabled() {
|
||||
let fixture = Fixture::new();
|
||||
@@ -1962,6 +2039,7 @@ end"#,
|
||||
"file_picker_view": "tree", "file_explorer_view": "tree",
|
||||
"git_blame": true,
|
||||
"git_gutter": "bar",
|
||||
"scroll_distance": 0,
|
||||
"top_bar": {}, "bottom_bar": {}, "labels": {},
|
||||
"popup": {"command": {}, "search": {}},
|
||||
"clipboard_mode": "system", "diff_context_lines": 10, "diff_expand_lines": 15,
|
||||
|
||||
@@ -141,6 +141,7 @@ module Vamp
|
||||
@bar = BarConfig.new
|
||||
@popup = PopupConfig.new
|
||||
@labels = {}
|
||||
@scroll_distance = 0
|
||||
@diff_context_lines = 10
|
||||
@diff_expand_lines = 15
|
||||
@diff_view_mode = "split"
|
||||
@@ -214,6 +215,12 @@ module Vamp
|
||||
end
|
||||
end
|
||||
|
||||
# @param distance [Integer, nil] nonnegative cursor margin, or nil to center
|
||||
# @return [Integer, nil]
|
||||
def scroll_distance=(distance)
|
||||
@scroll_distance = distance.nil? ? nil : diff_line_count!(distance, "scroll_distance", 0)
|
||||
end
|
||||
|
||||
# @param lines [Integer] nonnegative context lines around each diff change
|
||||
# @return [Integer]
|
||||
def diff_context_lines=(lines)
|
||||
@@ -285,6 +292,7 @@ module Vamp
|
||||
"file_explorer_view" => @file_explorer_view, "clipboard_mode" => @clipboard_mode,
|
||||
"git_blame" => @git_blame,
|
||||
"git_gutter" => @git_gutter,
|
||||
"scroll_distance" => @scroll_distance,
|
||||
"top_bar" => @bar.top, "bottom_bar" => @bar.bottom, "labels" => @labels,
|
||||
"popup" => { "command" => @popup.command, "search" => @popup.search },
|
||||
"diff_context_lines" => @diff_context_lines, "diff_expand_lines" => @diff_expand_lines,
|
||||
|
||||
@@ -256,6 +256,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_diff_context_options(config.diff_context_lines, config.diff_expand_lines);
|
||||
editor.set_diff_display_options(config.diff_view_mode, config.diff_split_orientation);
|
||||
if let Err(error) =
|
||||
|
||||
+468
-75
@@ -2,6 +2,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
fmt::Write as _,
|
||||
hash::{Hash, Hasher},
|
||||
io::{self, Stdout},
|
||||
panic::{self, AssertUnwindSafe, PanicHookInfo},
|
||||
sync::{Arc, LazyLock, Mutex},
|
||||
@@ -277,6 +278,27 @@ fn clamp_visual_bottom(
|
||||
start
|
||||
}
|
||||
|
||||
fn scroll_margin(size: usize, distance: Option<usize>) -> usize {
|
||||
distance.unwrap_or(size / 2).min(size / 2)
|
||||
}
|
||||
|
||||
fn scroll_offset(top: usize, cursor: usize, size: usize, distance: Option<usize>) -> usize {
|
||||
if size == 0 {
|
||||
return top;
|
||||
}
|
||||
let margin = scroll_margin(size, distance);
|
||||
if margin == size / 2 {
|
||||
return cursor.saturating_sub(margin);
|
||||
}
|
||||
if cursor < top.saturating_add(margin) {
|
||||
cursor.saturating_sub(margin)
|
||||
} else if cursor >= top.saturating_add(size - margin) {
|
||||
cursor.saturating_sub(size - margin - 1)
|
||||
} else {
|
||||
top
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BufferViewports {
|
||||
offsets: BTreeMap<usize, (usize, usize)>,
|
||||
@@ -508,6 +530,7 @@ impl BufferViewports {
|
||||
if let Some(panel) = dock
|
||||
&& let Some(picker) = editor.file_picker()
|
||||
{
|
||||
self.panels.picker_scroll.distance = editor.scroll_distance();
|
||||
draw_file_list(
|
||||
frame,
|
||||
picker,
|
||||
@@ -1617,7 +1640,7 @@ fn draw_document(
|
||||
),
|
||||
));
|
||||
if !wrapped {
|
||||
let limit = if document.anchor.is_some() {
|
||||
let limit = if document.anchor.is_some() || editor.scroll_distance() != Some(0) {
|
||||
editor.lines().len() - 1
|
||||
} else {
|
||||
editor.lines().len().saturating_sub(height)
|
||||
@@ -1661,7 +1684,24 @@ fn draw_document(
|
||||
column,
|
||||
),
|
||||
};
|
||||
if let Some(row) = toggle_row {
|
||||
let margin = scroll_margin(height, editor.scroll_distance());
|
||||
let anchor = toggle_row.or_else(|| {
|
||||
if margin == height / 2 {
|
||||
return Some(margin);
|
||||
}
|
||||
let (first, count) =
|
||||
visual_page(&mut document.wrap, editor.lines(), start, margin + 1);
|
||||
let last =
|
||||
visual_page(&mut document.wrap, editor.lines(), start, height - margin).0;
|
||||
if caret < first || count <= margin {
|
||||
Some(margin)
|
||||
} else if caret > last {
|
||||
Some(height - margin - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(row) = anchor {
|
||||
start = caret;
|
||||
for _ in 0..row {
|
||||
let Some(previous) =
|
||||
@@ -1671,21 +1711,9 @@ fn draw_document(
|
||||
};
|
||||
start = previous;
|
||||
}
|
||||
} else if caret < start {
|
||||
start = caret;
|
||||
} else if caret > visual_page(&mut document.wrap, editor.lines(), start, height).0 {
|
||||
start = caret;
|
||||
for _ in 1..height {
|
||||
let Some(previous) =
|
||||
previous_visual(&mut document.wrap, editor.lines(), start)
|
||||
else {
|
||||
break;
|
||||
};
|
||||
start = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
if document.anchor.is_none() {
|
||||
if document.anchor.is_none() && editor.scroll_distance() == Some(0) {
|
||||
start = clamp_visual_bottom(&mut document.wrap, editor.lines(), start, height);
|
||||
}
|
||||
*top = start.source;
|
||||
@@ -1706,17 +1734,11 @@ fn draw_document(
|
||||
}
|
||||
} else {
|
||||
if document.manual.is_none() {
|
||||
if cursor.row < *top {
|
||||
*top = cursor.row;
|
||||
} else if cursor.row >= top.saturating_add(height) {
|
||||
*top = cursor.row + 1 - height;
|
||||
if toggle_row.is_none() {
|
||||
*top = scroll_offset(*top, cursor.row, height, editor.scroll_distance());
|
||||
}
|
||||
let column = display_column(&editor.lines()[cursor.row], cursor.column);
|
||||
if column < *left {
|
||||
*left = column;
|
||||
} else if column >= left.saturating_add(width) {
|
||||
*left = column + 1 - width;
|
||||
}
|
||||
*left = scroll_offset(*left, column, width, editor.scroll_distance());
|
||||
}
|
||||
document.visible.extend(
|
||||
(*top..top.saturating_add(height).min(editor.lines().len())).map(|row| {
|
||||
@@ -1923,6 +1945,7 @@ fn draw_document(
|
||||
if let Some(panel) = dock
|
||||
&& let Some(picker) = editor.file_picker()
|
||||
{
|
||||
panels.picker_scroll.distance = editor.scroll_distance();
|
||||
draw_file_list(
|
||||
frame,
|
||||
picker,
|
||||
@@ -2059,6 +2082,16 @@ fn draw_overlays(
|
||||
caret: Option<(Rect, Position)>,
|
||||
panels: &mut WheelPanels,
|
||||
) {
|
||||
panels.picker_scroll.distance = editor.scroll_distance();
|
||||
if editor.completion().is_none() {
|
||||
panels.picker_scroll.completion = None;
|
||||
}
|
||||
if editor.palette().is_none() {
|
||||
panels.picker_scroll.palette = None;
|
||||
}
|
||||
if editor.diff().is_none_or(|diff| diff.phase == Phase::View) {
|
||||
panels.picker_scroll.revisions = None;
|
||||
}
|
||||
panels.input = None;
|
||||
panels.notification = None;
|
||||
let covered = editor.completion().is_some()
|
||||
@@ -2089,7 +2122,7 @@ fn draw_overlays(
|
||||
if let Some(completion) = editor.completion()
|
||||
&& let Some((_, caret)) = caret
|
||||
{
|
||||
panels.completion = draw_completion(frame, completion, area, caret);
|
||||
panels.completion = draw_completion(frame, completion, area, caret, panels);
|
||||
}
|
||||
if let Some(picker) = editor.file_picker()
|
||||
&& picker.presentation == FilePresentation::Floating
|
||||
@@ -2116,6 +2149,7 @@ fn draw_overlays(
|
||||
area,
|
||||
editor.mode(),
|
||||
editor.selection_navigation_hint(),
|
||||
panels,
|
||||
);
|
||||
}
|
||||
if let Some(hover) = editor.hover() {
|
||||
@@ -2415,6 +2449,12 @@ fn draw_diff(
|
||||
panels: &mut WheelPanels,
|
||||
editor: Option<&Editor>,
|
||||
) {
|
||||
if let Some(editor) = editor {
|
||||
panels.picker_scroll.distance = editor.scroll_distance();
|
||||
}
|
||||
if diff.phase == Phase::View {
|
||||
panels.picker_scroll.revisions = None;
|
||||
}
|
||||
let comparison = diff.comparison.as_ref();
|
||||
let selected = comparison.and_then(|comparison| comparison.files.get(diff.file));
|
||||
if diff.phase != Phase::View || !diff.selected_file_loaded() {
|
||||
@@ -2551,7 +2591,26 @@ fn draw_diff(
|
||||
1
|
||||
};
|
||||
let count = usize::from(results.height) / row_height;
|
||||
let first = diff.selected.saturating_sub(count.saturating_sub(1)).max(1);
|
||||
let mut key = std::collections::hash_map::DefaultHasher::new();
|
||||
(
|
||||
diff.phase as u8,
|
||||
diff.category,
|
||||
&diff.query,
|
||||
diff.comparison_mode as u8,
|
||||
diff.catalog
|
||||
.as_ref()
|
||||
.map(|catalog| (catalog.entries.as_ptr(), catalog.entries.len())),
|
||||
)
|
||||
.hash(&mut key);
|
||||
let first = PickerViewport::follow(
|
||||
&mut panels.picker_scroll.revisions,
|
||||
key.finish(),
|
||||
diff.selected.saturating_sub(1),
|
||||
usize::from(results.height),
|
||||
row_height,
|
||||
panels.picker_scroll.distance,
|
||||
)
|
||||
.saturating_add(1);
|
||||
let mut lines = Vec::with_capacity(count * row_height);
|
||||
for (index, revision) in entries
|
||||
.enumerate()
|
||||
@@ -2744,7 +2803,14 @@ fn draw_diff(
|
||||
let viewport = panels
|
||||
.diff_wrap
|
||||
.get_or_insert_with(DiffWrapViewport::default);
|
||||
viewport.update(diff, file, [before_area, after_area], caret, toggle_row);
|
||||
viewport.update(
|
||||
diff,
|
||||
file,
|
||||
[before_area, after_area],
|
||||
caret,
|
||||
toggle_row,
|
||||
editor.map_or(Some(0), Editor::scroll_distance),
|
||||
);
|
||||
top = viewport.top.display;
|
||||
} else {
|
||||
panels.diff_wrap = None;
|
||||
@@ -2763,10 +2829,8 @@ fn draw_diff(
|
||||
if let Some(anchor) = toggle_row {
|
||||
top = row.saturating_sub(anchor.min(height.saturating_sub(1)));
|
||||
}
|
||||
if row < top {
|
||||
top = row;
|
||||
} else if height > 0 && row >= top.saturating_add(height) {
|
||||
top = row + 1 - height;
|
||||
if toggle_row.is_none() {
|
||||
top = scroll_offset(top, row, height, editor.scroll_distance());
|
||||
}
|
||||
}
|
||||
let source_len = selected.map_or(0, |file| match focus {
|
||||
@@ -2779,11 +2843,7 @@ fn draw_diff(
|
||||
|file| diff_pane_text_width(diff, file, pane, focus),
|
||||
);
|
||||
let column = display_column(&editor.lines()[editor.cursor().row], editor.cursor().column);
|
||||
if column < *left {
|
||||
*left = column;
|
||||
} else if width > 0 && column >= left.saturating_add(width) {
|
||||
*left = column + 1 - width;
|
||||
}
|
||||
*left = scroll_offset(*left, column, width, editor.scroll_distance());
|
||||
}
|
||||
if let Some(id) = diff.comparison_id
|
||||
&& let Some(file) = selected.filter(|file| file.loaded)
|
||||
@@ -3479,6 +3539,7 @@ impl DiffWrapViewport {
|
||||
areas: [Rect; 2],
|
||||
caret: Option<(Focus, Cursor)>,
|
||||
anchor_row: Option<usize>,
|
||||
distance: Option<usize>,
|
||||
) {
|
||||
let Some(id) = diff.comparison_id else { return };
|
||||
let key = Some((id, diff.file, diff.live_revision));
|
||||
@@ -3522,23 +3583,29 @@ impl DiffWrapViewport {
|
||||
if let Some((focus, cursor)) = caret
|
||||
&& let Some(position) = self.caret_position(diff, file, focus, cursor)
|
||||
{
|
||||
if let Some(row) = anchor_row {
|
||||
self.top = position;
|
||||
for _ in 0..row.min(height.saturating_sub(1)) {
|
||||
self.top = self.step(diff, file, self.top, false);
|
||||
}
|
||||
self.fill(diff, file, paint_height);
|
||||
} else if position < self.top {
|
||||
self.top = position;
|
||||
self.fill(diff, file, paint_height);
|
||||
} else if !self
|
||||
let margin = scroll_margin(height, distance);
|
||||
let row = self
|
||||
.rows
|
||||
.iter()
|
||||
.take(height)
|
||||
.any(|row| row.position == position)
|
||||
{
|
||||
.position(|row| row.position == position);
|
||||
let anchor = anchor_row.or_else(|| {
|
||||
if height == 0 {
|
||||
None
|
||||
} else if margin == height / 2
|
||||
|| position < self.top
|
||||
|| row.is_some_and(|row| row < margin)
|
||||
{
|
||||
Some(margin)
|
||||
} else if row.is_none_or(|row| row >= height - margin) {
|
||||
Some(height - margin - 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(row) = anchor {
|
||||
self.top = position;
|
||||
for _ in 1..height {
|
||||
for _ in 0..row.min(height.saturating_sub(1)) {
|
||||
self.top = self.step(diff, file, self.top, false);
|
||||
}
|
||||
self.fill(diff, file, paint_height);
|
||||
@@ -3716,6 +3783,7 @@ struct WheelPanels {
|
||||
hover_syntax: SyntaxCache,
|
||||
files: Option<(Rect, usize, usize)>,
|
||||
manual_files: Option<FileWheelOffset>,
|
||||
picker_scroll: PickerScroll,
|
||||
diff: Option<DiffGeometry>,
|
||||
diff_colors: DiffColors,
|
||||
diff_syntax: Option<DiffSyntaxCache>,
|
||||
@@ -3723,27 +3791,98 @@ struct WheelPanels {
|
||||
diff_wrap: Option<DiffWrapViewport>,
|
||||
}
|
||||
|
||||
struct PickerScroll {
|
||||
distance: Option<usize>,
|
||||
files: Option<PickerViewport>,
|
||||
completion: Option<PickerViewport>,
|
||||
palette: Option<PickerViewport>,
|
||||
revisions: Option<PickerViewport>,
|
||||
}
|
||||
|
||||
impl Default for PickerScroll {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
distance: Some(0),
|
||||
files: None,
|
||||
completion: None,
|
||||
palette: None,
|
||||
revisions: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PickerViewport {
|
||||
key: u64,
|
||||
top: usize,
|
||||
}
|
||||
|
||||
impl PickerViewport {
|
||||
fn follow(
|
||||
viewport: &mut Option<Self>,
|
||||
key: u64,
|
||||
selected: usize,
|
||||
height: usize,
|
||||
row_height: usize,
|
||||
distance: Option<usize>,
|
||||
) -> usize {
|
||||
let viewport = viewport.get_or_insert_with(Self::default);
|
||||
if viewport.key != key {
|
||||
viewport.key = key;
|
||||
viewport.top = 0;
|
||||
}
|
||||
if height < row_height {
|
||||
return viewport.top;
|
||||
}
|
||||
let margin = scroll_margin(height, distance);
|
||||
let top = scroll_offset(
|
||||
viewport.top.saturating_mul(row_height),
|
||||
selected.saturating_mul(row_height),
|
||||
height,
|
||||
distance,
|
||||
);
|
||||
let first = selected.saturating_sub(margin.div_ceil(row_height));
|
||||
let last = selected
|
||||
.saturating_add(1)
|
||||
.saturating_sub((height - margin) / row_height);
|
||||
viewport.top = if margin == height / 2 || last > first {
|
||||
selected.saturating_sub((height / 2) / row_height)
|
||||
} else {
|
||||
top.div_ceil(row_height).clamp(last, first)
|
||||
};
|
||||
viewport.top
|
||||
}
|
||||
}
|
||||
|
||||
fn file_picker_key(picker: &FilePicker) -> u64 {
|
||||
let mut key = std::collections::hash_map::DefaultHasher::new();
|
||||
(
|
||||
&picker.root,
|
||||
&picker.query,
|
||||
picker.presentation as u8,
|
||||
picker.view as u8,
|
||||
picker.viewport_revision(),
|
||||
)
|
||||
.hash(&mut key);
|
||||
key.finish()
|
||||
}
|
||||
|
||||
struct FileWheelOffset {
|
||||
top: usize,
|
||||
root: std::path::PathBuf,
|
||||
query: String,
|
||||
state: (usize, usize, FilePresentation, FileView, bool, bool),
|
||||
state: (usize, u64),
|
||||
}
|
||||
|
||||
impl FileWheelOffset {
|
||||
fn state(picker: &FilePicker) -> (usize, usize, FilePresentation, FileView, bool, bool) {
|
||||
(
|
||||
picker.selected,
|
||||
picker.entries().len(),
|
||||
picker.presentation,
|
||||
picker.view,
|
||||
picker.searching,
|
||||
picker.focused,
|
||||
)
|
||||
fn state(picker: &FilePicker) -> (usize, u64) {
|
||||
(picker.selected, file_picker_key(picker))
|
||||
}
|
||||
|
||||
fn matches(&self, picker: &FilePicker) -> bool {
|
||||
self.state == Self::state(picker) && self.root == picker.root && self.query == picker.query
|
||||
fn matches(&self, picker: &FilePicker, key: u64) -> bool {
|
||||
self.state == (picker.selected, key)
|
||||
&& self.root == picker.root
|
||||
&& self.query == picker.query
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3751,6 +3890,7 @@ impl WheelPanels {
|
||||
fn begin_frame(&mut self) {
|
||||
if self.files.is_none() {
|
||||
self.manual_files = None;
|
||||
self.picker_scroll.files = None;
|
||||
}
|
||||
self.hover = None;
|
||||
self.caret = None;
|
||||
@@ -3761,28 +3901,35 @@ impl WheelPanels {
|
||||
self.previous_diff = self.diff.take();
|
||||
}
|
||||
|
||||
// Keyboard/paste may change the catalog without changing its length.
|
||||
fn follow_files(&mut self) {
|
||||
self.manual_files = None;
|
||||
}
|
||||
|
||||
fn file_top(&mut self, picker: &FilePicker, panel: Rect, height: usize) -> usize {
|
||||
let key = FileWheelOffset::state(picker).1;
|
||||
if self
|
||||
.manual_files
|
||||
.as_ref()
|
||||
.is_some_and(|offset| !offset.matches(picker))
|
||||
.is_some_and(|offset| !offset.matches(picker, key))
|
||||
{
|
||||
self.manual_files = None;
|
||||
}
|
||||
let count = picker.entries().len();
|
||||
let following = picker
|
||||
.selected
|
||||
.min(count.saturating_sub(1))
|
||||
.saturating_sub(height.saturating_sub(1));
|
||||
let following = PickerViewport::follow(
|
||||
&mut self.picker_scroll.files,
|
||||
key,
|
||||
picker.selected.min(count.saturating_sub(1)),
|
||||
height,
|
||||
1,
|
||||
self.picker_scroll.distance,
|
||||
);
|
||||
let top = self.manual_files.as_mut().map_or(following, |offset| {
|
||||
offset.top = offset.top.min(count.saturating_sub(height.max(1)));
|
||||
offset.top
|
||||
});
|
||||
if let Some(viewport) = &mut self.picker_scroll.files {
|
||||
viewport.top = top;
|
||||
}
|
||||
self.files = Some((panel, height, top));
|
||||
top
|
||||
}
|
||||
@@ -3866,10 +4013,11 @@ impl WheelPanels {
|
||||
&& panel.contains(position)
|
||||
{
|
||||
if height > 0 {
|
||||
let key = file_picker_key(picker);
|
||||
if self
|
||||
.manual_files
|
||||
.as_ref()
|
||||
.is_some_and(|offset| !offset.matches(picker))
|
||||
.is_some_and(|offset| !offset.matches(picker, key))
|
||||
{
|
||||
self.manual_files = None;
|
||||
}
|
||||
@@ -3877,7 +4025,7 @@ impl WheelPanels {
|
||||
top: drawn_top,
|
||||
root: picker.root.clone(),
|
||||
query: picker.query.clone(),
|
||||
state: FileWheelOffset::state(picker),
|
||||
state: (picker.selected, key),
|
||||
});
|
||||
offset.top = if down {
|
||||
offset.top.saturating_add(3)
|
||||
@@ -3964,6 +4112,7 @@ fn draw_completion(
|
||||
completion: &Completion,
|
||||
area: Rect,
|
||||
caret: Position,
|
||||
panels: &mut WheelPanels,
|
||||
) -> Option<Rect> {
|
||||
if area.width == 0 || area.height == 0 || !area.contains(caret) {
|
||||
return None;
|
||||
@@ -3975,7 +4124,16 @@ fn draw_completion(
|
||||
let above = caret.y - area.y;
|
||||
let downward = below >= desired_height || below >= above;
|
||||
let height = desired_height.min(if downward { below } else { above }.max(1));
|
||||
let first = selected.saturating_sub(usize::from(height) - 1);
|
||||
let mut key = std::collections::hash_map::DefaultHasher::new();
|
||||
(completion.items.as_ptr(), completion.items.len()).hash(&mut key);
|
||||
let first = PickerViewport::follow(
|
||||
&mut panels.picker_scroll.completion,
|
||||
key.finish(),
|
||||
selected,
|
||||
usize::from(height),
|
||||
1,
|
||||
panels.picker_scroll.distance,
|
||||
);
|
||||
let lines: Vec<Line> = (first..count)
|
||||
.take(usize::from(height))
|
||||
.map(|index| {
|
||||
@@ -4406,6 +4564,7 @@ fn draw_palette(
|
||||
area: Rect,
|
||||
mode: Mode,
|
||||
navigation_hint: &str,
|
||||
panels: &mut WheelPanels,
|
||||
) {
|
||||
if area.width == 0 || area.height == 0 {
|
||||
return;
|
||||
@@ -4473,6 +4632,7 @@ fn draw_palette(
|
||||
);
|
||||
frame.set_cursor_position((query_area.x + (query_width - offset) as u16, query_area.y));
|
||||
if palette.entries.is_empty() {
|
||||
panels.picker_scroll.palette = None;
|
||||
frame.render_widget(
|
||||
Paragraph::new(if palette.buffers {
|
||||
"No matching buffers"
|
||||
@@ -4488,7 +4648,17 @@ fn draw_palette(
|
||||
usize::from(results_area.height >= 2) + 1
|
||||
};
|
||||
let count = usize::from(results_area.height) / row_height;
|
||||
let first = palette.selected.saturating_sub(count - 1);
|
||||
let mut key = std::collections::hash_map::DefaultHasher::new();
|
||||
(&palette.query, palette.buffers).hash(&mut key);
|
||||
(palette.entries.as_ptr(), palette.entries.len()).hash(&mut key);
|
||||
let first = PickerViewport::follow(
|
||||
&mut panels.picker_scroll.palette,
|
||||
key.finish(),
|
||||
palette.selected.min(palette.entries.len() - 1),
|
||||
usize::from(results_area.height),
|
||||
row_height,
|
||||
panels.picker_scroll.distance,
|
||||
);
|
||||
let id_width = palette
|
||||
.entries
|
||||
.last()
|
||||
@@ -4689,6 +4859,109 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_distance_follows_document_and_diff_visual_rows_and_columns() {
|
||||
let directory = Directory::new();
|
||||
let text = (0..80)
|
||||
.map(|row| format!("row-{row:02} {}", "x".repeat(160)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
for diff in [false, true] {
|
||||
for wrapped in [false, true] {
|
||||
for distance in [Some(0), Some(2), None, Some(usize::MAX)] {
|
||||
let mut editor = if diff {
|
||||
let mut editor = readonly_diff_editor(syntax_diff_file(
|
||||
"history.txt",
|
||||
&text,
|
||||
&text.replace('x', "y"),
|
||||
));
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor
|
||||
} else {
|
||||
Editor::open(Some(directory.file("document.txt", &text)), vec![]).unwrap()
|
||||
};
|
||||
editor.set_scroll_distance(distance);
|
||||
if wrapped {
|
||||
editor.execute("word_wrap.toggle");
|
||||
}
|
||||
let mut views = BufferViewports::default();
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(80, 16)).unwrap();
|
||||
let render = |terminal: &mut Terminal<ratatui::backend::TestBackend>,
|
||||
views: &mut BufferViewports,
|
||||
editor: &mut Editor| {
|
||||
if diff {
|
||||
render_native_diff(terminal, views, editor)
|
||||
} else {
|
||||
terminal.draw(|frame| views.draw(frame, editor)).unwrap();
|
||||
terminal.get_cursor_position().unwrap()
|
||||
}
|
||||
};
|
||||
render(&mut terminal, &mut views, &mut editor);
|
||||
for _ in 0..30 {
|
||||
editor.handle(Input::Down);
|
||||
render(&mut terminal, &mut views, &mut editor);
|
||||
}
|
||||
let viewport = if diff {
|
||||
diff_inner(views.panels.diff.as_ref().unwrap().regions[1].0)
|
||||
} else {
|
||||
views.document.geometry.unwrap().1
|
||||
};
|
||||
let height = usize::from(viewport.height);
|
||||
let expected = if distance.is_none_or(|distance| distance >= height / 2) {
|
||||
height / 2
|
||||
} else {
|
||||
height - distance.unwrap() - 1
|
||||
};
|
||||
let caret = terminal.get_cursor_position().unwrap();
|
||||
assert_eq!(
|
||||
usize::from(caret.y - viewport.y),
|
||||
expected,
|
||||
"diff={diff}, wrap={wrapped}, distance={distance:?}"
|
||||
);
|
||||
if !wrapped && distance == Some(2) {
|
||||
editor.handle(Input::Up);
|
||||
let reversed = render(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(reversed.y + 1, caret.y);
|
||||
}
|
||||
if !wrapped {
|
||||
for _ in 0..120 {
|
||||
editor.handle(Input::Right);
|
||||
render(&mut terminal, &mut views, &mut editor);
|
||||
}
|
||||
let left = if diff {
|
||||
editor.diff().unwrap().left_before
|
||||
} else {
|
||||
views.offsets[&editor.buffer_id()].1
|
||||
};
|
||||
let width = if diff {
|
||||
usize::from(views.panels.diff.as_ref().unwrap().text_widths[0])
|
||||
} else {
|
||||
views.document.text_width
|
||||
};
|
||||
let expected = if distance.is_none_or(|distance| distance >= width / 2) {
|
||||
width / 2
|
||||
} else {
|
||||
width - distance.unwrap() - 1
|
||||
};
|
||||
assert_eq!(editor.cursor().column - left, expected);
|
||||
}
|
||||
if distance == Some(usize::MAX) {
|
||||
terminal.backend_mut().resize(60, 12);
|
||||
terminal.resize(Rect::new(0, 0, 60, 12)).unwrap();
|
||||
let caret = render(&mut terminal, &mut views, &mut editor);
|
||||
let viewport = if diff {
|
||||
diff_inner(views.panels.diff.as_ref().unwrap().regions[1].0)
|
||||
} else {
|
||||
views.document.geometry.unwrap().1
|
||||
};
|
||||
assert_eq!(caret.y - viewport.y, viewport.height / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn screen_text(terminal: &Terminal<ratatui::backend::TestBackend>) -> String {
|
||||
let buffer = terminal.backend().buffer();
|
||||
let area = buffer.area;
|
||||
@@ -10216,7 +10489,13 @@ end
|
||||
.collect();
|
||||
frame.render_widget(Paragraph::new(rows), frame.area());
|
||||
frame.set_cursor_position(caret);
|
||||
panel = draw_completion(frame, &completion, workspace, caret);
|
||||
panel = draw_completion(
|
||||
frame,
|
||||
&completion,
|
||||
workspace,
|
||||
caret,
|
||||
&mut WheelPanels::default(),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let mut panel = panel.unwrap();
|
||||
@@ -10247,7 +10526,14 @@ end
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
frame.set_cursor_position(caret);
|
||||
panel = draw_completion(frame, &completion, workspace, caret).unwrap();
|
||||
panel = draw_completion(
|
||||
frame,
|
||||
&completion,
|
||||
workspace,
|
||||
caret,
|
||||
&mut WheelPanels::default(),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -11634,7 +11920,6 @@ end
|
||||
panels.manual_files.as_ref().unwrap().top,
|
||||
session.editor().file_picker().unwrap().entries().len() - height
|
||||
);
|
||||
panels.follow_files();
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
@@ -11647,7 +11932,8 @@ end
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(panels.files.unwrap().2, 0);
|
||||
assert_eq!(panels.files.unwrap().2, entry_count - height);
|
||||
panels.follow_files();
|
||||
session.handle(Input::Ctrl('p'));
|
||||
assert!(session.editor().palette().is_some());
|
||||
assert!(panels.scroll(&mut session, mouse));
|
||||
@@ -11711,6 +11997,110 @@ end
|
||||
assert!(screen_text(&terminal).contains("> file-10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picker_viewport_preserves_direction_and_uses_visual_row_margins() {
|
||||
for row_height in [1, 2] {
|
||||
let mut viewport = None;
|
||||
let height = 4 * row_height;
|
||||
for selected in 0..8 {
|
||||
PickerViewport::follow(&mut viewport, 1, selected, height, row_height, Some(0));
|
||||
}
|
||||
for selected in (4..8).rev() {
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 1, selected, height, row_height, Some(0)),
|
||||
4,
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 1, 3, height, row_height, Some(0)),
|
||||
3,
|
||||
);
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 2, 3, height, row_height, Some(0)),
|
||||
0,
|
||||
);
|
||||
for distance in [None, Some(usize::MAX)] {
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 2, 7, height, row_height, distance),
|
||||
5,
|
||||
);
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 2, 0, height, row_height, distance),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut viewport = None;
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 1, 7, 8, 2, Some(1)),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 1, 6, 8, 2, Some(1)),
|
||||
5
|
||||
);
|
||||
assert_eq!(
|
||||
PickerViewport::follow(&mut viewport, 1, 7, 8, 2, Some(3)),
|
||||
5
|
||||
);
|
||||
assert_eq!(PickerViewport::follow(&mut viewport, 1, 7, 7, 2, None), 6);
|
||||
assert_eq!(PickerViewport::follow(&mut viewport, 1, 7, 3, 1, None), 6);
|
||||
assert_eq!(PickerViewport::follow(&mut viewport, 1, 7, 0, 1, None), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_viewport_keeps_manual_wheel_until_selection_or_catalog_changes() {
|
||||
let directory = Directory::new();
|
||||
for index in 0..12 {
|
||||
directory.file(&format!("file-{index:02}"), "");
|
||||
}
|
||||
let mut picker = FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Docked,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
let area = Rect::new(0, 0, 40, 8);
|
||||
picker.selected = 7;
|
||||
assert_eq!(panels.file_top(&picker, area, 5), 3);
|
||||
picker.selected = 6;
|
||||
assert_eq!(panels.file_top(&picker, area, 5), 3);
|
||||
panels.picker_scroll.distance = None;
|
||||
panels.manual_files = Some(FileWheelOffset {
|
||||
top: 0,
|
||||
root: picker.root.clone(),
|
||||
query: picker.query.clone(),
|
||||
state: FileWheelOffset::state(&picker),
|
||||
});
|
||||
picker.focused = !picker.focused;
|
||||
assert_eq!(panels.file_top(&picker, area, 3), 0);
|
||||
assert!(panels.manual_files.is_some());
|
||||
picker.selected = 7;
|
||||
assert_eq!(panels.file_top(&picker, area, 5), 5);
|
||||
assert!(panels.manual_files.is_none());
|
||||
panels.manual_files = Some(FileWheelOffset {
|
||||
top: 0,
|
||||
root: picker.root.clone(),
|
||||
query: picker.query.clone(),
|
||||
state: FileWheelOffset::state(&picker),
|
||||
});
|
||||
std::fs::rename(directory.0.join("file-00"), directory.0.join("new-file")).unwrap();
|
||||
picker = FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Docked,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap();
|
||||
picker.selected = 7;
|
||||
assert_eq!(picker.entries().len(), 12);
|
||||
assert_eq!(panels.file_top(&picker, area, 5), 5);
|
||||
assert!(panels.manual_files.is_none());
|
||||
panels.begin_frame();
|
||||
panels.begin_frame();
|
||||
assert!(panels.picker_scroll.files.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_list_window_follows_control_j_k_selection() {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
@@ -14970,6 +15360,7 @@ end
|
||||
area,
|
||||
editor.mode(),
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -15030,6 +15421,7 @@ end
|
||||
area,
|
||||
Mode::Visual,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
@@ -15073,6 +15465,7 @@ end
|
||||
area,
|
||||
Mode::Normal,
|
||||
editor.selection_navigation_hint(),
|
||||
&mut WheelPanels::default(),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user