use termesh_core::{BufferId, ProposalId};
use crate::change::ChangeSet;
use crate::selection::Selection;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Version(pub u64);
impl Version {
pub fn next(self) -> Self {
Version(self.0 + 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct UndoGroupId(pub u64);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditSource {
Keyboard,
Paste,
Formatter,
Lsp,
Agent(ProposalId),
Replace,
}
impl EditSource {
pub fn coalesces(&self) -> bool {
matches!(self, EditSource::Keyboard)
}
}
#[derive(Debug, Clone)]
pub struct EditTransaction {
pub buffer: BufferId,
pub base_version: Version,
pub changes: ChangeSet,
pub source: EditSource,
pub undo_group: UndoGroupId,
pub selection: Option<Selection>,
}
impl EditTransaction {
pub fn new(
buffer: BufferId,
base_version: Version,
changes: ChangeSet,
source: EditSource,
undo_group: UndoGroupId,
) -> Self {
Self { buffer, base_version, changes, source, undo_group, selection: None }
}
pub fn with_selection(mut self, selection: Selection) -> Self {
self.selection = Some(selection);
self
}
pub fn is_empty(&self) -> bool {
self.changes.is_identity()
}
pub fn proposal(&self) -> Option<ProposalId> {
match self.source {
EditSource::Agent(id) => Some(id),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tx(source: EditSource) -> EditTransaction {
EditTransaction::new(
BufferId::new(1),
Version(3),
ChangeSet::replace(10, 0, 0, "x"),
source,
UndoGroupId(7),
)
}
#[test]
fn agent_edits_are_traceable_through_source() {
let t = tx(EditSource::Agent(ProposalId::new(42)));
assert_eq!(t.proposal(), Some(ProposalId::new(42)));
assert_eq!(t.base_version, Version(3));
}
#[test]
fn non_agent_edits_carry_no_proposal() {
assert_eq!(tx(EditSource::Keyboard).proposal(), None);
}
#[test]
fn only_typing_coalesces_into_one_undo_step() {
assert!(EditSource::Keyboard.coalesces());
for source in [
EditSource::Paste,
EditSource::Formatter,
EditSource::Lsp,
EditSource::Replace,
EditSource::Agent(ProposalId::new(1)),
] {
assert!(!source.coalesces(), "{source:?} should be its own undo step");
}
}
#[test]
fn selection_defaults_to_derived_and_can_be_pinned() {
let t = tx(EditSource::Keyboard);
assert!(t.selection.is_none(), "derived by mapping unless told otherwise");
let pinned = tx(EditSource::Keyboard).with_selection(Selection::point(4));
assert_eq!(pinned.selection.unwrap().primary().head, 4);
}
#[test]
fn an_identity_change_is_an_empty_transaction() {
let t = EditTransaction::new(
BufferId::new(1),
Version(0),
ChangeSet::identity(5),
EditSource::Keyboard,
UndoGroupId(0),
);
assert!(t.is_empty());
}
#[test]
fn versions_advance_monotonically() {
assert_eq!(Version(4).next(), Version(5));
assert!(Version(4) < Version(5));
}
}