use crate::buffer_ref::BufferRef;
use crate::editor::Cursor;
const JUMPLIST_CAP: usize = 100;
#[derive(Debug, Clone)]
pub struct JumpEntry {
pub doc: BufferRef,
pub cursor: Cursor,
}
#[derive(Debug, Default, Clone)]
pub struct JumpList {
entries: Vec<JumpEntry>,
current: usize,
}
impl JumpList {
pub fn push(&mut self, from: JumpEntry) {
self.entries.truncate(self.current);
if let Some(last) = self.entries.last_mut()
&& last.doc == from.doc
&& last.cursor.row == from.cursor.row
{
last.cursor = from.cursor;
self.current = self.entries.len();
return;
}
self.entries.push(from);
self.enforce_cap();
self.current = self.entries.len();
}
fn enforce_cap(&mut self) -> usize {
if self.entries.len() > JUMPLIST_CAP {
let overflow = self.entries.len() - JUMPLIST_CAP;
self.entries.drain(0..overflow);
overflow
} else {
0
}
}
pub fn backward(&mut self, count: usize, here: JumpEntry) -> Option<JumpEntry> {
if self.current == 0 {
return None;
}
if self.current == self.entries.len() {
self.entries.push(here);
self.current -= self.enforce_cap();
}
let target = self.current.saturating_sub(count);
self.current = target;
self.entries.get(target).cloned()
}
pub fn forward(&mut self, count: usize) -> Option<JumpEntry> {
let last = self.entries.len().checked_sub(1)?;
if self.current >= last {
return None;
}
let target = (self.current + count).min(last);
self.current = target;
self.entries.get(target).cloned()
}
pub fn entries(&self) -> &[JumpEntry] {
&self.entries
}
}
#[cfg(test)]
mod jumplist_tests {
use super::{JumpEntry, JumpList};
use crate::buffer_ref::BufferRef;
use crate::editor::Cursor;
fn entry(row: usize) -> JumpEntry {
JumpEntry {
doc: BufferRef::Scratch(0),
cursor: Cursor { row, col: 0 },
}
}
fn rows(j: &JumpList) -> Vec<usize> {
j.entries().iter().map(|e| e.cursor.row).collect()
}
#[test]
fn back_then_forward_returns_to_origin() {
let mut j = JumpList::default();
j.push(entry(10));
j.push(entry(20));
assert_eq!(j.backward(1, entry(30)).unwrap().cursor.row, 20);
assert_eq!(j.backward(1, entry(30)).unwrap().cursor.row, 10);
assert_eq!(j.forward(1).unwrap().cursor.row, 20);
assert_eq!(j.forward(1).unwrap().cursor.row, 30);
assert!(j.forward(1).is_none(), "already at newest");
}
#[test]
fn back_past_oldest_returns_none() {
let mut j = JumpList::default();
j.push(entry(5));
assert_eq!(j.backward(1, entry(9)).unwrap().cursor.row, 5);
assert!(j.backward(1, entry(9)).is_none());
}
#[test]
fn new_jump_after_going_back_truncates_forward_history() {
let mut j = JumpList::default();
j.push(entry(10));
j.push(entry(20));
j.push(entry(30));
j.backward(2, entry(40));
j.push(entry(99));
assert_eq!(rows(&j), vec![10, 99]);
assert!(j.forward(1).is_none());
}
#[test]
fn large_count_clamps_to_oldest_and_newest() {
let mut j = JumpList::default();
j.push(entry(10));
j.push(entry(20));
j.push(entry(30));
assert_eq!(j.backward(10, entry(40)).unwrap().cursor.row, 10);
assert_eq!(j.forward(10).unwrap().cursor.row, 40);
assert!(j.forward(1).is_none(), "already at newest");
}
#[test]
fn tip_snapshot_respects_cap() {
let mut j = JumpList::default();
for r in 0..super::JUMPLIST_CAP {
j.push(entry(r));
}
assert_eq!(j.entries().len(), super::JUMPLIST_CAP);
let landed = j.backward(1, entry(9999)).unwrap();
assert_eq!(j.entries().len(), super::JUMPLIST_CAP);
assert_eq!(landed.cursor.row, super::JUMPLIST_CAP - 1);
assert_eq!(j.forward(1).unwrap().cursor.row, 9999);
}
#[test]
fn consecutive_same_line_jumps_collapse() {
let mut j = JumpList::default();
j.push(entry(10));
j.push(JumpEntry {
doc: BufferRef::Scratch(0),
cursor: Cursor { row: 10, col: 7 },
});
assert_eq!(j.entries().len(), 1);
assert_eq!(j.entries()[0].cursor.col, 7);
}
#[test]
fn caps_at_jumplist_cap() {
let mut j = JumpList::default();
for r in 0..super::JUMPLIST_CAP + 50 {
j.push(entry(r));
}
assert_eq!(j.entries().len(), super::JUMPLIST_CAP);
assert_eq!(
j.entries().last().unwrap().cursor.row,
super::JUMPLIST_CAP + 49
);
}
}