Add support for the shift modifier.

Because keyboard layouts vary, the shift-modifier `<s-…>` is only supported
for special keys (like `<up>` and `<home>`) and for ASCII lowercase where
we assume the shift-modifier just produces the matching uppercase character.
Even that's not universally true, since in Turkish `i` and `I` are not an
uppercase/lowercase pair, but Kakoune's default keyboard mappings already
assume en-US mappings for mnemonic purposes.

Mappings of the form `<s-x>` are normalized to `<X>` when `x` is an ASCII
character. `<backtab>` is removed, since we can now say `<s-tab>`.
This commit is contained in:
Tim Allen 2018-03-15 23:02:27 +11:00
parent d846400279
commit 50e422659b
9 changed files with 131 additions and 61 deletions

View File

@ -6,8 +6,8 @@ Usual keys are written using their ascii character, including capital
keys. Non printable keys use an alternate name, written between *<* keys. Non printable keys use an alternate name, written between *<*
and *>*, such as *<esc>* or *<del>*. Modified keys are written between and *>*, such as *<esc>* or *<del>*. Modified keys are written between
*<* and *>* as well, with the modifier specified as either *c* for *<* and *>* as well, with the modifier specified as either *c* for
Control, or *a* for Alt, followed by a *-* and the key (either its Control, *a* for Alt, or *s* for Shift, followed by a *-* and the key (either
name or ascii character), for example *<c-x>*, *<a-space>*, *<c-a-w>*. its name or ascii character), for example *<c-x>*, *<a-space>*, *<c-a-w>*.
In order to bind some keys to arbitrary ones, refer to <<mapping#,`:doc mapping`>> In order to bind some keys to arbitrary ones, refer to <<mapping#,`:doc mapping`>>
@ -95,18 +95,20 @@ it when pasting text.
== Movement == Movement
'word' is a sequence of alphanumeric characters or underscore, and 'WORD' 'word' is a sequence of alphanumeric characters or underscore, and 'WORD'
is a sequence of non whitespace characters is a sequence of non whitespace characters. Generally, a movement on it own
will move the selection to cover the text moved over, while holding down
the Shift modifier and moving will extend the selection instead.
*h*:: *h*, *<left>*::
select the character on the left of selection end select the character on the left of selection end
*j*:: *j*, *<down>*::
select the character below the selection end select the character below the selection end
*k*:: *k*, *<up>*::
select the character above the selection end select the character above the selection end
*l*:: *l*, *<right>*::
select the character on the right of selection end select the character on the right of selection end
*w*:: *w*::
@ -134,16 +136,10 @@ is a sequence of non whitespace characters
select to matching character, see the `matching_pairs` option select to matching character, see the `matching_pairs` option
in <<options#,`:doc options`>> in <<options#,`:doc options`>>
*M*::
extend selection to matching character
*x*:: *x*::
select line on which selection end lies (or next line when end lies select line on which selection end lies (or next line when end lies
on an end-of-line) on an end-of-line)
*X*::
similar to *x*, except the current selection is extended
*<a-x>*:: *<a-x>*::
expand selections to contain full lines (including end-of-lines) expand selections to contain full lines (including end-of-lines)
@ -154,10 +150,10 @@ is a sequence of non whitespace characters
*%*:: *%*::
select whole buffer select whole buffer
*<a-h>*:: *<a-h>*, *<home>*::
select to line begin select to line begin
*<a-l>*:: *<a-l>*, *<end>*::
select to line end select to line end
*/*:: */*::
@ -696,7 +692,7 @@ The following keys are recognized by this mode to help edition.
*<tab>*:: *<tab>*::
select next completion candidate select next completion candidate
*<backtab>*:: *<s-tab>*::
select previous completion candidate select previous completion candidate
*<c-r>*:: *<c-r>*::

View File

