strop_engine/editor/io/
save.rs1use 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 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 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}