Skip to main content

strop_engine/editor/filesystem/draft/
editing.rs

1use 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}