tix/app/editor_input.rs
1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4impl App {
5 /// Launch the editor on a temp copy of `filename` and settle the result
6 /// (tix #1510).
7 ///
8 /// The issue file's raw bytes are snapshotted **before** the child runs, and
9 /// the edited temp copy is compared against that snapshot **after** it exits
10 /// by [`App::after_editor`] — the one place the conflict decision is made.
11 /// Splitting the round trip this way is what lets the live
12 /// `Action::OpenEditor` shell-out and the programmatic entry point be the same
13 /// code path: the only difference between them is who suspends the terminal
14 /// around the child.
15 pub fn open_editor_round_trip(&mut self, filename: &str) -> Result<()> {
16 let name = editor::resolve_editor(self.store.config.editor.as_deref())?;
17 let original = self
18 .store
19 .get(IssueSelector::Filename(filename.to_string()))?;
20 let disk_path = self.store.issues_dir.join(&original.filename);
21 let snapshot = std::fs::read(&disk_path).map_err(TixError::Io)?;
22 let temp = std::env::temp_dir().join(format!(
23 "tix-edit-{}-{}.md",
24 std::process::id(),
25 original.number
26 ));
27 std::fs::write(&temp, original.to_file_contents()).map_err(TixError::Io)?;
28 self.pending_edit = Some(PendingEdit {
29 filename: original.filename.clone(),
30 temp: temp.clone(),
31 snapshot,
32 });
33 if let Err(e) = editor::run_editor(&name, &temp, &self.store.config) {
34 // The child failed to launch or exited non-zero, so nothing was
35 // written — and the copy is **kept**. Deleting it here (as the old
36 // flow did) is the automatic discard tix #1506 exists to remove; the
37 // temp copy is the only copy of anything the editor might have
38 // managed to save before it failed.
39 self.pending_edit = None;
40 self.status_msg = Some(format!(
41 "editor: {e}; your copy is kept at {}",
42 temp.display()
43 ));
44 return Ok(());
45 }
46 self.after_editor(&original.filename);
47 Ok(())
48 }
49
50 /// Finish an editor round trip: commit the edit, or refuse to because the file
51 /// changed underneath it (tix #1510).
52 ///
53 /// Both entry points arrive here — [`App::open_editor_round_trip`] for the live
54 /// shell-out, and direct callers that write the file themselves before calling
55 /// this. A round trip that recorded a pending edit is compared against its
56 /// pre-edit snapshot and may enter [`Mode::EditConflict`] **writing nothing**;
57 /// one with no pending edit has no pre-edit bytes to compare against, so it
58 /// keeps the pre-#1510 behaviour of persisting the file it is asked to read.
59 pub fn after_editor(&mut self, filename: &str) {
60 // The lint below reports the project's current rules, and the user may
61 // have declared a custom attribute in `tix.toml` since this session
62 // started (tix #1506): adopt the file before judging the edit.
63 self.reload_config_if_changed();
64 if let Some(pending) = self
65 .pending_edit
66 .take_if(|pending| pending.filename == filename)
67 {
68 self.settle_pending_edit(pending);
69 return;
70 }
71 match self
72 .store
73 .get(IssueSelector::Filename(filename.to_string()))
74 {
75 // No pending edit means this caller wrote the file itself, so there
76 // is no retained temp copy: the issue file is the working copy.
77 Ok(issue) => self.commit_edited_issue(filename, issue, "Edited", None),
78 Err(e) => {
79 self.status_msg = Some(format!("editor: {e}"));
80 }
81 }
82 }
83}