use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::editor::Editor;
use strop_core::history::History;
use strop_core::Buffer;
const UNDO_CAP: usize = 200;
#[derive(Debug, Default, Serialize, Deserialize)]
pub(crate) struct Session {
pub buffers: Vec<BufferState>,
pub current: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct BufferState {
pub path: String,
pub line: usize,
pub col: usize,
pub view_top: usize,
pub undo: Option<History>,
}
fn session_path(base_dir: Option<&Path>, cwd: &Path) -> Option<PathBuf> {
let base = base_dir?.to_path_buf();
let mut hasher = std::hash::DefaultHasher::new();
std::hash::Hash::hash(&cwd, &mut hasher);
let key = format!("{:016x}", std::hash::Hasher::finish(&hasher));
Some(
base.join("strop")
.join("sessions")
.join(format!("{key}.json")),
)
}
pub(crate) fn capture(editor: &Editor) -> Option<Session> {
let mut buffers = Vec::new();
for (id, doc) in editor.docs.iter() {
let buf = &doc.buf;
if buf.readonly || buf.path.is_none() {
continue;
}
let path = buf.path.clone()?;
let undo = if buf.history.depth() > 0 {
let mut h = buf.history.clone();
h.cap(UNDO_CAP);
Some(h)
} else {
None
};
buffers.push(BufferState {
path,
line: if id == editor.current() {
editor.buf().line_of(editor.head())
} else {
0
},
col: if id == editor.current() {
editor.buf().col_of(editor.head())
} else {
0
},
view_top: if id == editor.current() {
editor.view_top()
} else {
0
},
undo,
});
}
if buffers.is_empty() {
return None;
}
let current = buffers
.iter()
.position(|b| Some(&b.path) == editor.cur().buf.path.as_ref())
.unwrap_or(0);
Some(Session { buffers, current })
}
pub fn restore(editor: &mut Editor) -> bool {
let Some(path) = session_path(editor.state_dir.as_deref(), &editor.cwd) else {
return false;
};
let Ok(text) = std::fs::read_to_string(&path) else {
return false;
};
let Ok(session) = serde_json::from_str::<Session>(&text) else {
return false;
};
if session.buffers.is_empty() {
return false;
}
editor.docs.clear();
for b in &session.buffers {
let mut buf = Buffer::open(&b.path).unwrap_or_else(|_| Buffer::from_text(""));
if let Some(h) = &b.undo {
buf.history = h.clone();
}
editor.docs.insert(crate::editor::Document::new(buf));
}
let nth = session.current.min(editor.docs.len().saturating_sub(1));
let cur_id = editor
.docs
.iter()
.nth(nth)
.map(|(id, _)| id)
.expect("docs non-empty");
editor.view_mut().doc = cur_id;
let b = &session.buffers[nth];
editor.view_mut().view_top = b.view_top;
let line_start = editor
.buf()
.line_start(b.line.min(editor.buf().len_lines() - 1));
editor.set_head(editor.buf().clamp_boundary(line_start + b.col));
editor.clamp_cursor();
editor.mru = editor.docs.iter().map(|(id, _)| id).collect();
editor.touch_mru(editor.current());
editor.discover_git();
true
}
pub fn save(editor: &Editor) {
let Some(path) = session_path(editor.state_dir.as_deref(), &editor.cwd) else {
return;
};
let Some(session) = capture(editor) else {
return;
};
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(path, serde_json::to_string(&session).unwrap_or_default());
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::Command;
#[test]
fn roundtrip_restores_buffers_and_position() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("a.rs"), "fn a() {}\nfn b() {}\n").unwrap();
let mut e = Editor::new(Buffer::open(root.join("a.rs").to_str().unwrap()).unwrap());
e.cwd = root.to_path_buf();
e.state_dir = Some(root.join("state"));
e.feed_text("jl"); e.feed_text("ix"); e.feed(crate::editor::Key::Esc);
save(&e);
let mut e2 = Editor::new(Buffer::from_text(""));
e2.cwd = root.to_path_buf();
e2.state_dir = Some(root.join("state"));
assert!(restore(&mut e2));
assert_eq!(
e2.buf().path.as_deref(),
Some(root.join("a.rs").to_str().unwrap())
);
assert_eq!(e2.buf().line_of(e2.head()), 1);
assert_eq!(e2.buf().col_of(e2.head()), 1);
assert!(
e2.buf().history.depth() > 0,
"undo history crossed the session"
);
let _ = Command::new("true").output();
}
#[test]
fn empty_or_readonly_never_persist() {
let dir = tempfile::tempdir().unwrap();
let mut e = Editor::new(Buffer::from_text(""));
e.cwd = dir.path().to_path_buf();
e.state_dir = Some(dir.path().join("state"));
save(&e);
assert!(!dir.path().join("state").exists());
}
}