editor/libeditor/EditorCommands.cpp
author stransky <stransky@redhat.com>
Thu, 21 Oct 2021 13:15:02 +0000
changeset 596578 aceba6246b0e588f6fb1071dabb4d97159306b3c
parent 582403 cc22e0885eb911718a4c4ab7b5be6891fbff7d56
permissions -rw-r--r--
Bug 1735095 [Wayland] When font/text scale factor is used, don't reposition popups for +-1 pixel moves, r=jhorak When font scale is used, move-to-rect popup positiong may end up in endless loop due to rounding error while popup position oscilates around layout position. We don't have a fox for that so let's disable such small popup movements when font scale is used. Depends on D129153 Differential Revision: https://phabricator.services.mozilla.com/D129154

/* -*- 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 "EditorCommands.h"

#include "mozilla/ArrayUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/EditorBase.h"
#include "mozilla/FlushType.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/Maybe.h"
#include "mozilla/MozPromise.h"  // for mozilla::detail::Any
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Selection.h"
#include "nsCommandParams.h"
#include "nsIClipboard.h"
#include "nsIEditingSession.h"
#include "nsIPrincipal.h"
#include "nsISelectionController.h"
#include "nsITransferable.h"
#include "nsString.h"
#include "nsAString.h"

class nsISupports;

#define STATE_ENABLED "state_enabled"
#define STATE_ATTRIBUTE "state_attribute"
#define STATE_DATA "state_data"

namespace mozilla {

using detail::Any;

/******************************************************************************
 * mozilla::EditorCommand
 ******************************************************************************/

NS_IMPL_ISUPPORTS(EditorCommand, nsIControllerCommand)

NS_IMETHODIMP EditorCommand::IsCommandEnabled(const char* aCommandName,
                                              nsISupports* aCommandRefCon,
                                              bool* aIsEnabled) {
  if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aIsEnabled)) {
    return NS_ERROR_INVALID_ARG;
  }

  nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
  EditorBase* editorBase = editor ? editor->AsEditorBase() : nullptr;
  *aIsEnabled = IsCommandEnabled(GetInternalCommand(aCommandName),
                                 MOZ_KnownLive(editorBase));
  return NS_OK;
}

