/*
 * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform
 * Copyright (C) 2003-2022 The IdeaVim authors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package com.maddyhome.idea.vim.group;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.event.EditorMouseEvent;
import com.intellij.openapi.editor.event.EditorMouseListener;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.impl.TextRangeInterval;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.maddyhome.idea.vim.EventFacade;
import com.maddyhome.idea.vim.KeyHandler;
import com.maddyhome.idea.vim.RegisterActions;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.*;
import com.maddyhome.idea.vim.command.*;
import com.maddyhome.idea.vim.common.IndentConfig;
import com.maddyhome.idea.vim.common.TextRange;
import com.maddyhome.idea.vim.ex.ranges.LineRange;
import com.maddyhome.idea.vim.group.visual.VimSelection;
import com.maddyhome.idea.vim.group.visual.VisualModeHelperKt;
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase;
import com.maddyhome.idea.vim.handler.Motion;
import com.maddyhome.idea.vim.helper.*;
import com.maddyhome.idea.vim.key.KeyHandlerKeeper;
import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor;
import com.maddyhome.idea.vim.listener.VimInsertListener;
import com.maddyhome.idea.vim.listener.VimListenerSuppressor;
import com.maddyhome.idea.vim.newapi.*;
import com.maddyhome.idea.vim.options.OptionConstants;
import com.maddyhome.idea.vim.options.OptionScope;
import com.maddyhome.idea.vim.register.Register;
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString;
import kotlin.Pair;
import kotlin.text.StringsKt;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;

import javax.swing.*;
import java.awt.event.KeyEvent;
import java.math.BigInteger;
import java.util.*;

import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
import static com.maddyhome.idea.vim.mark.VimMarkConstants.*;
import static com.maddyhome.idea.vim.register.RegisterConstants.LAST_INSERTED_TEXT_REGISTER;

/**
 * Provides all the insert/replace related functionality
 */
public class ChangeGroup extends VimChangeGroupBase {

  private static final int MAX_REPEAT_CHARS_COUNT = 10000;

  public static final String VIM_MOTION_BIG_WORD_RIGHT = "VimMotionBigWordRightAction";
  public static final String VIM_MOTION_WORD_RIGHT = "VimMotionWordRightAction";
  public static final String VIM_MOTION_CAMEL_RIGHT = "VimMotionCamelRightAction";
  private static final String VIM_MOTION_WORD_END_RIGHT = "VimMotionWordEndRightAction";
  private static final String VIM_MOTION_BIG_WORD_END_RIGHT = "VimMotionBigWordEndRightAction";
  private static final String VIM_MOTION_CAMEL_END_RIGHT = "VimMotionCamelEndRightAction";
  private static final ImmutableSet<String> wordMotions = ImmutableSet.of(VIM_MOTION_WORD_RIGHT, VIM_MOTION_BIG_WORD_RIGHT, VIM_MOTION_CAMEL_RIGHT);

  @NonNls private static final String HEX_START = "0x";
  @NonNls private static final String MAX_HEX_INTEGER = "ffffffffffffffff";

  private @Nullable Command lastInsert;

  private final List<VimInsertListener> insertListeners = ContainerUtil.createLockFreeCopyOnWriteList();

  @Override
  public void setInsertRepeat(int lines, int column, boolean append) {
    repeatLines = lines;
    repeatColumn = column;
    repeatAppend = append;
  }

  /**
   * Begin insert before the cursor position
   *  @param editor  The editor to insert into
   * @param context The data context
   */
  @Override
  public void insertBeforeCursor(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    initInsert(editor, context, CommandState.Mode.INSERT);
  }