@ -65,15 +65,20 @@ be used:
Keys can also be wrapped in angle-brackets for consistency Keys can also be wrapped in angle-brackets for consistency
with the non-alphabetic keys below. with the non-alphabetic keys below.
*X*, *<X>*::
Holding down Shift while pressing the *x* key.
*<c-x>*:: *<c-x>*::
Holding down Control while pressing the *x* key. Holding down Control while pressing the *x* key.
*<a-x>*:: *<a-x>*::
Holding down Alt while pressing the *x* key. Holding down Alt while pressing the *x* key.
*<s-x>*, *X*, *<X>*, *<s-X>*::
Holding down Shift while pressing the *x* key.
*<s-x>*, *<s-X>* and *<X>* are treated as the same key. The *s-* modifier
only works with ASCII letters and cannot be used with other printable keys
(non-ASCII letters, digits, punctuation) because their shift behaviour
depends on your keyboard layout. The *s-* modifier _can_ be used with
special keys like *<up>* and *<tab>*.
*<c-a-x>*:: *<c-a-x>*::
Holding down Control and Alt while pressing the *x* key. Holding down Control and Alt while pressing the *x* key.
@ -92,9 +97,6 @@ be used:
*<tab>*:: *<tab>*::
The Tab key. The Tab key.
*<backtab>*::
The reverse-tab key. This is Shift-Tab on most keyboards.
*<backspace>*:: *<backspace>*::
The Backspace (delete to the left) key. The Backspace (delete to the left) key.
@ -110,3 +112,8 @@ be used:
*<f1>*, *<f2>*, ...*<f12>*:: *<f1>*, *<f2>*, ...*<f12>*::
Function keys. Function keys.
NOTE: Although Kakoune allows many key combinations to be mapped, not every
possible combination can be triggered. For example, due to limitations in
the way terminals handle control characters, mappings like *<c-s-a>* are
unlikely to work in Kakoune's terminal UI.

View File

@ -22,8 +22,16 @@ void on_assert_failed(const char* message);
on_assert_failed("assert failed \"" #__VA_ARGS__ \ on_assert_failed("assert failed \"" #__VA_ARGS__ \
"\" at " __FILE__ ":" TOSTRING(__LINE__)); \ "\" at " __FILE__ ":" TOSTRING(__LINE__)); \
} while (false) } while (false)
#define kak_expect_throw(exception_type, ...) try {\
__VA_ARGS__; \
on_assert_failed("expression \"" #__VA_ARGS__ \
"\" did not throw \"" #exception_type \
"\" at " __FILE__ ":" TOSTRING(__LINE__)); \
} catch (exception_type &err) {}
#else #else
#define kak_assert(...) do { (void)sizeof(__VA_ARGS__); } while(false) #define kak_assert(...) do { (void)sizeof(__VA_ARGS__); } while(false)
#define kak_expect_throw(_, ...) do { (void)sizeof(__VA_ARGS__); } while(false)
#endif #endif

View File

