Bug 1685491 - part 5: Move the code remapping arrow keys in vertical content to `NativeKeyBindings` r=smaug,jfkthame
authorMasayuki Nakano <masayuki@d-toybox.com>
Tue, 02 Feb 2021 03:29:31 +0000
changeset 565554 08a804df3b6c6ae295cec081aec09fcccede1f35
parent 565553 2d2d69daa750387301b7bc197468ff0d2ee1fc40
child 565555 1e374af7adbba58037b323a344665e8f0c4a2319
push id38162
push usernerli@mozilla.com
push dateTue, 02 Feb 2021 09:51:07 +0000
treeherdermozilla-central@57bcdf857d44 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, jfkthame
bugs1685491, 1077515, 1301497, 1103374
milestone87.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1685491 - part 5: Move the code remapping arrow keys in vertical content to `NativeKeyBindings` r=smaug,jfkthame Currently, this feature is implemented only on Linux and macOS (see also bug 1077515 and bug 1301497), and the code is really similar each other. Additionally, it always tries to query selection to check whether the caret is in vertical content or not if arrow keys are pressed. For avoiding a lot of query, this patch makes `TextEventDispatcher` cache writing mode at every selection change notification. However, unfortunately, it's not available when non-editable content has focus, but it should be out of scope of this bug since it requires a lot of changes. Anyway, with this patch, we can write a mochitest only on Linux and macOS. The following patch adds a test for this as a fix of bug 1103374. Differential Revision: https://phabricator.services.mozilla.com/D102881
dom/base/FuzzingFunctions.cpp
dom/base/FuzzingFunctions.h
dom/base/TextInputProcessor.cpp
dom/base/TextInputProcessor.h
dom/html/TextControlState.cpp
dom/interfaces/base/nsITextInputProcessor.idl
dom/ipc/BrowserParent.cpp
dom/ipc/BrowserParent.h
editor/libeditor/tests/test_bug1053048.html
widget/PuppetWidget.h
widget/TextEventDispatcher.cpp
widget/TextEventDispatcher.h
widget/TextEvents.h
widget/WidgetEventImpl.cpp
widget/cocoa/NativeKeyBindings.h
widget/cocoa/NativeKeyBindings.mm
widget/cocoa/nsChildView.h
widget/cocoa/nsChildView.mm
widget/cocoa/nsCocoaWindow.h
widget/cocoa/nsCocoaWindow.mm
widget/gtk/NativeKeyBindings.cpp
widget/gtk/NativeKeyBindings.h
widget/gtk/nsWindow.cpp
widget/gtk/nsWindow.h
widget/headless/HeadlessKeyBindings.cpp
widget/headless/HeadlessKeyBindings.h
widget/headless/HeadlessKeyBindingsCocoa.mm
widget/headless/HeadlessWidget.cpp
widget/headless/HeadlessWidget.h
widget/nsIWidget.h
--- a/dom/base/FuzzingFunctions.cpp
+++ b/dom/base/FuzzingFunctions.cpp
@@ -317,18 +317,20 @@ void FuzzingFunctions::SynthesizeKeyboar
     // This is possible if a keyboard event listener or something tries to
     // dispatch next keyboard events during dispatching a keyboard event via
     // TextInputProcessor.
     aRv.Throw(NS_ERROR_FAILURE);
     return;
   }
 
   // First, activate necessary modifiers.
+  // MOZ_KnownLive(event.mWidget) is safe because `event` is an instance in
+  // the stack, and `mWidget` is `nsCOMPtr<nsIWidget>`.
   Modifiers activatedModifiers = ActivateModifiers(
-      textInputProcessor, event.mModifiers, event.mWidget, aRv);
+      textInputProcessor, event.mModifiers, MOZ_KnownLive(event.mWidget), aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   // Then, dispatch keydown and keypress.
   aRv = textInputProcessor->Keydown(event, flags);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
@@ -336,18 +338,20 @@ void FuzzingFunctions::SynthesizeKeyboar
 
   // Then, dispatch keyup.
   aRv = textInputProcessor->Keyup(event, flags);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   // Finally, inactivate some modifiers which are activated by this call.
-  InactivateModifiers(textInputProcessor, activatedModifiers, event.mWidget,
-                      aRv);
+  // MOZ_KnownLive(event.mWidget) is safe because `event` is an instance in
+  // the stack, and `mWidget` is `nsCOMPtr<nsIWidget>`.
+  InactivateModifiers(textInputProcessor, activatedModifiers,
+                      MOZ_KnownLive(event.mWidget), aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return;
   }
 
   // Unfortunately, we cannot keep storing modifier state in the
   // TextInputProcessor since if we store it into a static variable,
   // we need to take care of resetting it when the caller wants.
   // However, that makes API more complicated.  So, until they need
--- a/dom/base/FuzzingFunctions.h
+++ b/dom/base/FuzzingFunctions.h
@@ -28,49 +28,48 @@ class FuzzingFunctions final {
   static void GarbageCollectCompacting(const GlobalObject&);
 
   static void CycleCollect(const GlobalObject&);
 
   static void MemoryPressure(const GlobalObject&);
 
   static void EnableAccessibility(const GlobalObject&, ErrorResult& aRv);
 
-  static void SynthesizeKeyboardEvents(const GlobalObject& aGlobalObject,
-                                       const nsAString& aKeyValue,
-                                       const KeyboardEventInit& aKeyboardEvent,
-                                       ErrorResult& aRv);
+  MOZ_CAN_RUN_SCRIPT_BOUNDARY static void SynthesizeKeyboardEvents(
+      const GlobalObject& aGlobalObject, const nsAString& aKeyValue,
+      const KeyboardEventInit& aKeyboardEvent, ErrorResult& aRv);
 
  private:
   /**
    * ActivateModifiers() activates aModifiers in the TextInputProcessor.
    *
    * @param aTextInputProcessor The TIP whose modifier state you want to change.
    * @param aModifiers          Modifiers which you want to activate.
    * @param aWidget             The widget which should be set to
    *                            WidgetKeyboardEvent.
    * @param aRv                 Returns error if TextInputProcessor fails to
    *                            dispatch a modifier key event.
    * @return                    Modifiers which are activated by the call.
    */
-  static Modifiers ActivateModifiers(TextInputProcessor* aTextInputProcessor,
-                                     Modifiers aModifiers, nsIWidget* aWidget,
-                                     ErrorResult& aRv);
+  MOZ_CAN_RUN_SCRIPT static Modifiers ActivateModifiers(
+      TextInputProcessor* aTextInputProcessor, Modifiers aModifiers,
+      nsIWidget* aWidget, ErrorResult& aRv);
 
   /**
    * InactivateModifiers() inactivates aModifiers in the TextInputProcessor.
    *
    * @param aTextInputProcessor The TIP whose modifier state you want to change.
    * @param aModifiers          Modifiers which you want to inactivate.
    * @param aWidget             The widget which should be set to
    *                            WidgetKeyboardEvent.
    * @param aRv                 Returns error if TextInputProcessor fails to
    *                            dispatch a modifier key event.
    * @return                    Modifiers which are inactivated by the call.
    */
-  static Modifiers InactivateModifiers(TextInputProcessor* aTextInputProcessor,
-                                       Modifiers aModifiers, nsIWidget* aWidget,
-                                       ErrorResult& aRv);
+  MOZ_CAN_RUN_SCRIPT static Modifiers InactivateModifiers(
+      TextInputProcessor* aTextInputProcessor, Modifiers aModifiers,
+      nsIWidget* aWidget, ErrorResult& aRv);
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_FuzzingFunctions
--- a/dom/base/TextInputProcessor.cpp
+++ b/dom/base/TextInputProcessor.cpp
@@ -6,16 +6,17 @@
 
 #include "mozilla/dom/Event.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/StaticPrefs_test.h"
 #include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TextInputProcessor.h"
+#include "mozilla/WritingModes.h"
 #include "mozilla/widget/IMEData.h"
 #include "mozilla/dom/KeyboardEvent.h"
 #include "nsContentUtils.h"
 #include "nsIDocShell.h"
 #include "nsIWidget.h"
 #include "nsPIDOMWindow.h"
 #include "nsPresContext.h"
 
@@ -1006,30 +1007,35 @@ nsresult TextInputProcessor::InitEditCom
 
   // Note that retrieving edit commands via PuppetWidget is expensive.
   // Let's skip it when the keyboard event is inputting text.
   if (aKeyboardEvent.IsInputtingText()) {
     aKeyboardEvent.PreventNativeKeyBindings();
     return NS_OK;
   }
 
+  Maybe<WritingMode> writingMode;
+  if (RefPtr<TextEventDispatcher> dispatcher = mDispatcher) {
+    writingMode = dispatcher->MaybeWritingModeAtSelection();
+  }
+
   // FYI: WidgetKeyboardEvent::InitAllEditCommands() isn't available here
   //      since it checks whether it's called in the main process to
   //      avoid performance issues so that we need to initialize each
   //      command manually here.
   if (NS_WARN_IF(!aKeyboardEvent.InitEditCommandsFor(
-          nsIWidget::NativeKeyBindingsForSingleLineEditor))) {
+          nsIWidget::NativeKeyBindingsForSingleLineEditor, writingMode))) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   if (NS_WARN_IF(!aKeyboardEvent.InitEditCommandsFor(
-          nsIWidget::NativeKeyBindingsForMultiLineEditor))) {
+          nsIWidget::NativeKeyBindingsForMultiLineEditor, writingMode))) {
     return NS_ERROR_NOT_AVAILABLE;
   }
   if (NS_WARN_IF(!aKeyboardEvent.InitEditCommandsFor(
-          nsIWidget::NativeKeyBindingsForRichTextEditor))) {
+          nsIWidget::NativeKeyBindingsForRichTextEditor, writingMode))) {
     return NS_ERROR_NOT_AVAILABLE;
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 TextInputProcessor::Keydown(Event* aDOMKeyEvent, uint32_t aKeyFlags,
--- a/dom/base/TextInputProcessor.h
+++ b/dom/base/TextInputProcessor.h
@@ -32,18 +32,19 @@ class TextInputProcessor final : public 
 
  public:
   TextInputProcessor();
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSITEXTINPUTPROCESSOR
 
   // TextEventDispatcherListener
-  NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
-                       const IMENotification& aNotification) override;
+  MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD
+  NotifyIME(TextEventDispatcher* aTextEventDispatcher,
+            const IMENotification& aNotification) override;
 
   NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;
 
   NS_IMETHOD_(void)
   OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;
 
   NS_IMETHOD_(void)
   WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
@@ -71,18 +72,19 @@ class TextInputProcessor final : public 
       nsPIDOMWindowInner* aWindow, nsITextInputProcessorCallback* aCallback,
       bool* aSucceeded);
 
   /**
    * The following Keydown() and KeyUp() are same as nsITextInputProcessor's
    * same name methods except the type of event class.  See explanation in
    * nsITextInputProcessor for the detail.
    */
