feat: configure cursor distance from viewport edges
This commit is contained in:
+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