diff --git a/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt b/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt index 82c8733d0a..dc6d5465da 100644 --- a/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt +++ b/src/main/java/com/maddyhome/idea/vim/action/change/OperatorAction.kt @@ -37,7 +37,7 @@ import com.maddyhome.idea.vim.vimscript.model.expressions.FunctionCallExpression import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression // todo make it multicaret -private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: SelectionType): Boolean { +private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, motionType: SelectionType): Boolean { val func = injector.globalOptions().operatorfunc if (func.isEmpty()) { VimPlugin.showMessage(MessageHelper.message("E774")) @@ -57,9 +57,9 @@ private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textR if (value is VimFuncref) { handler = value.handler } - } catch (ex: ExException) { + } catch (_: ExException) { // Get the argument for function('...') or funcref('...') for the error message - val functionName = if (expression is FunctionCallExpression && expression.arguments.size > 0) { + val functionName = if (expression is FunctionCallExpression && expression.arguments.isNotEmpty()) { expression.arguments[0].evaluate(editor, context, scriptContext).toString() } else { @@ -77,7 +77,7 @@ private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textR return false } - val arg = when (selectionType) { + val arg = when (motionType) { SelectionType.LINE_WISE -> "line" SelectionType.CHARACTER_WISE -> "char" SelectionType.BLOCK_WISE -> "block" @@ -101,19 +101,13 @@ internal class OperatorAction : VimActionHandler.SingleExecution() { override val argumentType: Argument.Type = Argument.Type.MOTION override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { - val argument = cmd.argument ?: return false + val argument = cmd.argument as? Argument.Motion ?: return false if (!editor.inRepeatMode) { argumentCaptured = argument } val range = getMotionRange(editor, context, argument, operatorArguments) - if (range != null) { - val selectionType = if (argument.motion.isLinewiseMotion()) { - SelectionType.LINE_WISE - } else { - SelectionType.CHARACTER_WISE - } - return doOperatorAction(editor, context, range, selectionType) + return doOperatorAction(editor, context, range, argument.getMotionType()) } return false } @@ -121,7 +115,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() { private fun getMotionRange( editor: VimEditor, context: ExecutionContext, - argument: Argument, + argument: Argument.Motion, operatorArguments: OperatorArguments, ): TextRange? { // Note that we're using getMotionRange2 in order to avoid normalising the linewise range into line start @@ -136,7 +130,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() { operatorArguments, )?.normalize()?.let { // If we're linewise, make sure the end offset isn't just the EOL char - if (argument.motion.isLinewiseMotion() && it.endOffset < editor.fileSize()) { + if (argument.getMotionType() == SelectionType.LINE_WISE && it.endOffset < editor.fileSize()) { TextRange(it.startOffset, it.endOffset + 1) } else { it diff --git a/src/main/java/com/maddyhome/idea/vim/action/change/RepeatChangeAction.kt b/src/main/java/com/maddyhome/idea/vim/action/change/RepeatChangeAction.kt index 39c8013ac6..8beb554206 100644 --- a/src/main/java/com/maddyhome/idea/vim/action/change/RepeatChangeAction.kt +++ b/src/main/java/com/maddyhome/idea/vim/action/change/RepeatChangeAction.kt @@ -25,7 +25,7 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { val state = injector.vimState - val lastCommand = VimRepeater.lastChangeCommand + var lastCommand = VimRepeater.lastChangeCommand if (lastCommand == null && Extension.lastExtensionHandler == null) return false @@ -57,12 +57,7 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() { ) } else if (!repeatHandler && lastCommand != null) { if (cmd.rawCount > 0) { - lastCommand.rawCount = cmd.count - val arg = lastCommand.argument - if (arg != null) { - val mot = arg.motion - mot.rawCount = 0 - } + lastCommand = lastCommand.copy(rawCount = cmd.rawCount) } state.executingCommand = lastCommand diff --git a/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesAction.kt b/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesAction.kt index deafa33b90..cf5ce68fe4 100644 --- a/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesAction.kt +++ b/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesAction.kt @@ -40,7 +40,7 @@ class DeleteJoinLinesAction : ChangeEditorActionHandler.ConditionalSingleExecuti ): Boolean { injector.editorGroup.notifyIdeaJoin(editor) - return injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, false, operatorArguments) + return injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, false) } override fun execute( diff --git a/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesSpacesAction.kt b/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesSpacesAction.kt index b810331c80..75a967c3a6 100644 --- a/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesSpacesAction.kt +++ b/src/main/java/com/maddyhome/idea/vim/action/change/delete/DeleteJoinLinesSpacesAction.kt @@ -35,7 +35,7 @@ class DeleteJoinLinesSpacesAction : ChangeEditorActionHandler.SingleExecution() injector.editorGroup.notifyIdeaJoin(editor) var res = true editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret -> - if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true, operatorArguments)) { + if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true)) { res = false } } diff --git a/src/main/java/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java b/src/main/java/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java index e22a26b176..ebf26f6578 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java +++ b/src/main/java/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java @@ -33,7 +33,6 @@ import java.util.ArrayDeque; import java.util.Deque; -import java.util.EnumSet; import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping; import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing; @@ -64,8 +63,8 @@ public void init() { */ private static class BracketPairs { // NOTE: brackets must match by the position, and ordered by rank (highest to lowest). - @NotNull private final String openBrackets; - @NotNull private final String closeBrackets; + private final @NotNull String openBrackets; + private final @NotNull String closeBrackets; static class ParseException extends Exception { public ParseException(@NotNull String message) { @@ -87,8 +86,7 @@ private enum ParseState { * @param bracketPairs comma-separated list of colon-separated bracket pairs. * @throws ParseException if a syntax error is detected. */ - @NotNull - static BracketPairs fromBracketPairList(@NotNull final String bracketPairs) throws ParseException { + static @NotNull BracketPairs fromBracketPairList(final @NotNull String bracketPairs) throws ParseException { StringBuilder openBrackets = new StringBuilder(); StringBuilder closeBrackets = new StringBuilder(); ParseState state = ParseState.OPEN; @@ -128,7 +126,7 @@ static BracketPairs fromBracketPairList(@NotNull final String bracketPairs) thro return new BracketPairs(openBrackets.toString(), closeBrackets.toString()); } - BracketPairs(@NotNull final String openBrackets, @NotNull final String closeBrackets) { + BracketPairs(final @NotNull String openBrackets, final @NotNull String closeBrackets) { assert openBrackets.length() == closeBrackets.length(); this.openBrackets = openBrackets; this.closeBrackets = closeBrackets; @@ -158,10 +156,9 @@ boolean isOpenBracket(final int ch) { } } - public static final BracketPairs DEFAULT_BRACKET_PAIRS = new BracketPairs("(", ")"); + private static final BracketPairs DEFAULT_BRACKET_PAIRS = new BracketPairs("(", ")"); - @Nullable - private static String bracketPairsVariable() { + private static @Nullable String bracketPairsVariable() { final Object value = VimPlugin.getVariableService().getGlobalVariableValue("argtextobj_pairs"); if (value instanceof VimString vimValue) { return vimValue.getValue(); @@ -192,13 +189,12 @@ static class ArgumentTextObjectHandler extends TextObjectActionHandler { this.isInner = isInner; } - @Nullable @Override - public TextRange getRange(@NotNull VimEditor editor, - @NotNull ImmutableVimCaret caret, - @NotNull ExecutionContext context, - int count, - int rawCount) { + public @Nullable TextRange getRange(@NotNull VimEditor editor, + @NotNull ImmutableVimCaret caret, + @NotNull ExecutionContext context, + int count, + int rawCount) { BracketPairs bracketPairs = DEFAULT_BRACKET_PAIRS; final String bracketPairsVar = bracketPairsVariable(); if (bracketPairsVar != null) { @@ -236,24 +232,22 @@ public TextRange getRange(@NotNull VimEditor editor, return new TextRange(finder.getLeftBound(), finder.getRightBound()); } - @NotNull @Override - public TextObjectVisualType getVisualType() { + public @NotNull TextObjectVisualType getVisualType() { return TextObjectVisualType.CHARACTER_WISE; } } @Override public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) { - @NotNull KeyHandler keyHandler = KeyHandler.getInstance(); @NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState(); - int count = Math.max(1, keyHandlerState.getCommandBuilder().getCount()); final ArgumentTextObjectHandler textObjectHandler = new ArgumentTextObjectHandler(isInner); //noinspection DuplicatedCode - if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) { + if (!(editor.getMode() instanceof Mode.OP_PENDING)) { + int count0 = operatorArguments.getCount0(); editor.nativeCarets().forEach((VimCaret caret) -> { - final TextRange range = textObjectHandler.getRange(editor, caret, context, count, 0); + final TextRange range = textObjectHandler.getRange(editor, caret, context, Math.max(1, count0), count0); if (range != null) { try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { if (editor.getMode() instanceof Mode.VISUAL) { @@ -265,8 +259,7 @@ public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context } }); } else { - keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count, - textObjectHandler, Command.Type.MOTION, EnumSet.noneOf(CommandFlags.class)))); + keyHandlerState.getCommandBuilder().addAction(textObjectHandler); } } } @@ -276,9 +269,9 @@ public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context * position */ private static class ArgBoundsFinder { - @NotNull private final CharSequence text; - @NotNull private final Document document; - @NotNull private final BracketPairs brackets; + private final @NotNull CharSequence text; + private final @NotNull Document document; + private final @NotNull BracketPairs brackets; private int leftBound = Integer.MAX_VALUE; private int rightBound = Integer.MIN_VALUE; private int leftBracket; @@ -305,7 +298,7 @@ private static class ArgBoundsFinder { * @param position starting position. */ boolean findBoundsAt(int position) throws IllegalStateException { - if (text.length() == 0) { + if (text.isEmpty()) { error = "empty document"; return false; } diff --git a/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt b/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt index 5f74fc9b11..29f268a0f4 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/commentary/CommentaryExtension.kt @@ -25,9 +25,6 @@ import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.getLineEndOffset import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.injector -import com.maddyhome.idea.vim.command.Argument -import com.maddyhome.idea.vim.command.Command -import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.TextObjectVisualType @@ -52,7 +49,6 @@ import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType -import java.util.* internal class CommentaryExtension : VimExtension { @@ -184,10 +180,8 @@ internal class CommentaryExtension : VimExtension { override val isRepeatable = true override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { - val command = Command(operatorArguments.count1, CommentaryTextObjectMotionHandler, Command.Type.MOTION, EnumSet.noneOf(CommandFlags::class.java)) - val keyState = KeyHandler.getInstance().keyHandlerState - keyState.commandBuilder.completeCommandPart(Argument(command)) + keyState.commandBuilder.addAction(CommentaryTextObjectMotionHandler) } } diff --git a/src/main/java/com/maddyhome/idea/vim/extension/matchit/Matchit.kt b/src/main/java/com/maddyhome/idea/vim/extension/matchit/Matchit.kt index 2fe06c9ed7..b3f7898f72 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/matchit/Matchit.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/matchit/Matchit.kt @@ -44,6 +44,7 @@ import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim +import com.maddyhome.idea.vim.state.mode.Mode import java.util.* import java.util.regex.Pattern @@ -93,34 +94,29 @@ internal class Matchit : VimExtension { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { val keyHandler = KeyHandler.getInstance() val keyState = keyHandler.keyHandlerState - val count = keyState.commandBuilder.count // Reset the command count so it doesn't transfer onto subsequent commands. keyState.commandBuilder.resetCount() // Normally we want to jump to the start of the matching pair. But when moving forward in operator // pending mode, we want to include the entire match. isInOpPending makes that distinction. - val isInOpPending = keyHandler.isOperatorPending(editor.mode, keyState) - - if (isInOpPending) { + if (editor.mode is Mode.OP_PENDING) { val matchitAction = MatchitAction() matchitAction.reverse = reverse matchitAction.isInOpPending = true - keyState.commandBuilder.completeCommandPart( - Argument( - Command( - count, - matchitAction, - Command.Type.MOTION, - EnumSet.noneOf(CommandFlags::class.java), - ), - ), - ) + keyState.commandBuilder.addAction(matchitAction) } else { editor.sortedCarets().forEach { caret -> injector.jumpService.saveJumpLocation(editor) - caret.moveToOffset(getMatchitOffset(editor.ij, caret.ij, count, isInOpPending, reverse)) + caret.moveToOffset( + getMatchitOffset( + editor.ij, + caret.ij, + operatorArguments.count0, + isInOpPending = false, + reverse + )) } } } @@ -354,7 +350,7 @@ private object FileTypePatterns { private val DEFAULT_PAIRS = setOf('(', ')', '[', ']', '{', '}') -private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPending: Boolean, reverse: Boolean): Int { +private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPending: Boolean, reverse: Boolean): Int { val virtualFile = EditorHelper.getVirtualFile(editor) var caretOffset = caret.offset @@ -367,9 +363,9 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPen val currentChar = editor.document.charsSequence[caretOffset] var motionOffset: Int? = null - if (count > 0) { + if (count0 > 0) { // Matchit doesn't affect the percent motion, so we fall back to the default behavior. - motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count) + motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count0) } else { // Check the simplest case first. if (DEFAULT_PAIRS.contains(currentChar)) { @@ -400,8 +396,7 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPen private fun getMotionOffset(motion: Motion): Int? { return when (motion) { - is Motion.AbsoluteOffset -> motion.offset - is Motion.AdjustedOffset -> motion.offset + is Motion.AdjustedOffset, is Motion.AbsoluteOffset -> motion.offset is Motion.Error, is Motion.NoMotion -> null } } diff --git a/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt b/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt index a1e724308d..e29efbcea1 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt @@ -555,12 +555,13 @@ private fun registerCommand(default: String, action: NerdAction) { } -private val actionsRoot: RootNode = RootNode() +private val actionsRoot: RootNode = RootNode("NERDTree") private var currentNode: CommandPartNode = actionsRoot + private fun collectShortcuts(node: Node): Set { return if (node is CommandPartNode) { - val res = node.keys.toMutableSet() - res += node.values.map { collectShortcuts(it) }.flatten() + val res = node.children.keys.toMutableSet() + res += node.children.values.map { collectShortcuts(it) }.flatten() res } else { emptySet() diff --git a/src/main/java/com/maddyhome/idea/vim/extension/replacewithregister/ReplaceWithRegister.kt b/src/main/java/com/maddyhome/idea/vim/extension/replacewithregister/ReplaceWithRegister.kt index 9d8104e4b7..031408e6c6 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/replacewithregister/ReplaceWithRegister.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/replacewithregister/ReplaceWithRegister.kt @@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.extension.replacewithregister import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor -import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ImmutableVimCaret @@ -166,18 +165,12 @@ private fun doReplace(editor: Editor, context: DataContext, caret: ImmutableVimC putToLine = -1, ) val vimEditor = editor.vim - val keyHandler = KeyHandler.getInstance() ClipboardOptionHelper.IdeaputDisabler().use { VimPlugin.getPut().putText( vimEditor, context.vim, putData, - operatorArguments = OperatorArguments( - keyHandler.isOperatorPending(vimEditor.mode, keyHandler.keyHandlerState), - 0, - editor.vim.mode, - ), saveToRegister = false ) } -} \ No newline at end of file +} diff --git a/src/main/java/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java b/src/main/java/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java index fea50a6f4a..26ca4d0af5 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java +++ b/src/main/java/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java @@ -29,14 +29,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.EnumSet; - import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping; import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing; /** * Port of vim-entire: - * https://github.com/kana/vim-textobj-entire + * vim-textobj-entire * *