-  nsresult Keydown(const WidgetKeyboardEvent& aKeyboardEvent,
-                   uint32_t aKeyFlags, uint32_t* aConsumedFlags = nullptr);
+  MOZ_CAN_RUN_SCRIPT nsresult Keydown(const WidgetKeyboardEvent& aKeyboardEvent,
+                                      uint32_t aKeyFlags,
+                                      uint32_t* aConsumedFlags = nullptr);
   nsresult Keyup(const WidgetKeyboardEvent& aKeyboardEvent, uint32_t aKeyFlags,
                  bool* aDoDefault = nullptr);
 
   /**
    * GuessCodeNameIndexOfPrintableKeyInUSEnglishLayout() returns CodeNameIndex
    * of a printable key which is in usual keyboard of the platform and when
    * active keyboard layout is US-English.
    * Note that this does not aware of option key mapping on macOS.
@@ -120,54 +122,55 @@ class TextInputProcessor final : public 
  protected:
   virtual ~TextInputProcessor();
 
  private:
   bool IsComposing() const;
   nsresult BeginInputTransactionInternal(
       mozIDOMWindow* aWindow, nsITextInputProcessorCallback* aCallback,
       bool aForTests, bool& aSucceeded);
-  nsresult CommitCompositionInternal(
+  MOZ_CAN_RUN_SCRIPT nsresult CommitCompositionInternal(
       const WidgetKeyboardEvent* aKeyboardEvent = nullptr,
       uint32_t aKeyFlags = 0, const nsAString* aCommitString = nullptr,
       bool* aSucceeded = nullptr);
-  nsresult CancelCompositionInternal(
-      const WidgetKeyboardEvent* aKeyboardEvent = nullptr,
-      uint32_t aKeyFlags = 0);
-  nsresult KeydownInternal(const WidgetKeyboardEvent& aKeyboardEvent,
-                           uint32_t aKeyFlags, bool aAllowToDispatchKeypress,
-                           uint32_t& aConsumedFlags);
+  MOZ_CAN_RUN_SCRIPT nsresult
+  CancelCompositionInternal(const WidgetKeyboardEvent* aKeyboardEvent = nullptr,
+                            uint32_t aKeyFlags = 0);
+  MOZ_CAN_RUN_SCRIPT nsresult
+  KeydownInternal(const WidgetKeyboardEvent& aKeyboardEvent, uint32_t aKeyFlags,
+                  bool aAllowToDispatchKeypress, uint32_t& aConsumedFlags);
   nsresult KeyupInternal(const WidgetKeyboardEvent& aKeyboardEvent,
                          uint32_t aKeyFlags, bool& aDoDefault);
   nsresult IsValidStateForComposition();
   void UnlinkFromTextEventDispatcher();
   nsresult PrepareKeyboardEventToDispatch(WidgetKeyboardEvent& aKeyboardEvent,
                                           uint32_t aKeyFlags);
   /**
    * InitEditCommands() initializes edit commands of aKeyboardEvent.
    * This must be called only in a content process, and aKeyboardEvent must
    * be used only for `eKeyPress` event.
    */
-  nsresult InitEditCommands(WidgetKeyboardEvent& aKeyboardEvent) const;
+  MOZ_CAN_RUN_SCRIPT nsresult
+  InitEditCommands(WidgetKeyboardEvent& aKeyboardEvent) const;
 
   bool IsValidEventTypeForComposition(
       const WidgetKeyboardEvent& aKeyboardEvent) const;
   nsresult PrepareKeyboardEventForComposition(
       dom::KeyboardEvent* aDOMKeyEvent, uint32_t& aKeyFlags,
       uint8_t aOptionalArgc, WidgetKeyboardEvent*& aKeyboardEvent);
 
   struct EventDispatcherResult {
     nsresult mResult;
     bool mDoDefault;
     bool mCanContinue;
 
     EventDispatcherResult()
         : mResult(NS_OK), mDoDefault(true), mCanContinue(true) {}
   };
-  EventDispatcherResult MaybeDispatchKeydownForComposition(
+  MOZ_CAN_RUN_SCRIPT EventDispatcherResult MaybeDispatchKeydownForComposition(
       const WidgetKeyboardEvent* aKeyboardEvent, uint32_t aKeyFlags);
   EventDispatcherResult MaybeDispatchKeyupForComposition(
       const WidgetKeyboardEvent* aKeyboardEvent, uint32_t aKeyFlags);
 
   /**
    * AutoPendingCompositionResetter guarantees to clear all pending composition
    * data in its destructor.
    */
--- a/dom/html/TextControlState.cpp
+++ b/dom/html/TextControlState.cpp
@@ -924,17 +924,17 @@ static void DoCommandCallback(Command aC
           controller->IsCommandEnabled(commandStr, &commandEnabled)))) {
     return;
   }
   if (commandEnabled) {
     controller->DoCommand(commandStr);
   }
 }
 
