Skip to main content

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}