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 *<*
and *>*, such as *<esc>* or *<del>*. Modified keys are written between
*<* and *>* as well, with the modifier specified as either *c* for
Control, or *a* for Alt, followed by a *-* and the key (either its
name or ascii character), for example *<c-x>*, *<a-space>*, *<c-a-w>*.
Control, *a* for Alt, or *s* for Shift, followed by a *-* and the key (either
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`>>
@ -95,18 +95,20 @@ it when pasting text.
== Movement
'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
*j*::
*j*, *<down>*::
select the character below the selection end
*k*::
*k*, *<up>*::
select the character above the selection end
*l*::
*l*, *<right>*::
select the character on the right of selection end
*w*::
@ -134,16 +136,10 @@ is a sequence of non whitespace characters
select to matching character, see the `matching_pairs` option
in <<options#,`:doc options`>>
*M*::
extend selection to matching character
*x*::
select line on which selection end lies (or next line when end lies
on an end-of-line)
*X*::
similar to *x*, except the current selection is extended
*<a-x>*::
expand selections to contain full lines (including end-of-lines)
@ -154,10 +150,10 @@ is a sequence of non whitespace characters
*%*::
select whole buffer
*<a-h>*::
*<a-h>*, *<home>*::
select to line begin
*<a-l>*::
*<a-l>*, *<end>*::
select to line end
*/*::
@ -696,7 +692,7 @@ The following keys are recognized by this mode to help edition.
*<tab>*::
select next completion candidate
*<backtab>*::
*<s-tab>*::
select previous completion candidate
*<c-r>*::

View File

@ -65,15 +65,20 @@ be used:
Keys can also be wrapped in angle-brackets for consistency
with the non-alphabetic keys below.
*X*, *<X>*::
Holding down Shift while pressing the *x* key.
*<c-x>*::
Holding down Control while pressing the *x* key.
*<a-x>*::
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>*::
Holding down Control and Alt while pressing the *x* key.
@ -92,9 +97,6 @@ be used:
*<tab>*::
The Tab key.
*<backtab>*::
The reverse-tab key. This is Shift-Tab on most keyboards.
*<backspace>*::
The Backspace (delete to the left) key.
@ -110,3 +112,8 @@ be used:
*<f1>*, *<f2>*, ...*<f12>*::
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__ \
"\" at " __FILE__ ":" TOSTRING(__LINE__)); \
} 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
#define kak_assert(...) do { (void)sizeof(__VA_ARGS__); } while(false)
#define kak_expect_throw(_, ...) do { (void)sizeof(__VA_ARGS__); } while(false)
#endif

View File

@ -461,15 +461,15 @@ public:
}
else if (key == ctrl('w'))
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);
else if (key == ctrl('b'))
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);
else if (key == ctrl('e'))
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);
else if (key == ctrl('k'))
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);
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'))
{
ChoiceList::const_reverse_iterator selected(m_selected+1);
@ -837,9 +837,9 @@ public:
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;
// first try, we need to ask our completer for completions
if (candidates.empty())
@ -1568,8 +1568,10 @@ InputHandler::ScopedForceNormal::~ScopedForceNormal()
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
((key.modifiers & ~Key::Modifiers::ControlAlt) or key.key <= 0x10FFFF);
((key.modifiers & ~valid_mods) or key.key <= 0x10FFFF);
}
void InputHandler::handle_key(Key key)

View File

@ -11,6 +11,11 @@
namespace Kakoune
{
struct key_parse_error : runtime_error
{
using runtime_error::runtime_error;
};
static Key canonicalize_ifn(Key key)
{
if (key.key > 0 and key.key < 27)
@ -19,6 +24,22 @@ static Key canonicalize_ifn(Key key)
key.modifiers = Key::Modifiers::Control;
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;
}
@ -53,7 +74,6 @@ static constexpr KeyAndName keynamemap[] = {
{ "pagedown", Key::PageDown },
{ "home", Key::Home },
{ "end", Key::End },
{ "backtab", Key::BackTab },
{ "del", Key::Delete },
{ "plus", '+' },
{ "minus", '-' },
@ -85,15 +105,16 @@ KeyList parse_keys(StringView str)
for (auto dash = find(desc, '-'); dash != desc.end(); dash = find(desc, '-'))
{
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));
switch(to_lower(desc[0_byte]))
{
case 'c': modifier |= Key::Modifiers::Control; break;
case 'a': modifier |= Key::Modifiers::Alt; break;
case 's': modifier |= Key::Modifiers::Shift; break;
default:
throw runtime_error(format("unable to parse modifier in '{}'",
throw key_parse_error(format("unable to parse modifier in '{}'",
full_desc));
}
desc = StringView{dash+1, desc.end()};
@ -104,17 +125,17 @@ KeyList parse_keys(StringView str)
if (name_it != std::end(keynamemap))
result.push_back(canonicalize_ifn({ modifier, name_it->key }));
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)
{
int val = str_to_int(desc.substr(1_byte));
if (val >= 1 and val <= 12)
result.emplace_back(modifier, Key::F1 + (val - 1));
else
throw runtime_error("only F1 through F12 are supported");
throw key_parse_error(format("only F1 through F12 are supported, not '{}'", desc));
}
else
throw runtime_error("unable to parse " +
throw key_parse_error("unable to parse " +
StringView{it.base(), end_it.base()+1});
it = end_it;
@ -165,13 +186,10 @@ String key_to_str(Key key)
else
res = String{key.key};
switch (key.modifiers)
{
case Key::Modifiers::Control: res = "c-" + res; named = true; break;
case Key::Modifiers::Alt: res = "a-" + res; named = true; break;
case Key::Modifiers::ControlAlt: res = "c-a-" + res; named = true; break;
default: break;
}
if (key.modifiers & Key::Modifiers::Shift) { res = "s-" + res; named = true; }
if (key.modifiers & Key::Modifiers::Alt) { res = "a-" + res; named = true; }
if (key.modifiers & Key::Modifiers::Control) { res = "c-" + res; named = true; }
if (named)
res = StringView{'<'} + res + StringView{'>'};
return res;
@ -182,8 +200,10 @@ UnitTest test_keys{[]()
KeyList keys{
{ ' ' },
{ 'c' },
{ Key::Modifiers::Alt, 'j' },
{ Key::Modifiers::Control, 'r' }
{ Key::Up },
alt('j'),
ctrl('r'),
shift(Key::Up),
};
String keys_as_str;
for (auto& key : keys)
@ -191,7 +211,28 @@ UnitTest test_keys{[]()
auto parsed_keys = parse_keys(keys_as_str);
kak_assert(keys == parsed_keys);
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,
Control = 1 << 0,
Alt = 1 << 1,
ControlAlt = Control | Alt,
Shift = 1 << 2,
MousePress = 1 << 2,
MouseRelease = 1 << 3,
MousePos = 1 << 4,
MouseWheelDown = 1 << 5,
MouseWheelUp = 1 << 6,
MousePress = 1 << 3,
MouseRelease = 1 << 4,
MousePos = 1 << 5,
MouseWheelDown = 1 << 6,
MouseWheelUp = 1 << 7,
MouseEvent = MousePress | MouseRelease | MousePos |
MouseWheelDown | MouseWheelUp,
Resize = 1 << 7,
Resize = 1 << 8,
};
enum NamedKey : Codepoint
{
@ -47,7 +47,6 @@ struct Key
Home,
End,
Tab,
BackTab,
F1,
F2,
F3,
@ -97,6 +96,10 @@ class StringView;
KeyList parse_keys(StringView str);
String key_to_str(Key key);
constexpr Key shift(Key key)
{
return { key.modifiers | Key::Modifiers::Shift, key.key };
}
constexpr Key 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 };
}
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)); }

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"
" * 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"
" * 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
{

View File

@ -570,15 +570,24 @@ Optional<Key> NCursesUI::get_next_key()
{
case KEY_BACKSPACE: case 127: return {Key::Backspace};
case KEY_DC: return {Key::Delete};
case KEY_SDC: return shift(Key::Delete);
case KEY_UP: return {Key::Up};
case KEY_SR: return shift(Key::Up);
case KEY_DOWN: return {Key::Down};
case KEY_SF: return shift(Key::Down);
case KEY_LEFT: return {Key::Left};
case KEY_SLEFT: return shift(Key::Left);
case KEY_RIGHT: return {Key::Right};
case KEY_SRIGHT: return shift(Key::Right);
case KEY_PPAGE: return {Key::PageUp};
case KEY_SPREVIOUS: return shift(Key::PageUp);
case KEY_NPAGE: return {Key::PageDown};
case KEY_SNEXT: return shift(Key::PageDown);
case KEY_HOME: return {Key::Home};
case KEY_SHOME: return shift(Key::Home);
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);
}

View File

@ -2062,6 +2062,11 @@ static const HashMap<Key, NormalCmd, MemoryDomain::Undefined, KeymapBackend> key
{ {'K'}, {"extend up", move<LineCount, Backward, 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>} },
{ {'f'}, {"select to next character included", select_to_next_char<SelectFlags::Inclusive>} },
{ {'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>>>} },
{ {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>>>} },
{ 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>>>} },
{ {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>>>} },
{ 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'}, {"extend line", repeated<select<SelectMode::Extend, select_line>>} },