@ -461,15 +461,15 @@ public:
} }
else if (key == ctrl('w')) else if (key == ctrl('w'))
to_next_word_begin<Word>(m_cursor_pos, m_line); to_next_word_begin<Word>(m_cursor_pos, m_line);
else if (key == ctrlalt('w')) else if (key == ctrl(alt('w')))
to_next_word_begin<WORD>(m_cursor_pos, m_line); to_next_word_begin<WORD>(m_cursor_pos, m_line);
else if (key == ctrl('b')) else if (key == ctrl('b'))
to_prev_word_begin<Word>(m_cursor_pos, m_line); to_prev_word_begin<Word>(m_cursor_pos, m_line);
else if (key == ctrlalt('b')) else if (key == ctrl(alt('b')))
to_prev_word_begin<WORD>(m_cursor_pos, m_line); to_prev_word_begin<WORD>(m_cursor_pos, m_line);
else if (key == ctrl('e')) else if (key == ctrl('e'))
to_next_word_end<Word>(m_cursor_pos, m_line); to_next_word_end<Word>(m_cursor_pos, m_line);
else if (key == ctrlalt('e')) else if (key == ctrl(alt('e')))
to_next_word_end<WORD>(m_cursor_pos, m_line); to_next_word_end<WORD>(m_cursor_pos, m_line);
else if (key == ctrl('k')) else if (key == ctrl('k'))
m_line = m_line.substr(0_char, m_cursor_pos).str(); m_line = m_line.substr(0_char, m_cursor_pos).str();
@ -623,7 +623,7 @@ public:
it = std::find_if(m_choices.begin(), m_selected, match_filter); it = std::find_if(m_choices.begin(), m_selected, match_filter);
select(it); select(it);
} }
else if (key == Key::Up or key == Key::BackTab or else if (key == Key::Up or key == shift(Key::Tab) or
key == ctrl('p') or (not m_edit_filter and key == 'k')) key == ctrl('p') or (not m_edit_filter and key == 'k'))
{ {
ChoiceList::const_reverse_iterator selected(m_selected+1); ChoiceList::const_reverse_iterator selected(m_selected+1);
@ -837,9 +837,9 @@ public:
m_refresh_completion_pending = true; m_refresh_completion_pending = true;
} }
} }
else if (key == Key::Tab or key == Key::BackTab) // tab completion else if (key == Key::Tab or key == shift(Key::Tab)) // tab completion
{ {
const bool reverse = (key == Key::BackTab); const bool reverse = (key == shift(Key::Tab));
CandidateList& candidates = m_completions.candidates; CandidateList& candidates = m_completions.candidates;
// first try, we need to ask our completer for completions // first try, we need to ask our completer for completions
if (candidates.empty()) if (candidates.empty())
@ -1568,8 +1568,10 @@ InputHandler::ScopedForceNormal::~ScopedForceNormal()
static bool is_valid(Key key) static bool is_valid(Key key)
{ {
constexpr Key::Modifiers valid_mods = (Key::Modifiers::Control | Key::Modifiers::Alt | Key::Modifiers::Shift);
return key != Key::Invalid and return key != Key::Invalid and
((key.modifiers & ~Key::Modifiers::ControlAlt) or key.key <= 0x10FFFF); ((key.modifiers & ~valid_mods) or key.key <= 0x10FFFF);
} }
void InputHandler::handle_key(Key key) void InputHandler::handle_key(Key key)

View File

@ -11,6 +11,11 @@
namespace Kakoune namespace Kakoune
{ {
struct key_parse_error : runtime_error
{
using runtime_error::runtime_error;
};
static Key canonicalize_ifn(Key key) static Key canonicalize_ifn(Key key)
{ {
if (key.key > 0 and key.key < 27) if (key.key > 0 and key.key < 27)
@ -19,6 +24,22 @@ static Key canonicalize_ifn(Key key)
key.modifiers = Key::Modifiers::Control; key.modifiers = Key::Modifiers::Control;
key.key = key.key - 1 + 'a'; key.key = key.key - 1 + 'a';
} }
if (key.modifiers & Key::Modifiers::Shift)
{
if (is_basic_alpha(key.key))
{
// Shift + ASCII letters is just the uppercase letter.
key.modifiers &= ~Key::Modifiers::Shift;
key.key = to_upper(key.key);
}
else if (key.key < 0xD800 || key.key > 0xDFFF)
{
// Shift + any other printable character is not allowed.
throw key_parse_error(format("Shift modifier only works on special keys and lowercase ASCII, not '{}'", key.key));
}
}
return key; return key;
} }
@ -53,7 +74,6 @@ static constexpr KeyAndName keynamemap[] = {
{ "pagedown", Key::PageDown }, { "pagedown", Key::PageDown },
{ "home", Key::Home }, { "home", Key::Home },
{ "end", Key::End }, { "end", Key::End },
{ "backtab", Key::BackTab },
{ "del", Key::Delete }, { "del", Key::Delete },
{ "plus", '+' }, { "plus", '+' },
{ "minus", '-' }, { "minus", '-' },
@ -85,16 +105,17 @@ KeyList parse_keys(StringView str)
for (auto dash = find(desc, '-'); dash != desc.end(); dash = find(desc, '-')) for (auto dash = find(desc, '-'); dash != desc.end(); dash = find(desc, '-'))
{ {
if (dash != desc.begin() + 1) if (dash != desc.begin() + 1)
throw runtime_error(format("unable to parse modifier in '{}'", throw key_parse_error(format("unable to parse modifier in '{}'",
full_desc)); full_desc));
switch(to_lower(desc[0_byte])) switch(to_lower(desc[0_byte]))
{ {
case 'c': modifier |= Key::Modifiers::Control; break; case 'c': modifier |= Key::Modifiers::Control; break;
case 'a': modifier |= Key::Modifiers::Alt; break; case 'a': modifier |= Key::Modifiers::Alt; break;
case 's': modifier |= Key::Modifiers::Shift; break;
default: default:
throw runtime_error(format("unable to parse modifier in '{}'", throw key_parse_error(format("unable to parse modifier in '{}'",
full_desc)); full_desc));
} }
desc = StringView{dash+1, desc.end()}; desc = StringView{dash+1, desc.end()};
} }
@ -104,17 +125,17 @@ KeyList parse_keys(StringView str)
if (name_it != std::end(keynamemap)) if (name_it != std::end(keynamemap))
result.push_back(canonicalize_ifn({ modifier, name_it->key })); result.push_back(canonicalize_ifn({ modifier, name_it->key }));
else if (desc.char_length() == 1) else if (desc.char_length() == 1)
result.emplace_back(modifier, desc[0_char]); result.push_back(canonicalize_ifn({ modifier, desc[0_char] }));
else if (to_lower(desc[0_byte]) == 'f' and desc.length() <= 3) else if (to_lower(desc[0_byte]) == 'f' and desc.length() <= 3)
{ {
int val = str_to_int(desc.substr(1_byte)); int val = str_to_int(desc.substr(1_byte));
if (val >= 1 and val <= 12) if (val >= 1 and val <= 12)
result.emplace_back(modifier, Key::F1 + (val - 1)); result.emplace_back(modifier, Key::F1 + (val - 1));
else else
throw runtime_error("only F1 through F12 are supported"); throw key_parse_error(format("only F1 through F12 are supported, not '{}'", desc));
} }
else else
throw runtime_error("unable to parse " + throw key_parse_error("unable to parse " +
StringView{it.base(), end_it.base()+1}); StringView{it.base(), end_it.base()+1});
it = end_it; it = end_it;
@ -165,13 +186,10 @@ String key_to_str(Key key)
else else
res = String{key.key}; res = String{key.key};
switch (key.modifiers) if (key.modifiers & Key::Modifiers::Shift) { res = "s-" + res; named = true; }
{ if (key.modifiers & Key::Modifiers::Alt) { res = "a-" + res; named = true; }
case Key::Modifiers::Control: res = "c-" + res; named = true; break; if (key.modifiers & Key::Modifiers::Control) { res = "c-" + res; named = true; }
case Key::Modifiers::Alt: res = "a-" + res; named = true; break;
case Key::Modifiers::ControlAlt: res = "c-a-" + res; named = true; break;
default: break;
}
if (named) if (named)
res = StringView{'<'} + res + StringView{'>'}; res = StringView{'<'} + res + StringView{'>'};
return res; return res;
@ -182,8 +200,10 @@ UnitTest test_keys{[]()
KeyList keys{ KeyList keys{
{ ' ' }, { ' ' },
{ 'c' }, { 'c' },
{ Key::Modifiers::Alt, 'j' }, { Key::Up },
{ Key::Modifiers::Control, 'r' } alt('j'),
ctrl('r'),
shift(Key::Up),
}; };
String keys_as_str; String keys_as_str;
for (auto& key : keys) for (auto& key : keys)
@ -191,7 +211,28 @@ UnitTest test_keys{[]()
auto parsed_keys = parse_keys(keys_as_str); auto parsed_keys = parse_keys(keys_as_str);
kak_assert(keys == parsed_keys); kak_assert(keys == parsed_keys);
kak_assert(ConstArrayView<Key>{parse_keys("a<c-a-b>c")} == kak_assert(ConstArrayView<Key>{parse_keys("a<c-a-b>c")} ==
ConstArrayView<Key>{'a', {Key::Modifiers::ControlAlt, 'b'}, 'c'}); ConstArrayView<Key>{'a', ctrl(alt({'b'})), 'c'});
kak_assert(parse_keys("x") == KeyList{ {'x'} });
kak_assert(parse_keys("<x>") == KeyList{ {'x'} });
kak_assert(parse_keys("<s-x>") == KeyList{ {'X'} });
kak_assert(parse_keys("<s-X>") == KeyList{ {'X'} });
kak_assert(parse_keys("<X>") == KeyList{ {'X'} });
kak_assert(parse_keys("X") == KeyList{ {'X'} });
kak_assert(parse_keys("<s-up>") == KeyList{ shift({Key::Up}) });
kak_assert(parse_keys("<s-tab>") == KeyList{ shift({Key::Tab}) });
kak_assert(key_to_str(shift({Key::Tab})) == "<s-tab>");
kak_expect_throw(key_parse_error, parse_keys("<-x>"));
kak_expect_throw(key_parse_error, parse_keys("<xy-z>"));
kak_expect_throw(key_parse_error, parse_keys("<x-y>"));
kak_expect_throw(key_parse_error, parse_keys("<s-/>"));
kak_expect_throw(key_parse_error, parse_keys("<s-ë>"));
kak_expect_throw(key_parse_error, parse_keys("<s-lt>"));
kak_expect_throw(key_parse_error, parse_keys("<f99>"));
kak_expect_throw(key_parse_error, parse_keys("<backtab>"));
kak_expect_throw(key_parse_error, parse_keys("<invalidkey>"));
}}; }};
} }

