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 fn request_save(&mut self, target: Option<PathBuf>, force: bool, close: bool) {
6        // auto_format (helix parity): a plain `:w` formats through the
7        // language server first; the save chains on the reply. A
8        // formatter failure or refusal never holds the save hostage.
9        if self.config.auto_format && target.is_none() && self.lsp_format_available() {
10            let before = self.lsp_state.navigation;
11            self.lsp_format();
12            if let Some(request) = self
13                .lsp_state
14                .navigation
15                .filter(|request| Some(*request) != before)
16            {
17                self.lsp_state.after_format = Some(crate::editor::lsp::state::AfterFormat::Save {
18                    document: self.current(),
19                    close,
20                    force,
21                    request,
22                });
23                return;
24            }
25        }
26        self.request_save_document(self.current(), target, force, close);
27    }
28
29    pub(crate) fn request_save_document(
30        &mut self,
31        document: DocumentId,
32        target: Option<PathBuf>,
33        force: bool,
34        close: bool,
35    ) -> bool {
36        if self.docs.get(document).is_some_and(|doc| {
37            matches!(
38                doc.source,
39                crate::editor::document::DocumentSource::Remote(_)
40                    | crate::editor::document::DocumentSource::RemoteDirectory(_)
41            )
42        }) {
43            return self.request_remote_save(document, target, force, close);
44        }
45        if target
46            .as_ref()
47            .and_then(|path| path.to_str())
48            .is_some_and(|path| path.starts_with("ssh://"))
49        {
50            self.message = "remote save-as is unsupported; no local fallback".into();
51            return false;
52        }
53        if self.io.saves.contains_key(&document) {
54            self.message = "write already in progress".into();
55            return false;
56        }
57        let Some(buffer) = self.docs.get(document).map(|doc| &doc.buf) else {
58            self.message = "write refused: source buffer closed".into();
59            return false;
60        };
61        let revision = buffer.revision();
62        let target = target.map(|path| self.cwd.join(path));
63        let work = match buffer.prepare_save(target.clone(), force) {
64            Ok(work) => work,
65            Err(error) => {
66                self.message = format!("write failed: {error}");
67                return false;
68            }
69        };
70        let request = match self.worker_ids.allocate() {
71            Ok(request) => request,
72            Err(error) => {
73                self.message = error.message;
74                return false;
75            }
76        };
77        let ticket = Ticket {
78            request,
79            key: SaveKey {
80                document,
81                revision,
82                focus: self.focus_epoch,
83                close,
84                target,
85                force,
86            },
87        };
88        self.io.saves.insert(document, ticket.clone());
89        self.message = "saving".into();
90        match self.tape.request("io.save", &ticket) {
91            Ok(false) => return true,
92            Ok(true) => {}
93            Err(error) => {
94                self.handle_io(IoEvent::Save(Box::new(Completion {
95                    ticket,
96                    outcome: Outcome::failed(FailureKind::Protocol, error.to_string()),
97                })));
98                return false;
99            }
100        }
101        let tx = self.io.tx.clone();
102        let handle = worker::spawn(
103            "strop-save",
104            move |outcome| {
105                let _ = tx.send(IoEvent::Save(Box::new(Completion { ticket, outcome })));
106            },
107            move |_| match work.execute() {
108                Ok(receipt) => Outcome::Success(receipt),
109                Err(error) => Outcome::failed(FailureKind::Io, error.to_string()),
110            },
111        );
112        self.worker_handles.insert(request, handle);
113        true
114    }
115
116    /// Preserve the formatting stage alongside the actual persistence outcome.
117    pub(crate) fn finish_save_feedback(&mut self, document: DocumentId) {
118        if let Some(warning) = self.io.format_warnings.remove(&document) {
119            self.message
120                .push_str(&format!(" — format warning: {warning}"));
121        }
122        self.finish_change_save(document);
123    }
124}