use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct Bookmark {
pub file_path: String,
pub line: u32,
pub is_new_line: bool,
pub label: Option<char>,
#[allow(dead_code)]
pub created_at: u64,
}
impl Bookmark {
pub fn new(file_path: String, line: u32, is_new_line: bool, label: Option<char>) -> Self {
let created_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
file_path,
line,
is_new_line,
label,
created_at,
}
}
pub fn location(&self) -> String {
format!("{}:{}", self.file_path, self.line)
}
}
#[derive(Debug, Default)]
pub struct BookmarkState {
pub bookmarks: Vec<Bookmark>,
pub current_index: Option<usize>,
pub list_visible: bool,
pub list_selected: usize,
}
impl BookmarkState {
pub fn new() -> Self {
Self::default()
}
pub fn toggle(&mut self, file_path: &str, line: u32, is_new_line: bool) -> bool {
if let Some(idx) = self.find_at(file_path, line, is_new_line) {
self.bookmarks.remove(idx);
if let Some(ci) = self.current_index {
if ci >= self.bookmarks.len() {
self.current_index = if self.bookmarks.is_empty() {
None
} else {
Some(self.bookmarks.len() - 1)
};
} else if ci > idx {
self.current_index = Some(ci - 1);
}
}
false
} else {
self.bookmarks.push(Bookmark::new(
file_path.to_string(),
line,
is_new_line,
None,
));
true
}
}
pub fn set_named(&mut self, file_path: &str, line: u32, is_new_line: bool, label: char) {
self.bookmarks.retain(|b| b.label != Some(label));
self.bookmarks.push(Bookmark::new(
file_path.to_string(),
line,
is_new_line,
Some(label),
));
}
fn find_at(&self, file_path: &str, line: u32, is_new_line: bool) -> Option<usize> {
self.bookmarks.iter().position(|b| {
b.file_path == file_path && b.line == line && b.is_new_line == is_new_line
})
}
pub fn find_named(&self, label: char) -> Option<&Bookmark> {
self.bookmarks.iter().find(|b| b.label == Some(label))
}
#[allow(dead_code)]
pub fn has_bookmark_at(
&self,
file_path: &str,
old_lineno: Option<u32>,
new_lineno: Option<u32>,
) -> bool {
self.bookmarks.iter().any(|b| {
b.file_path == file_path
&& ((b.is_new_line && new_lineno == Some(b.line))
|| (!b.is_new_line && old_lineno == Some(b.line)))
})
}
pub fn label_at(
&self,
file_path: &str,
old_lineno: Option<u32>,
new_lineno: Option<u32>,
) -> Option<Option<char>> {
self.bookmarks.iter().find_map(|b| {
if b.file_path == file_path
&& ((b.is_new_line && new_lineno == Some(b.line))
|| (!b.is_new_line && old_lineno == Some(b.line)))
{
Some(b.label)
} else {
None
}
})
}
pub fn next_after(&self, file_path: &str, line: u32) -> Option<(usize, &Bookmark)> {
let mut sorted: Vec<(usize, &Bookmark)> = self.bookmarks.iter().enumerate().collect();
sorted.sort_by(|(_, a), (_, b)| a.file_path.cmp(&b.file_path).then(a.line.cmp(&b.line)));
for (idx, bm) in &sorted {
if bm.file_path.as_str() > file_path || (bm.file_path == file_path && bm.line > line) {
return Some((*idx, bm));
}
}
sorted.first().map(|(idx, bm)| (*idx, *bm))
}
pub fn prev_before(&self, file_path: &str, line: u32) -> Option<(usize, &Bookmark)> {
let mut sorted: Vec<(usize, &Bookmark)> = self.bookmarks.iter().enumerate().collect();
sorted.sort_by(|(_, a), (_, b)| a.file_path.cmp(&b.file_path).then(a.line.cmp(&b.line)));
for (idx, bm) in sorted.iter().rev() {
if bm.file_path.as_str() < file_path || (bm.file_path == file_path && bm.line < line) {
return Some((*idx, *bm));
}
}
sorted.last().map(|(idx, bm)| (*idx, *bm))
}
pub fn delete(&mut self, idx: usize) -> bool {
if idx < self.bookmarks.len() {
self.bookmarks.remove(idx);
if self.list_selected >= self.bookmarks.len() && !self.bookmarks.is_empty() {
self.list_selected = self.bookmarks.len() - 1;
}
true
} else {
false
}
}
#[allow(dead_code)]
pub fn count(&self) -> usize {
self.bookmarks.len()
}
#[allow(dead_code)]
pub fn count_in_file(&self, file_path: &str) -> usize {
self.bookmarks
.iter()
.filter(|b| b.file_path == file_path)
.count()
}
pub fn all_sorted(&self) -> Vec<(usize, &Bookmark)> {
let mut sorted: Vec<(usize, &Bookmark)> = self.bookmarks.iter().enumerate().collect();
sorted.sort_by(|(_, a), (_, b)| a.file_path.cmp(&b.file_path).then(a.line.cmp(&b.line)));
sorted
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toggle_adds_and_removes() {
let mut state = BookmarkState::new();
assert!(state.toggle("foo.rs", 10, true));
assert_eq!(state.count(), 1);
assert!(!state.toggle("foo.rs", 10, true));
assert_eq!(state.count(), 0);
}
#[test]
fn named_bookmark_moves_on_reassign() {
let mut state = BookmarkState::new();
state.set_named("foo.rs", 10, true, 'a');
assert_eq!(state.count(), 1);
state.set_named("bar.rs", 20, true, 'a');
assert_eq!(state.count(), 1);
let bm = state.find_named('a').unwrap();
assert_eq!(bm.file_path, "bar.rs");
assert_eq!(bm.line, 20);
}
#[test]
fn has_bookmark_at_checks_correctly() {
let mut state = BookmarkState::new();
state.toggle("foo.rs", 10, true);
assert!(state.has_bookmark_at("foo.rs", None, Some(10)));
assert!(!state.has_bookmark_at("foo.rs", None, Some(11)));
assert!(!state.has_bookmark_at("bar.rs", None, Some(10)));
}
#[test]
fn next_and_prev_navigation() {
let mut state = BookmarkState::new();
state.toggle("a.rs", 5, true);
state.toggle("a.rs", 15, true);
state.toggle("b.rs", 3, true);
let (_, bm) = state.next_after("a.rs", 5).unwrap();
assert_eq!(bm.line, 15);
let (_, bm) = state.next_after("a.rs", 15).unwrap();
assert_eq!(bm.file_path, "b.rs");
let (_, bm) = state.prev_before("b.rs", 3).unwrap();
assert_eq!(bm.line, 15);
}
}