NS_IMETHODIMP EditorCommand::DoCommand(const char* aCommandName,
                                       nsISupports* aCommandRefCon) {
  if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aCommandRefCon)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
  if (NS_WARN_IF(!editor)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsresult rv = DoCommand(GetInternalCommand(aCommandName),
                          MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
  NS_WARNING_ASSERTION(
      NS_SUCCEEDED(rv),
      "Failed to do command from nsIControllerCommand::DoCommand()");
  return rv;
}

NS_IMETHODIMP EditorCommand::DoCommandParams(const char* aCommandName,
                                             nsICommandParams* aParams,
                                             nsISupports* aCommandRefCon) {
  if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aCommandRefCon)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
  if (NS_WARN_IF(!editor)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsCommandParams* params = aParams ? aParams->AsCommandParams() : nullptr;
  Command command = GetInternalCommand(aCommandName, params);
  EditorCommandParamType paramType = EditorCommand::GetParamType(command);
  if (paramType == EditorCommandParamType::None) {
    nsresult rv = DoCommandParam(
        command, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "Failed to do command from nsIControllerCommand::DoCommandParams()");
    return rv;
  }

  if (Any(paramType & EditorCommandParamType::Bool)) {
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      Maybe<bool> boolParam = Nothing();
      if (params) {
        ErrorResult error;
        boolParam = Some(params->GetBool(STATE_ATTRIBUTE, error));
        if (NS_WARN_IF(error.Failed())) {
          return error.StealNSResult();
        }
      }
      nsresult rv = DoCommandParam(
          command, boolParam, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    MOZ_ASSERT_UNREACHABLE("Unexpected state for bool");
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  // Special case for MultiStateCommandBase.  It allows both CString and String
  // in STATE_ATTRIBUTE and CString is preferred.
  if (Any(paramType & EditorCommandParamType::CString) &&
      Any(paramType & EditorCommandParamType::String)) {
    if (!params) {
      nsresult rv =
          DoCommandParam(command, VoidString(),
                         MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "Failed to do command from "
                           "nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      nsCString cStringParam;
      nsresult rv = params->GetCString(STATE_ATTRIBUTE, cStringParam);
      if (NS_SUCCEEDED(rv)) {
        NS_ConvertUTF8toUTF16 stringParam(cStringParam);
        nsresult rv =
            DoCommandParam(command, stringParam,
                           MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
        NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                             "Failed to do command from "
                             "nsIControllerCommand::DoCommandParams()");
        return rv;
      }
      nsString stringParam;
      DebugOnly<nsresult> rvIgnored =
          params->GetString(STATE_ATTRIBUTE, stringParam);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                           "Failed to get string from STATE_ATTRIBUTE");
      rv = DoCommandParam(command, stringParam,
                          MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                           "Failed to do command from "
                           "nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    MOZ_ASSERT_UNREACHABLE("Unexpected state for CString/String");
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  if (Any(paramType & EditorCommandParamType::CString)) {
    if (!params) {
      nsresult rv =
          DoCommandParam(command, VoidCString(),
                         MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      nsCString cStringParam;
      nsresult rv = params->GetCString(STATE_ATTRIBUTE, cStringParam);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
      rv = DoCommandParam(command, cStringParam,
                          MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    MOZ_ASSERT_UNREACHABLE("Unexpected state for CString");
    return NS_ERROR_NOT_IMPLEMENTED;
  }

  if (Any(paramType & EditorCommandParamType::String)) {
    if (!params) {
      nsresult rv =
          DoCommandParam(command, VoidString(),
                         MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "Failed to do command from nsIControllerCommand::DoCommandParams()");
      return rv;
    }
    nsString stringParam;
    if (Any(paramType & EditorCommandParamType::StateAttribute)) {
      nsresult rv = params->GetString(STATE_ATTRIBUTE, stringParam);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else if (Any(paramType & EditorCommandParamType::StateData)) {
      nsresult rv = params->GetString(STATE_DATA, stringParam);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        return rv;
      }
    } else {
      MOZ_ASSERT_UNREACHABLE("Unexpected state for String");
      return NS_ERROR_NOT_IMPLEMENTED;
    }
    nsresult rv = DoCommandParam(
        command, stringParam, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "Failed to do command from nsIControllerCommand::DoCommandParams()");
    return rv;
  }

  if (Any(paramType & EditorCommandParamType::Transferable)) {
    nsCOMPtr<nsITransferable> transferable;
    if (params) {
      nsCOMPtr<nsISupports> supports = params->GetISupports("transferable");
      transferable = do_QueryInterface(supports);
    }
    nsresult rv = DoCommandParam(
        command, transferable, MOZ_KnownLive(*editor->AsEditorBase()), nullptr);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "Failed to do command from nsIControllerCommand::DoCommandParams()");
    return rv;
  }

  MOZ_ASSERT_UNREACHABLE("Unexpected param type");
  return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP EditorCommand::GetCommandStateParams(
    const char* aCommandName, nsICommandParams* aParams,
    nsISupports* aCommandRefCon) {
  if (NS_WARN_IF(!aCommandName) || NS_WARN_IF(!aParams)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
  if (editor) {
    return GetCommandStateParams(GetInternalCommand(aCommandName),
                                 MOZ_KnownLive(*aParams->AsCommandParams()),
                                 MOZ_KnownLive(editor->AsEditorBase()),
                                 nullptr);
  }
  nsCOMPtr<nsIEditingSession> editingSession =
      do_QueryInterface(aCommandRefCon);
  if (editingSession) {
    return GetCommandStateParams(GetInternalCommand(aCommandName),
                                 MOZ_KnownLive(*aParams->AsCommandParams()),
                                 nullptr, editingSession);
  }
  return GetCommandStateParams(GetInternalCommand(aCommandName),
                               MOZ_KnownLive(*aParams->AsCommandParams()),
                               nullptr, nullptr);
}

/******************************************************************************
 * mozilla::UndoCommand
 ******************************************************************************/

StaticRefPtr<UndoCommand> UndoCommand::sInstance;

bool UndoCommand::IsCommandEnabled(Command aCommand,
                                   EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() && aEditorBase->CanUndo();
}

nsresult UndoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.UndoAsAction(1, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::UndoAsAction() failed");
  return rv;
}

nsresult UndoCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::RedoCommand
 ******************************************************************************/

StaticRefPtr<RedoCommand> RedoCommand::sInstance;

bool RedoCommand::IsCommandEnabled(Command aCommand,
                                   EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() && aEditorBase->CanRedo();
}

nsresult RedoCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.RedoAsAction(1, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::RedoAsAction() failed");
  return rv;
}

nsresult RedoCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CutCommand
 ******************************************************************************/

StaticRefPtr<CutCommand> CutCommand::sInstance;

bool CutCommand::IsCommandEnabled(Command aCommand,
                                  EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() &&
         aEditorBase->IsCutCommandEnabled();
}

nsresult CutCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                               nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.CutAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
  return rv;
}

nsresult CutCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CutOrDeleteCommand
 ******************************************************************************/

StaticRefPtr<CutOrDeleteCommand> CutOrDeleteCommand::sInstance;

bool CutOrDeleteCommand::IsCommandEnabled(Command aCommand,
                                          EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult CutOrDeleteCommand::DoCommand(Command aCommand,
                                       EditorBase& aEditorBase,
                                       nsIPrincipal* aPrincipal) const {
  dom::Selection* selection = aEditorBase.GetSelection();
  if (selection && selection->IsCollapsed()) {
    nsresult rv = aEditorBase.DeleteSelectionAsAction(
        nsIEditor::eNext, nsIEditor::eStrip, aPrincipal);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::DeleteSelectionAsAction() failed");
    return rv;
  }
  nsresult rv = aEditorBase.CutAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::CutAsAction() failed");
  return rv;
}

nsresult CutOrDeleteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CopyCommand
 ******************************************************************************/

StaticRefPtr<CopyCommand> CopyCommand::sInstance;

bool CopyCommand::IsCommandEnabled(Command aCommand,
                                   EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsCopyCommandEnabled();
}

nsresult CopyCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                nsIPrincipal* aPrincipal) const {
  // Shouldn't cause "beforeinput" event so that we don't need to specify
  // the given principal.
  return aEditorBase.Copy();
}

nsresult CopyCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::CopyOrDeleteCommand
 ******************************************************************************/

StaticRefPtr<CopyOrDeleteCommand> CopyOrDeleteCommand::sInstance;

bool CopyOrDeleteCommand::IsCommandEnabled(Command aCommand,
                                           EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult CopyOrDeleteCommand::DoCommand(Command aCommand,
                                        EditorBase& aEditorBase,
                                        nsIPrincipal* aPrincipal) const {
  dom::Selection* selection = aEditorBase.GetSelection();
  if (selection && selection->IsCollapsed()) {
    nsresult rv = aEditorBase.DeleteSelectionAsAction(
        nsIEditor::eNextWord, nsIEditor::eStrip, aPrincipal);
    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                         "EditorBase::DeleteSelectionAsAction() failed");
    return rv;
  }
  // Shouldn't cause "beforeinput" event so that we don't need to specify
  // the given principal.
  nsresult rv = aEditorBase.Copy();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::Copy() failed");
  return rv;
}

nsresult CopyOrDeleteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::PasteCommand
 ******************************************************************************/

StaticRefPtr<PasteCommand> PasteCommand::sInstance;

bool PasteCommand::IsCommandEnabled(Command aCommand,
                                    EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
}

nsresult PasteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                 nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.PasteAsAction(nsIClipboard::kGlobalClipboard, true,
                                          aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::PasteAsAction() failed");
  return rv;
}

nsresult PasteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::PasteTransferableCommand
 ******************************************************************************/

StaticRefPtr<PasteTransferableCommand> PasteTransferableCommand::sInstance;

bool PasteTransferableCommand::IsCommandEnabled(Command aCommand,
                                                EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable() &&
         aEditorBase->CanPasteTransferable(nullptr);
}

nsresult PasteTransferableCommand::DoCommand(Command aCommand,
                                             EditorBase& aEditorBase,
                                             nsIPrincipal* aPrincipal) const {
  return NS_ERROR_FAILURE;
}

nsresult PasteTransferableCommand::DoCommandParam(
    Command aCommand, nsITransferable* aTransferableParam,
    EditorBase& aEditorBase, nsIPrincipal* aPrincipal) const {
  if (NS_WARN_IF(!aTransferableParam)) {
    return NS_ERROR_INVALID_ARG;
  }
  nsresult rv =
      aEditorBase.PasteTransferableAsAction(aTransferableParam, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::PasteTransferableAsAction() failed");
  return rv;
}

nsresult PasteTransferableCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  if (NS_WARN_IF(!aEditorBase)) {
    return NS_ERROR_INVALID_ARG;
  }

  nsCOMPtr<nsISupports> supports = aParams.GetISupports("transferable");
  if (NS_WARN_IF(!supports)) {
    return NS_ERROR_FAILURE;
  }

  nsCOMPtr<nsITransferable> trans;
  trans = do_QueryInterface(supports);
  if (NS_WARN_IF(!trans)) {
    return NS_ERROR_FAILURE;
  }

  return aParams.SetBool(STATE_ENABLED,
                         aEditorBase->CanPasteTransferable(trans));
}

/******************************************************************************
 * mozilla::SwitchTextDirectionCommand
 ******************************************************************************/

StaticRefPtr<SwitchTextDirectionCommand> SwitchTextDirectionCommand::sInstance;

bool SwitchTextDirectionCommand::IsCommandEnabled(
    Command aCommand, EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult SwitchTextDirectionCommand::DoCommand(Command aCommand,
                                               EditorBase& aEditorBase,
                                               nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.ToggleTextDirectionAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::ToggleTextDirectionAsAction() failed");
  return rv;
}

nsresult SwitchTextDirectionCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::DeleteCommand
 ******************************************************************************/

StaticRefPtr<DeleteCommand> DeleteCommand::sInstance;

bool DeleteCommand::IsCommandEnabled(Command aCommand,
                                     EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  // We can generally delete whenever the selection is editable.  However,
  // cmd_delete doesn't make sense if the selection is collapsed because it's
  // directionless.
  bool isEnabled = aEditorBase->IsSelectionEditable();

  if (aCommand == Command::Delete && isEnabled) {
    return aEditorBase->CanDeleteSelection();
  }
  return isEnabled;
}

nsresult DeleteCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                  nsIPrincipal* aPrincipal) const {
  nsIEditor::EDirection deleteDir = nsIEditor::eNone;
  switch (aCommand) {
    case Command::Delete:
      // Really this should probably be eNone, but it only makes a difference
      // if the selection is collapsed, and then this command is disabled.  So
      // let's keep it as it always was to avoid breaking things.
      deleteDir = nsIEditor::ePrevious;
      break;
    case Command::DeleteCharForward:
      deleteDir = nsIEditor::eNext;
      break;
    case Command::DeleteCharBackward:
      deleteDir = nsIEditor::ePrevious;
      break;
    case Command::DeleteWordBackward:
      deleteDir = nsIEditor::ePreviousWord;
      break;
    case Command::DeleteWordForward:
      deleteDir = nsIEditor::eNextWord;
      break;
    case Command::DeleteToBeginningOfLine:
      deleteDir = nsIEditor::eToBeginningOfLine;
      break;
    case Command::DeleteToEndOfLine:
      deleteDir = nsIEditor::eToEndOfLine;
      break;
    default:
      MOZ_CRASH("Unrecognized nsDeleteCommand");
  }
  nsresult rv = aEditorBase.DeleteSelectionAsAction(
      deleteDir, nsIEditor::eStrip, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::DeleteSelectionAsAction() failed");
  return rv;
}

nsresult DeleteCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::SelectAllCommand
 ******************************************************************************/

StaticRefPtr<SelectAllCommand> SelectAllCommand::sInstance;

bool SelectAllCommand::IsCommandEnabled(Command aCommand,
                                        EditorBase* aEditorBase) const {
  // You can always select all, unless the selection is editable,
  // and the editable region is empty!
  if (!aEditorBase) {
    return true;
  }

  // You can select all if there is an editor which is non-empty
  return !aEditorBase->IsEmpty();
}

nsresult SelectAllCommand::DoCommand(Command aCommand, EditorBase& aEditorBase,
                                     nsIPrincipal* aPrincipal) const {
  // Shouldn't cause "beforeinput" event so that we don't need to specify
  // aPrincipal.
  nsresult rv = aEditorBase.SelectAll();
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "EditorBase::SelectAll() failed");
  return rv;
}

nsresult SelectAllCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::SelectionMoveCommands
 ******************************************************************************/

StaticRefPtr<SelectionMoveCommands> SelectionMoveCommands::sInstance;

bool SelectionMoveCommands::IsCommandEnabled(Command aCommand,
                                             EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

static const struct ScrollCommand {
  Command mReverseScroll;
  Command mForwardScroll;
  nsresult (NS_STDCALL nsISelectionController::*scroll)(bool);
} scrollCommands[] = {{Command::ScrollTop, Command::ScrollBottom,
                       &nsISelectionController::CompleteScroll},
                      {Command::ScrollPageUp, Command::ScrollPageDown,
                       &nsISelectionController::ScrollPage},
                      {Command::ScrollLineUp, Command::ScrollLineDown,
                       &nsISelectionController::ScrollLine}};

static const struct MoveCommand {
  Command mReverseMove;
  Command mForwardMove;
  Command mReverseSelect;
  Command mForwardSelect;
  nsresult (NS_STDCALL nsISelectionController::*move)(bool, bool);
} moveCommands[] = {
    {Command::CharPrevious, Command::CharNext, Command::SelectCharPrevious,
     Command::SelectCharNext, &nsISelectionController::CharacterMove},
    {Command::LinePrevious, Command::LineNext, Command::SelectLinePrevious,
     Command::SelectLineNext, &nsISelectionController::LineMove},
    {Command::WordPrevious, Command::WordNext, Command::SelectWordPrevious,
     Command::SelectWordNext, &nsISelectionController::WordMove},
    {Command::BeginLine, Command::EndLine, Command::SelectBeginLine,
     Command::SelectEndLine, &nsISelectionController::IntraLineMove},
    {Command::MovePageUp, Command::MovePageDown, Command::SelectPageUp,
     Command::SelectPageDown, &nsISelectionController::PageMove},
    {Command::MoveTop, Command::MoveBottom, Command::SelectTop,
     Command::SelectBottom, &nsISelectionController::CompleteMove}};

static const struct PhysicalCommand {
  Command mMove;
  Command mSelect;
  int16_t direction;
  int16_t amount;
} physicalCommands[] = {
    {Command::MoveLeft, Command::SelectLeft, nsISelectionController::MOVE_LEFT,
     0},
    {Command::MoveRight, Command::SelectRight,
     nsISelectionController::MOVE_RIGHT, 0},
    {Command::MoveUp, Command::SelectUp, nsISelectionController::MOVE_UP, 0},
    {Command::MoveDown, Command::SelectDown, nsISelectionController::MOVE_DOWN,
     0},
    {Command::MoveLeft2, Command::SelectLeft2,
     nsISelectionController::MOVE_LEFT, 1},
    {Command::MoveRight2, Command::SelectRight2,
     nsISelectionController::MOVE_RIGHT, 1},
    {Command::MoveUp2, Command::SelectUp2, nsISelectionController::MOVE_UP, 1},
    {Command::MoveDown2, Command::SelectDown2,
     nsISelectionController::MOVE_DOWN, 1}};

nsresult SelectionMoveCommands::DoCommand(Command aCommand,
                                          EditorBase& aEditorBase,
                                          nsIPrincipal* aPrincipal) const {
  RefPtr<dom::Document> document = aEditorBase.GetDocument();
  if (document) {
    // Most of the commands below (possibly all of them) need layout to
    // be up to date.
    document->FlushPendingNotifications(FlushType::Layout);
  }

  nsCOMPtr<nsISelectionController> selectionController =
      aEditorBase.GetSelectionController();
  if (NS_WARN_IF(!selectionController)) {
    return NS_ERROR_FAILURE;
  }

  // scroll commands
  for (size_t i = 0; i < ArrayLength(scrollCommands); i++) {
    const ScrollCommand& cmd = scrollCommands[i];
    if (aCommand == cmd.mReverseScroll) {
      return (selectionController->*(cmd.scroll))(false);
    }
    if (aCommand == cmd.mForwardScroll) {
      return (selectionController->*(cmd.scroll))(true);
    }
  }

  // caret movement/selection commands
  for (size_t i = 0; i < ArrayLength(moveCommands); i++) {
    const MoveCommand& cmd = moveCommands[i];
    if (aCommand == cmd.mReverseMove) {
      return (selectionController->*(cmd.move))(false, false);
    }
    if (aCommand == cmd.mForwardMove) {
      return (selectionController->*(cmd.move))(true, false);
    }
    if (aCommand == cmd.mReverseSelect) {
      return (selectionController->*(cmd.move))(false, true);
    }
    if (aCommand == cmd.mForwardSelect) {
      return (selectionController->*(cmd.move))(true, true);
    }
  }

  // physical-direction movement/selection
  for (size_t i = 0; i < ArrayLength(physicalCommands); i++) {
    const PhysicalCommand& cmd = physicalCommands[i];
    if (aCommand == cmd.mMove) {
      nsresult rv =
          selectionController->PhysicalMove(cmd.direction, cmd.amount, false);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "nsISelectionController::PhysicalMove() failed to move caret");
      return rv;
    }
    if (aCommand == cmd.mSelect) {
      nsresult rv =
          selectionController->PhysicalMove(cmd.direction, cmd.amount, true);
      NS_WARNING_ASSERTION(
          NS_SUCCEEDED(rv),
          "nsISelectionController::PhysicalMove() failed to select");
      return rv;
    }
  }

  return NS_ERROR_FAILURE;
}

nsresult SelectionMoveCommands::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::InsertPlaintextCommand
 ******************************************************************************/

StaticRefPtr<InsertPlaintextCommand> InsertPlaintextCommand::sInstance;

bool InsertPlaintextCommand::IsCommandEnabled(Command aCommand,
                                              EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult InsertPlaintextCommand::DoCommand(Command aCommand,
                                           EditorBase& aEditorBase,
                                           nsIPrincipal* aPrincipal) const {
  // XXX InsertTextAsAction() is not same as OnInputText().  However, other
  //     commands to insert line break or paragraph separator use OnInput*().
  //     According to the semantics of those methods, using *AsAction() is
  //     better, however, this may not cause two or more placeholder
  //     transactions to the top transaction since its name may not be
  //     nsGkAtoms::TypingTxnName.
  DebugOnly<nsresult> rvIgnored =
      aEditorBase.InsertTextAsAction(u""_ns, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                       "EditorBase::InsertTextAsAction() failed, but ignored");
  return NS_OK;
}

nsresult InsertPlaintextCommand::DoCommandParam(
    Command aCommand, const nsAString& aStringParam, EditorBase& aEditorBase,
    nsIPrincipal* aPrincipal) const {
  if (NS_WARN_IF(aStringParam.IsVoid())) {
    return NS_ERROR_INVALID_ARG;
  }

  // XXX InsertTextAsAction() is not same as OnInputText().  However, other
  //     commands to insert line break or paragraph separator use OnInput*().
  //     According to the semantics of those methods, using *AsAction() is
  //     better, however, this may not cause two or more placeholder
  //     transactions to the top transaction since its name may not be
  //     nsGkAtoms::TypingTxnName.
  DebugOnly<nsresult> rvIgnored =
      aEditorBase.InsertTextAsAction(aStringParam, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored),
                       "EditorBase::InsertTextAsAction() failed, but ignored");
  return NS_OK;
}

nsresult InsertPlaintextCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::InsertParagraphCommand
 ******************************************************************************/

StaticRefPtr<InsertParagraphCommand> InsertParagraphCommand::sInstance;

bool InsertParagraphCommand::IsCommandEnabled(Command aCommand,
                                              EditorBase* aEditorBase) const {
  if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult InsertParagraphCommand::DoCommand(Command aCommand,
                                           EditorBase& aEditorBase,
                                           nsIPrincipal* aPrincipal) const {
  if (aEditorBase.IsSingleLineEditor()) {
    return NS_ERROR_FAILURE;
  }
  if (aEditorBase.IsHTMLEditor()) {
    nsresult rv = MOZ_KnownLive(aEditorBase.AsHTMLEditor())
                      ->InsertParagraphSeparatorAsAction(aPrincipal);
    NS_WARNING_ASSERTION(
        NS_SUCCEEDED(rv),
        "HTMLEditor::InsertParagraphSeparatorAsAction() failed");
    return rv;
  }
  nsresult rv = aEditorBase.InsertLineBreakAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::InsertLineBreakAsAction() failed");
  return rv;
}

nsresult InsertParagraphCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::InsertLineBreakCommand
 ******************************************************************************/

StaticRefPtr<InsertLineBreakCommand> InsertLineBreakCommand::sInstance;

bool InsertLineBreakCommand::IsCommandEnabled(Command aCommand,
                                              EditorBase* aEditorBase) const {
  if (!aEditorBase || aEditorBase->IsSingleLineEditor()) {
    return false;
  }
  return aEditorBase->IsSelectionEditable();
}

nsresult InsertLineBreakCommand::DoCommand(Command aCommand,
                                           EditorBase& aEditorBase,
                                           nsIPrincipal* aPrincipal) const {
  if (aEditorBase.IsSingleLineEditor()) {
    return NS_ERROR_FAILURE;
  }
  nsresult rv = aEditorBase.InsertLineBreakAsAction(aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::InsertLineBreakAsAction() failed");
  return rv;
}

nsresult InsertLineBreakCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  return aParams.SetBool(STATE_ENABLED,
                         IsCommandEnabled(aCommand, aEditorBase));
}

/******************************************************************************
 * mozilla::PasteQuotationCommand
 ******************************************************************************/

StaticRefPtr<PasteQuotationCommand> PasteQuotationCommand::sInstance;

bool PasteQuotationCommand::IsCommandEnabled(Command aCommand,
                                             EditorBase* aEditorBase) const {
  if (!aEditorBase) {
    return false;
  }
  return !aEditorBase->IsSingleLineEditor() &&
         aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard);
}

nsresult PasteQuotationCommand::DoCommand(Command aCommand,
                                          EditorBase& aEditorBase,
                                          nsIPrincipal* aPrincipal) const {
  nsresult rv = aEditorBase.PasteAsQuotationAsAction(
      nsIClipboard::kGlobalClipboard, true, aPrincipal);
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "EditorBase::PasteAsQuotationAsAction() failed");
  return rv;
}

nsresult PasteQuotationCommand::GetCommandStateParams(
    Command aCommand, nsCommandParams& aParams, EditorBase* aEditorBase,
    nsIEditingSession* aEditingSession) const {
  if (!aEditorBase) {
    return NS_OK;
  }
  aParams.SetBool(STATE_ENABLED,
                  aEditorBase->CanPaste(nsIClipboard::kGlobalClipboard));
  return NS_OK;
}

}  // namespace mozilla