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 if self.collections.contains_key(&id)
91 && self.docs.get(id).is_some_and(|doc| {
92 doc.buf
93 .changes()
94 .iter()
95 .any(|c| c.origin == strop_core::ChangeOrigin::User)
96 })
97 {
98 self.sync_collection_write_back(id, map_active, &position);
99 return;
100 }
101 self.sync_filename_draft(id);
102 let change_count = self.docs.get(id).map_or(0, |doc| doc.buf.changes().len());
103 if change_count == 0 {
104 return;
105 }
106 for index in 0..change_count {
109 let change = self.doc(id).buf.changes()[index];
110 self.map_navigation_records(id, |offset| position(offset, &change));
111 }
112 let clean_views: std::collections::HashSet<DocumentId> = self
115 .collections
116 .iter()
117 .filter(|(cid, collection)| {
118 self.docs
119 .get(**cid)
120 .is_some_and(|doc| doc.buf.revision() == collection.revision)
121 })
122 .map(|(cid, _)| *cid)
123 .collect();
124 let collection_updates = self.prepare_collection_updates(id, &clean_views);
125 let Some(document) = self.docs.get_mut(id) else {
126 return;
127 };
128 if document.buf.changes().is_empty() {
129 return;
130 }
131 let active = self.active_pane;
132 for change in document.buf.changes() {
133 let map = |offset| position(offset, change);
134 for (owner, position) in self.marks.values_mut() {
135 if *owner == id {
136 *position = map(*position);
137 }
138 }
139 for (index, pane) in self.panes.iter_mut().enumerate() {
140 if pane.doc == id && (map_active || index != active) {
141 pane.sels.map_positions(map);
142 }
143 }
144 for collection in self.collections.values_mut() {
149 for excerpt in &mut collection.excerpts {
150 if excerpt.source == id {
151 let (s, e) = (change.edit.start_byte, change.edit.old_end_byte);
152 let new_len = change.edit.new_end_byte - change.edit.start_byte;
153 let delta = new_len as isize - (e - s) as isize;
154 let shift = |p: usize| (p as isize + delta) as usize;
155 let insertion = s == e;
156 excerpt.start = if excerpt.start <= s {
157 excerpt.start
158 } else if excerpt.start >= e {
159 shift(excerpt.start)
160 } else {
161 s
162 };
163 excerpt.end = if excerpt.end < s {
164 excerpt.end
165 } else if excerpt.end > e || (excerpt.end == e && !insertion) {
166 shift(excerpt.end)
167 } else {
168 s + new_len
171 };
172 for anchor in &mut excerpt.hit_anchors {
173 *anchor = map(*anchor);
174 }
175 excerpt.matches.retain_mut(|(start, length)| {
176 let finish = start.saturating_add(*length);
177 if s < finish && e > *start {
178 return false;
179 }
180 let mapped = map(*start);
181 *length = map(finish).saturating_sub(mapped);
182 *start = mapped;
183 true
184 });
185 }
186 }
187 }
188 }
189 self.analysis.edits(id, document.buf.changes());
190 document.buf.clear_changes();
191 self.publish_collection_updates(collection_updates);
192 }
193}
194
195fn map_position(position: usize, change: &Change) -> usize {
196 let edit = change.edit;
197 debug_assert!(edit.start_byte <= edit.old_end_byte);
198 debug_assert!(edit.new_end_byte >= edit.start_byte);
202 strop_core::editmap::map_position(
203 position,
204 edit.start_byte,
205 edit.old_end_byte,
206 edit.new_end_byte,
207 )
208}