use strop_core::{Buffer, Range};
use super::{Document, Editor, Key};
pub struct UndoBrowser {
pub browser: strop_core::id::DocumentId,
pub origin: strop_core::id::DocumentId,
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;
}
self.cancel_pending();
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
.as_ref()
.map(|p| p.to_string_lossy().into_owned())
.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));
}
self.push_jump(); let mut buf = Buffer::from_text(&text);
buf.readonly = true;
buf.name = Some("undo tree".into());
let id = self.docs.insert(Document::output(buf));
self.drop_stale_scratch(id);
self.switch_to(id);
self.set_head(0);
self.view_mut().view_top = 0;
self.view_mut().hscroll = strop_core::id::DisplayColumn::new(0);
if let Some(line) = rows.iter().position(|r| r.is_current) {
self.set_head(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.head());
let Some(Some(rev)) = ub.row_rev.get(line).copied() else {
return;
};
let (browser, origin) = (ub.browser, ub.origin);
if self.docs.get(origin).is_none() {
self.message = "undo origin is closed".into();
return;
}
let result = self.doc_mut(origin).buf.restore_revision(rev);
let moved = match result {
Ok(Some(moved)) => moved,
Ok(None) => return,
Err(error) => {
self.message = error.to_string();
return;
}
};
self.undo_browser = None;
self.view_mut().doc = browser;
self.close_buffer(true); self.view_mut().doc = origin;
let at = moved.start.get();
self.set_head(self.buf().clamp_boundary(at.min(self.buf().len_bytes())));
self.clamp_cursor();
self.flash(Range::charwise(self.head(), self.head()));
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
.docs
.get(ub.browser)
.is_some_and(|d| d.buf.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
}
}
impl Editor {
pub(crate) fn undo(&mut self) {
if self.collections.contains_key(&self.current()) {
self.collection_undo();
return;
}
let result = self.buf_mut().undo();
match result {
Ok(Some(moved)) => {
self.set_head(moved.start.get());
self.reland_extras();
self.clamp_cursor();
self.flash(Range::charwise(self.head(), self.head()));
}
Ok(None) => self.message = "already at oldest change".into(),
Err(error) => self.message = error.to_string(),
}
}
fn reland_extras(&mut self) {
let len = self.buf().len_bytes();
let extras: Vec<strop_core::selection::Selection> = self
.extra_selections()
.iter()
.map(|s| strop_core::selection::Selection {
anchor: self.buf().clamp_boundary(s.anchor.min(len)),
head: self.buf().clamp_boundary(s.head.min(len)),
})
.collect();
self.sels_mut().set_extra_selections(extras);
}
pub(crate) fn redo(&mut self) {
if self.collections.contains_key(&self.current()) {
self.collection_redo();
return;
}
let result = self.buf_mut().redo();
match result {
Ok(Some(moved)) => {
self.set_head(moved.end.get());
self.reland_extras();
self.clamp_cursor();
self.flash(Range::charwise(self.head(), self.head()));
}
Ok(None) => self.message = "nothing to redo".into(),
Err(error) => self.message = error.to_string(),
}
}
}
#[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().text().to_string(), "a\nb\n");
e.feed_text("u");
assert_eq!(e.buf().text().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().text().to_string(), "a\nb\n");
}
}