* vim-textobj-entire provides two text objects: @@ -51,7 +49,7 @@ * * * See also the reference manual for more details at: - * https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt + * text-obj-entire.txt * * @author Alexandre Grison (@agrison) */ @@ -94,13 +92,12 @@ static class EntireTextObjectHandler extends TextObjectActionHandler { this.ignoreLeadingAndTrailing = ignoreLeadingAndTrailing; } - @Nullable @Override - public TextRange getRange(@NotNull VimEditor editor, - @NotNull ImmutableVimCaret caret, - @NotNull ExecutionContext context, - int count, - int rawCount) { + public @Nullable TextRange getRange(@NotNull VimEditor editor, + @NotNull ImmutableVimCaret caret, + @NotNull ExecutionContext context, + int count, + int rawCount) { int start = 0, end = ((IjVimEditor)editor).getEditor().getDocument().getTextLength(); // for the `ie` text object we don't want leading an trailing spaces @@ -125,24 +122,22 @@ public TextRange getRange(@NotNull VimEditor editor, return new TextRange(start, end); } - @NotNull @Override - public TextObjectVisualType getVisualType() { + public @NotNull TextObjectVisualType getVisualType() { return TextObjectVisualType.CHARACTER_WISE; } } @Override public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) { - @NotNull KeyHandler keyHandler = KeyHandler.getInstance(); @NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState(); - int count = Math.max(1, keyHandlerState.getCommandBuilder().getCount()); final EntireTextObjectHandler textObjectHandler = new EntireTextObjectHandler(ignoreLeadingAndTrailing); //noinspection DuplicatedCode - if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) { + if (!(editor.getMode() instanceof Mode.OP_PENDING)) { + int count0 = operatorArguments.getCount0(); ((IjVimEditor) editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> { - final TextRange range = textObjectHandler.getRange(editor, new IjVimCaret(caret), context, count, 0); + final TextRange range = textObjectHandler.getRange(editor, new IjVimCaret(caret), context, Math.max(1, count0), count0); if (range != null) { try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { if (editor.getMode() instanceof Mode.VISUAL) { @@ -155,9 +150,7 @@ public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context }); } else { - keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count, - textObjectHandler, Command.Type.MOTION, - EnumSet.noneOf(CommandFlags.class)))); + keyHandlerState.getCommandBuilder().addAction(textObjectHandler); } } } diff --git a/src/main/java/com/maddyhome/idea/vim/extension/textobjindent/VimIndentObject.java b/src/main/java/com/maddyhome/idea/vim/extension/textobjindent/VimIndentObject.java index bbbda1210e..269e920526 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/textobjindent/VimIndentObject.java +++ b/src/main/java/com/maddyhome/idea/vim/extension/textobjindent/VimIndentObject.java @@ -30,14 +30,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.EnumSet; - import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping; import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping; /** * Port of vim-indent-object: - * https://github.com/michaeljsmith/vim-indent-object + * vim-indent-object * *

* vim-indent-object provides these text objects based on the cursor line's indentation: @@ -49,7 +47,7 @@ * * * See also the reference manual for more details at: - * https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt + * indent-object.txt * * @author Shrikant Kandula (@sharat87) */ @@ -98,13 +96,12 @@ static class IndentObjectHandler extends TextObjectActionHandler { this.includeBelow = includeBelow; } - @Nullable @Override - public TextRange getRange(@NotNull VimEditor editor, - @NotNull ImmutableVimCaret caret, - @NotNull ExecutionContext context, - int count, - int rawCount) { + public @Nullable TextRange getRange(@NotNull VimEditor editor, + @NotNull ImmutableVimCaret caret, + @NotNull ExecutionContext context, + int count, + int rawCount) { final CharSequence charSequence = ((IjVimEditor)editor).getEditor().getDocument().getCharsSequence(); final int caretOffset = ((IjVimCaret)caret).getCaret().getOffset(); @@ -249,9 +246,8 @@ public TextRange getRange(@NotNull VimEditor editor, return new TextRange(upperBoundaryOffset, lowerBoundaryOffset); } - @NotNull @Override - public TextObjectVisualType getVisualType() { + public @NotNull TextObjectVisualType getVisualType() { return TextObjectVisualType.LINE_WISE; } @@ -264,15 +260,14 @@ private boolean isIndentChar(char ch) { @Override public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) { IjVimEditor vimEditor = (IjVimEditor)editor; - @NotNull KeyHandler keyHandler = KeyHandler.getInstance(); @NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState(); - int count = Math.max(1, keyHandlerState.getCommandBuilder().getCount()); final IndentObjectHandler textObjectHandler = new IndentObjectHandler(includeAbove, includeBelow); - if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) { + if (!(editor.getMode() instanceof Mode.OP_PENDING)) { + int count0 = operatorArguments.getCount0(); ((IjVimEditor)editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> { - final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, count, 0); + final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, Math.max(1, count0), count0); if (range != null) { try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { if (editor.getMode() instanceof Mode.VISUAL) { @@ -285,9 +280,7 @@ public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context }); } else { - keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count, - textObjectHandler, Command.Type.MOTION, - EnumSet.noneOf(CommandFlags.class)))); + keyHandlerState.getCommandBuilder().addAction(textObjectHandler); } } } diff --git a/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt index b692c71be6..6ddf9890cb 100755 --- a/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt @@ -35,7 +35,7 @@ import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.TextRange -import com.maddyhome.idea.vim.ex.ExOutputModel +import com.maddyhome.idea.vim.handler.ExternalActionHandler import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset import com.maddyhome.idea.vim.handler.MotionActionHandler @@ -193,21 +193,16 @@ internal class MotionGroup : VimMotionGroupBase() { argument: Argument, operatorArguments: OperatorArguments, ): TextRange? { + if (argument !is Argument.Motion) { + throw RuntimeException("Unexpected argument passed to getMotionRange2: $argument") + } + var start: Int var end: Int - if (argument.type === Argument.Type.OFFSETS) { - val offsets = argument.offsets[caret.vim] ?: return null - val (first, second) = offsets.getNativeStartAndEnd() - start = first - end = second - } else { - val cmd = argument.motion - // Normalize the counts between the command and the motion argument - val cnt = cmd.count * operatorArguments.count1 - val raw = if (operatorArguments.count0 == 0 && cmd.rawCount == 0) 0 else cnt - if (cmd.action is MotionActionHandler) { - val action = cmd.action as MotionActionHandler + val action = argument.motion + when (action) { + is MotionActionHandler -> { // This is where we are now start = caret.offset @@ -216,8 +211,8 @@ internal class MotionGroup : VimMotionGroupBase() { editor.vim, caret.vim, IjEditorExecutionContext(context!!), - cmd.argument, - operatorArguments.withCount0(raw), + argument.argument, + operatorArguments ) // Invalid motion @@ -233,22 +228,32 @@ internal class MotionGroup : VimMotionGroupBase() { end++ } } - } else if (cmd.action is TextObjectActionHandler) { - val action = cmd.action as TextObjectActionHandler - val range = - action.getRange(editor.vim, caret.vim, IjEditorExecutionContext(context!!), cnt, raw) ?: return null + } + + is TextObjectActionHandler -> { + val range = action.getRange( + editor.vim, + caret.vim, + IjEditorExecutionContext(context!!), + operatorArguments.count1, + operatorArguments.count0 + ) ?: return null start = range.startOffset end = range.endOffset - if (cmd.isLinewiseMotion()) end-- - } else { - throw RuntimeException( - "Commands doesn't take " + cmd.action.javaClass.simpleName + " as an operator", - ) + if (argument.isLinewiseMotion()) end-- } + + is ExternalActionHandler -> { + val range = action.getRange(caret.vim) ?: return null + start = range.startOffset + end = range.endOffset + } + + else -> throw RuntimeException("Commands doesn't take " + action.javaClass.simpleName + " as an operator") } // This is a kludge for dw, dW, and d[w. Without this kludge, an extra newline is operated when it shouldn't be. - val id = argument.motion.action.id + val id = argument.motion.id if (id == VimChangeGroupBase.VIM_MOTION_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_BIG_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_CAMEL_RIGHT) { val text = editor.document.charsSequence.subSequence(start, end).toString() val lastNewLine = text.lastIndexOf('\n') @@ -258,6 +263,7 @@ internal class MotionGroup : VimMotionGroupBase() { } } } + return TextRange(start, end) } diff --git a/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt b/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt index 514b66562d..89123daeea 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt +++ b/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt @@ -16,7 +16,6 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.getLineEndForOffset import com.maddyhome.idea.vim.api.getLineStartForOffset -import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext import com.maddyhome.idea.vim.newapi.IjVimCaret @@ -94,6 +93,6 @@ internal fun VimEditor.exitSelectMode(adjustCaretPosition: Boolean) { } } -internal fun Editor.exitInsertMode(context: DataContext, operatorArguments: OperatorArguments) { - VimPlugin.getChange().processEscape(IjVimEditor(this), IjEditorExecutionContext(context), operatorArguments) +internal fun Editor.exitInsertMode(context: DataContext) { + VimPlugin.getChange().processEscape(IjVimEditor(this), IjEditorExecutionContext(context)) } diff --git a/src/main/java/com/maddyhome/idea/vim/listener/IJEditorFocusListener.kt b/src/main/java/com/maddyhome/idea/vim/listener/IJEditorFocusListener.kt index fdf06ebb4c..8f002bf4bb 100644 --- a/src/main/java/com/maddyhome/idea/vim/listener/IJEditorFocusListener.kt +++ b/src/main/java/com/maddyhome/idea/vim/listener/IJEditorFocusListener.kt @@ -17,7 +17,6 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector -import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.EditorListener import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.inInsertMode @@ -70,7 +69,7 @@ class IJEditorFocusListener : EditorListener { val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor) val mode = injector.vimState.mode when (mode) { - is Mode.INSERT -> editor.exitInsertMode(context, OperatorArguments(false, 0, mode)) + is Mode.INSERT -> editor.exitInsertMode(context) else -> {} } } @@ -91,4 +90,4 @@ class IJEditorFocusListener : EditorListener { ijEditor.document.isWritable && ijEditor.editorKind != EditorKind.DIFF } -} \ No newline at end of file +} diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt index f1c8420435..b6ee77478a 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt @@ -38,9 +38,7 @@ import com.maddyhome.idea.vim.api.VimSelectionModel import com.maddyhome.idea.vim.api.VimVisualPosition import com.maddyhome.idea.vim.api.VirtualFile import com.maddyhome.idea.vim.api.injector -import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.IndentConfig -import com.maddyhome.idea.vim.common.IndentConfig.Companion.create import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.ModeChangeListener import com.maddyhome.idea.vim.common.TextRange @@ -99,7 +97,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase( editor.vimChangeActionSwitchMode = value } override val indentConfig: VimIndentConfig - get() = create(editor) + get() = IndentConfig.create(editor) override fun fileSize(): Long = editor.fileSize.toLong() @@ -389,8 +387,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase( return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)) } - override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) { - editor.exitInsertMode(context.ij, operatorArguments) + override fun exitInsertMode(context: ExecutionContext) { + editor.exitInsertMode(context.ij) } override fun exitSelectModeNative(adjustCaret: Boolean) { @@ -539,4 +537,4 @@ internal class InsertTimeRecorder: ModeChangeListener { editor.forEachCaret { undo.endInsertSequence(it, it.offset, nanoTime) } } } -} \ No newline at end of file +} diff --git a/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java b/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java index 856c861f4d..7447c5e217 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java +++ b/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanel.java @@ -341,12 +341,12 @@ protected void textChanged(@NotNull DocumentEvent e) { } } - // Get the current count from the command builder. This value is coerced to at least 1, so will always be valid. - // The aggregated value includes any counts for operator and register selections, and the uncommitted count for - // the search command (`/` or `?`). E.g., `2"a3"b4"c5d6/` would return 720. - // If we're showing highlights for an ex command like `:s`, there won't be a command, but the value is already - // coerced to at least 1. - int count1 = KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getAggregatedUncommittedCount(); + // Get a snapshot of the count for the in progress command, and coerce it to 1. This value will include all + // count components - selecting register(s), operator and motions. E.g. `2"a3"b4"c5d6/` will return 720. + // If we're showing highlights for an Ex command like `:s`, the command builder will be empty, but we'll still + // get a valid value. + int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder() + .calculateCount0Snapshot()); if (labelText.equals("/") || labelText.equals("?") || searchCommand) { final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards @@ -528,9 +528,8 @@ public void componentResized(ComponentEvent e) { private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName()); - @NotNull @Override - public VimCommandLineCaret getCaret() { + public @NotNull VimCommandLineCaret getCaret() { return (VimCommandLineCaret) entry.getCaret(); } @@ -548,9 +547,8 @@ public void clearCurrentAction() { entry.clearCurrentAction(); } - @Nullable @Override - public Integer getPromptCharacterOffset() { + public @Nullable Integer getPromptCharacterOffset() { int offset = entry.currentActionPromptCharacterOffset; return offset == -1 ? null : offset; } @@ -570,8 +568,7 @@ public void focus() { IdeFocusManager.findInstance().requestFocus(entry, true); } - @Nullable - public VimInputInterceptor getInputInterceptor() { + public @Nullable VimInputInterceptor getInputInterceptor() { return myInputInterceptor; } @@ -584,15 +581,13 @@ public void setInputInterceptor(@Nullable VimInputInterceptor vimInputInterce myInputInterceptor = vimInputInterceptor; } - @Nullable @Override - public Function1 getInputProcessing() { + public @Nullable Function1 getInputProcessing() { return inputProcessing; } - @Nullable @Override - public Character getFinishOn() { + public @Nullable Character getFinishOn() { return finishOn; } diff --git a/src/main/resources/dictionaries/ideavim.dic b/src/main/resources/dictionaries/ideavim.dic index b5a8cfa502..8c72103b45 100644 --- a/src/main/resources/dictionaries/ideavim.dic +++ b/src/main/resources/dictionaries/ideavim.dic @@ -44,6 +44,7 @@ viminfo virtualedit visualbell visualdelay +whichwrap wrapscan nobomb diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/CopyActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/CopyActionTest.kt index 6622e6a80a..30b8f16b87 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/CopyActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/CopyActionTest.kt @@ -109,7 +109,7 @@ class CopyActionTest : VimTestCase() { @TestWithoutNeovim(reason = SkipNeovimReason.DIFFERENT) @Test fun testYankRegisterUsesLastEnteredRegister() { - typeTextInFile("\"a\"byl" + "\"ap", "hello world\n") + typeTextInFile("\"a\"byl" + "\"bp", "hello world\n") assertState("helllo world\n") } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionBackspaceActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionBackspaceActionTest.kt index a2ac883b33..9dbacdb913 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionBackspaceActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionBackspaceActionTest.kt @@ -85,4 +85,33 @@ class MotionBackspaceActionTest : VimTestCase() { enterCommand("set whichwrap=b") } } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @Test + fun `test backspace motion with operator`() { + doTest( + "d", + """ + lorem ${c}ipsum dolor sit amet + """.trimIndent(), + """ + lorem${c}ipsum dolor sit amet + """.trimIndent(), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @Test + fun `test backspace motion with operator at start of line`() { + doTest( + "d", + """ + lorem ipsum dolor sit amet + ${c}lorem ipsum dolor sit amet + """.trimIndent(), + """ + lorem ipsum dolor sit amet${c}lorem ipsum dolor sit amet + """.trimIndent(), + ) + } } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionSpaceActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionSpaceActionTest.kt index b46309e994..b6b9f0860b 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionSpaceActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionSpaceActionTest.kt @@ -85,4 +85,35 @@ class MotionSpaceActionTest : VimTestCase() { enterCommand("set whichwrap=s") } } + + @Suppress("SpellCheckingInspection") + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @Test + fun `test space motion with operator`() { + doTest( + "d", + """ + lorem ${c}ipsum dolor sit amet + """.trimIndent(), + """ + lorem ${c}psum dolor sit amet + """.trimIndent(), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @Test + fun `test space motion with operator at end of line`() { + doTest( + "d", + """ + lorem ipsum dolor sit ame${c}t + lorem ipsum dolor sit amet + """.trimIndent(), + """ + lorem ipsum dolor sit am${c}e + lorem ipsum dolor sit amet + """.trimIndent(), + ) + } } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/services/VimVariableServiceTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/services/VimVariableServiceTest.kt index ab4018082c..540211b63c 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/services/VimVariableServiceTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/services/VimVariableServiceTest.kt @@ -15,7 +15,7 @@ class VimVariableServiceTest : VimTestCase() { @Test fun `test v count variable without count specified`() { configureByText("\n") - enterCommand("nnoremap n ':echo ' .. v:count .. \"\\\"") + enterCommand("""nnoremap n ':echo ' .. v:count .. "\"""") typeText("n") assertExOutput("0") } @@ -23,15 +23,31 @@ class VimVariableServiceTest : VimTestCase() { @Test fun `test v count variable`() { configureByText("\n") - enterCommand("nnoremap n ':' .. \"\\\" .. 'echo ' .. v:count .. \"\\\"") + enterCommand("""nnoremap n ':' .. "\" .. 'echo ' .. v:count .. "\"""") typeText("5n") assertExOutput("5") } + @Test + fun `test v count variable with additional count during select register`() { + configureByText("\n") + enterCommand("""nnoremap n ':' .. "\" .. 'echo ' .. v:count .. "\"""") + typeText("2\"a5n") + assertExOutput("10") + } + + @Test + fun `test v count variable with additional pathological count during select register`() { + configureByText("\n") + enterCommand("""nnoremap n ':' .. "\" .. 'echo ' .. v:count .. "\"""") + typeText("2\"a3\"b4\"c5n") + assertExOutput("120") + } + @Test fun `test v count1 variable without count specified`() { configureByText("\n") - enterCommand("nnoremap n ':echo ' .. v:count1 .. \"\\\"") + enterCommand("""nnoremap n ':echo ' .. v:count1 .. "\"""") typeText("n") assertExOutput("1") } @@ -39,11 +55,27 @@ class VimVariableServiceTest : VimTestCase() { @Test fun `test v count1 variable`() { configureByText("\n") - enterCommand("nnoremap n ':' .. \"\\\" .. 'echo ' .. v:count1 .. \"\\\"") + enterCommand("""nnoremap n ':' .. "\" .. 'echo ' .. v:count1 .. "\"""") typeText("5n") assertExOutput("5") } + @Test + fun `test v count1 variable with additional count during select register`() { + configureByText("\n") + enterCommand("""nnoremap n ':' .. "\" .. 'echo ' .. v:count1 .. "\"""") + typeText("2\"a5n") + assertExOutput("10") + } + + @Test + fun `test v count1 variable with additional pathological count during select register`() { + configureByText("\n") + enterCommand("""nnoremap n ':' .. "\" .. 'echo ' .. v:count1 .. "\"""") + typeText("2\"a3\"b4\"c5n") + assertExOutput("120") + } + @Test fun `test mapping with updating jumplist`() { configureByText("${c}1\n2\n3\n4\n5\n6\n7\n8\n9\n") diff --git a/src/test/java/org/jetbrains/plugins/ideavim/extension/argtextobj/VimArgTextObjExtensionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/extension/argtextobj/VimArgTextObjExtensionTest.kt index 2b3bea1900..5ef87a0e73 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/extension/argtextobj/VimArgTextObjExtensionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/extension/argtextobj/VimArgTextObjExtensionTest.kt @@ -141,6 +141,16 @@ Mode.INSERT, ) } + @Test + fun testDeleteWithMultipleCounts() { + doTest( + "2d2aa", + "function(int arg1, char* arg2=\"a,b,c(d,e)\", bool arg3, string arg4, int arg5)", + "function()", + Mode.NORMAL(), + ) + } + @Test fun testSelectTwoArguments() { doTest( diff --git a/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt b/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt index 68d6c1c920..de661704fd 100644 --- a/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt +++ b/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt @@ -95,7 +95,7 @@ private class AvailableActions(private val editor: Editor) : ImperativeCommand { val currentNode = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentTrie() // Note: esc is always an option - val possibleKeys = (currentNode.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } + val possibleKeys = (currentNode.children.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } println("Keys: ${possibleKeys.joinToString(", ")}") val keyGenerator = Generator.integers(0, possibleKeys.lastIndex) .suchThat { injector.parser.toKeyNotation(possibleKeys[it]) !in stinkyKeysList } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt index 0070b9505c..b1a43aad03 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt @@ -17,14 +17,13 @@ import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingProcessor import com.maddyhome.idea.vim.command.OperatorArguments -import com.maddyhome.idea.vim.common.CurrentCommandState import com.maddyhome.idea.vim.diagnostic.VimLogger import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.impl.state.toMappingMode -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyConsumer import com.maddyhome.idea.vim.key.KeyStack +import com.maddyhome.idea.vim.key.RootNode import com.maddyhome.idea.vim.key.consumers.CharArgumentConsumer import com.maddyhome.idea.vim.key.consumers.CommandConsumer import com.maddyhome.idea.vim.key.consumers.CommandCountConsumer @@ -197,11 +196,9 @@ class KeyHandler { } private fun onUnknownKey(editor: VimEditor, keyState: KeyHandlerState) { - logger.trace("Command builder is set to BAD") - keyState.commandBuilder.commandState = CurrentCommandState.BAD_COMMAND editor.resetOpPending() - injector.vimState.resetRegisterPending() editor.isReplaceCharacter = false + // Note that this will also reset the CommandBuilder to NEW_COMMAND reset(keyState, editor.mode) } @@ -210,14 +207,6 @@ class KeyHandler { injector.messages.indicateError() } - fun isDuplicateOperatorKeyStroke(key: KeyStroke, mode: Mode, keyState: KeyHandlerState): Boolean { - return isOperatorPending(mode, keyState) && keyState.commandBuilder.isDuplicateOperatorKeyStroke(key) - } - - fun isOperatorPending(mode: Mode, keyState: KeyHandlerState): Boolean { - return mode is Mode.OP_PENDING && !keyState.commandBuilder.isEmpty - } - private fun executeCommand( editor: VimEditor, context: ExecutionContext, @@ -226,11 +215,7 @@ class KeyHandler { ) { logger.trace("Command execution") val command = keyState.commandBuilder.buildCommand() - val operatorArguments = OperatorArguments( - editor.mode is Mode.OP_PENDING, - command.rawCount, - editorState.mode, - ) + val operatorArguments = OperatorArguments(command.rawCount, editorState.mode) // If we were in "operator pending" mode, reset back to normal mode. // But opening command line should not reset operator pending mode (e.g. `d/foo` @@ -295,7 +280,7 @@ class KeyHandler { keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode())) } - private fun getKeyRoot(mappingMode: MappingMode): CommandPartNode { + private fun getKeyRoot(mappingMode: MappingMode): RootNode { return injector.keyGroup.getKeyRoot(mappingMode) } @@ -341,7 +326,7 @@ class KeyHandler { ) : Runnable { override fun run() { val editorState = injector.vimState - keyState.commandBuilder.commandState = CurrentCommandState.NEW_COMMAND + val register = cmd.register if (register != null) { injector.registerGroup.selectRegister(register) @@ -361,22 +346,15 @@ class KeyHandler { // mode we were in. This handles commands in those modes that temporarily allow us to execute normal // mode commands. An exception is if this command should leave us in the temporary mode such as // "select register" - val myMode = editorState.mode - val returnTo = myMode.returnTo - if (myMode is Mode.NORMAL && returnTo != null && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) { - when (returnTo) { - ReturnTo.INSERT -> { - editor.mode = Mode.INSERT - } - - ReturnTo.REPLACE -> { - editor.mode = Mode.REPLACE - } + if (editorState.mode is Mode.NORMAL && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) { + when (editorState.mode.returnTo) { + ReturnTo.INSERT -> editor.mode = Mode.INSERT + ReturnTo.REPLACE -> editor.mode = Mode.REPLACE + null -> {} } } - if (keyState.commandBuilder.isDone()) { - getInstance().reset(keyState, editorState.mode) - } + + instance.reset(keyState, editorState.mode) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeCharacterAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeCharacterAction.kt index e1c676ab35..6d9dc2e200 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeCharacterAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeCharacterAction.kt @@ -17,23 +17,17 @@ import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.lineLength import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Command -import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler -import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.state.KeyHandlerState -import java.util.* @CommandOrMotion(keys = ["r"], modes = [Mode.NORMAL]) class ChangeCharacterAction : ChangeEditorActionHandler.ForEachCaret() { override val type: Command.Type = Command.Type.CHANGE - override val argumentType: Argument.Type = Argument.Type.DIGRAPH - override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_ALLOW_DIGRAPH) - override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { editor.isReplaceCharacter = true } @@ -45,7 +39,7 @@ class ChangeCharacterAction : ChangeEditorActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Boolean { - return argument != null && changeCharacter(editor, caret, operatorArguments.count1, argument.character) + return argument is Argument.Character && changeCharacter(editor, caret, operatorArguments.count1, argument.character) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeLineAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeLineAction.kt index 2acd27c22d..3e3b1d1af9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeLineAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeLineAction.kt @@ -37,12 +37,11 @@ class ChangeLineAction : ChangeInInsertSequenceAction() { ): Boolean { // `S` command is a synonym of `cc` val motion = MotionDownLess1FirstNonSpaceAction() - val command = Command(1, motion, motion.type, motion.flags) return injector.changeGroup.changeMotion( editor, caret, context, - Argument(command), + Argument.Motion(motion, null), operatorArguments, ) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualAction.kt index 2006e79dd5..a43a17d7cc 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualAction.kt @@ -39,7 +39,6 @@ class ChangeVisualAction : VisualOperatorActionHandler.ForEachCaret() { range.toVimTextRange(false), range.type, context, - operatorArguments, ) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualCharacterAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualCharacterAction.kt index 89cc893f7f..4e155cfcea 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualCharacterAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualCharacterAction.kt @@ -15,16 +15,13 @@ import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Command -import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler -import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.state.KeyHandlerState -import java.util.* /** * @author vlan @@ -32,11 +29,8 @@ import java.util.* @CommandOrMotion(keys = ["r"], modes = [Mode.VISUAL]) class ChangeVisualCharacterAction : VisualOperatorActionHandler.ForEachCaret() { override val type: Command.Type = Command.Type.CHANGE - override val argumentType: Argument.Type = Argument.Type.DIGRAPH - override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_ALLOW_DIGRAPH) - override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { editor.isReplaceCharacter = true } @@ -50,7 +44,7 @@ class ChangeVisualCharacterAction : VisualOperatorActionHandler.ForEachCaret() { operatorArguments: OperatorArguments, ): Boolean { val argument = cmd.argument - return argument != null && + return argument is Argument.Character && changeCharacterRange(editor, caret, range.toVimTextRange(false), argument.character) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesAction.kt index 5c0b4afa39..8f51329557 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesAction.kt @@ -56,7 +56,6 @@ class ChangeVisualLinesAction : VisualOperatorActionHandler.ForEachCaret() { lineRange, SelectionType.LINE_WISE, context, - operatorArguments, ) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesEndAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesEndAction.kt index 42ccef098f..3e7adbeba5 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesEndAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeVisualLinesEndAction.kt @@ -53,7 +53,7 @@ class ChangeVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() { } } val blockRange = TextRange(starts, ends) - injector.changeGroup.changeRange(editor, caret, blockRange, SelectionType.BLOCK_WISE, context, operatorArguments) + injector.changeGroup.changeRange(editor, caret, blockRange, SelectionType.BLOCK_WISE, context) } else { val lineEndForOffset = editor.getLineEndForOffset(vimTextRange.endOffset) val endsWithNewLine = if (lineEndForOffset.toLong() == editor.fileSize()) 0 else 1 @@ -61,7 +61,7 @@ class ChangeVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() { editor.getLineStartForOffset(vimTextRange.startOffset), lineEndForOffset + endsWithNewLine, ) - injector.changeGroup.changeRange(editor, caret, lineRange, SelectionType.LINE_WISE, context, operatorArguments) + injector.changeGroup.changeRange(editor, caret, lineRange, SelectionType.LINE_WISE, context) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/FilterMotionAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/FilterMotionAction.kt index 7e7c27964d..5e30b8613c 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/FilterMotionAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/FilterMotionAction.kt @@ -31,7 +31,7 @@ class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterComman override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { // Start ex entry with the initial text set to the calculated range and `!` - startFilterCommand(editor, context, cmd) + startFilterCommand(editor, context, cmd.rawCount) return true } } @@ -63,13 +63,13 @@ class FilterMotionAction : VimActionHandler.SingleExecution(), FilterCommand, Du // Start ex entry with the initial text set to the calculated range and `!` val count = if (start.line < end.line) end.line - start.line + 1 else 1 - startFilterCommand(editor, context, Argument.EMPTY_COMMAND.copy(rawCount = count)) + startFilterCommand(editor, context, count) return true } } interface FilterCommand { - fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) { - injector.commandLine.createCommandPrompt(editor, context, cmd, initialText = "!") + fun startFilterCommand(editor: VimEditor, context: ExecutionContext, count0: Int) { + injector.commandLine.createCommandPrompt(editor, context, count0, initialText = "!") } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteMotionAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteMotionAction.kt index ec171da35e..e2fa5d3dd9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteMotionAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteMotionAction.kt @@ -38,6 +38,6 @@ class DeleteMotionAction : ChangeEditorActionHandler.ForEachCaret(), DuplicableO val (range, selectionType) = injector.changeGroup .getDeleteRangeAndType(editor, caret, context, argument, false, operatorArguments) ?: return false - return injector.changeGroup.deleteRange(editor, caret, range, selectionType, false, operatorArguments) + return injector.changeGroup.deleteRange(editor, caret, range, selectionType, false) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualAction.kt index db3c1ad2f3..07acb11ac3 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualAction.kt @@ -40,7 +40,6 @@ class DeleteVisualAction : VisualOperatorActionHandler.ForEachCaret() { range.toVimTextRange(false), selectionType, false, - operatorArguments, ) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesAction.kt index 2ef195eadb..ca2b154b7d 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesAction.kt @@ -56,6 +56,6 @@ class DeleteVisualLinesAction : VisualOperatorActionHandler.ForEachCaret() { Triple(caret, lineRange, SelectionType.LINE_WISE) } } - return injector.changeGroup.deleteRange(editor, usedCaret, usedRange, usedType, false, operatorArguments) + return injector.changeGroup.deleteRange(editor, usedCaret, usedRange, usedType, false) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesEndAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesEndAction.kt index 3650cf6f07..3b9f62d9af 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesEndAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/delete/DeleteVisualLinesEndAction.kt @@ -58,7 +58,6 @@ class DeleteVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() { blockRange, SelectionType.BLOCK_WISE, false, - operatorArguments, ) } else { val lineEndForOffset = editor.getLineEndForOffset(vimTextRange.endOffset) @@ -67,7 +66,7 @@ class DeleteVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() { editor.getLineStartForOffset(vimTextRange.startOffset), lineEndForOffset + endsWithNewLine, ) - injector.changeGroup.deleteRange(editor, caret, lineRange, SelectionType.LINE_WISE, false, operatorArguments) + injector.changeGroup.deleteRange(editor, caret, lineRange, SelectionType.LINE_WISE, false) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedDigraphAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedDigraphAction.kt index 275b010799..40baf8576d 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedDigraphAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedDigraphAction.kt @@ -52,7 +52,8 @@ class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { // The converted digraph character has been captured as an argument, push it back through key handler - val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character) + val argument = cmd.argument as? Argument.Character ?: return false + val keyStroke = KeyStroke.getKeyStroke(argument.character) val keyHandler = KeyHandler.getInstance() keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState) return true diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedLiteralAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedLiteralAction.kt index 57af079a39..882aa6412e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedLiteralAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertCompletedLiteralAction.kt @@ -52,7 +52,8 @@ class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { // The converted literal character has been captured as an argument, push it back through key handler - val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character) + val argument = cmd.argument as? Argument.Character ?: return false + val keyStroke = KeyStroke.getKeyStroke(argument.character) val keyHandler = KeyHandler.getInstance() keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState) return true diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeleteInsertedTextAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeleteInsertedTextAction.kt index 6cd1712fb7..85cd07160e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeleteInsertedTextAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeleteInsertedTextAction.kt @@ -36,7 +36,7 @@ class InsertDeleteInsertedTextAction : ChangeEditorActionHandler.ForEachCaret() argument: Argument?, operatorArguments: OperatorArguments, ): Boolean { - return insertDeleteInsertedText(editor, caret, operatorArguments) + return insertDeleteInsertedText(editor, caret) } } @@ -48,11 +48,7 @@ class InsertDeleteInsertedTextAction : ChangeEditorActionHandler.ForEachCaret() * @param caret The caret on which the action is performed * @return true if able to delete the text, false if not */ -private fun insertDeleteInsertedText( - editor: VimEditor, - caret: VimCaret, - operatorArguments: OperatorArguments, -): Boolean { +private fun insertDeleteInsertedText(editor: VimEditor, caret: VimCaret): Boolean { var deleteTo = caret.vimInsertStart.startOffset val offset = caret.offset if (offset == deleteTo) { @@ -65,7 +61,6 @@ private fun insertDeleteInsertedText( TextRange(deleteTo, offset), SelectionType.CHARACTER_WISE, false, - operatorArguments, ) return true } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeletePreviousWordAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeletePreviousWordAction.kt index 0bfa0a9b3f..8a133ecc05 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeletePreviousWordAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertDeletePreviousWordAction.kt @@ -37,7 +37,7 @@ class InsertDeletePreviousWordAction : ChangeEditorActionHandler.ForEachCaret() argument: Argument?, operatorArguments: OperatorArguments, ): Boolean { - return insertDeletePreviousWord(editor, caret, operatorArguments) + return insertDeletePreviousWord(editor, caret) } } @@ -50,7 +50,7 @@ class InsertDeletePreviousWordAction : ChangeEditorActionHandler.ForEachCaret() * @param editor The editor to delete the text from * @return true if able to delete text, false if not */ -private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret, operatorArguments: OperatorArguments): Boolean { +private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret): Boolean { val deleteTo: Int = if (caret.getBufferPosition().column == 0) { caret.offset - 1 } else { @@ -74,6 +74,6 @@ private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret, operato return false } val range = TextRange(deleteTo, caret.offset) - injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true, operatorArguments) + injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true) return true } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertRegisterAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertRegisterAction.kt index 49f1e7838f..cb31a7b04a 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertRegisterAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertRegisterAction.kt @@ -18,14 +18,10 @@ import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.RWLockLabel -import com.maddyhome.idea.vim.helper.isCloseKeyStroke -import com.maddyhome.idea.vim.key.interceptors.VimInputInterceptorBase import com.maddyhome.idea.vim.put.PutData import com.maddyhome.idea.vim.register.Register import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.vimscript.model.Script -import java.awt.event.KeyEvent -import javax.swing.KeyStroke @CommandOrMotion(keys = [""], modes = [Mode.INSERT]) class InsertRegisterAction : VimActionHandler.SingleExecution() { @@ -39,9 +35,8 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() { cmd: Command, operatorArguments: OperatorArguments, ): Boolean { - val argument = cmd.argument - - if (argument?.character == '=') { + val argument = cmd.argument as? Argument.Character ?: return false + if (argument.character == '=') { injector.commandLine.readInputAndProcess(editor, context, "=", finishOn = null) { input -> try { if (input.isNotEmpty()) { @@ -50,7 +45,7 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() { val textToStore = expression.toInsertableString() injector.registerGroup.storeTextSpecial('=', textToStore) } - insertRegister(editor, context, '=', operatorArguments) + insertRegister(editor, context, '=') } catch (e: ExException) { injector.messages.indicateError() injector.messages.showStatusBarMessage(editor, e.message) @@ -58,7 +53,7 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() { } return true } else { - return argument != null && insertRegister(editor, context, argument.character, operatorArguments) + return insertRegister(editor, context, argument.character) } } } @@ -72,18 +67,13 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() { * @return true if able to insert the register contents, false if not */ @RWLockLabel.SelfSynchronized -private fun insertRegister( - editor: VimEditor, - context: ExecutionContext, - key: Char, - operatorArguments: OperatorArguments, -): Boolean { +private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean { val register: Register? = injector.registerGroup.getRegister(key) if (register != null) { val text = register.rawText ?: injector.parser.toPrintableString(register.keys) val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), register.name) val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true) - injector.put.putText(editor, context, putData, operatorArguments = operatorArguments) + injector.put.putText(editor, context, putData) return true } return false diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockAppendAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockAppendAction.kt index b0427f0768..6be8e29de1 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockAppendAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockAppendAction.kt @@ -36,7 +36,7 @@ class VisualBlockAppendAction : VisualOperatorActionHandler.SingleExecution() { if (editor.isOneLineMode()) return false val range = caretsAndSelections.values.stream().findFirst().orElse(null) ?: return false return if (range.type == SelectionType.BLOCK_WISE) { - injector.changeGroup.blockInsert(editor, context, range.toVimTextRange(false), true, operatorArguments) + injector.changeGroup.initBlockInsert(editor, context, range.toVimTextRange(false), true) } else { injector.changeGroup.insertAfterLineEnd(editor, context) true diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockInsertAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockInsertAction.kt index 84c867e56a..16778bebab 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockInsertAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/VisualBlockInsertAction.kt @@ -36,7 +36,7 @@ class VisualBlockInsertAction : VisualOperatorActionHandler.SingleExecution() { if (editor.isOneLineMode()) return false val vimSelection = caretsAndSelections.values.stream().findFirst().orElse(null) ?: return false return if (vimSelection.type == SelectionType.BLOCK_WISE) { - injector.changeGroup.blockInsert(editor, context, vimSelection.toVimTextRange(false), false, operatorArguments) + injector.changeGroup.initBlockInsert(editor, context, vimSelection.toVimTextRange(false), false) } else { injector.changeGroup.insertBeforeFirstNonBlank(editor, context) true diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/copy/PutTextAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/copy/PutTextAction.kt index 1589af5520..a4765ddc8a 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/copy/PutTextAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/copy/PutTextAction.kt @@ -46,7 +46,7 @@ sealed class PutTextBaseAction( result } else { val putData = getPutDataForCaret(sortedCarets.single(), count) - injector.put.putText(editor, context, putData, operatorArguments) + injector.put.putText(editor, context, putData) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ExEntryAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ExEntryAction.kt index f0513ebd37..5a7ef42c93 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ExEntryAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ExEntryAction.kt @@ -26,7 +26,7 @@ class ExEntryAction : VimActionHandler.SingleExecution() { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { if (editor.isOneLineMode()) return false - injector.commandLine.createCommandPrompt(editor, context, cmd, initialText = "") + injector.commandLine.createCommandPrompt(editor, context, cmd.rawCount, initialText = "") return true } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/InsertRegisterAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/InsertRegisterAction.kt index de3b0d04f3..8ec16eb523 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/InsertRegisterAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/InsertRegisterAction.kt @@ -38,7 +38,8 @@ class InsertRegisterAction: VimActionHandler.SingleExecution() { val caretOffset = cmdLine.caret.offset - val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character) + val argument = cmd.argument as? Argument.Character ?: return false + val keyStroke = KeyStroke.getKeyStroke(argument.character) val pasteContent = if ((keyStroke.modifiers and KeyEvent.CTRL_DOWN_MASK) == 0) { injector.registerGroup.getRegister(keyStroke.keyChar)?.text } else { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/LeaveCommandLineAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/LeaveCommandLineAction.kt index 491e7912bd..a734410a51 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/LeaveCommandLineAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/LeaveCommandLineAction.kt @@ -13,6 +13,7 @@ import com.intellij.vim.annotations.Mode import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector +import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.OperatorArguments @@ -27,9 +28,9 @@ class LeaveCommandLineAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { - val argument = cmd.argument ?: return true - val historyType = VimHistory.Type.getTypeByLabel(argument.character.toString()) + val argument = cmd.argument as? Argument.ExString ?: return true + val historyType = VimHistory.Type.getTypeByLabel(argument.label.toString()) injector.historyGroup.addEntry(historyType, argument.string) return true } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt index dadd13f784..510909fbb8 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt @@ -34,8 +34,9 @@ class ProcessExEntryAction : MotionActionHandler.AmbiguousExecution() { override var motionType: MotionType = MotionType.EXCLUSIVE override fun getMotionActionHandler(argument: Argument?): MotionActionHandler { - if (argument?.processing != null) return ExecuteDefinedInputProcessingAction() - return if (argument?.character == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction(this) + check(argument is Argument.ExString) + if (argument.processing != null) return ExecuteDefinedInputProcessingAction() + return if (argument.label == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction(this) } } @@ -48,7 +49,7 @@ class ExecuteDefinedInputProcessingAction : MotionActionHandler.SingleExecution( argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.ExString) return Motion.Error val input = argument.string val processing = argument.processing!! @@ -62,11 +63,11 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) : get() = throw RuntimeException("Parent motion type should be used, as only it is accessed by other code") override fun getOffset(editor: VimEditor, caret: ImmutableVimCaret, context: ExecutionContext, argument: Argument?, operatorArguments: OperatorArguments): Motion { - if (argument == null) return Motion.Error - val offsetAndMotion = when (argument.character) { + if (argument !is Argument.ExString) return Motion.Error + val offsetAndMotion = when (argument.label) { '/' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.FORWARDS) '?' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.BACKWARDS) - else -> throw ExException("Unexpected search label ${argument.character}") + else -> throw ExException("Unexpected search label ${argument.label}") } // Vim doesn't treat not finding something as an error, although it might report either an error or warning message if (offsetAndMotion == null) return Motion.NoMotion @@ -79,7 +80,7 @@ class ProcessExCommandEntryAction : MotionActionHandler.SingleExecution() { override val motionType: MotionType = MotionType.LINE_WISE override fun getOffset(editor: VimEditor, context: ExecutionContext, argument: Argument?, operatorArguments: OperatorArguments): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.ExString) return Motion.Error try { // Exit Command-line mode and return to the previous mode before executing the command (this is set to Normal in diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/PlaybackRegisterAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/PlaybackRegisterAction.kt index 278c4a4ab0..4068063b41 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/PlaybackRegisterAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/PlaybackRegisterAction.kt @@ -31,7 +31,7 @@ class PlaybackRegisterAction : VimActionHandler.SingleExecution() { cmd: Command, operatorArguments: OperatorArguments, ): Boolean { - val argument = cmd.argument ?: return false + val argument = cmd.argument as? Argument.Character ?: return false val reg = argument.character val application = injector.application val res = arrayOf(false) @@ -49,7 +49,7 @@ class PlaybackRegisterAction : VimActionHandler.SingleExecution() { if (reg != '@') { // @ is not a register itself, it just tells vim to use the last register injector.macro.lastRegister = reg } - } catch (e: ExException) { + } catch (_: ExException) { res[0] = false } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/ToggleRecordingAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/ToggleRecordingAction.kt index 897c61c544..9c8713b86d 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/ToggleRecordingAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/macro/ToggleRecordingAction.kt @@ -26,7 +26,7 @@ class ToggleRecordingAction : VimActionHandler.SingleExecution() { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { return if (!injector.registerGroup.isRecording) { - val argument = cmd.argument ?: return false + val argument = cmd.argument as? Argument.Character ?: return false val reg = argument.character injector.registerGroup.startRecording(reg) } else { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowLeftAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowLeftAction.kt index 2b3ee26d4d..d56dc7c8de 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowLeftAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowLeftAction.kt @@ -19,10 +19,21 @@ import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.Motion +import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.NonShiftedSpecialKeyHandler -@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() { +private fun doMotion( + editor: VimEditor, + caret: ImmutableVimCaret, + count1: Int, + whichwrapKey: String, + allowPastEnd: Boolean, +): Motion { + val allowWrap = injector.options(editor).whichwrap.contains(whichwrapKey) + return injector.motion.getHorizontalMotion(editor, caret, count1, allowPastEnd, allowWrap) +} + +abstract class MotionNonShiftedArrowLeftBaseAction() : NonShiftedSpecialKeyHandler() { override val motionType: MotionType = MotionType.EXCLUSIVE override fun motion( @@ -32,8 +43,38 @@ class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - val allowWrap = injector.options(editor).whichwrap.contains("<") - val allowEnd = operatorArguments.isOperatorPending // d deletes \n with wrap enabled - return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap) + return doMotion(editor, caret, -operatorArguments.count1, "<", allowPastEnd) + } + + protected open val allowPastEnd: Boolean = false +} + +// Note that Select mode is handled in [SelectMotionArrowLeftAction] +@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL]) +class MotionArrowLeftAction : MotionNonShiftedArrowLeftBaseAction() + +@CommandOrMotion(keys = ["", ""], modes = [Mode.OP_PENDING]) +class MotionArrowLeftOpPendingAction : MotionNonShiftedArrowLeftBaseAction() { + // When the motion is used with an operator, the EOL character is counted. + // This allows e.g., `d` to delete the end of line character on the previous line when wrap is active + // ('whichwrap' contains "<") + // See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators + override val allowPastEnd = true +} + +// Just needs to be a plain motion handler - it's not shifted, and the non-shifted actions don't apply in Insert mode +@CommandOrMotion(keys = ["", ""], modes = [Mode.INSERT]) +class MotionArrowLeftInsertModeAction : MotionActionHandler.ForEachCaret() { + override val motionType: MotionType = MotionType.EXCLUSIVE + + override fun getOffset( + editor: VimEditor, + caret: ImmutableVimCaret, + context: ExecutionContext, + argument: Argument?, + operatorArguments: OperatorArguments, + ): Motion { + // Insert mode is always allowed past the end of the line + return doMotion(editor, caret, -operatorArguments.count1, "[", allowPastEnd = true) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowRightAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowRightAction.kt index c4acc5f5c3..c901d980b7 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowRightAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionArrowRightAction.kt @@ -19,12 +19,23 @@ import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.Motion +import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.NonShiftedSpecialKeyHandler import com.maddyhome.idea.vim.helper.isEndAllowed import com.maddyhome.idea.vim.helper.usesVirtualSpace -@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -class MotionArrowRightAction : NonShiftedSpecialKeyHandler() { +private fun doMotion( + editor: VimEditor, + caret: ImmutableVimCaret, + count1: Int, + whichwrapKey: String, + allowPastEnd: Boolean, +): Motion { + val allowWrap = injector.options(editor).whichwrap.contains(whichwrapKey) + return injector.motion.getHorizontalMotion(editor, caret, count1, allowPastEnd, allowWrap) +} + +abstract class MotionNonShiftedArrowRightBaseAction() : NonShiftedSpecialKeyHandler() { override val motionType: MotionType = MotionType.EXCLUSIVE override fun motion( @@ -34,9 +45,38 @@ class MotionArrowRightAction : NonShiftedSpecialKeyHandler() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - val allowPastEnd = editor.usesVirtualSpace || editor.isEndAllowed || - operatorArguments.isOperatorPending // because of `d` removing the last character - val allowWrap = injector.options(editor).whichwrap.contains(">") - return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd, allowWrap) + return doMotion(editor, caret, operatorArguments.count1, ">", allowPastEnd(editor)) + } + + protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed +} + +// Note that Select mode is handled with [SelectMotionArrowRightAction] +@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL]) +class MotionArrowRightAction : MotionNonShiftedArrowRightBaseAction() + +@CommandOrMotion(keys = ["", ""], modes = [Mode.OP_PENDING]) +class MotionArrowRightOpPendingAction : MotionNonShiftedArrowRightBaseAction() { + // When the motion is used with an operator, the EOL character is counted. + // This allows e.g., `d` to delete the last character in a line. Note that we can't use editor.isEndAllowed to + // give us this because the current mode when we execute the operator/motion is no longer OP_PENDING. + // See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators + override fun allowPastEnd(editor: VimEditor) = true +} + +// Just needs to be a plain motion handler - it's not shifted, and the non-shifted actions don't apply in Insert mode +@CommandOrMotion(keys = ["", ""], modes = [Mode.INSERT]) +class MotionArrowRightInsertModeAction : MotionActionHandler.ForEachCaret() { + override val motionType: MotionType = MotionType.EXCLUSIVE + + override fun getOffset( + editor: VimEditor, + caret: ImmutableVimCaret, + context: ExecutionContext, + argument: Argument?, + operatorArguments: OperatorArguments, + ): Motion { + // Insert mode is always allowed past the end of the line + return doMotion(editor, caret, operatorArguments.count1, "]", allowPastEnd = true) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionBackspaceAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionBackspaceAction.kt index b3c39d3839..50829c35ae 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionBackspaceAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionBackspaceAction.kt @@ -20,8 +20,8 @@ import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.MotionActionHandler -@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -class MotionBackspaceAction : MotionActionHandler.ForEachCaret() { +@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL]) +open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() { override fun getOffset( editor: VimEditor, caret: ImmutableVimCaret, @@ -30,24 +30,15 @@ class MotionBackspaceAction : MotionActionHandler.ForEachCaret() { operatorArguments: OperatorArguments, ): Motion { val allowWrap = injector.options(editor).whichwrap.contains("b") - return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd = false, allowWrap) + return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap) } override val motionType: MotionType = MotionType.EXCLUSIVE } -@CommandOrMotion(keys = [""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -class MotionSpaceAction : MotionActionHandler.ForEachCaret() { - override fun getOffset( - editor: VimEditor, - caret: ImmutableVimCaret, - context: ExecutionContext, - argument: Argument?, - operatorArguments: OperatorArguments, - ): Motion { - val allowWrap = injector.options(editor).whichwrap.contains("s") - return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd = false, allowWrap) - } - - override val motionType: MotionType = MotionType.EXCLUSIVE -} +// When the motion is used with an operator, the EOL character is counted. +// This allows e.g., `d` to delete the end of line character on the previous line when wrap is active +// ('whichwrap' contains "b") +// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators +@CommandOrMotion(keys = ["", ""], modes = [Mode.OP_PENDING]) +class MotionBackspaceOpPendingModeAction : MotionBackspaceAction(allowPastEnd = true) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLastColumnAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLastColumnAction.kt index efe905f913..2a66025e13 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLastColumnAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLastColumnAction.kt @@ -27,13 +27,9 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode import com.maddyhome.idea.vim.helper.isEndAllowed import java.util.* -@CommandOrMotion(keys = [""], modes = [Mode.INSERT]) -class MotionLastColumnInsertAction : MotionLastColumnAction() { - override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_SAVE_STROKE) -} +abstract class MotionLastColumnBaseAction(private val isMotionForOperator: Boolean = false) + : MotionActionHandler.ForEachCaret() { -@CommandOrMotion(keys = ["$"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -open class MotionLastColumnAction : MotionActionHandler.ForEachCaret() { override val motionType: MotionType = MotionType.INCLUSIVE override fun getOffset( @@ -43,13 +39,26 @@ open class MotionLastColumnAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - val allow = if (editor.inVisualMode) { + val allowPastEnd = if (editor.inVisualMode) { injector.options(editor).selection != "old" } else { - if (operatorArguments.isOperatorPending) false else editor.isEndAllowed + // Don't allow past end if this motion is for an operator. I.e., for something like `d$`, we don't want to delete + // the end of line character + if (isMotionForOperator) false else editor.isEndAllowed } - val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allow) + val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allowPastEnd) return Motion.AdjustedOffset(offset, VimMotionGroupBase.LAST_COLUMN) } } + +@CommandOrMotion(keys = ["$"], modes = [Mode.NORMAL, Mode.VISUAL]) +open class MotionLastColumnAction : MotionLastColumnBaseAction() + +@CommandOrMotion(keys = ["$"], modes = [Mode.OP_PENDING]) +class MotionLastColumnOpPendingAction : MotionLastColumnBaseAction(isMotionForOperator = true) + +@CommandOrMotion(keys = [""], modes = [Mode.INSERT]) +class MotionLastColumnInsertAction : MotionLastColumnAction() { + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_SAVE_STROKE) +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLeftAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLeftAction.kt index 081b1bf298..0525310a7a 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLeftAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionLeftAction.kt @@ -21,8 +21,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.MotionActionHandler -@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -class MotionLeftAction : MotionActionHandler.ForEachCaret() { +abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionActionHandler.ForEachCaret() { override val motionType: MotionType = MotionType.EXCLUSIVE override fun getOffset( @@ -33,23 +32,16 @@ class MotionLeftAction : MotionActionHandler.ForEachCaret() { operatorArguments: OperatorArguments, ): Motion { val allowWrap = injector.options(editor).whichwrap.contains("h") - val allowEnd = operatorArguments.isOperatorPending // dh deletes \n with wrap enabled - return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap) + return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap) } } -@CommandOrMotion(keys = ["", ""], modes = [Mode.INSERT]) -class MotionLeftInsertModeAction : MotionActionHandler.ForEachCaret() { - override val motionType: MotionType = MotionType.EXCLUSIVE +@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL]) +class MotionLeftAction : MotionLeftBaseAction(allowPastEnd = false) - override fun getOffset( - editor: VimEditor, - caret: ImmutableVimCaret, - context: ExecutionContext, - argument: Argument?, - operatorArguments: OperatorArguments, - ): Motion { - val allowWrap = injector.options(editor).whichwrap.contains("[") - return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, true, allowWrap) - } -} +// When the motion is used with an operator, the EOL character is counted. +// This allows e.g., `dh` to delete the end of line character on the previous line when wrap is active +// ('whichwrap' contains "h") +// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators +@CommandOrMotion(keys = ["h"], modes = [Mode.OP_PENDING]) +class MotionLeftOpPendingModeAction : MotionLeftBaseAction(allowPastEnd = true) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionRightAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionRightAction.kt index 74a474c636..9a602831d6 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionRightAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionRightAction.kt @@ -23,8 +23,7 @@ import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.helper.isEndAllowed import com.maddyhome.idea.vim.helper.usesVirtualSpace -@CommandOrMotion(keys = ["l"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) -class MotionRightAction : MotionActionHandler.ForEachCaret() { +abstract class MotionRightBaseAction() : MotionActionHandler.ForEachCaret() { override val motionType: MotionType = MotionType.EXCLUSIVE override fun getOffset( @@ -35,24 +34,20 @@ class MotionRightAction : MotionActionHandler.ForEachCaret() { operatorArguments: OperatorArguments, ): Motion { val allowWrap = injector.options(editor).whichwrap.contains("l") - val allowEnd = editor.usesVirtualSpace || editor.isEndAllowed || - operatorArguments.isOperatorPending // because of `dl` removing the last character - return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd = allowEnd, allowWrap) + return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd(editor), allowWrap) } + + protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed } -@CommandOrMotion(keys = ["", ""], modes = [Mode.INSERT]) -class MotionRightInsertAction : MotionActionHandler.ForEachCaret() { - override val motionType: MotionType = MotionType.EXCLUSIVE +@CommandOrMotion(keys = ["l"], modes = [Mode.NORMAL, Mode.VISUAL]) +class MotionRightAction : MotionRightBaseAction() - override fun getOffset( - editor: VimEditor, - caret: ImmutableVimCaret, - context: ExecutionContext, - argument: Argument?, - operatorArguments: OperatorArguments, - ): Motion { - val allowWrap = injector.options(editor).whichwrap.contains("]") - return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd = true, allowWrap) - } +@CommandOrMotion(keys = ["l"], modes = [Mode.OP_PENDING]) +class MotionRightOpPendingAction : MotionRightBaseAction() { + // When the motion is used with an operator, the EOL character is counted. + // This allows e.g., `dl` to delete the last character in a line. Note that we can't use editor.isEndAllowed to give + // us this because the current mode when we execute the operator/motion is no longer OP_PENDING. + // See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators + override fun allowPastEnd(editor: VimEditor) = true } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftLeftAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftArrowLeftAction.kt similarity index 76% rename from vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftLeftAction.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftArrowLeftAction.kt index cf7bdc2064..eead2ee806 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftLeftAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftArrowLeftAction.kt @@ -18,23 +18,19 @@ import com.maddyhome.idea.vim.api.moveToMotion import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler -/** - * @author Alex Plate - */ - @CommandOrMotion(keys = [""], modes = [Mode.INSERT, Mode.NORMAL, Mode.VISUAL, Mode.SELECT]) -class MotionShiftLeftAction : ShiftedArrowKeyHandler(true) { +class MotionShiftArrowLeftAction : ShiftedArrowKeyHandler(true) { override val type: Command.Type = Command.Type.OTHER_READONLY override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) { - val vertical = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true) - caret.moveToMotion(vertical) + val motion = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true) + caret.moveToMotion(motion) } override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) { val caret = editor.currentCaret() - val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false) - caret.moveToMotion(newOffset) + val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false) + caret.moveToMotion(motion) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftRightAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftArrowRightAction.kt similarity index 71% rename from vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftRightAction.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftArrowRightAction.kt index 9334c2109e..7dac488cf8 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftRightAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionShiftArrowRightAction.kt @@ -16,28 +16,21 @@ import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.moveToMotion import com.maddyhome.idea.vim.command.Command -import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler -/** - * @author Alex Plate - */ - @CommandOrMotion(keys = [""], modes = [Mode.INSERT, Mode.NORMAL, Mode.VISUAL, Mode.SELECT]) -class MotionShiftRightAction : ShiftedArrowKeyHandler(true) { +class MotionShiftArrowRightAction : ShiftedArrowKeyHandler(true) { override val type: Command.Type = Command.Type.OTHER_READONLY override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) { - val vertical = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true) - caret.moveToMotion(vertical) + val motion = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true) + caret.moveToMotion(motion) } override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) { val caret = editor.currentCaret() - val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false) - if (newOffset is Motion.AbsoluteOffset) { - caret.moveToOffset(newOffset.offset) - } + val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false) + caret.moveToMotion(motion) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionSpaceAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionSpaceAction.kt new file mode 100644 index 0000000000..98ba646ff8 --- /dev/null +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/MotionSpaceAction.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2003-2024 The IdeaVim authors + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE.txt file or at + * https://opensource.org/licenses/MIT. + */ + +package com.maddyhome.idea.vim.action.motion.leftright + +import com.intellij.vim.annotations.CommandOrMotion +import com.intellij.vim.annotations.Mode +import com.maddyhome.idea.vim.api.ExecutionContext +import com.maddyhome.idea.vim.api.ImmutableVimCaret +import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.api.injector +import com.maddyhome.idea.vim.api.options +import com.maddyhome.idea.vim.command.Argument +import com.maddyhome.idea.vim.command.MotionType +import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.handler.Motion +import com.maddyhome.idea.vim.handler.MotionActionHandler + +@CommandOrMotion(keys = [""], modes = [Mode.NORMAL, Mode.VISUAL]) +open class MotionSpaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() { + override fun getOffset( + editor: VimEditor, + caret: ImmutableVimCaret, + context: ExecutionContext, + argument: Argument?, + operatorArguments: OperatorArguments, + ): Motion { + val allowWrap = injector.options(editor).whichwrap.contains("s") + return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd, allowWrap) + } + + override val motionType: MotionType = MotionType.EXCLUSIVE +} + +@CommandOrMotion(keys = [""], modes = [Mode.OP_PENDING]) +class MotionSpaceOpPendingModeAction : MotionSpaceAction(allowPastEnd = true) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/TillCharacterMotion.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/TillCharacterMotion.kt index ac3c363084..2f143afbe0 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/TillCharacterMotion.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/leftright/TillCharacterMotion.kt @@ -15,15 +15,12 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Argument -import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.Direction import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.toMotionOrError -import com.maddyhome.idea.vim.helper.enumSetOf -import java.util.* enum class TillCharacterMotionType { LAST_F, @@ -51,9 +48,6 @@ sealed class TillCharacterMotion( private val finishBeforeCharacter: Boolean, ) : MotionActionHandler.ForEachCaret() { override val argumentType: Argument.Type = Argument.Type.DIGRAPH - - override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_ALLOW_DIGRAPH) - override val motionType: MotionType = if (direction == Direction.BACKWARDS) MotionType.EXCLUSIVE else MotionType.INCLUSIVE @@ -64,7 +58,7 @@ sealed class TillCharacterMotion( argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val res = if (finishBeforeCharacter) { injector.motion .moveCaretToBeforeNextCharacterOnLine( diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkAction.kt index b624bc9336..7b5062bd78 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkAction.kt @@ -37,7 +37,7 @@ class MotionGotoFileMarkAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, false) @@ -57,7 +57,7 @@ class MotionGotoFileMarkNoSaveJumpAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, false) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkLineAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkLineAction.kt index 0a04b3e96a..a765df47e9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkLineAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoFileMarkLineAction.kt @@ -37,7 +37,7 @@ class MotionGotoFileMarkLineAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, false) @@ -57,7 +57,7 @@ class MotionGotoFileMarkLineNoSaveJumpAction : MotionActionHandler.ForEachCaret( argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, true) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt index d8a1db92d2..1b6d4d8d32 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkAction.kt @@ -37,7 +37,7 @@ class MotionGotoMarkAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, false) @@ -57,7 +57,7 @@ class MotionGotoMarkNoSaveJumpAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, false) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkLineAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkLineAction.kt index e1c1e0c53a..14d39b1e24 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkLineAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionGotoMarkLineAction.kt @@ -37,7 +37,7 @@ class MotionGotoMarkLineAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, true) @@ -57,7 +57,7 @@ class MotionGotoMarkLineNoSaveJumpAction : MotionActionHandler.ForEachCaret() { argument: Argument?, operatorArguments: OperatorArguments, ): Motion { - if (argument == null) return Motion.Error + if (argument !is Argument.Character) return Motion.Error val mark = argument.character return injector.motion.moveCaretToMark(caret, mark, true) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionMarkAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionMarkAction.kt index 48cfaeca14..ff0f40b842 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionMarkAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/mark/MotionMarkAction.kt @@ -24,7 +24,6 @@ class MotionMarkAction : VimActionHandler.SingleExecution() { override val argumentType: Argument.Type = Argument.Type.CHARACTER override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { - val argument = cmd.argument - return argument != null && injector.markService.setMark(editor, argument.character) + return cmd.argument.let { it is Argument.Character && injector.markService.setMark(editor, it.character) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionLeftAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionArrowLeftAction.kt similarity index 91% rename from vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionLeftAction.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionArrowLeftAction.kt index 8b0ffdd0cc..d1854e1e35 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionLeftAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionArrowLeftAction.kt @@ -18,6 +18,7 @@ import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.toMotion @@ -28,7 +29,7 @@ import com.maddyhome.idea.vim.options.OptionConstants */ @CommandOrMotion(keys = [""], modes = [Mode.SELECT]) -class SelectMotionLeftAction : MotionActionHandler.ForEachCaret() { +class SelectMotionArrowLeftAction : MotionActionHandler.ForEachCaret() { override val motionType: MotionType = MotionType.EXCLUSIVE @@ -58,6 +59,6 @@ class SelectMotionLeftAction : MotionActionHandler.ForEachCaret() { } private companion object { - private val logger = injector.getLogger(SelectMotionLeftAction::class.java) + private val logger = vimLogger() } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionRightAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionArrowRightAction.kt similarity index 91% rename from vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionRightAction.kt rename to vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionArrowRightAction.kt index 1125b66bf4..9ab5e651fb 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionRightAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/motion/SelectMotionArrowRightAction.kt @@ -18,6 +18,7 @@ import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.toMotion @@ -28,7 +29,7 @@ import com.maddyhome.idea.vim.options.OptionConstants */ @CommandOrMotion(keys = [""], modes = [Mode.SELECT]) -class SelectMotionRightAction : MotionActionHandler.ForEachCaret() { +class SelectMotionArrowRightAction : MotionActionHandler.ForEachCaret() { override val motionType: MotionType = MotionType.EXCLUSIVE @@ -58,6 +59,6 @@ class SelectMotionRightAction : MotionActionHandler.ForEachCaret() { } private companion object { - private val logger = injector.getLogger(SelectMotionRightAction::class.java) + private val logger = vimLogger() } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt index 68bcaa8a57..a09929fd16 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt @@ -37,7 +37,22 @@ interface VimChangeGroup { fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) - fun processEscape(editor: VimEditor, context: ExecutionContext?, operatorArguments: OperatorArguments) + /** + * Enter Insert mode for block selection. + * + * Given a [TextRange] representing a block selection, position the primary caret either at the start column of the + * selection for insert, or the end of the first line for append. Then set the insert repeat counts for the extent of + * the block selection and start Insert mode. + * + * @param editor The Vim editor instance. + * @param context The execution context. + * @param range The range of text representing the block selection. + * @param append Whether to insert before the range, or append after it. + * @return True if the block was successfully inserted, false otherwise. + */ + fun initBlockInsert(editor: VimEditor, context: ExecutionContext, range: TextRange, append: Boolean): Boolean + + fun processEscape(editor: VimEditor, context: ExecutionContext?) fun processEnter(editor: VimEditor, caret: VimCaret, context: ExecutionContext) fun processEnter(editor: VimEditor, context: ExecutionContext) @@ -59,13 +74,7 @@ interface VimChangeGroup { fun deleteEndOfLine(editor: VimEditor, caret: VimCaret, count: Int, operatorArguments: OperatorArguments): Boolean - fun deleteJoinLines( - editor: VimEditor, - caret: VimCaret, - count: Int, - spaces: Boolean, - operatorArguments: OperatorArguments, - ): Boolean + fun deleteJoinLines(editor: VimEditor, caret: VimCaret, count: Int, spaces: Boolean): Boolean fun processKey(editor: VimEditor, key: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean @@ -93,7 +102,6 @@ interface VimChangeGroup { range: TextRange, type: SelectionType?, isChange: Boolean, - operatorArguments: OperatorArguments, saveToRegister: Boolean = true, ): Boolean fun changeCharacters(editor: VimEditor, caret: VimCaret, operatorArguments: OperatorArguments): Boolean @@ -113,8 +121,6 @@ interface VimChangeGroup { fun changeCaseToggleCharacter(editor: VimEditor, caret: VimCaret, count: Int): Boolean - fun blockInsert(editor: VimEditor, context: ExecutionContext, range: TextRange, append: Boolean, operatorArguments: OperatorArguments): Boolean - fun changeCaseRange(editor: VimEditor, caret: VimCaret, range: TextRange, type: ChangeCaseType): Boolean fun changeRange( @@ -123,7 +129,6 @@ interface VimChangeGroup { range: TextRange, type: SelectionType, context: ExecutionContext, - operatorArguments: OperatorArguments, ): Boolean fun changeCaseMotion(editor: VimEditor, caret: VimCaret, context: ExecutionContext?, type: ChangeCaseType, argument: Argument, operatorArguments: OperatorArguments): Boolean @@ -188,7 +193,6 @@ interface VimChangeGroup { context: ExecutionContext, count: Int, started: Boolean, - operatorArguments: OperatorArguments, ) fun type(vimEditor: VimEditor, context: ExecutionContext, key: Char) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt index 96984703c2..2a31929426 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt @@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.ChangesListener -import com.maddyhome.idea.vim.common.OperatedRange import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.vimLogger @@ -24,6 +23,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset +import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.helper.CharacterHelper import com.maddyhome.idea.vim.helper.CharacterHelper.charType import com.maddyhome.idea.vim.helper.NumberType @@ -111,7 +111,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { TextRange(caret.offset, endOffset.offset), SelectionType.CHARACTER_WISE, caret, - operatorArguments, ) val pos = caret.offset val norm = editor.normalizeOffset(caret.getBufferPosition().line, pos, isChange) @@ -162,44 +161,43 @@ abstract class VimChangeGroupBase : VimChangeGroup { range: TextRange, type: SelectionType?, caret: VimCaret, - operatorArguments: OperatorArguments, saveToRegister: Boolean = true, ): Boolean { var updatedRange = range + // Fix for https://youtrack.jetbrains.net/issue/VIM-35 if (!range.normalize(editor.fileSize().toInt())) { - updatedRange = if (range.startOffset == range.endOffset && range.startOffset == editor.fileSize() - .toInt() && range.startOffset != 0 - ) { + updatedRange = if (range.startOffset == range.endOffset + && range.startOffset == editor.fileSize().toInt() + && range.startOffset != 0) { TextRange(range.startOffset - 1, range.endOffset) } else { return false } } - val mode = operatorArguments.mode - if (type == null || - (mode == Mode.INSERT || mode == Mode.REPLACE) || - !saveToRegister || - caret.registerStorage.storeText(editor, updatedRange, type, true) - ) { - val startOffsets = updatedRange.startOffsets - val endOffsets = updatedRange.endOffsets - for (i in updatedRange.size() - 1 downTo 0) { - val (newRange, _) = editor.search( - startOffsets[i] to endOffsets[i], - editor, - LineDeleteShift.NL_ON_END - ) ?: continue - editor.deleteString(TextRange(newRange.first, newRange.second)) - } - if (type != null) { - val start = updatedRange.startOffset - injector.markService.setMark(caret, MARK_CHANGE_POS, start) - injector.markService.setChangeMarks(caret, TextRange(start, start + 1)) - } - return true + + val isInsertMode = editor.mode == Mode.INSERT || editor.mode == Mode.REPLACE + val shouldYank = type != null && !isInsertMode && saveToRegister + if (shouldYank && !caret.registerStorage.storeText(editor, updatedRange, type, isDelete = true)) { + return false } - return false + + val startOffsets = updatedRange.startOffsets + val endOffsets = updatedRange.endOffsets + for (i in updatedRange.size() - 1 downTo 0) { + val (newRange, _) = editor.search( + startOffsets[i] to endOffsets[i], + editor, + LineDeleteShift.NL_ON_END + ) ?: continue + editor.deleteString(TextRange(newRange.first, newRange.second)) + } + if (type != null) { + val start = updatedRange.startOffset + injector.markService.setMark(caret, MARK_CHANGE_POS, start) + injector.markService.setChangeMarks(caret, TextRange(start, start + 1)) + } + return true } /** @@ -239,11 +237,10 @@ abstract class VimChangeGroupBase : VimChangeGroup { editor: VimEditor, context: ExecutionContext, count: Int, - operatorArguments: OperatorArguments, ) { val myLastStrokes = lastStrokes ?: return for (caret in editor.nativeCarets()) { - for (i in 0 until count) { + repeat(count) { for (lastStroke in myLastStrokes) { when (lastStroke) { is NativeAction -> { @@ -252,7 +249,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { } is EditorActionHandlerBase -> { - injector.actionExecutor.executeVimAction(editor, lastStroke, context, operatorArguments) + injector.actionExecutor.executeVimAction(editor, lastStroke, context, OperatorArguments(0, editor.mode)) strokes.add(lastStroke) } @@ -281,7 +278,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { context: ExecutionContext, count: Int, started: Boolean, - operatorArguments: OperatorArguments, ) { for (caret in editor.nativeCarets()) { if (repeatLines > 0) { @@ -302,17 +298,17 @@ abstract class VimChangeGroupBase : VimChangeGroup { val updatedCount = if (started) (if (i == 0) count else count + 1) else count if (repeatColumn >= VimMotionGroupBase.LAST_COLUMN) { caret.moveToOffset(injector.motion.moveCaretToLineEnd(editor, bufferLine + i, true)) - repeatInsertText(editor, context, updatedCount, operatorArguments) + repeatInsertText(editor, context, updatedCount) } else if (editor.getVisualLineLength(visualLine + i) >= repeatColumn) { val visualPosition = VimVisualPosition(visualLine + i, repeatColumn, false) val inlaysCount = injector.engineEditorHelper.amountOfInlaysBeforeVisualPosition(editor, visualPosition) caret.moveToVisualPosition(VimVisualPosition(visualLine + i, repeatColumn + inlaysCount, false)) - repeatInsertText(editor, context, updatedCount, operatorArguments) + repeatInsertText(editor, context, updatedCount) } } caret.moveToOffset(position) } else { - repeatInsertText(editor, context, count, operatorArguments) + repeatInsertText(editor, context, count) val position = injector.motion.getHorizontalMotion(editor, caret, -1, false) caret.moveToMotion(position) } @@ -330,7 +326,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { val oldFragmentLength = oldFragment.length // Repeat buffer limits - if (repeatCharsCount > Companion.MAX_REPEAT_CHARS_COUNT) { + if (repeatCharsCount > MAX_REPEAT_CHARS_COUNT) { return } @@ -351,7 +347,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { if (oldFragmentLength > 0) { val editorDelete = injector.nativeActionManager.deleteAction if (editorDelete != null) { - for (i in 0 until oldFragmentLength) { + repeat(oldFragmentLength) { strokes.add(editorDelete) } } @@ -370,7 +366,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { val motionName = if (delta < 0) "VimMotionLeftAction" else "VimMotionRightAction" val action = injector.actionExecutor.findVimAction(motionName)!! val count = abs(delta) - for (i in 0 until count) { + repeat(count) { positionCaretActions.add(action) } return positionCaretActions @@ -449,25 +445,8 @@ abstract class VimChangeGroupBase : VimChangeGroup { if (mode == Mode.REPLACE) { editor.insertMode = false } - if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) { - val commandState = injector.vimState - repeatInsert( - editor, - context, - 1, - false, - OperatorArguments(false, 1, commandState.mode), - ) - } else { - val commandState = injector.vimState - repeatInsert( - editor, - context, - cmd.count, - false, - OperatorArguments(false, cmd.count, commandState.mode), - ) - } + val count = if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) 1 else cmd.count + repeatInsert(editor, context, count, false) if (mode == Mode.REPLACE) { editor.insertMode = true } @@ -532,9 +511,9 @@ abstract class VimChangeGroupBase : VimChangeGroup { exit: Boolean, operatorArguments: OperatorArguments, ) { - repeatInsertText(editor, context, 1, operatorArguments) + repeatInsertText(editor, context, 1) if (exit) { - editor.exitInsertMode(context, operatorArguments) + editor.exitInsertMode(context) } } @@ -544,7 +523,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { * * DEPRECATED. Please, don't use this function directly. Use ModeHelper.exitInsertMode in file ModeExtensions.kt */ - override fun processEscape(editor: VimEditor, context: ExecutionContext?, operatorArguments: OperatorArguments) { + override fun processEscape(editor: VimEditor, context: ExecutionContext?) { // Get the offset for marks before we exit insert mode - switching from insert to overtype subtracts one from the // column offset. val markGroup = injector.markService @@ -553,17 +532,24 @@ abstract class VimChangeGroupBase : VimChangeGroup { if (editor.mode is Mode.REPLACE) { editor.insertMode = true } - var cnt = if (lastInsert != null) lastInsert!!.count else 0 - if (lastInsert != null && lastInsert!!.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) { - cnt = 1 - } + val repeatCount0 = lastInsert?.let { + // How many times do we want to *repeat* the insert? For a simple insert or change action, this is count-1. But if + // the command is an operator+motion, then the count applies to the motion, not the insert/change. I.e., `2cw` + // changes two words, rather than inserting the change twice. This is the only place where we need to know who the + // count applies to + if (CommandFlags.FLAG_NO_REPEAT_INSERT in it.flags || it.action.argumentType == Argument.Type.MOTION) { + 0 + } else { + it.count - 1 + } + } ?: 0 if (vimDocument != null && vimDocumentListener != null) { vimDocument!!.removeChangeListener(vimDocumentListener!!) vimDocumentListener = null } lastStrokes = ArrayList(strokes) if (context != null) { - injector.changeGroup.repeatInsert(editor, context, if (cnt == 0) 0 else cnt - 1, true, operatorArguments) + injector.changeGroup.repeatInsert(editor, context, repeatCount0, true) } if (editor.mode is Mode.INSERT) { updateLastInsertedTextRegister() @@ -677,7 +663,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { val rangeToDelete = TextRange(startOffset, offset) editor.nativeCarets().filter { it != caret && rangeToDelete.contains(it.offset) } .forEach { editor.removeCaret(it) } - val res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE, caret, operatorArguments) + val res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE, caret) if (editor.usesVirtualSpace) { caret.moveToOffset(startOffset) } else { @@ -704,7 +690,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { caret: VimCaret, count: Int, spaces: Boolean, - operatorArguments: OperatorArguments, ): Boolean { var myCount = count if (myCount < 2) myCount = 2 @@ -713,7 +698,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { return if (lline + myCount > total) { false } else { - deleteJoinNLines(editor, caret, lline, myCount, spaces, operatorArguments) + deleteJoinNLines(editor, caret, lline, myCount, spaces) } } @@ -786,7 +771,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { logger.debug("offset=$offset") } if (offset != -1) { - val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret, operatorArguments) + val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret) if (res && caret.offset >= editor.fileSize() && caret.offset != 0) { caret.moveToOffset( injector.motion.moveCaretToRelativeLineStartSkipLeading( @@ -809,7 +794,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { lline + count <= total } if (!allowedExecution) return false - for (i in 0 until executions) { + repeat(executions) { val joinLinesAction = injector.nativeActionManager.joinLines if (joinLinesAction != null) { injector.actionExecutor.executeAction(editor, joinLinesAction, context) @@ -839,7 +824,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { val endLine = editor.offsetToBufferPosition(range.endOffset).line var count = endLine - startLine + 1 if (count < 2) count = 2 - return deleteJoinNLines(editor, caret, startLine, count, spaces, operatorArguments) + return deleteJoinNLines(editor, caret, startLine, count, spaces) } override fun joinViaIdeaBySelections( @@ -876,28 +861,25 @@ abstract class VimChangeGroupBase : VimChangeGroup { isChange: Boolean, operatorArguments: OperatorArguments, ): Pair? { + check(argument is Argument.Motion) { "Unexpected argument: $argument" } + val range = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) ?: return null + var motionType = argument.getMotionType() // 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 - var type: SelectionType = if (argument.motion.isLinewiseMotion()) { - SelectionType.LINE_WISE - } else { - SelectionType.CHARACTER_WISE - } - val motion = argument.motion - if (!isChange && !motion.isLinewiseMotion()) { + if (!isChange && motionType != SelectionType.LINE_WISE) { val start = editor.offsetToBufferPosition(range.startOffset) val end = editor.offsetToBufferPosition(range.endOffset) - if (start.line != end.line) { - if (!editor.anyNonWhitespace(range.startOffset, -1) && !editor.anyNonWhitespace(range.endOffset, 1)) { - type = SelectionType.LINE_WISE - } + if (start.line != end.line + && !editor.anyNonWhitespace(range.startOffset, -1) + && !editor.anyNonWhitespace(range.endOffset, 1)) { + motionType = SelectionType.LINE_WISE } } - return Pair(range, type) + return Pair(range, motionType) } /** @@ -917,13 +899,12 @@ abstract class VimChangeGroupBase : VimChangeGroup { range: TextRange, type: SelectionType?, isChange: Boolean, - operatorArguments: OperatorArguments, saveToRegister: Boolean, ): Boolean { val intendedColumn = caret.vimLastColumn val removeLastNewLine = removeLastNewLine(editor, range, type) - val res = deleteText(editor, range, type, caret, operatorArguments, saveToRegister) + val res = deleteText(editor, range, type, caret, saveToRegister) var processedCaret = editor.findLastVersionOfCaret(caret) ?: caret if (removeLastNewLine) { val textLength = editor.fileSize().toInt() @@ -1042,7 +1023,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { startLine: Int, count: Int, spaces: Boolean, - operatorArguments: OperatorArguments, ): Boolean { // Don't move the caret until we've successfully deleted text. If we're on the last line, we don't want to move the // caret and then be unable to delete @@ -1059,7 +1039,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { return i > 1 } // Note that caret isn't moved here; it's only used for register + mark storage - deleteText(editor, TextRange(startOffset, endOffset), null, caret, operatorArguments) + deleteText(editor, TextRange(startOffset, endOffset), null, caret) if (spaces && !hasTrailingWhitespace) { insertText(editor, caret, startOffset, " ") } @@ -1146,8 +1126,8 @@ abstract class VimChangeGroupBase : VimChangeGroup { ): Boolean { var count0 = operatorArguments.count0 // Vim treats cw as ce and cW as cE if cursor is on a non-blank character - val motion = argument.motion - val id = motion.action.id + var motionArgument = argument as? Argument.Motion ?: return false + val id = motionArgument.motion.id var kludge = false val bigWord = id == VIM_MOTION_BIG_WORD_RIGHT val chars = editor.text() @@ -1157,7 +1137,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { val charType = charType(editor, chars[offset], bigWord) if (charType !== CharacterHelper.CharacterType.WHITESPACE) { val lastWordChar = offset >= fileSize - 1 || charType(editor, chars[offset + 1], bigWord) !== charType - if (wordMotions.contains(id) && lastWordChar && motion.count == 1) { + if (wordMotions.contains(id) && lastWordChar && operatorArguments.count1 == 1) { val res = deleteCharacter(editor, caret, 1, true, operatorArguments) if (res) { editor.vimChangeActionSwitchMode = Mode.INSERT @@ -1167,49 +1147,50 @@ abstract class VimChangeGroupBase : VimChangeGroup { when (id) { VIM_MOTION_WORD_RIGHT -> { kludge = true - motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT) + motionArgument = Argument.Motion( + injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT) as MotionActionHandler, + motionArgument.argument + ) } VIM_MOTION_BIG_WORD_RIGHT -> { kludge = true - motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT) + motionArgument = Argument.Motion( + injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT) as MotionActionHandler, + motionArgument.argument + ) } VIM_MOTION_CAMEL_RIGHT -> { kludge = true - motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT) + motionArgument = Argument.Motion( + injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT) as MotionActionHandler, + motionArgument.argument + ) } } } } if (kludge) { - val cnt = operatorArguments.count1 * motion.count - val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, cnt, bigWord, false) - val pos2 = injector.searchHelper.findNextWordEnd(editor, pos1, -cnt, bigWord, false) + val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, operatorArguments.count1, bigWord, false) + val pos2 = injector.searchHelper.findNextWordEnd(editor, pos1, -operatorArguments.count1, bigWord, false) if (logger.isDebug()) { logger.debug("pos=$offset") logger.debug("pos1=$pos1") logger.debug("pos2=$pos2") logger.debug("count=" + operatorArguments.count1) - logger.debug("arg.count=" + motion.count) } - if (pos2 == offset) { - if (operatorArguments.count1 > 1) { - count0-- - } else if (motion.count > 1) { - motion.rawCount = motion.count - 1 - } else { - motion.flags = EnumSet.noneOf(CommandFlags::class.java) - } + if (pos2 == offset && operatorArguments.count1 > 1) { + count0-- } } val (first, second) = getDeleteRangeAndType( editor, caret, context, - argument, + motionArgument, true, - operatorArguments.withCount0(count0), + operatorArguments.copy(count0 = count0), ) ?: return false return changeRange( editor, @@ -1217,7 +1198,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { first, second, context, - operatorArguments, ) } @@ -1242,7 +1222,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { * @param caret The caret to be moved after range deletion * @param range The range to change * @param type The type of the range - * @param operatorArguments * @return true if able to delete the range, false if not */ override fun changeRange( @@ -1251,7 +1230,6 @@ abstract class VimChangeGroupBase : VimChangeGroup { range: TextRange, type: SelectionType, context: ExecutionContext, - operatorArguments: OperatorArguments, ): Boolean { var col = 0 var lines = 0 @@ -1264,7 +1242,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { } val after = range.endOffset >= editor.fileSize() val lp = editor.offsetToBufferPosition(injector.motion.moveCaretToCurrentLineStartSkipLeading(editor, caret)) - val res = deleteRange(editor, caret, range, type, true, operatorArguments) + val res = deleteRange(editor, caret, range, type, true) val updatedCaret = editor.findLastVersionOfCaret(caret) ?: caret if (res) { if (type === SelectionType.LINE_WISE) { @@ -1926,7 +1904,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { pos++ } if (pos > wsoff) { - deleteText(editor, TextRange(wsoff, pos), null, caret, operatorArguments, true) + deleteText(editor, TextRange(wsoff, pos), null, caret, true) } } } @@ -1958,23 +1936,21 @@ abstract class VimChangeGroupBase : VimChangeGroup { } } - override fun blockInsert( + override fun initBlockInsert( editor: VimEditor, context: ExecutionContext, range: TextRange, append: Boolean, - operatorArguments: OperatorArguments, ): Boolean { val lines = getLinesCountInVisualBlock(editor, range) val startPosition = editor.offsetToBufferPosition(range.startOffset) - val mode = operatorArguments.mode - val visualBlockMode = mode is Mode.VISUAL && mode.selectionType === SelectionType.BLOCK_WISE + // Note that when called, we're likely to have moved from Visual (block) to Normal, which means all secondary carets + // will have been removed. Even if not, this would move them all to the same location, which would remove them and + // leave only the primary caret. for (caret in editor.carets()) { val line = startPosition.line var column = startPosition.column - if (!visualBlockMode) { - column = 0 - } else if (append) { + if (append) { column += range.maxLength if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) { column = VimMotionGroupBase.LAST_COLUMN @@ -1986,18 +1962,10 @@ abstract class VimChangeGroupBase : VimChangeGroup { val offset = editor.getLineEndOffset(line) insertText(editor, caret, offset, pad) } - if (visualBlockMode || !append) { - caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column))) - } - if (visualBlockMode) { - setInsertRepeat(lines, column, append) - } - } - if (visualBlockMode || !append) { - insertBeforeCursor(editor, context) - } else { - insertAfterCursor(editor, context) + caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column))) + setInsertRepeat(lines, column, append) } + insertBeforeCursor(editor, context) return true } @@ -2070,9 +2038,3 @@ abstract class VimChangeGroupBase : VimChangeGroup { VimChangeGroup.ChangeCaseType.UPPER -> Character.toUpperCase(ch) } } - -fun OperatedRange.toType(): SelectionType = when (this) { - is OperatedRange.Characters -> SelectionType.CHARACTER_WISE - is OperatedRange.Lines -> SelectionType.LINE_WISE - is OperatedRange.Block -> SelectionType.BLOCK_WISE -} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineService.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineService.kt index 7395c189e5..04dd9756e1 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineService.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineService.kt @@ -8,8 +8,6 @@ package com.maddyhome.idea.vim.api -import com.maddyhome.idea.vim.command.Command - interface VimCommandLineService { fun isCommandLineSupported(editor: VimEditor): Boolean @@ -26,10 +24,10 @@ interface VimCommandLineService { * @param initialText The initial text for the entry */ fun createSearchPrompt(editor: VimEditor, context: ExecutionContext, label: String, initialText: String): VimCommandLine - fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, command: Command, initialText: String): VimCommandLine + fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, count0: Int, initialText: String): VimCommandLine @Deprecated("Please use ModalInputService.create()") fun createWithoutShortcuts(editor: VimEditor, context: ExecutionContext, label: String, initText: String): VimCommandLine fun fullReset() -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt index d74f034f08..390fe8a225 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt @@ -8,7 +8,6 @@ package com.maddyhome.idea.vim.api -import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd @@ -50,15 +49,15 @@ abstract class VimCommandLineServiceBase : VimCommandLineService { return createCommandLinePrompt(editor, context, removeSelections = false, label, initialText) } - override fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, command: Command, initialText: String): VimCommandLine { - val rangeText = getRange(editor, command) + override fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, count0: Int, initialText: String): VimCommandLine { + val rangeText = getRange(editor, count0) return createCommandLinePrompt(editor, context, removeSelections = true, label = ":", rangeText + initialText) } - protected fun getRange(editor: VimEditor, cmd: Command) = when { + protected fun getRange(editor: VimEditor, count0: Int) = when { editor.inVisualMode -> "'<,'>" - cmd.rawCount == 1 -> "." - cmd.rawCount > 1 -> ".,.+" + (cmd.count - 1) + count0 == 1 -> "." + count0 > 1 -> ".,.+" + (count0 - 1) else -> "" } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt index 004de0583a..473f0eaadd 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt @@ -245,7 +245,9 @@ interface VimEditor { // Can be used as a key to store something for specific project val projectId: String - fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) + @Deprecated("Use overload without OperatorArguments", replaceWith = ReplaceWith("exitInsertMode(context)")) + fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) { exitInsertMode(context) } + fun exitInsertMode(context: ExecutionContext) fun exitSelectModeNative(adjustCaret: Boolean) var vimLastSelectionType: SelectionType? @@ -350,4 +352,4 @@ interface VimFoldRegion { var isExpanded: Boolean val startOffset: Int val endOffset: Int -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt index db9e0c8c3d..5733bbc45b 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt @@ -10,17 +10,17 @@ package com.maddyhome.idea.vim.api import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.extension.ExtensionHandler -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyMapping import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.MappingInfo import com.maddyhome.idea.vim.key.MappingOwner +import com.maddyhome.idea.vim.key.RootNode import com.maddyhome.idea.vim.key.ShortcutOwnerInfo import com.maddyhome.idea.vim.vimscript.model.expressions.Expression import javax.swing.KeyStroke interface VimKeyGroup { - fun getKeyRoot(mappingMode: MappingMode): CommandPartNode + fun getKeyRoot(mappingMode: MappingMode): RootNode fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer fun getActions(editor: VimEditor, keyStroke: KeyStroke): List fun getKeymapConflicts(keyStroke: KeyStroke): List diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt index 1d7a7b3d4c..5395e9c833 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt @@ -12,7 +12,6 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.handler.EditorActionHandlerBase -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyMapping import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.MappingInfo @@ -30,7 +29,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { @JvmField val myShortcutConflicts: MutableMap = LinkedHashMap() val requiredShortcutKeys: MutableSet = HashSet(300) - val keyRoots: MutableMap> = EnumMap(MappingMode::class.java) + val keyRoots: MutableMap> = EnumMap(MappingMode::class.java) val keyMappings: MutableMap = EnumMap(MappingMode::class.java) override fun removeKeyMapping(modes: Set, keys: List) { @@ -63,7 +62,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { * @param mappingMode The mapping mode * @return The key mapping tree root */ - override fun getKeyRoot(mappingMode: MappingMode): CommandPartNode = keyRoots.getOrPut(mappingMode) { RootNode() } + override fun getKeyRoot(mappingMode: MappingMode): RootNode = keyRoots.getOrPut(mappingMode) { RootNode(mappingMode.name.get(0).lowercase()) } override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt index fb984c4958..38e7b73978 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimMotionGroupBase.kt @@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.Graphemes import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.group.findMatchingPairOnCurrentLine +import com.maddyhome.idea.vim.handler.ExternalActionHandler import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset import com.maddyhome.idea.vim.handler.MotionActionHandler @@ -116,9 +117,9 @@ abstract class VimMotionGroupBase : VimMotionGroup { val text = editor.text() val oldOffset = caret.offset var current = oldOffset - for (i in 0 until count.absoluteValue) { + repeat(count.absoluteValue) { val newOffset = if (count > 0) Graphemes.next(text, current) else Graphemes.prev(text, current) - current = newOffset ?: break + current = newOffset ?: return@repeat } val offset = if (allowWrap) { @@ -313,69 +314,69 @@ abstract class VimMotionGroupBase : VimMotionGroup { argument: Argument, operatorArguments: OperatorArguments, ): TextRange? { + if (argument !is Argument.Motion) { + throw RuntimeException("Unexpected argument passed to getMotionRange2: $argument") + } + var start: Int var end: Int - if (argument.type === Argument.Type.OFFSETS) { - val offsets = argument.offsets[caret] ?: return null - val (first, second) = offsets.getNativeStartAndEnd() - start = first - end = second - } else { - val cmd = argument.motion - // Normalize the counts between the command and the motion argument - val cnt = cmd.count * operatorArguments.count1 - val raw = if (operatorArguments.count0 == 0 && cmd.rawCount == 0) 0 else cnt - val cmdAction = cmd.action - if (cmdAction is MotionActionHandler) { + + val action = argument.motion + when (action) { + is MotionActionHandler -> { // This is where we are now start = caret.offset // Execute the motion (without moving the cursor) and get where we end - val motion = - cmdAction.getHandlerOffset(editor, caret, context, cmd.argument, operatorArguments.withCount0(raw)) + val motion = action.getHandlerOffset(editor, caret, context, argument.argument, operatorArguments) + if (Motion.Error == motion || Motion.NoMotion == motion) return null - // Invalid motion - if (Motion.Error == motion) return null - if (Motion.NoMotion == motion) return null end = (motion as AbsoluteOffset).offset // If inclusive, add the last character to the range - if (cmdAction.motionType === MotionType.INCLUSIVE) { + if (action.motionType === MotionType.INCLUSIVE) { if (start > end) { if (start < editor.fileSize()) start++ } else { if (end < editor.fileSize()) end++ } } - } else if (cmdAction is TextObjectActionHandler) { - val range: TextRange = cmdAction.getRange(editor, caret, context, cnt, raw) + } + + is TextObjectActionHandler -> { + val range: TextRange = action.getRange(editor, caret, context, operatorArguments.count1, operatorArguments.count0) ?: return null start = range.startOffset end = range.endOffset - if (cmd.isLinewiseMotion()) end-- - } else { - throw RuntimeException( - "Commands doesn't take " + cmdAction.javaClass.simpleName + " as an operator", - ) + if (argument.isLinewiseMotion()) end-- } - // Normalize the range - if (start > end) { - val t = start - start = end - end = t + is ExternalActionHandler -> { + val range: TextRange = action.getRange(caret) ?: return null + start = range.startOffset + end = range.endOffset + if (argument.isLinewiseMotion()) end-- } - // If we are a linewise motion we need to normalize the start and stop then move the start to the beginning - // of the line and move the end to the end of the line. - if (cmd.isLinewiseMotion()) { - if (caret.getBufferPosition().line != editor.lineCount() - 1) { - start = editor.getLineStartForOffset(start) - end = min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt() - } else { - start = editor.getLineStartForOffset(start) - end = editor.getLineEndForOffset(end) - } + else -> throw RuntimeException("Commands doesn't take " + action.javaClass.simpleName + " as an operator") + } + + // Normalize the range + if (start > end) { + val t = start + start = end + end = t + } + + // If we are a linewise motion we need to normalize the start and stop then move the start to the beginning + // of the line and move the end to the end of the line. + if (argument.isLinewiseMotion()) { + if (caret.getBufferPosition().line != editor.lineCount() - 1) { + start = editor.getLineStartForOffset(start) + end = min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt() + } else { + start = editor.getLineStartForOffset(start) + end = editor.getLineEndForOffset(end) } } @@ -383,13 +384,14 @@ abstract class VimMotionGroupBase : VimMotionGroup { val text = editor.text().subSequence(start, end).toString() val lastNewLine = text.lastIndexOf('\n') if (lastNewLine > 0) { - val id = argument.motion.action.id + val id = action.id if (id == "VimMotionWordRightAction" || id == "VimMotionBigWordRightAction" || id == "VimMotionCamelRightAction") { if (!editor.anyNonWhitespace(end, -1)) { end = start + lastNewLine } } } + return TextRange(start, end) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Argument.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Argument.kt index 89af74d904..b33def0540 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Argument.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Argument.kt @@ -8,51 +8,88 @@ package com.maddyhome.idea.vim.command -import com.maddyhome.idea.vim.api.ExecutionContext -import com.maddyhome.idea.vim.api.ImmutableVimCaret -import com.maddyhome.idea.vim.api.VimEditor -import com.maddyhome.idea.vim.group.visual.VimSelection -import com.maddyhome.idea.vim.handler.Motion +import com.maddyhome.idea.vim.handler.EditorActionHandlerBase +import com.maddyhome.idea.vim.handler.ExternalActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler -import java.util.* +import com.maddyhome.idea.vim.handler.TextObjectActionHandler +import com.maddyhome.idea.vim.state.mode.SelectionType /** - * This represents a command argument. - * TODO please make it a sealed class and not a giant collection of fields with default values, it's not safe + * Represents an argument to a command's action + * + * A [Command] is made up of an optional register and count, and an action. That action might be a simple command such + * as `i` to start Insert mode, or a motion `w` to move to the next word. Or it might require an argument, such as a + * character like in the motion `fx` or an ex-string in the command `d/foo`. Or it might be another action, representing + * a motion, such as `dw`. That motion argument's action might itself have an action (`dfx`). */ -class Argument private constructor( - val character: Char = 0.toChar(), - val motion: Command = EMPTY_COMMAND, - val offsets: Map = emptyMap(), - val string: String = "", - val processing: ((String) -> Unit)? = null, - val type: Type, -) { - constructor(motionArg: Command) : this(motion = motionArg, type = Type.MOTION) - constructor(charArg: Char) : this(character = charArg, type = Type.CHARACTER) - constructor(label: Char, strArg: String, processing: ((String) -> Unit)?) : this(character = label, string = strArg, processing = processing, type = Type.EX_STRING) - constructor(offsets: Map) : this(offsets = offsets, type = Type.OFFSETS) +sealed class Argument { + /** A simple character argument */ + class Character(val character: Char) : Argument() - enum class Type { - MOTION, CHARACTER, DIGRAPH, EX_STRING, OFFSETS + /** An argument representing the user's input from the Ex command line, typically a search string */ + class ExString(val label: Char, val string: String, val processing: ((String) -> Unit)?) : Argument() + + /** + * Represents an argument that is a motion. Used by operator commands + * + * A command is either an action (like `i`), a motion (like `w`) or an operator that takes a motion as an argument + * (like `dw`). A motion argument is a motion action handler with its own optional argument. The motion action handler + * could be a [MotionActionHandler] or [TextObjectActionHandler], or even the [ExternalActionHandler] that tracks the + * caret moves from an external action such as EasyMotion/AceJump. A motion might be a simple motion such as `w` to + * move a word, or require a character argument (`f`), or even an ex-string (`/foo`). + * + * Note that a motion argument does not have a count - that is owned by the fully built command. When executing the + * command, the count applies to the motion action, not the operator action. This just means the operator action + * does not use the count. (`3i` means insert the following typed text three times. But `3cw` means change the next + * three words with the following inserted text, rather than change the next word by inserting the following text + * three times.) + * + * @see Command + */ + class Motion private constructor(val motion: EditorActionHandlerBase, val argument: Argument? = null) : Argument() { + constructor(motion: MotionActionHandler, argument: Argument?) : this(motion as EditorActionHandlerBase, argument) + constructor(motion: TextObjectActionHandler) : this(motion as EditorActionHandlerBase) + constructor(motion: ExternalActionHandler) : this (motion as EditorActionHandlerBase) + + fun getMotionType() = if (isLinewiseMotion()) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE + + fun isLinewiseMotion(): Boolean { + return motion.let { + when (it) { + is TextObjectActionHandler -> it.visualType == TextObjectVisualType.LINE_WISE + is MotionActionHandler -> it.motionType == MotionType.LINE_WISE + is ExternalActionHandler -> it.isLinewiseMotion + else -> error("Command is not a motion: $motion") + } + } + } + + fun withArgument(argument: Argument) = Motion(motion, argument) } - companion object { - @JvmField - val EMPTY_COMMAND: Command = Command( - 0, - object : MotionActionHandler.SingleExecution() { - override fun getOffset( - editor: VimEditor, - context: ExecutionContext, - argument: Argument?, - operatorArguments: OperatorArguments, - ) = Motion.NoMotion - - override val motionType: MotionType = MotionType.EXCLUSIVE - }, - Command.Type.MOTION, - EnumSet.noneOf(CommandFlags::class.java), - ) + /** + * Represents the type of argument, or the type of an expected argument while entering a command + */ + enum class Type { + + /** + * A motion argument used to complete an operator, such as `dw` or `diw` + * + * A motion argument will often have its own argument, such as when deleting up to the next occurrence of a + * character, as in `dfx`. + */ + MOTION, + + /** A character argument, such as the character to move to with the `f` command. */ + CHARACTER, + + /** + * Used to represent an expected argument type rather than an actual argument type + * + * When building a command, an operator can say that it expects a digraph or literal argument, in which case the key + * handler will allow ``, `` and ``, and start the digraph state machine. The finished digraph is + * converted into a character, and a character argument is added to the operator action. + */ + DIGRAPH } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Command.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Command.kt index 610c804c89..652fca7af9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Command.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/Command.kt @@ -8,34 +8,47 @@ package com.maddyhome.idea.vim.command -import com.maddyhome.idea.vim.api.ExecutionContext -import com.maddyhome.idea.vim.api.VimCaret -import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.handler.EditorActionHandlerBase -import com.maddyhome.idea.vim.handler.MotionActionHandler -import com.maddyhome.idea.vim.handler.TextObjectActionHandler import java.util.* /** - * This represents a single Vim command to be executed (operator, motion, text object, etc.). It may optionally include - * an argument if appropriate for the command. The command has a count and a type. + * This represents a single Vim command to be executed (action, motion, operator+motion, v_textobject, etc.) + * + * A command is an action, with a type that determines how it is handled, such as [Type.MOTION], [Type.CHANGE], + * [Type.OTHER_SELF_SYNCHRONIZED], etc. It also exposes the action's [CommandFlags] which are also used to help execute + * the action. + * + * A command's action can require an argument, which can be either a character (e.g., `fx`) or the input from the Ex + * command line. It can also be a motion, in which case the command is an operator+motion, such as `dw`. The motion + * argument is an action that might also have an argument, such as `dfx` or `d/foo`. + * + * A command can optionally include a count and a register. More than one count can be entered, before an operator and + * then before the motion argument, e.g. `2d3w`. This is intuitively "delete the next three words, twice", which is the + * same as "delete the next six words". While both the operator and motion have a count while being built, the final + * command has a single count that is the product of all count components. In this example, the command would have a + * final count of `6`. + * + * Note that for a command that is an operator+motion command, the count applies to the motion, rather than the + * operator. For example, `3i` will insert the following typed text three times, while `3cw` will change the next three + * words with the following typed text, rather than changing the next word with the typed text three times. The command + * still has a single count, and to handle this, the operator action should ignore the count, while the motion action + * should use it when calculating the movement. + * + * As an additional interesting pathological edge case, it's possible to enter a count when selecting a register, and + * it's possible to select multiple registers while building a command; the last register wins. This means that + * `2"a3"b4"c5d6w` will delete 720 words and store the text in register `c`. + * + * @see OperatorArguments */ data class Command( - var rawCount: Int, - var action: EditorActionHandlerBase, + val register: Char?, + val rawCount: Int, + val action: EditorActionHandlerBase, + val argument: Argument?, val type: Type, - var flags: EnumSet, + val flags: EnumSet, ) { - constructor(rawCount: Int, register: Char) : this( - rawCount, - NonExecutableActionHandler, - Type.SELECT_REGISTER, - EnumSet.of(CommandFlags.FLAG_EXPECT_MORE), - ) { - this.register = register - } - init { action.process(this) } @@ -43,20 +56,7 @@ data class Command( val count: Int get() = rawCount.coerceAtLeast(1) - var argument: Argument? = null - var register: Char? = null - - fun isLinewiseMotion(): Boolean { - return when (action) { - is TextObjectActionHandler -> (action as TextObjectActionHandler).visualType == TextObjectVisualType.LINE_WISE - is MotionActionHandler -> (action as MotionActionHandler).motionType == MotionType.LINE_WISE - else -> error("Command is not a motion: $action") - } - } - - override fun toString(): String { - return "Action = ${action.id}" - } + override fun toString() = "Action = ${action.id}" enum class Type { /** @@ -85,10 +85,6 @@ data class Command( COPY, PASTE, - /** - * Represents commands that select the register. - */ - SELECT_REGISTER, OTHER_READONLY, OTHER_WRITABLE, @@ -112,18 +108,3 @@ data class Command( } } } - -private object NonExecutableActionHandler : EditorActionHandlerBase(false) { - override val type: Command.Type - get() = error("This action should not be executed") - - override fun baseExecute( - editor: VimEditor, - caret: VimCaret, - context: ExecutionContext, - cmd: Command, - operatorArguments: OperatorArguments, - ): Boolean { - error("This action should not be executed") - } -} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt index 2409ada7dc..1bf45cb41b 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt @@ -15,66 +15,78 @@ import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.handler.EditorActionHandlerBase +import com.maddyhome.idea.vim.handler.ExternalActionHandler +import com.maddyhome.idea.vim.handler.MotionActionHandler +import com.maddyhome.idea.vim.handler.TextObjectActionHandler +import com.maddyhome.idea.vim.helper.StrictMode +import com.maddyhome.idea.vim.helper.noneOfEnum +import com.maddyhome.idea.vim.key.CommandNode import com.maddyhome.idea.vim.key.CommandPartNode -import com.maddyhome.idea.vim.key.Node import com.maddyhome.idea.vim.key.RootNode import org.jetbrains.annotations.TestOnly import javax.swing.KeyStroke -class CommandBuilder( +class CommandBuilder private constructor( private var currentCommandPartNode: CommandPartNode, - private val commandParts: ArrayDeque, + private val counts: MutableList, private val keyList: MutableList, - initialUncommittedRawCount: Int, ) : Cloneable { - constructor( - currentCommandPartNode: CommandPartNode, - initialUncommittedRawCount: Int = 0 - ) : this( - currentCommandPartNode, - ArrayDeque(), - mutableListOf(), - initialUncommittedRawCount - ) + constructor(rootNode: RootNode, initialUncommittedRawCount: Int = 0) + : this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf()) - var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND + private var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND + private var selectedRegister: Char? = null + private var action: EditorActionHandlerBase? = null + private var argument: Argument? = null + private var fallbackArgumentType: Argument.Type? = null + + private val motionArgument + get() = argument as? Argument.Motion + + private var currentCount: Int + get() = counts.last() + set(value) { + counts[counts.size - 1] = value + } + + /** Provide the typed keys for `'showcmd'` */ + val keys: Iterable get() = keyList + + /** Returns true if the command builder is clean and ready to start building */ + val isEmpty + get() = commandState == CurrentCommandState.NEW_COMMAND + && selectedRegister == null + && counts.size == 1 + && action == null + && argument == null + && fallbackArgumentType == null + + /** Returns true if the command is ready to be built and executed */ + val isReady + get() = commandState == CurrentCommandState.READY /** - * The current uncommitted count for the currently in-progress command part + * Returns the current total count, as the product of all entered count components. The value is not coerced. * - * TODO: Investigate usages. This value cannot be trusted - * TODO: Rename to uncommittedRawCount + * This value is not reliable! Please use [Command.rawCount] or [Command.count] instead of this function. * - * This value is not coerced, and can be 0. + * This value is a snapshot of the count for a currently in-progress command, and should not be used for anything + * other than reporting on the state of the command. This value is likely to change as the user continues entering the + * command. There are very few expected uses of this value. Examples include calculating `'incsearch'` highlighting + * for an in-progress search command, or the `v:count` and `v:count1` variables used during an expression mapping. * - * There are very few reasons for using this value. It is incomplete (the user could type another digit), and there - * can be other committed command parts, such as operator and multiple register selections, each of which will can a - * count (e.g., `2"a3"b4"c5d6` waiting for a motion). The count is only final after [buildCommand], and then only via - * [Command.count] or [Command.rawCount]. + * The returned value is the product of all count components. In other words, given a command that is an + * operator+motion, both the operator and motion can have a count, such as `2d3w`, which means delete the next six + * words. Furthermore, Vim allows a count when selecting register, and it is valid to select register multiple times. + * E.g., `2"a3"b4"c5d6w` will delete the next 720 words and save the text to the register `c`. * - * The [aggregatedUncommittedCount] property can be used to get the current total count across all command parts, - * although this value is also not guaranteed to be final. + * The returned value is not coerced. If no count components are specified, the returned value is 0. If any components + * are specified, the value will naturally be greater than 0. */ - var count: Int = initialUncommittedRawCount - private set - - /** - * The current aggregated, but uncommitted count for all command parts in the command builder, coerced to 1 - * - * This value multiplies together the count for command parts currently committed, such as operator and multiple - * register selections, as well as the current uncommitted count for the next command part. E.g., `2"a3"b4"c5d6` will - * multiply each count together to get what would be the final count. All counts are coerced to at least 1 before - * multiplying, which means the result will also be at least 1. - * - * Note that there are very few uses for this value. The final value should be retrieved from [Command.count] or - * [Command.rawCount] after a call to [buildCommand]. This value is expected to be used for `'incsearch'` - * highlighting. - */ - val aggregatedUncommittedCount: Int - get() = (commandParts.map { it.count }.reduceOrNull { acc, i -> acc * i } ?: 1) * count.coerceAtLeast(1) - - val keys: Iterable get() = keyList + fun calculateCount0Snapshot(): Int { + return if (counts.all { it == 0 }) 0 else counts.map { it.coerceAtLeast(1) }.reduce { acc, i -> acc * i } + } // TODO: Try to remove this. We shouldn't be looking at the unbuilt command // This is used by the extension mapping handler, to select the current register before invoking the extension. We @@ -84,7 +96,18 @@ class CommandBuilder( // still change, if more keys are processed. E.g., it's perfectly valid to select register multiple times `"a"b`. // This doesn't cause any issues with existing extensions val register: Char? - get() = commandParts.lastOrNull { it.register != null }?.register + get() = selectedRegister + + // TODO: Try to remove this too. Also used by extension handling + fun hasCurrentCommandPartArgument() = motionArgument != null || argument != null + + // TODO: And remove this too. More extension special case code + // It's used by the Matchit extension to incorrectly reset the command builder. Extensions need a way to properly + // handle the command builder. I.e., they should act like expression mappings, which return keys to evaluate, or an + // empty string to leave state as it is - either way, it's an explicit choice. Currently, extensions mostly ignore it + fun resetCount() { + counts[counts.size - 1] = 0 + } /** * The argument type for the current in-progress command part's action @@ -92,79 +115,202 @@ class CommandBuilder( * For digraph arguments, this can fall back to [Argument.Type.CHARACTER] if there isn't a digraph match. */ val expectedArgumentType: Argument.Type? - get() = fallbackArgumentType ?: commandParts.lastOrNull()?.action?.argumentType + get() = fallbackArgumentType + ?: motionArgument?.let { return it.motion.argumentType } + ?: action?.argumentType - private var fallbackArgumentType: Argument.Type? = null + /** + * Returns true if the command builder is waiting for an argument + * + * The command builder might be waiting for the argument to a simple motion action such as `f`, waiting for a + * character to move to, or it might be waiting for the argument to a motion that is itself an argument to an operator + * argument. For example, the character argument to `f` in `df{character}`. + */ + val isAwaitingArgument: Boolean + get() = expectedArgumentType != null && (motionArgument?.let { it.argument == null } ?: (argument == null)) - val isReady: Boolean get() = commandState == CurrentCommandState.READY - val isEmpty: Boolean get() = commandParts.isEmpty() - val isAtDefaultState: Boolean get() = isEmpty && count == 0 && expectedArgumentType == null + fun fallbackToCharacterArgument() { + logger.trace("fallbackToCharacterArgument is executed") + // Finished handling DIGRAPH. We either succeeded, in which case handle the converted character, or failed to parse, + // in which case try to handle input as a character argument. + assert(expectedArgumentType == Argument.Type.DIGRAPH) { "Cannot move state from $expectedArgumentType to CHARACTER" } + fallbackArgumentType = Argument.Type.CHARACTER + } + + fun isAwaitingCharOrDigraphArgument(): Boolean { + val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH + logger.debug { "Awaiting char or digraph: $awaiting" } + return awaiting + } val isExpectingCount: Boolean get() { return commandState == CurrentCommandState.NEW_COMMAND && + !isRegisterPending && expectedArgumentType != Argument.Type.CHARACTER && expectedArgumentType != Argument.Type.DIGRAPH } - fun pushCommandPart(action: EditorActionHandlerBase) { - logger.trace { "pushCommandPart is executed. action = $action" } - commandParts.add(Command(count, action, action.type, action.flags)) - fallbackArgumentType = null - count = 0 + /** + * Returns true if the user has typed some count characters + * + * Used to know if `0` should be mapped or not. Vim allows "0" to be mapped, but not while entering a count. Also used + * to know if there are count characters available to delete. + */ + fun hasCountCharacters() = currentCount > 0 + + fun addCountCharacter(key: KeyStroke) { + currentCount = (currentCount * 10) + (key.keyChar - '0') + // If count overflows and flips negative, reset to 999999999L. In Vim, count is a long, which is *usually* 32 bits, + // so will flip at 2147483648. We store count as an Int, which is also 32 bit. + // See https://github.com/vim/vim/blob/b376ace1aeaa7614debc725487d75c8f756dd773/src/normal.c#L631 + if (currentCount < 0) { + currentCount = 999999999 + } + addKey(key) } - fun pushCommandPart(register: Char) { - logger.trace { "pushCommandPart is executed. register = $register" } - // We will never execute this command, but we need to push something to correctly handle counts on either side of a - // select register command part. e.g. 2"a2d2w or even crazier 2"a2"a2"a2"a2"a2d2w - commandParts.add(Command(count, register)) - fallbackArgumentType = null - count = 0 + fun deleteCountCharacter() { + currentCount /= 10 + keyList.removeAt(keyList.size - 1) } - fun fallbackToCharacterArgument() { - logger.trace { "fallbackToCharacterArgument is executed" } - // Finished handling DIGRAPH. We either succeeded, in which case handle the converted character, or failed to parse, - // in which case try to handle input as a character argument. - assert(expectedArgumentType == Argument.Type.DIGRAPH) { "Cannot move state from $expectedArgumentType to CHARACTER" } - fallbackArgumentType = Argument.Type.CHARACTER + var isRegisterPending: Boolean = false + private set + + fun startWaitingForRegister(key: KeyStroke) { + isRegisterPending = true + addKey(key) + } + + fun selectRegister(register: Char) { + logger.trace { "Selected register '$register'" } + selectedRegister = register + isRegisterPending = false + fallbackArgumentType = null + counts.add(0) } + /** + * Adds a keystroke to the command builder + * + * Only public use is when entering a digraph/literal, where each key isn't handled by [CommandBuilder], but should + * be added to the `'showcmd'` output. + */ fun addKey(key: KeyStroke) { - logger.trace { "added key to command builder" } + logger.trace { "added key to command builder: $key" } keyList.add(key) } - fun addCountCharacter(key: KeyStroke) { - count = (count * 10) + (key.keyChar - '0') - // If count overflows and flips negative, reset to 999999999L. In Vim, count is a long, which is *usually* 32 bits, - // so will flip at 2147483648. We store count as an Int, which is also 32 bit. - // See https://github.com/vim/vim/blob/b376ace1aeaa7614debc725487d75c8f756dd773/src/normal.c#L631 - if (count < 0) { - count = 999999999 + /** + * Add an action to the command + * + * This can be an action such as delete the current character - `x`, a motion like `w`, an operator like `d` or a + * motion that will be used as the argument of an operator - the `w` in `dw`. + */ + fun addAction(action: EditorActionHandlerBase) { + logger.trace { "addAction is executed. action = $action" } + + if (this.action == null) { + this.action = action + } + else { + StrictMode.assert(argument == null, "Command builder already has an action and a fully populated argument") + argument = when (action) { + is MotionActionHandler -> Argument.Motion(action, null) + is TextObjectActionHandler -> Argument.Motion(action) + is ExternalActionHandler -> Argument.Motion(action) + else -> throw RuntimeException("Unexpected action type: $action") + } } - addKey(key) - } - fun deleteCountCharacter() { - count /= 10 - keyList.removeAt(keyList.size - 1) + // Push a new count component, so we get an extra count for e.g. an operator's motion + counts.add(0) + fallbackArgumentType = null + + if (!isAwaitingArgument) { + logger.trace("Action does not require an argument. Setting command state to READY") + commandState = CurrentCommandState.READY + } } - fun setCurrentCommandPartNode(newNode: CommandPartNode) { - logger.trace { "setCurrentCommandPartNode is executed" } - currentCommandPartNode = newNode + /** + * Add an argument to the command + * + * This might be a simple character argument, such as `x` in `fx`, or an ex-string argument to a search motion, like + * `d/foo`. If the command is an operator+motion, the motion is both an action and an argument. While it is simpler + * to use [addAction], it will still work if the motion action can also be wrapped in an [Argument.Motion] and passed + * to [addArgument]. + */ + fun addArgument(argument: Argument) { + logger.trace("addArgument is executed") + + // If the command's action is an operator, the argument will be a motion, which might be waiting for its argument. + // If so, update the motion argument to include the given argument + this.argument = motionArgument?.withArgument(argument) ?: argument + + fallbackArgumentType = null + + if (!isAwaitingArgument) { + logger.trace("Argument is simple type, or motion with own argument. No further argument required. Setting command state to READY") + commandState = CurrentCommandState.READY + } } - fun getChildNode(key: KeyStroke): Node? { - return currentCommandPartNode[key] + /** + * Process a keystroke, matching an action if available + * + * If the given keystroke matches an action, the [processor] is invoked with the action instance. Typically, the + * caller will end up passing the action back to [addAction], but there are more housekeeping steps that stop us + * encapsulating it completely. + * + * If the given keystroke does not yet match an action, the internal state is updated to track the current command + * part node. + */ + fun processKey(key: KeyStroke, processor: (EditorActionHandlerBase) -> Unit): Boolean { + val node = currentCommandPartNode[key] + when (node) { + is CommandNode -> { + logger.trace { "Found full command node ($key) - ${node.debugString}" } + addKey(key) + processor(node.actionHolder.instance) + return true + } + is CommandPartNode -> { + logger.trace { "Found command part node ($key) - ${node.debugString}" } + currentCommandPartNode = node + addKey(key) + return true + } + } + + logger.trace { "No command/command part node found for key: $key" } + return false } - fun isAwaitingCharOrDigraphArgument(): Boolean { - val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH - logger.debug { "Awaiting char or digraph: $awaiting" } - return awaiting + /** + * Map a keystroke that duplicates an operator into the `_` "current line" motion + * + * Some commands like `dd` or `yy` or `cc` are treated as special cases by Vim. There is no `d`, `y` or `c` motion, + * so for convenience, Vim maps the repeated operator keystroke as meaning "operate on the current line", and replaces + * the second keystroke with the `_` motion. I.e. `dd` becomes `d_`, `yy` becomes `y_`, `cc` becomes `c_`, etc. + * + * @see DuplicableOperatorAction + */ + fun convertDuplicateOperatorKeyStrokeToMotion(key: KeyStroke): KeyStroke { + logger.trace { "convertDuplicateOperatorKeyStrokeToMotion is executed. key = $key" } + + // Simple check to ensure that we're in OP_PENDING. If we don't have an action, we don't have an operator. If we + // have an argument, we can't be in OP_PENDING + if (action != null && argument == null) { + (action as? DuplicableOperatorAction)?.let { + logger.trace { "action = $action" } + if (it.duplicateWith == key.keyChar) { + return KeyStroke.getKeyStroke('_') + } + } + } + return key } fun isBuildingMultiKeyCommand(): Boolean { @@ -178,67 +324,47 @@ class CommandBuilder( return isMultikey } - fun isDone(): Boolean { - return commandParts.isEmpty() - } - - fun completeCommandPart(argument: Argument) { - logger.trace { "completeCommandPart is executed" } - commandParts.last().argument = argument - commandState = CurrentCommandState.READY - } - - fun isDuplicateOperatorKeyStroke(key: KeyStroke): Boolean { - logger.trace { "entered isDuplicateOperatorKeyStroke" } - val action = commandParts.last().action as? DuplicableOperatorAction - logger.trace { "action = $action" } - return action?.duplicateWith == key.keyChar - } - - fun hasCurrentCommandPartArgument(): Boolean { - return commandParts.lastOrNull()?.argument != null - } - + /** + * Build the command with the current counts, register, actions and arguments + * + * The command builder is reset after the command is built. + */ fun buildCommand(): Command { - var command: Command = commandParts.removeFirst() - while (commandParts.size > 0) { - val next = commandParts.removeFirst() - next.rawCount = if (command.rawCount == 0 && next.rawCount == 0) 0 else command.count * next.count - command.rawCount = 0 - if (command.type == Command.Type.SELECT_REGISTER) { - next.register = command.register - command.register = null - command = next - } else { - command.argument = Argument(next) - assert(commandParts.size == 0) - } - } - fallbackArgumentType = null + val rawCount = calculateCount0Snapshot() + val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum()) + resetAll(currentCommandPartNode.root as RootNode) return command } - fun resetAll(commandPartNode: CommandPartNode) { - logger.trace { "resetAll is executed" } - resetInProgressCommandPart(commandPartNode) + fun resetAll(rootNode: RootNode) { + logger.trace("resetAll is executed") + currentCommandPartNode = rootNode commandState = CurrentCommandState.NEW_COMMAND - commandParts.clear() + counts.clear() + counts.add(0) + isRegisterPending = false + selectedRegister = null + action = null + argument = null keyList.clear() fallbackArgumentType = null } - fun resetCount() { - count = 0 - } - - fun resetInProgressCommandPart(commandPartNode: CommandPartNode) { - logger.trace { "resetInProgressCommandPart is executed" } - count = 0 - setCurrentCommandPartNode(commandPartNode) + /** + * Change the command trie root node used to find commands for the current mode + * + * Typically, we reset the command trie root node after a command is executed, using the root node of the current + * mode - this is handled by [resetAll]. This function allows us to change the root node without executing a command + * or fully resetting the command builder, such as when switching to Op-pending while entering an operator+motion. + */ + fun resetCommandTrieRootNode(rootNode: RootNode) { + logger.trace("resetCommandTrieRootNode is executed") + currentCommandPartNode = rootNode } @TestOnly fun getCurrentTrie(): CommandPartNode = currentCommandPartNode + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -246,10 +372,12 @@ class CommandBuilder( other as CommandBuilder if (currentCommandPartNode != other.currentCommandPartNode) return false - if (commandParts != other.commandParts) return false + if (counts != other.counts) return false + if (selectedRegister != other.selectedRegister) return false + if (action != other.action) return false + if (argument != other.argument) return false if (keyList != other.keyList) return false if (commandState != other.commandState) return false - if (count != other.count) return false if (expectedArgumentType != other.expectedArgumentType) return false if (fallbackArgumentType != other.fallbackArgumentType) return false @@ -258,24 +386,38 @@ class CommandBuilder( override fun hashCode(): Int { var result = currentCommandPartNode.hashCode() - result = 31 * result + commandParts.hashCode() + result = 31 * result + counts.hashCode() + result = 31 * result + selectedRegister.hashCode() + result = 31 * result + action.hashCode() + result = 31 * result + argument.hashCode() result = 31 * result + keyList.hashCode() result = 31 * result + commandState.hashCode() - result = 31 * result + count - result = 31 * result + (expectedArgumentType?.hashCode() ?: 0) - result = 31 * result + (fallbackArgumentType?.hashCode() ?: 0) + result = 31 * result + expectedArgumentType.hashCode() + result = 31 * result + fallbackArgumentType.hashCode() return result } public override fun clone(): CommandBuilder { - val result = CommandBuilder(currentCommandPartNode, ArrayDeque(commandParts), keyList.toMutableList(), count) + val result = CommandBuilder( + currentCommandPartNode, + counts.toMutableList(), + keyList.toMutableList() + ) + result.selectedRegister = selectedRegister + result.action = action + result.argument = argument result.commandState = commandState result.fallbackArgumentType = fallbackArgumentType return result } override fun toString(): String { - return "Command state = $commandState, key list = ${ injector.parser.toKeyNotation(keyList) }, command parts = ${ commandParts }, count = $count\n" + + return "Command state = $commandState, " + + "key list = ${ injector.parser.toKeyNotation(keyList) }, " + + "selected register = $selectedRegister, " + + "counts = $counts, " + + "action = $action, " + + "argument = $argument, " + "command part node - $currentCommandPartNode" } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandFlags.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandFlags.kt index b9aed1a331..3f5e7c741c 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandFlags.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandFlags.kt @@ -68,11 +68,6 @@ enum class CommandFlags { */ FLAG_EXPECT_MORE, - /** - * Indicate that the character argument may come from a digraph - */ - FLAG_ALLOW_DIGRAPH, - FLAG_START_EX, FLAG_END_EX, diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt index a5f4a426fa..0c95470936 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt @@ -42,12 +42,12 @@ object MappingProcessor: KeyConsumer { val keyState = keyProcessResultBuilder.state val mappingState = keyState.mappingState val commandBuilder = keyState.commandBuilder - if (commandBuilder.isAwaitingCharOrDigraphArgument() || - commandBuilder.isBuildingMultiKeyCommand() || - isMappingDisabledForKey(key, keyState) || - injector.vimState.isRegisterPending + if (commandBuilder.isAwaitingCharOrDigraphArgument() + || commandBuilder.isBuildingMultiKeyCommand() + || commandBuilder.isRegisterPending + || isMappingDisabledForKey(key, keyState) ) { - log.debug("Finish key processing, returning false") + log.debug("Mapping not applicable. Finish key processing, returning false") return false } mappingState.stopMappingTimer() @@ -74,7 +74,7 @@ object MappingProcessor: KeyConsumer { // "0" can be mapped, but the mapping isn't applied when entering a count. Other digits are always mapped, even when // entering a count. // See `:help :map-modes` - val isMappingDisabled = key.keyChar == '0' && keyState.commandBuilder.count > 0 + val isMappingDisabled = key.keyChar == '0' && keyState.commandBuilder.hasCountCharacters() log.debug { "Mapping disabled for key: $isMappingDisabled" } return isMappingDisabled } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/OperatorArguments.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/OperatorArguments.kt index 76c2595e35..6d04f1b974 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/OperatorArguments.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/OperatorArguments.kt @@ -8,21 +8,53 @@ package com.maddyhome.idea.vim.command +import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.state.mode.Mode /** - * [count0] is a raw count entered by user. May be zero. - * [count1] is the same count, but 1-based. If [count0] is zero, [count1] is one. - * The terminology is taken directly from vim. - * If no count is provided, [count0] defaults to zero. + * Represents arguments used when executing a command - either an action, operator or motion + * + * TODO: Remove, rename or otherwise refactor this class + * + * Problems with this class: + * * The name is misleading, as it is used when executing motions that do not have an operator, as well as when + * executing the operator itself. Or even when executing actions that are neither operators nor motions + * * It is not clear if this represents the arguments to an operator (while the operator's [Command] also has arguments) + * * [mode] is the mode _before_ the command is completed, which is not guaranteed to be the same as the mode once the + * command completes. There is no indication of this difference, which could lead to confusion + * * The count is (correctly) the count for the whole command, rather than the operator, or the operator's arguments + * (the in-progress motion) + * + * @param isOperatorPending Deprecated. The value is used to indicate that a command is operator+motion and was + * previously used to change the behaviour of the motion (the EOL character is counted in this scenario - see + * `:help whichwrap`). It is better to register a separate action for [Mode.OP_PENDING] rather than expect a runtime + * flag for something that can be handled statically. + * @param count0 The raw count of the entire command. E.g., if the command is `2d3w`, then this count will be `6`, even + * when this class is passed to the `d` operator action (the count applies to the motion). + * @param mode Deprecated. The mode of the editor at the time that the [OperatorArguments] is created, which is _before_ + * the command is completed. This was previously used to check for [Mode.OP_PENDING], but is no longer required. Prefer + * [VimEditor.mode] instead. */ -data class OperatorArguments( - val isOperatorPending: Boolean, - val count0: Int, +data class OperatorArguments - val mode: Mode, +@Deprecated( + "Use overload without isOperatorPending. Value can be calculated from mode", + replaceWith = ReplaceWith("OperatorArguments(count0, mode)"), +) constructor( + // This is used by EasyMotion + @Deprecated("It is better to register a separate OP_PENDING action than switch on a runtime flag") val isOperatorPending: Boolean, + val count0: Int, + @Deprecated("Represents the mode when the OperatorArguments was created, not the current mode. Prefer editor.mode") val mode: Mode, ) { - val count1: Int = count0.coerceAtLeast(1) - fun withCount0(count0: Int): OperatorArguments = this.copy(count0 = count0) + /** + * Create a new instance of [OperatorArguments] + * + * @param count0 The 0-based count for the whole command + * @param mode Only used for the deprecated [OperatorArguments.mode] property + */ + @Suppress("DEPRECATION") + constructor(count0: Int, mode: Mode) : this(mode is Mode.OP_PENDING, count0, mode) + + val count1: Int = count0.coerceAtLeast(1) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/CurrentCommandState.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/CurrentCommandState.kt index ca7d152036..5895345385 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/CurrentCommandState.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/CurrentCommandState.kt @@ -11,5 +11,4 @@ package com.maddyhome.idea.vim.common enum class CurrentCommandState { NEW_COMMAND, READY, - BAD_COMMAND, } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimRange.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimRange.kt deleted file mode 100644 index c86082969f..0000000000 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimRange.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2003-2023 The IdeaVim authors - * - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE.txt file or at - * https://opensource.org/licenses/MIT. - */ - -package com.maddyhome.idea.vim.common - -import com.maddyhome.idea.vim.api.LineDeleteShift - -sealed class OperatedRange { - class Lines( - val text: CharSequence, - val lineAbove: Int, - val linesOperated: Int, - val shiftType: LineDeleteShift, - ) : OperatedRange() - - class Characters(val text: CharSequence, val leftOffset: Int, val rightOffset: Int) : OperatedRange() - class Block : OperatedRange() { - init { - TODO() - } - } -} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/ExternalActionHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/ExternalActionHandler.kt new file mode 100644 index 0000000000..b70e4e4a05 --- /dev/null +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/ExternalActionHandler.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2003-2024 The IdeaVim authors + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE.txt file or at + * https://opensource.org/licenses/MIT. + */ + +package com.maddyhome.idea.vim.handler + +import com.maddyhome.idea.vim.api.ExecutionContext +import com.maddyhome.idea.vim.api.ImmutableVimCaret +import com.maddyhome.idea.vim.api.VimCaret +import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandBuilder +import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.group.visual.VimSelection + +/** + * Represents an action that has already been performed, externally to IdeaVim + * + * This class is used to allow IdeaVim to work with IDE actions that move the caret(s) externally to the IdeaVim action + * system. It supports for extensions such as IdeaVim-EasyMotion, which provides commands and mappings for the AceJump + * IntelliJ plugin. Simple mappings to AceJump IDE actions would move the caret(s), but wouldn't integrate with IdeaVim + * operators, such as `d` for delete or extend the Visual selection. + * + * It will track the start and end offsets of all carets, and acts very much like [TextObjectActionHandler], providing + * a range for each caret that can be used by an operator action, or to extend selection. + * + * In more detail: IdeaVim will track the caret start locations before invoking the IdeaVim-EasyMotion extension mapping + * handlers. The invoked handler will use the AceJump API to register a callback for when the movement is complete, and + * then asks AceJump to move the caret. Once complete, the handler notifies IdeaVim, which calculates the end locations. + * The start/end ranges are wrapped in this action handler and pushed to the [CommandBuilder], which completes and then + * executes the command. Just like a text object handler, an operator will get the range for a caret, and act on it. + */ +class ExternalActionHandler(private val ranges: Map) : EditorActionHandlerBase(true) { + override val type: Command.Type = Command.Type.MOTION + + // If all we have are ranges, then almost by definition, this is character-wise motion. However, the ranges might be + // complete lines... + val isLinewiseMotion = false + + override fun baseExecute( + editor: VimEditor, + caret: VimCaret, + context: ExecutionContext, + cmd: Command, + operatorArguments: OperatorArguments, + ): Boolean { + + // This would normally act like a simple motion, but that's already been handled by the external action. No need to + // do anything. + return true + } + + fun getRange(caret: ImmutableVimCaret) = ranges[caret]?.toVimTextRange() +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt index 5ebad12c76..ca7933626b 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt @@ -13,7 +13,6 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimCaretListener import com.maddyhome.idea.vim.api.VimEditor -import com.maddyhome.idea.vim.api.getOffset import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.normalizeOffset import com.maddyhome.idea.vim.command.Argument diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/impl/state/VimStateMachineImpl.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/impl/state/VimStateMachineImpl.kt index 1734616892..193a1e5277 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/impl/state/VimStateMachineImpl.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/impl/state/VimStateMachineImpl.kt @@ -22,7 +22,6 @@ import java.util.* class VimStateMachineImpl : VimStateMachine { override var mode: Mode = Mode.NORMAL() override var isDotRepeatInProgress: Boolean = false - override var isRegisterPending: Boolean = false override var isReplaceCharacter: Boolean = false /** @@ -41,16 +40,9 @@ class VimStateMachineImpl : VimStateMachine { get() = executingCommand?.flags ?: noneOfEnum() - override fun resetRegisterPending() { - if (isRegisterPending) { - isRegisterPending = false - } - } - override fun reset() { mode = Mode.NORMAL() isDotRepeatInProgress = false - isRegisterPending = false isReplaceCharacter = false executingCommand = null } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/MappingInfo.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/MappingInfo.kt index 4b97516cdb..105d912210 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/MappingInfo.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/MappingInfo.kt @@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ImmutableVimCaret import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector -import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.argumentCaptured import com.maddyhome.idea.vim.diagnostic.trace @@ -23,6 +22,7 @@ import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection.Companion.create +import com.maddyhome.idea.vim.handler.ExternalActionHandler import com.maddyhome.idea.vim.helper.VimNlsSafe import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.mode.Mode @@ -147,12 +147,10 @@ class ToHandlerMappingInfo( override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { LOG.debug("Executing 'ToHandler' mapping info...") - val keyHandler = KeyHandler.getInstance() // Cache isOperatorPending in case the extension changes the mode while moving the caret // See CommonExtensionTest - // TODO: Is this legal? Should we assert in this case? - val shouldCalculateOffsets: Boolean = keyHandler.isOperatorPending(editor.mode, keyState) + val shouldCalculateOffsets: Boolean = editor.mode is Mode.OP_PENDING val startOffsets: Map = editor.carets().associateWith { it.offset } @@ -180,7 +178,7 @@ class ToHandlerMappingInfo( } } - val operatorArguments = OperatorArguments(keyHandler.isOperatorPending(editor.mode, keyState), keyState.commandBuilder.count, editor.mode) + val operatorArguments = OperatorArguments(keyState.commandBuilder.calculateCount0Snapshot(), editor.mode) val register = keyState.commandBuilder.register if (register != null) { injector.registerGroup.selectRegister(register) @@ -238,7 +236,7 @@ class ToHandlerMappingInfo( } } if (offsets.isNotEmpty()) { - keyState.commandBuilder.completeCommandPart(Argument(offsets)) + keyState.commandBuilder.addAction(ExternalActionHandler(offsets)) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt index 16a64f3adf..1c01bf2439 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt @@ -40,17 +40,36 @@ import javax.swing.KeyStroke * and the user should complete the sequence, it's [CommandPartNode] */ @Suppress("GrazieInspection") -interface Node +interface Node { + val debugString: String + val parent: Node? + + val root: Node + get() = parent?.root ?: this +} /** Represents a complete command */ -data class CommandNode(val actionHolder: T) : Node { - override fun toString(): String { - return "COMMAND NODE (${ actionHolder.toString() })" - } +data class CommandNode(override val parent: Node, val actionHolder: T, private val name: String) : Node { + override val debugString: String + get() = toString() + + override fun toString() = "COMMAND NODE ($name - ${actionHolder.toString()})" } /** Represents a part of the command */ -open class CommandPartNode : Node, HashMap>() { +open class CommandPartNode( + override val parent: Node?, + internal val name: String, + internal val depth: Int) : Node { + + val children = mutableMapOf>() + + operator fun set(stroke: KeyStroke, node: Node) { + children[stroke] = node + } + + operator fun get(stroke: KeyStroke): Node? = children[stroke] + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -58,21 +77,32 @@ open class CommandPartNode : Node, HashMap>() { return true } - override fun hashCode(): Int { - return super.hashCode() - } + override fun hashCode() = super.hashCode() - override fun toString(): String { - return """ - COMMAND PART NODE( - ${entries.joinToString(separator = "\n") { " " + injector.parser.toKeyNotation(it.key) + " - " + it.value }} - ) - """.trimIndent() - } + override fun toString() = "COMMAND PART NODE ($name - ${children.size} children)" + + override val debugString + get() = buildString { + append("COMMAND PART NODE(") + appendLine(name) + children.entries.forEach { + repeat(depth + 1) { append(" ") } + append(injector.parser.toKeyNotation(it.key)) + append(" - ") + appendLine(it.value.debugString) + } + repeat(depth) { append(" ") } + append(")") + } } /** Represents a root node for the mode */ -class RootNode : CommandPartNode() +class RootNode(name: String) : CommandPartNode(null, name, 0) { + override val debugString: String + get() = "ROOT NODE ($name)\n" + super.debugString + + override fun toString() = "ROOT NODE ($name - ${children.size} children)" +} fun Node.addLeafs(keyStrokes: List, actionHolder: T) { var node: Node = this @@ -90,7 +120,16 @@ private fun addNode(base: CommandPartNode, actionHolder: T, key: KeyStrok val existing = base[key] if (existing != null) return existing - val newNode: Node = if (isLastInSequence) CommandNode(actionHolder) else CommandPartNode() + val childName = injector.parser.toKeyNotation(key) + val name = when (base) { + is RootNode -> base.name + "_" + childName + else -> base.name + childName + } + val newNode: Node = if (isLastInSequence) { + CommandNode(base, actionHolder, name) + } else { + CommandPartNode(base, name, base.depth + 1) + } base[key] = newNode return newNode } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CharArgumentConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CharArgumentConsumer.kt index 4a2aab70d9..6919c74871 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CharArgumentConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CharArgumentConsumer.kt @@ -62,7 +62,7 @@ class CharArgumentConsumer: KeyConsumer { processBuilder.addExecutionStep { _, lambdaEditor, _ -> // Create the character argument, add it to the current command, and signal we are ready to process the command logger.trace("Add character argument to the current command") - commandBuilder.completeCommandPart(Argument(mutableChKey)) + commandBuilder.addArgument(Argument.Character(mutableChKey)) lambdaEditor.isReplaceCharacter = false } } else { @@ -73,4 +73,4 @@ class CharArgumentConsumer: KeyConsumer { } } } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt index 4eb6250000..b35246e348 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt @@ -10,22 +10,17 @@ package com.maddyhome.idea.vim.key.consumers import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyProcessResult -import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandFlags -import com.maddyhome.idea.vim.common.CurrentCommandState import com.maddyhome.idea.vim.common.argumentCaptured import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.handler.EditorActionHandlerBase -import com.maddyhome.idea.vim.key.CommandNode -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyConsumer -import com.maddyhome.idea.vim.key.Node import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.VimStateMachine import com.maddyhome.idea.vim.state.mode.Mode @@ -44,60 +39,25 @@ class CommandConsumer : KeyConsumer { mappingCompleted: Boolean, keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder, ): Boolean { - logger.trace { "Entered CommandConsumer" } + logger.trace("Entered CommandConsumer") val commandBuilder = keyProcessResultBuilder.state.commandBuilder - // Ask the key/action tree if this is an appropriate key at this point in the command and if so, - // return the node matching this keystroke - logger.trace("Getting the node for the current mode") - logger.trace("command builder - $commandBuilder") - val node: Node? = mapOpCommand(key, commandBuilder.getChildNode(key), editor.mode, keyProcessResultBuilder.state) - logger.trace("node: $node") - - when (node) { - is CommandNode -> { - logger.trace("Node is a command node") - handleCommandNode(node, keyProcessResultBuilder) - keyProcessResultBuilder.addExecutionStep { lambdaKeyState, _, _ -> lambdaKeyState.commandBuilder.addKey(key) } - return true - } - is CommandPartNode -> { - logger.trace("Node is a command part node") - commandBuilder.setCurrentCommandPartNode(node) - commandBuilder.addKey(key) - return true - } + logger.trace { "command builder - $commandBuilder" } - else -> { - return false - } - } - } + // Map duplicate operator keystrokes. E.g., given `dd`, there is no `d` motion, so the second keystroke is mapped to + // `_`. Same with `cc` and `yy`, etc. + val keystroke = + if (editor.mode is Mode.OP_PENDING) commandBuilder.convertDuplicateOperatorKeyStrokeToMotion(key) else key + logger.trace { "Original keystroke: $key, substituted keystroke: $keystroke" } - /** - * See the description for [com.maddyhome.idea.vim.command.DuplicableOperatorAction] - */ - private fun mapOpCommand( - key: KeyStroke, - node: Node?, - mode: Mode, - keyState: KeyHandlerState, - ): Node? { - logger.trace("entered mapOpCommand. key = ${ injector.parser.toKeyNotation(key) }, node = $node, mode = $mode") - return if (KeyHandler.getInstance().isDuplicateOperatorKeyStroke(key, mode, keyState)) { - logger.trace("it is a duplicate operator key stroke") - keyState.commandBuilder.getChildNode(KeyStroke.getKeyStroke('_')) - } else { - node - } + return commandBuilder.processKey(keystroke) { handleAction(it, keyProcessResultBuilder) } } - private fun handleCommandNode(node: CommandNode, processBuilder: KeyProcessResult.KeyProcessResultBuilder) { - logger.trace("Handle command node") - // The user entered a valid command. Create the command and add it to the stack. - val action = node.actionHolder.instance + private fun handleAction(action: EditorActionHandlerBase, processBuilder: KeyProcessResult.KeyProcessResultBuilder) { val keyState = processBuilder.state + logger.trace { "Handle command action: ${action.id}" } + if (action.flags.contains(CommandFlags.FLAG_START_EX)) { keyState.enterCommandLine() injector.redrawService.redrawStatusLine() @@ -109,7 +69,6 @@ class CommandConsumer : KeyConsumer { val commandBuilder = keyState.commandBuilder val expectedArgumentType = commandBuilder.expectedArgumentType - commandBuilder.pushCommandPart(action) if (!checkArgumentCompatibility(expectedArgumentType, action)) { logger.trace("Return from command node handling") processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ -> @@ -117,10 +76,9 @@ class CommandConsumer : KeyConsumer { } return } - if (action.argumentType == null) { - logger.trace("Set command state to READY") - commandBuilder.commandState = CurrentCommandState.READY - } else { + + commandBuilder.addAction(action) + if (commandBuilder.isAwaitingArgument) { processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext -> logger.trace("Set waiting for the argument") val argumentType = action.argumentType @@ -138,7 +96,7 @@ class CommandConsumer : KeyConsumer { val processing = commandLine.inputProcessing commandLine.close(refocusOwningEditor = true, resetCaret = true) - commandBuilder.completeCommandPart(Argument(label[0], text, processing)) + commandBuilder.addArgument(Argument.ExString(label[0], text, processing)) } } } @@ -154,7 +112,7 @@ class CommandConsumer : KeyConsumer { val commandBuilder = keyState.commandBuilder if (argument == Argument.Type.MOTION) { if (editorState.isDotRepeatInProgress && argumentCaptured != null) { - commandBuilder.completeCommandPart(argumentCaptured!!) + commandBuilder.addArgument(argumentCaptured!!) } editor.mode = Mode.OP_PENDING(editorState.mode.returnTo) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandCountConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandCountConsumer.kt index c868dc3434..15fd2c548c 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandCountConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandCountConsumer.kt @@ -8,7 +8,6 @@ package com.maddyhome.idea.vim.key.consumers -import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyProcessResult import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector @@ -34,27 +33,25 @@ class CommandCountConsumer : KeyConsumer { ): Boolean { logger.trace { "Entered CommandCountConsumer" } val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar - if (!isCommandCountKey(chKey, keyProcessResultBuilder.state, editor)) return false + if (!isCommandCountKey(chKey, keyProcessResultBuilder.state)) return false keyProcessResultBuilder.state.commandBuilder.addCountCharacter(key) return true } - private fun isCommandCountKey(chKey: Char, keyState: KeyHandlerState, editor: VimEditor): Boolean { - // Make sure to avoid handling '0' as the start of a count. + private fun isCommandCountKey(chKey: Char, keyState: KeyHandlerState): Boolean { val editorState = injector.vimState val commandBuilder = keyState.commandBuilder - val notRegisterPendingCommand = editorState.mode is Mode.NORMAL && !editorState.isRegisterPending - val visualMode = editorState.mode is Mode.VISUAL && !editorState.isRegisterPending - val opPendingMode = editorState.mode is Mode.OP_PENDING - - if (notRegisterPendingCommand || visualMode || opPendingMode) { - if (commandBuilder.isExpectingCount && Character.isDigit(chKey) && (commandBuilder.count > 0 || chKey != '0')) { - logger.debug("This is a command key count") - return true - } + + // Make sure to avoid handling '0' as the start of a count. + if (Character.isDigit(chKey) && !(chKey == '0' && !commandBuilder.hasCountCharacters()) + && (editorState.mode is Mode.NORMAL || editorState.mode is Mode.VISUAL || editorState.mode is Mode.OP_PENDING) + && commandBuilder.isExpectingCount) { + logger.debug("This is a command count key") + return true } - logger.debug("This is NOT a command key count") + + logger.debug("This is NOT a command count key") return false } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DeleteCommandConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DeleteCommandConsumer.kt index 2b34d7793c..097ebf5b08 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DeleteCommandConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DeleteCommandConsumer.kt @@ -8,10 +8,8 @@ package com.maddyhome.idea.vim.key.consumers -import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyProcessResult import com.maddyhome.idea.vim.api.VimEditor -import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.key.KeyConsumer @@ -40,12 +38,19 @@ class DeleteCommandConsumer : KeyConsumer { private fun isDeleteCommandCountKey(key: KeyStroke, keyState: KeyHandlerState, mode: Mode): Boolean { // See `:help N` + if (key.keyCode != KeyEvent.VK_DELETE) { + logger.debug("Not a delete key") + return false + } + val commandBuilder = keyState.commandBuilder - val isDeleteCommandKeyCount = - (mode is Mode.NORMAL || mode is Mode.VISUAL || mode is Mode.OP_PENDING) && - commandBuilder.isExpectingCount && commandBuilder.count > 0 && key.keyCode == KeyEvent.VK_DELETE + if ((mode is Mode.NORMAL || mode is Mode.VISUAL || mode is Mode.OP_PENDING) + && commandBuilder.isExpectingCount && commandBuilder.hasCountCharacters()) { + logger.debug("Deleting a count character") + return true + } - logger.debug { "This is a delete command key count: $isDeleteCommandKeyCount" } - return isDeleteCommandKeyCount + logger.debug("Not a delete key") + return false } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt index a8397a81c2..d87b81a1cd 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt @@ -57,7 +57,7 @@ class EditorResetConsumer : KeyConsumer { if (commandBuilder.isAwaitingCharOrDigraphArgument()) { editor.isReplaceCharacter = false } - if (commandBuilder.isAtDefaultState) { + if (commandBuilder.isEmpty) { val register = injector.registerGroup if (register.currentRegister == register.defaultRegister) { var indicateError = true @@ -78,4 +78,4 @@ class EditorResetConsumer : KeyConsumer { } KeyHandler.getInstance().reset(keyState, editor.mode) } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt index 4830f98d6f..34fe4c4f87 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt @@ -31,22 +31,22 @@ class RegisterConsumer : KeyConsumer { keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder, ): Boolean { logger.trace { "Entered RegisterConsumer" } - if (!injector.vimState.isRegisterPending) return false + val commandBuilder = keyProcessResultBuilder.state.commandBuilder + if (!commandBuilder.isRegisterPending) return false logger.trace("Pending mode.") - keyProcessResultBuilder.state.commandBuilder.addKey(key) + commandBuilder.addKey(key) val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar - handleSelectRegister(editor, chKey, keyProcessResultBuilder) + handleSelectRegister(chKey, keyProcessResultBuilder) return true } - private fun handleSelectRegister(editor: VimEditor, chKey: Char, processBuilder: KeyProcessResult.KeyProcessResultBuilder) { + private fun handleSelectRegister(chKey: Char, processBuilder: KeyProcessResult.KeyProcessResultBuilder) { logger.trace("Handle select register") - injector.vimState.resetRegisterPending() if (injector.registerGroup.isValid(chKey)) { logger.trace("Valid register") - processBuilder.state.commandBuilder.pushCommandPart(chKey) + processBuilder.state.commandBuilder.selectRegister(chKey) } else { processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ -> logger.trace("Invalid register, set command state to BAD_COMMAND") @@ -54,4 +54,4 @@ class RegisterConsumer : KeyConsumer { } } } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/SelectRegisterConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/SelectRegisterConsumer.kt index 2d19b6f701..60f96e4864 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/SelectRegisterConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/SelectRegisterConsumer.kt @@ -8,7 +8,6 @@ package com.maddyhome.idea.vim.key.consumers -import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyProcessResult import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector @@ -36,9 +35,8 @@ class SelectRegisterConsumer : KeyConsumer { if (!isSelectRegister(key, state)) return false logger.trace("Select register") - state.commandBuilder.addKey(key) keyProcessResultBuilder.addExecutionStep { _, lambdaEditor, _ -> - injector.vimState.isRegisterPending = true + state.commandBuilder.startWaitingForRegister(key) } return true } @@ -48,10 +46,6 @@ class SelectRegisterConsumer : KeyConsumer { if (vimState.mode !is Mode.NORMAL && vimState.mode !is Mode.VISUAL) { return false } - return if (vimState.isRegisterPending) { - true - } else { - key.keyChar == '"' && !KeyHandler.getInstance().isOperatorPending(vimState.mode, keyState) && keyState.commandBuilder.expectedArgumentType == null - } + return keyState.commandBuilder.isRegisterPending || key.keyChar == '"' } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt index a7fd25b03c..bc2f0139be 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt @@ -11,7 +11,6 @@ package com.maddyhome.idea.vim.put import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimEditor -import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.helper.RWLockLabel @@ -34,7 +33,6 @@ interface VimPut { editor: VimEditor, context: ExecutionContext, data: PutData, - operatorArguments: OperatorArguments, updateVisualMarks: Boolean = false, modifyRegister: Boolean = true, ): Boolean diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt index 6333b22a55..23debc3695 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt @@ -23,7 +23,6 @@ import com.maddyhome.idea.vim.api.lineLength import com.maddyhome.idea.vim.api.moveToMotion import com.maddyhome.idea.vim.api.setChangeMarks import com.maddyhome.idea.vim.api.setVisualSelectionMarks -import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.diagnostic.VimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger @@ -46,17 +45,16 @@ abstract class VimPutBase : VimPut { editor: VimEditor, context: ExecutionContext, data: PutData, - operatorArguments: OperatorArguments, updateVisualMarks: Boolean, saveToRegister: Boolean, ): Boolean { val additionalData = collectPreModificationData(editor, data) - deleteSelectedText(editor, data, operatorArguments, saveToRegister) + deleteSelectedText(editor, data, saveToRegister) val processedText = processText(null, data) ?: return false putTextAndSetCaretPosition(editor, context, processedText, data, additionalData) if (updateVisualMarks) { - wrapInsertedTextWithVisualMarks(editor.currentCaret(), data, processedText) + wrapInsertedTextWithVisualMarks(editor.currentCaret(), data) } return true @@ -77,15 +75,11 @@ abstract class VimPutBase : VimPut { } } - private fun wasTextInsertedLineWise(text: ProcessedTextData): Boolean { - return text.typeInRegister == SelectionType.LINE_WISE - } - /** * see ":h gv": * After using "p" or "P" in Visual mode the text that was put will be selected */ - private fun wrapInsertedTextWithVisualMarks(caret: VimCaret, data: PutData, text: ProcessedTextData) { + private fun wrapInsertedTextWithVisualMarks(caret: VimCaret, data: PutData) { val textLength: Int = data.textData?.rawText?.length ?: return val caretsAndSelections = data.visualSelection?.caretsAndSelections ?: return val selection = caretsAndSelections[caret] ?: caretsAndSelections.entries.firstOrNull()?.value ?: return @@ -98,7 +92,7 @@ abstract class VimPutBase : VimPut { } @RWLockLabel.SelfSynchronized - private fun deleteSelectedText(editor: VimEditor, caret: VimCaret, data: PutData, operatorArguments: OperatorArguments, saveToRegister: Boolean): VimCaret? { + private fun deleteSelectedText(editor: VimEditor, caret: VimCaret, data: PutData, saveToRegister: Boolean): VimCaret? { if (data.visualSelection == null) return null if (!caret.isValid) return null @@ -107,13 +101,13 @@ abstract class VimPutBase : VimPut { val range = selectionForCaret.toVimTextRange(false).normalize() injector.application.runWriteAction { - injector.changeGroup.deleteRange(editor, caret, range, selectionForCaret.type, false, operatorArguments, saveToRegister) + injector.changeGroup.deleteRange(editor, caret, range, selectionForCaret.type, false, saveToRegister) } return caret.moveToInlayAwareOffset(range.startOffset) } @RWLockLabel.SelfSynchronized - private fun deleteSelectedText(editor: VimEditor, data: PutData, operatorArguments: OperatorArguments, saveToRegister: Boolean) { + private fun deleteSelectedText(editor: VimEditor, data: PutData, saveToRegister: Boolean) { if (data.visualSelection == null) return data.visualSelection.caretsAndSelections.entries.sortedByDescending { it.key.getBufferPosition() } @@ -122,7 +116,7 @@ abstract class VimPutBase : VimPut { val range = selection.toVimTextRange(false).normalize() injector.application.runWriteAction { - injector.changeGroup.deleteRange(editor, caret, range, selection.type, false, operatorArguments, saveToRegister) + injector.changeGroup.deleteRange(editor, caret, range, selection.type, false, saveToRegister) } caret.moveToInlayAwareOffset(range.startOffset) } @@ -297,7 +291,7 @@ abstract class VimPutBase : VimPut { var updated = caret if (currentLine + lineCount >= editor.nativeLineCount()) { val limit = currentLine + lineCount - editor.nativeLineCount() - for (i in 0 until limit) { + repeat(limit) { updated = updated.moveToOffset(editor.fileSize().toInt()) updated = injector.changeGroup.insertText(editor, updated, "\n") } @@ -526,14 +520,13 @@ abstract class VimPutBase : VimPut { editor, caret, data, - OperatorArguments(false, 0, editor.mode), modifyRegister, ) ?: return false } val processedText = processText(currentCaret, data) ?: return false currentCaret = putForCaret(editor, currentCaret, data, additionalData, context, processedText) if (updateVisualMarks) { - wrapInsertedTextWithVisualMarks(currentCaret, data, processedText) + wrapInsertedTextWithVisualMarks(currentCaret, data) } return true } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt index 12776aee07..151c97bc16 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt @@ -23,7 +23,6 @@ import com.maddyhome.idea.vim.api.VimScrollingModel import com.maddyhome.idea.vim.api.VimSelectionModel import com.maddyhome.idea.vim.api.VimVisualPosition import com.maddyhome.idea.vim.api.VirtualFile -import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.VimEditorReplaceMask @@ -755,7 +754,7 @@ class VimRegex(pattern: String) { override val projectId: String = "no project, I am just a piece of text wrapped into an Enditor for Regexp to work" - override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) {} + override fun exitInsertMode(context: ExecutionContext) {} override fun exitSelectModeNative(adjustCaret: Boolean) {} @@ -793,4 +792,3 @@ class VimRegex(pattern: String) { } } } - diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/register/VimRegisterGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/register/VimRegisterGroupBase.kt index 8f539df6c9..24b0bdb28b 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/register/VimRegisterGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/register/VimRegisterGroupBase.kt @@ -14,6 +14,7 @@ import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.getText import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.injector +import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.diagnostic.VimLogger import com.maddyhome.idea.vim.diagnostic.debug @@ -151,9 +152,8 @@ abstract class VimRegisterGroupBase : VimRegisterGroup { val currentCommand = injector.vimState.executingCommand if (currentCommand != null) { val argument = currentCommand.argument - if (argument != null) { - val motionCommand = argument.motion - val action = motionCommand.action + if (argument is Argument.Motion) { + val action = argument.motion return action.id == "VimMotionPercentOrMatchAction" || action.id == "VimMotionSentencePreviousStartAction" || action.id == "VimMotionSentenceNextStartAction" || diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt index 0dbb3602dd..41b2c0a18f 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt @@ -33,17 +33,32 @@ data class KeyHandlerState( get() = commandLineCommandBuilder ?: editorCommandBuilder fun enterCommandLine() { - // Create a new command builder for the command line, so we can handle nested commands inside the command line. - // The command that starts the command line is added to the new command builder and immediately executed, opening - // the command line UI. - // When we match the command that accepts or cancels the command line, we remove this nested command builder, and - // that command is added to the editor command builder, immediately completed and executed. - // The user might already have entered some state in the editor command builder, specifically uncommitted count. - // E.g., the user might have typed `3:` expecting the command line to be displayed with `:.,.+2` as initial text. - // We do not reset the uncommitted count in the editor command builder. The Ex actions ignore it, preferring the - // range in the text command. The search actions use it, and it will be combined with an operator count as expected. - // E.g., `2d3/foo` will delete up to the 6th occurrence of `foo` - commandLineCommandBuilder = CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.CMD_LINE), editorCommandBuilder.count) + // Create a new command builder for commands entered inside the command line, which allows for nested commands, such + // as inserting a digraph while entering a search pattern as a delete motion - `d/fooOK`. + // When matching an action that opens a command line, the new builder is created before the action is added to a + // builder, which means it gets added to the new command line builder. Since there are no arguments required by the + // action, the command is completed and executed (and the command line builder reset), and the command line UI is + // opened. + // When matching an action that accepts or cancels a command line, the command line builder is removed before the + // action is added, so it is added to the editor's command line builder. The key handler recognises the action to + // accept the command line, and will add the command line contents as an argument, and then execute the command. + // The user might have already entered some state in the editor command builder, specifically uncommitted count. + // E.g., the user might have typed `3:` expecting the command line to be displayed with `:.,.+2` as initial text, or + // started something like `2d3/foo` to delete up to the 6th occurrence of `foo`. (In both examples, the uncommitted + // count is `3`.) + // We pass the current, in progress count for the editor command builder to the new command line builder, where it + // becomes the count for the new action. The `:` handler will transform it into a range, while the search handler + // will ignore it. + // In other words, the `:` handler uses the count eagerly, where it becomes part of the command line that is + // executed (by the editor command builder) when the command line UI is closed. But the search handler doesn't use + // it - it's needed by the search motion action executed by the editor command builder once the command line UI is + // accepted. For this reason, we do NOT clear the uncommitted count from the editor command builder. A command such + // as `2d3/foo` becomes a delete operator action with a search action motion argument, which itself has an Ex string + // argument with the search string. The command has a count of `6`. And a command such as `3:p` becomes an action to + // process Ex entry with an argument of `.,.+2p` and a count of 3. The count is ignored by this action. + // Note that we use the calculated count. In Vim, `2"a3"b:` transforms to `:.,.+5`, which is the same behaviour + commandLineCommandBuilder = CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.CMD_LINE), + editorCommandBuilder.calculateCount0Snapshot()) } fun leaveCommandLine() { @@ -53,7 +68,7 @@ data class KeyHandlerState( fun partialReset(mode: Mode) { logger.trace("entered partialReset. mode: $mode") mappingState.resetMappingSequence() - commandBuilder.resetInProgressCommandPart(injector.keyGroup.getKeyRoot(mode.toMappingMode())) + commandBuilder.resetCommandTrieRootNode(injector.keyGroup.getKeyRoot(mode.toMappingMode())) } fun reset(mode: Mode) { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/VimStateMachine.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/VimStateMachine.kt index a8fa2ecf83..66b463f971 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/VimStateMachine.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/VimStateMachine.kt @@ -20,7 +20,6 @@ import java.util.* interface VimStateMachine { val mode: Mode var isDotRepeatInProgress: Boolean - var isRegisterPending: Boolean val isReplaceCharacter: Boolean /** @@ -36,8 +35,6 @@ interface VimStateMachine { var executingCommand: Command? val executingCommandFlags: EnumSet - fun resetRegisterPending() - fun reset() companion object { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt index 47d259e24c..e89d4ee4ed 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/Command.kt @@ -8,7 +8,6 @@ package com.maddyhome.idea.vim.vimscript.model.commands -import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimEditor @@ -28,7 +27,6 @@ import com.maddyhome.idea.vim.state.mode.isBlock import com.maddyhome.idea.vim.vimscript.model.Executable import com.maddyhome.idea.vim.vimscript.model.ExecutionResult import com.maddyhome.idea.vim.vimscript.model.VimLContext -import java.util.* sealed class Command(private val commandRange: Range, val commandArgument: String) : Executable { override lateinit var vimContext: VimLContext @@ -91,13 +89,7 @@ sealed class Command(private val commandRange: Range, val commandArgument: Strin return ExecutionResult.Error } - val keyHandler = KeyHandler.getInstance() - val keyState = keyHandler.keyHandlerState - val operatorArguments = OperatorArguments( - keyHandler.isOperatorPending(editor.mode, keyState), - 0, - editor.mode, - ) + val operatorArguments = OperatorArguments(0, editor.mode) val runCommand = { runCommand(editor, context, operatorArguments) } return when (argFlags.access) { @@ -337,7 +329,7 @@ sealed class Command(private val commandRange: Range, val commandArgument: Strin try { validate(editor) } - catch (t: Throwable) { + catch (_: Throwable) { return null } return getLineRange(editor) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/DeleteLinesCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/DeleteLinesCommand.kt index 15f957b3d9..aeba9d72ae 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/DeleteLinesCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/DeleteLinesCommand.kt @@ -36,9 +36,7 @@ data class DeleteLinesCommand(val range: Range, val argument: String) : Command. if (!injector.registerGroup.selectRegister(register)) return ExecutionResult.Error val textRange = getLineRangeWithCount(editor, caret).toTextRange(editor) - return if (injector.changeGroup - .deleteRange(editor, caret, textRange, SelectionType.LINE_WISE, false, operatorArguments) - ) { + return if (injector.changeGroup.deleteRange(editor, caret, textRange, SelectionType.LINE_WISE, false)) { ExecutionResult.Success } else { ExecutionResult.Error diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/NormalCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/NormalCommand.kt index 828183a501..e362237799 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/NormalCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/NormalCommand.kt @@ -49,7 +49,7 @@ data class NormalCommand(val range: Range, val argument: String) : Command.Singl } } is Mode.CMD_LINE -> injector.commandLine.getActiveCommandLine()?.close(refocusOwningEditor = true, resetCaret = false) - Mode.INSERT, Mode.REPLACE -> editor.exitInsertMode(context, OperatorArguments(false, 1, editor.mode)) + Mode.INSERT, Mode.REPLACE -> editor.exitInsertMode(context) is Mode.SELECT -> editor.exitSelectModeNative(false) is Mode.OP_PENDING, is Mode.NORMAL -> Unit } @@ -79,7 +79,7 @@ data class NormalCommand(val range: Range, val argument: String) : Command.Singl injector.commandLine.getActiveCommandLine()?.close(refocusOwningEditor = true, resetCaret = false) } if (mode is Mode.INSERT || mode is Mode.REPLACE) { - editor.exitInsertMode(context, OperatorArguments(false, 1, mode)) + editor.exitInsertMode(context) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/PutLinesCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/PutLinesCommand.kt index f2b74df7a8..216865a80e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/PutLinesCommand.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/model/commands/PutLinesCommand.kt @@ -56,6 +56,6 @@ data class PutLinesCommand(val range: Range, val argument: String) : Command.Sin caretAfterInsertedText = false, putToLine = line, ) - return if (injector.put.putText(editor, context, putData, operatorArguments = operatorArguments)) ExecutionResult.Success else ExecutionResult.Error + return if (injector.put.putText(editor, context, putData)) ExecutionResult.Success else ExecutionResult.Error } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/services/VimVariableServiceBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/services/VimVariableServiceBase.kt index ef2051ef00..e1d32a28bd 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/services/VimVariableServiceBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/vimscript/services/VimVariableServiceBase.kt @@ -171,20 +171,14 @@ abstract class VimVariableServiceBase : VariableService { return getBufferVariables(editor)[name] } + @Suppress("SpellCheckingInspection") protected open fun getVimVariable(name: String, editor: VimEditor, context: ExecutionContext, vimContext: VimLContext): VimDataType? { + // Note that the v:count variables might be incorrect in scenarios other than mappings, when there is a command in + // progress. However, I've only seen it used inside mappings, so don't know return when (name) { - "count" -> { - val count = KeyHandler.getInstance().keyHandlerState.commandBuilder.count - VimInt(count) - } - "count1" -> { - val count1 = KeyHandler.getInstance().keyHandlerState.commandBuilder.count.coerceAtLeast(1) - VimInt(count1) - } - "searchforward" -> { - val searchForward = if (injector.searchGroup.getLastSearchDirection() == Direction.FORWARDS) 1 else 0 - VimInt(searchForward) - } + "count" -> VimInt(KeyHandler.getInstance().keyHandlerState.commandBuilder.calculateCount0Snapshot()) + "count1" -> VimInt(KeyHandler.getInstance().keyHandlerState.commandBuilder.calculateCount0Snapshot().coerceAtLeast(1)) + "searchforward" -> VimInt(if (injector.searchGroup.getLastSearchDirection() == Direction.FORWARDS) 1 else 0) else -> throw ExException("The 'v:' scope is not implemented yet :(") } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/yank/YankGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/yank/YankGroupBase.kt index be85e3f37e..e0e1a2e5b9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/yank/YankGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/yank/YankGroupBase.kt @@ -80,8 +80,8 @@ open class YankGroupBase : VimYankGroup { argument: Argument, operatorArguments: OperatorArguments, ): Boolean { - val motion = argument.motion - val type = if (motion.isLinewiseMotion()) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE + val motion = argument as? Argument.Motion ?: return false + val motionType = motion.getMotionType() val nativeCaretCount = editor.nativeCarets().size if (nativeCaretCount <= 0) return false @@ -90,7 +90,12 @@ open class YankGroupBase : VimYankGroup { val ranges = ArrayList>(nativeCaretCount) // This logic is from original vim - val startOffsets = if (argument.motion.action is MotionDownLess1FirstNonSpaceAction) null else HashMap(nativeCaretCount) + val startOffsets = + if (argument.motion is MotionDownLess1FirstNonSpaceAction) { + null + } else { + HashMap(nativeCaretCount) + } for (caret in editor.nativeCarets()) { val motionRange = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) @@ -102,7 +107,7 @@ open class YankGroupBase : VimYankGroup { caretToRange[caret] = TextRange(motionRange.startOffset, motionRange.endOffset) } - val range = getTextRange(ranges, type) ?: return false + val range = getTextRange(ranges, motionType) ?: return false if (range.size() == 0) return false @@ -110,7 +115,7 @@ open class YankGroupBase : VimYankGroup { editor, caretToRange, range, - type, + motionType, startOffsets, ) } diff --git a/vim-engine/src/main/resources/ksp-generated/engine_commands.json b/vim-engine/src/main/resources/ksp-generated/engine_commands.json index 9595eea955..6e7008f7e8 100644 --- a/vim-engine/src/main/resources/ksp-generated/engine_commands.json +++ b/vim-engine/src/main/resources/ksp-generated/engine_commands.json @@ -17,7 +17,12 @@ { "keys": "$", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionLastColumnAction", - "modes": "NXO" + "modes": "NX" + }, + { + "keys": "$", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionLastColumnOpPendingAction", + "modes": "O" }, { "keys": "%", @@ -112,7 +117,12 @@ { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionBackspaceAction", - "modes": "NXO" + "modes": "NX" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionBackspaceOpPendingModeAction", + "modes": "O" }, { "keys": "", @@ -247,7 +257,12 @@ { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionBackspaceAction", - "modes": "NXO" + "modes": "NX" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionBackspaceOpPendingModeAction", + "modes": "O" }, { "keys": "", @@ -742,16 +757,21 @@ { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowLeftAction", - "modes": "NXO" + "modes": "NX" }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionLeftInsertModeAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowLeftInsertModeAction", "modes": "I" }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.select.motion.SelectMotionLeftAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowLeftOpPendingAction", + "modes": "O" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.select.motion.SelectMotionArrowLeftAction", "modes": "S" }, { @@ -792,16 +812,21 @@ { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowRightAction", - "modes": "NXO" + "modes": "NX" }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionRightInsertAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowRightInsertModeAction", "modes": "I" }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.select.motion.SelectMotionRightAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowRightOpPendingAction", + "modes": "O" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.select.motion.SelectMotionArrowRightAction", "modes": "S" }, { @@ -831,7 +856,7 @@ }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionShiftLeftAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionShiftArrowLeftAction", "modes": "INXS" }, { @@ -841,7 +866,7 @@ }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionShiftRightAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionShiftArrowRightAction", "modes": "INXS" }, { @@ -857,7 +882,12 @@ { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionSpaceAction", - "modes": "NXO" + "modes": "NX" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionSpaceOpPendingModeAction", + "modes": "O" }, { "keys": "", @@ -887,23 +917,33 @@ { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowLeftAction", - "modes": "NXO" + "modes": "NX" }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionLeftInsertModeAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowLeftInsertModeAction", "modes": "I" }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowLeftOpPendingAction", + "modes": "O" + }, { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowRightAction", - "modes": "NXO" + "modes": "NX" }, { "keys": "", - "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionRightInsertAction", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowRightInsertModeAction", "modes": "I" }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionArrowRightOpPendingAction", + "modes": "O" + }, { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.updown.MotionArrowUpAction", @@ -1667,7 +1707,12 @@ { "keys": "h", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionLeftAction", - "modes": "NXO" + "modes": "NX" + }, + { + "keys": "h", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionLeftOpPendingModeAction", + "modes": "O" }, { "keys": "i", @@ -1777,7 +1822,12 @@ { "keys": "l", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionRightAction", - "modes": "NXO" + "modes": "NX" + }, + { + "keys": "l", + "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionRightOpPendingAction", + "modes": "O" }, { "keys": "m",