Files
sousa-gecko/widget/tests/gtest/TestNativeKeyBindings.cpp
Stephen A Pohl 75815c9be0 Bug 2049117: Let Shift+Option+Arrow selection cross paragraph boundaries on macOS. r=mac-reviewers,bradwerth
On macOS, Shift+Option+ArrowDown/ArrowUp is interpreted by AppKit as a single moveParagraphForwardAndModifySelection: / moveParagraphBackwardAndModifySelection: selector, which we mapped to a single SelectEndParagraph / SelectBeginParagraph command. That command extends the selection to the current paragraph's edge but cannot advance past it, so once the selection focus sits at a paragraph boundary, repeated presses stop extending the selection and only a single paragraph can be selected.

The non-selecting Option+ArrowDown/ArrowUp binding already avoids this by emitting a character move (moveForward: / moveBackward:) before the paragraph move so the caret first steps across the boundary. Mirror that for the selecting variants by expanding the two moveParagraph...AndModifySelection: selectors into a character-selection command followed by the paragraph-selection command. This keeps them distinct from the Emacs-style Ctrl+Shift+E / Ctrl+Shift+A bindings, whose native selectors intentionally do not cross paragraph boundaries.

Differential Revision: https://phabricator.services.mozilla.com/D315279
2026-08-05 03:16:36 +00:00

