strop_engine/editor/
transact.rs1mod capability;
3pub use capability::{BufferEdit, DocumentEdit};
4use strop_core::id::{BufferRevision, DocumentId};
5use strop_core::{Change, EditError, Replacement};
6
7#[derive(Debug, Clone)]
9pub struct ChangeSet {
10 pub edits: Vec<Replacement>,
11 pub undo_open: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15pub enum ApplyError {
16 #[error("no such document")]
17 NoDocument,
18 #[error(transparent)]
19 Edit(#[from] EditError),
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Committed {
24 pub revision: BufferRevision,
25}
26
27impl super::Editor {
28 pub(crate) fn apply(
29 &mut self,
30 document: DocumentId,
31 base: BufferRevision,
32 changes: ChangeSet,
33 ) -> Result<Committed, ApplyError> {
34 let buffer = &self.docs.get(document).ok_or(ApplyError::NoDocument)?.buf;
35 let prepared = buffer.prepare_replacements(base, changes.edits)?;
36 if prepared.is_empty() {
37 return Ok(Committed { revision: base });
38 }
39 if self
40 .pending
41 .prompt()
42 .is_some_and(|prompt| prompt.origin().pane.doc == document)
43 {
44 self.cancel_pending();
45 }
46 let revision = self
47 .doc_mut(document)
48 .buf
49 .apply_prepared(prepared, changes.undo_open)?;
50 Ok(Committed { revision })
51 }
52
53 pub(crate) fn replace_system(
54 &mut self,
55 document: DocumentId,
56 text: &str,
57 ) -> Result<(), EditError> {
58 self.doc_mut(document).buf.system_edit().replace_all(text)
59 }
60
61 pub(crate) fn tx_begin(&mut self) {
62 self.buf_mut().begin_undo_group();
63 }
64 pub(crate) fn tx_commit(&mut self) {
65 self.buf_mut().commit_undo_group();
66 strop_trace::record_with(strop_trace::EventKind::History, || {
67 serde_json::json!({
68 "operation":"commit","buffer":self.buf().trace_id(),"document":self.current(),
69 "revision":self.buf().revision(),"history_nodes":self.buf().history().depth(),
70 })
71 });
72 }
73
74 pub(super) fn sync_document(&mut self, id: DocumentId, map_active: bool) {
78 self.sync_document_positions(id, map_active, map_position);
79 }
80
81 fn sync_document_positions(
82 &mut self,
83 id: DocumentId,
84 map_active: bool,
85 position: impl Fn(usize, &Change) -> usize,
86 ) {
87 let clean_views: std::collections::HashSet<DocumentId> = self
90 .collections
91 .iter()
92 .filter(|(cid, collection)| {
93 self.docs
94 .get(**cid)
95 .is_some_and(|doc| doc.buf.revision() == collection.revision)
96 })
97 .map(|(cid, _)| *cid)
98 .collect();
99 let Some(document) = self.docs.get_mut(id) else {
100 return;
101 };
102 if document.buf.changes().is_empty() {
103 return;
104 }
105 let active = self.active_pane;
106 let mut splices: Vec<(DocumentId, usize)> = Vec::new();
113 let mut renders: Vec<DocumentId> = Vec::new();
114 for change in document.buf.changes() {
115 let (start, end) = (change.edit.start_byte, change.edit.old_end_byte);
116 for (collection_id, collection) in self.collections.iter() {
117 if renders.contains(collection_id) {
118 continue;
119 }
120 if !clean_views.contains(collection_id) {
121 continue;
122 }
123 for (index, excerpt) in collection.excerpts.iter().enumerate() {
124 if excerpt.source != id {
125 continue;
126 }
127 if end < excerpt.start || start > excerpt.end {
131 continue;
132 }
133 if start > excerpt.start && end < excerpt.end {
134 if !splices.contains(&(*collection_id, index)) {
135 splices.push((*collection_id, index));
136 }
137 } else {
138 renders.push(*collection_id);
139 break;
140 }
141 }
142 }
143 let map = |offset| position(offset, change);
144 for (owner, position) in self.marks.values_mut() {
145 if *owner == id {
146 *position = map(*position);
147 }
148 }
149 for (owner, position) in self
150 .jumplist_past
151 .iter_mut()
152 .chain(self.jumplist_future.iter_mut())
153 {
154 if *owner == id {
155 *position = map(*position);
156 }
157 }
158 for (index, pane) in self.panes.iter_mut().enumerate() {
159 if pane.doc == id && (map_active || index != active) {
160 pane.sels.map_positions(map);
161 }
162 }
163 for collection in self.collections.values_mut() {
168 for excerpt in &mut collection.excerpts {
169 if excerpt.source == id {
170 let (s, e) = (change.edit.start_byte, change.edit.old_end_byte);
171 let new_len = change.edit.new_end_byte - change.edit.start_byte;
172 let delta = new_len as isize - (e - s) as isize;
173 let shift = |p: usize| (p as isize + delta) as usize;
174 let insertion = s == e;
175 excerpt.start = if excerpt.start <= s {
176 excerpt.start
177 } else if excerpt.start >= e {
178 shift(excerpt.start)
179 } else {
180 s
181 };
182 excerpt.end = if excerpt.end < s {
183 excerpt.end
184 } else if excerpt.end > e || (excerpt.end == e && !insertion) {
185 shift(excerpt.end)
186 } else {
187 s + new_len
190 };
191 }
192 }
193 }
194 }
195 self.analysis.edits(id, document.buf.changes());
196 document.buf.clear_changes();
197 #[cfg(test)]
199 if !renders.is_empty() || !splices.is_empty() {
200 eprintln!("hook on doc {id:?}: renders={renders:?} splices={splices:?}");
201 }
202 for collection_id in renders {
203 self.collection_render_view(collection_id);
204 }
205 for (collection_id, index) in splices {
206 if self
208 .collections
209 .get(&collection_id)
210 .is_some_and(|collection| {
211 self.docs
212 .get(collection_id)
213 .is_some_and(|doc| doc.buf.revision() == collection.revision)
214 })
215 {
216 self.collection_splice_excerpt(collection_id, index);
217 }
218 }
219 }
220}
221
222fn map_position(position: usize, change: &Change) -> usize {
223 let edit = change.edit;
224 debug_assert!(edit.start_byte <= edit.old_end_byte);
225 debug_assert!(edit.new_end_byte >= edit.start_byte);
229 strop_core::editmap::map_position(
230 position,
231 edit.start_byte,
232 edit.old_end_byte,
233 edit.new_end_byte,
234 )
235}