View File

@ -19,17 +19,17 @@ struct Key
None = 0, None = 0,
Control = 1 << 0, Control = 1 << 0,
Alt = 1 << 1, Alt = 1 << 1,
ControlAlt = Control | Alt, Shift = 1 << 2,
MousePress = 1 << 2, MousePress = 1 << 3,
MouseRelease = 1 << 3, MouseRelease = 1 << 4,
MousePos = 1 << 4, MousePos = 1 << 5,
MouseWheelDown = 1 << 5, MouseWheelDown = 1 << 6,
MouseWheelUp = 1 << 6, MouseWheelUp = 1 << 7,
MouseEvent = MousePress | MouseRelease | MousePos | MouseEvent = MousePress | MouseRelease | MousePos |
MouseWheelDown | MouseWheelUp, MouseWheelDown | MouseWheelUp,
Resize = 1 << 7, Resize = 1 << 8,
}; };
enum NamedKey : Codepoint enum NamedKey : Codepoint
{ {
@ -47,7 +47,6 @@ struct Key
Home, Home,
End, End,
Tab, Tab,
BackTab,
F1, F1,
F2, F2,
F3, F3,
@ -97,6 +96,10 @@ class StringView;
KeyList parse_keys(StringView str); KeyList parse_keys(StringView str);
String key_to_str(Key key); String key_to_str(Key key);
constexpr Key shift(Key key)
{
return { key.modifiers | Key::Modifiers::Shift, key.key };
}
constexpr Key alt(Key key) constexpr Key alt(Key key)
{ {
return { key.modifiers | Key::Modifiers::Alt, key.key }; return { key.modifiers | Key::Modifiers::Alt, key.key };
@ -105,10 +108,6 @@ constexpr Key ctrl(Key key)
{ {
return { key.modifiers | Key::Modifiers::Control, key.key }; return { key.modifiers | Key::Modifiers::Control, key.key };
} }
constexpr Key ctrlalt(Key key)
{
return { key.modifiers | Key::Modifiers::ControlAlt, key.key };
}
constexpr Codepoint encode_coord(DisplayCoord coord) { return (Codepoint)(((int)coord.line << 16) | ((int)coord.column & 0x0000FFFF)); } constexpr Codepoint encode_coord(DisplayCoord coord) { return (Codepoint)(((int)coord.line << 16) | ((int)coord.column & 0x0000FFFF)); }

View File

@ -53,7 +53,8 @@ static const char* startup_info =
" * 'x' will only jump to next line if full line is already selected\n" " * 'x' will only jump to next line if full line is already selected\n"
" * WORD text object moved to <a-w> instead of W for consistency\n" " * WORD text object moved to <a-w> instead of W for consistency\n"
" * rotate main selection moved to ), rotate content to <a-)>, ( for backward\n" " * rotate main selection moved to ), rotate content to <a-)>, ( for backward\n"
" * faces are now scoped, set-face command takes an additional scope parameter\n"; " * faces are now scoped, set-face command takes an additional scope parameter\n"
" * <backtab> key is gone, use <s-tab> instead\n";
struct startup_error : runtime_error struct startup_error : runtime_error
{ {

View File

@ -570,15 +570,24 @@ Optional<Key> NCursesUI::get_next_key()
{ {
case KEY_BACKSPACE: case 127: return {Key::Backspace}; case KEY_BACKSPACE: case 127: return {Key::Backspace};
case KEY_DC: return {Key::Delete}; case KEY_DC: return {Key::Delete};
case KEY_SDC: return shift(Key::Delete);
case KEY_UP: return {Key::Up}; case KEY_UP: return {Key::Up};
case KEY_SR: return shift(Key::Up);
case KEY_DOWN: return {Key::Down}; case KEY_DOWN: return {Key::Down};
case KEY_SF: return shift(Key::Down);
case KEY_LEFT: return {Key::Left}; case KEY_LEFT: return {Key::Left};
case KEY_SLEFT: return shift(Key::Left);
case KEY_RIGHT: return {Key::Right}; case KEY_RIGHT: return {Key::Right};
case KEY_SRIGHT: return shift(Key::Right);
case KEY_PPAGE: return {Key::PageUp}; case KEY_PPAGE: return {Key::PageUp};
case KEY_SPREVIOUS: return shift(Key::PageUp);
case KEY_NPAGE: return {Key::PageDown}; case KEY_NPAGE: return {Key::PageDown};
case KEY_SNEXT: return shift(Key::PageDown);
case KEY_HOME: return {Key::Home}; case KEY_HOME: return {Key::Home};
case KEY_SHOME: return shift(Key::Home);
case KEY_END: return {Key::End}; case KEY_END: return {Key::End};
case KEY_BTAB: return {Key::BackTab}; case KEY_SEND: return shift(Key::End);
case KEY_BTAB: return shift(Key::Tab);
case KEY_RESIZE: return resize(m_dimensions); case KEY_RESIZE: return resize(m_dimensions);
} }

View File

@ -2062,6 +2062,11 @@ static const HashMap<Key, NormalCmd, MemoryDomain::Undefined, KeymapBackend> key
{ {'K'}, {"extend up", move<LineCount, Backward, SelectMode::Extend>} }, { {'K'}, {"extend up", move<LineCount, Backward, SelectMode::Extend>} },
{ {'L'}, {"extend right", move<CharCount, Forward, SelectMode::Extend>} }, { {'L'}, {"extend right", move<CharCount, Forward, SelectMode::Extend>} },
{ shift(Key::Left), {"extend left", move<CharCount, Backward, SelectMode::Extend>} },
{ shift(Key::Down), {"extend down", move<LineCount, Forward, SelectMode::Extend>} },
{ shift(Key::Up), {"extend up", move<LineCount, Backward, SelectMode::Extend>} },
{ shift(Key::Right), {"extend right", move<CharCount, Forward, SelectMode::Extend>} },
{ {'t'}, {"select to next character", select_to_next_char<SelectFlags::None>} }, { {'t'}, {"select to next character", select_to_next_char<SelectFlags::None>} },
{ {'f'}, {"select to next character included", select_to_next_char<SelectFlags::Inclusive>} }, { {'f'}, {"select to next character included", select_to_next_char<SelectFlags::Inclusive>} },
{ {'T'}, {"extend to next character", select_to_next_char<SelectFlags::Extend>} }, { {'T'}, {"extend to next character", select_to_next_char<SelectFlags::Extend>} },
@ -2140,9 +2145,11 @@ static const HashMap<Key, NormalCmd, MemoryDomain::Undefined, KeymapBackend> key
{ {alt('l')}, {"select to line end", repeated<select<SelectMode::Replace, select_to_line_end<false>>>} }, { {alt('l')}, {"select to line end", repeated<select<SelectMode::Replace, select_to_line_end<false>>>} },
{ {Key::End}, {"select to line end", repeated<select<SelectMode::Replace, select_to_line_end<false>>>} }, { {Key::End}, {"select to line end", repeated<select<SelectMode::Replace, select_to_line_end<false>>>} },
{ {alt('L')}, {"extend to line end", repeated<select<SelectMode::Extend, select_to_line_end<false>>>} }, { {alt('L')}, {"extend to line end", repeated<select<SelectMode::Extend, select_to_line_end<false>>>} },
{ shift(Key::End), {"extend to line end", repeated<select<SelectMode::Extend, select_to_line_end<false>>>} },
{ {alt('h')}, {"select to line begin", repeated<select<SelectMode::Replace, select_to_line_begin<false>>>} }, { {alt('h')}, {"select to line begin", repeated<select<SelectMode::Replace, select_to_line_begin<false>>>} },
{ {Key::Home}, {"select to line begin", repeated<select<SelectMode::Replace, select_to_line_begin<false>>>} }, { {Key::Home}, {"select to line begin", repeated<select<SelectMode::Replace, select_to_line_begin<false>>>} },
{ {alt('H')}, {"extend to line begin", repeated<select<SelectMode::Extend, select_to_line_begin<false>>>} }, { {alt('H')}, {"extend to line begin", repeated<select<SelectMode::Extend, select_to_line_begin<false>>>} },
{ shift(Key::Home), {"extend to line begin", repeated<select<SelectMode::Extend, select_to_line_begin<false>>>} },
{ {'x'}, {"select line", repeated<select<SelectMode::Replace, select_line>>} }, { {'x'}, {"select line", repeated<select<SelectMode::Replace, select_line>>} },
{ {'X'}, {"extend line", repeated<select<SelectMode::Extend, select_line>>} }, { {'X'}, {"extend line", repeated<select<SelectMode::Extend, select_line>>} },