use strop_core::{Buffer, Range};
use super::{Editor, Key};
pub struct UndoBrowser {
pub browser: usize,
pub origin: usize,
pub row_rev: Vec<Option<usize>>,
}
impl Editor {
pub(crate) fn open_undo_tree(&mut self) {
if self.buf().readonly {
self.message = "readonly buffer".into();
return;
}
let rows = self.buf().history.tree_rows();
if rows.is_empty() {
self.message = "no undo history".into();
return;
}
let origin = self.current;
let name = self
.buf()
.path
.clone()
.unwrap_or_else(|| "[scratch]".into());
let mut text = format!("undo tree — {name} (enter: restore · q: close)\n");
let mut row_rev = vec![None];
for r in &rows {
let indent = " ".repeat(r.depth.saturating_sub(1));
let cur = if r.is_current { " ← current" } else { "" };
let branch = if r.branches { "⑂ " } else { "" };
text.push_str(&format!("{}* {}#{}{cur}\n", indent, branch, r.index));
row_rev.push(Some(r.index));
}
let mut buf = Buffer::from_text(&text);
buf.readonly = true;
buf.name = Some("undo tree".into());
self.buffers.push(buf);
self.surfaces.push(None);
self.highlighters.push(None);
self.current = self.buffers.len() - 1;
self.touch_mru(self.current);
self.cursor = 0;
self.view_top = 0;
if let Some(line) = rows.iter().position(|r| r.is_current) {
self.cursor = self.buf().line_start(line + 1);
}
self.undo_browser = Some(UndoBrowser {
browser: self.current,
origin,
row_rev,
});
}
fn undo_tree_jump(&mut self) {
let Some(ub) = &self.undo_browser else { return };
let line = self.buf().line_of(self.cursor);
let Some(Some(rev)) = ub.row_rev.get(line).copied() else {
return;
};
let (browser, origin) = (ub.browser, ub.origin);
let ops = self.buffers[origin].history.ops_to(rev);
self.undo_browser = None;
self.current = browser;
self.close_buffer(true); self.current = if origin > browser { origin - 1 } else { origin };
let Some(ops) = ops else { return };
let at = ops.iter().map(|e| e.at).min().unwrap_or(0);
self.buf_mut().apply_history(ops);
self.cursor = self.buf().clamp_boundary(at.min(self.buf().len_bytes()));
self.clamp_cursor();
self.flash(Range::charwise(self.cursor, self.cursor));
self.message = format!("restored revision {rev} — u to walk back");
}
pub(crate) fn feed_undo_browser(&mut self, key: Key) -> bool {
let Some(ub) = &self.undo_browser else {
return false;
};
let alive = self
.buffers
.get(ub.browser)
.is_some_and(|b| b.name.as_deref() == Some("undo tree"));
if !alive {
self.undo_browser = None;
return false;
}
if self.current != ub.browser {
return false;
}
if key == Key::Enter {
self.undo_tree_jump();
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use strop_core::Buffer;
#[test]
fn browser_lists_tree_and_restores_branch() {
let mut e = Editor::new(Buffer::from_text("a\n"));
e.feed_text("ob\x1b"); e.feed_text("u");
e.feed_text("oc\x1b"); e.feed_text(" u");
let br = e.undo_browser.as_ref().expect("browser open");
assert_eq!(e.buf().name.as_deref(), Some("undo tree"));
assert_eq!(br.row_rev.len(), 3); e.feed_text("j"); e.feed(crate::editor::Key::Enter);
assert_eq!(e.buf().name.as_deref(), None, "back on the file");
assert_eq!(e.buf().rope.to_string(), "a\nb\n");
e.feed_text("u");
assert_eq!(e.buf().rope.to_string(), "a\n");
}
#[test]
fn browser_q_closes_back_to_origin() {
let mut e = Editor::new(Buffer::from_text("a\n"));
e.feed_text("ob\x1b");
e.feed_text(" u");
assert!(e.undo_browser.is_some());
e.feed_text("q");
assert_eq!(e.buf().name.as_deref(), None);
assert_eq!(e.buf().rope.to_string(), "a\nb\n");
}
}