strop_engine/editor/filesystem/draft/
editing.rs1use super::*;
2use crate::editor::directory::DirectoryTask;
3impl Editor {
4 pub fn filename_draft(&self, document: DocumentId) -> Option<&Draft> {
5 self.docs
6 .get(document)?
7 .directory_metadata_ref()?
8 .draft
9 .as_ref()
10 }
11 pub(crate) fn sync_filename_draft(&mut self, document: DocumentId) {
12 let Some(doc) = self.docs.get_mut(document) else {
13 return;
14 };
15 let super::super::super::DocumentSource::Directory(source) = &mut doc.source else {
16 return;
17 };
18 let Some(draft) = source.draft.as_mut() else {
19 return;
20 };
21 draft.sync(&doc.buf);
22 }
23 pub(crate) fn begin_filename_draft(&mut self) -> Result<(), String> {
24 let source = self
25 .directory()
26 .ok_or("Edit names requires a Directory; use Space e or :browse first")?;
27 if matches!(
28 source.location.filesystem,
29 strop_workspace::Filesystem::Container(_)
30 ) {
31 return Err("container filename drafts are unavailable: container mutations are read-only by policy".into());
32 }
33 if source.stale.is_some() {
34 return Err("refresh the stale Directory before editing names".into());
35 }
36 if self.filesystem.blocks(&source.location) {
37 return Err("filesystem operations must settle or verify before editing names".into());
38 }
39 self.start_directory_task(self.current(), DirectoryTask::EditNames, None)
40 }
41 pub(crate) fn discard_filename_draft(&mut self) -> Result<(), String> {
42 let document = self.current();
43 let phase = self
44 .filename_draft(document)
45 .ok_or("no filename draft is active")?
46 .phase;
47 if matches!(phase, Phase::Applying(_) | Phase::Unconfirmed(_)) {
48 return Err(
49 "admitted filesystem outcomes must settle or verify before discarding the draft"
50 .into(),
51 );
52 }
53 if self.filesystem.pending.as_ref().is_some_and(|proposal| {
54 proposal.ticket.key.origin == document && proposal.ticket.key.draft.is_some()
55 }) {
56 self.retire_filesystem_review("filename draft discarded");
57 }
58 if self
59 .filesystem
60 .preparing
61 .as_ref()
62 .is_some_and(|ticket| ticket.key.origin == document && ticket.key.draft.is_some())
63 {
64 if let Some(ticket) = self.filesystem.preparing.take() {
65 if let Some(handle) = self.worker_handles.remove(&ticket.request) {
66 handle.cancel(worker::CancelReason::Dismissed);
67 }
68 }
69 self.filesystem.preparing_copies.clear();
70 }
71 if let Some(doc) = self.docs.get_mut(document) {
72 if let Some(draft) = doc
73 .directory_metadata_mut()
74 .and_then(|source| source.draft.as_mut())
75 {
76 draft.phase = Phase::Reloading;
77 }
78 doc.buf.readonly = true;
79 doc.buf.dirty = false;
80 }
81 self.start_directory_task(document, DirectoryTask::Reload, None)
82 }
83 pub(crate) fn filename_copy_policy(&mut self, argument: &str) -> Result<(), String> {
84 let policy = match argument {
85 "stored" => CopyVersion::Stored,
86 "buffer" => CopyVersion::Buffer,
87 _ => return Err("use :fs copies stored or :fs copies buffer".into()),
88 };
89 let document = self.current();
90 let draft = self
91 .docs
92 .get_mut(document)
93 .and_then(|doc| doc.directory_metadata_mut())
94 .and_then(|source| source.draft.as_mut())
95 .ok_or("copy policy requires a filename draft")?;
96 if !draft.editable() {
97 return Err("filename draft is waiting for an admitted operation".into());
98 }
99 draft.intent_epoch = draft
100 .intent_epoch
101 .checked_add(1)
102 .ok_or("filename intent identity exhausted")?;
103 draft.copy_version = Some(policy);
104 self.message = format!("filename draft copies use {policy:?} contents");
105 Ok(())
106 }
107 pub(crate) fn filename_removal_policy(&mut self, argument: &str) -> Result<(), String> {
108 let policy = match argument {
109 "trash" => Removal::Trash,
110 "permanent" => Removal::Permanent,
111 _ => return Err("use :fs deletes trash or :fs deletes permanent".into()),
112 };
113 let document = self.current();
114 let draft = self
115 .docs
116 .get_mut(document)
117 .and_then(|doc| doc.directory_metadata_mut())
118 .and_then(|source| source.draft.as_mut())
119 .ok_or("deletion policy requires a filename draft")?;
120 if !draft.editable() {
121 return Err("filename draft is waiting for an admitted operation".into());
122 }
123 if policy == Removal::Trash && draft.root.filesystem != strop_workspace::Filesystem::Local {
124 return Err(
125 "remote Trash is unavailable; permanent removal must be chosen explicitly".into(),
126 );
127 }
128 draft.intent_epoch = draft
129 .intent_epoch
130 .checked_add(1)
131 .ok_or("filename intent identity exhausted")?;
132 draft.removal = policy;
133 self.message = format!("filename draft deletions use {policy:?}");
134 Ok(())
135 }
136 pub(crate) fn prepare_filename_draft(&mut self, document: DocumentId) -> Result<(), String> {
137 let draft = self
138 .filename_draft(document)
139 .cloned()
140 .ok_or("no filename draft is active")?;
141 if !draft.editable() {
142 return Err("filename draft is waiting for its admitted operation".into());
143 }
144 let mut copies = HashMap::new();
145 let mut seen = std::collections::HashSet::new();
146 for row in &draft.geometry.rows {
147 let Some(Origin::Copy(source)) = &row.origin else {
148 continue;
149 };
150 if !seen.insert(source.location.clone()) {
151 continue;
152 }
153 let target = crate::files::FileTarget::from_location(&source.location)
154 .map_err(|error| error.to_string())?;
155 let open = self
156 .docs
157 .iter()
158 .find_map(|(_, doc)| doc.matches_target(&target).then_some(doc));
159 if draft.copy_version.is_none() && open.is_some_and(|doc| doc.buf.dirty) {
160 return Err("draft copies include unsaved sources; choose :fs copies stored or :fs copies buffer before :w".into());
161 }
162 if draft.copy_version == Some(CopyVersion::Buffer) {
163 let open = open.ok_or(
164 "copy current buffer requires that source to be open; choose stored otherwise",
165 )?;
166 copies.insert(source.location.clone(), open.buf.snapshot());
167 }
168 }
169 let key = FsKey {
170 origin: document,
171 revision: self.doc(document).buf.revision(),
172 focus: self.focus_epoch,
173 open_created: false,
174 intents: Arc::new(Vec::new()),
175 recovery: None,
176 draft: Some(Stamp {
177 id: draft.id,
178 intent: draft.intent_epoch,
179 root: draft.root.clone(),
180 }),
181 };
182 let text = self.doc(document).buf.snapshot();
183 let environment = self.filesystem.environment.clone();
184 self.start_filesystem_preparation(key, copies, move |token| {
185 let work = || -> Result<PreparedFilesystem, String> {
186 let compiled = draft.compile(&text, &token)?;
187 let batch = strop_fs::batch::prepare(&compiled.intents, &environment, &token).map_err(|error| error.to_string())?;
188 for step in &batch.steps {
189 let Some(source) = step.source.as_ref() else { continue };
190 if let Some(expected) = compiled.sources.iter().find(|expected| step.intent.source.as_ref() == Some(&expected.location)) {
191 if !expected.value.as_ref().zip(source.value.as_ref()).is_some_and(|(before, now)| compile::matches_observed(before, now)) {
192 return Err(format!("filename source changed since the captured listing: {}; draft retained", expected.location.label()));
193 }
194 }
195 }
196 let draft_targets = compiled.targets.into_iter().map(|(row, target)| {
197 let resolved = batch.steps.iter().find(|step| step.intent.destination.as_ref() == Some(&target))
198 .and_then(|step| step.destination.as_ref()).map(|destination| destination.location.clone()).unwrap_or(target);
199 (row, resolved)
200 }).collect();
201 Ok(PreparedFilesystem { batch, draft_targets })
202 };
203 match work() {
204 Ok(batch) => Outcome::Success(batch),
205 Err(_) if token.is_cancelled() => Outcome::Cancelled(worker::CancelReason::Superseded),
206 Err(error) => Outcome::failed(FailureKind::InvalidInput, error),
207 }
208 })
209 }
210 pub(crate) fn filename_draft_fresh(
211 &self,
212 document: DocumentId,
213 revision: BufferRevision,
214 stamp: &Stamp,
215 ) -> bool {
216 self.docs.get(document).is_some_and(|doc| {
217 doc.buf.revision() == revision
218 && doc.directory_metadata_ref().is_some_and(|source| {
219 source.location == stamp.root
220 && source.draft.as_ref().is_some_and(|draft| {
221 draft.id == stamp.id
222 && draft.intent_epoch == stamp.intent
223 && draft.editable()
224 })
225 })
226 })
227 }
228 pub(crate) fn freeze_filename_draft(
229 &mut self,
230 document: DocumentId,
231 stamp: &Stamp,
232 operation: WorkerId,
233 ) {
234 if let Some(doc) = self.docs.get_mut(document) {
235 if let Some(draft) = doc
236 .directory_metadata_mut()
237 .and_then(|source| source.draft.as_mut())
238 .filter(|draft| draft.id == stamp.id)
239 {
240 draft.phase = Phase::Applying(operation);
241 doc.buf.readonly = true;
242 }
243 }
244 }
245 pub(crate) fn finish_filename_draft(
246 &mut self,
247 document: DocumentId,
248 stamp: &Stamp,
249 operation: WorkerId,
250 committed: bool,
251 unconfirmed: bool,
252 ) {
253 let published: std::collections::HashSet<_> = self
254 .filesystem
255 .history
256 .iter()
257 .find(|attempt| attempt.ticket.request == operation)
258 .into_iter()
259 .flat_map(|attempt| &attempt.receipts)
260 .filter(|receipt| receipt.outcome.is_committed())
261 .filter_map(|receipt| {
262 receipt
263 .operation
264 .destination
265 .as_ref()
266 .map(|destination| destination.location.clone())
267 })
268 .collect();
269 let Some(doc) = self.docs.get_mut(document) else {
270 return;
271 };
272 let Some(draft) = doc
273 .directory_metadata_mut()
274 .and_then(|source| source.draft.as_mut())
275 .filter(|draft| draft.id == stamp.id)
276 else {
277 return;
278 };
279 draft.phase = if unconfirmed {
280 Phase::Unconfirmed(operation)
281 } else if committed {
282 Phase::Reloading
283 } else {
284 Phase::Editing
285 };
286 if committed && !unconfirmed {
287 let originals: HashMap<_, _> = draft
288 .geometry
289 .rows
290 .iter()
291 .filter_map(|row| {
292 if let Some(Origin::Original(index)) = &row.origin {
293 draft
294 .base
295 .get(*index)
296 .map(|source| (row.id, source.location.clone()))
297 } else {
298 None
299 }
300 })
301 .collect();
302 draft.targets.retain(|row, location| {
303 published.contains(location) || originals.get(row) == Some(location)
304 });
305 }
306 doc.buf.readonly = unconfirmed || committed;
307 if committed && !unconfirmed {
308 doc.buf.dirty = false;
309 if let Err(error) = self.start_directory_task(document, DirectoryTask::Reload, None) {
310 self.message = error;
311 }
312 }
313 }
314}