use crate::Position;
pub const JUMPLIST_LIMIT: usize = 100;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct JumpList {
entries: Vec<Position>,
cursor: usize,
}
impl JumpList {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Vec::new(),
cursor: 0,
}
}
pub fn push(&mut self, from: Position) {
self.entries.truncate(self.cursor);
if self
.entries
.last()
.is_some_and(|last| last.line == from.line)
{
self.entries.pop();
}
self.entries.push(from);
self.trim();
self.cursor = self.entries.len();
}
pub fn back(&mut self, current: Position) -> Option<Position> {
if self.entries.is_empty() {
return None;
}
if self.cursor == self.entries.len() {
if self
.entries
.last()
.is_some_and(|last| last.line == current.line)
{
} else {
self.entries.push(current);
self.trim();
}
self.cursor = self.entries.len().saturating_sub(1);
}
if self.cursor == 0 {
return None;
}
self.cursor -= 1;
self.entries.get(self.cursor).copied()
}
pub fn forward(&mut self) -> Option<Position> {
if self.cursor + 1 >= self.entries.len() {
return None;
}
self.cursor += 1;
self.entries.get(self.cursor).copied()
}
#[must_use]
pub fn entries(&self) -> &[Position] {
&self.entries
}
#[must_use]
pub fn len(&self) -> usize {
self.entries.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn trim(&mut self) {
while self.entries.len() > JUMPLIST_LIMIT {
self.entries.remove(0);
self.cursor = self.cursor.saturating_sub(1);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(line: u32) -> Position {
Position::new(line, 0)
}
#[test]
fn an_empty_list_has_nowhere_to_go() {
let mut j = JumpList::new();
assert_eq!(j.back(p(5)), None);
assert_eq!(j.forward(), None);
assert!(j.is_empty());
}
#[test]
fn back_returns_to_where_the_jump_was_taken_from() {
let mut j = JumpList::new();
j.push(p(3));
assert_eq!(j.back(p(40)), Some(p(3)), "<C-o> returns to the origin");
}
#[test]
fn forward_returns_to_where_back_was_pressed_from() {
let mut j = JumpList::new();
j.push(p(3));
assert_eq!(j.back(p(40)), Some(p(3)));
assert_eq!(j.forward(), Some(p(40)), "<C-i> comes back to where I was");
}
#[test]
fn walking_back_twice_visits_both_origins_newest_first() {
let mut j = JumpList::new();
j.push(p(1)); j.push(p(10));
assert_eq!(j.back(p(50)), Some(p(10)));
assert_eq!(j.back(p(10)), Some(p(1)));
assert_eq!(j.back(p(1)), None, "nothing older than the first jump");
}
#[test]
fn a_new_jump_abandons_the_forward_history() {
let mut j = JumpList::new();
j.push(p(1));
j.push(p(10));
j.back(p(50));
j.push(p(20));
assert_eq!(j.forward(), None, "the abandoned future must be gone");
assert_eq!(
j.back(p(99)),
Some(p(20)),
"the new branch is what we return to"
);
}
#[test]
fn consecutive_jumps_from_one_line_collapse() {
let mut j = JumpList::new();
j.push(Position::new(7, 0));
j.push(Position::new(7, 20));
j.push(Position::new(7, 40));
assert_eq!(j.len(), 1, "one entry for the line, got {:?}", j.entries());
}
#[test]
fn jumps_from_different_lines_all_survive() {
let mut j = JumpList::new();
j.push(p(1));
j.push(p(2));
j.push(p(3));
assert_eq!(j.len(), 3);
}
#[test]
fn the_list_is_bounded_and_drops_the_oldest() {
let mut j = JumpList::new();
for line in 0..(JUMPLIST_LIMIT as u32 + 25) {
j.push(p(line));
}
assert_eq!(j.len(), JUMPLIST_LIMIT, "bounded");
assert_eq!(
j.entries().first().copied(),
Some(p(25)),
"the oldest entries are the ones dropped",
);
}
#[test]
fn back_from_the_same_line_does_not_duplicate_it() {
let mut j = JumpList::new();
j.push(p(3));
j.back(p(3));
assert_eq!(
j.len(),
1,
"no duplicate entry for one line: {:?}",
j.entries()
);
}
#[test]
fn forward_past_the_newest_end_is_none() {
let mut j = JumpList::new();
j.push(p(1));
j.back(p(9));
assert_eq!(j.forward(), Some(p(9)));
assert_eq!(j.forward(), None, "cannot walk past where I started");
}
}