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
|
//! Key names, and the two word motions the entry needs.
//!
//! A key crosses this ABI as a string — `"ctrl+u"`, `"page-down"`, `"a"` —
//! because that is the only shape jolt can match on, and because a name is
//! something a test can type. The same names glimmer-tui's `k/match?` takes.
/// Where the word before `at` starts.
pub fn word_left(text: &[char], at: usize) -> usize {
let mut i = at.min(text.len());
while i > 0 && text[i - 1].is_whitespace() {
i -= 1;
}
while i > 0 && !text[i - 1].is_whitespace() {
i -= 1;
}
i
}
/// Where the word after `at` ends.
pub fn word_right(text: &[char], at: usize) -> usize {
let mut i = at.min(text.len());
while i < text.len() && text[i].is_whitespace() {
i += 1;
}
while i < text.len() && !text[i].is_whitespace() {
i += 1;
}
i
}
#[cfg(feature = "terminal")]
mod terminal {
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
/// The name for one key event, or `None` for a key with no name — a bare
/// modifier press, or a media key nothing here would do anything with.
pub fn name(event: KeyEvent) -> Option<String> {
let mods = event.modifiers;
let base = match event.code {
KeyCode::Char(' ') => "space".to_owned(),
KeyCode::Char(c) => {
// Shift is already in the character the terminal sent; saying
// it twice would make `A` into `shift+a`, which nothing wants
// to match on.
let mut name = c.to_string();
if mods.contains(KeyModifiers::CONTROL) || mods.contains(KeyModifiers::ALT) {
name = c.to_lowercase().to_string();
}
name
}
KeyCode::Enter => "enter".into(),
KeyCode::Tab => "tab".into(),
KeyCode::BackTab => return Some("shift+tab".into()),
KeyCode::Backspace => "backspace".into(),
KeyCode::Delete => "delete".into(),
KeyCode::Insert => "insert".into(),
KeyCode::Esc => "esc".into(),
KeyCode::Up => "up".into(),
KeyCode::Down => "down".into(),
KeyCode::Left => "left".into(),
KeyCode::Right => "right".into(),
KeyCode::Home => "home".into(),
KeyCode::End => "end".into(),
KeyCode::PageUp => "page-up".into(),
KeyCode::PageDown => "page-down".into(),
KeyCode::F(n) => format!("f{n}"),
_ => return None,
};
let mut out = String::new();
if mods.contains(KeyModifiers::CONTROL) {
out.push_str("ctrl+");
}
if mods.contains(KeyModifiers::ALT) {
out.push_str("alt+");
}
if mods.contains(KeyModifiers::SHIFT) && !matches!(event.code, KeyCode::Char(_)) {
out.push_str("shift+");
}
out.push_str(&base);
Some(out)
}
}
#[cfg(feature = "terminal")]
pub use terminal::name;
#[cfg(test)]
mod tests {
use super::*;
fn chars(text: &str) -> Vec<char> {
text.chars().collect()
}
#[test]
fn word_motions_step_over_the_gap_and_then_the_word() {
let text = chars("one two three");
assert_eq!(word_left(&text, 13), 8);
assert_eq!(word_left(&text, 8), 4);
assert_eq!(word_left(&text, 0), 0);
assert_eq!(word_right(&text, 0), 3);
assert_eq!(word_right(&text, 3), 7);
assert_eq!(word_right(&text, 13), 13);
}
}
|