1use std::collections::HashMap;
13use std::sync::OnceLock;
14
15use serde::Serialize;
16
17use crate::keymap::{parse_keys, Key, KeyCode, Mods, ScriptItem};
18use crate::marks::MarkId;
19use crate::msg::{By, Dir, Msg};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum Category {
25 Move,
26 Select,
27 Edit,
28 Clipboard,
29 History,
30 Structure,
31 View,
32 File,
33}
34
35impl Category {
36 pub fn name(self) -> &'static str {
37 match self {
38 Category::Move => "Move",
39 Category::Select => "Select",
40 Category::Edit => "Edit",
41 Category::Clipboard => "Clipboard",
42 Category::History => "History",
43 Category::Structure => "Structure",
44 Category::View => "View",
45 Category::File => "File",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
52pub struct CommandInfo {
53 pub id: &'static str,
55 pub name: &'static str,
57 pub description: &'static str,
58 pub category: Category,
59 #[serde(skip_serializing_if = "std::ops::Not::not")]
61 pub outline_only: bool,
62 #[serde(skip_serializing_if = "std::ops::Not::not")]
64 pub takes_block: bool,
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
69#[serde(rename_all = "snake_case")]
70pub enum Platform {
71 Any,
73 Mac,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
81pub struct Binding {
82 pub keys: &'static str,
83 pub command: &'static str,
84 pub platform: Platform,
85 #[serde(skip_serializing_if = "std::ops::Not::not")]
87 pub outline: bool,
88}
89
90macro_rules! cmd {
91 ($id:literal, $name:literal, $cat:ident, $desc:literal) => {
92 CommandInfo { id: $id, name: $name, description: $desc, category: Category::$cat, outline_only: false, takes_block: false }
93 };
94 ($id:literal, $name:literal, $cat:ident, $desc:literal, outline) => {
95 CommandInfo { id: $id, name: $name, description: $desc, category: Category::$cat, outline_only: true, takes_block: false }
96 };
97 ($id:literal, $name:literal, $cat:ident, $desc:literal, block) => {
98 CommandInfo { id: $id, name: $name, description: $desc, category: Category::$cat, outline_only: true, takes_block: true }
99 };
100}
101
102const COMMANDS: &[CommandInfo] = &[
103 cmd!("move.left", "Left", Move, "Move left one character"),
104 cmd!("move.right", "Right", Move, "Move right one character"),
105 cmd!("move.up", "Up", Move, "Move up one row"),
106 cmd!("move.down", "Down", Move, "Move down one row"),
107 cmd!("move.word_left", "Word left", Move, "Move to the start of the previous word"),
108 cmd!("move.word_right", "Word right", Move, "Move to the end of the next word"),
109 cmd!("move.line_start", "Line start", Move, "Move to the start of the row"),
110 cmd!("move.line_end", "Line end", Move, "Move to the end of the row"),
111 cmd!("move.doc_start", "Document start", Move, "Move to the start of the document"),
112 cmd!("move.doc_end", "Document end", Move, "Move to the end of the document"),
113 cmd!("move.page_up", "Page up", Move, "Move up a screenful"),
114 cmd!("move.page_down", "Page down", Move, "Move down a screenful"),
115 cmd!("move.block_up", "Block up", Move, "Move to the start of this block, then the one before"),
116 cmd!("move.block_down", "Block down", Move, "Move to the start of the next block"),
117 cmd!("select.left", "Select left", Select, "Extend the selection left one character"),
118 cmd!("select.right", "Select right", Select, "Extend the selection right one character"),
119 cmd!("select.up", "Select up", Select, "Extend the selection up one row"),
120 cmd!("select.down", "Select down", Select, "Extend the selection down one row"),
121 cmd!("select.word_left", "Select word left", Select, "Extend the selection to the previous word start"),
122 cmd!("select.word_right", "Select word right", Select, "Extend the selection to the next word end"),
123 cmd!("select.line_start", "Select to line start", Select, "Extend the selection to the start of the row"),
124 cmd!("select.line_end", "Select to line end", Select, "Extend the selection to the end of the row"),
125 cmd!("select.doc_start", "Select to document start", Select, "Extend the selection to the start of the document"),
126 cmd!("select.doc_end", "Select to document end", Select, "Extend the selection to the end of the document"),
127 cmd!("select.page_up", "Select page up", Select, "Extend the selection up a screenful"),
128 cmd!("select.page_down", "Select page down", Select, "Extend the selection down a screenful"),
129 cmd!("select.block_up", "Select block up", Select, "Extend the selection to the previous block start"),
130 cmd!("select.block_down", "Select block down", Select, "Extend the selection to the next block start"),
131 cmd!("select.all", "Select all", Select, "Select the whole document"),
132 cmd!("select.collapse", "Collapse selection", Select, "Collapse every selection to its caret"),
133 cmd!("select.block", "Select block", Select, "Select a block's whole content", block),
134 cmd!("edit.newline", "New line", Edit, "Break the line (in a list, start the next item)"),
135 cmd!("edit.soft_break", "Line break", Edit, "Break the line inside the block", outline),
136 cmd!("edit.insert_tab", "Tab", Edit, "Insert a tab character"),
137 cmd!("edit.backspace", "Backspace", Edit, "Delete the character before the caret, or the selection"),
138 cmd!("edit.delete_forward", "Delete", Edit, "Delete the character after the caret, or the selection"),
139 cmd!("edit.delete_word", "Delete word", Edit, "Delete back to the previous word start"),
140 cmd!("edit.delete_word_forward", "Delete word forward", Edit, "Delete forward to the next word end"),
141 cmd!("edit.delete_to_line_start", "Delete to line start", Edit, "Delete back to the start of the row"),
142 cmd!("edit.delete_to_line_end", "Delete to line end", Edit, "Delete forward to the end of the row"),
143 cmd!("edit.kill_line", "Kill line", Edit, "Delete to the end of the line, or the line break when there"),
144 cmd!("structure.indent", "Indent", Structure, "Nest the block (or every selected block) one level deeper", outline),
145 cmd!("structure.outdent", "Outdent", Structure, "Un-nest the block (or every selected block) one level", outline),
146 cmd!("structure.move_up", "Move block up", Structure, "Swap the block and its children with the sibling above", outline),
147 cmd!("structure.move_down", "Move block down", Structure, "Swap the block and its children with the sibling below", outline),
148 cmd!("clip.copy", "Copy", Clipboard, "Copy the selection"),
149 cmd!("clip.cut", "Cut", Clipboard, "Cut the selection"),
150 cmd!("clip.paste", "Paste", Clipboard, "Paste (Markdown becomes blocks in an outline)"),
151 cmd!("clip.paste_plain", "Paste as plain text", Clipboard, "Paste as plain paragraphs, never list items"),
152 cmd!("history.undo", "Undo", History, "Undo the last step"),
153 cmd!("history.redo", "Redo", History, "Redo the step undone"),
154 cmd!("view.fold", "Fold", View, "Hide a block's children in this view", block),
155 cmd!("view.unfold", "Unfold", View, "Show a folded block's children", block),
156 cmd!("view.fold_toggle", "Toggle fold", View, "Fold or unfold a block", block),
157 cmd!("file.save", "Save", File, "Write the document to its file"),
158 cmd!("file.quit", "Quit", File, "Quit (a second time to discard unsaved changes)"),
159];
160
161pub fn commands() -> &'static [CommandInfo] {
163 COMMANDS
164}
165
166pub fn command(id: &str) -> Option<&'static CommandInfo> {
168 COMMANDS.iter().find(|c| c.id == id)
169}
170
171pub fn command_msg(id: &str, block: Option<MarkId>) -> Option<Msg> {
174 use Dir::{Backward as B, Forward as F};
175 let mv = |dir, by, extend| Some(Msg::Move { dir, by, extend });
176 let (verb, extend) = match id.split_once('.') {
177 Some(("move", v)) => (v, false),
178 Some(("select", v)) if !matches!(v, "all" | "collapse" | "block") => (v, true),
179 _ => ("", false),
180 };
181 let motion = match verb {
182 "left" => mv(B, By::Grapheme, extend),
183 "right" => mv(F, By::Grapheme, extend),
184 "up" => mv(B, By::VisualLine, extend),
185 "down" => mv(F, By::VisualLine, extend),
186 "word_left" => mv(B, By::Word, extend),
187 "word_right" => mv(F, By::Word, extend),
188 "line_start" => mv(B, By::LineStart, extend),
189 "line_end" => mv(F, By::LineEnd, extend),
190 "doc_start" => mv(B, By::DocStart, extend),
191 "doc_end" => mv(F, By::DocEnd, extend),
192 "page_up" => mv(B, By::Page, extend),
193 "page_down" => mv(F, By::Page, extend),
194 "block_up" => mv(B, By::Block, extend),
195 "block_down" => mv(F, By::Block, extend),
196 _ => None,
197 };
198 if motion.is_some() {
199 return motion;
200 }
201 Some(match id {
202 "select.all" => Msg::SelectAll,
203 "select.collapse" => Msg::Collapse,
204 "select.block" => Msg::SelectBlock { id: block? },
205 "edit.newline" => Msg::InsertNewline,
206 "edit.soft_break" => Msg::SoftBreak,
207 "edit.insert_tab" => Msg::InsertText { text: "\t".into() },
208 "edit.backspace" => Msg::DeleteBackward,
209 "edit.delete_forward" => Msg::DeleteForward,
210 "edit.delete_word" => Msg::DeleteWordBackward,
211 "edit.delete_word_forward" => Msg::DeleteWordForward,
212 "edit.delete_to_line_start" => Msg::DeleteToLineStart,
213 "edit.delete_to_line_end" => Msg::DeleteToLineEnd,
214 "edit.kill_line" => Msg::KillLine,
215 "structure.indent" => Msg::Indent,
216 "structure.outdent" => Msg::Outdent,
217 "structure.move_up" => Msg::MoveBlock { dir: B },
218 "structure.move_down" => Msg::MoveBlock { dir: F },
219 "clip.copy" => Msg::Copy,
220 "clip.cut" => Msg::Cut,
221 "clip.paste" => Msg::Paste { text: None },
222 "clip.paste_plain" => Msg::PastePlain { text: None },
223 "history.undo" => Msg::Undo,
224 "history.redo" => Msg::Redo,
225 "view.fold" => Msg::Fold { id: block? },
226 "view.unfold" => Msg::Unfold { id: block? },
227 "view.fold_toggle" => Msg::ToggleFold { id: block? },
228 "file.save" => Msg::Save,
229 "file.quit" => Msg::Quit,
230 _ => return None,
231 })
232}
233
234macro_rules! bind {
235 ($keys:literal, $cmd:literal) => {
236 Binding { keys: $keys, command: $cmd, platform: Platform::Any, outline: false }
237 };
238 ($keys:literal, $cmd:literal, mac) => {
239 Binding { keys: $keys, command: $cmd, platform: Platform::Mac, outline: false }
240 };
241 ($keys:literal, $cmd:literal, outline) => {
242 Binding { keys: $keys, command: $cmd, platform: Platform::Any, outline: true }
243 };
244}
245
246const BINDINGS: &[Binding] = &[
249 bind!("<left>", "move.left"),
250 bind!("<right>", "move.right"),
251 bind!("<up>", "move.up"),
252 bind!("<down>", "move.down"),
253 bind!("<a-left>", "move.word_left"),
254 bind!("<c-left>", "move.word_left"),
255 bind!("<a-b>", "move.word_left"),
256 bind!("<a-right>", "move.word_right"),
257 bind!("<c-right>", "move.word_right"),
258 bind!("<a-f>", "move.word_right"),
259 bind!("<d-left>", "move.line_start", mac),
260 bind!("<home>", "move.line_start"),
261 bind!("<c-a>", "move.line_start"),
262 bind!("<d-right>", "move.line_end", mac),
263 bind!("<end>", "move.line_end"),
264 bind!("<c-e>", "move.line_end"),
265 bind!("<d-up>", "move.doc_start", mac),
266 bind!("<c-home>", "move.doc_start"),
267 bind!("<d-home>", "move.doc_start", mac),
268 bind!("<d-down>", "move.doc_end", mac),
269 bind!("<c-end>", "move.doc_end"),
270 bind!("<d-end>", "move.doc_end", mac),
271 bind!("<pgup>", "move.page_up"),
272 bind!("<pgdn>", "move.page_down"),
273 bind!("<c-up>", "move.block_up", outline),
274 bind!("<c-down>", "move.block_down", outline),
275 bind!("<s-left>", "select.left"),
276 bind!("<s-right>", "select.right"),
277 bind!("<s-up>", "select.up"),
278 bind!("<s-down>", "select.down"),
279 bind!("<a-s-left>", "select.word_left"),
280 bind!("<c-s-left>", "select.word_left"),
281 bind!("<a-s-b>", "select.word_left"),
282 bind!("<a-s-right>", "select.word_right"),
283 bind!("<c-s-right>", "select.word_right"),
284 bind!("<a-s-f>", "select.word_right"),
285 bind!("<d-s-left>", "select.line_start", mac),
286 bind!("<s-home>", "select.line_start"),
287 bind!("<c-s-a>", "select.line_start"),
288 bind!("<d-s-right>", "select.line_end", mac),
289 bind!("<s-end>", "select.line_end"),
290 bind!("<c-s-e>", "select.line_end"),
291 bind!("<d-s-up>", "select.doc_start", mac),
292 bind!("<c-s-home>", "select.doc_start"),
293 bind!("<d-s-home>", "select.doc_start", mac),
294 bind!("<d-s-down>", "select.doc_end", mac),
295 bind!("<c-s-end>", "select.doc_end"),
296 bind!("<d-s-end>", "select.doc_end", mac),
297 bind!("<s-pgup>", "select.page_up"),
298 bind!("<s-pgdn>", "select.page_down"),
299 bind!("<c-s-up>", "select.block_up", outline),
300 bind!("<c-s-down>", "select.block_down", outline),
301 bind!("<d-a>", "select.all", mac),
302 bind!("<a-a>", "select.all"),
303 bind!("<esc>", "select.collapse"),
304 bind!("<s-esc>", "select.collapse"),
305 bind!("<cr>", "edit.newline"),
306 bind!("<s-cr>", "edit.newline"),
307 bind!("<s-cr>", "edit.soft_break", outline),
308 bind!("<c-j>", "edit.soft_break", outline),
309 bind!("<tab>", "edit.insert_tab"),
310 bind!("<bs>", "edit.backspace"),
311 bind!("<s-bs>", "edit.backspace"),
312 bind!("<c-h>", "edit.backspace"),
313 bind!("<del>", "edit.delete_forward"),
314 bind!("<s-del>", "edit.delete_forward"),
315 bind!("<a-bs>", "edit.delete_word"),
316 bind!("<c-bs>", "edit.delete_word"),
317 bind!("<c-w>", "edit.delete_word"),
318 bind!("<a-del>", "edit.delete_word_forward"),
319 bind!("<c-del>", "edit.delete_word_forward"),
320 bind!("<a-d>", "edit.delete_word_forward"),
321 bind!("<d-bs>", "edit.delete_to_line_start", mac),
322 bind!("<c-u>", "edit.delete_to_line_start"),
323 bind!("<d-del>", "edit.delete_to_line_end", mac),
324 bind!("<c-k>", "edit.kill_line"),
325 bind!("<tab>", "structure.indent", outline),
326 bind!("<s-tab>", "structure.outdent", outline),
327 bind!("<a-up>", "structure.move_up", outline),
328 bind!("<a-down>", "structure.move_down", outline),
329 bind!("<d-c>", "clip.copy", mac),
330 bind!("<c-c>", "clip.copy"),
331 bind!("<d-x>", "clip.cut", mac),
332 bind!("<c-x>", "clip.cut"),
333 bind!("<d-v>", "clip.paste", mac),
334 bind!("<c-v>", "clip.paste"),
335 bind!("<a-v>", "clip.paste_plain", outline),
336 bind!("<d-z>", "history.undo", mac),
337 bind!("<c-z>", "history.undo"),
338 bind!("<d-s-z>", "history.redo", mac),
339 bind!("<c-s-z>", "history.redo"),
340 bind!("<d-y>", "history.redo", mac),
341 bind!("<c-y>", "history.redo"),
342 bind!("<d-r>", "history.redo", mac),
343 bind!("<c-r>", "history.redo"),
344 bind!("<d-s>", "file.save", mac),
345 bind!("<c-s>", "file.save"),
346 bind!("<d-q>", "file.quit", mac),
347 bind!("<c-q>", "file.quit"),
348];
349
350pub fn default_keymap(outline: bool) -> Vec<Binding> {
353 let outline_keys: Vec<&str> = BINDINGS.iter().filter(|b| b.outline).map(|b| b.keys).collect();
354 BINDINGS
355 .iter()
356 .filter(|b| if outline { b.outline || !outline_keys.contains(&b.keys) } else { !b.outline })
357 .copied()
358 .collect()
359}
360
361pub fn key_notation(key: &Key) -> String {
364 let mut mods = key.mods;
365 let name = match key.code {
366 KeyCode::Char(c) => {
367 if c.is_ascii_uppercase() {
368 mods.shift = true;
369 }
370 match c.to_ascii_lowercase() {
371 ' ' => "space".to_string(),
372 '<' => "lt".to_string(),
373 '>' => "gt".to_string(),
374 c => c.to_string(),
375 }
376 }
377 KeyCode::Enter => "cr".into(),
378 KeyCode::Backspace => "bs".into(),
379 KeyCode::Delete => "del".into(),
380 KeyCode::Left => "left".into(),
381 KeyCode::Right => "right".into(),
382 KeyCode::Up => "up".into(),
383 KeyCode::Down => "down".into(),
384 KeyCode::Home => "home".into(),
385 KeyCode::End => "end".into(),
386 KeyCode::PageUp => "pgup".into(),
387 KeyCode::PageDown => "pgdn".into(),
388 KeyCode::Tab => "tab".into(),
389 KeyCode::BackTab => {
390 mods.shift = true;
391 "tab".into()
392 }
393 KeyCode::Esc => "esc".into(),
394 };
395 let mut s = String::from("<");
396 for (on, p) in [(mods.ctrl, "c-"), (mods.alt, "a-"), (mods.cmd, "d-"), (mods.shift, "s-")] {
397 if on {
398 s.push_str(p);
399 }
400 }
401 s.push_str(&name);
402 s.push('>');
403 s
404}
405
406pub fn binding_key(b: &Binding) -> Option<Key> {
408 match parse_keys(b.keys).ok()?.as_slice() {
409 [ScriptItem::Key(k)] => Some(*k),
410 _ => None,
411 }
412}
413
414fn table(outline: bool) -> &'static HashMap<String, &'static str> {
415 static PLAIN: OnceLock<HashMap<String, &'static str>> = OnceLock::new();
416 static OUTLINE: OnceLock<HashMap<String, &'static str>> = OnceLock::new();
417 let cell = if outline { &OUTLINE } else { &PLAIN };
418 cell.get_or_init(|| {
419 default_keymap(outline)
420 .into_iter()
421 .filter_map(|b| {
422 let key = binding_key(&b)?;
423 let cmd = COMMANDS.iter().find(|c| c.id == b.command)?.id;
424 Some((key_notation(&key), cmd))
425 })
426 .collect()
427 })
428}
429
430pub fn command_for(outline: bool, key: &Key) -> Option<&'static str> {
432 table(outline).get(&key_notation(key)).copied()
433}
434
435pub(crate) fn lookup(outline: bool, key: &Key) -> Option<Msg> {
438 if let Some(id) = command_for(outline, key) {
439 return command_msg(id, None);
440 }
441 match key.code {
442 KeyCode::Char(c) if !(key.mods.ctrl || key.mods.alt || key.mods.cmd) => {
443 let c = if key.mods.shift { c.to_uppercase().next().unwrap_or(c) } else { c };
444 Some(Msg::InsertText { text: c.to_string() })
445 }
446 _ => None,
447 }
448}
449
450impl Mods {
451 pub fn none() -> Mods {
453 Mods::default()
454 }
455}