1use super::RemoteEvent;
4use crate::editor::document::DocumentSource;
5use crate::editor::io::IoEvent;
6use crate::editor::Editor;
7use ropey::Rope;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use strop_core::id::{BufferRevision, DocumentId};
11use strop_core::worker::{self, CancelReason, Completion, Outcome, Ticket, WorkerId};
12use strop_remote::save::{
13 self as transport, RemoteSaveError, RemoteSaveReceipt, RemoteVersion, Verification,
14};
15use strop_workspace::RemoteFile;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub(crate) struct WritePermit {
19 id: WorkerId,
20 version: RemoteVersion,
21}
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23pub(crate) enum WriteAction {
24 Enable,
25 Save { close: bool },
26 Verify,
27}
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct RemoteWriteKey {
30 document: DocumentId,
31 revision: BufferRevision,
32 file: RemoteFile,
33 permit: Option<WorkerId>,
34 focus: u64,
35 action: WriteAction,
36}
37#[derive(Serialize, Deserialize)]
38pub enum RemoteWriteResult {
39 Enabled(RemoteVersion),
40 Saved(RemoteSaveReceipt),
41 Verified(Verification),
42 Refused(RemoteSaveError),
43}
44#[derive(Default)]
45pub(crate) struct WriteState {
46 pending: HashMap<DocumentId, Ticket<RemoteWriteKey>>,
47 attempts: HashMap<DocumentId, Attempt>,
48}
49struct Attempt {
50 permit: WorkerId,
51 revision: BufferRevision,
52 before: RemoteVersion,
53 contents: Rope,
54 unconfirmed: bool,
55}
56enum Work {
57 Enable {
58 file: RemoteFile,
59 contents: Rope,
60 },
61 Save {
62 before: RemoteVersion,
63 contents: Rope,
64 close: bool,
65 },
66 Verify {
67 before: RemoteVersion,
68 contents: Rope,
69 },
70}
71impl Work {
72 fn action(&self) -> WriteAction {
73 match self {
74 Self::Enable { .. } => WriteAction::Enable,
75 Self::Save { close, .. } => WriteAction::Save { close: *close },
76 Self::Verify { .. } => WriteAction::Verify,
77 }
78 }
79 fn file(&self) -> &RemoteFile {
80 match self {
81 Self::Enable { file, .. } => file,
82 Self::Save { before, .. } | Self::Verify { before, .. } => before.file(),
83 }
84 }
85 fn before(&self) -> Option<&RemoteVersion> {
86 match self {
87 Self::Enable { .. } => None,
88 Self::Save { before, .. } | Self::Verify { before, .. } => Some(before),
89 }
90 }
91 fn contents(&self) -> &Rope {
92 match self {
93 Self::Enable { contents, .. }
94 | Self::Save { contents, .. }
95 | Self::Verify { contents, .. } => contents,
96 }
97 }
98 fn execute(self, token: &worker::CancelToken) -> RemoteWriteResult {
99 let result = match self {
100 Self::Enable { file, contents } => {
101 transport::prepare_edit(&file, &contents, token).map(RemoteWriteResult::Enabled)
102 }
103 Self::Save {
104 before, contents, ..
105 } => transport::save(&before, &contents, token).map(RemoteWriteResult::Saved),
106 Self::Verify { before, contents } => {
107 transport::verify(&before, &contents, token).map(RemoteWriteResult::Verified)
108 }
109 };
110 result.unwrap_or_else(RemoteWriteResult::Refused)
111 }
112}
113impl WriteState {
114 pub(super) fn pending(&self) -> bool {
115 !self.pending.is_empty()
116 }
117}
118
119impl Editor {
120 pub(super) fn enable_remote_edit(&mut self) -> Result<(), String> {
121 let document = self.current();
122 if self.remote_refresh_pending(document) {
123 return Err("remote refresh is pending".into());
124 }
125 let source = self
126 .cur()
127 .remote_metadata()
128 .ok_or("open a remote regular file first")?;
129 if source.write.is_some() {
130 return Err("remote editing is already enabled".into());
131 }
132 if !self.remote_window_complete() {
133 return Err("remote editing requires a complete, non-following snapshot".into());
134 }
135 if self.remote.writes.pending.contains_key(&document) {
136 return Err("remote write operation already pending".into());
137 }
138 let work = Work::Enable {
139 file: source.file.clone(),
140 contents: self.buf().snapshot(),
141 };
142 self.start_remote_write(document, self.buf().revision(), None, work)
143 }
144
145 pub(crate) fn request_remote_save(
146 &mut self,
147 document: DocumentId,
148 target: Option<std::path::PathBuf>,
149 force: bool,
150 close: bool,
151 ) {
152 let result = self.prepare_remote_save(document, target, force, close);
153 if let Err(error) = result {
154 self.message = error;
155 }
156 }
157 fn prepare_remote_save(
158 &mut self,
159 document: DocumentId,
160 target: Option<std::path::PathBuf>,
161 force: bool,
162 close: bool,
163 ) -> Result<(), String> {
164 if target.is_some() {
165 return Err("remote save-as is unsupported; no local fallback".into());
166 }
167 if self.remote_refresh_pending(document) {
168 return Err("remote refresh is pending".into());
169 }
170 if self.remote.writes.pending.contains_key(&document) {
171 return Err("remote write operation already pending".into());
172 }
173 if self
174 .remote
175 .writes
176 .attempts
177 .get(&document)
178 .is_some_and(|attempt| attempt.unconfirmed)
179 {
180 return Err(
181 "remote save outcome is unconfirmed; use :remote verify before another save".into(),
182 );
183 }
184 let doc = self.docs.get(document).ok_or("no such remote document")?;
185 let source = doc
186 .remote_metadata()
187 .ok_or("remote directories cannot be saved")?;
188 let permit = source
189 .write
190 .as_ref()
191 .ok_or("remote file is read-only; use :remote edit first")?;
192 if doc.buf.readonly && !force {
193 return Err("readonly buffer; remote write authority remains available via :w!".into());
194 }
195 if !source.window.is_complete() || self.remote_following(document) {
196 return Err("partial/following remote windows cannot be saved".into());
197 }
198 if permit.version.file() != &source.file {
199 return Err("remote write permit belongs to another file".into());
200 }
201 let id = permit.id;
202 let before = permit.version.clone();
203 let contents = doc.buf.snapshot();
204 let revision = doc.buf.revision();
205 self.remote.writes.attempts.insert(
206 document,
207 Attempt {
208 permit: id,
209 revision,
210 before: before.clone(),
211 contents: contents.clone(),
212 unconfirmed: false,
213 },
214 );
215 let result = self.start_remote_write(
216 document,
217 revision,
218 Some(id),
219 Work::Save {
220 before,
221 contents,
222 close,
223 },
224 );
225 if result.is_err() {
226 self.remote.writes.attempts.remove(&document);
227 }
228 result
229 }
230
231 pub(super) fn verify_remote_save(&mut self) -> Result<(), String> {
232 let document = self.current();
233 if self.remote.writes.pending.contains_key(&document) {
234 return Err("remote write operation already pending".into());
235 }
236 let attempt = self
237 .remote
238 .writes
239 .attempts
240 .get(&document)
241 .filter(|attempt| attempt.unconfirmed)
242 .ok_or("no unconfirmed remote save to verify")?;
243 let permit = self
244 .cur()
245 .remote_metadata()
246 .and_then(|source| source.write.as_ref())
247 .ok_or("remote write permit was revoked")?;
248 if permit.id != attempt.permit {
249 return Err("remote save attempt belongs to an old permit".into());
250 }
251 let work = Work::Verify {
252 before: attempt.before.clone(),
253 contents: attempt.contents.clone(),
254 };
255 self.start_remote_write(document, attempt.revision, Some(attempt.permit), work)
256 }
257
258 fn start_remote_write(
259 &mut self,
260 document: DocumentId,
261 revision: BufferRevision,
262 permit: Option<WorkerId>,
263 work: Work,
264 ) -> Result<(), String> {
265 let request = self.worker_ids.allocate().map_err(|error| error.message)?;
266 let ticket = Ticket {
267 request,
268 key: RemoteWriteKey {
269 document,
270 revision,
271 file: work.file().clone(),
272 permit,
273 focus: self.focus_epoch,
274 action: work.action(),
275 },
276 };
277 self.remote.writes.pending.insert(document, ticket.clone());
278 self.message = match ticket.key.action {
279 WriteAction::Enable => "checking remote edit authority",
280 WriteAction::Save { .. } => "saving remotely",
281 WriteAction::Verify => "verifying remote save",
282 }
283 .into();
284 match self.tape.request(
285 "remote.write",
286 &(&ticket, work.before(), work.contents().len_bytes()),
287 ) {
288 Ok(false) => return Ok(()),
289 Ok(true) => {}
290 Err(error) => {
291 self.remote.writes.pending.remove(&document);
292 return Err(format!("remote write replay diverged: {error}"));
293 }
294 }
295 let sender = self.io.tx.clone();
296 let handle = worker::spawn(
297 "remote-write",
298 move |outcome| {
299 let _ = sender.send(IoEvent::Remote(RemoteEvent::Write(Box::new(Completion {
300 ticket,
301 outcome,
302 }))));
303 },
304 move |token| Outcome::Success(work.execute(&token)),
305 );
306 self.worker_handles.insert(request, handle);
307 Ok(())
308 }
309
310 pub(super) fn remote_write_done(
311 &mut self,
312 completion: Completion<RemoteWriteKey, RemoteWriteResult>,
313 ) {
314 let document = completion.ticket.key.document;
315 if self.remote.writes.pending.get(&document) != Some(&completion.ticket) {
316 return;
317 }
318 self.remote.writes.pending.remove(&document);
319 self.worker_handles.remove(&completion.ticket.request);
320 let request = completion.ticket.request;
321 let key = completion.ticket.key;
322 let valid = self
323 .docs
324 .get(document)
325 .and_then(|doc| doc.remote_metadata())
326 .is_some_and(|source| {
327 source.file == key.file
328 && match key.action {
329 WriteAction::Enable => {
330 source.write.is_none()
331 && source.window.is_complete()
332 && !self.remote_following(document)
333 && self.doc(document).buf.revision() == key.revision
334 }
335 _ => source
336 .write
337 .as_ref()
338 .is_some_and(|permit| Some(permit.id) == key.permit),
339 }
340 });
341 if !valid {
342 if key.action == WriteAction::Enable
343 && !self.docs.is_empty()
344 && self.current() == document
345 {
346 self.message =
347 "remote edit admission cancelled: snapshot or follow state changed".into();
348 }
349 return;
350 }
351 match completion.outcome {
352 Outcome::Success(RemoteWriteResult::Enabled(version))
353 if key.action == WriteAction::Enable && version.file() == &key.file =>
354 {
355 let mut doc = self.doc_mut(document);
356 if let DocumentSource::Remote(source) = &mut doc.source {
357 source.write = Some(WritePermit {
358 id: request,
359 version,
360 });
361 source.selection = strop_remote::ReadSelection::Full;
362 doc.buf.readonly = false;
363 }
364 drop(doc);
365 self.message =
366 "remote editing enabled (cooperative locks; other programs can still race)"
367 .into();
368 }
369 Outcome::Success(RemoteWriteResult::Saved(receipt))
370 if matches!(key.action, WriteAction::Save { .. }) =>
371 {
372 self.accept_remote_receipt(key, receipt)
373 }
374 Outcome::Success(RemoteWriteResult::Verified(Verification::Written(receipt)))
375 if key.action == WriteAction::Verify =>
376 {
377 self.accept_remote_receipt(key, receipt)
378 }
379 Outcome::Success(RemoteWriteResult::Verified(Verification::Unchanged(version)))
380 if key.action == WriteAction::Verify =>
381 {
382 if let Some(attempt) = self.remote.writes.attempts.get(&document) {
383 if attempt.before != version {
384 self.remote_write_uncertain(
385 document,
386 "verification baseline differs".into(),
387 );
388 return;
389 }
390 }
391 self.remote.writes.attempts.remove(&document);
392 self.message = "remote original is unchanged; local edits remain unsaved".into();
393 }
394 Outcome::Success(RemoteWriteResult::Refused(error)) => {
395 if error.is_unconfirmed() {
396 self.remote_write_uncertain(document, error.to_string());
397 } else {
398 if key.action != WriteAction::Verify
399 || matches!(
400 &error,
401 RemoteSaveError::Refused {
402 kind: transport::RefusalKind::Conflict,
403 ..
404 }
405 )
406 {
407 self.remote.writes.attempts.remove(&document);
408 }
409 self.message = error.to_string();
410 }
411 }
412 Outcome::Cancelled(_) if key.action == WriteAction::Enable => {
413 self.message = "remote edit admission cancelled".into()
414 }
415 Outcome::Failed { failure, .. } if key.action == WriteAction::Enable => {
416 self.message = format!("remote edit admission failed: {}", failure.message)
417 }
418 Outcome::Cancelled(_) => self.remote_write_uncertain(
419 document,
420 "cancelled; the remote outcome is unconfirmed — :remote verify".into(),
421 ),
422 Outcome::Failed { failure, .. } => self.remote_write_uncertain(
423 document,
424 format!(
425 "{}; remote outcome unconfirmed — :remote verify",
426 failure.message
427 ),
428 ),
429 Outcome::Success(_) => self.remote_write_uncertain(
430 document,
431 "remote write result does not match its request".into(),
432 ),
433 }
434 }
435
436 fn remote_write_uncertain(&mut self, document: DocumentId, message: String) {
437 if let Some(attempt) = self.remote.writes.attempts.get_mut(&document) {
438 attempt.unconfirmed = true;
439 }
440 self.message = message;
441 }
442 fn accept_remote_receipt(&mut self, key: RemoteWriteKey, receipt: RemoteSaveReceipt) {
443 let version = receipt.into_version();
444 if version.file() != &key.file {
445 self.remote_write_uncertain(
446 key.document,
447 "receipt belongs to a different remote file".into(),
448 );
449 return;
450 }
451 let current = {
452 let mut doc = self.doc_mut(key.document);
453 let DocumentSource::Remote(source) = &mut doc.source else {
454 return;
455 };
456 let Some(permit) = source.write.as_mut() else {
457 return;
458 };
459 let size = version.size();
460 if Some(permit.id) != key.permit {
461 return;
462 }
463 permit.version = version;
464 source.window =
465 strop_remote::RemoteWindow::resolve(&strop_remote::ReadSelection::Full, size);
466 source.selection = strop_remote::ReadSelection::Full;
467 debug_assert!(doc.buf.path.is_none());
469 doc.buf.acknowledge_saved_revision(key.revision)
470 };
471 self.remote.writes.attempts.remove(&key.document);
472 self.message = if current {
473 "written remotely"
474 } else {
475 "remote snapshot written; newer edits remain unsaved"
476 }
477 .into();
478 if current
479 && matches!(key.action, WriteAction::Save { close: true })
480 && !self.docs.is_empty()
481 && self.current() == key.document
482 && self.focus_epoch == key.focus
483 {
484 self.close_pane_or_buffer(false);
485 }
486 }
487
488 pub(crate) fn revoke_remote_write(&mut self, document: DocumentId) {
489 if let Some(doc) = self.docs.get_mut(document) {
490 if let DocumentSource::Remote(source) = &mut doc.source {
491 source.write = None;
492 doc.buf.readonly = true;
493 }
494 }
495 self.remote.writes.attempts.remove(&document);
496 if let Some(ticket) = self.remote.writes.pending.remove(&document) {
497 if let Some(handle) = self.worker_handles.remove(&ticket.request) {
498 handle.cancel(CancelReason::OwnerClosed);
499 }
500 }
501 }
502 pub(crate) fn remote_write_blocks_refresh(&self, document: DocumentId) -> bool {
503 self.remote.writes.pending.contains_key(&document)
504 || self
505 .remote
506 .writes
507 .attempts
508 .get(&document)
509 .is_some_and(|attempt| attempt.unconfirmed)
510 }
511 pub(crate) fn cancel_remote_write(&mut self, document: DocumentId) {
512 if let Some(ticket) = self.remote.writes.pending.get(&document).cloned() {
513 if ticket.key.action == WriteAction::Enable {
514 self.remote.writes.pending.remove(&document);
515 self.message = "remote edit admission cancelled".into();
516 }
517 if let Some(handle) = self.worker_handles.remove(&ticket.request) {
518 handle.cancel(CancelReason::Dismissed);
519 }
520 }
521 }
522 pub(crate) fn remote_write_pending(&self, request: WorkerId) -> bool {
523 self.remote.writes.pending.values().any(|ticket| {
524 ticket.request == request
525 && matches!(
526 ticket.key.action,
527 WriteAction::Save { .. } | WriteAction::Verify
528 )
529 })
530 }
531 pub fn remote_write_status(&self) -> Option<&'static str> {
532 if self.docs.is_empty() {
533 return None;
534 }
535 let pending = self.remote.writes.pending.get(&self.current())?;
536 Some(match pending.key.action {
537 WriteAction::Enable => "checking edit",
538 WriteAction::Save { .. } => "saving",
539 WriteAction::Verify => "verifying save",
540 })
541 }
542 pub(crate) fn remote_edit_authorized(&self) -> bool {
543 self.cur()
544 .remote_metadata()
545 .is_some_and(|source| source.write.is_some())
546 }
547}
548
549#[cfg(test)]
550mod tests;