use std::{
fs, io,
path::{Path, PathBuf},
};
#[must_use]
pub fn recall(path: &Path) -> Option<(usize, usize)> {
let (stored, position) = entry(&read())?;
(stored == canonical(path)).then_some(position)
}
pub fn remember(path: &Path, line: usize, column: usize) -> io::Result<()> {
let Some(file) = file() else {
return Err(io::Error::other("no state directory"));
};
if let Some(parent) = file.parent() {
fs::create_dir_all(parent)?;
}
fs::write(file, render(&canonical(path), line, column))
}
#[must_use]
pub fn file() -> Option<PathBuf> {
let base = std::env::var_os("XDG_STATE_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.or_else(|| {
std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local").join("state"))
})?;
Some(base.join("idet").join("position"))
}
fn read() -> String {
file()
.and_then(|path| fs::read_to_string(path).ok())
.unwrap_or_default()
}
fn canonical(path: &Path) -> PathBuf {
fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn entry(text: &str) -> Option<(PathBuf, (usize, usize))> {
let mut fields = text.trim_end().splitn(3, ' ');
let line = fields.next()?.parse().ok()?;
let column = fields.next()?.parse().ok()?;
let path = fields.next()?;
(!path.is_empty()).then(|| (PathBuf::from(path), (line, column)))
}
fn render(path: &Path, line: usize, column: usize) -> String {
format!("{line} {column} {}\n", path.display())
}
#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use super::{entry, render};
#[test]
fn what_is_written_is_what_is_read_back() {
let path = Path::new("/home/p/a file with spaces.txt");
assert_eq!(
entry(&render(path, 12, 3)),
Some((path.to_path_buf(), (12, 3)))
);
}
#[test]
fn a_broken_line_is_no_position() {
assert_eq!(entry(""), None);
assert_eq!(entry("nonsense"), None);
assert_eq!(entry("7 /home/p/missing-column.txt"), None);
assert_eq!(entry("1 2 "), None);
assert_eq!(
entry("1 2 /home/p/good.txt"),
Some((PathBuf::from("/home/p/good.txt"), (1, 2)))
);
}
}