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