-NS_IMETHODIMP
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
 TextInputListener::HandleEvent(Event* aEvent) {
   if (aEvent->DefaultPrevented()) {
     return NS_OK;
   }
 
   if (!aEvent->IsTrusted()) {
     return NS_OK;
   }
--- a/dom/interfaces/base/nsITextInputProcessor.idl
+++ b/dom/interfaces/base/nsITextInputProcessor.idl
@@ -310,17 +310,17 @@ interface nsITextInputProcessor : nsISup
    *                        are set to "Process" and DOM_VK_PROCESSKEY
    *                        automatically.  You can prevent this behavior
    *                        with KEY_DONT_MARK_KEYDOWN_AS_PROCESSED.
    * @param aKeyFlags       See KEY_* constants.
    * @return                Returns true if composition starts normally.
    *                        Otherwise, returns false because it might be
    *                        canceled by the web application.
    */
-  [optional_argc]
+  [can_run_script, optional_argc]
     boolean startComposition([optional] in Event aKeyboardEvent,
                              [optional] in unsigned long aKeyFlags);
 
   /**
    * Set new composition string.  Pending composition will be flushed by
    * a call of flushPendingComposition().  However, if the new composition
    * string isn't empty, you need to call appendClauseToPendingComposition() to
    * fill all characters of aString with one or more clauses before flushing.
@@ -404,17 +404,17 @@ interface nsITextInputProcessor : nsISup
    *                        with KEY_DONT_MARK_KEYDOWN_AS_PROCESSED.
    * @param aKeyFlags       See KEY_* constants.
    * @return                Returns true if there is a composition already or
    *                        starting composition automatically.
    *                        Otherwise, i.e., if it cannot start composition
    *                        automatically, e.g., canceled by web apps, returns
    *                        false.
    */
-  [optional_argc]
+  [can_run_script, optional_argc]
     boolean flushPendingComposition(
       [optional] in Event aKeyboardEvent,
       [optional] in unsigned long aKeyFlags);
 
   /**
    * commitComposition() will commit composition with the last composition
    * string.  If there is no composition, this will throw an exception.
    *
@@ -423,17 +423,17 @@ interface nsITextInputProcessor : nsISup
    *                        dispatches only keydown event first.  Otherwise,
    *                        dispatches keydown first and keyup at last.
    *                        key value and keyCode values of keydown event
    *                        are set to "Process" and DOM_VK_PROCESSKEY
    *                        automatically.  You can prevent this behavior
    *                        with KEY_DONT_MARK_KEYDOWN_AS_PROCESSED.
    * @param aKeyFlags       See KEY_* constants.
    */
-  [optional_argc]
+  [can_run_script, optional_argc]
     void commitComposition([optional] in Event aKeyboardEvent,
                            [optional] in unsigned long aKeyFlags);
 
   /**
    * commitCompositionWith() will commit composition with the specific string.
    * If there is no composition, this will start composition and commit it
    * with the specified string.
    *
@@ -448,17 +448,17 @@ interface nsITextInputProcessor : nsISup
    *                        with KEY_DONT_MARK_KEYDOWN_AS_PROCESSED.
    * @param aKeyFlags       See KEY_* constants.
    * @return                Returns true if there is a composition already or
    *                        starting composition automatically.
    *                        Otherwise, i.e., if it cannot start composition
    *                        automatically, e.g., canceled by web apps, returns
    *                        false.
    */
-  [optional_argc]
+  [can_run_script, optional_argc]
     boolean commitCompositionWith(in AString aCommitString,
                                   [optional] in Event aKeyboardEvent,
                                   [optional] in unsigned long aKeyFlags);
 
   /**
    * cancelComposition() will cancel composition.  This is for now the same as
    * calling commitComposition("").  However, in the future, this might work
    * better.  If your IME needs to cancel composition, use this instead of
@@ -472,17 +472,17 @@ interface nsITextInputProcessor : nsISup
    *                        dispatches only keydown event first.  Otherwise,
    *                        dispatches keydown first and keyup at last.
    *                        key value and keyCode values of keydown event
    *                        are set to "Process" and DOM_VK_PROCESSKEY
    *                        automatically.  You can prevent this behavior
    *                        with KEY_DONT_MARK_KEYDOWN_AS_PROCESSED.
    * @param aKeyFlags       See KEY_* constants.
    */
-  [optional_argc]
+  [can_run_script, optional_argc]
     void cancelComposition([optional] in Event aKeyboardEvent,
                            [optional] in unsigned long aKeyFlags);
 
   // Specifying KEY_DEFAULT_PREVENTED can dispatch key events whose
   // defaultPrevented are true.  Note that if this is specified, keypress event
   // won't be fired.
   const unsigned long KEY_DEFAULT_PREVENTED                        = 0x00000001;
   // If KEY_NON_PRINTABLE_KEY is specified and the .key value isn't valid
@@ -568,17 +568,17 @@ interface nsITextInputProcessor : nsISup
    *                        consumed. No keypress event will be dispatched in
    *                        this case.
    *                        KEYPRESS_IS_CONSUMED, if the keypress event(s) is
    *                        consumed when dispatched.
    *                        Note that keypress event is always consumed by
    *                        native code for the printable keys (indicating the
    *                        default action has been taken).
    */
-  [optional_argc]
+  [can_run_script, optional_argc]
     unsigned long keydown(in Event aKeyboardEvent,
                           [optional] in unsigned long aKeyFlags);
 
   /**
    * Similar to keydown(), but this dispatches only a keyup event.
    */
   [optional_argc]
     boolean keyup(in Event aKeyboardEvent,
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -49,16 +49,17 @@
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/net/NeckoChild.h"
 #include "mozilla/net/CookieJarSettings.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/ProcessHangMonitor.h"
 #include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TouchEvents.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/Unused.h"
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsDebug.h"
 #include "nsFocusManager.h"
@@ -1685,17 +1686,22 @@ mozilla::ipc::IPCResult BrowserParent::R
 
   WidgetKeyboardEvent localEvent(aEvent);
   localEvent.mWidget = widget;
 
   if (NS_FAILED(widget->AttachNativeKeyEvent(localEvent))) {
     return IPC_OK();
   }
 
-  if (localEvent.InitEditCommandsFor(keyBindingsType)) {
+  Maybe<WritingMode> writingMode;
+  if (RefPtr<widget::TextEventDispatcher> dispatcher =
+          widget->GetTextEventDispatcher()) {
+    writingMode = dispatcher->MaybeWritingModeAtSelection();
+  }
+  if (localEvent.InitEditCommandsFor(keyBindingsType, writingMode)) {
     *aCommands = localEvent.EditCommandsConstRef(keyBindingsType).Clone();
   }
 
   return IPC_OK();
 }
 
 class SynthesizedEventObserver : public nsIObserver {
   NS_DECL_ISUPPORTS
@@ -1851,17 +1857,24 @@ void BrowserParent::SendRealKeyEvent(Wid
   aEvent.mRefPoint = TransformParentToChild(aEvent.mRefPoint);
 
   // NOTE: If you call `InitAllEditCommands()` for the other messages too,
   //       you also need to update
   //       TextEventDispatcher::DispatchKeyboardEventInternal().
   if (aEvent.mMessage == eKeyPress) {
     // XXX Should we do this only when input context indicates an editor having
     //     focus and the key event won't cause inputting text?
-    aEvent.InitAllEditCommands();
+    Maybe<WritingMode> writingMode;
+    if (aEvent.mWidget) {
+      if (RefPtr<widget::TextEventDispatcher> dispatcher =
+              aEvent.mWidget->GetTextEventDispatcher()) {
+        writingMode = dispatcher->MaybeWritingModeAtSelection();
+      }
+    }
+    aEvent.InitAllEditCommands(writingMode);
   } else {
     aEvent.PreventNativeKeyBindings();
   }
   DebugOnly<bool> ret =
       Manager()->IsInputPriorityEventEnabled()
           ? PBrowserParent::SendRealKeyEvent(aEvent)
           : PBrowserParent::SendNormalPriorityRealKeyEvent(aEvent);
 
--- a/dom/ipc/BrowserParent.h
+++ b/dom/ipc/BrowserParent.h
@@ -499,19 +499,20 @@ class BrowserParent final : public PBrow
 
   bool MapEventCoordinatesForChildProcess(mozilla::WidgetEvent* aEvent);
 
   void MapEventCoordinatesForChildProcess(const LayoutDeviceIntPoint& aOffset,
                                           mozilla::WidgetEvent* aEvent);
 
   LayoutDeviceToCSSScale GetLayoutDeviceToCSSScale();
 
-  mozilla::ipc::IPCResult RecvRequestNativeKeyBindings(
-      const uint32_t& aType, const mozilla::WidgetKeyboardEvent& aEvent,
-      nsTArray<mozilla::CommandInt>* aCommands);
+  MOZ_CAN_RUN_SCRIPT_BOUNDARY mozilla::ipc::IPCResult
+  RecvRequestNativeKeyBindings(const uint32_t& aType,
+                               const mozilla::WidgetKeyboardEvent& aEvent,
+                               nsTArray<mozilla::CommandInt>* aCommands);
 
   mozilla::ipc::IPCResult RecvSynthesizeNativeKeyEvent(
       const int32_t& aNativeKeyboardLayout, const int32_t& aNativeKeyCode,
       const uint32_t& aModifierFlags, const nsString& aCharacters,
       const nsString& aUnmodifiedCharacters, const uint64_t& aObserverId);
 
   mozilla::ipc::IPCResult RecvSynthesizeNativeMouseEvent(
       const LayoutDeviceIntPoint& aPoint, const uint32_t& aNativeMessage,
@@ -549,17 +550,22 @@ class BrowserParent final : public PBrow
   void SendRealMouseEvent(WidgetMouseEvent& aEvent);
 
   void SendRealDragEvent(WidgetDragEvent& aEvent, uint32_t aDragAction,
                          uint32_t aDropEffect, nsIPrincipal* aPrincipal,
                          nsIContentSecurityPolicy* aCsp);
 
   void SendMouseWheelEvent(WidgetWheelEvent& aEvent);
 
-  void SendRealKeyEvent(WidgetKeyboardEvent& aEvent);
+  /**
+   * Only when the event is synthesized, retrieving writing mode may flush
+   * the layout.
+   */
+  MOZ_CAN_RUN_SCRIPT_BOUNDARY void SendRealKeyEvent(
+      WidgetKeyboardEvent& aEvent);
 
   void SendRealTouchEvent(WidgetTouchEvent& aEvent);
 
   /**
    * Different from above Send*Event(), these methods return true if the
    * event has been posted to the remote process or failed to do that but
    * shouldn't be handled by following event listeners.
    * If you need to check if it's actually posted to the remote process,
--- a/editor/libeditor/tests/test_bug1053048.html
+++ b/editor/libeditor/tests/test_bug1053048.html
@@ -12,46 +12,48 @@ https://bugzilla.mozilla.org/show_bug.cg
   <script type="application/javascript">
 
   /** Test for Bug 1053048 **/
   SimpleTest.waitForExplicitFinish();
   SimpleTest.waitForFocus(runTests);
 
   const nsISelectionListener = SpecialPowers.Ci.nsISelectionListener;
 
-  function runTests() {
+  async function runTests() {
     var textarea = SpecialPowers.wrap(document.getElementById("textarea"));
     textarea.focus();
 
     var editor = textarea.editor;
     var selectionPrivate = editor.selection;
 
-    var selectionListener = {
-      count: 0,
-      notifySelectionChanged(aDocument, aSelection, aReason) {
-        ok(true, "selectionStart: " + textarea.selectionStart);
-        ok(true, "selectionEnd: " + textarea.selectionEnd);
-        this.count++;
-      },
-    };
-
     // Move caret to the end of the textarea
     synthesizeMouse(textarea, 290, 10, {});
     is(textarea.selectionStart, 3, "selectionStart should be 3 (after \"foo\")");
     is(textarea.selectionEnd, 3, "selectionEnd should be 3 (after \"foo\")");
 
+    // This test **was** trying to check whether a selection listener which
+    // runs while an editor handles an edit action does not stop handling it.
+    // However, this selection listener caught previous selection change
+    // notification immediately before synthesizing the `Enter` key press
+    // unexpectedly.  And now, selection listener may not run immediately after
+    // synthesizing the key press.  So, we don't need to check whether a
+    // notification actually comes here.
+    let selectionListener = {
+      notifySelectionChanged(aDocument, aSelection, aReason) {
+        ok(true, "selectionStart: " + textarea.selectionStart);
+        ok(true, "selectionEnd: " + textarea.selectionEnd);
+      },
+    };
     selectionPrivate.addSelectionListener(selectionListener);
-
-    sendKey("RETURN");
-    is(selectionListener.count, 1, "nsISelectionListener.notifySelectionChanged() should be called");
+    synthesizeKey("KEY_Enter");
     is(textarea.selectionStart, 4, "selectionStart should be 4");
     is(textarea.selectionEnd, 4, "selectionEnd should be 4");
     is(textarea.value, "foo\n", "The line break should be appended");
+    selectionPrivate.removeSelectionListener(selectionListener);
 
-    selectionPrivate.removeSelectionListener(selectionListener);
     SimpleTest.finish();
   }
   </script>
 </head>
 <body>
 <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1053048">Mozilla Bug 1053048</a>
 <p id="display"></p>
 <div id="content" style="display: none">
--- a/widget/PuppetWidget.h
+++ b/widget/PuppetWidget.h
@@ -148,17 +148,17 @@ class PuppetWidget : public nsBaseWidget
   void SetConfirmedTargetAPZC(
       uint64_t aInputBlockId,
       const nsTArray<ScrollableLayerGuid>& aTargets) const override;
   void UpdateZoomConstraints(
       const uint32_t& aPresShellId, const ScrollableLayerGuid::ViewID& aViewId,
       const mozilla::Maybe<ZoomConstraints>& aConstraints) override;
   bool AsyncPanZoomEnabled() const override;
 
-  virtual bool GetEditCommands(
+  MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
       NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
       nsTArray<mozilla::CommandInt>& aCommands) override;
 
   friend struct AutoCacheNativeKeyCommands;
 
   //
   // nsBaseWidget methods we override
   //
--- a/widget/TextEventDispatcher.cpp
+++ b/widget/TextEventDispatcher.cpp
@@ -182,16 +182,17 @@ void TextEventDispatcher::EndInputTransa
 
 void TextEventDispatcher::OnDestroyWidget() {
   mWidget = nullptr;
   mHasFocus = false;
   ClearNotificationRequests();
   mPendingComposition.Clear();
   nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
   mListener = nullptr;
+  mWritingMode.reset();
   mInputTransactionType = eNoInputTransaction;
   if (listener) {
     listener->OnRemovedFrom(this);
   }
 }
 
 nsresult TextEventDispatcher::GetState() const {
   nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
@@ -221,16 +222,43 @@ void TextEventDispatcher::InitEvent(Widg
     MOZ_ASSERT(!XRE_IsContentProcess(),
                "Why did the content process start native event transaction?");
     MOZ_ASSERT(aEvent.AsCompositionEvent()->mNativeIMEContext.IsValid(),
                "Native IME context shouldn't be invalid");
   }
 #endif  // #ifdef DEBUG
 }
 
+Maybe<WritingMode> TextEventDispatcher::MaybeWritingModeAtSelection() const {
+  if (mWritingMode.isSome()) {
+    return mWritingMode;
+  }
+
+  if (NS_WARN_IF(!mWidget)) {
+    return Nothing();
+  }
+
+  WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+                                                 mWidget);
+  nsEventStatus status = nsEventStatus_eIgnore;
+  const_cast<TextEventDispatcher*>(this)->DispatchEvent(
+      mWidget, querySelectedTextEvent, status);
+  if (!querySelectedTextEvent.FoundSelection()) {
+    if (mHasFocus) {
+      mWritingMode.reset();
+    }
+    return Nothing();
+  }
+
+  if (mHasFocus) {
+    mWritingMode = Some(querySelectedTextEvent.mReply->mWritingMode);
+  }
+  return Some(querySelectedTextEvent.mReply->mWritingMode);
+}
+
 nsresult TextEventDispatcher::DispatchEvent(nsIWidget* aWidget,
                                             WidgetGUIEvent& aEvent,
                                             nsEventStatus& aStatus) {
   MOZ_ASSERT(!aEvent.AsInputEvent(), "Use DispatchInputEvent()");
 
   RefPtr<TextEventDispatcher> kungFuDeathGrip(this);
   nsCOMPtr<nsIWidget> widget(aWidget);
   mDispatchingEvent++;
@@ -383,28 +411,35 @@ nsresult TextEventDispatcher::CommitComp
 
 nsresult TextEventDispatcher::NotifyIME(
     const IMENotification& aIMENotification) {
   nsresult rv = NS_ERROR_NOT_IMPLEMENTED;
 
   switch (aIMENotification.mMessage) {
     case NOTIFY_IME_OF_BLUR:
       mHasFocus = false;
+      mWritingMode.reset();
       ClearNotificationRequests();
       break;
     case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
       // If content handles composition events when native IME doesn't have
       // composition, that means that we completely finished handling
       // composition(s).  Note that when focused content is in a remote
       // process, this is sent when all dispatched composition events
       // have been handled in the remote process.
       if (!IsComposing()) {
         mIsHandlingComposition = false;
       }
       break;
+    case NOTIFY_IME_OF_SELECTION_CHANGE:
+      if (mHasFocus) {
+        mWritingMode =
+            Some(aIMENotification.mSelectionChangeData.GetWritingMode());
+      }
+      break;
     default:
       break;
   }
 
   // First, send the notification to current input transaction's listener.
   nsCOMPtr<TextEventDispatcherListener> listener = do_QueryReferent(mListener);
   if (listener) {
     rv = listener->NotifyIME(this, aIMENotification);
--- a/widget/TextEventDispatcher.h
+++ b/widget/TextEventDispatcher.h
@@ -5,19 +5,21 @@
 
 #ifndef mozilla_textcompositionsynthesizer_h_
 #define mozilla_textcompositionsynthesizer_h_
 
 #include "mozilla/RefPtr.h"
 #include "nsString.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/TextEventDispatcherListener.h"
 #include "mozilla/TextRange.h"
 #include "mozilla/widget/IMEData.h"
+#include "WritingModes.h"
 
 class nsIWidget;
 
 namespace mozilla {
 namespace widget {
 
 class PuppetWidget;
 
@@ -144,16 +146,23 @@ class TextEventDispatcher final {
     if (mInputTransactionType == eNoInputTransaction ||
         mInputTransactionType == eNativeInputTransaction) {
       return nullptr;
     }
     return const_cast<TextEventDispatcher*>(this);
   }
 
   /**
+   * MaybeWritingModeAtSelection() returns writing mode at current selection. If
+   * it's not stored, this tries to retrieve it.  Then, chrome script can run
+   * due to flushing the layout if an element in chrome has focus.
+   */
+  MOZ_CAN_RUN_SCRIPT Maybe<WritingMode> MaybeWritingModeAtSelection() const;
+
+  /**
    * StartComposition() starts composition explicitly.
    *
    * @param aEventTime  If this is not nullptr, WidgetCompositionEvent will
    *                    be initialized with this.  Otherwise, initialized
    *                    with the time at initializing.
    */
   nsresult StartComposition(nsEventStatus& aStatus,
                             const WidgetEventTime* aEventTime = nullptr);
@@ -321,16 +330,19 @@ class TextEventDispatcher final {
   // be referred by JS.  Therefore, the listener might be difficult to release
   // itself if this is a strong reference.  Additionally, it's difficult to
   // check if a method to uninstall the listener is called by valid instance.
   // So, using weak reference is the best way in this case.
   nsWeakPtr mListener;
   // mIMENotificationRequests should store current IME's notification requests.
   // So, this may be invalid when IME doesn't have focus.
   IMENotificationRequests mIMENotificationRequests;
+  // mWritingMode may store writing mode at current selection while this has
+  // focus (i.e., while this can receive selection change notifications).
+  mutable Maybe<WritingMode> mWritingMode;
 
   // mPendingComposition stores new composition string temporarily.
   // These values will be used for dispatching eCompositionChange event
   // in Flush().  When Flush() is called, the members will be cleared
   // automatically.
   class PendingComposition {
    public:
     PendingComposition();
--- a/widget/TextEvents.h
+++ b/widget/TextEvents.h
@@ -404,31 +404,109 @@ class WidgetKeyboardEvent : public Widge
     // generating WM_KEYDOWN for VK_PACKET (inputting any Unicode characters
     // without keyboard layout information) and VK_BACK (Backspace) to remove
     // previous character(s) and those messages may be marked as "repeat" by
     // their bug.
     return mIsRepeat && mMaybeSkippableInRemoteProcess;
   }
 
   /**
+   * If the key is an arrow key, and the current selection is in a vertical
+   * content, the caret should be moved to physically.  However, arrow keys
+   * are mapped to logical move commands in horizontal content.  Therefore,
+   * we need to check writing mode if and only if the key is an arrow key, and
+   * need to remap the command to logical command in vertical content if the
+   * writing mode at selection is vertical.  These methods help to convert
+   * arrow keys in horizontal content to correspnding direction arrow keys
+   * in vertical content.
+   */
+  bool NeedsToRemapNavigationKey() const {
+    // TODO: Use mKeyNameIndex instead.
+    return mKeyCode >= NS_VK_LEFT && mKeyCode <= NS_VK_DOWN;
+  }
+
+  uint32_t GetRemappedKeyCode(const WritingMode& aWritingMode) const {
+    if (!aWritingMode.IsVertical()) {
+      return mKeyCode;
+    }
+    switch (mKeyCode) {
+      case NS_VK_LEFT:
+        return aWritingMode.IsVerticalLR() ? NS_VK_UP : NS_VK_DOWN;
+      case NS_VK_RIGHT:
+        return aWritingMode.IsVerticalLR() ? NS_VK_DOWN : NS_VK_UP;
+      case NS_VK_UP:
+        return NS_VK_LEFT;
+      case NS_VK_DOWN:
+        return NS_VK_RIGHT;
+      default:
+        return mKeyCode;
+    }
+  }
+
+  KeyNameIndex GetRemappedKeyNameIndex(const WritingMode& aWritingMode) const {
+    if (!aWritingMode.IsVertical()) {
+      return mKeyNameIndex;
+    }
+    uint32_t remappedKeyCode = GetRemappedKeyCode(aWritingMode);
+    if (remappedKeyCode == mKeyCode) {
+      return mKeyNameIndex;
+    }
+    switch (remappedKeyCode) {
+      case NS_VK_LEFT:
+        return KEY_NAME_INDEX_ArrowLeft;
+      case NS_VK_RIGHT:
+        return KEY_NAME_INDEX_ArrowRight;
+      case NS_VK_UP:
+        return KEY_NAME_INDEX_ArrowUp;
+      case NS_VK_DOWN:
+        return KEY_NAME_INDEX_ArrowDown;
+      default:
+        MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+        return mKeyNameIndex;
+    }
+  }
+
+  /**
    * Retrieves all edit commands from mWidget.  This shouldn't be called when
    * the instance is an untrusted event, doesn't have widget or in non-chrome
    * process.
+   *
+   * @param aWritingMode
+   *                    When writing mode of focused element is vertical, this
+   *                    will resolve some key's physical direction to logical
+   *                    direction.  For doing it, this must be set to the
+   *                    writing mode at current selection.  However, when there
+   *                    is no focused element and no selection ranges, this
+   *                    should be set to Nothing().  Using the result of
+   *                    `TextEventDispatcher::MaybeWritingModeAtSelection()` is
+   *                    recommended.
    */
-  void InitAllEditCommands();
+  MOZ_CAN_RUN_SCRIPT void InitAllEditCommands(
+      const Maybe<WritingMode>& aWritingMode);
 
   /**
    * Retrieves edit commands from mWidget only for aType.  This shouldn't be
    * called when the instance is an untrusted event or doesn't have widget.
    *
+   * @param aWritingMode
+   *                    When writing mode of focused element is vertical, this
+   *                    will resolve some key's physical direction to logical
+   *                    direction.  For doing it, this must be set to the
+   *                    writing mode at current selection.  However, when there
+   *                    is no focused element and no selection ranges, this
+   *                    should be set to Nothing().  Using the result of
+   *                    `TextEventDispatcher::MaybeWritingModeAtSelection()` is
+   *                    recommended.
    * @return            false if some resource is not available to get
    *                    commands unexpectedly.  Otherwise, true even if
    *                    retrieved command is nothing.
    */
-  bool InitEditCommandsFor(nsIWidget::NativeKeyBindingsType aType);
+  MOZ_CAN_RUN_SCRIPT bool InitEditCommandsFor(
+      nsIWidget::NativeKeyBindingsType aType,
+      const Maybe<WritingMode>& aWritingMode);
 
   /**
    * PreventNativeKeyBindings() makes the instance to not cause any edit
    * actions even if it matches with a native key binding.
    */
   void PreventNativeKeyBindings() {
     mEditCommandsForSingleLineEditor.Clear();
     mEditCommandsForMultiLineEditor.Clear();
@@ -467,18 +545,19 @@ class WidgetKeyboardEvent : public Widge
 
   /**
    * Execute edit commands for aType.
    *
    * @return        true if the caller should do nothing anymore.
    *                false, otherwise.
    */
   typedef void (*DoCommandCallback)(Command, void*);
-  bool ExecuteEditCommands(nsIWidget::NativeKeyBindingsType aType,
-                           DoCommandCallback aCallback, void* aCallbackData);
+  MOZ_CAN_RUN_SCRIPT bool ExecuteEditCommands(
+      nsIWidget::NativeKeyBindingsType aType, DoCommandCallback aCallback,
+      void* aCallbackData);
 
   // If the key should cause keypress events, this returns true.
   // Otherwise, false.
   bool ShouldCauseKeypressEvents() const;
 
   // mCharCode value of non-eKeyPress events is always 0.  However, if
   // non-eKeyPress event has one or more alternative char code values,
   // its first item should be the mCharCode value of following eKeyPress event.
--- a/widget/WidgetEventImpl.cpp
+++ b/widget/WidgetEventImpl.cpp
@@ -2,23 +2,26 @@
 /* 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/. */
 
 #include "mozilla/BasicEvents.h"
 #include "mozilla/ContentEvents.h"
 #include "mozilla/EventStateManager.h"
 #include "mozilla/InternalMutationEvent.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/StaticPrefs_mousewheel.h"
 #include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TouchEvents.h"
+#include "mozilla/WritingModes.h"
 #include "mozilla/dom/KeyboardEventBinding.h"
 #include "nsCommandParams.h"
 #include "nsContentUtils.h"
 #include "nsIContent.h"
 #include "nsIDragSession.h"
 #include "nsPrintfCString.h"
 
 #if defined(XP_WIN)
@@ -752,17 +755,18 @@ const char16_t* const WidgetKeyboardEven
 };
 #undef NS_DEFINE_PHYSICAL_KEY_CODE_NAME
 
 WidgetKeyboardEvent::KeyNameIndexHashtable*
     WidgetKeyboardEvent::sKeyNameIndexHashtable = nullptr;
 WidgetKeyboardEvent::CodeNameIndexHashtable*
     WidgetKeyboardEvent::sCodeNameIndexHashtable = nullptr;
 
-void WidgetKeyboardEvent::InitAllEditCommands() {
+void WidgetKeyboardEvent::InitAllEditCommands(
+    const Maybe<WritingMode>& aWritingMode) {
   // If this event is synthesized for tests, we don't need to retrieve the
   // command via the main process.  So, we don't need widget and can trust
   // the event.
   if (!mFlags.mIsSynthesizedForTests) {
     // If the event was created without widget, e.g., created event in chrome
     // script, this shouldn't execute native key bindings.
     if (NS_WARN_IF(!mWidget)) {
       return;
@@ -776,63 +780,69 @@ void WidgetKeyboardEvent::InitAllEditCom
 
     MOZ_ASSERT(
         XRE_IsParentProcess(),
         "It's too expensive to retrieve all edit commands from remote process");
     MOZ_ASSERT(!AreAllEditCommandsInitialized(),
                "Shouldn't be called two or more times");
   }
 
-  DebugOnly<bool> okIgnored =
-      InitEditCommandsFor(nsIWidget::NativeKeyBindingsForSingleLineEditor);
+  DebugOnly<bool> okIgnored = InitEditCommandsFor(
+      nsIWidget::NativeKeyBindingsForSingleLineEditor, aWritingMode);
   NS_WARNING_ASSERTION(
       okIgnored,
       "InitEditCommandsFor(nsIWidget::NativeKeyBindingsForSingleLineEditor) "
       "failed, but ignored");
-  okIgnored =
-      InitEditCommandsFor(nsIWidget::NativeKeyBindingsForMultiLineEditor);
+  okIgnored = InitEditCommandsFor(
+      nsIWidget::NativeKeyBindingsForMultiLineEditor, aWritingMode);
   NS_WARNING_ASSERTION(
       okIgnored,
       "InitEditCommandsFor(nsIWidget::NativeKeyBindingsForMultiLineEditor) "
       "failed, but ignored");
-  okIgnored =
-      InitEditCommandsFor(nsIWidget::NativeKeyBindingsForRichTextEditor);
+  okIgnored = InitEditCommandsFor(nsIWidget::NativeKeyBindingsForRichTextEditor,
+                                  aWritingMode);
   NS_WARNING_ASSERTION(
       okIgnored,
       "InitEditCommandsFor(nsIWidget::NativeKeyBindingsForRichTextEditor) "
       "failed, but ignored");
 }
 
 bool WidgetKeyboardEvent::InitEditCommandsFor(
-    nsIWidget::NativeKeyBindingsType aType) {
+    nsIWidget::NativeKeyBindingsType aType,
+    const Maybe<WritingMode>& aWritingMode) {
   bool& initialized = IsEditCommandsInitializedRef(aType);
   if (initialized) {
     return true;
   }
   nsTArray<CommandInt>& commands = EditCommandsRef(aType);
 
   // If this event is synthesized for tests, we shouldn't access customized
   // shortcut settings of the environment.  Therefore, we don't need to check
   // whether `widget` is set or not.  And we can treat synthesized events are
   // always trusted.
   if (mFlags.mIsSynthesizedForTests) {
     MOZ_DIAGNOSTIC_ASSERT(IsTrusted());
 #if defined(MOZ_WIDGET_GTK) || defined(XP_MACOSX)
     // TODO: We should implement `NativeKeyBindings` for Windows and Android
     //       too in bug 1301497 for getting rid of the #if.
-    widget::NativeKeyBindings::GetEditCommandsForTests(aType, *this, commands);
+    widget::NativeKeyBindings::GetEditCommandsForTests(aType, *this,
+                                                       aWritingMode, commands);
 #endif
     initialized = true;
     return true;
   }
 
   if (NS_WARN_IF(!mWidget) || NS_WARN_IF(!IsTrusted())) {
     return false;
   }
-  initialized = mWidget->GetEditCommands(aType, *this, commands);
+  // `nsIWidget::GetEditCommands()` will retrieve `WritingMode` at selection
+  // again, but it should be almost zero-cost since `TextEventDispatcher`
+  // caches the value.
+  nsCOMPtr<nsIWidget> widget = mWidget;
+  initialized = widget->GetEditCommands(aType, *this, commands);
   return initialized;
 }
 
 bool WidgetKeyboardEvent::ExecuteEditCommands(
     nsIWidget::NativeKeyBindingsType aType, DoCommandCallback aCallback,
     void* aCallbackData) {
   // If the event was created without widget, e.g., created event in chrome
   // script, this shouldn't execute native key bindings.
@@ -841,18 +851,25 @@ bool WidgetKeyboardEvent::ExecuteEditCom
   }
 
   // This event should be trusted event here and we shouldn't expose native
   // key binding information to web contents with untrusted events.
   if (NS_WARN_IF(!IsTrusted())) {
     return false;
   }
 
-  if (NS_WARN_IF(!InitEditCommandsFor(aType))) {
-    return false;
+  if (!IsEditCommandsInitializedRef(aType)) {
+    Maybe<WritingMode> writingMode;
+    if (RefPtr<widget::TextEventDispatcher> textEventDispatcher =
+            mWidget->GetTextEventDispatcher()) {
+      writingMode = textEventDispatcher->MaybeWritingModeAtSelection();
+    }
+    if (NS_WARN_IF(!InitEditCommandsFor(aType, writingMode))) {
+      return false;
+    }
   }
 
   const nsTArray<CommandInt>& commands = EditCommandsRef(aType);
   if (commands.IsEmpty()) {
     return false;
   }
 
   for (CommandInt command : commands) {
--- a/widget/cocoa/NativeKeyBindings.h
+++ b/widget/cocoa/NativeKeyBindings.h
@@ -9,16 +9,21 @@
 #include "mozilla/Attributes.h"
 #include "mozilla/EventForwards.h"
 #include "nsDataHashtable.h"
 #include "nsIWidget.h"
 
 struct objc_selector;
 
 namespace mozilla {
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
 namespace widget {
 
 typedef nsDataHashtable<nsPtrHashKey<objc_selector>, Command>
     SelectorCommandHashtable;
 
 class NativeKeyBindings final {
   typedef nsIWidget::NativeKeyBindingsType NativeKeyBindingsType;
 
@@ -28,21 +33,23 @@ class NativeKeyBindings final {
 
   /**
    * GetEditCommandsForTests() returns commands performed in native widget
    * in typical environment.  I.e., this does NOT refer customized shortcut
    * key mappings of the environment.
    */
   static void GetEditCommandsForTests(NativeKeyBindingsType aType,
                                       const WidgetKeyboardEvent& aEvent,
+                                      const Maybe<WritingMode>& aWritingMode,
                                       nsTArray<CommandInt>& aCommands);
 
   void Init(NativeKeyBindingsType aType);
 
   void GetEditCommands(const WidgetKeyboardEvent& aEvent,
+                       const Maybe<WritingMode>& aWritingMode,
                        nsTArray<CommandInt>& aCommands);
 
  private:
   NativeKeyBindings();
 
   void AppendEditCommandsForSelector(objc_selector* aSelector,
                                      nsTArray<CommandInt>& aCommands) const;
 
--- a/widget/cocoa/NativeKeyBindings.mm
+++ b/widget/cocoa/NativeKeyBindings.mm
@@ -3,17 +3,19 @@
  * 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/. */
 
 #include "NativeKeyBindings.h"
 
 #include "nsTArray.h"
 #include "nsCocoaUtils.h"
 #include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
 
 #import <Cocoa/Cocoa.h>
 
 namespace mozilla {
 namespace widget {
 
 static LazyLogModule gNativeKeyBindingsLog("NativeKeyBindings");
 
@@ -177,16 +179,17 @@ void NativeKeyBindings::Init(NativeKeyBi
   // SEL_TO_COMMAND(transposeWords:, );
   // SEL_TO_COMMAND(uppercaseWord:, );
   // SEL_TO_COMMAND(yank:, );
 }
 
 #undef SEL_TO_COMMAND
 
 void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
+                                        const Maybe<WritingMode>& aWritingMode,
                                         nsTArray<CommandInt>& aCommands) {
   MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
   MOZ_ASSERT(aCommands.IsEmpty());
 
   MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info, ("%p NativeKeyBindings::GetEditCommands", this));
 
   // Recover the current event, which should always be the key down we are
   // responding to.
@@ -195,16 +198,54 @@ void NativeKeyBindings::GetEditCommands(
 
   if (!cocoaEvent || [cocoaEvent type] != NSEventTypeKeyDown) {
     MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
             ("%p NativeKeyBindings::GetEditCommands, no Cocoa key down event", this));
 
     return;
   }
 
+  if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
+      aWritingMode.ref().IsVertical()) {
+    NSEvent* originalEvent = cocoaEvent;
+
+    // TODO: Use KeyNameIndex rather than legacy keyCode.
+    uint32_t remappedGeckoKeyCode = aEvent.GetRemappedKeyCode(aWritingMode.ref());
+    uint32_t remappedCocoaKeyCode = 0;
+    switch (remappedGeckoKeyCode) {
+      case NS_VK_UP:
+        remappedCocoaKeyCode = kVK_UpArrow;
+        break;
+      case NS_VK_DOWN:
+        remappedCocoaKeyCode = kVK_DownArrow;
+        break;
+      case NS_VK_LEFT:
+        remappedCocoaKeyCode = kVK_LeftArrow;
+        break;
+      case NS_VK_RIGHT:
+        remappedCocoaKeyCode = kVK_RightArrow;
+        break;
+      default:
+        MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+        return;
+    }
+    unichar ch = nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(remappedGeckoKeyCode);
+    NSString* chars = [[[NSString alloc] initWithCharacters:&ch length:1] autorelease];
+    cocoaEvent = [NSEvent keyEventWithType:[originalEvent type]
+                                  location:[originalEvent locationInWindow]
+                             modifierFlags:[originalEvent modifierFlags]
+                                 timestamp:[originalEvent timestamp]
+                              windowNumber:[originalEvent windowNumber]
+                                   context:[originalEvent context]
+                                characters:chars
+               charactersIgnoringModifiers:chars
+                                 isARepeat:[originalEvent isARepeat]
+                                   keyCode:remappedCocoaKeyCode];
+  }
+
   MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
           ("%p NativeKeyBindings::GetEditCommands, interpreting", this));
 
   AutoTArray<KeyBindingsCommand, 2> bindingCommands;
   nsCocoaUtils::GetCommandsFromKeyEvent(cocoaEvent, bindingCommands);
 
   MOZ_LOG(gNativeKeyBindingsLog, LogLevel::Info,
           ("%p NativeKeyBindings::GetEditCommands, bindingCommands=%zu", this,
@@ -267,26 +308,28 @@ void NativeKeyBindings::LogEditCommands(
             ("%p %s, command=%s", this, aDescription,
              WidgetKeyboardEvent::GetCommandStr(geckoCommand)));
   }
 }
 
 // static
 void NativeKeyBindings::GetEditCommandsForTests(NativeKeyBindingsType aType,
                                                 const WidgetKeyboardEvent& aEvent,
+                                                const Maybe<WritingMode>& aWritingMode,
                                                 nsTArray<CommandInt>& aCommands) {
   MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
 
   // The following mapping is checked on Big Sur. Some of them are defined in:
   // https://support.apple.com/en-us/HT201236#text
   NativeKeyBindings* instance = NativeKeyBindings::GetInstance(aType);
   if (NS_WARN_IF(!instance)) {
     return;
   }
-  switch (aEvent.mKeyNameIndex) {
+  switch (aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
+                                : aEvent.mKeyNameIndex) {
     case KEY_NAME_INDEX_USE_STRING:
       if (!aEvent.IsControl() || aEvent.IsAlt() || aEvent.IsMeta()) {
         break;
       }
       switch (aEvent.PseudoCharCode()) {
         case 'a':
         case 'A':
           instance->AppendEditCommandsForSelector(
--- a/widget/cocoa/nsChildView.h
+++ b/widget/cocoa/nsChildView.h
@@ -370,23 +370,19 @@ class nsChildView final : public nsBaseW
   [[nodiscard]] virtual nsresult GetSelectionAsPlaintext(nsAString& aResult) override;
 
   virtual void SetInputContext(const InputContext& aContext,
                                const InputContextAction& aAction) override;
   virtual InputContext GetInputContext() override;
   virtual TextEventDispatcherListener* GetNativeTextEventDispatcherListener() override;
   [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
       mozilla::WidgetKeyboardEvent& aEvent) override;
-  virtual bool GetEditCommands(NativeKeyBindingsType aType,
-                               const mozilla::WidgetKeyboardEvent& aEvent,
-                               nsTArray<mozilla::CommandInt>& aCommands) override;
-  void GetEditCommandsRemapped(NativeKeyBindingsType aType,
-                               const mozilla::WidgetKeyboardEvent& aEvent,
-                               nsTArray<mozilla::CommandInt>& aCommands, uint32_t aGeckoKeyCode,
-                               uint32_t aCocoaKeyCode);
+  MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+      NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
+      nsTArray<mozilla::CommandInt>& aCommands) override;
 
   virtual void SuppressAnimation(bool aSuppress) override;
 
   virtual nsresult SynthesizeNativeKeyEvent(int32_t aNativeKeyboardLayout, int32_t aNativeKeyCode,
                                             uint32_t aModifierFlags, const nsAString& aCharacters,
                                             const nsAString& aUnmodifiedCharacters,
                                             nsIObserver* aObserver) override;
 
--- a/widget/cocoa/nsChildView.mm
+++ b/widget/cocoa/nsChildView.mm
@@ -9,22 +9,25 @@
 #include "mozilla/Unused.h"
 
 #include <unistd.h>
 #include <math.h>
 
 #include "nsChildView.h"
 #include "nsCocoaWindow.h"
 
+#include "mozilla/Maybe.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/PresShell.h"
+#include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TouchEvents.h"
 #include "mozilla/WheelHandlingHelper.h"  // for WheelDeltaAdjustmentStrategy
+#include "mozilla/WritingModes.h"
 #include "mozilla/dom/DataTransfer.h"
 #include "mozilla/dom/MouseEventBinding.h"
 #include "mozilla/dom/SimpleGestureEventBinding.h"
 #include "mozilla/dom/WheelEventBinding.h"
 
 #include "nsArrayUtils.h"
 #include "nsExceptionHandler.h"
 #include "nsObjCExceptions.h"
@@ -1530,103 +1533,32 @@ TextEventDispatcherListener* nsChildView
   return mTextInputHandler;
 }
 
 nsresult nsChildView::AttachNativeKeyEvent(mozilla::WidgetKeyboardEvent& aEvent) {
   NS_ENSURE_TRUE(mTextInputHandler, NS_ERROR_NOT_AVAILABLE);
   return mTextInputHandler->AttachNativeKeyEvent(aEvent);
 }
 
-void nsChildView::GetEditCommandsRemapped(NativeKeyBindingsType aType,
-                                          const WidgetKeyboardEvent& aEvent,
-                                          nsTArray<CommandInt>& aCommands, uint32_t aGeckoKeyCode,
-                                          uint32_t aCocoaKeyCode) {
-  NSEvent* originalEvent = reinterpret_cast<NSEvent*>(aEvent.mNativeKeyEvent);
-
-  WidgetKeyboardEvent modifiedEvent(aEvent);
-  modifiedEvent.mKeyCode = aGeckoKeyCode;
-
-  unichar ch = nsCocoaUtils::ConvertGeckoKeyCodeToMacCharCode(aGeckoKeyCode);
-  NSString* chars = [[[NSString alloc] initWithCharacters:&ch length:1] autorelease];
-
-  modifiedEvent.mNativeKeyEvent = [NSEvent keyEventWithType:[originalEvent type]
-                                                   location:[originalEvent locationInWindow]
-                                              modifierFlags:[originalEvent modifierFlags]
-                                                  timestamp:[originalEvent timestamp]
-                                               windowNumber:[originalEvent windowNumber]
-                                                    context:[originalEvent context]
-                                                 characters:chars
-                                charactersIgnoringModifiers:chars
-                                                  isARepeat:[originalEvent isARepeat]
-                                                    keyCode:aCocoaKeyCode];
-
-  NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
-  keyBindings->GetEditCommands(modifiedEvent, aCommands);
-}
-
 bool nsChildView::GetEditCommands(NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
                                   nsTArray<CommandInt>& aCommands) {
   // Validate the arguments.
   if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
     return false;
   }
 
-  // If the key is a cursor-movement arrow, and the current selection has
-  // vertical writing-mode, we'll remap so that the movement command
-  // generated (in terms of characters/lines) will be appropriate for
-  // the physical direction of the arrow.
-  if (aEvent.mKeyCode >= NS_VK_LEFT && aEvent.mKeyCode <= NS_VK_DOWN) {
-    // XXX This may be expensive. Should use the cache in TextInputHandler.
-    WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, this);
-    DispatchWindowEvent(querySelectedTextEvent);
-
-    if (querySelectedTextEvent.FoundSelection() &&
-        querySelectedTextEvent.mReply->mWritingMode.IsVertical()) {
-      uint32_t geckoKey = 0;
-      uint32_t cocoaKey = 0;
-
-      switch (aEvent.mKeyCode) {
-        case NS_VK_LEFT:
-          if (querySelectedTextEvent.mReply->mWritingMode.IsVerticalLR()) {
-            geckoKey = NS_VK_UP;
-            cocoaKey = kVK_UpArrow;
-          } else {
-            geckoKey = NS_VK_DOWN;
-            cocoaKey = kVK_DownArrow;
-          }
-          break;
-
-        case NS_VK_RIGHT:
-          if (querySelectedTextEvent.mReply->mWritingMode.IsVerticalLR()) {
-            geckoKey = NS_VK_DOWN;
-            cocoaKey = kVK_DownArrow;
-          } else {
-            geckoKey = NS_VK_UP;
-            cocoaKey = kVK_UpArrow;
-          }
-          break;
-
-        case NS_VK_UP:
-          geckoKey = NS_VK_LEFT;
-          cocoaKey = kVK_LeftArrow;
-          break;
-
-        case NS_VK_DOWN:
-          geckoKey = NS_VK_RIGHT;
-          cocoaKey = kVK_RightArrow;
-          break;
-      }
-
-      GetEditCommandsRemapped(aType, aEvent, aCommands, geckoKey, cocoaKey);
-      return true;
+  Maybe<WritingMode> writingMode;
+  if (aEvent.NeedsToRemapNavigationKey()) {
+    if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+      writingMode = dispatcher->MaybeWritingModeAtSelection();
     }
   }
 
   NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
-  keyBindings->GetEditCommands(aEvent, aCommands);
+  keyBindings->GetEditCommands(aEvent, writingMode, aCommands);
   return true;
 }
 
 NSView<mozView>* nsChildView::GetEditorView() {
   NSView<mozView>* editorView = mView;
   // We need to get editor's view. E.g., when the focus is in the bookmark
   // dialog, the view is <panel> element of the dialog.  At this time, the key
   // events are processed the parent window's view that has native focus.
--- a/widget/cocoa/nsCocoaWindow.h
+++ b/widget/cocoa/nsCocoaWindow.h
@@ -333,19 +333,19 @@ class nsCocoaWindow final : public nsBas
   NSWindow* GetCocoaWindow() { return mWindow; }
 
   void SetMenuBar(nsMenuBarX* aMenuBar);
   nsMenuBarX* GetMenuBar();
 
   virtual void SetInputContext(const InputContext& aContext,
                                const InputContextAction& aAction) override;
   virtual InputContext GetInputContext() override { return mInputContext; }
-  virtual bool GetEditCommands(NativeKeyBindingsType aType,
-                               const mozilla::WidgetKeyboardEvent& aEvent,
-                               nsTArray<mozilla::CommandInt>& aCommands) override;
+  MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+      NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
+      nsTArray<mozilla::CommandInt>& aCommands) override;
 
   void SetPopupWindowLevel();
 
   bool InFullScreenMode() const { return mInFullScreenMode; }
 
   void PauseCompositor();
   void ResumeCompositor();
 
--- a/widget/cocoa/nsCocoaWindow.mm
+++ b/widget/cocoa/nsCocoaWindow.mm
@@ -37,20 +37,22 @@
 #include "nsPresContext.h"
 #include "nsDocShell.h"
 
 #include "gfxPlatform.h"
 #include "qcms.h"
 
 #include "mozilla/AutoRestore.h"
 #include "mozilla/BasicEvents.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/Preferences.h"
+#include "mozilla/PresShell.h"
 #include "mozilla/StaticPrefs_gfx.h"
 #include "mozilla/StaticPrefs_widget.h"
-#include "mozilla/PresShell.h"
+#include "mozilla/WritingModes.h"
 #include "mozilla/layers/CompositorBridgeChild.h"
 #include <algorithm>
 
 namespace mozilla {
 namespace layers {
 class LayerManager;
 }  // namespace layers
 }  // namespace mozilla
@@ -2589,17 +2591,21 @@ void nsCocoaWindow::SetInputContext(cons
 bool nsCocoaWindow::GetEditCommands(NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
                                     nsTArray<CommandInt>& aCommands) {
   // Validate the arguments.
   if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
     return false;
   }
 
   NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
-  keyBindings->GetEditCommands(aEvent, aCommands);
+  // When the keyboard event is fired from this widget, it must mean that no web content has focus
+  // because any web contents should be on `nsChildView`.  And in any locales, the system UI is
+  // always horizontal layout.  So, let's pass `Nothing()` for the writing mode here, it won't be
+  // treated as in a vertical content.
+  keyBindings->GetEditCommands(aEvent, Nothing(), aCommands);
   return true;
 }
 
 already_AddRefed<nsIWidget> nsIWidget::CreateTopLevelWindow() {
   nsCOMPtr<nsIWidget> window = new nsCocoaWindow();
   return window.forget();
 }
 
--- a/widget/gtk/NativeKeyBindings.cpp
+++ b/widget/gtk/NativeKeyBindings.cpp
@@ -1,24 +1,27 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/MathAlgorithms.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
 
 #include "NativeKeyBindings.h"
 #include "nsString.h"
 #include "nsMemory.h"
 #include "nsGtkKeyUtils.h"
 
 #include <gtk/gtk.h>
 #include <gdk/gdkkeysyms.h>
+#include <gdk/gdkkeysyms-compat.h>
 #include <gdk/gdk.h>
 
 namespace mozilla {
 namespace widget {
 
 static nsTArray<CommandInt>* gCurrentCommands = nullptr;
 static bool gHandled = false;
 
@@ -271,29 +274,51 @@ void NativeKeyBindings::Init(NativeKeyBi
 }
 
 NativeKeyBindings::~NativeKeyBindings() {
   gtk_widget_destroy(mNativeTarget);
   g_object_unref(mNativeTarget);
 }
 
 void NativeKeyBindings::GetEditCommands(const WidgetKeyboardEvent& aEvent,
+                                        const Maybe<WritingMode>& aWritingMode,
                                         nsTArray<CommandInt>& aCommands) {
   MOZ_ASSERT(!aEvent.mFlags.mIsSynthesizedForTests);
   MOZ_ASSERT(aCommands.IsEmpty());
 
   // It must be a DOM event dispached by chrome script.
   if (!aEvent.mNativeKeyEvent) {
     return;
   }
 
   guint keyval;
-
   if (aEvent.mCharCode) {
     keyval = gdk_unicode_to_keyval(aEvent.mCharCode);
+  } else if (aWritingMode.isSome() && aEvent.NeedsToRemapNavigationKey() &&
+             aWritingMode.ref().IsVertical()) {
+    // TODO: Use KeyNameIndex rather than legacy keyCode.
+    uint32_t remappedGeckoKeyCode =
+        aEvent.GetRemappedKeyCode(aWritingMode.ref());
+    switch (remappedGeckoKeyCode) {
+      case NS_VK_UP:
+        keyval = GDK_Up;
+        break;
+      case NS_VK_DOWN:
+        keyval = GDK_Down;
+        break;
+      case NS_VK_LEFT:
+        keyval = GDK_Left;
+        break;
+      case NS_VK_RIGHT:
+        keyval = GDK_Right;
+        break;
+      default:
+        MOZ_ASSERT_UNREACHABLE("Add a case for the new remapped key");
+        return;
+    }
   } else {
     keyval = static_cast<GdkEventKey*>(aEvent.mNativeKeyEvent)->keyval;
   }
 
   if (GetEditCommandsInternal(aEvent, aCommands, keyval)) {
     return;
   }
 
@@ -340,30 +365,33 @@ bool NativeKeyBindings::GetEditCommandsI
   MOZ_ASSERT(!gHandled || !aCommands.IsEmpty());
 
   return gHandled;
 }
 
 // static
 void NativeKeyBindings::GetEditCommandsForTests(
     NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
-    nsTArray<CommandInt>& aCommands) {
+    const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
   MOZ_DIAGNOSTIC_ASSERT(aEvent.IsTrusted());
 
   if (aEvent.IsAlt() || aEvent.IsMeta() || aEvent.IsOS()) {
     return;
   }
 
   static const size_t kBackward = 0;
   static const size_t kForward = 1;
   const size_t extentSelection = aEvent.IsShift() ? 1 : 0;
   // https://github.com/GNOME/gtk/blob/1f141c19533f4b3f397c3959ade673ce243b6138/gtk/gtktext.c#L1289
   // https://github.com/GNOME/gtk/blob/c5dd34344f0c660ceffffb3bf9da43c263db16e1/gtk/gtktextview.c#L1534
   Command command = Command::DoNothing;
-  switch (aEvent.mKeyNameIndex) {
+  const KeyNameIndex remappedKeyNameIndex =
+      aWritingMode.isSome() ? aEvent.GetRemappedKeyNameIndex(aWritingMode.ref())
+                            : aEvent.mKeyNameIndex;
+  switch (remappedKeyNameIndex) {
     case KEY_NAME_INDEX_USE_STRING:
       switch (aEvent.PseudoCharCode()) {
         case 'a':
         case 'A':
           if (aEvent.IsControl()) {
             command = Command::SelectAll;
           }
           break;
@@ -411,60 +439,60 @@ void NativeKeyBindings::GetEditCommandsF
     case KEY_NAME_INDEX_Delete:
       if (aEvent.IsShift()) {
         command = Command::Cut;
         break;
       }
       [[fallthrough]];
     case KEY_NAME_INDEX_Backspace: {
       const size_t direction =
-          aEvent.mKeyNameIndex == KEY_NAME_INDEX_Delete ? kForward : kBackward;
+          remappedKeyNameIndex == KEY_NAME_INDEX_Delete ? kForward : kBackward;
       const GtkDeleteType amount =
           aEvent.IsControl() && aEvent.IsShift()
               ? GTK_DELETE_PARAGRAPH_ENDS
               // FYI: Shift key for Backspace is ignored to help mis-typing.
               : (aEvent.IsControl() ? GTK_DELETE_WORD_ENDS : GTK_DELETE_CHARS);
       command = sDeleteCommands[amount][direction];
       break;
     }
     case KEY_NAME_INDEX_ArrowLeft:
     case KEY_NAME_INDEX_ArrowRight: {
-      const size_t direction = aEvent.mKeyNameIndex == KEY_NAME_INDEX_ArrowRight
+      const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowRight
                                    ? kForward
                                    : kBackward;
       const GtkMovementStep amount = aEvent.IsControl()
                                          ? GTK_MOVEMENT_WORDS
                                          : GTK_MOVEMENT_VISUAL_POSITIONS;
       command = sMoveCommands[amount][extentSelection][direction];
       break;
     }
     case KEY_NAME_INDEX_ArrowUp:
     case KEY_NAME_INDEX_ArrowDown: {
-      const size_t direction = aEvent.mKeyNameIndex == KEY_NAME_INDEX_ArrowDown
+      const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_ArrowDown
                                    ? kForward
                                    : kBackward;
       const GtkMovementStep amount = aEvent.IsControl()
                                          ? GTK_MOVEMENT_PARAGRAPHS
                                          : GTK_MOVEMENT_DISPLAY_LINES;
       command = sMoveCommands[amount][extentSelection][direction];
       break;
     }
     case KEY_NAME_INDEX_Home:
     case KEY_NAME_INDEX_End: {
       const size_t direction =
-          aEvent.mKeyNameIndex == KEY_NAME_INDEX_End ? kForward : kBackward;
+          remappedKeyNameIndex == KEY_NAME_INDEX_End ? kForward : kBackward;
       const GtkMovementStep amount = aEvent.IsControl()
                                          ? GTK_MOVEMENT_BUFFER_ENDS
                                          : GTK_MOVEMENT_DISPLAY_LINE_ENDS;
       command = sMoveCommands[amount][extentSelection][direction];
       break;
     }
     case KEY_NAME_INDEX_PageUp:
     case KEY_NAME_INDEX_PageDown: {
-      const size_t direction = aEvent.mKeyNameIndex == KEY_NAME_INDEX_PageDown
+      const size_t direction = remappedKeyNameIndex == KEY_NAME_INDEX_PageDown
                                    ? kForward
                                    : kBackward;
       const GtkMovementStep amount = aEvent.IsControl()
                                          ? GTK_MOVEMENT_HORIZONTAL_PAGES
                                          : GTK_MOVEMENT_PAGES;
       command = sMoveCommands[amount][extentSelection][direction];
       break;
     }
--- a/widget/gtk/NativeKeyBindings.h
+++ b/widget/gtk/NativeKeyBindings.h
@@ -10,37 +10,44 @@
 #include "mozilla/EventForwards.h"
 #include "nsIWidget.h"
 
 #include <glib.h>  // for guint
 
 using GtkWidget = struct _GtkWidget;
 
 namespace mozilla {
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
 namespace widget {
 
 class NativeKeyBindings final {
   typedef nsIWidget::NativeKeyBindingsType NativeKeyBindingsType;
 
  public:
   static NativeKeyBindings* GetInstance(NativeKeyBindingsType aType);
   static void Shutdown();
 
   /**
    * GetEditCommandsForTests() returns commands performed in native widget
    * in typical environment.  I.e., this does NOT refer customized shortcut
    * key mappings of the environment.
    */
   static void GetEditCommandsForTests(NativeKeyBindingsType aType,
                                       const WidgetKeyboardEvent& aEvent,
+                                      const Maybe<WritingMode>& aWritingMode,
                                       nsTArray<CommandInt>& aCommands);
 
   void Init(NativeKeyBindingsType aType);
 
   void GetEditCommands(const WidgetKeyboardEvent& aEvent,
+                       const Maybe<WritingMode>& aWritingMode,
                        nsTArray<CommandInt>& aCommands);
 
  private:
   ~NativeKeyBindings();
 
   bool GetEditCommandsInternal(const WidgetKeyboardEvent& aEvent,
                                nsTArray<CommandInt>& aCommands, guint aKeyval);
 
--- a/widget/gtk/nsWindow.cpp
+++ b/widget/gtk/nsWindow.cpp
@@ -4,28 +4,31 @@
 /* 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/. */
 
 #include "nsWindow.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/EventForwards.h"
+#include "mozilla/Maybe.h"
 #include "mozilla/MiscEvents.h"
 #include "mozilla/MouseEvents.h"
 #include "mozilla/PresShell.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/StaticPrefs_apz.h"
 #include "mozilla/StaticPrefs_ui.h"
+#include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/TouchEvents.h"
 #include "mozilla/UniquePtrExtensions.h"
 #include "mozilla/WidgetUtils.h"
+#include "mozilla/WritingModes.h"
 #include "mozilla/X11Util.h"
 #include "mozilla/XREAppData.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/WheelEventBinding.h"
 #include "InputData.h"
 #include "nsAppRunner.h"
 #include <algorithm>
 
@@ -7422,96 +7425,33 @@ InputContext nsWindow::GetInputContext()
 
 TextEventDispatcherListener* nsWindow::GetNativeTextEventDispatcherListener() {
   if (NS_WARN_IF(!mIMContext)) {
     return nullptr;
   }
   return mIMContext;
 }
 
-void nsWindow::GetEditCommandsRemapped(NativeKeyBindingsType aType,
-                                       const WidgetKeyboardEvent& aEvent,
-                                       nsTArray<CommandInt>& aCommands,
-                                       uint32_t aGeckoKeyCode,
-                                       uint32_t aNativeKeyCode) {
-  // If aEvent.mNativeKeyEvent is nullptr, the event was created by chrome
-  // script.  In such case, we shouldn't expose the OS settings to it.
-  // So, just ignore such events here.
-  if (!aEvent.mNativeKeyEvent) {
-    return;
-  }
-  WidgetKeyboardEvent modifiedEvent(aEvent);
-  modifiedEvent.mKeyCode = aGeckoKeyCode;
-  static_cast<GdkEventKey*>(modifiedEvent.mNativeKeyEvent)->keyval =
-      aNativeKeyCode;
-
-  NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
-  keyBindings->GetEditCommands(modifiedEvent, aCommands);
-}
-
 bool nsWindow::GetEditCommands(NativeKeyBindingsType aType,
                                const WidgetKeyboardEvent& aEvent,
                                nsTArray<CommandInt>& aCommands) {
   // Validate the arguments.
   if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
     return false;
   }
 
-  if (aEvent.mKeyCode >= NS_VK_LEFT && aEvent.mKeyCode <= NS_VK_DOWN) {
-    // Check if we're targeting content with vertical writing mode,
-    // and if so remap the arrow keys.
-    // XXX This may be expensive.
-    WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
-                                                   this);
-    nsEventStatus status;
-    DispatchEvent(&querySelectedTextEvent, status);
-
-    if (querySelectedTextEvent.FoundSelection() &&
-        querySelectedTextEvent.mReply->mWritingMode.IsVertical()) {
-      uint32_t geckoCode = 0;
-      uint32_t gdkCode = 0;
-      switch (aEvent.mKeyCode) {
-        case NS_VK_LEFT:
-          if (querySelectedTextEvent.mReply->mWritingMode.IsVerticalLR()) {
-            geckoCode = NS_VK_UP;
-            gdkCode = GDK_Up;
-          } else {
-            geckoCode = NS_VK_DOWN;
-            gdkCode = GDK_Down;
-          }
-          break;
-
-        case NS_VK_RIGHT:
-          if (querySelectedTextEvent.mReply->mWritingMode.IsVerticalLR()) {
-            geckoCode = NS_VK_DOWN;
-            gdkCode = GDK_Down;
-          } else {
-            geckoCode = NS_VK_UP;
-            gdkCode = GDK_Up;
-          }
-          break;
-
-        case NS_VK_UP:
-          geckoCode = NS_VK_LEFT;
-          gdkCode = GDK_Left;
-          break;
-
-        case NS_VK_DOWN:
-          geckoCode = NS_VK_RIGHT;
-          gdkCode = GDK_Right;
-          break;
-      }
-
-      GetEditCommandsRemapped(aType, aEvent, aCommands, geckoCode, gdkCode);
-      return true;
+  Maybe<WritingMode> writingMode;
+  if (aEvent.NeedsToRemapNavigationKey()) {
+    if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+      writingMode = dispatcher->MaybeWritingModeAtSelection();
     }
   }
 
   NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
-  keyBindings->GetEditCommands(aEvent, aCommands);
+  keyBindings->GetEditCommands(aEvent, writingMode, aCommands);
   return true;
 }
 
 already_AddRefed<DrawTarget> nsWindow::StartRemoteDrawingInRegion(
     LayoutDeviceIntRegion& aInvalidRegion, BufferMode* aBufferMode) {
   return mSurfaceProvider.StartRemoteDrawingInRegion(aInvalidRegion,
                                                      aBufferMode);
 }
--- a/widget/gtk/nsWindow.h
+++ b/widget/gtk/nsWindow.h
@@ -314,21 +314,17 @@ class nsWindow final : public nsBaseWidg
   mozilla::TimeStamp GetEventTimeStamp(guint32 aEventTime);
   mozilla::CurrentX11TimeGetter* GetCurrentTimeGetter();
 
   virtual void SetInputContext(const InputContext& aContext,
                                const InputContextAction& aAction) override;
   virtual InputContext GetInputContext() override;
   virtual TextEventDispatcherListener* GetNativeTextEventDispatcherListener()
       override;
-  void GetEditCommandsRemapped(NativeKeyBindingsType aType,
-                               const mozilla::WidgetKeyboardEvent& aEvent,
-                               nsTArray<mozilla::CommandInt>& aCommands,
-                               uint32_t aGeckoKeyCode, uint32_t aNativeKeyCode);
-  virtual bool GetEditCommands(
+  MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
       NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
       nsTArray<mozilla::CommandInt>& aCommands) override;
 
   // These methods are for toplevel windows only.
   void ResizeTransparencyBitmap();
   void ApplyTransparencyBitmap();
   void ClearTransparencyBitmap();
 
--- a/widget/headless/HeadlessKeyBindings.cpp
+++ b/widget/headless/HeadlessKeyBindings.cpp
@@ -1,15 +1,17 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
  * 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/. */
 
 #include "HeadlessKeyBindings.h"
 #include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/WritingModes.h"
 
 namespace mozilla {
 namespace widget {
 
 HeadlessKeyBindings& HeadlessKeyBindings::GetInstance() {
   static UniquePtr<HeadlessKeyBindings> sInstance;
   if (!sInstance) {
     sInstance.reset(new HeadlessKeyBindings());
@@ -21,14 +23,14 @@ HeadlessKeyBindings& HeadlessKeyBindings
 nsresult HeadlessKeyBindings::AttachNativeKeyEvent(
     WidgetKeyboardEvent& aEvent) {
   // Stub for non-mac platforms.
   return NS_OK;
 }
 
 void HeadlessKeyBindings::GetEditCommands(
     nsIWidget::NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
-    nsTArray<CommandInt>& aCommands) {
+    const Maybe<WritingMode>& aWritingMode, nsTArray<CommandInt>& aCommands) {
   // Stub for non-mac platforms.
 }
 
 }  // namespace widget
 }  // namespace mozilla
--- a/widget/headless/HeadlessKeyBindings.h
+++ b/widget/headless/HeadlessKeyBindings.h
@@ -6,30 +6,36 @@
 #ifndef mozilla_widget_HeadlessKeyBindings_h
 #define mozilla_widget_HeadlessKeyBindings_h
 
 #include "mozilla/TextEvents.h"
 #include "nsIWidget.h"
 #include "nsTArray.h"
 
 namespace mozilla {
+
+class WritingMode;
+template <typename T>
+class Maybe;
+
 namespace widget {
 
 /**
  * Helper to emulate native key bindings. Currently only MacOS is supported.
  */
 
 class HeadlessKeyBindings final {
  public:
   HeadlessKeyBindings() = default;
 
   static HeadlessKeyBindings& GetInstance();
 
   void GetEditCommands(nsIWidget::NativeKeyBindingsType aType,
                        const WidgetKeyboardEvent& aEvent,
+                       const Maybe<WritingMode>& aWritingMode,
                        nsTArray<CommandInt>& aCommands);
   [[nodiscard]] nsresult AttachNativeKeyEvent(WidgetKeyboardEvent& aEvent);
 };
 
 }  // namespace widget
 }  // namespace mozilla
 
 #endif  // mozilla_widget_HeadlessKeyBindings_h
--- a/widget/headless/HeadlessKeyBindingsCocoa.mm
+++ b/widget/headless/HeadlessKeyBindingsCocoa.mm
@@ -3,16 +3,18 @@
  * 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/. */
 
 #include "HeadlessKeyBindings.h"
 #import <Cocoa/Cocoa.h>
 #include "nsCocoaUtils.h"
 #include "NativeKeyBindings.h"
 #include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/WritingModes.h"
 
 namespace mozilla {
 namespace widget {
 
 HeadlessKeyBindings& HeadlessKeyBindings::GetInstance() {
   static UniquePtr<HeadlessKeyBindings> sInstance;
   if (!sInstance) {
     sInstance.reset(new HeadlessKeyBindings());
@@ -28,20 +30,21 @@ nsresult HeadlessKeyBindings::AttachNati
 
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 void HeadlessKeyBindings::GetEditCommands(nsIWidget::NativeKeyBindingsType aType,
                                           const WidgetKeyboardEvent& aEvent,
+                                          const Maybe<WritingMode>& aWritingMode,
                                           nsTArray<CommandInt>& aCommands) {
   // Convert the widget keyboard into a cocoa event so it can be translated
   // into commands in the NativeKeyBindings.
   WidgetKeyboardEvent modifiedEvent(aEvent);
   modifiedEvent.mNativeKeyEvent = nsCocoaUtils::MakeNewCococaEventFromWidgetEvent(aEvent, 0, nil);
 
   NativeKeyBindings* keyBindings = NativeKeyBindings::GetInstance(aType);
-  keyBindings->GetEditCommands(modifiedEvent, aCommands);
+  keyBindings->GetEditCommands(modifiedEvent, aWritingMode, aCommands);
 }
 
 }  // namespace widget
 }  // namespace mozilla
--- a/widget/headless/HeadlessWidget.cpp
+++ b/widget/headless/HeadlessWidget.cpp
@@ -5,17 +5,20 @@
 #include "HeadlessWidget.h"
 #include "HeadlessCompositorWidget.h"
 #include "Layers.h"
 #include "BasicLayers.h"
 #include "BasicEvents.h"
 #include "MouseEvents.h"
 #include "mozilla/gfx/gfxVars.h"
 #include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TextEventDispatcher.h"
 #include "mozilla/TextEvents.h"
+#include "mozilla/WritingModes.h"
 #include "mozilla/widget/HeadlessWidgetTypes.h"
 #include "mozilla/widget/PlatformWidgetTypes.h"
 #include "nsIScreen.h"
 #include "HeadlessKeyBindings.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
@@ -395,18 +398,25 @@ nsresult HeadlessWidget::AttachNativeKey
 bool HeadlessWidget::GetEditCommands(NativeKeyBindingsType aType,
                                      const WidgetKeyboardEvent& aEvent,
                                      nsTArray<CommandInt>& aCommands) {
   // Validate the arguments.
   if (NS_WARN_IF(!nsIWidget::GetEditCommands(aType, aEvent, aCommands))) {
     return false;
   }
 
+  Maybe<WritingMode> writingMode;
+  if (aEvent.NeedsToRemapNavigationKey()) {
+    if (RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher()) {
+      writingMode = dispatcher->MaybeWritingModeAtSelection();
+    }
+  }
+
   HeadlessKeyBindings& bindings = HeadlessKeyBindings::GetInstance();
-  bindings.GetEditCommands(aType, aEvent, aCommands);
+  bindings.GetEditCommands(aType, aEvent, writingMode, aCommands);
   return true;
 }
 
 nsresult HeadlessWidget::DispatchEvent(WidgetGUIEvent* aEvent,
                                        nsEventStatus& aStatus) {
 #ifdef DEBUG
   debug_DumpEvent(stdout, aEvent->mWidget, aEvent, "HeadlessWidget", 0);
 #endif
--- a/widget/headless/HeadlessWidget.h
+++ b/widget/headless/HeadlessWidget.h
@@ -119,19 +119,19 @@ class HeadlessWidget : public nsBaseWidg
       PLayerTransactionChild* aShadowManager = nullptr,
       LayersBackend aBackendHint = mozilla::layers::LayersBackend::LAYERS_NONE,
       LayerManagerPersistence aPersistence = LAYER_MANAGER_CURRENT) override;
 
   void SetCompositorWidgetDelegate(CompositorWidgetDelegate* delegate) override;
 
   [[nodiscard]] virtual nsresult AttachNativeKeyEvent(
       WidgetKeyboardEvent& aEvent) override;
-  virtual bool GetEditCommands(NativeKeyBindingsType aType,
-                               const WidgetKeyboardEvent& aEvent,
-                               nsTArray<CommandInt>& aCommands) override;
+  MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+      NativeKeyBindingsType aType, const WidgetKeyboardEvent& aEvent,
+      nsTArray<CommandInt>& aCommands) override;
 
   virtual nsresult DispatchEvent(WidgetGUIEvent* aEvent,
                                  nsEventStatus& aStatus) override;
 
   virtual nsresult SynthesizeNativeMouseEvent(LayoutDeviceIntPoint aPoint,
                                               uint32_t aNativeMessage,
                                               uint32_t aModifierFlags,
                                               nsIObserver* aObserver) override;
--- a/widget/nsIWidget.h
+++ b/widget/nsIWidget.h
@@ -1878,19 +1878,19 @@ class nsIWidget : public nsISupports {
    * Retrieve edit commands when the key combination of aEvent is used
    * in platform native applications.
    */
   enum NativeKeyBindingsType : uint8_t {
     NativeKeyBindingsForSingleLineEditor,
     NativeKeyBindingsForMultiLineEditor,
     NativeKeyBindingsForRichTextEditor
   };
-  virtual bool GetEditCommands(NativeKeyBindingsType aType,
-                               const mozilla::WidgetKeyboardEvent& aEvent,
-                               nsTArray<mozilla::CommandInt>& aCommands);
+  MOZ_CAN_RUN_SCRIPT virtual bool GetEditCommands(
+      NativeKeyBindingsType aType, const mozilla::WidgetKeyboardEvent& aEvent,
+      nsTArray<mozilla::CommandInt>& aCommands);
 
   /*
    * Retrieves a reference to notification requests of IME.  Note that the
    * reference is valid while the nsIWidget instance is alive.  So, if you
    * need to store the reference for a long time, you need to grab the widget
    * instance too.
    */
   const IMENotificationRequests& IMENotificationRequestsRef();