use crate::{BufferId, Position};
pub const JUMPLIST_LIMIT: usize = 100;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema,
)]
pub struct Spot {
pub buffer: BufferId,
pub pos: Position,
}
impl Spot {
#[must_use]
pub const fn new(buffer: BufferId, pos: Position) -> Self {
Self { buffer, pos }
}
#[must_use]
pub const fn same_line(&self, other: &Self) -> bool {
self.buffer.0 == other.buffer.0 && self.pos.line == other.pos.line
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct JumpList {
entries: Vec<Spot>,
cursor: usize,
}
impl JumpList {
#[must_use]
pub const fn new() -> Self {
Self {
entries: Vec::new(),
cursor: 0,
}
}
pub fn push(&mut self, from: Spot) {
self.entries.truncate(self.cursor);
if self
.entries
.last()
.is_some_and(|last| last.same_line(&from))
{
self.entries.pop();
}
self.entries.push(from);
self.trim();
self.cursor = self.entries.len();
}
pub fn back(&mut self, current: Spot) -> Option<Spot> {
if self.entries.is_empty() {
return None;
}
if self.cursor == self.entries.len() {
if self
.entries
.last()
.is_some_and(|last| last.same_line(¤t))
{
} 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<Spot> {
if self.cursor + 1 >= self.entries.len() {
return None;
}
self.cursor += 1;
self.entries.get(self.cursor).copied()
}
#[must_use]
pub fn entries(&self) -> &[Spot] {
&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) -> Spot {
Spot::new(BufferId(1), Position::new(line, 0))
}
fn q(line: u32) -> Spot {
Spot::new(BufferId(2), 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(Spot::new(BufferId(1), Position::new(7, 0)));
j.push(Spot::new(BufferId(1), Position::new(7, 20)));
j.push(Spot::new(BufferId(1), 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");
}
#[test]
fn the_same_line_in_a_different_buffer_does_not_collapse() {
let mut j = JumpList::new();
j.push(p(7));
j.push(q(7));
assert_eq!(
j.entries().len(),
2,
"line 7 of two different buffers is two places: {:?}",
j.entries(),
);
}
#[test]
fn walking_back_returns_the_buffer_too() {
let mut j = JumpList::new();
j.push(p(3));
let back = j.back(q(9)).expect("something older");
assert_eq!(back.buffer, BufferId(1), "must return the buffer we left");
assert_eq!(back.pos.line, 3);
}
}