  /**
   * Begin insert before the first non-blank on the current line
   *
   * @param editor The editor to insert into
   */
  @Override
  public void insertBeforeFirstNonBlank(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      MotionGroup.moveCaret(((IjVimEditor) editor).getEditor(), caret, VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, new IjVimCaret(caret)));
    }
    initInsert(editor, context, CommandState.Mode.INSERT);
  }

  /**
   * Begin insert before the start of the current line
   *  @param editor  The editor to insert into
   * @param context The data context
   */
  @Override
  public void insertLineStart(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      MotionGroup.moveCaret(((IjVimEditor) editor).getEditor(), caret, VimPlugin.getMotion().moveCaretToLineStart(editor, new IjVimCaret(caret)));
    }
    initInsert(editor, context, CommandState.Mode.INSERT);
  }

  /**
   * Begin insert after the cursor position
   *  @param editor  The editor to insert into
   * @param context The data context
   */
  @Override
  public void insertAfterCursor(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      MotionGroup.moveCaret(((IjVimEditor) editor).getEditor(), caret, VimPlugin.getMotion().getOffsetOfHorizontalMotion(editor, new IjVimCaret(caret), 1, true));
    }
    initInsert(editor, context, CommandState.Mode.INSERT);
  }

  @Override
  public void insertAfterLineEnd(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      MotionGroup.moveCaret(((IjVimEditor) editor).getEditor(), caret, injector.getMotion().moveCaretToLineEnd(editor, new IjVimCaret(caret)));
    }
    initInsert(editor, context, CommandState.Mode.INSERT);
  }

  /**
   * Begin insert before the current line by creating a new blank line above the current line
   * for all carets
   *
   * @param editor The editor to insert into
   */
  @Override
  public void insertNewLineAbove(final @NotNull VimEditor editor, @NotNull ExecutionContext context) {
    if (((IjVimEditor) editor).getEditor().isOneLineMode()) return;

    // See also EditorStartNewLineBefore. That will move the caret to line start, call EditorEnter to create a new line,
    //   and then move up and call EditorLineEnd. We get better indent positioning by going to the line end of the
    //   previous line and hitting enter, especially with plain text files.
    // However, we'll use EditorStartNewLineBefore in PyCharm notebooks where the last character of the previous line
    //   may be locked with a guard

    // Note that we're deliberately bypassing MotionGroup.moveCaret to avoid side effects, most notably unncessary
    // scrolling
    Set<Caret> firstLiners = new HashSet<>();
    Set<Pair<Caret, Integer>> moves = new HashSet<>();
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      final int offset;
      if (caret.getVisualPosition().line == 0) {
        // Fake indenting for the first line. Works well for plain text to match the existing indent
        offset = VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, new IjVimCaret(caret));
        firstLiners.add(caret);
      }
      else {
        offset = VimPlugin.getMotion().moveCaretToLineEnd(editor, caret.getLogicalPosition().line - 1, true);
      }
      moves.add(new Pair<>(caret, offset));
    }

    // Check if the "last character on previous line" has a guard
    // This is actively used in pycharm notebooks https://youtrack.jetbrains.com/issue/VIM-2495
    boolean hasGuards = moves.stream().anyMatch(it -> ((IjVimEditor) editor).getEditor().getDocument().getOffsetGuard(it.getSecond()) != null);
    if (!hasGuards) {
      for (Pair<Caret, Integer> move : moves) {
        move.getFirst().moveToOffset(move.getSecond());
      }

      initInsert(editor, context, CommandState.Mode.INSERT);
      runEnterAction(editor, context);

      for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
        if (firstLiners.contains(caret)) {
          final int offset = VimPlugin.getMotion().moveCaretToLineEnd(editor, 0, true);
          injector.getMotion().moveCaret(editor, new IjVimCaret(caret), offset);
        }
      }
    } else {
      initInsert(editor, context, CommandState.Mode.INSERT);
      runEnterAboveAction(editor, context);
    }

    MotionGroup.scrollCaretIntoView(((IjVimEditor) editor).getEditor());
  }

  /**
   * Inserts a new line above the caret position
   *
   * @param editor The editor to insert into
   * @param caret  The caret to insert above
   * @param col    The column to indent to
   */
  private void insertNewLineAbove(@NotNull VimEditor editor, @NotNull VimCaret caret, int col) {
    if (((IjVimEditor) editor).getEditor().isOneLineMode()) return;

    boolean firstLiner = false;
    if (((IjVimCaret) caret).getCaret().getVisualPosition().line == 0) {
      injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineStart(editor,
                                                                                      caret));
      firstLiner = true;
    }
    else {
      injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().getVerticalMotionOffset(editor, caret, -1));
      injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret));
    }

    UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
    insertText(editor, caret, "\n" + IndentConfig.create(((IjVimEditor) editor).getEditor()).createIndentBySize(col));

    if (firstLiner) {
      injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().getVerticalMotionOffset(editor, caret, -1));
    }
  }

  /**
   * Begin insert after the current line by creating a new blank line below the current line
   * for all carets
   *  @param editor  The editor to insert into
   * @param context The data context
   */
  @Override
  public void insertNewLineBelow(final @NotNull VimEditor editor, final @NotNull ExecutionContext context) {
    if (((IjVimEditor) editor).getEditor().isOneLineMode()) return;

    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      injector.getMotion().moveCaret(editor, new IjVimCaret(caret), VimPlugin.getMotion().moveCaretToLineEnd(editor, new IjVimCaret(caret)));
    }

    initInsert(editor, context, CommandState.Mode.INSERT);
    runEnterAction(editor, context);

    MotionGroup.scrollCaretIntoView(((IjVimEditor) editor).getEditor());
  }

  /**
   * Inserts a new line below the caret position
   *
   * @param editor The editor to insert into
   * @param caret  The caret to insert after
   * @param col    The column to indent to
   */
  private void insertNewLineBelow(@NotNull VimEditor editor, @NotNull VimCaret caret, int col) {
    if (((IjVimEditor) editor).getEditor().isOneLineMode()) return;

    injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret));
    UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
    insertText(editor, caret, "\n" + IndentConfig.create(((IjVimEditor) editor).getEditor()).createIndentBySize(col));
  }

  private void runEnterAction(VimEditor editor, @NotNull ExecutionContext context) {
    CommandState state = CommandState.getInstance(editor);
    if (!state.isDotRepeatInProgress()) {
      // While repeating the enter action has been already executed because `initInsert` repeats the input
      final NativeAction action = VimInjectorKt.getInjector().getNativeActionManager().getEnterAction();
      if (action != null) {
        strokes.add(action);
        VimInjectorKt.getInjector().getActionExecutor().executeAction(action, context);
      }
    }
  }

  private void runEnterAboveAction(VimEditor editor, @NotNull ExecutionContext context) {
    CommandState state = CommandState.getInstance(editor);
    if (!state.isDotRepeatInProgress()) {
      // While repeating the enter action has been already executed because `initInsert` repeats the input
      final NativeAction action = VimInjectorKt.getInjector().getNativeActionManager().getCreateLineAboveCaret();
      if (action != null) {
        strokes.add(action);
        VimInjectorKt.getInjector().getActionExecutor().executeAction(action, context);
      }
    }
  }

  /**
   * Begin insert at the location of the previous insert
   *
   * @param editor The editor to insert into
   */
  @Override
  public void insertAtPreviousInsert(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    editor.removeSecondaryCarets();

    final VimCaret caret = editor.primaryCaret();
    final int offset = VimPlugin.getMotion().moveCaretToMark(editor, '^', false);
    if (offset != -1) {
      injector.getMotion().moveCaret(editor, caret, offset);
    }

    insertBeforeCursor(editor, context);
  }

  /**
   * Inserts previously inserted text
   *  @param editor  The editor to insert into
   * @param context The data context
   * @param exit    true if insert mode should be exited after the insert, false should stay in insert mode
   */
  @Override
  public void insertPreviousInsert(@NotNull VimEditor editor,
                                   @NotNull ExecutionContext context,
                                   boolean exit,
                                   @NotNull OperatorArguments operatorArguments) {
    repeatInsertText(editor, context, 1, operatorArguments);
    if (exit) {
      ModeHelper.exitInsertMode(((IjVimEditor) editor).getEditor(), ((IjExecutionContext) context).getContext(), operatorArguments);
    }
  }

  /**
   * Inserts the contents of the specified register
   *
   * @param editor  The editor to insert the text into
   * @param context The data context
   * @param key     The register name
   * @return true if able to insert the register contents, false if not
   */
  @Override
  public boolean insertRegister(@NotNull VimEditor editor, @NotNull ExecutionContext context, char key) {
    final Register register = VimPlugin.getRegister().getRegister(key);
    if (register != null) {
      final List<KeyStroke> keys = register.getKeys();
      for (KeyStroke k : keys) {
        processKey(editor, context, k);
      }
      return true;
    }

    return false;
  }

  private @Nullable DocumentListener documentListener;

  /**
   * If the cursor is currently after the start of the current insert this deletes all the newly inserted text.
   * Otherwise it deletes all text from the cursor back to the first non-blank in the line.
   *
   * @param editor The editor to delete the text from
   * @param caret  The caret on which the action is performed
   * @return true if able to delete the text, false if not
   */
  @Override
  public boolean insertDeleteInsertedText(@NotNull VimEditor editor, @NotNull VimCaret caret) {
    int deleteTo = UserDataManager.getVimInsertStart(((IjVimCaret) caret).getCaret()).getStartOffset();
    int offset = ((IjVimCaret) caret).getCaret().getOffset();
    if (offset == deleteTo) {
      deleteTo = VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor,
                                                                       caret);
    }

    if (deleteTo != -1) {
      deleteRange(editor, caret, new TextRange(deleteTo, offset), SelectionType.CHARACTER_WISE, false);

      return true;
    }

    return false;
  }

  /**
   * Deletes the text from the cursor to the start of the previous word
   * <p>
   * TODO This behavior should be configured via the `backspace` option
   *
   * @param editor The editor to delete the text from
   * @return true if able to delete text, false if not
   */
  @Override
  public boolean insertDeletePreviousWord(@NotNull VimEditor editor, @NotNull VimCaret caret) {
    final int deleteTo;
    if (((IjVimCaret) caret).getCaret().getLogicalPosition().column == 0) {
      deleteTo = ((IjVimCaret) caret).getCaret().getOffset() - 1;
    }
    else {
      int pointer = ((IjVimCaret) caret).getCaret().getOffset() - 1;
      final CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
      while (pointer >= 0 && chars.charAt(pointer) == ' ' && chars.charAt(pointer) != '\n') pointer--;
      if (chars.charAt(pointer) == '\n') {
        deleteTo = pointer + 1;
      }
      else {
        Motion motion = VimPlugin.getMotion().findOffsetOfNextWord(editor, pointer + 1, -1, false);
        if (motion instanceof Motion.AbsoluteOffset) {
          deleteTo = ((Motion.AbsoluteOffset)motion).getOffset();
        }
        else {
          return false;
        }
      }
    }
    if (deleteTo < 0) {
      return false;
    }
    final TextRange range = new TextRange(deleteTo, ((IjVimCaret) caret).getCaret().getOffset());
    deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true);
    return true;
  }

  /**
   * Begin insert/replace mode
   *  @param editor  The editor to insert into
   * @param context The data context
   * @param mode    The mode - indicate insert or replace
   */
  @Override
  public void initInsert(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull CommandState.Mode mode) {
    final CommandState state = CommandState.getInstance(editor);

    for (VimCaret caret : editor.nativeCarets()) {
      UserDataManager.setVimInsertStart(
        ((IjVimCaret) caret).getCaret(),
        ((IjVimEditor) editor).getEditor().getDocument().createRangeMarker(caret.getOffset().getPoint(), caret.getOffset().getPoint())
      );
      if (caret.equals(editor.primaryCaret())) {
        VimPlugin.getMark().setMark(editor, MARK_CHANGE_START, caret.getOffset().getPoint());
      }
    }

    final Command cmd = state.getExecutingCommand();
    if (cmd != null && state.isDotRepeatInProgress()) {
      state.pushModes(mode, CommandState.SubMode.NONE);
      if (mode == CommandState.Mode.REPLACE) {
        setInsertEditorState(editor, false);
      }
      if (cmd.getFlags().contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) {
        CommandState commandState = CommandState.getInstance(editor);
        repeatInsert(editor, context, 1, false,
                     new OperatorArguments(false, 1, commandState.getMode(), commandState.getSubMode()));
      }
      else {
        CommandState commandState = CommandState.getInstance(editor);
        repeatInsert(editor, context, cmd.getCount(), false,
                     new OperatorArguments(false, cmd.getCount(), commandState.getMode(), commandState.getSubMode()));
      }
      if (mode == CommandState.Mode.REPLACE) {
        setInsertEditorState(editor, true);
      }
      state.popModes();
    }
    else {
      lastInsert = cmd;
      strokes.clear();
      repeatCharsCount = 0;
      final EventFacade eventFacade = EventFacade.getInstance();
      if (document != null && documentListener != null) {
        eventFacade.removeDocumentListener(document, documentListener);
      }
      document = ((IjVimEditor) editor).getEditor().getDocument();
      documentListener = new InsertActionsDocumentListener();
      eventFacade.addDocumentListener(document, documentListener);
      oldOffset = ((IjVimEditor) editor).getEditor().getCaretModel().getOffset();
      setInsertEditorState(editor, mode == CommandState.Mode.INSERT);
      state.pushModes(mode, CommandState.SubMode.NONE);
    }

    notifyListeners(((IjVimEditor) editor).getEditor());
  }

  // Workaround for VIM-1546. Another solution is highly appreciated.
  public boolean tabAction = false;

  private final @NotNull EditorMouseListener listener = new EditorMouseListener() {
    @Override
    public void mouseClicked(@NotNull EditorMouseEvent event) {
      Editor editor = event.getEditor();
      if (CommandStateHelper.inInsertMode(editor)) {
        clearStrokes(new IjVimEditor(editor));
      }
    }
  };

  @Override
  public void editorCreated(VimEditor editor) {
    EventFacade.getInstance().addEditorMouseListener(((IjVimEditor) editor).getEditor(), listener);
  }

  @Override
  public void editorReleased(VimEditor editor) {
    EventFacade.getInstance().removeEditorMouseListener(((IjVimEditor) editor).getEditor(), listener);
  }

  /**
   * This repeats the previous insert count times
   *  @param editor  The editor to insert into
   * @param context The data context
   * @param count   The number of times to repeat the previous insert
   */
  private void repeatInsertText(@NotNull VimEditor editor,
                                @NotNull ExecutionContext context,
                                int count,
                                @NotNull OperatorArguments operatorArguments) {
    if (lastStrokes == null) {
      return;
    }

    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      for (int i = 0; i < count; i++) {
        for (Object lastStroke : lastStrokes) {
          if (lastStroke instanceof AnAction) {
            VimInjectorKt.getInjector().getActionExecutor().executeAction(new IjNativeAction((AnAction)lastStroke), context);
            strokes.add(lastStroke);
          }
          if (lastStroke instanceof NativeAction) {
            VimInjectorKt.getInjector().getActionExecutor().executeAction((NativeAction)lastStroke, context);
            strokes.add(lastStroke);
          }
          else if (lastStroke instanceof EditorActionHandlerBase) {
            VimInjectorKt.getInjector().getActionExecutor().executeVimAction(editor, (EditorActionHandlerBase)lastStroke, context, operatorArguments);
            strokes.add(lastStroke);
          }
          else if (lastStroke instanceof char[]) {
            final char[] chars = (char[])lastStroke;
            insertText(editor, new IjVimCaret(caret), new String(chars));
          }
        }
      }
    }
  }

  /**
   * Terminate insert/replace mode after the user presses Escape or Ctrl-C
   * <p>
   * DEPRECATED. Please, don't use this function directly. Use ModeHelper.exitInsertMode in file ModeExtensions.kt
   */
  @Override
  public void processEscape(@NotNull VimEditor editor, @Nullable ExecutionContext context, @NotNull OperatorArguments operatorArguments) {
    // Get the offset for marks before we exit insert mode - switching from insert to overtype subtracts one from the
    // column offset.
    int offset = ((IjVimEditor) editor).getEditor().getCaretModel().getPrimaryCaret().getOffset();
    final MarkGroup markGroup = VimPlugin.getMark();
    markGroup.setMark(editor, '^', offset);
    markGroup.setMark(editor, MARK_CHANGE_END, offset);

    if (CommandState.getInstance(editor).getMode() == CommandState.Mode.REPLACE) {
      setInsertEditorState(editor, true);
    }

    int cnt = lastInsert != null ? lastInsert.getCount() : 0;
    if (lastInsert != null && (lastInsert.getFlags().contains(CommandFlags.FLAG_NO_REPEAT_INSERT))) {
      cnt = 1;
    }

    if (document != null && documentListener != null) {
      EventFacade.getInstance().removeDocumentListener(document, documentListener);
      documentListener = null;
    }

    lastStrokes = new ArrayList<>(strokes);

    if (context != null) {
      repeatInsert(editor, context, cnt == 0 ? 0 : cnt - 1, true, operatorArguments);
    }

    if (CommandState.getInstance(editor).getMode() == CommandState.Mode.INSERT) {
      updateLastInsertedTextRegister();
    }

    // The change pos '.' mark is the offset AFTER processing escape, and after switching to overtype
    offset = ((IjVimEditor) editor).getEditor().getCaretModel().getPrimaryCaret().getOffset();
    markGroup.setMark(editor, MARK_CHANGE_POS, offset);

    CommandState.getInstance(editor).popModes();
    exitAllSingleCommandInsertModes(editor);
  }

  /**
   * Processes the Enter key by running the first successful action registered for "ENTER" keystroke.
   * <p>
   * If this is REPLACE mode we need to turn off OVERWRITE before and then turn OVERWRITE back on after sending the
   * "ENTER" key.
   *
   * @param editor  The editor to press "Enter" in
   * @param context The data context
   */
  @Override
  public void processEnter(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    if (CommandState.getInstance(editor).getMode() == CommandState.Mode.REPLACE) {
      setInsertEditorState(editor, true);
    }
    final KeyStroke enterKeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
    final List<NativeAction> actions = VimPlugin.getKey().getActions(editor, enterKeyStroke);
    for (NativeAction action : actions) {
      if (VimInjectorKt.getInjector().getActionExecutor().executeAction(action, context)) {
        break;
      }
    }
    if (CommandState.getInstance(editor).getMode() == CommandState.Mode.REPLACE) {
      setInsertEditorState(editor, false);
    }
  }

  /**
   * Inserts the character above/below the cursor at the cursor location
   *
   * @param editor The editor to insert into
   * @param caret  The caret to insert after
   * @param dir    1 for getting from line below cursor, -1 for getting from line above cursor
   * @return true if able to get the character and insert it, false if not
   */
  @Override
  public boolean insertCharacterAroundCursor(@NotNull VimEditor editor, @NotNull VimCaret caret, int dir) {
    boolean res = false;

    VimVisualPosition vp = caret.getVisualPosition();
    vp = new VimVisualPosition(vp.getLine() + dir, vp.getColumn(), false);
    int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor(), EditorHelper.visualLineToLogicalLine(((IjVimEditor) editor).getEditor(), vp.getLine()));
    if (vp.getColumn() < len) {
      int offset = EditorHelper.visualPositionToOffset(((IjVimEditor) editor).getEditor(), new VisualPosition(vp.getLine(), vp.getColumn()));
      CharSequence charsSequence = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
      if (offset < charsSequence.length()) {
        char ch = charsSequence.charAt(offset);
        ((IjVimEditor) editor).getEditor().getDocument().insertString(((IjVimCaret) caret).getCaret().getOffset(), Character.toString(ch));
        injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion()
          .getOffsetOfHorizontalMotion(editor, caret, 1, true));
        res = true;
      }
    }

    return res;
  }

  /**
   * Sets the insert/replace state of the editor.
   */
  private void setInsertEditorState(@NotNull VimEditor editor, boolean value) {
    final EditorEx editorEx = ObjectUtils.tryCast(((IjVimEditor) editor).getEditor(), EditorEx.class);
    if (editorEx == null) return;
    editorEx.setInsertMode(value);
  }

  /**
   * Performs a mode switch after change action
   *  @param editor   The editor to switch mode in
   * @param context  The data context
   * @param toSwitch The mode to switch to
   */
  @Override
  public void processPostChangeModeSwitch(@NotNull VimEditor editor,
                                          @NotNull ExecutionContext context,
                                          @NotNull CommandState.Mode toSwitch) {
    if (toSwitch == CommandState.Mode.INSERT) {
      initInsert(editor, context, CommandState.Mode.INSERT);
    }
  }

  /**
   * This repeats the previous insert count times
   *
   * @param editor  The editor to insert into
   * @param context The data context
   * @param count   The number of times to repeat the previous insert
   */
  private void repeatInsert(@NotNull VimEditor editor,
                            @NotNull ExecutionContext context,
                            int count,
                            boolean started,
                            @NotNull OperatorArguments operatorArguments) {
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      if (repeatLines > 0) {
        final int visualLine = caret.getVisualPosition().line;
        final int logicalLine = caret.getLogicalPosition().line;
        final int position = editor.logicalPositionToOffset(new VimLogicalPosition(logicalLine, repeatColumn, false));

        for (int i = 0; i < repeatLines; i++) {
          if (repeatAppend &&
              repeatColumn < VimMotionGroupBase.LAST_COLUMN &&
              EditorHelper.getVisualLineLength(((IjVimEditor) editor).getEditor(), visualLine + i) < repeatColumn) {
            final String pad = EditorHelper.pad(((IjVimEditor) editor).getEditor(), ((IjExecutionContext) context).getContext(), logicalLine + i, repeatColumn);
            if (pad.length() > 0) {
              final int offset = ((IjVimEditor) editor).getEditor().getDocument().getLineEndOffset(logicalLine + i);
              insertText(editor, new IjVimCaret(caret), offset, pad);
            }
          }
          int updatedCount = started ? (i == 0 ? count : count + 1) : count;
          if (repeatColumn >= VimMotionGroupBase.LAST_COLUMN) {
            caret.moveToOffset(VimPlugin.getMotion().moveCaretToLineEnd(editor, logicalLine + i, true));
            repeatInsertText(editor, context, updatedCount, operatorArguments);
          }
          else if (EditorHelper.getVisualLineLength(((IjVimEditor) editor).getEditor(), visualLine + i) >= repeatColumn) {
            VisualPosition visualPosition = new VisualPosition(visualLine + i, repeatColumn);
            int inlaysCount = InlayHelperKt.amountOfInlaysBeforeVisualPosition(((IjVimEditor) editor).getEditor(), visualPosition);
            caret.moveToVisualPosition(new VisualPosition(visualLine + i, repeatColumn + inlaysCount));
            repeatInsertText(editor, context, updatedCount, operatorArguments);
          }
        }

        injector.getMotion().moveCaret(editor, new IjVimCaret(caret), position);
      }
      else {
        repeatInsertText(editor, context, count, operatorArguments);
        final int position = VimPlugin.getMotion().getOffsetOfHorizontalMotion(editor, new IjVimCaret(caret), -1, false);

        injector.getMotion().moveCaret(editor, new IjVimCaret(caret), position);
      }
    }

    repeatLines = 0;
    repeatColumn = 0;
    repeatAppend = false;
  }

  /**
   * Processes the user pressing the Insert key while in INSERT or REPLACE mode. This simply toggles the
   * Insert/Overwrite state which updates the status bar.
   *
   * @param editor The editor to toggle the state in
   */
  @Override
  public void processInsert(VimEditor editor) {
    final EditorEx editorEx = ObjectUtils.tryCast(((IjVimEditor) editor).getEditor(), EditorEx.class);
    if (editorEx == null) return;
    editorEx.setInsertMode(!editorEx.isInsertMode());
    CommandState.getInstance(editor).toggleInsertOverwrite();
  }

  /**
   * This processes all keystrokes in Insert/Replace mode that were converted into Commands. Some of these
   * commands need to be saved off so the inserted/replaced text can be repeated properly later if needed.
   *
   * @param editor The editor the command was executed in
   * @param cmd    The command that was executed
   */
  @Override
  public void processCommand(@NotNull VimEditor editor, @NotNull Command cmd) {
    // return value never used here
    if (cmd.getFlags().contains(CommandFlags.FLAG_SAVE_STROKE)) {
      strokes.add(cmd.getAction());
    }
    else if (cmd.getFlags().contains(CommandFlags.FLAG_CLEAR_STROKES)) {
      clearStrokes(editor);
    }
  }

  /**
   * Clears all the keystrokes from the current insert command
   *
   * @param editor The editor to clear strokes from.
   */
  private void clearStrokes(@NotNull VimEditor editor) {
    strokes.clear();
    repeatCharsCount = 0;
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      UserDataManager
        .setVimInsertStart(caret, ((IjVimEditor) editor).getEditor().getDocument().createRangeMarker(caret.getOffset(), caret.getOffset()));
    }
  }

  /**
   * Deletes count character after the caret from the editor
   *
   * @param editor The editor to remove characters from
   * @param caret  The caret on which the operation is performed
   * @param count  The numbers of characters to delete.
   * @return true if able to delete, false if not
   */
  @Override
  public boolean deleteCharacter(@NotNull VimEditor editor, @NotNull VimCaret caret, int count, boolean isChange) {
    final int endOffset = VimPlugin.getMotion().getOffsetOfHorizontalMotion(editor, caret, count, true);
    if (endOffset != -1) {
      final boolean res = deleteText(editor, new TextRange(((IjVimCaret) caret).getCaret().getOffset(), endOffset), SelectionType.CHARACTER_WISE);

      final int pos = ((IjVimCaret) caret).getCaret().getOffset();
      final int norm = EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret().getLogicalPosition().line, pos, isChange);
      if (norm != pos ||
          ((IjVimEditor) editor).getEditor().offsetToVisualPosition(norm) != EditorUtil.inlayAwareOffsetToVisualPosition(((IjVimEditor) editor).getEditor(), norm)) {
        injector.getMotion().moveCaret(editor, caret, norm);
      }
      // Always move the caret. Our position might or might not have changed, but an inlay might have been moved to our
      // location, or deleting the character(s) might have caused us to scroll sideways in long files. Moving the caret
      // will make sure it's in the right place, and visible
      final int offset =
        EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret().getLogicalPosition().line, ((IjVimCaret) caret).getCaret().getOffset(), isChange);
      injector.getMotion().moveCaret(editor, caret, offset);

      return res;
    }

    return false;
  }

  /**
   * While in INSERT or REPLACE mode the user can enter a single NORMAL mode command and then automatically
   * return to INSERT or REPLACE mode.
   *
   * @param editor The editor to put into NORMAL mode for one command
   */
  @Override
  public void processSingleCommand(@NotNull VimEditor editor) {
    CommandState.getInstance(editor).pushModes(CommandState.Mode.INSERT_NORMAL, CommandState.SubMode.NONE);
    clearStrokes(editor);
  }

  /**
   * Delete from the cursor to the end of count - 1 lines down
   *
   * @param editor The editor to delete from
   * @param caret  VimCaret on the position to start
   * @param count  The number of lines affected
   * @return true if able to delete the text, false if not
   */
  @Override
  public boolean deleteEndOfLine(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
    int initialOffset = ((IjVimCaret) caret).getCaret().getOffset();
    int offset = VimPlugin.getMotion().moveCaretToLineEndOffset(editor,
                                                                caret, count - 1, true);
    int lineStart = VimPlugin.getMotion().moveCaretToLineStart(editor,
                                                               caret);

    int startOffset = initialOffset;
    if (offset == initialOffset && offset != lineStart) startOffset--; // handle delete from virtual space

    //noinspection ConstantConditions
    if (offset != -1) {
      final TextRange rangeToDelete = new TextRange(startOffset, offset);
      editor.nativeCarets().stream().filter(c -> !c.equals(caret) && rangeToDelete.contains(c.getOffset().getPoint()))
        .forEach(editor::removeCaret);
      boolean res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE);

      if (EngineHelperKt.getUsesVirtualSpace()) {
        injector.getMotion().moveCaret(editor, caret, startOffset);
      }
      else {
        int pos = VimPlugin.getMotion().getOffsetOfHorizontalMotion(editor, caret, -1, false);
        if (pos != -1) {
          injector.getMotion().moveCaret(editor, caret, pos);
        }
      }

      return res;
    }

    return false;
  }

  /**
   * Joins count lines together starting at the cursor. No count or a count of one still joins two lines.
   *
   * @param editor The editor to join the lines in
   * @param caret  The caret in the first line to be joined.
   * @param count  The number of lines to join
   * @param spaces If true the joined lines will have one space between them and any leading space on the second line
   *               will be removed. If false, only the newline is removed to join the lines.
   * @return true if able to join the lines, false if not
   */
  @Override
  public boolean deleteJoinLines(@NotNull VimEditor editor, @NotNull VimCaret caret, int count, boolean spaces) {
    if (count < 2) count = 2;
    int lline = ((IjVimCaret) caret).getCaret().getLogicalPosition().line;
    int total = EditorHelper.getLineCount(((IjVimEditor) editor).getEditor());
    //noinspection SimplifiableIfStatement
    if (lline + count > total) {
      return false;
    }

    return deleteJoinNLines(editor, caret, lline, count, spaces);
  }

  /**
   * This processes all "regular" keystrokes entered while in insert/replace mode
   *
   * @param editor  The editor the character was typed into
   * @param context The data context
   * @param key     The user entered keystroke
   * @return true if this was a regular character, false if not
   */
  @Override
  public boolean processKey(final @NotNull VimEditor editor,
                            final @NotNull ExecutionContext context,
                            final @NotNull KeyStroke key) {
    if (logger.isDebugEnabled()) {
      logger.debug("processKey(" + key + ")");
    }

    if (key.getKeyChar() != KeyEvent.CHAR_UNDEFINED) {
      type(editor, context, key.getKeyChar());
      return true;
    }

    // Shift-space
    if (key.getKeyCode() == 32 && ((key.getModifiers() & KeyEvent.SHIFT_DOWN_MASK) != 0)) {
      type(editor, context, ' ');
      return true;
    }


    return false;
  }

  private void type(@NotNull VimEditor vimEditor, @NotNull ExecutionContext context, char key) {
    Editor editor = ((IjVimEditor) vimEditor).getEditor();
    DataContext ijContext = IjExecutionContextKt.getIj(context);
    final Document doc = ((IjVimEditor) vimEditor).getEditor().getDocument();
    CommandProcessor.getInstance().executeCommand(editor.getProject(), () -> ApplicationManager.getApplication()
                                                    .runWriteAction(() -> KeyHandlerKeeper.getInstance().getOriginalHandler().execute(editor, key, ijContext)), "", doc,
                                                  UndoConfirmationPolicy.DEFAULT, doc);
    MotionGroup.scrollCaretIntoView(editor);
  }

  @Override
  public boolean processKeyInSelectMode(final @NotNull VimEditor editor,
                                        final @NotNull ExecutionContext context,
                                        final @NotNull KeyStroke key) {
    boolean res;
    try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) {
      res = processKey(editor, context, key);

      ModeHelper.exitSelectMode(editor, false);
      KeyHandler.getInstance().reset(editor);

      if (isPrintableChar(key.getKeyChar()) || activeTemplateWithLeftRightMotion(editor, key)) {
        VimPlugin.getChange().insertBeforeCursor(editor, context);
      }
    }

    return res;
  }

  private boolean isPrintableChar(char c) {
    Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
    return (!Character.isISOControl(c)) &&
           c != KeyEvent.CHAR_UNDEFINED &&
           block != null &&
           block != Character.UnicodeBlock.SPECIALS;
  }

  private boolean activeTemplateWithLeftRightMotion(VimEditor editor, KeyStroke keyStroke) {
    return HelperKt.isTemplateActive(((IjVimEditor) editor).getEditor()) &&
           (keyStroke.getKeyCode() == KeyEvent.VK_LEFT || keyStroke.getKeyCode() == KeyEvent.VK_RIGHT);
  }

  /**
   * Deletes count lines including the current line
   *
   * @param editor The editor to remove the lines from
   * @param count  The number of lines to delete
   * @return true if able to delete the lines, false if not
   */

  @Override
  public boolean deleteLine(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
    int start = VimPlugin.getMotion().moveCaretToLineStart(editor,
                                                           caret);
    int offset = Math.min(VimPlugin.getMotion().moveCaretToLineEndOffset(editor,
                                                                         caret, count - 1, true) + 1,
                          EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()));
    if (logger.isDebugEnabled()) {
      logger.debug("start=" + start);
      logger.debug("offset=" + offset);
    }
    if (offset != -1) {
      boolean res = deleteText(editor, new TextRange(start, offset), SelectionType.LINE_WISE);
      if (res && ((IjVimCaret) caret).getCaret().getOffset() >= EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()) && ((IjVimCaret) caret).getCaret().getOffset() != 0) {
        injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineStartSkipLeadingOffset(editor,
                                                                                                caret, -1));
      }

      return res;
    }

    return false;
  }

  /**
   * Joins all the lines selected by the current visual selection.
   *
   * @param editor The editor to join the lines in
   * @param caret  The caret to be moved after joining
   * @param range  The range of the visual selection
   * @param spaces If true the joined lines will have one space between them and any leading space on the second line
   *               will be removed. If false, only the newline is removed to join the lines.
   * @return true if able to join the lines, false if not
   */
  @Override
  public boolean deleteJoinRange(@NotNull VimEditor editor, @NotNull VimCaret caret, @NotNull TextRange range, boolean spaces) {
    int startLine = editor.offsetToLogicalPosition(range.getStartOffset()).getLine();
    int endLine = editor.offsetToLogicalPosition(range.getEndOffset()).getLine();
    int count = endLine - startLine + 1;
    if (count < 2) count = 2;

    return deleteJoinNLines(editor, caret, startLine, count, spaces);
  }

  /**
   * This does the actual joining of the lines
   *
   * @param editor    The editor to join the lines in
   * @param caret     The caret on the starting line (to be moved)
   * @param startLine The starting logical line
   * @param count     The number of lines to join including startLine
   * @param spaces    If true the joined lines will have one space between them and any leading space on the second line
   *                  will be removed. If false, only the newline is removed to join the lines.
   * @return true if able to join the lines, false if not
   */
  private boolean deleteJoinNLines(@NotNull VimEditor editor,
                                   @NotNull VimCaret caret,
                                   int startLine,
                                   int count,
                                   boolean spaces) {
    // start my moving the cursor to the very end of the first line
    injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, startLine, true));
    for (int i = 1; i < count; i++) {
      int start = VimPlugin.getMotion().moveCaretToLineEnd(editor, caret);
      int trailingWhitespaceStart = VimPlugin.getMotion().moveCaretToLineEndSkipLeadingOffset(editor,
                                                                                              caret, 0);
      boolean hasTrailingWhitespace = start != trailingWhitespaceStart + 1;

      injector.getMotion().moveCaret(editor, caret, start);
      int offset;
      if (spaces) {
        offset = VimPlugin.getMotion().moveCaretToLineStartSkipLeadingOffset(editor,
                                                                             caret, 1);
      }
      else {
        offset = VimPlugin.getMotion().moveCaretToLineStart(editor, ((IjVimCaret) caret).getCaret().getLogicalPosition().line + 1);
      }
      deleteText(editor, new TextRange(((IjVimCaret) caret).getCaret().getOffset(), offset), null);
      if (spaces && !hasTrailingWhitespace) {
        insertText(editor, caret, " ");
        injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().getOffsetOfHorizontalMotion(editor,
                                                                                      caret, -1, true));
      }
    }

    return true;
  }

  @Override
  public boolean joinViaIdeaByCount(@NotNull VimEditor editor, @NotNull ExecutionContext context, int count) {
    int executions = count > 1 ? count - 1 : 1;
    final boolean allowedExecution = ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets().stream().anyMatch(caret -> {
      int lline = caret.getLogicalPosition().line;
      int total = EditorHelper.getLineCount(((IjVimEditor) editor).getEditor());
      return lline + count <= total;
    });
    if (!allowedExecution) return false;
    for (int i = 0; i < executions; i++) {
      NativeAction joinLinesAction = VimInjectorKt.getInjector().getNativeActionManager().getJoinLines();
      if (joinLinesAction != null) {
        VimInjectorKt.getInjector().getActionExecutor().executeAction(joinLinesAction, context);
      }
    }
    return true;
  }

  @Override
  public void joinViaIdeaBySelections(@NotNull VimEditor editor,
                                      @NotNull ExecutionContext context,
                                      @NotNull Map<@NotNull VimCaret, @NotNull ? extends VimSelection> caretsAndSelections) {
    caretsAndSelections.forEach((caret, range) -> {
      if (!caret.isValid()) return;
      final Pair<Integer, Integer> nativeRange = range.getNativeStartAndEnd();
      ((IjVimCaret) caret).getCaret().setSelection(nativeRange.getFirst(), nativeRange.getSecond());
    });
    NativeAction joinLinesAction = VimInjectorKt.getInjector().getNativeActionManager().getJoinLines();
    if (joinLinesAction != null) {
      VimInjectorKt.getInjector().getActionExecutor().executeAction(joinLinesAction, context);
    }
    ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets().forEach(caret -> {
      caret.removeSelection();
      final VisualPosition currentVisualPosition = caret.getVisualPosition();
      if (currentVisualPosition.line < 1) return;
      final VisualPosition newVisualPosition =
        new VisualPosition(currentVisualPosition.line - 1, currentVisualPosition.column);
      caret.moveToVisualPosition(newVisualPosition);
    });
  }

  /**
   * Begin Replace mode
   *  @param editor  The editor to replace in
   * @param context The data context
   */
  @Override
  public void changeReplace(@NotNull VimEditor editor, @NotNull ExecutionContext context) {
    initInsert(editor, context, CommandState.Mode.REPLACE);
  }

  /**
   * Replace each of the next count characters with the character ch
   *
   * @param editor The editor to change
   * @param caret  The caret to perform action on
   * @param count  The number of characters to change
   * @param ch     The character to change to
   * @return true if able to change count characters, false if not
   */
  @Override
  public boolean changeCharacter(@NotNull VimEditor editor, @NotNull VimCaret caret, int count, char ch) {
    int col = ((IjVimCaret) caret).getCaret().getLogicalPosition().column;
    int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor());
    int offset = ((IjVimCaret) caret).getCaret().getOffset();
    if (len - col < count) {
      return false;
    }

    // Special case - if char is newline, only add one despite count
    int num = count;
    String space = null;
    if (ch == '\n') {
      num = 1;
      space = EditorHelper.getLeadingWhitespace(((IjVimEditor) editor).getEditor(), editor.offsetToLogicalPosition(offset).getLine());
      if (logger.isDebugEnabled()) {
        logger.debug("space='" + space + "'");
      }
    }

    StringBuilder repl = new StringBuilder(count);
    for (int i = 0; i < num; i++) {
      repl.append(ch);
    }

    replaceText(editor, offset, offset + count, repl.toString());

    // Indent new line if we replaced with a newline
    if (ch == '\n') {
      insertText(editor, caret, offset + 1, space);
      int slen = space.length();
      if (slen == 0) {
        slen++;
      }
      InlayHelperKt.moveToInlayAwareOffset(((IjVimCaret) caret).getCaret(), offset + slen);
    }

    return true;
  }

  /**
   * Each character in the supplied range gets replaced with the character ch
   *
   * @param editor The editor to change
   * @param range  The range to change
   * @param ch     The replacing character
   * @return true if able to change the range, false if not
   */
  @Override
  public boolean changeCharacterRange(@NotNull VimEditor editor, @NotNull TextRange range, char ch) {
    if (logger.isDebugEnabled()) {
      logger.debug("change range: " + range + " to " + ch);
    }

    CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
    int[] starts = range.getStartOffsets();
    int[] ends = range.getEndOffsets();
    for (int j = ends.length - 1; j >= 0; j--) {
      for (int i = starts[j]; i < ends[j]; i++) {
        if (i < chars.length() && '\n' != chars.charAt(i)) {
          replaceText(editor, i, i + 1, Character.toString(ch));
        }
      }
    }

    return true;
  }

  @Override
  public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType(@NotNull VimEditor editor,
                                                                        @NotNull VimCaret caret,
                                                                        @NotNull ExecutionContext context,
                                                                        final @NotNull Argument argument,
                                                                        boolean isChange,
                                                                        @NotNull OperatorArguments operatorArguments) {
    final TextRange range =
      injector.getMotion().getMotionRange(editor, caret, context, argument, operatorArguments);
    if (range == null) return null;

    // Delete motion commands that are not linewise become linewise if all the following are true:
    // 1) The range is across multiple lines
    // 2) There is only whitespace before the start of the range
    // 3) There is only whitespace after the end of the range
    SelectionType type;
    if (argument.getMotion().isLinewiseMotion()) {
      type = SelectionType.LINE_WISE;
    }
    else {
      type = SelectionType.CHARACTER_WISE;
    }
    final Command motion = argument.getMotion();
    if (!isChange && !motion.isLinewiseMotion()) {
      VimLogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset());
      VimLogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset());
      if (start.getLine() != end.getLine()) {
        if (!SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getStartOffset(), -1) &&
            !SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getEndOffset(), 1)) {
          type = SelectionType.LINE_WISE;
        }
      }
    }
    return new Pair<>(range, type);
  }

  @Override
  public @Nullable Pair<@NotNull TextRange, @NotNull SelectionType> getDeleteRangeAndType2(@NotNull VimEditor editor,
                                                                         @NotNull VimCaret caret,
                                                                         @NotNull ExecutionContext context,
                                                                         final @NotNull Argument argument,
                                                                         boolean isChange,
                                                                         @NotNull OperatorArguments operatorArguments) {
    final TextRange range = MotionGroup.getMotionRange2(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), ((IjExecutionContext) context).getContext(), argument, operatorArguments);
    if (range == null) return null;

    // Delete motion commands that are not linewise become linewise if all the following are true:
    // 1) The range is across multiple lines
    // 2) There is only whitespace before the start of the range
    // 3) There is only whitespace after the end of the range
    SelectionType type;
    if (argument.getMotion().isLinewiseMotion()) {
      type = SelectionType.LINE_WISE;
    }
    else {
      type = SelectionType.CHARACTER_WISE;
    }
    final Command motion = argument.getMotion();
    if (!isChange && !motion.isLinewiseMotion()) {
      VimLogicalPosition start = editor.offsetToLogicalPosition(range.getStartOffset());
      VimLogicalPosition end = editor.offsetToLogicalPosition(range.getEndOffset());
      if (start.getLine() != end.getLine()) {
        if (!SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getStartOffset(), -1) &&
            !SearchHelper.anyNonWhitespace(((IjVimEditor) editor).getEditor(), range.getEndOffset(), 1)) {
          type = SelectionType.LINE_WISE;
        }
      }
    }
    return new Pair<>(range, type);
  }

  /**
   * Delete the range of text.
   *
   * @param editor   The editor to delete the text from
   * @param caret    The caret to be moved after deletion
   * @param range    The range to delete
   * @param type     The type of deletion
   * @param isChange Is from a change action
   * @return true if able to delete the text, false if not
   */
  @Override
  public boolean deleteRange(@NotNull VimEditor editor,
                             @NotNull VimCaret caret,
                             @NotNull TextRange range,
                             @Nullable SelectionType type,
                             boolean isChange) {

    // Update the last column before we delete, or we might be retrieving the data for a line that no longer exists
    UserDataManager.setVimLastColumn(((IjVimCaret) caret).getCaret(), InlayHelperKt.getInlayAwareVisualColumn(((IjVimCaret) caret).getCaret()));

    boolean removeLastNewLine = removeLastNewLine(editor, range, type);
    final boolean res = deleteText(editor, range, type);
    if (removeLastNewLine) {
      int textLength = ((IjVimEditor) editor).getEditor().getDocument().getTextLength();
      ((IjVimEditor) editor).getEditor().getDocument().deleteString(textLength - 1, textLength);
    }

    if (res) {
      int pos = EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), range.getStartOffset(), isChange);
      if (type == SelectionType.LINE_WISE) {
        pos = VimPlugin.getMotion()
          .moveCaretToLineWithStartOfLineOption(editor, editor.offsetToLogicalPosition(pos).getLine(),
                                                caret);
      }
      injector.getMotion().moveCaret(editor, caret, pos);
    }
    return res;
  }

  private boolean removeLastNewLine(@NotNull VimEditor editor, @NotNull TextRange range, @Nullable SelectionType type) {
    int endOffset = range.getEndOffset();
    int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor());
    if (endOffset > fileSize) {
      if (injector.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.ideastrictmodeName, OptionConstants.ideastrictmodeName)) {
        throw new IllegalStateException("Incorrect offset. File size: " + fileSize + ", offset: " + endOffset);
      }
      endOffset = fileSize;
    }
    return type == SelectionType.LINE_WISE &&
           range.getStartOffset() != 0 &&
           ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence().charAt(endOffset - 1) != '\n' &&
           endOffset == fileSize;
  }

  @Override
  public void insertLineAround(@NotNull VimEditor editor, @NotNull ExecutionContext context, int shift) {
    com.maddyhome.idea.vim.newapi.ChangeGroupKt.insertLineAround(editor, context, shift);
  }

  @Override
  public boolean deleteRange2(@NotNull VimEditor editor,
                              @NotNull VimCaret caret,
                              @NotNull TextRange range,
                              @NotNull SelectionType type) {
    return com.maddyhome.idea.vim.newapi.ChangeGroupKt.deleteRange(editor, caret, range, type);
  }

  /**
   * Delete count characters and then enter insert mode
   *
   * @param editor The editor to change
   * @param caret  The caret to be moved
   * @param count  The number of characters to change
   * @return true if able to delete count characters, false if not
   */
  @Override
  public boolean changeCharacters(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
    int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor());
    int col = ((IjVimCaret) caret).getCaret().getLogicalPosition().column;
    if (col + count >= len) {
      return changeEndOfLine(editor, caret, 1);
    }

    boolean res = deleteCharacter(editor, caret, count, true);
    if (res) {
      UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
    }

    return res;
  }

  /**
   * Delete from the cursor to the end of count - 1 lines down and enter insert mode
   *
   * @param editor The editor to change
   * @param caret  The caret to perform action on
   * @param count  The number of lines to change
   * @return true if able to delete count lines, false if not
   */
  @Override
  public boolean changeEndOfLine(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
    boolean res = deleteEndOfLine(editor, caret, count);
    if (res) {
      injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineEnd(editor, caret));
      UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
    }

    return res;
  }

  /**
   * Delete the text covered by the motion command argument and enter insert mode
   *
   * @param editor   The editor to change
   * @param caret    The caret on which the motion is supposed to be performed
   * @param context  The data context
   * @param argument The motion command
   * @return true if able to delete the text, false if not
   */
  @Override
  public boolean changeMotion(@NotNull VimEditor editor,
                              @NotNull VimCaret caret,
                              @NotNull ExecutionContext context,
                              @NotNull Argument argument,
                              @NotNull OperatorArguments operatorArguments) {
    int count0 = operatorArguments.getCount0();
    // Vim treats cw as ce and cW as cE if cursor is on a non-blank character
    final Command motion = argument.getMotion();

    String id = motion.getAction().getId();
    boolean kludge = false;
    boolean bigWord = id.equals(VIM_MOTION_BIG_WORD_RIGHT);
    final CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
    final int offset = ((IjVimCaret) caret).getCaret().getOffset();
    int fileSize = EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor());
    if (fileSize > 0 && offset < fileSize) {
      final CharacterHelper.CharacterType charType = CharacterHelper.charType(chars.charAt(offset), bigWord);
      if (charType != CharacterHelper.CharacterType.WHITESPACE) {
        final boolean lastWordChar =
          offset >= fileSize - 1 || CharacterHelper.charType(chars.charAt(offset + 1), bigWord) != charType;
        if (wordMotions.contains(id) && lastWordChar && motion.getCount() == 1) {
          final boolean res = deleteCharacter(editor, caret, 1, true);
          if (res) {
            UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
          }
          return res;
        }
        switch (id) {
          case VIM_MOTION_WORD_RIGHT:
            kludge = true;
            motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_WORD_END_RIGHT));

            break;
          case VIM_MOTION_BIG_WORD_RIGHT:
            kludge = true;
            motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT));

            break;
          case VIM_MOTION_CAMEL_RIGHT:
            kludge = true;
            motion.setAction(RegisterActions.findActionOrDie(VIM_MOTION_CAMEL_END_RIGHT));

            break;
        }
      }
    }

    if (kludge) {
      int cnt = operatorArguments.getCount1() * motion.getCount();
      int pos1 = SearchHelper.findNextWordEnd(chars, offset, fileSize, cnt, bigWord, false);
      int pos2 = SearchHelper.findNextWordEnd(chars, pos1, fileSize, -cnt, bigWord, false);
      if (logger.isDebugEnabled()) {
        logger.debug("pos=" + offset);
        logger.debug("pos1=" + pos1);
        logger.debug("pos2=" + pos2);
        logger.debug("count=" + operatorArguments.getCount1());
        logger.debug("arg.count=" + motion.getCount());
      }
      if (pos2 == offset) {
        if (operatorArguments.getCount1() > 1) {
          count0--;
        }
        else if (motion.getCount() > 1) {
          motion.setCount(motion.getCount() - 1);
        }
        else {
          motion.setFlags(EnumSet.noneOf(CommandFlags.class));
        }
      }
    }

    if (VimPlugin.getOptionService().isSet(OptionScope.GLOBAL.INSTANCE, OptionConstants.experimentalapiName, OptionConstants.experimentalapiName)) {
      Pair<TextRange, SelectionType> deleteRangeAndType =
        getDeleteRangeAndType2(editor, caret, context, argument, true, operatorArguments.withCount0(count0));
      if (deleteRangeAndType == null) return false;
      ChangeGroupKt.changeRange(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), deleteRangeAndType.getFirst(), deleteRangeAndType.getSecond(), ((IjExecutionContext) context).getContext());
      return true;
    }
    else {
      Pair<TextRange, SelectionType> deleteRangeAndType =
        getDeleteRangeAndType(editor, caret, context, argument, true, operatorArguments.withCount0(count0));
      if (deleteRangeAndType == null) return false;
      return changeRange(editor, caret, deleteRangeAndType.getFirst(), deleteRangeAndType.getSecond(), context);
    }
  }

  /**
   * Counts number of lines in the visual block.
   * <p>
   * The result includes empty and short lines which does not have explicit start position (caret).
   *
   * @param editor The editor the block was selected in
   * @param range  The range corresponding to the selected block
   * @return total number of lines
   */
  public static int getLinesCountInVisualBlock(@NotNull VimEditor editor, @NotNull TextRange range) {
    final int[] startOffsets = range.getStartOffsets();
    if (startOffsets.length == 0) return 0;
    final VimLogicalPosition firstStart = editor.offsetToLogicalPosition(startOffsets[0]);
    final VimLogicalPosition lastStart = editor.offsetToLogicalPosition(startOffsets[range.size() - 1]);
    return lastStart.getLine() - firstStart.getLine() + 1;
  }

  /**
   * Toggles the case of count characters
   *
   * @param editor The editor to change
   * @param caret  The caret on which the operation is performed
   * @param count  The number of characters to change
   * @return true if able to change count characters
   */
  @Override
  public boolean changeCaseToggleCharacter(@NotNull VimEditor editor, @NotNull VimCaret caret, int count) {
    final int offset = VimPlugin.getMotion().getOffsetOfHorizontalMotion(editor, caret, count, true);
    if (offset == -1) {
      return false;
    }
    changeCase(editor, ((IjVimCaret) caret).getCaret().getOffset(), offset, CharacterHelper.CASE_TOGGLE);
    injector.getMotion().moveCaret(editor, caret, EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), offset, false));
    return true;
  }

  @Override
  public boolean blockInsert(@NotNull VimEditor editor,
                             @NotNull ExecutionContext context,
                             @NotNull TextRange range,
                             boolean append,
                             @NotNull OperatorArguments operatorArguments) {
    final int lines = getLinesCountInVisualBlock(editor, range);
    final VimLogicalPosition startPosition = editor.offsetToLogicalPosition(range.getStartOffset());

    boolean visualBlockMode = operatorArguments.getMode() == CommandState.Mode.VISUAL &&
                              operatorArguments.getSubMode() == CommandState.SubMode.VISUAL_BLOCK;
    for (Caret caret : ((IjVimEditor) editor).getEditor().getCaretModel().getAllCarets()) {
      final int line = startPosition.getLine();
      int column = startPosition.getColumn();
      if (!visualBlockMode) {
        column = 0;
      }
      else if (append) {
        column += range.getMaxLength();
        if (UserDataManager.getVimLastColumn(caret) == VimMotionGroupBase.LAST_COLUMN) {
          column = VimMotionGroupBase.LAST_COLUMN;
        }
      }

      final int lineLength = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor(), line);
      if (column < VimMotionGroupBase.LAST_COLUMN && lineLength < column) {
        final String pad = EditorHelper.pad(((IjVimEditor) editor).getEditor(), ((IjExecutionContext) context).getContext(), line, column);
        final int offset = ((IjVimEditor) editor).getEditor().getDocument().getLineEndOffset(line);
        insertText(editor, new IjVimCaret(caret), offset, pad);
      }

      if (visualBlockMode || !append) {
        InlayHelperKt.moveToInlayAwareLogicalPosition(caret, new LogicalPosition(line, column));
      }
      if (visualBlockMode) {
        setInsertRepeat(lines, column, append);
      }
    }

    if (visualBlockMode || !append) {
      insertBeforeCursor(editor, context);
    }
    else {
      insertAfterCursor(editor, context);
    }

    return true;
  }

  /**
   * Changes the case of all the characters in the range
   *
   * @param editor The editor to change
   * @param caret  The caret to be moved
   * @param range  The range to change
   * @param type   The case change type (TOGGLE, UPPER, LOWER)
   * @return true if able to delete the text, false if not
   */
  @Override
  public boolean changeCaseRange(@NotNull VimEditor editor, @NotNull VimCaret caret, @NotNull TextRange range, char type) {
    int[] starts = range.getStartOffsets();
    int[] ends = range.getEndOffsets();
    for (int i = ends.length - 1; i >= 0; i--) {
      changeCase(editor, starts[i], ends[i], type);
    }
    injector.getMotion().moveCaret(editor, caret, range.getStartOffset());
    return true;
  }

  /**
   * This performs the actual case change.
   *
   * @param editor The editor to change
   * @param start  The start offset to change
   * @param end    The end offset to change
   * @param type   The type of change (TOGGLE, UPPER, LOWER)
   */
  private void changeCase(@NotNull VimEditor editor, int start, int end, char type) {
    if (start > end) {
      int t = end;
      end = start;
      start = t;
    }
    end = EditorHelper.normalizeOffset(((IjVimEditor) editor).getEditor(), end);

    CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
    StringBuilder sb = new StringBuilder();
    for (int i = start; i < end; i++) {
      sb.append(CharacterHelper.changeCase(chars.charAt(i), type));
    }
    replaceText(editor, start, end, sb.toString());
  }

  /**
   * Deletes the range of text and enters insert mode
   *
   * @param editor The editor to change
   * @param caret  The caret to be moved after range deletion
   * @param range  The range to change
   * @param type   The type of the range
   * @return true if able to delete the range, false if not
   */
  @Override
  public boolean changeRange(@NotNull VimEditor editor,
                             @NotNull VimCaret caret,
                             @NotNull TextRange range,
                             @NotNull SelectionType type,
                             ExecutionContext context) {
    int col = 0;
    int lines = 0;
    if (type == SelectionType.BLOCK_WISE) {
      lines = getLinesCountInVisualBlock(editor, range);
      col = editor.offsetToLogicalPosition(range.getStartOffset()).getColumn();
      if (UserDataManager.getVimLastColumn(((IjVimCaret) caret).getCaret()) == VimMotionGroupBase.LAST_COLUMN) {
        col = VimMotionGroupBase.LAST_COLUMN;
      }
    }
    boolean after = range.getEndOffset() >= EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor());

    final VimLogicalPosition lp = editor.offsetToLogicalPosition(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, caret));

    boolean res = deleteRange(editor, caret, range, type, true);
    if (res) {
      if (type == SelectionType.LINE_WISE) {
        // Please don't use `getDocument().getText().isEmpty()` because it converts CharSequence into String
        if (((IjVimEditor) editor).getEditor().getDocument().getTextLength() == 0) {
          insertBeforeCursor(editor, context);
        }
        else if (after && !EditorHelperRt.endsWithNewLine(((IjVimEditor) editor).getEditor())) {
          insertNewLineBelow(editor, caret, lp.getColumn());
        }
        else {
          insertNewLineAbove(editor, caret, lp.getColumn());
        }
      }
      else {
        if (type == SelectionType.BLOCK_WISE) {
          setInsertRepeat(lines, col, false);
        }
        UserDataManager.setVimChangeActionSwitchMode(((IjVimEditor) editor).getEditor(), CommandState.Mode.INSERT);
      }
    }
    else {
      insertBeforeCursor(editor, context);
    }

    return true;
  }

  private void restoreCursor(@NotNull VimEditor editor, @NotNull VimCaret caret, int startLine) {
    if (!caret.equals(editor.primaryCaret())) {
      ((IjVimEditor) editor).getEditor().getCaretModel().addCaret(
        ((IjVimEditor) editor).getEditor().offsetToVisualPosition(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, startLine)), false);
    }
  }

  /**
   * Changes the case of all the character moved over by the motion argument.
   *
   * @param editor   The editor to change
   * @param caret    The caret on which motion pretends to be performed
   * @param context  The data context
   * @param type     The case change type (TOGGLE, UPPER, LOWER)
   * @param argument The motion command
   * @return true if able to delete the text, false if not
   */
  @Override
  public boolean changeCaseMotion(@NotNull VimEditor editor,
                                  @NotNull VimCaret caret,
                                  ExecutionContext context,
                                  char type,
                                  @NotNull Argument argument,
                                  @NotNull OperatorArguments operatorArguments) {
    final TextRange range = injector.getMotion().getMotionRange(editor, caret, context, argument,
                                                       operatorArguments);
    return range != null && changeCaseRange(editor, caret, range, type);
  }

  @Override
  public boolean reformatCodeMotion(@NotNull VimEditor editor,
                                    @NotNull VimCaret caret,
                                    ExecutionContext context,
                                    @NotNull Argument argument,
                                    @NotNull OperatorArguments operatorArguments) {
    final TextRange range = injector.getMotion().getMotionRange(editor, caret, context, argument,
                                                       operatorArguments);
    return range != null && reformatCodeRange(editor, caret, range);
  }

  @Override
  public void reformatCodeSelection(@NotNull VimEditor editor, @NotNull VimCaret caret, @NotNull VimSelection range) {
    final TextRange textRange = range.toVimTextRange(true);
    reformatCodeRange(editor, caret, textRange);
  }

  private boolean reformatCodeRange(@NotNull VimEditor editor, @NotNull VimCaret caret, @NotNull TextRange range) {
    int[] starts = range.getStartOffsets();
    int[] ends = range.getEndOffsets();
    final int firstLine = editor.offsetToLogicalPosition(range.getStartOffset()).getLine();
    for (int i = ends.length - 1; i >= 0; i--) {
      final int startOffset = EditorHelper.getLineStartForOffset(((IjVimEditor) editor).getEditor(), starts[i]);
      final int endOffset = EditorHelper.getLineEndForOffset(((IjVimEditor) editor).getEditor(), ends[i] - (startOffset == ends[i] ? 0 : 1));
      reformatCode(editor, startOffset, endOffset);
    }
    final int newOffset = VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, firstLine);
    injector.getMotion().moveCaret(editor, caret, newOffset);
    return true;
  }

  private void reformatCode(@NotNull VimEditor editor, int start, int end) {
    final Project project = ((IjVimEditor) editor).getEditor().getProject();
    if (project == null) return;
    final PsiFile file = PsiUtilBase.getPsiFileInEditor(((IjVimEditor) editor).getEditor(), project);
    if (file == null) return;
    final com.intellij.openapi.util.TextRange textRange = com.intellij.openapi.util.TextRange.create(start, end);
    CodeStyleManager.getInstance(project).reformatText(file, Collections.singletonList(textRange));
  }

  @Override
  public void autoIndentMotion(@NotNull VimEditor editor,
                               @NotNull VimCaret caret,
                               @NotNull ExecutionContext context,
                               @NotNull Argument argument,
                               @NotNull OperatorArguments operatorArguments) {
    final TextRange range = injector.getMotion().getMotionRange(editor, caret, context, argument, operatorArguments);
    if (range != null) {
      autoIndentRange(editor, caret, context,
                      new TextRange(range.getStartOffset(), EngineHelperKt.getEndOffsetInclusive(range)));
    }
  }

  @Override
  public void autoIndentRange(@NotNull VimEditor editor,
                              @NotNull VimCaret caret,
                              @NotNull ExecutionContext context,
                              @NotNull TextRange range) {
    final int startOffset = injector.getEngineEditorHelper().getLineStartForOffset(editor, range.getStartOffset());
    final int endOffset = injector.getEngineEditorHelper().getLineEndForOffset(editor, range.getEndOffset());

    VisualModeHelperKt.vimSetSystemSelectionSilently(((IjVimEditor) editor).getEditor().getSelectionModel(), startOffset, endOffset);

    NativeAction joinLinesAction = VimInjectorKt.getInjector().getNativeActionManager().getIndentLines();
    if (joinLinesAction != null) {
      VimInjectorKt.getInjector().getActionExecutor().executeAction(joinLinesAction, context);
    }

    final int firstLine = editor.offsetToLogicalPosition(Math.min(startOffset, endOffset)).getLine();
    final int newOffset = VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, firstLine);
    injector.getMotion().moveCaret(editor, caret, newOffset);
    restoreCursor(editor, caret, ((IjVimCaret) caret).getCaret().getLogicalPosition().line);
  }

  @Override
  public void indentLines(@NotNull VimEditor editor,
                          @NotNull VimCaret caret,
                          @NotNull ExecutionContext context,
                          int lines,
                          int dir) {
    int start = ((IjVimCaret) caret).getCaret().getOffset();
    int end = VimPlugin.getMotion().moveCaretToLineEndOffset(editor, caret, lines - 1, true);
    indentRange(editor, caret, context, new TextRange(start, end), 1, dir);
  }

  /**
   * Inserts text into the document
   *
   * @param editor The editor to insert into
   * @param caret  The caret to start insertion in
   * @param str    The text to insert
   */
  @Override
  public void insertText(@NotNull VimEditor editor, @NotNull VimCaret caret, int offset, @NotNull String str) {
    ((IjVimEditor) editor).getEditor().getDocument().insertString(offset, str);
    InlayHelperKt.moveToInlayAwareOffset(((IjVimCaret) caret).getCaret(), offset + str.length());

    VimPlugin.getMark().setMark(editor, MARK_CHANGE_POS, offset);
  }

  @Override
  public void insertText(@NotNull VimEditor editor, @NotNull VimCaret caret, @NotNull String str) {
    insertText(editor, caret, ((IjVimCaret) caret).getCaret().getOffset(), str);
  }

  public void insertText(@NotNull VimEditor editor,
                         @NotNull VimCaret caret,
                         @NotNull VimLogicalPosition start,
                         @NotNull String str) {
    insertText(editor, caret, editor.logicalPositionToOffset(start), str);
  }

  @Override
  public void indentMotion(@NotNull VimEditor editor,
                           @NotNull VimCaret caret,
                           @NotNull ExecutionContext context,
                           @NotNull Argument argument,
                           int dir,
                           @NotNull OperatorArguments operatorArguments) {
    final TextRange range =
      injector.getMotion().getMotionRange(editor, caret, context, argument, operatorArguments);
    if (range != null) {
      indentRange(editor, caret, context, range, 1, dir);
    }
  }

  /**
   * Replace text in the editor
   *
   * @param editor The editor to replace text in
   * @param start  The start offset to change
   * @param end    The end offset to change
   * @param str    The new text
   */
  private void replaceText(@NotNull VimEditor editor, int start, int end, @NotNull String str) {
    ((IjVimEditor) editor).getEditor().getDocument().replaceString(start, end, str);

    final int newEnd = start + str.length();
    VimPlugin.getMark().setChangeMarks(editor, new TextRange(start, newEnd));
    VimPlugin.getMark().setMark(editor, MARK_CHANGE_POS, newEnd);
  }

  @Override
  public void indentRange(@NotNull VimEditor editor,
                          @NotNull VimCaret caret,
                          @NotNull ExecutionContext context,
                          @NotNull TextRange range,
                          int count,
                          int dir) {
    if (logger.isDebugEnabled()) {
      logger.debug("count=" + count);
    }

    // Update the last column before we indent, or we might be retrieving the data for a line that no longer exists
    UserDataManager.setVimLastColumn(((IjVimCaret) caret).getCaret(), InlayHelperKt.getInlayAwareVisualColumn(((IjVimCaret) caret).getCaret()));

    IndentConfig indentConfig = IndentConfig.create(((IjVimEditor) editor).getEditor(), ((IjExecutionContext) context).getContext());

    final int sline = editor.offsetToLogicalPosition(range.getStartOffset()).getLine();
    final VimLogicalPosition endLogicalPosition = editor.offsetToLogicalPosition(range.getEndOffset());
    final int eline =
      endLogicalPosition.getColumn() == 0 ? Math.max(endLogicalPosition.getLine() - 1, 0) : endLogicalPosition.getLine();

    if (range.isMultiple()) {
      final int from = editor.offsetToLogicalPosition(range.getStartOffset()).getColumn();
      if (dir == 1) {
        // Right shift blockwise selection
        final String indent = indentConfig.createIndentByCount(count);

        for (int l = sline; l <= eline; l++) {
          int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor(), l);
          if (len > from) {
            VimLogicalPosition spos = new VimLogicalPosition(l, from, false);
            insertText(editor, caret, spos, indent);
          }
        }
      }
      else {
        // Left shift blockwise selection
        CharSequence chars = ((IjVimEditor) editor).getEditor().getDocument().getCharsSequence();
        for (int l = sline; l <= eline; l++) {
          int len = EditorHelper.getLineLength(((IjVimEditor) editor).getEditor(), l);
          if (len > from) {
            VimLogicalPosition spos = new VimLogicalPosition(l, from, false);
            VimLogicalPosition epos = new VimLogicalPosition(l, from + indentConfig.getTotalIndent(count) - 1, false);
            int wsoff = editor.logicalPositionToOffset(spos);
            int weoff = editor.logicalPositionToOffset(epos);
            int pos;
            for (pos = wsoff; pos <= weoff; pos++) {
              if (CharacterHelper.charType(chars.charAt(pos), false) != CharacterHelper.CharacterType.WHITESPACE) {
                break;
              }
            }
            if (pos > wsoff) {
              deleteText(editor, new TextRange(wsoff, pos), null);
            }
          }
        }
      }
    }
    else {
      // Shift non-blockwise selection
      for (int l = sline; l <= eline; l++) {
        final int soff = injector.getEngineEditorHelper().getLineStartOffset(editor, l);
        final int eoff = injector.getEngineEditorHelper().getLineEndOffset(editor, l, true);
        final int woff = VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, l);
        final int col = ((IjVimEditor) editor).getEditor().offsetToVisualPosition(woff).getColumn();
        final int limit = Math.max(0, col + dir * indentConfig.getTotalIndent(count));
        if (col > 0 || soff != eoff) {
          final String indent = indentConfig.createIndentBySize(limit);
          replaceText(editor, soff, woff, indent);
        }
      }
    }

    if (!CommandStateHelper.inInsertMode(((IjVimEditor) editor).getEditor())) {
      if (!range.isMultiple()) {
        injector.getMotion().moveCaret(editor, caret, VimPlugin.getMotion().moveCaretToLineWithStartOfLineOption(editor, sline, caret));
      }
      else {
        injector.getMotion().moveCaret(editor, caret, range.getStartOffset());
      }
    }

    UserDataManager.setVimLastColumn(((IjVimCaret) caret).getCaret(), caret.getVisualPosition().getColumn());
  }

  /**
   * Delete text from the document. This will fail if being asked to store the deleted text into a read-only
   * register.
   * <p>
   * End offset of range is exclusive
   * <p>
   * delete new TextRange(1, 5)
   * 0123456789
   * Hello, xyz
   * .||||....
   * <p>
   * end <= text.length
   *
   * @param editor The editor to delete from
   * @param range  The range to delete
   * @param type   The type of deletion
   * @return true if able to delete the text, false if not
   */
  private boolean deleteText(final @NotNull VimEditor editor,
                             final @NotNull TextRange range,
                             @Nullable SelectionType type) {
    TextRange updatedRange = range;
    // Fix for https://youtrack.jetbrains.net/issue/VIM-35
    if (!range.normalize(EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()))) {
      if (range.getStartOffset() == range.getEndOffset() &&
          range.getStartOffset() == EditorHelperRt.getFileSize(((IjVimEditor) editor).getEditor()) &&
          range.getStartOffset() != 0) {
        updatedRange = new TextRange(range.getStartOffset() - 1, range.getEndOffset());
      }
      else {
        return false;
      }
    }

    if (type == null ||
        CommandStateHelper.inInsertMode(((IjVimEditor) editor).getEditor()) ||
        VimPlugin.getRegister().storeText(editor, updatedRange, type, true)) {
      final Document document = ((IjVimEditor) editor).getEditor().getDocument();
      final int[] startOffsets = updatedRange.getStartOffsets();
      final int[] endOffsets = updatedRange.getEndOffsets();
      for (int i = updatedRange.size() - 1; i >= 0; i--) {
        document.deleteString(startOffsets[i], endOffsets[i]);
      }

      if (type != null) {
        final int start = updatedRange.getStartOffset();
        VimPlugin.getMark().setMark(editor, MARK_CHANGE_POS, start);
        VimPlugin.getMark().setChangeMarks(editor, new TextRange(start, start + 1));
      }

      return true;
    }

    return false;
  }

  /**
   * Sort range of text with a given comparator
   *
   * @param editor         The editor to replace text in
   * @param range          The range to sort
   * @param lineComparator The comparator to use to sort
   * @return true if able to sort the text, false if not
   */
  public boolean sortRange(@NotNull VimEditor editor, @NotNull LineRange range, @NotNull Comparator<String> lineComparator) {
    final int startLine = range.startLine;
    final int endLine = range.endLine;
    final int count = endLine - startLine + 1;
    if (count < 2) {
      return false;
    }

    final int startOffset = ((IjVimEditor) editor).getEditor().getDocument().getLineStartOffset(startLine);
    final int endOffset = ((IjVimEditor) editor).getEditor().getDocument().getLineEndOffset(endLine);

    return sortTextRange(editor, startOffset, endOffset, lineComparator);
  }

  /**
   * Sorts a text range with a comparator. Returns true if a replace was performed, false otherwise.
   *
   * @param editor         The editor to replace text in
   * @param start          The starting position for the sort
   * @param end            The ending position for the sort
   * @param lineComparator The comparator to use to sort
   * @return true if able to sort the text, false if not
   */
  private boolean sortTextRange(@NotNull VimEditor editor,
                                int start,
                                int end,
                                @NotNull Comparator<String> lineComparator) {
    final String selectedText = ((IjVimEditor) editor).getEditor().getDocument().getText(new TextRangeInterval(start, end));
    final List<String> lines = Lists.newArrayList(Splitter.on("\n").split(selectedText));
    if (lines.size() < 1) {
      return false;
    }
    lines.sort(lineComparator);
    replaceText(editor, start, end, StringUtil.join(lines, "\n"));
    return true;
  }

  /**
   * Perform increment and decrement for numbers in visual mode
   * <p>
   * Flag [avalanche] marks if increment (or decrement) should be performed in avalanche mode
   * (for v_g_Ctrl-A and v_g_Ctrl-X commands)
   *
   * @return true
   */
  @Override
  public boolean changeNumberVisualMode(final @NotNull VimEditor editor,
                                        @NotNull VimCaret caret,
                                        @NotNull TextRange selectedRange,
                                        final int count,
                                        boolean avalanche) {
    String nf = ((VimString) VimPlugin.getOptionService().getOptionValue(new OptionScope.LOCAL(editor), OptionConstants.nrformatsName, OptionConstants.nrformatsName)).getValue();
    boolean alpha = nf.contains("alpha");
    boolean hex = nf.contains("hex");
    boolean octal = nf.contains("octal");

    @NotNull List<Pair<TextRange, SearchHelper.NumberType>> numberRanges =
      SearchHelper.findNumbersInRange(((IjVimEditor) editor).getEditor(), selectedRange, alpha, hex, octal);

    List<String> newNumbers = new ArrayList<>();
    for (int i = 0; i < numberRanges.size(); i++) {
      Pair<TextRange, SearchHelper.NumberType> numberRange = numberRanges.get(i);
      int iCount = avalanche ? (i + 1) * count : count;
      String newNumber = changeNumberInRange(editor, numberRange, iCount, alpha, hex, octal);
      newNumbers.add(newNumber);
    }

    for (int i = newNumbers.size() - 1; i >= 0; i--) {
      // Replace text bottom up. In other direction ranges will be desynchronized after inc numbers like 99
      Pair<TextRange, SearchHelper.NumberType> rangeToReplace = numberRanges.get(i);
      String newNumber = newNumbers.get(i);
      replaceText(editor, rangeToReplace.getFirst().getStartOffset(), rangeToReplace.getFirst().getEndOffset(), newNumber);
    }

    InlayHelperKt.moveToInlayAwareOffset(((IjVimCaret) caret).getCaret(), selectedRange.getStartOffset());
    return true;
  }

  private void exitAllSingleCommandInsertModes(@NotNull VimEditor editor) {
    while (CommandStateHelper.inSingleCommandMode(((IjVimEditor) editor).getEditor())) {
      CommandState.getInstance(editor).popModes();
      if (CommandStateHelper.inInsertMode(((IjVimEditor) editor).getEditor())) {
        CommandState.getInstance(editor).popModes();
      }
    }
  }

  private final List<Object> strokes = new ArrayList<>();
  private int repeatCharsCount;
  private @Nullable List<Object> lastStrokes;

  @Override
  public boolean changeNumber(final @NotNull VimEditor editor, @NotNull VimCaret caret, final int count) {
    final String nf = ((VimString) VimPlugin.getOptionService().getOptionValue(new OptionScope.LOCAL(editor), OptionConstants.nrformatsName, OptionConstants.nrformatsName)).getValue();
    final boolean alpha = nf.contains("alpha");
    final boolean hex = nf.contains("hex");
    final boolean octal = nf.contains("octal");

    @Nullable Pair<TextRange, SearchHelper.NumberType> range =
      SearchHelper.findNumberUnderCursor(((IjVimEditor) editor).getEditor(), ((IjVimCaret) caret).getCaret(), alpha, hex, octal);
    if (range == null) {
      logger.debug("no number on line");
      return false;
    }

    String newNumber = changeNumberInRange(editor, range, count, alpha, hex, octal);
    if (newNumber == null) {
      return false;
    }
    else {
      replaceText(editor, range.getFirst().getStartOffset(), range.getFirst().getEndOffset(), newNumber);
      InlayHelperKt.moveToInlayAwareOffset(((IjVimCaret) caret).getCaret(), range.getFirst().getStartOffset() + newNumber.length() - 1);
      return true;
    }
  }

  @Override
  public void reset() {
    strokes.clear();
    repeatCharsCount = 0;
    if (lastStrokes != null) {
      lastStrokes.clear();
    }
  }

  @Override
  public void saveStrokes(String newStrokes) {
    char[] chars = newStrokes.toCharArray();
    strokes.add(chars);
  }

  private int repeatLines;
  private int repeatColumn;
  private boolean repeatAppend;
  private boolean lastLower = true;
  private Document document;

  public @Nullable String changeNumberInRange(final @NotNull VimEditor editor,
                                              Pair<TextRange, SearchHelper.NumberType> range,
                                              final int count,
                                              boolean alpha,
                                              boolean hex,
                                              boolean octal) {
    String text = EditorHelper.getText(((IjVimEditor) editor).getEditor(), range.getFirst());
    SearchHelper.NumberType numberType = range.getSecond();
    if (logger.isDebugEnabled()) {
      logger.debug("found range " + range);
      logger.debug("text=" + text);
    }
    String number = text;
    if (text.length() == 0) {
      return null;
    }

    char ch = text.charAt(0);
    if (hex && SearchHelper.NumberType.HEX.equals(numberType)) {
      if (!text.toLowerCase().startsWith(HEX_START)) {
        throw new RuntimeException("Hex number should start with 0x: " + text);
      }
      for (int i = text.length() - 1; i >= 2; i--) {
        int index = "abcdefABCDEF".indexOf(text.charAt(i));
        if (index >= 0) {
          lastLower = index < 6;
          break;
        }
      }

      BigInteger num = new BigInteger(text.substring(2), 16);
      num = num.add(BigInteger.valueOf(count));
      if (num.compareTo(BigInteger.ZERO) < 0) {
        num = new BigInteger(MAX_HEX_INTEGER, 16).add(BigInteger.ONE).add(num);
      }
      number = num.toString(16);
      number = StringsKt.padStart(number, text.length() - 2, '0');

      if (!lastLower) {
        number = number.toUpperCase();
      }

      number = text.substring(0, 2) + number;
    }
    else if (octal && SearchHelper.NumberType.OCT.equals(numberType) && text.length() > 1) {
      if (!text.startsWith("0")) throw new RuntimeException("Oct number should start with 0: " + text);
      BigInteger num = new BigInteger(text, 8).add(BigInteger.valueOf(count));

      if (num.compareTo(BigInteger.ZERO) < 0) {
        num = new BigInteger("1777777777777777777777", 8).add(BigInteger.ONE).add(num);
      }
      number = num.toString(8);
      number = "0" + StringsKt.padStart(number, text.length() - 1, '0');
    }
    else if (alpha && SearchHelper.NumberType.ALPHA.equals(numberType)) {
      if (!Character.isLetter(ch)) throw new RuntimeException("Not alpha number : " + text);
      ch += count;
      if (Character.isLetter(ch)) {
        number = String.valueOf(ch);
      }
    }
    else if (SearchHelper.NumberType.DEC.equals(numberType)) {
      if (ch != '-' && !Character.isDigit(ch)) throw new RuntimeException("Not dec number : " + text);
      boolean pad = ch == '0';
      int len = text.length();
      if (ch == '-' && text.charAt(1) == '0') {
        pad = true;
        len--;
      }

      BigInteger num = new BigInteger(text);
      num = num.add(BigInteger.valueOf(count));
      number = num.toString();

      if (!octal && pad) {
        boolean neg = false;
        if (number.charAt(0) == '-') {
          neg = true;
          number = number.substring(1);
        }
        number = StringsKt.padStart(number, len, '0');
        if (neg) {
          number = "-" + number;
        }
      }
    }

    return number;
  }

  public void addInsertListener(VimInsertListener listener) {
    insertListeners.add(listener);
  }

  public void removeInsertListener(VimInsertListener listener) {
    insertListeners.remove(listener);
  }

  private void notifyListeners(Editor editor) {
    insertListeners.forEach(listener -> listener.insertModeStarted(editor));
  }

  @Override
  @TestOnly
  public void resetRepeat() {
    setInsertRepeat(0, 0, false);
  }

  private void updateLastInsertedTextRegister() {
    StringBuilder textToPutRegister = new StringBuilder();
    if (lastStrokes != null) {
      for (Object lastStroke : lastStrokes) {
        if (lastStroke instanceof char[]) {
          final char[] chars = (char[])lastStroke;
          textToPutRegister.append(new String(chars));
        }
      }
    }
    VimPlugin.getRegister().storeTextSpecial(LAST_INSERTED_TEXT_REGISTER, textToPutRegister.toString());
  }

  private int oldOffset = -1;

  private class InsertActionsDocumentListener implements DocumentListener {
    @Override
    public void documentChanged(@NotNull DocumentEvent e) {
      final String newFragment = e.getNewFragment().toString();
      final String oldFragment = e.getOldFragment().toString();
      final int newFragmentLength = newFragment.length();
      final int oldFragmentLength = oldFragment.length();

      // Repeat buffer limits
      if (repeatCharsCount > MAX_REPEAT_CHARS_COUNT) {
        return;
      }

      // <Enter> is added to strokes as an action during processing in order to indent code properly in the repeat
      // command
      if (newFragment.startsWith("\n") && newFragment.trim().isEmpty()) {
        strokes.addAll(getAdjustCaretActions(e));
        oldOffset = -1;
        return;
      }

      // Ignore multi-character indents as they should be inserted automatically while repeating <Enter> actions
      if (!tabAction && newFragmentLength > 1 && newFragment.trim().isEmpty()) {
        return;
      }
      tabAction = false;

      strokes.addAll(getAdjustCaretActions(e));

      if (oldFragmentLength > 0) {
        final AnAction editorDelete = ActionManager.getInstance().getAction("EditorDelete");
        for (int i = 0; i < oldFragmentLength; i++) {
          strokes.add(editorDelete);
        }
      }

      if (newFragmentLength > 0) {
        strokes.add(newFragment.toCharArray());
      }
      repeatCharsCount += newFragmentLength;
      oldOffset = e.getOffset() + newFragmentLength;
    }

    private @NotNull List<EditorActionHandlerBase> getAdjustCaretActions(@NotNull DocumentEvent e) {
      final int delta = e.getOffset() - oldOffset;
      if (oldOffset >= 0 && delta != 0) {
        final List<EditorActionHandlerBase> positionCaretActions = new ArrayList<>();
        final String motionName = delta < 0 ? "VimMotionLeftAction" : "VimMotionRightAction";
        final EditorActionHandlerBase action = RegisterActions.findAction(motionName);
        final int count = Math.abs(delta);
        for (int i = 0; i < count; i++) {
          positionCaretActions.add(action);
        }
        return positionCaretActions;
      }
      return Collections.emptyList();
    }
  }

  private static final Logger logger = Logger.getInstance(ChangeGroup.class.getName());
}