381 lines
12 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifdef XP_MACOSX
# include "NativeKeyBindings.h"
# include "gtest/gtest.h"
# include "mozilla/EventForwards.h"
# include "mozilla/Maybe.h"
# include "mozilla/NativeKeyBindingsType.h"
# include "mozilla/TextEvents.h"
// The tests here check that you get what you expect in terms of edit commands
// when you feed key presses through GetEditCommandsForTests.
//
// Basically, you specify a key combination, that gets mapped to an Obj-C
// method selector in NativeKeyBindings::GetEditCommandsForTests, which
// is then converted into a set of Gecko edit commands by
// NativeKeyBindings::AppendEditCommandsForSelector. Those edit commands
// are checked against an expected set in the test case.
namespace mozilla {
using widget::NativeKeyBindings;
struct NativeKeyBindingsTestCase {
uint32_t mKeyCode;
KeyNameIndex mKeyNameIndex;
Modifiers mModifiers;
// For KEY_NAME_INDEX_USE_STRING events.
uint32_t mPseudoCharCode;
NativeKeyBindingsType mEditorType;
nsTArray<Command> mExpectedCommands;
const char* mDescription;
};
static nsTArray<CommandInt> GetEditCommands(
const NativeKeyBindingsTestCase& aTest) {
WidgetKeyboardEvent event(true, eKeyDown, nullptr);
event.mKeyCode = aTest.mKeyCode;
event.mKeyNameIndex = aTest.mKeyNameIndex;
event.mModifiers = aTest.mModifiers;
event.mPseudoCharCode = aTest.mPseudoCharCode;
event.mFlags.mIsSynthesizedForTests = true;
nsTArray<CommandInt> commands;
NativeKeyBindings::GetEditCommandsForTests(aTest.mEditorType, event,
Nothing(), commands);
return commands;
}
static void CheckCommands(const NativeKeyBindingsTestCase& aTest) {
nsTArray<CommandInt> commands = GetEditCommands(aTest);
ASSERT_EQ(commands.Length(), aTest.mExpectedCommands.Length())
<< aTest.mDescription;
for (size_t i = 0; i < commands.Length(); i++) {
ASSERT_EQ(commands[i], static_cast<CommandInt>(aTest.mExpectedCommands[i]))
<< aTest.mDescription << " command[" << i << "]";
}
}
TEST(NativeKeyBindings, MetaShiftArrowUp)
{
NativeKeyBindingsTestCase test{
NS_VK_UP,
KEY_NAME_INDEX_ArrowUp,
MODIFIER_META | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectTop},
"Meta+Shift+ArrowUp should select to beginning of document"};
CheckCommands(test);
}
TEST(NativeKeyBindings, MetaShiftArrowDown)
{
NativeKeyBindingsTestCase test{
NS_VK_DOWN,
KEY_NAME_INDEX_ArrowDown,
MODIFIER_META | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectBottom},
"Meta+Shift+ArrowDown should select to end of document"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltArrowLeftWordMovement)
{
NativeKeyBindingsTestCase test{NS_VK_LEFT,
KEY_NAME_INDEX_ArrowLeft,
MODIFIER_ALT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::WordPrevious},
"Alt+ArrowLeft should move word left"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltArrowRightWordMovement)
{
NativeKeyBindingsTestCase test{NS_VK_RIGHT,
KEY_NAME_INDEX_ArrowRight,
MODIFIER_ALT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::WordNext},
"Alt+ArrowRight should move word right"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowLeftWordSelection)
{
NativeKeyBindingsTestCase test{NS_VK_LEFT,
KEY_NAME_INDEX_ArrowLeft,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectWordPrevious},
"Alt+Shift+ArrowLeft should select word left"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowRightWordSelection)
{
NativeKeyBindingsTestCase test{
NS_VK_RIGHT,
KEY_NAME_INDEX_ArrowRight,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectWordNext},
"Alt+Shift+ArrowRight should select word right"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltMetaArrowLeftNoOp)
{
NativeKeyBindingsTestCase test{
NS_VK_LEFT,
KEY_NAME_INDEX_ArrowLeft,
MODIFIER_ALT | MODIFIER_META,
0,
NativeKeyBindingsType::MultiLineEditor,
{},
"Alt+Meta+ArrowLeft should produce no commands"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltMetaArrowRightNoOp)
{
NativeKeyBindingsTestCase test{
NS_VK_RIGHT,
KEY_NAME_INDEX_ArrowRight,
MODIFIER_ALT | MODIFIER_META,
0,
NativeKeyBindingsType::MultiLineEditor,
{},
"Alt+Meta+ArrowRight should produce no commands"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltArrowUpParagraphMovement)
{
NativeKeyBindingsTestCase test{
NS_VK_UP,
KEY_NAME_INDEX_ArrowUp,
MODIFIER_ALT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::CharPrevious, Command::BeginParagraph},
"Alt+ArrowUp should move to beginning of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltArrowDownParagraphMovement)
{
NativeKeyBindingsTestCase test{
NS_VK_DOWN,
KEY_NAME_INDEX_ArrowDown,
MODIFIER_ALT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::CharNext, Command::EndParagraph},
"Alt+ArrowDown should move to end of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowUpParagraphSelection)
{
NativeKeyBindingsTestCase test{
NS_VK_UP,
KEY_NAME_INDEX_ArrowUp,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectCharPrevious, Command::SelectBeginParagraph},
"Alt+Shift+ArrowUp should select to beginning of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowDownParagraphSelection)
{
NativeKeyBindingsTestCase test{
NS_VK_DOWN,
KEY_NAME_INDEX_ArrowDown,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectCharNext, Command::SelectEndParagraph},
"Alt+Shift+ArrowDown should select to end of paragraph"};
CheckCommands(test);
}
// The paragraph selection selectors are expanded in
// AppendEditCommandsForSelector rather than mapped in the per-editor-type
// table, so check that every editor type gets the same expansion.
TEST(NativeKeyBindings, AltShiftArrowUpParagraphSelectionRichTextEditor)
{
NativeKeyBindingsTestCase test{
NS_VK_UP,
KEY_NAME_INDEX_ArrowUp,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::RichTextEditor,
{Command::SelectCharPrevious, Command::SelectBeginParagraph},
"Alt+Shift+ArrowUp should select to beginning of paragraph in a rich "
"text editor"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowDownParagraphSelectionRichTextEditor)
{
NativeKeyBindingsTestCase test{
NS_VK_DOWN,
KEY_NAME_INDEX_ArrowDown,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::RichTextEditor,
{Command::SelectCharNext, Command::SelectEndParagraph},
"Alt+Shift+ArrowDown should select to end of paragraph in a rich text "
"editor"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowUpParagraphSelectionSingleLineEditor)
{
NativeKeyBindingsTestCase test{
NS_VK_UP,
KEY_NAME_INDEX_ArrowUp,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::SingleLineEditor,
{Command::SelectCharPrevious, Command::SelectBeginParagraph},
"Alt+Shift+ArrowUp should select to beginning of paragraph in a single "
"line editor"};
CheckCommands(test);
}
TEST(NativeKeyBindings, AltShiftArrowDownParagraphSelectionSingleLineEditor)
{
NativeKeyBindingsTestCase test{
NS_VK_DOWN,
KEY_NAME_INDEX_ArrowDown,
MODIFIER_ALT | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::SingleLineEditor,
{Command::SelectCharNext, Command::SelectEndParagraph},
"Alt+Shift+ArrowDown should select to end of paragraph in a single line "
"editor"};
CheckCommands(test);
}
TEST(NativeKeyBindings, CtrlABeginParagraph)
{
NativeKeyBindingsTestCase test{
NS_VK_A,
KEY_NAME_INDEX_USE_STRING,
MODIFIER_CONTROL,
'a',
NativeKeyBindingsType::MultiLineEditor,
{Command::BeginParagraph},
"Ctrl+A should move to beginning of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, CtrlEEndParagraph)
{
NativeKeyBindingsTestCase test{NS_VK_E,
KEY_NAME_INDEX_USE_STRING,
MODIFIER_CONTROL,
'e',
NativeKeyBindingsType::MultiLineEditor,
{Command::EndParagraph},
"Ctrl+E should move to end of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, CtrlShiftASelectBeginParagraph)
{
NativeKeyBindingsTestCase test{
NS_VK_A,
KEY_NAME_INDEX_USE_STRING,
MODIFIER_CONTROL | MODIFIER_SHIFT,
'A',
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectBeginParagraph},
"Ctrl+Shift+A should select to beginning of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, CtrlShiftESelectEndParagraph)
{
NativeKeyBindingsTestCase test{
NS_VK_E,
KEY_NAME_INDEX_USE_STRING,
MODIFIER_CONTROL | MODIFIER_SHIFT,
'E',
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectEndParagraph},
"Ctrl+Shift+E should select to end of paragraph"};
CheckCommands(test);
}
TEST(NativeKeyBindings, MetaArrowLeftMoveToLeftEndOfLine)
{
NativeKeyBindingsTestCase test{
NS_VK_LEFT,
KEY_NAME_INDEX_ArrowLeft,
MODIFIER_META,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::MoveLeft3},
"Meta+ArrowLeft should move to left end of line"};
CheckCommands(test);
}
TEST(NativeKeyBindings, MetaArrowRightMoveToRightEndOfLine)
{
NativeKeyBindingsTestCase test{
NS_VK_RIGHT,
KEY_NAME_INDEX_ArrowRight,
MODIFIER_META,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::MoveRight3},
"Meta+ArrowRight should move to right end of line"};
CheckCommands(test);
}
TEST(NativeKeyBindings, MetaShiftArrowLeftSelectToLeftEndOfLine)
{
NativeKeyBindingsTestCase test{
NS_VK_LEFT,
KEY_NAME_INDEX_ArrowLeft,
MODIFIER_META | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectLeft3},
"Meta+Shift+ArrowLeft should select to left end of line"};
CheckCommands(test);
}
TEST(NativeKeyBindings, MetaShiftArrowRightSelectToRightEndOfLine)
{
NativeKeyBindingsTestCase test{
NS_VK_RIGHT,
KEY_NAME_INDEX_ArrowRight,
MODIFIER_META | MODIFIER_SHIFT,
0,
NativeKeyBindingsType::MultiLineEditor,
{Command::SelectRight3},
"Meta+Shift+ArrowRight should select to right end of line"};
CheckCommands(test);
}
} // namespace mozilla
#endif // XP_MACOSX