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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
//! `Action`: what the user intends, independent of where the carets are
//! (PLAN 3.5). Key bindings map to actions and macros record them.
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::buffer::{BufferId, Indent};
use crate::column_edit::ColumnInsert;
use crate::position::VisualCol;
use crate::search::SearchQuery;
use crate::selection::SelectionSet;
use crate::text_tools::{Codec, InvalidJson};
use crate::transform::{CaseMode, SortOptions};
use crate::view::ViewId;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Motion {
/// One grapheme cluster.
Left,
Right,
WordLeft,
WordRight,
/// Smart home: first non-blank of the line, or column 0 if already there.
Home,
LineStart,
LineEnd,
/// Home with word wrap: to the start of the caret's display row, or
/// `Home` when already there or on a line's first row. Without wrap it
/// is `Home`.
RowHome,
/// End with word wrap: to the end of the caret's display row, or the
/// line end when already there. Without wrap it is `LineEnd`.
RowEnd,
DocStart,
DocEnd,
/// Vertical moves keep a remembered visual column.
Up,
Down,
/// Page moves by the given number of lines (the view's page height).
PageUp(usize),
PageDown(usize),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum Action {
InsertText(String),
/// Like `InsertText`, but when there are several selections and the text
/// has exactly that many lines, each selection gets one line.
Paste(String),
/// Tab key: a tab character, or spaces to the next tab stop when the
/// editor is set to indent with spaces.
InsertTab,
/// Newline plus the current line's leading whitespace (auto-indent).
InsertNewline,
DeleteBackward,
DeleteForward,
DeleteWordBackward,
DeleteWordForward,
MoveCursor {
motion: Motion,
extend: bool,
},
SelectAll,
SelectWord,
SelectLine,
AddCursorAbove,
AddCursorBelow,
/// Collapse to a caret at the primary selection's head.
CollapseSelections,
/// Replace the selections (mouse clicks and drags). Positions are clamped
/// into the document and onto char boundaries.
SetSelections(SelectionSet),
/// Rectangular selection in visual columns: one selection per line from
/// the anchor corner to the head corner. Lines shorter than the rectangle
/// get an empty selection at their end (no virtual space).
ColumnSelect {
anchor_line: usize,
anchor_col: VisualCol,
head_line: usize,
head_col: VisualCol,
},
/// Grow or shrink the rectangle by moving its head corner (Alt+Shift+
/// arrows); starts a rectangle at the primary caret if there is none.
ColumnExtend(Direction),
/// Paste a rectangular clip: line `i` goes to the caret's visual column
/// on the `i`-th line below it, padding short lines with spaces and
/// adding lines at the end of the document as needed. With several
/// selections or a non-empty one it behaves like `Paste`.
PasteBlock(String),
/// Select the next (or previous) match after the primary selection.
/// Fails with `NotFound` when there is none (macros stop there).
Find {
query: SearchQuery,
backward: bool,
},
/// Replace the primary selection if it is exactly a match, then find
/// the next match.
Replace {
query: SearchQuery,
replacement: String,
},
/// Replace every match (inside the selections with "in selection") as
/// one undo step.
ReplaceAll {
query: SearchQuery,
replacement: String,
},
/// One selection per match.
SelectAllMatches(SearchQuery),
/// Multi-select Next: add the next occurrence of the primary
/// selection's text (after it, going round from the end) as a new
/// primary selection. With a caret, select the word at it first.
MultiSelectNext {
match_case: bool,
whole_word: bool,
},
/// Multi-select All: a selection on every occurrence of the primary
/// selection's text (or of the word at the caret).
MultiSelectAll {
match_case: bool,
whole_word: bool,
},
/// Convert each selection (or the caret's line) to a case.
ConvertCase(CaseMode),
/// Sort the selected lines, the rectangle's lines keyed by the text in
/// the rectangle, or the whole document when nothing is selected.
SortLines(SortOptions),
/// Put the lines Sort Lines would sort in a random order (Randomize
/// Line Order); the seed decides the order, so a macro replays it.
ShuffleLines {
seed: u64,
},
/// Remove trailing spaces and tabs from the selected lines (all lines
/// when nothing is selected).
TrimTrailingWhitespace,
/// Duplicate the caret's line, or each selection.
DuplicateLine,
MoveLinesUp,
MoveLinesDown,
/// Join the selected lines (or the caret's line and the next).
JoinLines,
/// Split each selected line (or the caret's line) wider than `width`
/// columns at the blanks between words, as Notepad++'s Split Lines;
/// the new lines start with the line's indentation.
SplitLines {
width: usize,
},
/// Remove empty lines from the selected lines (all lines when nothing
/// is selected); with `include_whitespace`, also lines of only spaces
/// and tabs.
RemoveEmptyLines {
include_whitespace: bool,
},
/// Remove lines equal to an earlier selected line (all lines when
/// nothing is selected), keeping the order; with `consecutive_only`,
/// only lines equal to the line right before them.
RemoveDuplicateLines {
consecutive_only: bool,
},
/// Delete the lines the selections touch (the caret's line), with their
/// line breaks: Notepad++'s Line Delete (Ctrl+Shift+L).
DeleteLine,
/// Swap each caret's line with the line above it (Ctrl+T); the caret
/// goes to the start of its line, as in Notepad++.
TransposeLines,
/// Insert a line above each caret's line (a selection's first line),
/// indented like it, and move the caret there (Ctrl+Alt+Enter).
InsertLineAbove,
/// Insert a line below each caret's line (a selection's last line),
/// indented like it, and move the caret there (Ctrl+Alt+Shift+Enter).
InsertLineBelow,
/// Reverse the order of the selected lines (every line when nothing is
/// selected).
ReverseLines,
/// Remove leading spaces and tabs from the selected lines (all lines
/// when nothing is selected).
TrimLeadingWhitespace,
/// Remove leading and trailing spaces and tabs from the selected lines
/// (all lines when nothing is selected).
TrimLeadingAndTrailingWhitespace,
/// Replace the line breaks between the selected lines (all lines) with
/// spaces, keeping the blanks around them.
EolToSpace,
/// Replace every tab on the selected lines (all lines) with spaces to
/// the same tab stop.
TabsToSpaces,
/// Replace spaces with tabs on the selected lines (all lines) wherever
/// the columns stay the same: the indentation, and runs of two or more
/// spaces reaching a tab stop.
SpacesToTabs,
/// The Column Editor: insert text or a number sequence at one column
/// on many lines (see [`ColumnInsert`]).
ColumnInsert(ColumnInsert),
/// Fold the region holding the caret: the caret's line if it starts a
/// region, else the innermost region around it. Fold actions change the
/// view only (no undo step) and are not recorded in macros.
Fold,
/// Unfold the caret's line, or every fold inside the region around it.
Unfold,
/// Unfold the caret's line if it is folded, else `Fold`.
ToggleFold,
FoldAll,
UnfoldAll,
/// Fold every region of this level: the nesting depth for indentation
/// (1 is outermost), the heading level in Markdown.
FoldLevel(usize),
UnfoldLevel(usize),
/// Move the caret to a line and visual column, both counted from 1 as
/// in the status bar (column 1 is the line start).
GoToLine {
line: usize,
column: usize,
},
/// Indent the selected lines, or the caret's line, by one level (Tab
/// does this only when a selection spans lines).
Indent,
/// Shift+Tab: remove one level of indentation from the selected lines,
/// or the caret's line. (Tab with a selection over several lines
/// indents them.)
Unindent,
/// Comment the selected lines (or the caret's line) with the language's
/// line comment, or uncomment them if they all are. Languages with only
/// block comments get a block comment around the lines.
ToggleLineComment,
/// Comment the selected lines (or the caret's line) even if they
/// already are, as Notepad++'s Single Line Comment. Languages with only
/// block comments get a block comment around the lines unless they are
/// in one.
CommentLines,
/// Remove one line comment from each selected line that has one (or
/// the block comment around the lines), as Notepad++'s Single Line
/// Uncomment. Other lines are left alone.
UncommentLines,
/// Wrap each selection (or the caret's line) in a block comment, or
/// unwrap it.
ToggleBlockComment,
/// Rewrite every line's leading whitespace as spaces or as tabs, keeping
/// its width, and indent with that from now on.
ConvertIndentation {
to_spaces: bool,
},
/// Move the caret to the bracket matching the one beside it.
GoToMatchingBracket,
/// Select from the bracket beside the caret to its match, both included.
SelectToMatchingBracket,
/// Pretty-print the JSON in each selection, or the whole document when
/// nothing is selected: one member or element per line, indented with
/// `indent` (the document's indentation when `None`). Key order,
/// numbers and strings are kept exactly. Fails on invalid JSON.
FormatJson {
#[serde(default)]
indent: Option<Indent>,
},
/// Remove the whitespace between the JSON tokens of each selection (or
/// the whole document).
MinifyJson,
/// Encode each selection, or the whole document when nothing is
/// selected, as UTF-8 with the document's line endings.
Encode(Codec),
/// Decode each selection (or the whole document). Fails when it is not
/// valid or the bytes are not UTF-8 text.
Decode(Codec),
/// Bookmark each caret's line, or remove the bookmarks if every one of
/// those lines has one (Ctrl+F2). Bookmarks belong to the document;
/// changing them is not an undo step.
ToggleBookmark,
/// Move the caret to the start of the next bookmarked line, wrapping
/// around to the first (F2).
NextBookmark,
/// Move the caret to the start of the previous bookmarked line,
/// wrapping around to the last (Shift+F2).
PrevBookmark,
/// Remove every bookmark.
ClearBookmarks,
/// Bookmark every line without a bookmark, and remove the others.
InverseBookmarks,
/// Bookmark every line holding the start of a match (in the selections
/// with "in selection"), keeping the bookmarks already there.
BookmarkMatches(SearchQuery),
/// Delete the bookmarked lines with their line breaks.
RemoveBookmarkedLines,
/// Delete every line without a bookmark.
RemoveUnbookmarkedLines,
/// Replace the text of each bookmarked line (not its line break) with
/// this text: Paste to (Replace) Bookmarked Lines.
ReplaceBookmarkedLines(String),
Undo,
Redo,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ActionError {
UnknownView(ViewId),
UnknownBuffer(BufferId),
InvalidPattern(String),
NotFound,
/// The buffer is read-only.
ReadOnly,
/// The language has no comment markers ochrepad knows.
NoCommentSyntax,
/// The JSON is not valid (boxed to keep errors small).
InvalidJson(Box<InvalidJson>),
/// The text cannot be decoded; the message says why.
Decode(String),
/// A bookmark command found no bookmarks.
NoBookmarks,
}
impl fmt::Display for ActionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ActionError::UnknownView(v) => write!(f, "no view with id {}", v.0),
ActionError::UnknownBuffer(b) => write!(f, "no buffer with id {}", b.0),
ActionError::InvalidPattern(e) => write!(f, "invalid search pattern: {e}"),
ActionError::NotFound => f.write_str("no match found"),
ActionError::ReadOnly => f.write_str("the document is read-only"),
ActionError::NoCommentSyntax => {
f.write_str("no comment markers are known for this language")
}
ActionError::InvalidJson(e) => write!(
f,
"invalid JSON on line {}, column {}: {}",
e.line, e.column, e.message
),
ActionError::Decode(message) => f.write_str(message),
ActionError::NoBookmarks => f.write_str("there are no bookmarks"),
}
}
}
impl std::error::Error for ActionError {}