/* * 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()); }