Skip to main content

strop_engine/editor/io/
save.rs

1//! Save admission, optional formatting and owned source writes.
2use super::*;
3
4impl Editor {
5    pub(crate) fn local_write_pending(&self) -> bool {
6        !self.io.saves.is_empty()
7            || matches!(self.lsp_state.after_format.as_ref(),
8                Some(crate::editor::lsp::state::AfterFormat::Save { document, .. })
9                    if self.docs.get(*document).is_some_and(|doc| matches!(doc.source, crate::editor::document::DocumentSource::File)))
10    }
11
12    pub fn request_save(&mut self, target: Option<PathBuf>, force: bool, close: bool) {
13        if self.directory().is_some() {
14            self.request_save_document(self.current(), target, force, close);
15            return;
16        }
17        // auto_format (helix parity): a plain `:w` formats through the
18        // language server first; the save chains on the reply. A
19        // formatter failure or refusal never holds the save hostage.
20        if self.config.auto_format && target.is_none() && self.lsp_format_available() {
21            let before = self.lsp_state.navigation;
22            self.lsp_format();
23            if let Some(request) = self
24                .lsp_state
25                .navigation
26                .filter(|request| Some(*request) != before)
27            {
28                self.lsp_state.after_format = Some(crate::editor::lsp::state::AfterFormat::Save {
29                    document: self.current(),
30                    close,
31                    force,
32                    request,
33                });
34                return;
35            }
36        }
37        self.request_save_document(self.current(), target, force, close);
38    }
39
40    pub(crate) fn request_save_document(
41        &mut self,
42        document: DocumentId,
43        target: Option<PathBuf>,
44        force: bool,
45        close: bool,
46    ) -> bool {
47        let blocked = if let Some(target) = target.as_ref() {
48            self.filesystem
49                .blocks(&strop_workspace::ResourceLocation::local(
50                    self.cwd.join(target),
51                ))
52        } else {
53            self.filesystem_blocks_document(document)
54        };
55        if blocked {
56            self.message =
57                "filesystem operation pending or unconfirmed; verify before saving this binding"
58                    .into();
59            return false;
60        }
61        if self
62            .docs
63            .get(document)
64            .is_some_and(|doc| doc.directory_metadata_ref().is_some())
65        {
66            if self.filename_draft(document).is_some() {
67                if target.is_some() || close {
68                    self.message = "use :w without a target to review filename changes; filesystem application is explicit".into();
69                    return false;
70                }
71                return match self.prepare_filename_draft(document) {
72                    Ok(()) => true,
73                    Err(error) => {
74                        self.message = error;
75                        false
76                    }
77                };
78            }
79            self.message = "Directory buffers have no file write binding; use :fs edit".into();
80            return false;
81        }
82        if self.docs.get(document).is_some_and(|doc| {
83            matches!(
84                doc.source,
85                crate::editor::document::DocumentSource::Remote(_)
86            )
87        }) {
88            return self.request_remote_save(document, target, force, close);
89        }
90        if target
91            .as_ref()
92            .and_then(|path| path.to_str())
93            .is_some_and(|path| path.starts_with("ssh://"))
94        {
95            self.message = "remote save-as is unsupported; no local fallback".into();
96            return false;
97        }
98        if self
99            .filesystem
100            .blocks_namespace(&strop_workspace::Filesystem::Local)
101        {
102            self.message = "local filesystem mutation is pending or unconfirmed; settle or verify its receipt before saving".into();
103            return false;
104        }
105        if self.io.saves.contains_key(&document) {
106            self.message = "write already in progress".into();
107            return false;
108        }
109        let Some(buffer) = self.docs.get(document).map(|doc| &doc.buf) else {
110            self.message = "write refused: source buffer closed".into();
111            return false;
112        };
113        let revision = buffer.revision();
114        let target = target.map(|path| self.cwd.join(path));
115        let work = match buffer.prepare_save(target.clone(), force) {
116            Ok(work) => work,
117            Err(error) => {
118                self.message = format!("write failed: {error}");
119                return false;
120            }
121        };
122        let request = match self.worker_ids.allocate() {
123            Ok(request) => request,
124            Err(error) => {
125                self.message = error.message;
126                return false;
127            }
128        };
129        let ticket = Ticket {
130            request,
131            key: SaveKey {
132                document,
133                revision,
134                focus: self.focus_epoch,
135                close,
136                target,
137                force,
138            },
139        };
140        self.io.saves.insert(document, ticket.clone());
141        self.message = "saving".into();
142        match self.tape.request("io.save", &ticket) {
143            Ok(false) => return true,
144            Ok(true) => {}
145            Err(error) => {
146                self.handle_io(IoEvent::Save(Box::new(Completion {
147                    ticket,
148                    outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
149                })));
150                return false;
151            }
152        }
153        let tx = self.io.tx.clone();
154        let handle = worker::spawn(
155            "strop-save",
156            move |outcome| {
157                let _ = tx.send(IoEvent::Save(Box::new(Completion { ticket, outcome })));
158            },
159            move |_| match work.execute() {
160                Ok(receipt) => Outcome::Success(receipt),
161                Err(error) => Outcome::failed(FailureKind::Io, error.to_string()),
162            },
163        );
164        self.worker_handles.insert(request, handle);
165        true
166    }
167
168    /// Preserve the formatting stage alongside the actual persistence outcome.
169    pub(crate) fn finish_save_feedback(&mut self, document: DocumentId) {
170        if let Some(warning) = self.io.format_warnings.remove(&document) {
171            self.message
172                .push_str(&format!(" — format warning: {warning}"));
173        }
174        self.finish_change_save(document);
175    }
176}