use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::buffer_ref::BufferRef;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bookmark {
pub target: BufferRef,
pub line: usize,
}
#[derive(Debug, Serialize, Deserialize)]
struct StoredMark {
path: String,
line: usize,
}
pub struct BookmarkStore {
pub marks: Vec<Bookmark>,
root: PathBuf,
}
impl BookmarkStore {
pub fn load(startup_cwd: &Path) -> Self {
let root = project_root(startup_cwd);
let marks = read_all()
.remove(&root_key(&root))
.unwrap_or_default()
.into_iter()
.map(|m| Bookmark {
target: BufferRef::File(PathBuf::from(m.path)),
line: m.line,
})
.collect();
Self { marks, root }
}
pub fn add(&mut self, target: BufferRef, line: usize) -> bool {
if self.marks.iter().any(|m| m.target == target) {
return false;
}
self.marks.push(Bookmark { target, line });
self.persist();
true
}
pub fn remove_target(&mut self, target: &BufferRef) {
let before = self.marks.len();
self.marks.retain(|m| &m.target != target);
if self.marks.len() != before {
self.persist();
}
}
fn persist(&self) {
let Some(path) = bookmarks_path() else {
return;
};
let mut all = read_all();
let stored: Vec<StoredMark> = self
.marks
.iter()
.filter_map(|m| match &m.target {
BufferRef::File(p) => Some(StoredMark {
path: p.to_string_lossy().into_owned(),
line: m.line,
}),
BufferRef::Scratch(_) => None,
})
.collect();
if stored.is_empty() {
all.remove(&root_key(&self.root));
} else {
all.insert(root_key(&self.root), stored);
}
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
match serde_json::to_string_pretty(&all) {
Ok(json) => {
if let Err(e) = std::fs::write(&path, json) {
crate::vlog!("bookmark persist: write {}: {e}", path.display());
}
}
Err(e) => crate::vlog!("bookmark persist: serialize: {e}"),
}
}
}
fn read_all() -> BTreeMap<String, Vec<StoredMark>> {
let Some(path) = bookmarks_path() else {
return BTreeMap::new();
};
let Ok(text) = std::fs::read_to_string(&path) else {
return BTreeMap::new();
};
serde_json::from_str(&text).unwrap_or_default()
}
fn root_key(root: &Path) -> String {
root.to_string_lossy().into_owned()
}
fn project_root(startup_cwd: &Path) -> PathBuf {
let start = startup_cwd
.canonicalize()
.unwrap_or_else(|_| startup_cwd.to_path_buf());
for dir in start.ancestors() {
if dir.join(".git").exists() {
return dir.to_path_buf();
}
}
start
}
fn bookmarks_path() -> Option<PathBuf> {
if let Some(xdg) = std::env::var_os("XDG_STATE_HOME") {
return Some(PathBuf::from(xdg).join("vorto").join("bookmarks.json"));
}
let home = std::env::var_os("HOME")?;
Some(
PathBuf::from(home)
.join(".local")
.join("state")
.join("vorto")
.join("bookmarks.json"),
)
}