Skip to main content

ochrepad_core/
macros.rs

1//! Macro playback. Recording happens in [`Editor::apply`]: while recording,
2//! every successful recordable action is appended.
3
4use std::fmt;
5
6use serde::{Deserialize, Serialize};
7
8use crate::action::{Action, ActionError};
9use crate::editor::Editor;
10use crate::view::ViewId;
11
12/// A saved macro.
13#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14pub struct NamedMacro {
15    pub name: String,
16    /// An optional key binding, e.g. "Ctrl+Shift+1" (the app parses it).
17    #[serde(default)]
18    pub shortcut: Option<String>,
19    pub actions: Vec<Action>,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum Repeat {
24    Times(usize),
25    /// Until an action fails (typically a Find with no more matches), a run
26    /// starts and ends with the primary caret at the end of the document
27    /// (so a "process this line, move down" macro handles the last line
28    /// once), or a run changes nothing.
29    UntilEnd,
30}
31
32/// Safety limit for [`Repeat::UntilEnd`].
33pub const MAX_UNTIL_END_RUNS: usize = 1_000_000;
34
35/// Where playback stopped.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct MacroError {
38    /// 1-based run number.
39    pub run: usize,
40    /// 1-based step within the run.
41    pub step: usize,
42    pub action: Action,
43    pub error: ActionError,
44}
45
46impl fmt::Display for MacroError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        write!(
49            f,
50            "the macro stopped at step {} of run {}: {}",
51            self.step, self.run, self.error
52        )
53    }
54}
55
56impl std::error::Error for MacroError {}
57
58impl Action {
59    /// Whether the macro recorder keeps this action. Mouse-made selections
60    /// (absolute positions) and undo/redo are left out, as in Notepad++.
61    pub fn is_recordable(&self) -> bool {
62        !matches!(
63            self,
64            Action::SetSelections(_)
65                | Action::ColumnSelect { .. }
66                | Action::Undo
67                | Action::Redo
68                | Action::Fold
69                | Action::Unfold
70                | Action::ToggleFold
71                | Action::FoldAll
72                | Action::UnfoldAll
73                | Action::FoldLevel(_)
74                | Action::UnfoldLevel(_)
75        )
76    }
77}
78
79/// (revision, selections, caret at end of document).
80type MacroState = (u64, crate::selection::SelectionSet, bool);
81
82impl Editor {
83    /// Plays `actions` on `view`. The whole playback is one undo step and
84    /// is not itself recorded. Returns the number of complete runs.
85    ///
86    /// With [`Repeat::Times`] the first failing action aborts playback and is
87    /// reported. With [`Repeat::UntilEnd`] a failure is the normal way to
88    /// stop.
89    pub fn play_macro(
90        &mut self,
91        view: ViewId,
92        actions: &[Action],
93        repeat: Repeat,
94    ) -> Result<usize, MacroError> {
95        let fail = |error| MacroError {
96            run: 1,
97            step: 1,
98            action: actions.first().cloned().unwrap_or(Action::Undo),
99            error,
100        };
101        self.begin_undo_group(view).map_err(fail)?;
102        let recording = self.take_recording();
103        let result = self.run_macro(view, actions, repeat);
104        self.restore_recording(recording);
105        self.end_undo_group(view).map_err(fail)?;
106        result
107    }
108
109    fn run_macro(
110        &mut self,
111        view: ViewId,
112        actions: &[Action],
113        repeat: Repeat,
114    ) -> Result<usize, MacroError> {
115        if actions.is_empty() {
116            return Ok(0);
117        }
118        let limit = match repeat {
119            Repeat::Times(n) => n,
120            Repeat::UntilEnd => MAX_UNTIL_END_RUNS,
121        };
122        let until_end = repeat == Repeat::UntilEnd;
123        let mut runs = 0;
124        while runs < limit {
125            let before = self.macro_state(view);
126            for (i, action) in actions.iter().enumerate() {
127                if let Err(error) = self.apply(view, action.clone()) {
128                    if until_end {
129                        return Ok(runs);
130                    }
131                    return Err(MacroError {
132                        run: runs + 1,
133                        step: i + 1,
134                        action: action.clone(),
135                        error,
136                    });
137                }
138            }
139            runs += 1;
140            if until_end {
141                let after = self.macro_state(view);
142                let at_end = |s: &Option<MacroState>| s.as_ref().is_some_and(|s| s.2);
143                if after == before || (at_end(&before) && at_end(&after)) {
144                    break;
145                }
146            }
147        }
148        Ok(runs)
149    }
150
151    /// (revision, selections, caret at end) — to detect runs that change
152    /// nothing and the end of the document.
153    fn macro_state(&self, view: ViewId) -> Option<MacroState> {
154        let v = self.view(view)?;
155        let b = self.buffer(v.buffer())?;
156        let at_end = v.selections.primary().head >= b.doc().len();
157        Some((b.revision(), v.selections.clone(), at_end))
158    }
159}