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 ) -> bool {
152 match self.prepare_remote_save(document, target, force, close) {
153 Ok(()) => true,
154 Err(error) => {
155 self.message = error;
156 false
157 }
158 }
159 }
160 fn prepare_remote_save(
161 &mut self,
162 document: DocumentId,
163 target: Option<std::path::PathBuf>,
164 force: bool,
165 close: bool,
166 ) -> Result<(), String> {
167 if target.is_some() {
168 return Err("remote save-as is unsupported; no local fallback".into());
169 }
170 if self.remote_refresh_pending(document) {
171 return Err("remote refresh is pending".into());
172 }
173 if self.remote.writes.pending.contains_key(&document) {
174 return Err("remote write operation already pending".into());
175 }
176 if self
177 .remote
178 .writes
179 .attempts
180 .get(&document)
181 .is_some_and(|attempt| attempt.unconfirmed)
182 {
183 return Err(
184 "remote save outcome is unconfirmed; use :remote verify before another save".into(),
185 );
186 }
187 let doc = self.docs.get(document).ok_or("no such remote document")?;
188 let source = doc
189 .remote_metadata()
190 .ok_or("remote directories cannot be saved")?;
191 let permit = source
192 .write
193 .as_ref()
194 .ok_or("remote file is read-only; use :remote edit first")?;
195 if doc.buf.readonly && !force {
196 return Err("readonly buffer; remote write authority remains available via :w!".into());
197 }
198 if !source.window.is_complete() || self.remote_following(document) {
199 return Err("partial/following remote windows cannot be saved".into());
200 }
201 if permit.version.file() != &source.file {
202 return Err("remote write permit belongs to another file".into());
203 }
204 let id = permit.id;
205 let before = permit.version.clone();
206 let contents = doc.buf.snapshot();
207 let revision = doc.buf.revision();
208 self.remote.writes.attempts.insert(
209 document,
210 Attempt {
211 permit: id,
212 revision,
213 before: before.clone(),
214 contents: contents.clone(),
215 unconfirmed: false,
216 },
217 );
218 let result = self.start_remote_write(
219 document,
220 revision,
221 Some(id),
222 Work::Save {
223 before,
224 contents,
225 close,
226 },
227 );
228 if result.is_err() {
229 self.remote.writes.attempts.remove(&document);
230 }
231 result
232 }
233
234 pub(super) fn verify_remote_save(&mut self) -> Result<(), String> {
235 let document = self.current();
236 if self.remote.writes.pending.contains_key(&document) {
237 return Err("remote write operation already pending".into());
238 }
239 let attempt = self
240 .remote
241 .writes
242 .attempts
243 .get(&document)
244 .filter(|attempt| attempt.unconfirmed)
245 .ok_or("no unconfirmed remote save to verify")?;
246 let permit = self
247 .cur()
248 .remote_metadata()
249 .and_then(|source| source.write.as_ref())
250 .ok_or("remote write permit was revoked")?;
251 if permit.id != attempt.permit {
252 return Err("remote save attempt belongs to an old permit".into());
253 }
254 let work = Work::Verify {
255 before: attempt.before.clone(),
256 contents: attempt.contents.clone(),
257 };
258 self.start_remote_write(document, attempt.revision, Some(attempt.permit), work)
259 }
260
261 fn start_remote_write(
262 &mut self,
263 document: DocumentId,
264 revision: BufferRevision,
265 permit: Option<WorkerId>,
266 work: Work,
267 ) -> Result<(), String> {
268 let request = self.worker_ids.allocate().map_err(|error| error.message)?;
269 let ticket = Ticket {
270 request,
271 key: RemoteWriteKey {
272 document,
273 revision,
274 file: work.file().clone(),
275 permit,
276 focus: self.focus_epoch,
277 action: work.action(),
278 },
279 };
280 self.remote.writes.pending.insert(document, ticket.clone());
281 self.message = match ticket.key.action {
282 WriteAction::Enable => "checking remote edit authority",
283 WriteAction::Save { .. } => "saving remotely",
284 WriteAction::Verify => "verifying remote save",
285 }
286 .into();
287 match self.tape.request(
288 "remote.write",
289 &(&ticket, work.before(), work.contents().len_bytes()),
290 ) {
291 Ok(false) => return Ok(()),
292 Ok(true) => {}
293 Err(error) => {
294 self.remote.writes.pending.remove(&document);
295 return Err(format!("remote write replay diverged: {error}"));
296 }
297 }
298 let sender = self.io.tx.clone();
299 let handle = worker::spawn(
300 "remote-write",
301 move |outcome| {
302 let _ = sender.send(IoEvent::Remote(RemoteEvent::Write(Box::new(Completion {
303 ticket,
304 outcome,
305 }))));
306 },
307 move |token| Outcome::Success(work.execute(&token)),
308 );
309 self.worker_handles.insert(request, handle);
310 Ok(())
311 }
312
313 pub(super) fn remote_write_done(
314 &mut self,
315 completion: Completion<RemoteWriteKey, RemoteWriteResult>,
316 ) {
317 let document = completion.ticket.key.document;
318 if self.remote.writes.pending.get(&document) != Some(&completion.ticket) {
319 return;
320 }
321 self.remote.writes.pending.remove(&document);
322 self.worker_handles.remove(&completion.ticket.request);
323 let request = completion.ticket.request;
324 let key = completion.ticket.key;
325 let valid = self
326 .docs
327 .get(document)
328 .and_then(|doc| doc.remote_metadata())
329 .is_some_and(|source| {
330 source.file == key.file
331 && match key.action {
332 WriteAction::Enable => {
333 source.write.is_none()
334 && source.window.is_complete()
335 && !self.remote_following(document)
336 && self.doc(document).buf.revision() == key.revision
337 }
338 _ => source
339 .write
340 .as_ref()
341 .is_some_and(|permit| Some(permit.id) == key.permit),
342 }
343 });
344 if !valid {
345 self.collection_save_progress(document, false);
346 if key.action == WriteAction::Enable
347 && !self.docs.is_empty()
348 && self.current() == document
349 {
350 self.message =
351 "remote edit admission cancelled: snapshot or follow state changed".into();
352 }
353 if matches!(key.action, WriteAction::Save { .. }) {
354 self.message = "remote save result discarded: source authority changed; disk outcome unconfirmed".into();
355 self.finish_save_feedback(document);
356 }
357 return;
358 }
359 match completion.outcome {
360 Outcome::Success(RemoteWriteResult::Enabled(version))
361 if key.action == WriteAction::Enable && version.file() == &key.file =>
362 {
363 let mut doc = self.doc_mut(document);
364 if let DocumentSource::Remote(source) = &mut doc.source {
365 source.write = Some(WritePermit {
366 id: request,
367 version,
368 });
369 source.selection = strop_remote::ReadSelection::Full;
370 doc.buf.readonly = false;
371 }
372 drop(doc);
373 self.message =
374 "remote editing enabled (cooperative locks; other programs can still race)"
375 .into();
376 }
377 Outcome::Success(RemoteWriteResult::Saved(receipt))
378 if matches!(key.action, WriteAction::Save { .. }) =>
379 {
380 return self.accept_remote_receipt(key, receipt);
381 }
382 Outcome::Success(RemoteWriteResult::Verified(Verification::Written(receipt)))
383 if key.action == WriteAction::Verify =>
384 {
385 return self.accept_remote_receipt(key, receipt);
386 }
387 Outcome::Success(RemoteWriteResult::Verified(Verification::Unchanged(version)))
388 if key.action == WriteAction::Verify =>
389 {
390 if let Some(attempt) = self.remote.writes.attempts.get(&document) {
391 if attempt.before != version {
392 self.remote_write_uncertain(
393 document,
394 "verification baseline differs".into(),
395 );
396 return;
397 }
398 }
399 self.remote.writes.attempts.remove(&document);
400 self.message = "remote original is unchanged; local edits remain unsaved".into();
401 }
402 Outcome::Success(RemoteWriteResult::Refused(error)) => {
403 if error.is_unconfirmed() {
404 self.remote_write_uncertain(document, error.to_string());
405 } else {
406 if key.action != WriteAction::Verify
407 || matches!(
408 &error,
409 RemoteSaveError::Refused {
410 kind: transport::RefusalKind::Conflict,
411 ..
412 }
413 )
414 {
415 self.remote.writes.attempts.remove(&document);
416 }
417 self.message = error.to_string();
418 }
419 }
420 Outcome::Cancelled(_) if key.action == WriteAction::Enable => {
421 self.message = "remote edit admission cancelled".into()
422 }
423 Outcome::Failed { failure, .. } if key.action == WriteAction::Enable => {
424 self.message = format!("remote edit admission failed: {}", failure.message)
425 }
426 Outcome::Cancelled(_) => self.remote_write_uncertain(
427 document,
428 "cancelled; the remote outcome is unconfirmed — :remote verify".into(),
429 ),
430 Outcome::Failed { failure, .. } => self.remote_write_uncertain(
431 document,
432 format!(
433 "{}; remote outcome unconfirmed — :remote verify",
434 failure.message
435 ),
436 ),
437 Outcome::Success(_) => self.remote_write_uncertain(
438 document,
439 "remote write result does not match its request".into(),
440 ),
441 }
442 if matches!(key.action, WriteAction::Save { .. }) {
443 self.finish_save_feedback(document);
444 self.collection_save_progress(document, false);
445 }
446 }
447
448 fn remote_write_uncertain(&mut self, document: DocumentId, message: String) {
449 if let Some(attempt) = self.remote.writes.attempts.get_mut(&document) {
450 attempt.unconfirmed = true;
451 }
452 self.message = message;
453 self.finish_save_feedback(document);
454 self.collection_save_progress(document, false);
455 }
456 fn accept_remote_receipt(&mut self, key: RemoteWriteKey, receipt: RemoteSaveReceipt) {
457 let version = receipt.into_version();
458 if version.file() != &key.file {
459 self.remote_write_uncertain(
460 key.document,
461 "receipt belongs to a different remote file".into(),
462 );
463 return;
464 }
465 let current = {
466 let mut doc = self.doc_mut(key.document);
467 let DocumentSource::Remote(source) = &mut doc.source else {
468 return;
469 };
470 let Some(permit) = source.write.as_mut() else {
471 return;
472 };
473 let size = version.size();
474 if Some(permit.id) != key.permit {
475 return;
476 }
477 permit.version = version;
478 source.window =
479 strop_remote::RemoteWindow::resolve(&strop_remote::ReadSelection::Full, size);
480 source.selection = strop_remote::ReadSelection::Full;
481 debug_assert!(doc.buf.path.is_none());
483 doc.buf.acknowledge_saved_revision(key.revision)
484 };
485 self.remote.writes.attempts.remove(&key.document);
486 self.message = if current {
487 "written remotely"
488 } else {
489 "remote snapshot written; newer edits remain unsaved"
490 }
491 .into();
492 self.finish_save_feedback(key.document);
493 self.collection_save_progress(key.document, current);
494 if current
495 && matches!(key.action, WriteAction::Save { close: true })
496 && !self.docs.is_empty()
497 && self.current() == key.document
498 && self.focus_epoch == key.focus
499 {
500 self.close_pane_or_buffer(false);
501 }
502 }
503
504 pub(crate) fn revoke_remote_write(&mut self, document: DocumentId) {
505 if let Some(doc) = self.docs.get_mut(document) {
506 if let DocumentSource::Remote(source) = &mut doc.source {
507 source.write = None;
508 doc.buf.readonly = true;
509 }
510 }
511 self.remote.writes.attempts.remove(&document);
512 if let Some(ticket) = self.remote.writes.pending.remove(&document) {
513 if let Some(handle) = self.worker_handles.remove(&ticket.request) {
514 handle.cancel(CancelReason::OwnerClosed);
515 }
516 }
517 }
518 pub(crate) fn remote_write_blocks_refresh(&self, document: DocumentId) -> bool {
519 self.remote.writes.pending.contains_key(&document)
520 || self
521 .remote
522 .writes
523 .attempts
524 .get(&document)
525 .is_some_and(|attempt| attempt.unconfirmed)
526 }
527 pub(crate) fn cancel_remote_write(&mut self, document: DocumentId) {
528 if let Some(ticket) = self.remote.writes.pending.get(&document).cloned() {
529 if ticket.key.action == WriteAction::Enable {
530 self.remote.writes.pending.remove(&document);
531 self.message = "remote edit admission cancelled".into();
532 }
533 if let Some(handle) = self.worker_handles.remove(&ticket.request) {
534 handle.cancel(CancelReason::Dismissed);
535 }
536 }
537 }
538 pub(crate) fn remote_write_pending(&self, request: WorkerId) -> bool {
539 self.remote.writes.pending.values().any(|ticket| {
540 ticket.request == request
541 && matches!(
542 ticket.key.action,
543 WriteAction::Save { .. } | WriteAction::Verify
544 )
545 })
546 }
547 pub fn remote_write_status(&self) -> Option<&'static str> {
548 if self.docs.is_empty() {
549 return None;
550 }
551 let pending = self.remote.writes.pending.get(&self.current())?;
552 Some(match pending.key.action {
553 WriteAction::Enable => "checking edit",
554 WriteAction::Save { .. } => "saving",
555 WriteAction::Verify => "verifying save",
556 })
557 }
558 pub(crate) fn remote_edit_authorized(&self) -> bool {
559 self.cur()
560 .remote_metadata()
561 .is_some_and(|source| source.write.is_some())
562 }
563}
564
565#[cfg(test)]
566mod tests;