termesh_editor/
transaction.rs1use termesh_core::{BufferId, ProposalId};
9
10use crate::change::ChangeSet;
11use crate::selection::Selection;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
15pub struct Version(pub u64);
16
17impl Version {
18 pub fn next(self) -> Self {
19 Version(self.0 + 1)
20 }
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
29pub struct UndoGroupId(pub u64);
30
31#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum EditSource {
35 Keyboard,
36 Paste,
37 Formatter,
38 Lsp,
39 Agent(ProposalId),
40 Replace,
41}
42
43impl EditSource {
44 pub fn coalesces(&self) -> bool {
49 matches!(self, EditSource::Keyboard)
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct EditTransaction {
56 pub buffer: BufferId,
57 pub base_version: Version,
58 pub changes: ChangeSet,
59 pub source: EditSource,
60 pub undo_group: UndoGroupId,
61 pub selection: Option<Selection>,
71}
72
73impl EditTransaction {
74 pub fn new(
77 buffer: BufferId,
78 base_version: Version,
79 changes: ChangeSet,
80 source: EditSource,
81 undo_group: UndoGroupId,
82 ) -> Self {
83 Self { buffer, base_version, changes, source, undo_group, selection: None }
84 }
85
86 pub fn with_selection(mut self, selection: Selection) -> Self {
88 self.selection = Some(selection);
89 self
90 }
91
92 pub fn is_empty(&self) -> bool {
94 self.changes.is_identity()
95 }
96
97 pub fn proposal(&self) -> Option<ProposalId> {
99 match self.source {
100 EditSource::Agent(id) => Some(id),
101 _ => None,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 fn tx(source: EditSource) -> EditTransaction {
111 EditTransaction::new(
112 BufferId::new(1),
113 Version(3),
114 ChangeSet::replace(10, 0, 0, "x"),
115 source,
116 UndoGroupId(7),
117 )
118 }
119
120 #[test]
121 fn agent_edits_are_traceable_through_source() {
122 let t = tx(EditSource::Agent(ProposalId::new(42)));
123 assert_eq!(t.proposal(), Some(ProposalId::new(42)));
124 assert_eq!(t.base_version, Version(3));
125 }
126
127 #[test]
128 fn non_agent_edits_carry_no_proposal() {
129 assert_eq!(tx(EditSource::Keyboard).proposal(), None);
130 }
131
132 #[test]
133 fn only_typing_coalesces_into_one_undo_step() {
134 assert!(EditSource::Keyboard.coalesces());
135 for source in [
136 EditSource::Paste,
137 EditSource::Formatter,
138 EditSource::Lsp,
139 EditSource::Replace,
140 EditSource::Agent(ProposalId::new(1)),
141 ] {
142 assert!(!source.coalesces(), "{source:?} should be its own undo step");
143 }
144 }
145
146 #[test]
147 fn selection_defaults_to_derived_and_can_be_pinned() {
148 let t = tx(EditSource::Keyboard);
149 assert!(t.selection.is_none(), "derived by mapping unless told otherwise");
150
151 let pinned = tx(EditSource::Keyboard).with_selection(Selection::point(4));
152 assert_eq!(pinned.selection.unwrap().primary().head, 4);
153 }
154
155 #[test]
156 fn an_identity_change_is_an_empty_transaction() {
157 let t = EditTransaction::new(
158 BufferId::new(1),
159 Version(0),
160 ChangeSet::identity(5),
161 EditSource::Keyboard,
162 UndoGroupId(0),
163 );
164 assert!(t.is_empty());
165 }
166
167 #[test]
168 fn versions_advance_monotonically() {
169 assert_eq!(Version(4).next(), Version(5));
170 assert!(Version(4) < Version(5));
171 }
172}