mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-05-21 16:34:05 +02:00
2299 lines
96 KiB
Java
2299 lines
96 KiB
Java
/*
|
|
* 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());
|
|
}
|