diff --git a/doc/pages/keys.asciidoc b/doc/pages/keys.asciidoc index 8de96aa7..7b988b05 100644 --- a/doc/pages/keys.asciidoc +++ b/doc/pages/keys.asciidoc @@ -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 ** or **. 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 **, **, **. +Control, *a* for Alt, or *s* for Shift, followed by a *-* and the key (either +its name or ascii character), for example **, **, **. In order to bind some keys to arbitrary ones, refer to <> @@ -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*, **:: select the character on the left of selection end -*j*:: +*j*, **:: select the character below the selection end -*k*:: +*k*, **:: select the character above the selection end -*l*:: +*l*, **:: 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 <> -*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 - **:: 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 to line begin -**:: +**, **:: select to line end */*:: @@ -696,7 +692,7 @@ The following keys are recognized by this mode to help edition. **:: select next completion candidate -**:: +**:: select previous completion candidate **:: diff --git a/doc/pages/mapping.asciidoc b/doc/pages/mapping.asciidoc index 3ebc62c2..c21bd90b 100644 --- a/doc/pages/mapping.asciidoc +++ b/doc/pages/mapping.asciidoc @@ -65,15 +65,20 @@ be used: Keys can also be wrapped in angle-brackets for consistency with the non-alphabetic keys below. -*X*, **:: - Holding down Shift while pressing the *x* key. - **:: Holding down Control while pressing the *x* key. **:: Holding down Alt while pressing the *x* key. +**, *X*, **, **:: + Holding down Shift while pressing the *x* key. + **, ** and ** 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 ** and **. + **:: Holding down Control and Alt while pressing the *x* key. @@ -92,9 +97,6 @@ be used: **:: The Tab key. -**:: - The reverse-tab key. This is Shift-Tab on most keyboards. - **:: The Backspace (delete to the left) key. @@ -110,3 +112,8 @@ be used: **, **, ...**:: 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 ** are +unlikely to work in Kakoune's terminal UI. diff --git a/src/assert.hh b/src/assert.hh index 3d807ea6..ff642c9a 100644 --- a/src/assert.hh +++ b/src/assert.hh @@ -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 diff --git a/src/input_handler.cc b/src/input_handler.cc index 2ee1d40d..1466ae02 100644 --- a/src/input_handler.cc +++ b/src/input_handler.cc @@ -461,15 +461,15 @@ public: } else if (key == ctrl('w')) to_next_word_begin(m_cursor_pos, m_line); - else if (key == ctrlalt('w')) + else if (key == ctrl(alt('w'))) to_next_word_begin(m_cursor_pos, m_line); else if (key == ctrl('b')) to_prev_word_begin(m_cursor_pos, m_line); - else if (key == ctrlalt('b')) + else if (key == ctrl(alt('b'))) to_prev_word_begin(m_cursor_pos, m_line); else if (key == ctrl('e')) to_next_word_end(m_cursor_pos, m_line); - else if (key == ctrlalt('e')) + else if (key == ctrl(alt('e'))) to_next_word_end(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) diff --git a/src/keys.cc b/src/keys.cc index 412b1c63..e4318707 100644 --- a/src/keys.cc +++ b/src/keys.cc @@ -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,16 +105,17 @@ 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 '{}'", - full_desc)); + 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 '{}'", - full_desc)); + 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{parse_keys("ac")} == - ConstArrayView{'a', {Key::Modifiers::ControlAlt, 'b'}, 'c'}); + ConstArrayView{'a', ctrl(alt({'b'})), 'c'}); + + kak_assert(parse_keys("x") == KeyList{ {'x'} }); + kak_assert(parse_keys("") == KeyList{ {'x'} }); + kak_assert(parse_keys("") == KeyList{ {'X'} }); + kak_assert(parse_keys("") == KeyList{ {'X'} }); + kak_assert(parse_keys("") == KeyList{ {'X'} }); + kak_assert(parse_keys("X") == KeyList{ {'X'} }); + kak_assert(parse_keys("") == KeyList{ shift({Key::Up}) }); + kak_assert(parse_keys("") == KeyList{ shift({Key::Tab}) }); + + kak_assert(key_to_str(shift({Key::Tab})) == ""); + + kak_expect_throw(key_parse_error, parse_keys("<-x>")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); + kak_expect_throw(key_parse_error, parse_keys("")); }}; } diff --git a/src/keys.hh b/src/keys.hh index e1e6ff35..9b632dcb 100644 --- a/src/keys.hh +++ b/src/keys.hh @@ -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)); } diff --git a/src/main.cc b/src/main.cc index 53986525..e152fa94 100644 --- a/src/main.cc +++ b/src/main.cc @@ -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 instead of W for consistency\n" " * rotate main selection moved to ), rotate content to , ( 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" +" * key is gone, use instead\n"; struct startup_error : runtime_error { diff --git a/src/ncurses_ui.cc b/src/ncurses_ui.cc index 42023ebb..e6863945 100644 --- a/src/ncurses_ui.cc +++ b/src/ncurses_ui.cc @@ -570,15 +570,24 @@ Optional 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); } diff --git a/src/normal.cc b/src/normal.cc index 4662a23d..3d4a266a 100644 --- a/src/normal.cc +++ b/src/normal.cc @@ -2062,6 +2062,11 @@ static const HashMap key { {'K'}, {"extend up", move} }, { {'L'}, {"extend right", move} }, + { shift(Key::Left), {"extend left", move} }, + { shift(Key::Down), {"extend down", move} }, + { shift(Key::Up), {"extend up", move} }, + { shift(Key::Right), {"extend right", move} }, + { {'t'}, {"select to next character", select_to_next_char} }, { {'f'}, {"select to next character included", select_to_next_char} }, { {'T'}, {"extend to next character", select_to_next_char} }, @@ -2140,9 +2145,11 @@ static const HashMap key { {alt('l')}, {"select to line end", repeated>>} }, { {Key::End}, {"select to line end", repeated>>} }, { {alt('L')}, {"extend to line end", repeated>>} }, + { shift(Key::End), {"extend to line end", repeated>>} }, { {alt('h')}, {"select to line begin", repeated>>} }, { {Key::Home}, {"select to line begin", repeated>>} }, { {alt('H')}, {"extend to line begin", repeated>>} }, + { shift(Key::Home), {"extend to line begin", repeated>>} }, { {'x'}, {"select line", repeated>} }, { {'X'}, {"extend line", repeated>} },