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