1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2024-11-24 22:42:53 +01:00

Compare commits

..

23 Commits

Author SHA1 Message Date
cc9385f2a9
Set plugin version to chylex-40 2024-08-25 10:17:38 +02:00
32a2384a46
Revert "VIM-2074 Backspace behaviour is incorrect in Replace mode"
This reverts commit 9bbeab80
2024-08-25 10:17:38 +02:00
fdd850de5a
Fix(VIM-3615): Escape closes dialog while waiting for more keys 2024-08-25 10:10:21 +02:00
0f7116b136
Add action to run last macro in all opened files 2024-08-25 06:11:12 +02:00
db8f0251fb
Stop macro execution after a failed search 2024-08-25 06:11:12 +02:00
2ca2c1e774
Revert per-caret registers 2024-08-25 06:11:10 +02:00
f3c32da4d1
Revert "Factor disposable objects on editor opening"
This reverts commit 1fa78935
2024-08-25 06:10:58 +02:00
1a3b34d457
Fix(VIM-3364): Exception with mapped Generate action 2024-08-25 06:10:58 +02:00
1f9159996d
Apply scrolloff after executing native IDEA actions 2024-08-25 06:10:58 +02:00
65c3acd891
Stay on same line after reindenting 2024-08-25 06:10:58 +02:00
223f65c003
Update search register when using f/t 2024-08-25 06:10:58 +02:00
293b854620
Automatically add unambiguous imports after running a macro 2024-08-25 06:10:58 +02:00
9e7c5fd603
Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2024-08-25 06:10:58 +02:00
00c799595f
Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2024-08-25 06:10:58 +02:00
8577b5ed20
Add support for count for visual and line motion surround 2024-08-25 06:10:58 +02:00
3af7a991a0
Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2024-08-25 06:10:57 +02:00
212af1798d
Fix(VIM-696) Restore visual mode after undo/redo, and disable incompatible actions 2024-08-25 06:10:57 +02:00
002ef8f72f
Respect count with <Action> mappings 2024-08-25 06:10:57 +02:00
9115af6b3d
Change matchit plugin to use HTML patterns in unrecognized files 2024-08-25 06:10:57 +02:00
25ca42d371
Reset insert mode when switching active editor 2024-08-25 06:10:57 +02:00
408687c9b3
Disable switching to insert mode for some editors 2024-08-25 06:10:54 +02:00
85e00bf8fc
Remove update checker 2024-08-25 06:10:31 +02:00
bd6f2d4b2f
Set custom plugin version 2024-08-25 06:10:31 +02:00
126 changed files with 1136 additions and 1602 deletions

View File

@ -27,7 +27,6 @@ object Project : Project({
// Active tests // Active tests
buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT")) buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(TestingBuildType("2024.1.1", "<default>")) buildType(TestingBuildType("2024.1.1", "<default>"))
buildType(TestingBuildType("2024.2", "<default>"))
buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT")) buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(PropertyBased) buildType(PropertyBased)

View File

@ -535,10 +535,6 @@ Contributors:
[![icon][github]](https://github.com/igorbabko) [![icon][github]](https://github.com/igorbabko)
&nbsp; &nbsp;
Igor Babko Igor Babko
* [![icon][mail]](mailto:533601+felixwiemuth@users.noreply.github.com)
[![icon][github]](https://github.com/felixwiemuth)
&nbsp;
Felix Wiemuth
Previous contributors: Previous contributors:

View File

@ -8,7 +8,7 @@ Every effort is made to make these options compatible with Vim behaviour.
However, some differences are inevitable. However, some differences are inevitable.
``` ```
'clipboard' 'cb' Defines clipboard behavior 'clipboard' 'cb' Defines clipboard behavioue
A comma-separated list of words to control clipboard behaviour: A comma-separated list of words to control clipboard behaviour:
unnamed The clipboard register '*' is used instead of the unnamed The clipboard register '*' is used instead of the
unnamed register unnamed register

View File

@ -20,7 +20,7 @@ ideaVersion=2024.2
# Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type # Values for type: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html#intellij-extension-type
ideaType=IC ideaType=IC
instrumentPluginCode=true instrumentPluginCode=true
version=chylex-41 version=chylex-40
javaVersion=17 javaVersion=17
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1

View File

@ -8,10 +8,6 @@
package com.maddyhome.idea.vim package com.maddyhome.idea.vim
import com.intellij.ide.BrowserUtil
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginStateListener
import com.intellij.ide.plugins.PluginStateManager
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
@ -44,18 +40,6 @@ internal class PluginStartup : ProjectActivity/*, LightEditCompatible*/ {
// This code should be executed once // This code should be executed once
VimPlugin.getInstance().initialize() VimPlugin.getInstance().initialize()
// Uninstall survey. Should be registered once for all projects
PluginStateManager.addStateListener(object : PluginStateListener {
override fun install(p0: IdeaPluginDescriptor) {/*Nothing*/
}
override fun uninstall(descriptor: IdeaPluginDescriptor) {
if (descriptor.pluginId == VimPlugin.getPluginId()) {
BrowserUtil.open("https://surveys.jetbrains.com/s3/ideavim-uninstall-feedback")
}
}
})
} }
} }

View File

@ -37,7 +37,7 @@ import com.maddyhome.idea.vim.vimscript.model.expressions.FunctionCallExpression
import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression import com.maddyhome.idea.vim.vimscript.model.expressions.SimpleExpression
// todo make it multicaret // todo make it multicaret
private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, motionType: SelectionType): Boolean { private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: SelectionType): Boolean {
val func = injector.globalOptions().operatorfunc val func = injector.globalOptions().operatorfunc
if (func.isEmpty()) { if (func.isEmpty()) {
VimPlugin.showMessage(MessageHelper.message("E774")) VimPlugin.showMessage(MessageHelper.message("E774"))
@ -57,9 +57,9 @@ private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textR
if (value is VimFuncref) { if (value is VimFuncref) {
handler = value.handler handler = value.handler
} }
} catch (_: ExException) { } catch (ex: ExException) {
// Get the argument for function('...') or funcref('...') for the error message // Get the argument for function('...') or funcref('...') for the error message
val functionName = if (expression is FunctionCallExpression && expression.arguments.isNotEmpty()) { val functionName = if (expression is FunctionCallExpression && expression.arguments.size > 0) {
expression.arguments[0].evaluate(editor, context, scriptContext).toString() expression.arguments[0].evaluate(editor, context, scriptContext).toString()
} }
else { else {
@ -77,7 +77,7 @@ private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textR
return false return false
} }
val arg = when (motionType) { val arg = when (selectionType) {
SelectionType.LINE_WISE -> "line" SelectionType.LINE_WISE -> "line"
SelectionType.CHARACTER_WISE -> "char" SelectionType.CHARACTER_WISE -> "char"
SelectionType.BLOCK_WISE -> "block" SelectionType.BLOCK_WISE -> "block"
@ -101,13 +101,19 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
override val argumentType: Argument.Type = Argument.Type.MOTION override val argumentType: Argument.Type = Argument.Type.MOTION
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
val argument = cmd.argument as? Argument.Motion ?: return false val argument = cmd.argument ?: return false
if (!editor.inRepeatMode) { if (!editor.inRepeatMode) {
argumentCaptured = argument argumentCaptured = argument
} }
val range = getMotionRange(editor, context, argument, operatorArguments) val range = getMotionRange(editor, context, argument, operatorArguments)
if (range != null) { if (range != null) {
return doOperatorAction(editor, context, range, argument.getMotionType()) val selectionType = if (argument.motion.isLinewiseMotion()) {
SelectionType.LINE_WISE
} else {
SelectionType.CHARACTER_WISE
}
return doOperatorAction(editor, context, range, selectionType)
} }
return false return false
} }
@ -115,7 +121,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
private fun getMotionRange( private fun getMotionRange(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
argument: Argument.Motion, argument: Argument,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): TextRange? { ): TextRange? {
// Note that we're using getMotionRange2 in order to avoid normalising the linewise range into line start // Note that we're using getMotionRange2 in order to avoid normalising the linewise range into line start
@ -130,7 +136,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
operatorArguments, operatorArguments,
)?.normalize()?.let { )?.normalize()?.let {
// If we're linewise, make sure the end offset isn't just the EOL char // If we're linewise, make sure the end offset isn't just the EOL char
if (argument.getMotionType() == SelectionType.LINE_WISE && it.endOffset < editor.fileSize()) { if (argument.motion.isLinewiseMotion() && it.endOffset < editor.fileSize()) {
TextRange(it.startOffset, it.endOffset + 1) TextRange(it.startOffset, it.endOffset + 1)
} else { } else {
it it

View File

@ -25,7 +25,7 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
val state = injector.vimState val state = injector.vimState
var lastCommand = VimRepeater.lastChangeCommand val lastCommand = VimRepeater.lastChangeCommand
if (lastCommand == null && Extension.lastExtensionHandler == null) return false if (lastCommand == null && Extension.lastExtensionHandler == null) return false
@ -57,7 +57,12 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
) )
} else if (!repeatHandler && lastCommand != null) { } else if (!repeatHandler && lastCommand != null) {
if (cmd.rawCount > 0) { if (cmd.rawCount > 0) {
lastCommand = lastCommand.copy(rawCount = cmd.rawCount) lastCommand.rawCount = cmd.count
val arg = lastCommand.argument
if (arg != null) {
val mot = arg.motion
mot.rawCount = 0
}
} }
state.executingCommand = lastCommand state.executingCommand = lastCommand

View File

@ -40,7 +40,7 @@ class DeleteJoinLinesAction : ChangeEditorActionHandler.ConditionalSingleExecuti
): Boolean { ): Boolean {
injector.editorGroup.notifyIdeaJoin(editor) injector.editorGroup.notifyIdeaJoin(editor)
return injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, false) return injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, false, operatorArguments)
} }
override fun execute( override fun execute(

View File

@ -35,7 +35,7 @@ class DeleteJoinLinesSpacesAction : ChangeEditorActionHandler.SingleExecution()
injector.editorGroup.notifyIdeaJoin(editor) injector.editorGroup.notifyIdeaJoin(editor)
var res = true var res = true
editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret -> editor.nativeCarets().sortedByDescending { it.offset }.forEach { caret ->
if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true)) { if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true, operatorArguments)) {
res = false res = false
} }
} }

View File

@ -22,6 +22,11 @@ import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.* import java.util.*
@CommandOrMotion(keys = ["<C-H>", "<BS>"], modes = [Mode.INSERT])
internal class VimEditorBackSpace : IdeActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE) {
override val type: Command.Type = Command.Type.DELETE
}
@CommandOrMotion(keys = ["<Del>"], modes = [Mode.INSERT]) @CommandOrMotion(keys = ["<Del>"], modes = [Mode.INSERT])
internal class VimEditorDelete : IdeActionHandler(IdeActions.ACTION_EDITOR_DELETE) { internal class VimEditorDelete : IdeActionHandler(IdeActions.ACTION_EDITOR_DELETE) {
override val type: Command.Type = Command.Type.DELETE override val type: Command.Type = Command.Type.DELETE

View File

@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Deque; 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.putExtensionHandlerMapping;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing; import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissing;
@ -63,8 +64,8 @@ public class VimArgTextObjExtension implements VimExtension {
*/ */
private static class BracketPairs { private static class BracketPairs {
// NOTE: brackets must match by the position, and ordered by rank (highest to lowest). // NOTE: brackets must match by the position, and ordered by rank (highest to lowest).
private final @NotNull String openBrackets; @NotNull private final String openBrackets;
private final @NotNull String closeBrackets; @NotNull private final String closeBrackets;
static class ParseException extends Exception { static class ParseException extends Exception {
public ParseException(@NotNull String message) { public ParseException(@NotNull String message) {
@ -86,7 +87,8 @@ public class VimArgTextObjExtension implements VimExtension {
* @param bracketPairs comma-separated list of colon-separated bracket pairs. * @param bracketPairs comma-separated list of colon-separated bracket pairs.
* @throws ParseException if a syntax error is detected. * @throws ParseException if a syntax error is detected.
*/ */
static @NotNull BracketPairs fromBracketPairList(final @NotNull String bracketPairs) throws ParseException { @NotNull
static BracketPairs fromBracketPairList(@NotNull final String bracketPairs) throws ParseException {
StringBuilder openBrackets = new StringBuilder(); StringBuilder openBrackets = new StringBuilder();
StringBuilder closeBrackets = new StringBuilder(); StringBuilder closeBrackets = new StringBuilder();
ParseState state = ParseState.OPEN; ParseState state = ParseState.OPEN;
@ -126,7 +128,7 @@ public class VimArgTextObjExtension implements VimExtension {
return new BracketPairs(openBrackets.toString(), closeBrackets.toString()); return new BracketPairs(openBrackets.toString(), closeBrackets.toString());
} }
BracketPairs(final @NotNull String openBrackets, final @NotNull String closeBrackets) { BracketPairs(@NotNull final String openBrackets, @NotNull final String closeBrackets) {
assert openBrackets.length() == closeBrackets.length(); assert openBrackets.length() == closeBrackets.length();
this.openBrackets = openBrackets; this.openBrackets = openBrackets;
this.closeBrackets = closeBrackets; this.closeBrackets = closeBrackets;
@ -156,9 +158,10 @@ public class VimArgTextObjExtension implements VimExtension {
} }
} }
private static final BracketPairs DEFAULT_BRACKET_PAIRS = new BracketPairs("(", ")"); public static final BracketPairs DEFAULT_BRACKET_PAIRS = new BracketPairs("(", ")");
private static @Nullable String bracketPairsVariable() { @Nullable
private static String bracketPairsVariable() {
final Object value = VimPlugin.getVariableService().getGlobalVariableValue("argtextobj_pairs"); final Object value = VimPlugin.getVariableService().getGlobalVariableValue("argtextobj_pairs");
if (value instanceof VimString vimValue) { if (value instanceof VimString vimValue) {
return vimValue.getValue(); return vimValue.getValue();
@ -189,12 +192,13 @@ public class VimArgTextObjExtension implements VimExtension {
this.isInner = isInner; this.isInner = isInner;
} }
@Nullable
@Override @Override
public @Nullable TextRange getRange(@NotNull VimEditor editor, public TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret, @NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context, @NotNull ExecutionContext context,
int count, int count,
int rawCount) { int rawCount) {
BracketPairs bracketPairs = DEFAULT_BRACKET_PAIRS; BracketPairs bracketPairs = DEFAULT_BRACKET_PAIRS;
final String bracketPairsVar = bracketPairsVariable(); final String bracketPairsVar = bracketPairsVariable();
if (bracketPairsVar != null) { if (bracketPairsVar != null) {
@ -232,22 +236,24 @@ public class VimArgTextObjExtension implements VimExtension {
return new TextRange(finder.getLeftBound(), finder.getRightBound()); return new TextRange(finder.getLeftBound(), finder.getRightBound());
} }
@NotNull
@Override @Override
public @NotNull TextObjectVisualType getVisualType() { public TextObjectVisualType getVisualType() {
return TextObjectVisualType.CHARACTER_WISE; return TextObjectVisualType.CHARACTER_WISE;
} }
} }
@Override @Override
public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) { public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) {
@NotNull KeyHandler keyHandler = KeyHandler.getInstance();
@NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState(); @NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState();
int count = Math.max(1, keyHandlerState.getCommandBuilder().getCount());
final ArgumentTextObjectHandler textObjectHandler = new ArgumentTextObjectHandler(isInner); final ArgumentTextObjectHandler textObjectHandler = new ArgumentTextObjectHandler(isInner);
//noinspection DuplicatedCode //noinspection DuplicatedCode
if (!(editor.getMode() instanceof Mode.OP_PENDING)) { if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) {
int count0 = operatorArguments.getCount0();
editor.nativeCarets().forEach((VimCaret caret) -> { editor.nativeCarets().forEach((VimCaret caret) -> {
final TextRange range = textObjectHandler.getRange(editor, caret, context, Math.max(1, count0), count0); final TextRange range = textObjectHandler.getRange(editor, caret, context, count, 0);
if (range != null) { if (range != null) {
try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) {
if (editor.getMode() instanceof Mode.VISUAL) { if (editor.getMode() instanceof Mode.VISUAL) {
@ -259,7 +265,8 @@ public class VimArgTextObjExtension implements VimExtension {
} }
}); });
} else { } else {
keyHandlerState.getCommandBuilder().addAction(textObjectHandler); keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count,
textObjectHandler, Command.Type.MOTION, EnumSet.noneOf(CommandFlags.class))));
} }
} }
} }
@ -269,9 +276,9 @@ public class VimArgTextObjExtension implements VimExtension {
* position * position
*/ */
private static class ArgBoundsFinder { private static class ArgBoundsFinder {
private final @NotNull CharSequence text; @NotNull private final CharSequence text;
private final @NotNull Document document; @NotNull private final Document document;
private final @NotNull BracketPairs brackets; @NotNull private final BracketPairs brackets;
private int leftBound = Integer.MAX_VALUE; private int leftBound = Integer.MAX_VALUE;
private int rightBound = Integer.MIN_VALUE; private int rightBound = Integer.MIN_VALUE;
private int leftBracket; private int leftBracket;
@ -298,7 +305,7 @@ public class VimArgTextObjExtension implements VimExtension {
* @param position starting position. * @param position starting position.
*/ */
boolean findBoundsAt(int position) throws IllegalStateException { boolean findBoundsAt(int position) throws IllegalStateException {
if (text.isEmpty()) { if (text.length() == 0) {
error = "empty document"; error = "empty document";
return false; return false;
} }

View File

@ -25,6 +25,9 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getLineEndOffset import com.maddyhome.idea.vim.api.getLineEndOffset
import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector 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.MappingMode
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.command.TextObjectVisualType import com.maddyhome.idea.vim.command.TextObjectVisualType
@ -49,6 +52,7 @@ import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import java.util.*
internal class CommentaryExtension : VimExtension { internal class CommentaryExtension : VimExtension {
@ -180,8 +184,10 @@ internal class CommentaryExtension : VimExtension {
override val isRepeatable = true override val isRepeatable = true
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { 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 val keyState = KeyHandler.getInstance().keyHandlerState
keyState.commandBuilder.addAction(CommentaryTextObjectMotionHandler) keyState.commandBuilder.completeCommandPart(Argument(command))
} }
} }

View File

@ -44,7 +44,6 @@ import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import java.util.* import java.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -94,29 +93,34 @@ internal class Matchit : VimExtension {
override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) { override fun execute(editor: VimEditor, context: ExecutionContext, operatorArguments: OperatorArguments) {
val keyHandler = KeyHandler.getInstance() val keyHandler = KeyHandler.getInstance()
val keyState = keyHandler.keyHandlerState val keyState = keyHandler.keyHandlerState
val count = keyState.commandBuilder.count
// Reset the command count so it doesn't transfer onto subsequent commands. // Reset the command count so it doesn't transfer onto subsequent commands.
keyState.commandBuilder.resetCount() keyState.commandBuilder.resetCount()
// Normally we want to jump to the start of the matching pair. But when moving forward in operator // 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. // pending mode, we want to include the entire match. isInOpPending makes that distinction.
if (editor.mode is Mode.OP_PENDING) { val isInOpPending = keyHandler.isOperatorPending(editor.mode, keyState)
if (isInOpPending) {
val matchitAction = MatchitAction() val matchitAction = MatchitAction()
matchitAction.reverse = reverse matchitAction.reverse = reverse
matchitAction.isInOpPending = true matchitAction.isInOpPending = true
keyState.commandBuilder.addAction(matchitAction) keyState.commandBuilder.completeCommandPart(
Argument(
Command(
count,
matchitAction,
Command.Type.MOTION,
EnumSet.noneOf(CommandFlags::class.java),
),
),
)
} else { } else {
editor.sortedCarets().forEach { caret -> editor.sortedCarets().forEach { caret ->
injector.jumpService.saveJumpLocation(editor) injector.jumpService.saveJumpLocation(editor)
caret.moveToOffset( caret.moveToOffset(getMatchitOffset(editor.ij, caret.ij, count, isInOpPending, reverse))
getMatchitOffset(
editor.ij,
caret.ij,
operatorArguments.count0,
isInOpPending = false,
reverse
))
} }
} }
} }
@ -350,7 +354,7 @@ private object FileTypePatterns {
private val DEFAULT_PAIRS = setOf('(', ')', '[', ']', '{', '}') private val DEFAULT_PAIRS = setOf('(', ')', '[', ']', '{', '}')
private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPending: Boolean, reverse: Boolean): Int { private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPending: Boolean, reverse: Boolean): Int {
val virtualFile = EditorHelper.getVirtualFile(editor) val virtualFile = EditorHelper.getVirtualFile(editor)
var caretOffset = caret.offset var caretOffset = caret.offset
@ -363,9 +367,9 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPe
val currentChar = editor.document.charsSequence[caretOffset] val currentChar = editor.document.charsSequence[caretOffset]
var motionOffset: Int? = null var motionOffset: Int? = null
if (count0 > 0) { if (count > 0) {
// Matchit doesn't affect the percent motion, so we fall back to the default behavior. // Matchit doesn't affect the percent motion, so we fall back to the default behavior.
motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count0) motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count)
} else { } else {
// Check the simplest case first. // Check the simplest case first.
if (DEFAULT_PAIRS.contains(currentChar)) { if (DEFAULT_PAIRS.contains(currentChar)) {
@ -396,7 +400,8 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPe
private fun getMotionOffset(motion: Motion): Int? { private fun getMotionOffset(motion: Motion): Int? {
return when (motion) { return when (motion) {
is Motion.AdjustedOffset, is Motion.AbsoluteOffset -> motion.offset is Motion.AbsoluteOffset -> motion.offset
is Motion.AdjustedOffset -> motion.offset
is Motion.Error, is Motion.NoMotion -> null is Motion.Error, is Motion.NoMotion -> null
} }
} }

View File

@ -555,13 +555,12 @@ private fun registerCommand(default: String, action: NerdAction) {
} }
private val actionsRoot: RootNode<NerdAction> = RootNode("NERDTree") private val actionsRoot: RootNode<NerdAction> = RootNode()
private var currentNode: CommandPartNode<NerdAction> = actionsRoot private var currentNode: CommandPartNode<NerdAction> = actionsRoot
private fun collectShortcuts(node: Node<NerdAction>): Set<KeyStroke> { private fun collectShortcuts(node: Node<NerdAction>): Set<KeyStroke> {
return if (node is CommandPartNode<NerdAction>) { return if (node is CommandPartNode<NerdAction>) {
val res = node.children.keys.toMutableSet() val res = node.keys.toMutableSet()
res += node.children.values.map { collectShortcuts(it) }.flatten() res += node.values.map { collectShortcuts(it) }.flatten()
res res
} else { } else {
emptySet() emptySet()

View File

@ -10,6 +10,7 @@ package com.maddyhome.idea.vim.extension.replacewithregister
import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret import com.maddyhome.idea.vim.api.ImmutableVimCaret
@ -165,11 +166,17 @@ private fun doReplace(editor: Editor, context: DataContext, caret: ImmutableVimC
putToLine = -1, putToLine = -1,
) )
val vimEditor = editor.vim val vimEditor = editor.vim
val keyHandler = KeyHandler.getInstance()
ClipboardOptionHelper.IdeaputDisabler().use { ClipboardOptionHelper.IdeaputDisabler().use {
VimPlugin.getPut().putText( VimPlugin.getPut().putText(
vimEditor, vimEditor,
context.vim, context.vim,
putData, putData,
operatorArguments = OperatorArguments(
keyHandler.isOperatorPending(vimEditor.mode, keyHandler.keyHandlerState),
0,
editor.vim.mode,
),
saveToRegister = false saveToRegister = false
) )
} }

View File

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

View File

@ -30,12 +30,14 @@ import com.maddyhome.idea.vim.state.mode.Mode;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; 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.putExtensionHandlerMapping;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping; import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping;
/** /**
* Port of vim-indent-object: * Port of vim-indent-object:
* <a href="https://github.com/michaeljsmith/vim-indent-object">vim-indent-object</a> * https://github.com/michaeljsmith/vim-indent-object
* *
* <p> * <p>
* vim-indent-object provides these text objects based on the cursor line's indentation: * vim-indent-object provides these text objects based on the cursor line's indentation:
@ -47,7 +49,7 @@ import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping;
* </ul> * </ul>
* *
* See also the reference manual for more details at: * See also the reference manual for more details at:
* <a href="https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt">indent-object.txt</a> * https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt
* *
* @author Shrikant Kandula (@sharat87) * @author Shrikant Kandula (@sharat87)
*/ */
@ -96,12 +98,13 @@ public class VimIndentObject implements VimExtension {
this.includeBelow = includeBelow; this.includeBelow = includeBelow;
} }
@Nullable
@Override @Override
public @Nullable TextRange getRange(@NotNull VimEditor editor, public TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret, @NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context, @NotNull ExecutionContext context,
int count, int count,
int rawCount) { int rawCount) {
final CharSequence charSequence = ((IjVimEditor)editor).getEditor().getDocument().getCharsSequence(); final CharSequence charSequence = ((IjVimEditor)editor).getEditor().getDocument().getCharsSequence();
final int caretOffset = ((IjVimCaret)caret).getCaret().getOffset(); final int caretOffset = ((IjVimCaret)caret).getCaret().getOffset();
@ -246,8 +249,9 @@ public class VimIndentObject implements VimExtension {
return new TextRange(upperBoundaryOffset, lowerBoundaryOffset); return new TextRange(upperBoundaryOffset, lowerBoundaryOffset);
} }
@NotNull
@Override @Override
public @NotNull TextObjectVisualType getVisualType() { public TextObjectVisualType getVisualType() {
return TextObjectVisualType.LINE_WISE; return TextObjectVisualType.LINE_WISE;
} }
@ -260,14 +264,15 @@ public class VimIndentObject implements VimExtension {
@Override @Override
public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) { public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) {
IjVimEditor vimEditor = (IjVimEditor)editor; IjVimEditor vimEditor = (IjVimEditor)editor;
@NotNull KeyHandler keyHandler = KeyHandler.getInstance();
@NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState(); @NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState();
int count = Math.max(1, keyHandlerState.getCommandBuilder().getCount());
final IndentObjectHandler textObjectHandler = new IndentObjectHandler(includeAbove, includeBelow); final IndentObjectHandler textObjectHandler = new IndentObjectHandler(includeAbove, includeBelow);
if (!(editor.getMode() instanceof Mode.OP_PENDING)) { if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) {
int count0 = operatorArguments.getCount0();
((IjVimEditor)editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> { ((IjVimEditor)editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> {
final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, Math.max(1, count0), count0); final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, count, 0);
if (range != null) { if (range != null) {
try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) { try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) {
if (editor.getMode() instanceof Mode.VISUAL) { if (editor.getMode() instanceof Mode.VISUAL) {
@ -280,7 +285,9 @@ public class VimIndentObject implements VimExtension {
}); });
} else { } else {
keyHandlerState.getCommandBuilder().addAction(textObjectHandler); keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count,
textObjectHandler, Command.Type.MOTION,
EnumSet.noneOf(CommandFlags.class))));
} }
} }
} }

View File

@ -103,11 +103,6 @@ class ChangeGroup : VimChangeGroupBase() {
} }
} }
override fun processBackspace(editor: VimEditor, context: ExecutionContext) {
injector.actionExecutor.executeAction(editor, name = IdeActions.ACTION_EDITOR_BACKSPACE, context = context)
injector.scroll.scrollCaretIntoView(editor)
}
private fun restoreCursor(editor: VimEditor, caret: VimCaret, startLine: Int) { private fun restoreCursor(editor: VimEditor, caret: VimCaret, startLine: Int) {
if (caret != editor.primaryCaret()) { if (caret != editor.primaryCaret()) {
(editor as IjVimEditor).editor.caretModel.addCaret( (editor as IjVimEditor).editor.caretModel.addCaret(

View File

@ -35,7 +35,7 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.handler.ExternalActionHandler import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
@ -193,16 +193,21 @@ internal class MotionGroup : VimMotionGroupBase() {
argument: Argument, argument: Argument,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): TextRange? { ): TextRange? {
if (argument !is Argument.Motion) {
throw RuntimeException("Unexpected argument passed to getMotionRange2: $argument")
}
var start: Int var start: Int
var end: 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 // This is where we are now
start = caret.offset start = caret.offset
@ -211,8 +216,8 @@ internal class MotionGroup : VimMotionGroupBase() {
editor.vim, editor.vim,
caret.vim, caret.vim,
IjEditorExecutionContext(context!!), IjEditorExecutionContext(context!!),
argument.argument, cmd.argument,
operatorArguments operatorArguments.withCount0(raw),
) )
// Invalid motion // Invalid motion
@ -228,32 +233,22 @@ internal class MotionGroup : VimMotionGroupBase() {
end++ end++
} }
} }
} } else if (cmd.action is TextObjectActionHandler) {
val action = cmd.action as TextObjectActionHandler
is TextObjectActionHandler -> { val range =
val range = action.getRange( action.getRange(editor.vim, caret.vim, IjEditorExecutionContext(context!!), cnt, raw) ?: return null
editor.vim,
caret.vim,
IjEditorExecutionContext(context!!),
operatorArguments.count1,
operatorArguments.count0
) ?: return null
start = range.startOffset start = range.startOffset
end = range.endOffset end = range.endOffset
if (argument.isLinewiseMotion()) end-- if (cmd.isLinewiseMotion()) end--
} else {
throw RuntimeException(
"Commands doesn't take " + cmd.action.javaClass.simpleName + " as an operator",
)
} }
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. // 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.id val id = argument.motion.action.id
if (id == VimChangeGroupBase.VIM_MOTION_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_BIG_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_CAMEL_RIGHT) { 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 text = editor.document.charsSequence.subSequence(start, end).toString()
val lastNewLine = text.lastIndexOf('\n') val lastNewLine = text.lastIndexOf('\n')
@ -263,7 +258,6 @@ internal class MotionGroup : VimMotionGroupBase() {
} }
} }
} }
return TextRange(start, end) return TextRange(start, end)
} }

View File

@ -62,7 +62,6 @@ internal class IjActionExecutor : VimActionExecutor {
override val ACTION_EXPAND_REGION_RECURSIVELY: String override val ACTION_EXPAND_REGION_RECURSIVELY: String
get() = IdeActions.ACTION_EXPAND_REGION_RECURSIVELY get() = IdeActions.ACTION_EXPAND_REGION_RECURSIVELY
override val ACTION_EXPAND_COLLAPSE_TOGGLE: String override val ACTION_EXPAND_COLLAPSE_TOGGLE: String
// [VERSION UPDATE] 2024.3+ Replace raw "ExpandCollapseToggleAction" with IdeActions.ACTION_EXPAND_COLLAPSE_TOGGLE_REGION from the platform.
get() = "ExpandCollapseToggleAction" get() = "ExpandCollapseToggleAction"
/** /**

View File

@ -16,6 +16,7 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getLineEndForOffset import com.maddyhome.idea.vim.api.getLineEndForOffset
import com.maddyhome.idea.vim.api.getLineStartForOffset 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.listener.SelectionVimListenerSuppressor
import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext
import com.maddyhome.idea.vim.newapi.IjVimCaret import com.maddyhome.idea.vim.newapi.IjVimCaret
@ -93,6 +94,6 @@ internal fun VimEditor.exitSelectMode(adjustCaretPosition: Boolean) {
} }
} }
internal fun Editor.exitInsertMode(context: DataContext) { internal fun Editor.exitInsertMode(context: DataContext, operatorArguments: OperatorArguments) {
VimPlugin.getChange().processEscape(IjVimEditor(this), IjEditorExecutionContext(context)) VimPlugin.getChange().processEscape(IjVimEditor(this), IjEditorExecutionContext(context), operatorArguments)
} }

View File

@ -24,9 +24,7 @@ import com.maddyhome.idea.vim.common.InsertSequence
import com.maddyhome.idea.vim.ex.ExOutputModel import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.group.visual.VisualChange import com.maddyhome.idea.vim.group.visual.VisualChange
import com.maddyhome.idea.vim.group.visual.vimLeadSelectionOffset import com.maddyhome.idea.vim.group.visual.vimLeadSelectionOffset
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.ui.ExOutputPanel import com.maddyhome.idea.vim.ui.ExOutputPanel
@ -125,7 +123,6 @@ internal var Editor.vimExOutput: ExOutputModel? by userData()
internal var Editor.vimTestInputModel: TestInputModel? by userData() internal var Editor.vimTestInputModel: TestInputModel? by userData()
internal var Editor.vimChangeActionSwitchMode: Mode? by userData() internal var Editor.vimChangeActionSwitchMode: Mode? by userData()
internal var Editor.replaceMask: VimEditorReplaceMask? by userData()
internal var Caret.currentInsert: InsertSequence? by userData() internal var Caret.currentInsert: InsertSequence? by userData()
internal val Caret.insertHistory: MutableList<InsertSequence> by userDataOr { mutableListOf() } internal val Caret.insertHistory: MutableList<InsertSequence> by userDataOr { mutableListOf() }

View File

@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector 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.common.EditorListener
import com.maddyhome.idea.vim.helper.inInsertMode import com.maddyhome.idea.vim.helper.inInsertMode
import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.ij
@ -64,7 +65,7 @@ class IJEditorFocusListener : EditorListener {
val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor) val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor)
val mode = injector.vimState.mode val mode = injector.vimState.mode
when (mode) { when (mode) {
is Mode.INSERT -> editor.exitInsertMode(context) is Mode.INSERT -> editor.exitInsertMode(context, OperatorArguments(false, 0, mode))
else -> {} else -> {}
} }
} }
@ -78,4 +79,3 @@ class IJEditorFocusListener : EditorListener {
KeyHandler.getInstance().reset(editor) KeyHandler.getInstance().reset(editor)
} }
} }

View File

@ -16,9 +16,7 @@ import com.intellij.codeInsight.lookup.impl.actions.ChooseItemAction
import com.intellij.codeInsight.template.Template import com.intellij.codeInsight.template.Template
import com.intellij.codeInsight.template.TemplateEditingAdapter import com.intellij.codeInsight.template.TemplateEditingAdapter
import com.intellij.codeInsight.template.TemplateManagerListener import com.intellij.codeInsight.template.TemplateManagerListener
import com.intellij.codeInsight.template.impl.TemplateManagerImpl
import com.intellij.codeInsight.template.impl.TemplateState import com.intellij.codeInsight.template.impl.TemplateState
import com.intellij.codeInsight.template.impl.actions.NextVariableAction
import com.intellij.find.FindModelListener import com.intellij.find.FindModelListener
import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.ActionUpdateThread
@ -154,10 +152,6 @@ internal object IdeaSpecifics {
KeyHandler.getInstance().reset(it.vim) KeyHandler.getInstance().reset(it.vim)
} }
} }
else if (action is NextVariableAction && TemplateManagerImpl.getTemplateState(editor) == null) {
editor.vim.exitInsertMode(event.dataContext.vim)
KeyHandler.getInstance().reset(editor.vim)
}
//endregion //endregion
if (caretOffset != -1 && caretOffset != editor.caretModel.offset) { if (caretOffset != -1 && caretOffset != editor.caretModel.offset) {

View File

@ -66,6 +66,7 @@ import com.maddyhome.idea.vim.api.coerceOffset
import com.maddyhome.idea.vim.api.getLineEndForOffset import com.maddyhome.idea.vim.api.getLineEndForOffset
import com.maddyhome.idea.vim.api.getLineStartForOffset import com.maddyhome.idea.vim.api.getLineStartForOffset
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.group.EditorGroup import com.maddyhome.idea.vim.group.EditorGroup
import com.maddyhome.idea.vim.group.FileGroup import com.maddyhome.idea.vim.group.FileGroup
import com.maddyhome.idea.vim.group.IjOptions import com.maddyhome.idea.vim.group.IjOptions
@ -394,8 +395,7 @@ internal object VimListenerManager {
editor.vim.mode = Mode.NORMAL() editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim) KeyHandler.getInstance().reset(editor.vim)
} }
// Breaks relativenumber for some reason injector.scroll.scrollCaretIntoView(editor.vim)
// injector.scroll.scrollCaretIntoView(editor.vim)
} }
MotionGroup.fileEditorManagerSelectionChangedCallback(event) MotionGroup.fileEditorManagerSelectionChangedCallback(event)

View File

@ -17,24 +17,6 @@ internal class IjLiveRange(val marker: RangeMarker) : LiveRange {
override val endOffset: Int override val endOffset: Int
get() = marker.endOffset get() = marker.endOffset
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as IjLiveRange
if (startOffset != other.startOffset) return false
if (endOffset != other.endOffset) return false
return true
}
override fun hashCode(): Int {
var result = startOffset
result = 31 * result + endOffset
return result
}
} }
val RangeMarker.vim: LiveRange val RangeMarker.vim: LiveRange

View File

@ -38,12 +38,12 @@ import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVisualPosition import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.VirtualFile import com.maddyhome.idea.vim.api.VirtualFile
import com.maddyhome.idea.vim.api.injector 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
import com.maddyhome.idea.vim.common.IndentConfig.Companion.create
import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.ModeChangeListener import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
import com.maddyhome.idea.vim.common.forgetAllReplaceMasks
import com.maddyhome.idea.vim.group.visual.vimSetSystemBlockSelectionSilently import com.maddyhome.idea.vim.group.visual.vimSetSystemBlockSelectionSilently
import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.EditorHelper
import com.maddyhome.idea.vim.helper.StrictMode import com.maddyhome.idea.vim.helper.StrictMode
@ -53,7 +53,6 @@ import com.maddyhome.idea.vim.helper.fileSize
import com.maddyhome.idea.vim.helper.getTopLevelEditor import com.maddyhome.idea.vim.helper.getTopLevelEditor
import com.maddyhome.idea.vim.helper.inExMode import com.maddyhome.idea.vim.helper.inExMode
import com.maddyhome.idea.vim.helper.isTemplateActive import com.maddyhome.idea.vim.helper.isTemplateActive
import com.maddyhome.idea.vim.helper.replaceMask
import com.maddyhome.idea.vim.helper.vimChangeActionSwitchMode import com.maddyhome.idea.vim.helper.vimChangeActionSwitchMode
import com.maddyhome.idea.vim.helper.vimLastSelectionType import com.maddyhome.idea.vim.helper.vimLastSelectionType
import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl
@ -76,11 +75,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
// TBH, I don't like the names. Need to think a bit more about this // TBH, I don't like the names. Need to think a bit more about this
val editor = editor.getTopLevelEditor() val editor = editor.getTopLevelEditor()
val originalEditor = editor val originalEditor = editor
override var replaceMask: VimEditorReplaceMask?
get() = editor.replaceMask
set(value) {
editor.replaceMask = value
}
override fun updateMode(mode: Mode) { override fun updateMode(mode: Mode) {
(injector.vimState as VimStateMachineImpl).mode = mode (injector.vimState as VimStateMachineImpl).mode = mode
@ -97,7 +91,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
editor.vimChangeActionSwitchMode = value editor.vimChangeActionSwitchMode = value
} }
override val indentConfig: VimIndentConfig override val indentConfig: VimIndentConfig
get() = IndentConfig.create(editor) get() = create(editor)
override fun fileSize(): Long = editor.fileSize.toLong() override fun fileSize(): Long = editor.fileSize.toLong()
@ -404,8 +398,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight)) return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight))
} }
override fun exitInsertMode(context: ExecutionContext) { override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) {
editor.exitInsertMode(context.ij) editor.exitInsertMode(context.ij, operatorArguments)
} }
override fun exitSelectModeNative(adjustCaret: Boolean) { override fun exitSelectModeNative(adjustCaret: Boolean) {
@ -477,7 +471,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
get() = (editor as? EditorEx)?.isInsertMode ?: false get() = (editor as? EditorEx)?.isInsertMode ?: false
set(value) { set(value) {
(editor as? EditorEx)?.isInsertMode = value (editor as? EditorEx)?.isInsertMode = value
forgetAllReplaceMasks()
} }
override val document: VimDocument override val document: VimDocument

View File

@ -192,13 +192,6 @@ private object VimActionsPopup {
null, null,
), ),
) )
actionGroup.add(
HelpLink(
"Take Survey ↗",
"https://surveys.jetbrains.com/s3/ideavim-usage-survey",
AllIcons.Actions.IntentionBulb,
),
)
actionGroup.addSeparator(MessageHelper.message("action.eap.choice.active.text")) actionGroup.addSeparator(MessageHelper.message("action.eap.choice.active.text"))
actionGroup.add(JoinEap) actionGroup.add(JoinEap)

View File

@ -344,12 +344,12 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
} }
} }
// Get a snapshot of the count for the in progress command, and coerce it to 1. This value will include all // Get the current count from the command builder. This value is coerced to at least 1, so will always be valid.
// count components - selecting register(s), operator and motions. E.g. `2"a3"b4"c5d6/` will return 720. // The aggregated value includes any counts for operator and register selections, and the uncommitted count for
// If we're showing highlights for an Ex command like `:s`, the command builder will be empty, but we'll still // the search command (`/` or `?`). E.g., `2"a3"b4"c5d6/` would return 720.
// get a valid value. // If we're showing highlights for an ex command like `:s`, there won't be a command, but the value is already
int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder() // coerced to at least 1.
.calculateCount0Snapshot()); int count1 = KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getAggregatedUncommittedCount();
if ((labelText.equals("/") || labelText.equals("?") || searchCommand) && !injector.getMacro().isExecutingMacro()) { if ((labelText.equals("/") || labelText.equals("?") || searchCommand) && !injector.getMacro().isExecutingMacro()) {
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
@ -531,8 +531,9 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName()); private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName());
@NotNull
@Override @Override
public @NotNull VimCommandLineCaret getCaret() { public VimCommandLineCaret getCaret() {
return (VimCommandLineCaret) entry.getCaret(); return (VimCommandLineCaret) entry.getCaret();
} }
@ -550,8 +551,9 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
entry.clearCurrentAction(); entry.clearCurrentAction();
} }
@Nullable
@Override @Override
public @Nullable Integer getPromptCharacterOffset() { public Integer getPromptCharacterOffset() {
int offset = entry.currentActionPromptCharacterOffset; int offset = entry.currentActionPromptCharacterOffset;
return offset == -1 ? null : offset; return offset == -1 ? null : offset;
} }
@ -571,7 +573,8 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
IdeFocusManager.findInstance().requestFocus(entry, true); IdeFocusManager.findInstance().requestFocus(entry, true);
} }
public @Nullable VimInputInterceptor<?> getInputInterceptor() { @Nullable
public VimInputInterceptor<?> getInputInterceptor() {
return myInputInterceptor; return myInputInterceptor;
} }
@ -584,13 +587,15 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
myInputInterceptor = vimInputInterceptor; myInputInterceptor = vimInputInterceptor;
} }
@Nullable
@Override @Override
public @Nullable Function1<String, Unit> getInputProcessing() { public Function1<String, Unit> getInputProcessing() {
return inputProcessing; return inputProcessing;
} }
@Nullable
@Override @Override
public @Nullable Character getFinishOn() { public Character getFinishOn() {
return finishOn; return finishOn;
} }

View File

@ -44,7 +44,6 @@ viminfo
virtualedit virtualedit
visualbell visualbell
visualdelay visualdelay
whichwrap
wrapscan wrapscan
nobomb nobomb

View File

@ -4,6 +4,16 @@
"class": "com.maddyhome.idea.vim.action.change.RepeatChangeAction", "class": "com.maddyhome.idea.vim.action.change.RepeatChangeAction",
"modes": "N" "modes": "N"
}, },
{
"keys": "<BS>",
"class": "com.maddyhome.idea.vim.action.editor.VimEditorBackSpace",
"modes": "I"
},
{
"keys": "<C-H>",
"class": "com.maddyhome.idea.vim.action.editor.VimEditorBackSpace",
"modes": "I"
},
{ {
"keys": "<C-I>", "keys": "<C-I>",
"class": "com.maddyhome.idea.vim.action.editor.VimEditorTab", "class": "com.maddyhome.idea.vim.action.editor.VimEditorTab",

View File

@ -7,7 +7,6 @@
*/ */
package org.jetbrains.plugins.ideavim.action package org.jetbrains.plugins.ideavim.action
import com.intellij.idea.TestFor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo import com.maddyhome.idea.vim.state.mode.ReturnTo
@ -1069,14 +1068,4 @@ foobaz
Mode.NORMAL(), Mode.NORMAL(),
) )
} }
@Test
@TestFor(issues = ["VIM-2074"])
fun `backspace with replace mode`() {
configureByText("${c}Hello world")
typeText("R1111")
assertState("1111o world")
typeText("<BS><BS><BS>")
assertState("1ello world")
}
} }

View File

@ -109,7 +109,7 @@ class CopyActionTest : VimTestCase() {
@TestWithoutNeovim(reason = SkipNeovimReason.DIFFERENT) @TestWithoutNeovim(reason = SkipNeovimReason.DIFFERENT)
@Test @Test
fun testYankRegisterUsesLastEnteredRegister() { fun testYankRegisterUsesLastEnteredRegister() {
typeTextInFile("\"a\"byl" + "\"bp", "hel<caret>lo world\n") typeTextInFile("\"a\"byl" + "\"ap", "hel<caret>lo world\n")
assertState("helllo world\n") assertState("helllo world\n")
} }

View File

@ -85,33 +85,4 @@ class MotionBackspaceActionTest : VimTestCase() {
enterCommand("set whichwrap=b") enterCommand("set whichwrap=b")
} }
} }
@TestWithoutNeovim(SkipNeovimReason.OPTION)
@Test
fun `test backspace motion with operator`() {
doTest(
"d<BS>",
"""
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<BS>",
"""
lorem ipsum dolor sit amet
${c}lorem ipsum dolor sit amet
""".trimIndent(),
"""
lorem ipsum dolor sit amet${c}lorem ipsum dolor sit amet
""".trimIndent(),
)
}
} }

View File

@ -85,35 +85,4 @@ class MotionSpaceActionTest : VimTestCase() {
enterCommand("set whichwrap=s") enterCommand("set whichwrap=s")
} }
} }
@Suppress("SpellCheckingInspection")
@TestWithoutNeovim(SkipNeovimReason.OPTION)
@Test
fun `test space motion with operator`() {
doTest(
"d<Space>",
"""
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<Space>",
"""
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(),
)
}
} }

View File

@ -15,7 +15,7 @@ class VimVariableServiceTest : VimTestCase() {
@Test @Test
fun `test v count variable without count specified`() { fun `test v count variable without count specified`() {
configureByText("\n") configureByText("\n")
enterCommand("""nnoremap <expr> n ':echo ' .. v:count .. "\<CR>"""") enterCommand("nnoremap <expr> n ':echo ' .. v:count .. \"\\<CR>\"")
typeText("n") typeText("n")
assertExOutput("0") assertExOutput("0")
} }
@ -23,31 +23,15 @@ class VimVariableServiceTest : VimTestCase() {
@Test @Test
fun `test v count variable`() { fun `test v count variable`() {
configureByText("\n") configureByText("\n")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count .. "\<CR>"""") enterCommand("nnoremap <expr> n ':' .. \"\\<C-u>\" .. 'echo ' .. v:count .. \"\\<CR>\"")
typeText("5n") typeText("5n")
assertExOutput("5") assertExOutput("5")
} }
@Test
fun `test v count variable with additional count during select register`() {
configureByText("\n")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count .. "\<CR>"""")
typeText("2\"a5n")
assertExOutput("10")
}
@Test
fun `test v count variable with additional pathological count during select register`() {
configureByText("\n")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count .. "\<CR>"""")
typeText("2\"a3\"b4\"c5n")
assertExOutput("120")
}
@Test @Test
fun `test v count1 variable without count specified`() { fun `test v count1 variable without count specified`() {
configureByText("\n") configureByText("\n")
enterCommand("""nnoremap <expr> n ':echo ' .. v:count1 .. "\<CR>"""") enterCommand("nnoremap <expr> n ':echo ' .. v:count1 .. \"\\<CR>\"")
typeText("n") typeText("n")
assertExOutput("1") assertExOutput("1")
} }
@ -55,27 +39,11 @@ class VimVariableServiceTest : VimTestCase() {
@Test @Test
fun `test v count1 variable`() { fun `test v count1 variable`() {
configureByText("\n") configureByText("\n")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count1 .. "\<CR>"""") enterCommand("nnoremap <expr> n ':' .. \"\\<C-u>\" .. 'echo ' .. v:count1 .. \"\\<CR>\"")
typeText("5n") typeText("5n")
assertExOutput("5") assertExOutput("5")
} }
@Test
fun `test v count1 variable with additional count during select register`() {
configureByText("\n")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count1 .. "\<CR>"""")
typeText("2\"a5n")
assertExOutput("10")
}
@Test
fun `test v count1 variable with additional pathological count during select register`() {
configureByText("\n")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count1 .. "\<CR>"""")
typeText("2\"a3\"b4\"c5n")
assertExOutput("120")
}
@Test @Test
fun `test mapping with updating jumplist`() { fun `test mapping with updating jumplist`() {
configureByText("${c}1\n2\n3\n4\n5\n6\n7\n8\n9\n") configureByText("${c}1\n2\n3\n4\n5\n6\n7\n8\n9\n")

View File

@ -141,16 +141,6 @@ Mode.INSERT,
) )
} }
@Test
fun testDeleteWithMultipleCounts() {
doTest(
"2d2aa",
"function(int <caret>arg1, char* arg<caret>2=\"a,b,c(d,e)\", bool arg3, string arg4, int arg5)",
"function(<caret>)",
Mode.NORMAL(),
)
}
@Test @Test
fun testSelectTwoArguments() { fun testSelectTwoArguments() {
doTest( doTest(

View File

@ -95,7 +95,7 @@ private class AvailableActions(private val editor: Editor) : ImperativeCommand {
val currentNode = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentTrie() val currentNode = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentTrie()
// Note: esc is always an option // Note: esc is always an option
val possibleKeys = (currentNode.children.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } val possibleKeys = (currentNode.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) }
println("Keys: ${possibleKeys.joinToString(", ")}") println("Keys: ${possibleKeys.joinToString(", ")}")
val keyGenerator = Generator.integers(0, possibleKeys.lastIndex) val keyGenerator = Generator.integers(0, possibleKeys.lastIndex)
.suchThat { injector.parser.toKeyNotation(possibleKeys[it]) !in stinkyKeysList } .suchThat { injector.parser.toKeyNotation(possibleKeys[it]) !in stinkyKeysList }

View File

@ -17,13 +17,14 @@ import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.command.MappingProcessor import com.maddyhome.idea.vim.command.MappingProcessor
import com.maddyhome.idea.vim.command.OperatorArguments 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.VimLogger
import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.trace
import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.impl.state.toMappingMode 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.KeyConsumer
import com.maddyhome.idea.vim.key.KeyStack 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.CharArgumentConsumer
import com.maddyhome.idea.vim.key.consumers.CommandConsumer import com.maddyhome.idea.vim.key.consumers.CommandConsumer
import com.maddyhome.idea.vim.key.consumers.CommandCountConsumer import com.maddyhome.idea.vim.key.consumers.CommandCountConsumer
@ -196,9 +197,11 @@ class KeyHandler {
} }
private fun onUnknownKey(editor: VimEditor, keyState: KeyHandlerState) { private fun onUnknownKey(editor: VimEditor, keyState: KeyHandlerState) {
logger.trace("Command builder is set to BAD")
keyState.commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
editor.resetOpPending() editor.resetOpPending()
injector.vimState.resetRegisterPending()
editor.isReplaceCharacter = false editor.isReplaceCharacter = false
// Note that this will also reset the CommandBuilder to NEW_COMMAND
reset(keyState, editor.mode) reset(keyState, editor.mode)
} }
@ -207,6 +210,14 @@ class KeyHandler {
injector.messages.indicateError() 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( private fun executeCommand(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
@ -215,7 +226,11 @@ class KeyHandler {
) { ) {
logger.trace("Command execution") logger.trace("Command execution")
val command = keyState.commandBuilder.buildCommand() val command = keyState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(command.rawCount, editorState.mode) val operatorArguments = OperatorArguments(
editor.mode is Mode.OP_PENDING,
command.rawCount,
editorState.mode,
)
// If we were in "operator pending" mode, reset back to normal 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` // But opening command line should not reset operator pending mode (e.g. `d/foo`
@ -280,7 +295,7 @@ class KeyHandler {
keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode())) keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode()))
} }
private fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> { private fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> {
return injector.keyGroup.getKeyRoot(mappingMode) return injector.keyGroup.getKeyRoot(mappingMode)
} }
@ -326,7 +341,7 @@ class KeyHandler {
) : Runnable { ) : Runnable {
override fun run() { override fun run() {
val editorState = injector.vimState val editorState = injector.vimState
keyState.commandBuilder.commandState = CurrentCommandState.NEW_COMMAND
val register = cmd.register val register = cmd.register
if (register != null) { if (register != null) {
injector.registerGroup.selectRegister(register) injector.registerGroup.selectRegister(register)
@ -346,15 +361,22 @@ class KeyHandler {
// mode we were in. This handles commands in those modes that temporarily allow us to execute normal // 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 // mode commands. An exception is if this command should leave us in the temporary mode such as
// "select register" // "select register"
if (editorState.mode is Mode.NORMAL && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) { val myMode = editorState.mode
when (editorState.mode.returnTo) { val returnTo = myMode.returnTo
ReturnTo.INSERT -> editor.mode = Mode.INSERT if (myMode is Mode.NORMAL && returnTo != null && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) {
ReturnTo.REPLACE -> editor.mode = Mode.REPLACE when (returnTo) {
null -> {} ReturnTo.INSERT -> {
editor.mode = Mode.INSERT
}
ReturnTo.REPLACE -> {
editor.mode = Mode.REPLACE
}
} }
} }
if (keyState.commandBuilder.isDone()) {
instance.reset(keyState, editorState.mode) getInstance().reset(keyState, editorState.mode)
}
} }
} }

View File

@ -17,17 +17,23 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.lineLength import com.maddyhome.idea.vim.api.lineLength
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command 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.command.OperatorArguments
import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.KeyHandlerState
import java.util.*
@CommandOrMotion(keys = ["r"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["r"], modes = [Mode.NORMAL])
class ChangeCharacterAction : ChangeEditorActionHandler.ForEachCaret() { class ChangeCharacterAction : ChangeEditorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE
override val argumentType: Argument.Type = Argument.Type.DIGRAPH override val argumentType: Argument.Type = Argument.Type.DIGRAPH
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_ALLOW_DIGRAPH)
override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
editor.isReplaceCharacter = true editor.isReplaceCharacter = true
} }
@ -39,7 +45,7 @@ class ChangeCharacterAction : ChangeEditorActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
return argument is Argument.Character && changeCharacter(editor, caret, operatorArguments.count1, argument.character) return argument != null && changeCharacter(editor, caret, operatorArguments.count1, argument.character)
} }
} }

View File

@ -37,11 +37,12 @@ class ChangeLineAction : ChangeInInsertSequenceAction() {
): Boolean { ): Boolean {
// `S` command is a synonym of `cc` // `S` command is a synonym of `cc`
val motion = MotionDownLess1FirstNonSpaceAction() val motion = MotionDownLess1FirstNonSpaceAction()
val command = Command(1, motion, motion.type, motion.flags)
return injector.changeGroup.changeMotion( return injector.changeGroup.changeMotion(
editor, editor,
caret, caret,
context, context,
Argument.Motion(motion, null), Argument(command),
operatorArguments, operatorArguments,
) )
} }

View File

@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler
@CommandOrMotion(keys = ["R"], modes = [Mode.NORMAL]) @CommandOrMotion(keys = ["R"], modes = [Mode.NORMAL])
@ -40,5 +39,4 @@ class ChangeReplaceAction : ChangeEditorActionHandler.SingleExecution() {
*/ */
private fun changeReplace(editor: VimEditor, context: ExecutionContext) { private fun changeReplace(editor: VimEditor, context: ExecutionContext) {
injector.changeGroup.initInsert(editor, context, com.maddyhome.idea.vim.state.mode.Mode.REPLACE) injector.changeGroup.initInsert(editor, context, com.maddyhome.idea.vim.state.mode.Mode.REPLACE)
editor.replaceMask = VimEditorReplaceMask()
} }

View File

@ -39,6 +39,7 @@ class ChangeVisualAction : VisualOperatorActionHandler.ForEachCaret() {
range.toVimTextRange(false), range.toVimTextRange(false),
range.type, range.type,
context, context,
operatorArguments,
) )
} }
} }

View File

@ -15,13 +15,16 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command 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.command.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.group.visual.VimSelection import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler import com.maddyhome.idea.vim.handler.VisualOperatorActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.KeyHandlerState
import java.util.*
/** /**
* @author vlan * @author vlan
@ -29,8 +32,11 @@ import com.maddyhome.idea.vim.state.KeyHandlerState
@CommandOrMotion(keys = ["r"], modes = [Mode.VISUAL]) @CommandOrMotion(keys = ["r"], modes = [Mode.VISUAL])
class ChangeVisualCharacterAction : VisualOperatorActionHandler.ForEachCaret() { class ChangeVisualCharacterAction : VisualOperatorActionHandler.ForEachCaret() {
override val type: Command.Type = Command.Type.CHANGE override val type: Command.Type = Command.Type.CHANGE
override val argumentType: Argument.Type = Argument.Type.DIGRAPH override val argumentType: Argument.Type = Argument.Type.DIGRAPH
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_ALLOW_DIGRAPH)
override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) { override fun onStartWaitingForArgument(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
editor.isReplaceCharacter = true editor.isReplaceCharacter = true
} }
@ -44,7 +50,7 @@ class ChangeVisualCharacterAction : VisualOperatorActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val argument = cmd.argument val argument = cmd.argument
return argument is Argument.Character && return argument != null &&
changeCharacterRange(editor, caret, range.toVimTextRange(false), argument.character) changeCharacterRange(editor, caret, range.toVimTextRange(false), argument.character)
} }
} }

View File

@ -56,6 +56,7 @@ class ChangeVisualLinesAction : VisualOperatorActionHandler.ForEachCaret() {
lineRange, lineRange,
SelectionType.LINE_WISE, SelectionType.LINE_WISE,
context, context,
operatorArguments,
) )
} }
} }

View File

@ -53,7 +53,7 @@ class ChangeVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() {
} }
} }
val blockRange = TextRange(starts, ends) val blockRange = TextRange(starts, ends)
injector.changeGroup.changeRange(editor, caret, blockRange, SelectionType.BLOCK_WISE, context) injector.changeGroup.changeRange(editor, caret, blockRange, SelectionType.BLOCK_WISE, context, operatorArguments)
} else { } else {
val lineEndForOffset = editor.getLineEndForOffset(vimTextRange.endOffset) val lineEndForOffset = editor.getLineEndForOffset(vimTextRange.endOffset)
val endsWithNewLine = if (lineEndForOffset.toLong() == editor.fileSize()) 0 else 1 val endsWithNewLine = if (lineEndForOffset.toLong() == editor.fileSize()) 0 else 1
@ -61,7 +61,7 @@ class ChangeVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() {
editor.getLineStartForOffset(vimTextRange.startOffset), editor.getLineStartForOffset(vimTextRange.startOffset),
lineEndForOffset + endsWithNewLine, lineEndForOffset + endsWithNewLine,
) )
injector.changeGroup.changeRange(editor, caret, lineRange, SelectionType.LINE_WISE, context) injector.changeGroup.changeRange(editor, caret, lineRange, SelectionType.LINE_WISE, context, operatorArguments)
} }
} }
} }

View File

@ -31,7 +31,7 @@ class FilterVisualLinesAction : VimActionHandler.SingleExecution(), FilterComman
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { 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 `!` // Start ex entry with the initial text set to the calculated range and `!`
startFilterCommand(editor, context, cmd.rawCount) startFilterCommand(editor, context, cmd)
return true 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 `!` // 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 val count = if (start.line < end.line) end.line - start.line + 1 else 1
startFilterCommand(editor, context, count) startFilterCommand(editor, context, Argument.EMPTY_COMMAND.copy(rawCount = count))
return true return true
} }
} }
interface FilterCommand { interface FilterCommand {
fun startFilterCommand(editor: VimEditor, context: ExecutionContext, count0: Int) { fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) {
injector.commandLine.createCommandPrompt(editor, context, count0, initialText = "!") injector.commandLine.createCommandPrompt(editor, context, cmd, initialText = "!")
} }
} }

View File

@ -38,6 +38,6 @@ class DeleteMotionAction : ChangeEditorActionHandler.ForEachCaret(), DuplicableO
val (range, selectionType) = injector.changeGroup val (range, selectionType) = injector.changeGroup
.getDeleteRangeAndType(editor, caret, context, argument, false, operatorArguments) .getDeleteRangeAndType(editor, caret, context, argument, false, operatorArguments)
?: return false ?: return false
return injector.changeGroup.deleteRange(editor, caret, range, selectionType, false) return injector.changeGroup.deleteRange(editor, caret, range, selectionType, false, operatorArguments)
} }
} }

View File

@ -40,6 +40,7 @@ class DeleteVisualAction : VisualOperatorActionHandler.ForEachCaret() {
range.toVimTextRange(false), range.toVimTextRange(false),
selectionType, selectionType,
false, false,
operatorArguments,
) )
} }
} }

View File

@ -56,6 +56,6 @@ class DeleteVisualLinesAction : VisualOperatorActionHandler.ForEachCaret() {
Triple(caret, lineRange, SelectionType.LINE_WISE) Triple(caret, lineRange, SelectionType.LINE_WISE)
} }
} }
return injector.changeGroup.deleteRange(editor, usedCaret, usedRange, usedType, false) return injector.changeGroup.deleteRange(editor, usedCaret, usedRange, usedType, false, operatorArguments)
} }
} }

View File

@ -58,6 +58,7 @@ class DeleteVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() {
blockRange, blockRange,
SelectionType.BLOCK_WISE, SelectionType.BLOCK_WISE,
false, false,
operatorArguments,
) )
} else { } else {
val lineEndForOffset = editor.getLineEndForOffset(vimTextRange.endOffset) val lineEndForOffset = editor.getLineEndForOffset(vimTextRange.endOffset)
@ -66,7 +67,7 @@ class DeleteVisualLinesEndAction : VisualOperatorActionHandler.ForEachCaret() {
editor.getLineStartForOffset(vimTextRange.startOffset), editor.getLineStartForOffset(vimTextRange.startOffset),
lineEndForOffset + endsWithNewLine, lineEndForOffset + endsWithNewLine,
) )
injector.changeGroup.deleteRange(editor, caret, lineRange, SelectionType.LINE_WISE, false) injector.changeGroup.deleteRange(editor, caret, lineRange, SelectionType.LINE_WISE, false, operatorArguments)
} }
} }
} }

View File

@ -1,40 +0,0 @@
/*
* 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.change.insert
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.VimEditor
import com.maddyhome.idea.vim.api.getLineStartForOffset
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["<C-H>", "<BS>"], modes = [Mode.INSERT])
internal class InsertBackspaceAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_WRITABLE
override fun execute( editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments, ): Boolean {
if (editor.insertMode) {
injector.changeGroup.processBackspace(editor, context)
} else {
for (caret in editor.carets()) {
val offset = (caret.offset - 1).takeIf { it > 0 } ?: continue
val oldChar = editor.replaceMask?.popChange(editor, offset)
if (oldChar != null) {
injector.changeGroup.replaceText(editor, caret, offset, offset + 1, oldChar.toString())
}
caret.moveToOffset(offset)
}
}
return true
}
}

View File

@ -52,8 +52,7 @@ class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { 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 // The converted digraph character has been captured as an argument, push it back through key handler
val argument = cmd.argument as? Argument.Character ?: return false val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character)
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance() val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState) keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState)
return true return true

View File

@ -52,8 +52,7 @@ class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { 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 // The converted literal character has been captured as an argument, push it back through key handler
val argument = cmd.argument as? Argument.Character ?: return false val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character)
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance() val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState) keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState)
return true return true

View File

@ -36,7 +36,7 @@ class InsertDeleteInsertedTextAction : ChangeEditorActionHandler.ForEachCaret()
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
return insertDeleteInsertedText(editor, caret) return insertDeleteInsertedText(editor, caret, operatorArguments)
} }
} }
@ -48,7 +48,11 @@ class InsertDeleteInsertedTextAction : ChangeEditorActionHandler.ForEachCaret()
* @param caret The caret on which the action is performed * @param caret The caret on which the action is performed
* @return true if able to delete the text, false if not * @return true if able to delete the text, false if not
*/ */
private fun insertDeleteInsertedText(editor: VimEditor, caret: VimCaret): Boolean { private fun insertDeleteInsertedText(
editor: VimEditor,
caret: VimCaret,
operatorArguments: OperatorArguments,
): Boolean {
var deleteTo = caret.vimInsertStart.startOffset var deleteTo = caret.vimInsertStart.startOffset
val offset = caret.offset val offset = caret.offset
if (offset == deleteTo) { if (offset == deleteTo) {
@ -61,6 +65,7 @@ private fun insertDeleteInsertedText(editor: VimEditor, caret: VimCaret): Boolea
TextRange(deleteTo, offset), TextRange(deleteTo, offset),
SelectionType.CHARACTER_WISE, SelectionType.CHARACTER_WISE,
false, false,
operatorArguments,
) )
return true return true
} }

View File

@ -37,7 +37,7 @@ class InsertDeletePreviousWordAction : ChangeEditorActionHandler.ForEachCaret()
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
return insertDeletePreviousWord(editor, caret) return insertDeletePreviousWord(editor, caret, operatorArguments)
} }
} }
@ -50,7 +50,7 @@ class InsertDeletePreviousWordAction : ChangeEditorActionHandler.ForEachCaret()
* @param editor The editor to delete the text from * @param editor The editor to delete the text from
* @return true if able to delete text, false if not * @return true if able to delete text, false if not
*/ */
private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret): Boolean { private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret, operatorArguments: OperatorArguments): Boolean {
val deleteTo: Int = if (caret.getBufferPosition().column == 0) { val deleteTo: Int = if (caret.getBufferPosition().column == 0) {
caret.offset - 1 caret.offset - 1
} else { } else {
@ -74,6 +74,6 @@ private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret): Boolea
return false return false
} }
val range = TextRange(deleteTo, caret.offset) val range = TextRange(deleteTo, caret.offset)
injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true) injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true, operatorArguments)
return true return true
} }

View File

@ -18,10 +18,14 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.RWLockLabel 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.put.PutData
import com.maddyhome.idea.vim.register.Register import com.maddyhome.idea.vim.register.Register
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.vimscript.model.Script import com.maddyhome.idea.vim.vimscript.model.Script
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
@CommandOrMotion(keys = ["<C-R>"], modes = [Mode.INSERT]) @CommandOrMotion(keys = ["<C-R>"], modes = [Mode.INSERT])
class InsertRegisterAction : VimActionHandler.SingleExecution() { class InsertRegisterAction : VimActionHandler.SingleExecution() {
@ -35,8 +39,9 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
cmd: Command, cmd: Command,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val argument = cmd.argument as? Argument.Character ?: return false val argument = cmd.argument
if (argument.character == '=') {
if (argument?.character == '=') {
injector.commandLine.readInputAndProcess(editor, context, "=", finishOn = null) { input -> injector.commandLine.readInputAndProcess(editor, context, "=", finishOn = null) { input ->
try { try {
if (input.isNotEmpty()) { if (input.isNotEmpty()) {
@ -45,7 +50,7 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
val textToStore = expression.toInsertableString() val textToStore = expression.toInsertableString()
injector.registerGroup.storeTextSpecial('=', textToStore) injector.registerGroup.storeTextSpecial('=', textToStore)
} }
insertRegister(editor, context, '=') insertRegister(editor, context, '=', operatorArguments)
} catch (e: ExException) { } catch (e: ExException) {
injector.messages.indicateError() injector.messages.indicateError()
injector.messages.showStatusBarMessage(editor, e.message) injector.messages.showStatusBarMessage(editor, e.message)
@ -53,7 +58,7 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
} }
return true return true
} else { } else {
return insertRegister(editor, context, argument.character) return argument != null && insertRegister(editor, context, argument.character, operatorArguments)
} }
} }
} }
@ -67,13 +72,18 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
* @return true if able to insert the register contents, false if not * @return true if able to insert the register contents, false if not
*/ */
@RWLockLabel.SelfSynchronized @RWLockLabel.SelfSynchronized
private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean { private fun insertRegister(
editor: VimEditor,
context: ExecutionContext,
key: Char,
operatorArguments: OperatorArguments,
): Boolean {
val register: Register? = injector.registerGroup.getRegister(key) val register: Register? = injector.registerGroup.getRegister(key)
if (register != null) { if (register != null) {
val text = register.rawText ?: injector.parser.toPrintableString(register.keys) val text = register.rawText ?: injector.parser.toPrintableString(register.keys)
val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), register.name) val textData = PutData.TextData(text, SelectionType.CHARACTER_WISE, emptyList(), register.name)
val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true) val putData = PutData(textData, null, 1, insertTextBeforeCaret = true, rawIndent = true, caretAfterInsertedText = true)
injector.put.putText(editor, context, putData) injector.put.putText(editor, context, putData, operatorArguments = operatorArguments)
return true return true
} }
return false return false

View File

@ -36,7 +36,7 @@ class VisualBlockAppendAction : VisualOperatorActionHandler.SingleExecution() {
if (editor.isOneLineMode()) return false if (editor.isOneLineMode()) return false
val range = caretsAndSelections.values.stream().findFirst().orElse(null) ?: return false val range = caretsAndSelections.values.stream().findFirst().orElse(null) ?: return false
return if (range.type == SelectionType.BLOCK_WISE) { return if (range.type == SelectionType.BLOCK_WISE) {
injector.changeGroup.initBlockInsert(editor, context, range.toVimTextRange(false), true) injector.changeGroup.blockInsert(editor, context, range.toVimTextRange(false), true, operatorArguments)
} else { } else {
injector.changeGroup.insertAfterLineEnd(editor, context) injector.changeGroup.insertAfterLineEnd(editor, context)
true true

View File

@ -36,7 +36,7 @@ class VisualBlockInsertAction : VisualOperatorActionHandler.SingleExecution() {
if (editor.isOneLineMode()) return false if (editor.isOneLineMode()) return false
val vimSelection = caretsAndSelections.values.stream().findFirst().orElse(null) ?: return false val vimSelection = caretsAndSelections.values.stream().findFirst().orElse(null) ?: return false
return if (vimSelection.type == SelectionType.BLOCK_WISE) { return if (vimSelection.type == SelectionType.BLOCK_WISE) {
injector.changeGroup.initBlockInsert(editor, context, vimSelection.toVimTextRange(false), false) injector.changeGroup.blockInsert(editor, context, vimSelection.toVimTextRange(false), false, operatorArguments)
} else { } else {
injector.changeGroup.insertBeforeFirstNonBlank(editor, context) injector.changeGroup.insertBeforeFirstNonBlank(editor, context)
true true

View File

@ -52,7 +52,7 @@ sealed class PutTextBaseAction(
} }
result result
} else { } else {
injector.put.putText(editor, context, getPutData(count)) injector.put.putText(editor, context, getPutData(count), operatorArguments)
} }
} }

View File

@ -26,7 +26,7 @@ class ExEntryAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
if (editor.isOneLineMode()) return false if (editor.isOneLineMode()) return false
injector.commandLine.createCommandPrompt(editor, context, cmd.rawCount, initialText = "") injector.commandLine.createCommandPrompt(editor, context, cmd, initialText = "")
return true return true
} }
} }

View File

@ -38,8 +38,7 @@ class InsertRegisterAction: VimActionHandler.SingleExecution() {
val caretOffset = cmdLine.caret.offset val caretOffset = cmdLine.caret.offset
val argument = cmd.argument as? Argument.Character ?: return false val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character)
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val pasteContent = if ((keyStroke.modifiers and KeyEvent.CTRL_DOWN_MASK) == 0) { val pasteContent = if ((keyStroke.modifiers and KeyEvent.CTRL_DOWN_MASK) == 0) {
injector.registerGroup.getRegister(keyStroke.keyChar)?.text injector.registerGroup.getRegister(keyStroke.keyChar)?.text
} else { } else {

View File

@ -13,7 +13,6 @@ import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector 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.Command
import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
@ -28,8 +27,8 @@ class LeaveCommandLineAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_READONLY override val type: Command.Type = Command.Type.OTHER_READONLY
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
val argument = cmd.argument as? Argument.ExString ?: return true val argument = cmd.argument ?: return true
val historyType = VimHistory.Type.getTypeByLabel(argument.label.toString()) val historyType = VimHistory.Type.getTypeByLabel(argument.character.toString())
injector.historyGroup.addEntry(historyType, argument.string) injector.historyGroup.addEntry(historyType, argument.string)
return true return true
} }

View File

@ -34,9 +34,8 @@ class ProcessExEntryAction : MotionActionHandler.AmbiguousExecution() {
override var motionType: MotionType = MotionType.EXCLUSIVE override var motionType: MotionType = MotionType.EXCLUSIVE
override fun getMotionActionHandler(argument: Argument?): MotionActionHandler { override fun getMotionActionHandler(argument: Argument?): MotionActionHandler {
check(argument is Argument.ExString) if (argument?.processing != null) return ExecuteDefinedInputProcessingAction()
if (argument.processing != null) return ExecuteDefinedInputProcessingAction() return if (argument?.character == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction(this)
return if (argument.label == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction(this)
} }
} }
@ -49,7 +48,7 @@ class ExecuteDefinedInputProcessingAction : MotionActionHandler.SingleExecution(
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.ExString) return Motion.Error if (argument == null) return Motion.Error
val input = argument.string val input = argument.string
val processing = argument.processing!! val processing = argument.processing!!
@ -63,11 +62,11 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
get() = throw RuntimeException("Parent motion type should be used, as only it is accessed by other code") 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 { override fun getOffset(editor: VimEditor, caret: ImmutableVimCaret, context: ExecutionContext, argument: Argument?, operatorArguments: OperatorArguments): Motion {
if (argument !is Argument.ExString) return Motion.Error if (argument == null) return Motion.Error
val offsetAndMotion = when (argument.label) { val offsetAndMotion = when (argument.character) {
'/' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.FORWARDS) '/' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.FORWARDS)
'?' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.BACKWARDS) '?' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.BACKWARDS)
else -> throw ExException("Unexpected search label ${argument.label}") else -> throw ExException("Unexpected search label ${argument.character}")
} }
if (offsetAndMotion == null) return Motion.Error if (offsetAndMotion == null) return Motion.Error
parentAction.motionType = offsetAndMotion.second parentAction.motionType = offsetAndMotion.second
@ -79,7 +78,7 @@ class ProcessExCommandEntryAction : MotionActionHandler.SingleExecution() {
override val motionType: MotionType = MotionType.LINE_WISE override val motionType: MotionType = MotionType.LINE_WISE
override fun getOffset(editor: VimEditor, context: ExecutionContext, argument: Argument?, operatorArguments: OperatorArguments): Motion { override fun getOffset(editor: VimEditor, context: ExecutionContext, argument: Argument?, operatorArguments: OperatorArguments): Motion {
if (argument !is Argument.ExString) return Motion.Error if (argument == null) return Motion.Error
try { try {
// Exit Command-line mode and return to the previous mode before executing the command (this is set to Normal in // Exit Command-line mode and return to the previous mode before executing the command (this is set to Normal in

View File

@ -31,7 +31,7 @@ class PlaybackRegisterAction : VimActionHandler.SingleExecution() {
cmd: Command, cmd: Command,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val argument = cmd.argument as? Argument.Character ?: return false val argument = cmd.argument ?: return false
val reg = argument.character val reg = argument.character
val application = injector.application val application = injector.application
val res = arrayOf(false) 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 if (reg != '@') { // @ is not a register itself, it just tells vim to use the last register
injector.macro.lastRegister = reg injector.macro.lastRegister = reg
} }
} catch (_: ExException) { } catch (e: ExException) {
res[0] = false res[0] = false
} }
} }

View File

@ -26,7 +26,7 @@ class ToggleRecordingAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
return if (!injector.registerGroup.isRecording) { return if (!injector.registerGroup.isRecording) {
val argument = cmd.argument as? Argument.Character ?: return false val argument = cmd.argument ?: return false
val reg = argument.character val reg = argument.character
injector.registerGroup.startRecording(reg) injector.registerGroup.startRecording(reg)
} else { } else {

View File

@ -19,21 +19,10 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion 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.handler.NonShiftedSpecialKeyHandler
private fun doMotion( @CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
editor: VimEditor, class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() {
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 val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion( override fun motion(
@ -43,38 +32,8 @@ abstract class MotionNonShiftedArrowLeftBaseAction() : NonShiftedSpecialKeyHandl
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
return doMotion(editor, caret, -operatorArguments.count1, "<", allowPastEnd) val allowWrap = injector.options(editor).whichwrap.contains("<")
} val allowEnd = operatorArguments.isOperatorPending // d<Left> deletes \n with wrap enabled
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap)
protected open val allowPastEnd: Boolean = false
}
// Note that Select mode is handled in [SelectMotionArrowLeftAction]
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionArrowLeftAction : MotionNonShiftedArrowLeftBaseAction()
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], 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<Left>` 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 = ["<Left>", "<kLeft>"], 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)
} }
} }

View File

@ -19,23 +19,12 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion 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.handler.NonShiftedSpecialKeyHandler
import com.maddyhome.idea.vim.helper.isEndAllowed import com.maddyhome.idea.vim.helper.isEndAllowed
import com.maddyhome.idea.vim.helper.usesVirtualSpace import com.maddyhome.idea.vim.helper.usesVirtualSpace
private fun doMotion( @CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
editor: VimEditor, class MotionArrowRightAction : NonShiftedSpecialKeyHandler() {
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 val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion( override fun motion(
@ -45,38 +34,9 @@ abstract class MotionNonShiftedArrowRightBaseAction() : NonShiftedSpecialKeyHand
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
return doMotion(editor, caret, operatorArguments.count1, ">", allowPastEnd(editor)) val allowPastEnd = editor.usesVirtualSpace || editor.isEndAllowed ||
} operatorArguments.isOperatorPending // because of `d<Right>` removing the last character
val allowWrap = injector.options(editor).whichwrap.contains(">")
protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd, allowWrap)
}
// Note that Select mode is handled with [SelectMotionArrowRightAction]
@CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionArrowRightAction : MotionNonShiftedArrowRightBaseAction()
@CommandOrMotion(keys = ["<Right>", "<kRight>"], 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<Right>` 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 = ["<Right>", "<kRight>"], 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)
} }
} }

View File

@ -20,8 +20,8 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
@CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.NORMAL, Mode.VISUAL]) @CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() { class MotionBackspaceAction : MotionActionHandler.ForEachCaret() {
override fun getOffset( override fun getOffset(
editor: VimEditor, editor: VimEditor,
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
@ -30,15 +30,24 @@ open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : Mo
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("b") val allowWrap = injector.options(editor).whichwrap.contains("b")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap) return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd = false, allowWrap)
} }
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
} }
// When the motion is used with an operator, the EOL character is counted. @CommandOrMotion(keys = ["<Space>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
// This allows e.g., `d<BS>` to delete the end of line character on the previous line when wrap is active class MotionSpaceAction : MotionActionHandler.ForEachCaret() {
// ('whichwrap' contains "b") override fun getOffset(
// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators editor: VimEditor,
@CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.OP_PENDING]) caret: ImmutableVimCaret,
class MotionBackspaceOpPendingModeAction : MotionBackspaceAction(allowPastEnd = true) 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
}

View File

@ -27,9 +27,13 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.helper.isEndAllowed import com.maddyhome.idea.vim.helper.isEndAllowed
import java.util.* import java.util.*
abstract class MotionLastColumnBaseAction(private val isMotionForOperator: Boolean = false) @CommandOrMotion(keys = ["<End>"], modes = [Mode.INSERT])
: MotionActionHandler.ForEachCaret() { class MotionLastColumnInsertAction : MotionLastColumnAction() {
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_SAVE_STROKE)
}
@CommandOrMotion(keys = ["$"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
open class MotionLastColumnAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.INCLUSIVE override val motionType: MotionType = MotionType.INCLUSIVE
override fun getOffset( override fun getOffset(
@ -39,26 +43,13 @@ abstract class MotionLastColumnBaseAction(private val isMotionForOperator: Boole
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowPastEnd = if (editor.inVisualMode) { val allow = if (editor.inVisualMode) {
injector.options(editor).selection != "old" injector.options(editor).selection != "old"
} else { } else {
// Don't allow past end if this motion is for an operator. I.e., for something like `d$`, we don't want to delete if (operatorArguments.isOperatorPending) false else editor.isEndAllowed
// the end of line character
if (isMotionForOperator) false else editor.isEndAllowed
} }
val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allowPastEnd) val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allow)
return Motion.AdjustedOffset(offset, VimMotionGroupBase.LAST_COLUMN) 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 = ["<End>"], modes = [Mode.INSERT])
class MotionLastColumnInsertAction : MotionLastColumnAction() {
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_SAVE_STROKE)
}

View File

@ -21,7 +21,8 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionActionHandler.ForEachCaret() { @CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionLeftAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset( override fun getOffset(
@ -32,16 +33,23 @@ abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionA
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("h") val allowWrap = injector.options(editor).whichwrap.contains("h")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap) val allowEnd = operatorArguments.isOperatorPending // dh deletes \n with wrap enabled
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap)
} }
} }
@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL]) @CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.INSERT])
class MotionLeftAction : MotionLeftBaseAction(allowPastEnd = false) class MotionLeftInsertModeAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
// When the motion is used with an operator, the EOL character is counted. override fun getOffset(
// This allows e.g., `dh` to delete the end of line character on the previous line when wrap is active editor: VimEditor,
// ('whichwrap' contains "h") caret: ImmutableVimCaret,
// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators context: ExecutionContext,
@CommandOrMotion(keys = ["h"], modes = [Mode.OP_PENDING]) argument: Argument?,
class MotionLeftOpPendingModeAction : MotionLeftBaseAction(allowPastEnd = true) operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("[")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, true, allowWrap)
}
}

View File

@ -23,7 +23,8 @@ import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.helper.isEndAllowed import com.maddyhome.idea.vim.helper.isEndAllowed
import com.maddyhome.idea.vim.helper.usesVirtualSpace import com.maddyhome.idea.vim.helper.usesVirtualSpace
abstract class MotionRightBaseAction() : MotionActionHandler.ForEachCaret() { @CommandOrMotion(keys = ["l"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionRightAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset( override fun getOffset(
@ -34,20 +35,24 @@ abstract class MotionRightBaseAction() : MotionActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("l") val allowWrap = injector.options(editor).whichwrap.contains("l")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd(editor), allowWrap) 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)
} }
protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed
} }
@CommandOrMotion(keys = ["l"], modes = [Mode.NORMAL, Mode.VISUAL]) @CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.INSERT])
class MotionRightAction : MotionRightBaseAction() class MotionRightInsertAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
@CommandOrMotion(keys = ["l"], modes = [Mode.OP_PENDING]) override fun getOffset(
class MotionRightOpPendingAction : MotionRightBaseAction() { editor: VimEditor,
// When the motion is used with an operator, the EOL character is counted. caret: ImmutableVimCaret,
// This allows e.g., `dl` to delete the last character in a line. Note that we can't use editor.isEndAllowed to give context: ExecutionContext,
// us this because the current mode when we execute the operator/motion is no longer OP_PENDING. argument: Argument?,
// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators operatorArguments: OperatorArguments,
override fun allowPastEnd(editor: VimEditor) = true ): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("]")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd = true, allowWrap)
}
} }

View File

@ -18,19 +18,23 @@ import com.maddyhome.idea.vim.api.moveToMotion
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler
/**
* @author Alex Plate
*/
@CommandOrMotion(keys = ["<S-Left>"], modes = [Mode.INSERT, Mode.NORMAL, Mode.VISUAL, Mode.SELECT]) @CommandOrMotion(keys = ["<S-Left>"], modes = [Mode.INSERT, Mode.NORMAL, Mode.VISUAL, Mode.SELECT])
class MotionShiftArrowLeftAction : ShiftedArrowKeyHandler(true) { class MotionShiftLeftAction : ShiftedArrowKeyHandler(true) {
override val type: Command.Type = Command.Type.OTHER_READONLY override val type: Command.Type = Command.Type.OTHER_READONLY
override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) { override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) {
val motion = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true) val vertical = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true)
caret.moveToMotion(motion) caret.moveToMotion(vertical)
} }
override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) { override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) {
val caret = editor.currentCaret() val caret = editor.currentCaret()
val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false) val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false)
caret.moveToMotion(motion) caret.moveToMotion(newOffset)
} }
} }

View File

@ -16,21 +16,28 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.moveToMotion import com.maddyhome.idea.vim.api.moveToMotion
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler
/**
* @author Alex Plate
*/
@CommandOrMotion(keys = ["<S-Right>"], modes = [Mode.INSERT, Mode.NORMAL, Mode.VISUAL, Mode.SELECT]) @CommandOrMotion(keys = ["<S-Right>"], modes = [Mode.INSERT, Mode.NORMAL, Mode.VISUAL, Mode.SELECT])
class MotionShiftArrowRightAction : ShiftedArrowKeyHandler(true) { class MotionShiftRightAction : ShiftedArrowKeyHandler(true) {
override val type: Command.Type = Command.Type.OTHER_READONLY override val type: Command.Type = Command.Type.OTHER_READONLY
override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) { override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) {
val motion = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true) val vertical = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true)
caret.moveToMotion(motion) caret.moveToMotion(vertical)
} }
override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) { override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) {
val caret = editor.currentCaret() val caret = editor.currentCaret()
val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false) val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false)
caret.moveToMotion(motion) if (newOffset is Motion.AbsoluteOffset) {
caret.moveToOffset(newOffset.offset)
}
} }
} }

View File

@ -1,41 +0,0 @@
/*
* 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 = ["<Space>"], 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 = ["<Space>"], modes = [Mode.OP_PENDING])
class MotionSpaceOpPendingModeAction : MotionSpaceAction(allowPastEnd = true)

View File

@ -15,12 +15,15 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument 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.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.Direction import com.maddyhome.idea.vim.common.Direction
import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.toMotionOrError import com.maddyhome.idea.vim.handler.toMotionOrError
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
enum class TillCharacterMotionType { enum class TillCharacterMotionType {
LAST_F, LAST_F,
@ -48,6 +51,9 @@ sealed class TillCharacterMotion(
private val finishBeforeCharacter: Boolean, private val finishBeforeCharacter: Boolean,
) : MotionActionHandler.ForEachCaret() { ) : MotionActionHandler.ForEachCaret() {
override val argumentType: Argument.Type = Argument.Type.DIGRAPH override val argumentType: Argument.Type = Argument.Type.DIGRAPH
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_ALLOW_DIGRAPH)
override val motionType: MotionType = override val motionType: MotionType =
if (direction == Direction.BACKWARDS) MotionType.EXCLUSIVE else MotionType.INCLUSIVE if (direction == Direction.BACKWARDS) MotionType.EXCLUSIVE else MotionType.INCLUSIVE
@ -58,7 +64,7 @@ sealed class TillCharacterMotion(
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val res = if (finishBeforeCharacter) { val res = if (finishBeforeCharacter) {
injector.motion injector.motion
.moveCaretToBeforeNextCharacterOnLine( .moveCaretToBeforeNextCharacterOnLine(

View File

@ -37,7 +37,7 @@ class MotionGotoFileMarkAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, false) return injector.motion.moveCaretToMark(caret, mark, false)
@ -57,7 +57,7 @@ class MotionGotoFileMarkNoSaveJumpAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, false) return injector.motion.moveCaretToMark(caret, mark, false)

View File

@ -37,7 +37,7 @@ class MotionGotoFileMarkLineAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, false) return injector.motion.moveCaretToMark(caret, mark, false)
@ -57,7 +57,7 @@ class MotionGotoFileMarkLineNoSaveJumpAction : MotionActionHandler.ForEachCaret(
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, true) return injector.motion.moveCaretToMark(caret, mark, true)

View File

@ -37,7 +37,7 @@ class MotionGotoMarkAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, false) return injector.motion.moveCaretToMark(caret, mark, false)
@ -57,7 +57,7 @@ class MotionGotoMarkNoSaveJumpAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, false) return injector.motion.moveCaretToMark(caret, mark, false)

View File

@ -37,7 +37,7 @@ class MotionGotoMarkLineAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, true) return injector.motion.moveCaretToMark(caret, mark, true)
@ -57,7 +57,7 @@ class MotionGotoMarkLineNoSaveJumpAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument !is Argument.Character) return Motion.Error if (argument == null) return Motion.Error
val mark = argument.character val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, true) return injector.motion.moveCaretToMark(caret, mark, true)

View File

@ -24,6 +24,7 @@ class MotionMarkAction : VimActionHandler.SingleExecution() {
override val argumentType: Argument.Type = Argument.Type.CHARACTER override val argumentType: Argument.Type = Argument.Type.CHARACTER
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean { override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
return cmd.argument.let { it is Argument.Character && injector.markService.setMark(editor, it.character) } val argument = cmd.argument
return argument != null && injector.markService.setMark(editor, argument.character)
} }
} }

View File

@ -18,7 +18,6 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments 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.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.toMotion import com.maddyhome.idea.vim.handler.toMotion
@ -29,7 +28,7 @@ import com.maddyhome.idea.vim.options.OptionConstants
*/ */
@CommandOrMotion(keys = ["<Left>"], modes = [Mode.SELECT]) @CommandOrMotion(keys = ["<Left>"], modes = [Mode.SELECT])
class SelectMotionArrowLeftAction : MotionActionHandler.ForEachCaret() { class SelectMotionLeftAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
@ -59,6 +58,6 @@ class SelectMotionArrowLeftAction : MotionActionHandler.ForEachCaret() {
} }
private companion object { private companion object {
private val logger = vimLogger<SelectMotionArrowLeftAction>() private val logger = injector.getLogger(SelectMotionLeftAction::class.java)
} }
} }

View File

@ -18,7 +18,6 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments 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.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.toMotion import com.maddyhome.idea.vim.handler.toMotion
@ -29,7 +28,7 @@ import com.maddyhome.idea.vim.options.OptionConstants
*/ */
@CommandOrMotion(keys = ["<Right>"], modes = [Mode.SELECT]) @CommandOrMotion(keys = ["<Right>"], modes = [Mode.SELECT])
class SelectMotionArrowRightAction : MotionActionHandler.ForEachCaret() { class SelectMotionRightAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
@ -59,6 +58,6 @@ class SelectMotionArrowRightAction : MotionActionHandler.ForEachCaret() {
} }
private companion object { private companion object {
private val logger = vimLogger<SelectMotionArrowRightAction>() private val logger = injector.getLogger(SelectMotionRightAction::class.java)
} }
} }

View File

@ -37,26 +37,10 @@ interface VimChangeGroup {
fun initInsert(editor: VimEditor, context: ExecutionContext, mode: Mode) 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, caret: VimCaret, context: ExecutionContext)
fun processEnter(editor: VimEditor, context: ExecutionContext) fun processEnter(editor: VimEditor, context: ExecutionContext)
fun processBackspace(editor: VimEditor, context: ExecutionContext)
fun processPostChangeModeSwitch(editor: VimEditor, context: ExecutionContext, toSwitch: Mode) fun processPostChangeModeSwitch(editor: VimEditor, context: ExecutionContext, toSwitch: Mode)
@ -74,7 +58,13 @@ interface VimChangeGroup {
fun deleteEndOfLine(editor: VimEditor, caret: VimCaret, count: Int, operatorArguments: OperatorArguments): Boolean fun deleteEndOfLine(editor: VimEditor, caret: VimCaret, count: Int, operatorArguments: OperatorArguments): Boolean
fun deleteJoinLines(editor: VimEditor, caret: VimCaret, count: Int, spaces: Boolean): Boolean fun deleteJoinLines(
editor: VimEditor,
caret: VimCaret,
count: Int,
spaces: Boolean,
operatorArguments: OperatorArguments,
): Boolean
fun processKey(editor: VimEditor, key: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean fun processKey(editor: VimEditor, key: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean
@ -102,6 +92,7 @@ interface VimChangeGroup {
range: TextRange, range: TextRange,
type: SelectionType?, type: SelectionType?,
isChange: Boolean, isChange: Boolean,
operatorArguments: OperatorArguments,
saveToRegister: Boolean = true, saveToRegister: Boolean = true,
): Boolean ): Boolean
fun changeCharacters(editor: VimEditor, caret: VimCaret, operatorArguments: OperatorArguments): Boolean fun changeCharacters(editor: VimEditor, caret: VimCaret, operatorArguments: OperatorArguments): Boolean
@ -121,6 +112,8 @@ interface VimChangeGroup {
fun changeCaseToggleCharacter(editor: VimEditor, caret: VimCaret, count: Int): Boolean 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 changeCaseRange(editor: VimEditor, caret: VimCaret, range: TextRange, type: ChangeCaseType): Boolean
fun changeRange( fun changeRange(
@ -129,6 +122,7 @@ interface VimChangeGroup {
range: TextRange, range: TextRange,
type: SelectionType, type: SelectionType,
context: ExecutionContext, context: ExecutionContext,
operatorArguments: OperatorArguments,
): Boolean ): Boolean
fun changeCaseMotion(editor: VimEditor, caret: VimCaret, context: ExecutionContext?, type: ChangeCaseType, argument: Argument, operatorArguments: OperatorArguments): Boolean fun changeCaseMotion(editor: VimEditor, caret: VimCaret, context: ExecutionContext?, type: ChangeCaseType, argument: Argument, operatorArguments: OperatorArguments): Boolean
@ -193,6 +187,7 @@ interface VimChangeGroup {
context: ExecutionContext, context: ExecutionContext,
count: Int, count: Int,
started: Boolean, started: Boolean,
operatorArguments: OperatorArguments,
) )
fun type(vimEditor: VimEditor, context: ExecutionContext, key: Char) fun type(vimEditor: VimEditor, context: ExecutionContext, key: Char)

View File

@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.ChangesListener 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.common.TextRange
import com.maddyhome.idea.vim.diagnostic.debug import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger
@ -23,7 +24,6 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.Motion import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset 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
import com.maddyhome.idea.vim.helper.CharacterHelper.charType import com.maddyhome.idea.vim.helper.CharacterHelper.charType
import com.maddyhome.idea.vim.helper.NumberType import com.maddyhome.idea.vim.helper.NumberType
@ -111,6 +111,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
TextRange(caret.offset, endOffset.offset), TextRange(caret.offset, endOffset.offset),
SelectionType.CHARACTER_WISE, SelectionType.CHARACTER_WISE,
caret, caret,
operatorArguments,
) )
val pos = caret.offset val pos = caret.offset
val norm = editor.normalizeOffset(caret.getBufferPosition().line, pos, isChange) val norm = editor.normalizeOffset(caret.getBufferPosition().line, pos, isChange)
@ -161,21 +162,21 @@ abstract class VimChangeGroupBase : VimChangeGroup {
range: TextRange, range: TextRange,
type: SelectionType?, type: SelectionType?,
caret: VimCaret, caret: VimCaret,
operatorArguments: OperatorArguments,
saveToRegister: Boolean = true, saveToRegister: Boolean = true,
): Boolean { ): Boolean {
var updatedRange = range var updatedRange = range
// Fix for https://youtrack.jetbrains.net/issue/VIM-35 // Fix for https://youtrack.jetbrains.net/issue/VIM-35
if (!range.normalize(editor.fileSize().toInt())) { if (!range.normalize(editor.fileSize().toInt())) {
updatedRange = if (range.startOffset == range.endOffset updatedRange = if (range.startOffset == range.endOffset && range.startOffset == editor.fileSize()
&& range.startOffset == editor.fileSize().toInt() .toInt() && range.startOffset != 0
&& range.startOffset != 0) { ) {
TextRange(range.startOffset - 1, range.endOffset) TextRange(range.startOffset - 1, range.endOffset)
} else { } else {
return false return false
} }
} }
val mode = editor.mode val mode = operatorArguments.mode
if (type == null || if (type == null ||
(mode == Mode.INSERT || mode == Mode.REPLACE) || (mode == Mode.INSERT || mode == Mode.REPLACE) ||
!saveToRegister || !saveToRegister ||
@ -238,10 +239,11 @@ abstract class VimChangeGroupBase : VimChangeGroup {
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
count: Int, count: Int,
operatorArguments: OperatorArguments,
) { ) {
val myLastStrokes = lastStrokes ?: return val myLastStrokes = lastStrokes ?: return
for (caret in editor.nativeCarets()) { for (caret in editor.nativeCarets()) {
repeat(count) { for (i in 0 until count) {
for (lastStroke in myLastStrokes) { for (lastStroke in myLastStrokes) {
when (lastStroke) { when (lastStroke) {
is NativeAction -> { is NativeAction -> {
@ -250,7 +252,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
} }
is EditorActionHandlerBase -> { is EditorActionHandlerBase -> {
injector.actionExecutor.executeVimAction(editor, lastStroke, context, OperatorArguments(0, editor.mode)) injector.actionExecutor.executeVimAction(editor, lastStroke, context, operatorArguments)
strokes.add(lastStroke) strokes.add(lastStroke)
} }
@ -279,6 +281,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
context: ExecutionContext, context: ExecutionContext,
count: Int, count: Int,
started: Boolean, started: Boolean,
operatorArguments: OperatorArguments,
) { ) {
for (caret in editor.nativeCarets()) { for (caret in editor.nativeCarets()) {
if (repeatLines > 0) { if (repeatLines > 0) {
@ -299,17 +302,17 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val updatedCount = if (started) (if (i == 0) count else count + 1) else count val updatedCount = if (started) (if (i == 0) count else count + 1) else count
if (repeatColumn >= VimMotionGroupBase.LAST_COLUMN) { if (repeatColumn >= VimMotionGroupBase.LAST_COLUMN) {
caret.moveToOffset(injector.motion.moveCaretToLineEnd(editor, bufferLine + i, true)) caret.moveToOffset(injector.motion.moveCaretToLineEnd(editor, bufferLine + i, true))
repeatInsertText(editor, context, updatedCount) repeatInsertText(editor, context, updatedCount, operatorArguments)
} else if (editor.getVisualLineLength(visualLine + i) >= repeatColumn) { } else if (editor.getVisualLineLength(visualLine + i) >= repeatColumn) {
val visualPosition = VimVisualPosition(visualLine + i, repeatColumn, false) val visualPosition = VimVisualPosition(visualLine + i, repeatColumn, false)
val inlaysCount = injector.engineEditorHelper.amountOfInlaysBeforeVisualPosition(editor, visualPosition) val inlaysCount = injector.engineEditorHelper.amountOfInlaysBeforeVisualPosition(editor, visualPosition)
caret.moveToVisualPosition(VimVisualPosition(visualLine + i, repeatColumn + inlaysCount, false)) caret.moveToVisualPosition(VimVisualPosition(visualLine + i, repeatColumn + inlaysCount, false))
repeatInsertText(editor, context, updatedCount) repeatInsertText(editor, context, updatedCount, operatorArguments)
} }
} }
caret.moveToOffset(position) caret.moveToOffset(position)
} else { } else {
repeatInsertText(editor, context, count) repeatInsertText(editor, context, count, operatorArguments)
val position = injector.motion.getHorizontalMotion(editor, caret, -1, false) val position = injector.motion.getHorizontalMotion(editor, caret, -1, false)
caret.moveToMotion(position) caret.moveToMotion(position)
} }
@ -327,7 +330,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val oldFragmentLength = oldFragment.length val oldFragmentLength = oldFragment.length
// Repeat buffer limits // Repeat buffer limits
if (repeatCharsCount > MAX_REPEAT_CHARS_COUNT) { if (repeatCharsCount > Companion.MAX_REPEAT_CHARS_COUNT) {
return return
} }
@ -348,7 +351,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (oldFragmentLength > 0) { if (oldFragmentLength > 0) {
val editorDelete = injector.nativeActionManager.deleteAction val editorDelete = injector.nativeActionManager.deleteAction
if (editorDelete != null) { if (editorDelete != null) {
repeat(oldFragmentLength) { for (i in 0 until oldFragmentLength) {
strokes.add(editorDelete) strokes.add(editorDelete)
} }
} }
@ -367,7 +370,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val motionName = if (delta < 0) "VimMotionLeftAction" else "VimMotionRightAction" val motionName = if (delta < 0) "VimMotionLeftAction" else "VimMotionRightAction"
val action = injector.actionExecutor.findVimAction(motionName)!! val action = injector.actionExecutor.findVimAction(motionName)!!
val count = abs(delta) val count = abs(delta)
repeat(count) { for (i in 0 until count) {
positionCaretActions.add(action) positionCaretActions.add(action)
} }
return positionCaretActions return positionCaretActions
@ -446,8 +449,25 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (mode == Mode.REPLACE) { if (mode == Mode.REPLACE) {
editor.insertMode = false editor.insertMode = false
} }
val count = if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) 1 else cmd.count if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) {
repeatInsert(editor, context, count, false) 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),
)
}
if (mode == Mode.REPLACE) { if (mode == Mode.REPLACE) {
editor.insertMode = true editor.insertMode = true
} }
@ -512,9 +532,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
exit: Boolean, exit: Boolean,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
) { ) {
repeatInsertText(editor, context, 1) repeatInsertText(editor, context, 1, operatorArguments)
if (exit) { if (exit) {
editor.exitInsertMode(context) editor.exitInsertMode(context, operatorArguments)
} }
} }
@ -524,7 +544,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* *
* DEPRECATED. Please, don't use this function directly. Use ModeHelper.exitInsertMode in file ModeExtensions.kt * DEPRECATED. Please, don't use this function directly. Use ModeHelper.exitInsertMode in file ModeExtensions.kt
*/ */
override fun processEscape(editor: VimEditor, context: ExecutionContext?) { override fun processEscape(editor: VimEditor, context: ExecutionContext?, operatorArguments: OperatorArguments) {
// Get the offset for marks before we exit insert mode - switching from insert to overtype subtracts one from the // Get the offset for marks before we exit insert mode - switching from insert to overtype subtracts one from the
// column offset. // column offset.
val markGroup = injector.markService val markGroup = injector.markService
@ -533,24 +553,17 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (editor.mode is Mode.REPLACE) { if (editor.mode is Mode.REPLACE) {
editor.insertMode = true editor.insertMode = true
} }
val repeatCount0 = lastInsert?.let { var cnt = if (lastInsert != null) lastInsert!!.count else 0
// How many times do we want to *repeat* the insert? For a simple insert or change action, this is count-1. But if if (lastInsert != null && lastInsert!!.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) {
// the command is an operator+motion, then the count applies to the motion, not the insert/change. I.e., `2cw` cnt = 1
// 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) { if (vimDocument != null && vimDocumentListener != null) {
vimDocument!!.removeChangeListener(vimDocumentListener!!) vimDocument!!.removeChangeListener(vimDocumentListener!!)
vimDocumentListener = null vimDocumentListener = null
} }
lastStrokes = ArrayList(strokes) lastStrokes = ArrayList(strokes)
if (context != null) { if (context != null) {
injector.changeGroup.repeatInsert(editor, context, repeatCount0, true) injector.changeGroup.repeatInsert(editor, context, if (cnt == 0) 0 else cnt - 1, true, operatorArguments)
} }
if (editor.mode is Mode.INSERT) { if (editor.mode is Mode.INSERT) {
updateLastInsertedTextRegister() updateLastInsertedTextRegister()
@ -664,7 +677,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val rangeToDelete = TextRange(startOffset, offset) val rangeToDelete = TextRange(startOffset, offset)
editor.nativeCarets().filter { it != caret && rangeToDelete.contains(it.offset) } editor.nativeCarets().filter { it != caret && rangeToDelete.contains(it.offset) }
.forEach { editor.removeCaret(it) } .forEach { editor.removeCaret(it) }
val res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE, caret) val res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE, caret, operatorArguments)
if (editor.usesVirtualSpace) { if (editor.usesVirtualSpace) {
caret.moveToOffset(startOffset) caret.moveToOffset(startOffset)
} else { } else {
@ -691,6 +704,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
caret: VimCaret, caret: VimCaret,
count: Int, count: Int,
spaces: Boolean, spaces: Boolean,
operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
var myCount = count var myCount = count
if (myCount < 2) myCount = 2 if (myCount < 2) myCount = 2
@ -699,7 +713,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return if (lline + myCount > total) { return if (lline + myCount > total) {
false false
} else { } else {
deleteJoinNLines(editor, caret, lline, myCount, spaces) deleteJoinNLines(editor, caret, lline, myCount, spaces, operatorArguments)
} }
} }
@ -717,14 +731,12 @@ abstract class VimChangeGroupBase : VimChangeGroup {
): Boolean { ): Boolean {
logger.debug { "processKey($key)" } logger.debug { "processKey($key)" }
if (key.keyChar != KeyEvent.CHAR_UNDEFINED) { if (key.keyChar != KeyEvent.CHAR_UNDEFINED) {
editor.replaceMask?.recordChangeAtCaret(editor)
processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, key.keyChar) } processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, key.keyChar) }
return true return true
} }
// Shift-space // Shift-space
if (key.keyCode == 32 && key.modifiers and KeyEvent.SHIFT_DOWN_MASK != 0) { if (key.keyCode == 32 && key.modifiers and KeyEvent.SHIFT_DOWN_MASK != 0) {
editor.replaceMask?.recordChangeAtCaret(editor)
processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, ' ') } processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, ' ') }
return true return true
} }
@ -772,7 +784,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
logger.debug("offset=$offset") logger.debug("offset=$offset")
} }
if (offset != -1) { if (offset != -1) {
val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret) val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret, operatorArguments)
if (res && caret.offset >= editor.fileSize() && caret.offset != 0) { if (res && caret.offset >= editor.fileSize() && caret.offset != 0) {
caret.moveToOffset( caret.moveToOffset(
injector.motion.moveCaretToRelativeLineStartSkipLeading( injector.motion.moveCaretToRelativeLineStartSkipLeading(
@ -795,7 +807,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
lline + count <= total lline + count <= total
} }
if (!allowedExecution) return false if (!allowedExecution) return false
repeat(executions) { for (i in 0 until executions) {
val joinLinesAction = injector.nativeActionManager.joinLines val joinLinesAction = injector.nativeActionManager.joinLines
if (joinLinesAction != null) { if (joinLinesAction != null) {
injector.actionExecutor.executeAction(editor, joinLinesAction, context) injector.actionExecutor.executeAction(editor, joinLinesAction, context)
@ -825,7 +837,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val endLine = editor.offsetToBufferPosition(range.endOffset).line val endLine = editor.offsetToBufferPosition(range.endOffset).line
var count = endLine - startLine + 1 var count = endLine - startLine + 1
if (count < 2) count = 2 if (count < 2) count = 2
return deleteJoinNLines(editor, caret, startLine, count, spaces) return deleteJoinNLines(editor, caret, startLine, count, spaces, operatorArguments)
} }
override fun joinViaIdeaBySelections( override fun joinViaIdeaBySelections(
@ -862,25 +874,28 @@ abstract class VimChangeGroupBase : VimChangeGroup {
isChange: Boolean, isChange: Boolean,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Pair<TextRange, SelectionType>? { ): Pair<TextRange, SelectionType>? {
check(argument is Argument.Motion) { "Unexpected argument: $argument" }
val range = injector.motion.getMotionRange(editor, caret, context, argument, operatorArguments) ?: return null 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: // Delete motion commands that are not linewise become linewise if all the following are true:
// 1) The range is across multiple lines // 1) The range is across multiple lines
// 2) There is only whitespace before the start of the range // 2) There is only whitespace before the start of the range
// 3) There is only whitespace after the end of the range // 3) There is only whitespace after the end of the range
if (!isChange && motionType != SelectionType.LINE_WISE) { var type: SelectionType = if (argument.motion.isLinewiseMotion()) {
SelectionType.LINE_WISE
} else {
SelectionType.CHARACTER_WISE
}
val motion = argument.motion
if (!isChange && !motion.isLinewiseMotion()) {
val start = editor.offsetToBufferPosition(range.startOffset) val start = editor.offsetToBufferPosition(range.startOffset)
val end = editor.offsetToBufferPosition(range.endOffset) val end = editor.offsetToBufferPosition(range.endOffset)
if (start.line != end.line if (start.line != end.line) {
&& !editor.anyNonWhitespace(range.startOffset, -1) if (!editor.anyNonWhitespace(range.startOffset, -1) && !editor.anyNonWhitespace(range.endOffset, 1)) {
&& !editor.anyNonWhitespace(range.endOffset, 1)) { type = SelectionType.LINE_WISE
motionType = SelectionType.LINE_WISE }
} }
} }
return Pair(range, motionType) return Pair(range, type)
} }
/** /**
@ -900,12 +915,13 @@ abstract class VimChangeGroupBase : VimChangeGroup {
range: TextRange, range: TextRange,
type: SelectionType?, type: SelectionType?,
isChange: Boolean, isChange: Boolean,
operatorArguments: OperatorArguments,
saveToRegister: Boolean, saveToRegister: Boolean,
): Boolean { ): Boolean {
val intendedColumn = caret.vimLastColumn val intendedColumn = caret.vimLastColumn
val removeLastNewLine = removeLastNewLine(editor, range, type) val removeLastNewLine = removeLastNewLine(editor, range, type)
val res = deleteText(editor, range, type, caret, saveToRegister) val res = deleteText(editor, range, type, caret, operatorArguments, saveToRegister)
var processedCaret = editor.findLastVersionOfCaret(caret) ?: caret var processedCaret = editor.findLastVersionOfCaret(caret) ?: caret
if (removeLastNewLine) { if (removeLastNewLine) {
val textLength = editor.fileSize().toInt() val textLength = editor.fileSize().toInt()
@ -1024,6 +1040,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
startLine: Int, startLine: Int,
count: Int, count: Int,
spaces: Boolean, spaces: Boolean,
operatorArguments: OperatorArguments,
): Boolean { ): 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 // 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 // caret and then be unable to delete
@ -1040,7 +1057,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return i > 1 return i > 1
} }
// Note that caret isn't moved here; it's only used for register + mark storage // Note that caret isn't moved here; it's only used for register + mark storage
deleteText(editor, TextRange(startOffset, endOffset), null, caret) deleteText(editor, TextRange(startOffset, endOffset), null, caret, operatorArguments)
if (spaces && !hasTrailingWhitespace) { if (spaces && !hasTrailingWhitespace) {
insertText(editor, caret, startOffset, " ") insertText(editor, caret, startOffset, " ")
} }
@ -1127,8 +1144,8 @@ abstract class VimChangeGroupBase : VimChangeGroup {
): Boolean { ): Boolean {
var count0 = operatorArguments.count0 var count0 = operatorArguments.count0
// Vim treats cw as ce and cW as cE if cursor is on a non-blank character // Vim treats cw as ce and cW as cE if cursor is on a non-blank character
var motionArgument = argument as? Argument.Motion ?: return false val motion = argument.motion
val id = motionArgument.motion.id val id = motion.action.id
var kludge = false var kludge = false
val bigWord = id == VIM_MOTION_BIG_WORD_RIGHT val bigWord = id == VIM_MOTION_BIG_WORD_RIGHT
val chars = editor.text() val chars = editor.text()
@ -1138,7 +1155,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val charType = charType(editor, chars[offset], bigWord) val charType = charType(editor, chars[offset], bigWord)
if (charType !== CharacterHelper.CharacterType.WHITESPACE) { if (charType !== CharacterHelper.CharacterType.WHITESPACE) {
val lastWordChar = offset >= fileSize - 1 || charType(editor, chars[offset + 1], bigWord) !== charType val lastWordChar = offset >= fileSize - 1 || charType(editor, chars[offset + 1], bigWord) !== charType
if (wordMotions.contains(id) && lastWordChar && operatorArguments.count1 == 1) { if (wordMotions.contains(id) && lastWordChar && motion.count == 1) {
val res = deleteCharacter(editor, caret, 1, true, operatorArguments) val res = deleteCharacter(editor, caret, 1, true, operatorArguments)
if (res) { if (res) {
editor.vimChangeActionSwitchMode = Mode.INSERT editor.vimChangeActionSwitchMode = Mode.INSERT
@ -1148,50 +1165,49 @@ abstract class VimChangeGroupBase : VimChangeGroup {
when (id) { when (id) {
VIM_MOTION_WORD_RIGHT -> { VIM_MOTION_WORD_RIGHT -> {
kludge = true kludge = true
motionArgument = Argument.Motion( motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT)
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
} }
VIM_MOTION_BIG_WORD_RIGHT -> { VIM_MOTION_BIG_WORD_RIGHT -> {
kludge = true kludge = true
motionArgument = Argument.Motion( motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT)
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
} }
VIM_MOTION_CAMEL_RIGHT -> { VIM_MOTION_CAMEL_RIGHT -> {
kludge = true kludge = true
motionArgument = Argument.Motion( motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT)
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
} }
} }
} }
} }
if (kludge) { if (kludge) {
val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, operatorArguments.count1, bigWord, false) val cnt = operatorArguments.count1 * motion.count
val pos2 = injector.searchHelper.findNextWordEnd(editor, pos1, -operatorArguments.count1, bigWord, false) val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, cnt, bigWord, false)
val pos2 = injector.searchHelper.findNextWordEnd(editor, pos1, -cnt, bigWord, false)
if (logger.isDebug()) { if (logger.isDebug()) {
logger.debug("pos=$offset") logger.debug("pos=$offset")
logger.debug("pos1=$pos1") logger.debug("pos1=$pos1")
logger.debug("pos2=$pos2") logger.debug("pos2=$pos2")
logger.debug("count=" + operatorArguments.count1) logger.debug("count=" + operatorArguments.count1)
logger.debug("arg.count=" + motion.count)
} }
if (pos2 == offset && operatorArguments.count1 > 1) { if (pos2 == offset) {
count0-- if (operatorArguments.count1 > 1) {
count0--
} else if (motion.count > 1) {
motion.rawCount = motion.count - 1
} else {
motion.flags = EnumSet.noneOf(CommandFlags::class.java)
}
} }
} }
val (first, second) = getDeleteRangeAndType( val (first, second) = getDeleteRangeAndType(
editor, editor,
caret, caret,
context, context,
motionArgument, argument,
true, true,
operatorArguments.copy(count0 = count0), operatorArguments.withCount0(count0),
) ?: return false ) ?: return false
return changeRange( return changeRange(
editor, editor,
@ -1199,6 +1215,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
first, first,
second, second,
context, context,
operatorArguments,
) )
} }
@ -1223,6 +1240,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
* @param caret The caret to be moved after range deletion * @param caret The caret to be moved after range deletion
* @param range The range to change * @param range The range to change
* @param type The type of the range * @param type The type of the range
* @param operatorArguments
* @return true if able to delete the range, false if not * @return true if able to delete the range, false if not
*/ */
override fun changeRange( override fun changeRange(
@ -1231,6 +1249,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
range: TextRange, range: TextRange,
type: SelectionType, type: SelectionType,
context: ExecutionContext, context: ExecutionContext,
operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
var col = 0 var col = 0
var lines = 0 var lines = 0
@ -1243,7 +1262,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
} }
val after = range.endOffset >= editor.fileSize() val after = range.endOffset >= editor.fileSize()
val lp = editor.offsetToBufferPosition(injector.motion.moveCaretToCurrentLineStartSkipLeading(editor, caret)) val lp = editor.offsetToBufferPosition(injector.motion.moveCaretToCurrentLineStartSkipLeading(editor, caret))
val res = deleteRange(editor, caret, range, type, true) val res = deleteRange(editor, caret, range, type, true, operatorArguments)
val updatedCaret = editor.findLastVersionOfCaret(caret) ?: caret val updatedCaret = editor.findLastVersionOfCaret(caret) ?: caret
if (res) { if (res) {
if (type === SelectionType.LINE_WISE) { if (type === SelectionType.LINE_WISE) {
@ -1905,7 +1924,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
pos++ pos++
} }
if (pos > wsoff) { if (pos > wsoff) {
deleteText(editor, TextRange(wsoff, pos), null, caret, true) deleteText(editor, TextRange(wsoff, pos), null, caret, operatorArguments, true)
} }
} }
} }
@ -1937,21 +1956,23 @@ abstract class VimChangeGroupBase : VimChangeGroup {
} }
} }
override fun initBlockInsert( override fun blockInsert(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
range: TextRange, range: TextRange,
append: Boolean, append: Boolean,
operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val lines = getLinesCountInVisualBlock(editor, range) val lines = getLinesCountInVisualBlock(editor, range)
val startPosition = editor.offsetToBufferPosition(range.startOffset) val startPosition = editor.offsetToBufferPosition(range.startOffset)
// Note that when called, we're likely to have moved from Visual (block) to Normal, which means all secondary carets val mode = operatorArguments.mode
// will have been removed. Even if not, this would move them all to the same location, which would remove them and val visualBlockMode = mode is Mode.VISUAL && mode.selectionType === SelectionType.BLOCK_WISE
// leave only the primary caret.
for (caret in editor.carets()) { for (caret in editor.carets()) {
val line = startPosition.line val line = startPosition.line
var column = startPosition.column var column = startPosition.column
if (append) { if (!visualBlockMode) {
column = 0
} else if (append) {
column += range.maxLength column += range.maxLength
if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) { if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) {
column = VimMotionGroupBase.LAST_COLUMN column = VimMotionGroupBase.LAST_COLUMN
@ -1963,10 +1984,18 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val offset = editor.getLineEndOffset(line) val offset = editor.getLineEndOffset(line)
insertText(editor, caret, offset, pad) insertText(editor, caret, offset, pad)
} }
caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column))) if (visualBlockMode || !append) {
setInsertRepeat(lines, column, append) caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column)))
}
if (visualBlockMode) {
setInsertRepeat(lines, column, append)
}
}
if (visualBlockMode || !append) {
insertBeforeCursor(editor, context)
} else {
insertAfterCursor(editor, context)
} }
insertBeforeCursor(editor, context)
return true return true
} }
@ -2039,3 +2068,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
VimChangeGroup.ChangeCaseType.UPPER -> Character.toUpperCase(ch) 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
}

View File

@ -8,6 +8,8 @@
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.command.Command
interface VimCommandLineService { interface VimCommandLineService {
fun isCommandLineSupported(editor: VimEditor): Boolean fun isCommandLineSupported(editor: VimEditor): Boolean
@ -24,7 +26,7 @@ interface VimCommandLineService {
* @param initialText The initial text for the entry * @param initialText The initial text for the entry
*/ */
fun createSearchPrompt(editor: VimEditor, context: ExecutionContext, label: String, initialText: String): VimCommandLine fun createSearchPrompt(editor: VimEditor, context: ExecutionContext, label: String, initialText: String): VimCommandLine
fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, count0: Int, initialText: String): VimCommandLine fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, command: Command, initialText: String): VimCommandLine
@Deprecated("Please use ModalInputService.create()") @Deprecated("Please use ModalInputService.create()")
fun createWithoutShortcuts(editor: VimEditor, context: ExecutionContext, label: String, initText: String): VimCommandLine fun createWithoutShortcuts(editor: VimEditor, context: ExecutionContext, label: String, initText: String): VimCommandLine

View File

@ -8,6 +8,7 @@
package com.maddyhome.idea.vim.api 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.ex.ExException
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
@ -49,15 +50,15 @@ abstract class VimCommandLineServiceBase : VimCommandLineService {
return createCommandLinePrompt(editor, context, removeSelections = false, label, initialText) return createCommandLinePrompt(editor, context, removeSelections = false, label, initialText)
} }
override fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, count0: Int, initialText: String): VimCommandLine { override fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, command: Command, initialText: String): VimCommandLine {
val rangeText = getRange(editor, count0) val rangeText = getRange(editor, command)
return createCommandLinePrompt(editor, context, removeSelections = true, label = ":", rangeText + initialText) return createCommandLinePrompt(editor, context, removeSelections = true, label = ":", rangeText + initialText)
} }
protected fun getRange(editor: VimEditor, count0: Int) = when { protected fun getRange(editor: VimEditor, cmd: Command) = when {
editor.inVisualMode -> "'<,'>" editor.inVisualMode -> "'<,'>"
count0 == 1 -> "." cmd.rawCount == 1 -> "."
count0 > 1 -> ".,.+" + (count0 - 1) cmd.rawCount > 1 -> ".,.+" + (cmd.count - 1)
else -> "" else -> ""
} }
} }

View File

@ -11,7 +11,6 @@ package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.common.VimEditorReplaceMask
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo import com.maddyhome.idea.vim.state.mode.ReturnTo
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
@ -129,7 +128,6 @@ interface VimEditor {
val lfMakesNewLine: Boolean val lfMakesNewLine: Boolean
var vimChangeActionSwitchMode: Mode? var vimChangeActionSwitchMode: Mode?
val indentConfig: VimIndentConfig val indentConfig: VimIndentConfig
var replaceMask: VimEditorReplaceMask?
fun fileSize(): Long fun fileSize(): Long
@ -246,9 +244,7 @@ interface VimEditor {
// Can be used as a key to store something for specific project // Can be used as a key to store something for specific project
val projectId: String val projectId: String
@Deprecated("Use overload without OperatorArguments", replaceWith = ReplaceWith("exitInsertMode(context)")) fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments)
fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) { exitInsertMode(context) }
fun exitInsertMode(context: ExecutionContext)
fun exitSelectModeNative(adjustCaret: Boolean) fun exitSelectModeNative(adjustCaret: Boolean)
var vimLastSelectionType: SelectionType? var vimLastSelectionType: SelectionType?

View File

@ -8,7 +8,6 @@
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.forgetAllReplaceMasks
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
abstract class VimEditorBase : VimEditor { abstract class VimEditorBase : VimEditor {
@ -19,9 +18,6 @@ abstract class VimEditorBase : VimEditor {
if (vimState.mode == value) return if (vimState.mode == value) return
val oldValue = vimState.mode val oldValue = vimState.mode
if (oldValue == Mode.REPLACE) {
forgetAllReplaceMasks()
}
updateMode(value) updateMode(value)
injector.listenersNotifier.notifyModeChanged(this, oldValue) injector.listenersNotifier.notifyModeChanged(this, oldValue)
} }

View File

@ -10,17 +10,17 @@ package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.action.change.LazyVimCommand
import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.extension.ExtensionHandler 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.KeyMapping
import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.KeyMappingLayer
import com.maddyhome.idea.vim.key.MappingInfo import com.maddyhome.idea.vim.key.MappingInfo
import com.maddyhome.idea.vim.key.MappingOwner 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.key.ShortcutOwnerInfo
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import javax.swing.KeyStroke import javax.swing.KeyStroke
interface VimKeyGroup { interface VimKeyGroup {
fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand>
fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer
fun getActions(editor: VimEditor, keyStroke: KeyStroke): List<NativeAction> fun getActions(editor: VimEditor, keyStroke: KeyStroke): List<NativeAction>
fun getKeymapConflicts(keyStroke: KeyStroke): List<NativeAction> fun getKeymapConflicts(keyStroke: KeyStroke): List<NativeAction>

View File

@ -12,6 +12,7 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand
import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase 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.KeyMapping
import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.KeyMappingLayer
import com.maddyhome.idea.vim.key.MappingInfo import com.maddyhome.idea.vim.key.MappingInfo
@ -29,7 +30,7 @@ abstract class VimKeyGroupBase : VimKeyGroup {
@JvmField @JvmField
val myShortcutConflicts: MutableMap<KeyStroke, ShortcutOwnerInfo> = LinkedHashMap() val myShortcutConflicts: MutableMap<KeyStroke, ShortcutOwnerInfo> = LinkedHashMap()
val requiredShortcutKeys: MutableSet<RequiredShortcut> = HashSet(300) val requiredShortcutKeys: MutableSet<RequiredShortcut> = HashSet(300)
val keyRoots: MutableMap<MappingMode, RootNode<LazyVimCommand>> = EnumMap(MappingMode::class.java) val keyRoots: MutableMap<MappingMode, CommandPartNode<LazyVimCommand>> = EnumMap(MappingMode::class.java)
val keyMappings: MutableMap<MappingMode, KeyMapping> = EnumMap(MappingMode::class.java) val keyMappings: MutableMap<MappingMode, KeyMapping> = EnumMap(MappingMode::class.java)
override fun removeKeyMapping(modes: Set<MappingMode>, keys: List<KeyStroke>) { override fun removeKeyMapping(modes: Set<MappingMode>, keys: List<KeyStroke>) {
@ -62,7 +63,7 @@ abstract class VimKeyGroupBase : VimKeyGroup {
* @param mappingMode The mapping mode * @param mappingMode The mapping mode
* @return The key mapping tree root * @return The key mapping tree root
*/ */
override fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode(mappingMode.name.get(0).lowercase()) } override fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode() }
override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode) override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode)

View File

@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.Graphemes import com.maddyhome.idea.vim.common.Graphemes
import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.group.findMatchingPairOnCurrentLine 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
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
@ -117,9 +116,9 @@ abstract class VimMotionGroupBase : VimMotionGroup {
val text = editor.text() val text = editor.text()
val oldOffset = caret.offset val oldOffset = caret.offset
var current = oldOffset var current = oldOffset
repeat(count.absoluteValue) { for (i in 0 until count.absoluteValue) {
val newOffset = if (count > 0) Graphemes.next(text, current) else Graphemes.prev(text, current) val newOffset = if (count > 0) Graphemes.next(text, current) else Graphemes.prev(text, current)
current = newOffset ?: return@repeat current = newOffset ?: break
} }
val offset = if (allowWrap) { val offset = if (allowWrap) {
@ -314,69 +313,69 @@ abstract class VimMotionGroupBase : VimMotionGroup {
argument: Argument, argument: Argument,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): TextRange? { ): TextRange? {
if (argument !is Argument.Motion) {
throw RuntimeException("Unexpected argument passed to getMotionRange2: $argument")
}
var start: Int var start: Int
var end: Int var end: Int
if (argument.type === Argument.Type.OFFSETS) {
val action = argument.motion val offsets = argument.offsets[caret] ?: return null
when (action) { val (first, second) = offsets.getNativeStartAndEnd()
is MotionActionHandler -> { 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) {
// This is where we are now // This is where we are now
start = caret.offset start = caret.offset
// Execute the motion (without moving the cursor) and get where we end // Execute the motion (without moving the cursor) and get where we end
val motion = action.getHandlerOffset(editor, caret, context, argument.argument, operatorArguments) val motion =
if (Motion.Error == motion || Motion.NoMotion == motion) return null cmdAction.getHandlerOffset(editor, caret, context, cmd.argument, operatorArguments.withCount0(raw))
// Invalid motion
if (Motion.Error == motion) return null
if (Motion.NoMotion == motion) return null
end = (motion as AbsoluteOffset).offset end = (motion as AbsoluteOffset).offset
// If inclusive, add the last character to the range // If inclusive, add the last character to the range
if (action.motionType === MotionType.INCLUSIVE) { if (cmdAction.motionType === MotionType.INCLUSIVE) {
if (start > end) { if (start > end) {
if (start < editor.fileSize()) start++ if (start < editor.fileSize()) start++
} else { } else {
if (end < editor.fileSize()) end++ 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 ?: return null
start = range.startOffset start = range.startOffset
end = range.endOffset end = range.endOffset
if (argument.isLinewiseMotion()) end-- if (cmd.isLinewiseMotion()) end--
}
is ExternalActionHandler -> {
val range: TextRange = action.getRange(caret) ?: return null
start = range.startOffset
end = range.endOffset
if (argument.isLinewiseMotion()) 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 { } else {
start = editor.getLineStartForOffset(start) throw RuntimeException(
end = editor.getLineEndForOffset(end) "Commands doesn't take " + cmdAction.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 (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)
}
} }
} }
@ -384,14 +383,13 @@ abstract class VimMotionGroupBase : VimMotionGroup {
val text = editor.text().subSequence(start, end).toString() val text = editor.text().subSequence(start, end).toString()
val lastNewLine = text.lastIndexOf('\n') val lastNewLine = text.lastIndexOf('\n')
if (lastNewLine > 0) { if (lastNewLine > 0) {
val id = action.id val id = argument.motion.action.id
if (id == "VimMotionWordRightAction" || id == "VimMotionBigWordRightAction" || id == "VimMotionCamelRightAction") { if (id == "VimMotionWordRightAction" || id == "VimMotionBigWordRightAction" || id == "VimMotionCamelRightAction") {
if (!editor.anyNonWhitespace(end, -1)) { if (!editor.anyNonWhitespace(end, -1)) {
end = start + lastNewLine end = start + lastNewLine
} }
} }
} }
return TextRange(start, end) return TextRange(start, end)
} }

View File

@ -8,88 +8,51 @@
package com.maddyhome.idea.vim.command package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.handler.ExternalActionHandler 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.MotionActionHandler import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler import java.util.*
import com.maddyhome.idea.vim.state.mode.SelectionType
/** /**
* Represents an argument to a command's action * 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
* 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`).
*/ */
sealed class Argument { class Argument private constructor(
/** A simple character argument */ val character: Char = 0.toChar(),
class Character(val character: Char) : Argument() val motion: Command = EMPTY_COMMAND,
val offsets: Map<ImmutableVimCaret, VimSelection> = 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<ImmutableVimCaret, VimSelection>) : this(offsets = offsets, type = Type.OFFSETS)
/** An argument representing the user's input from the Ex command line, typically a search string */ enum class Type {
class ExString(val label: Char, val string: String, val processing: ((String) -> Unit)?) : Argument() MOTION, CHARACTER, DIGRAPH, EX_STRING, OFFSETS
/**
* 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 {
* Represents the type of argument, or the type of an expected argument while entering a command @JvmField
*/ val EMPTY_COMMAND: Command = Command(
enum class Type { 0,
object : MotionActionHandler.SingleExecution() {
override fun getOffset(
editor: VimEditor,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
) = Motion.NoMotion
/** override val motionType: MotionType = MotionType.EXCLUSIVE
* A motion argument used to complete an operator, such as `dw` or `diw` },
* Command.Type.MOTION,
* A motion argument will often have its own argument, such as when deleting up to the next occurrence of a EnumSet.noneOf(CommandFlags::class.java),
* 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 `<C-K>`, `<C-V>` and `<C-Q>`, 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
} }
} }

View File

@ -8,47 +8,34 @@
package com.maddyhome.idea.vim.command 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.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import java.util.* import java.util.*
/** /**
* This represents a single Vim command to be executed (action, motion, operator+motion, v_textobject, etc.) * 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.
* 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( data class Command(
val register: Char?, var rawCount: Int,
val rawCount: Int, var action: EditorActionHandlerBase,
val action: EditorActionHandlerBase,
val argument: Argument?,
val type: Type, val type: Type,
val flags: EnumSet<CommandFlags>, var flags: EnumSet<CommandFlags>,
) { ) {
constructor(rawCount: Int, register: Char) : this(
rawCount,
NonExecutableActionHandler,
Type.SELECT_REGISTER,
EnumSet.of(CommandFlags.FLAG_EXPECT_MORE),
) {
this.register = register
}
init { init {
action.process(this) action.process(this)
} }
@ -56,7 +43,20 @@ data class Command(
val count: Int val count: Int
get() = rawCount.coerceAtLeast(1) get() = rawCount.coerceAtLeast(1)
override fun toString() = "Action = ${action.id}" 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}"
}
enum class Type { enum class Type {
/** /**
@ -85,6 +85,10 @@ data class Command(
COPY, COPY,
PASTE, PASTE,
/**
* Represents commands that select the register.
*/
SELECT_REGISTER,
OTHER_READONLY, OTHER_READONLY,
OTHER_WRITABLE, OTHER_WRITABLE,
@ -108,3 +112,18 @@ 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")
}
}

View File

@ -15,78 +15,66 @@ import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.trace
import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase 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.CommandPartNode
import com.maddyhome.idea.vim.key.Node
import com.maddyhome.idea.vim.key.RootNode import com.maddyhome.idea.vim.key.RootNode
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import javax.swing.KeyStroke import javax.swing.KeyStroke
class CommandBuilder private constructor( class CommandBuilder(
private var currentCommandPartNode: CommandPartNode<LazyVimCommand>, private var currentCommandPartNode: CommandPartNode<LazyVimCommand>,
private val counts: MutableList<Int>, private val commandParts: ArrayDeque<Command>,
private val keyList: MutableList<KeyStroke>, private val keyList: MutableList<KeyStroke>,
initialUncommittedRawCount: Int,
) : Cloneable { ) : Cloneable {
constructor(rootNode: RootNode<LazyVimCommand>, initialUncommittedRawCount: Int = 0) constructor(
: this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf()) currentCommandPartNode: CommandPartNode<LazyVimCommand>,
initialUncommittedRawCount: Int = 0
) : this(
currentCommandPartNode,
ArrayDeque(),
mutableListOf(),
initialUncommittedRawCount
)
private var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND 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<KeyStroke> 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
/** /**
* Returns the current total count, as the product of all entered count components. The value is not coerced. * The current uncommitted count for the currently in-progress command part
* *
* This value is not reliable! Please use [Command.rawCount] or [Command.count] instead of this function. * TODO: Investigate usages. This value cannot be trusted
* TODO: Rename to uncommittedRawCount
* *
* This value is a snapshot of the count for a currently in-progress command, and should not be used for anything * This value is not coerced, and can be 0.
* 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.
* *
* The returned value is the product of all count components. In other words, given a command that is an * There are very few reasons for using this value. It is incomplete (the user could type another digit), and there
* operator+motion, both the operator and motion can have a count, such as `2d3w`, which means delete the next six * can be other committed command parts, such as operator and multiple register selections, each of which will can a
* words. Furthermore, Vim allows a count when selecting register, and it is valid to select register multiple times. * count (e.g., `2"a3"b4"c5d6` waiting for a motion). The count is only final after [buildCommand], and then only via
* E.g., `2"a3"b4"c5d6w` will delete the next 720 words and save the text to the register `c`. * [Command.count] or [Command.rawCount].
* *
* The returned value is not coerced. If no count components are specified, the returned value is 0. If any components * The [aggregatedUncommittedCount] property can be used to get the current total count across all command parts,
* are specified, the value will naturally be greater than 0. * although this value is also not guaranteed to be final.
*/ */
fun calculateCount0Snapshot(): Int { var count: Int = initialUncommittedRawCount
return if (counts.all { it == 0 }) 0 else counts.map { it.coerceAtLeast(1) }.reduce { acc, i -> acc * i } 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<KeyStroke> get() = keyList
// TODO: Try to remove this. We shouldn't be looking at the unbuilt command // 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 // This is used by the extension mapping handler, to select the current register before invoking the extension. We
@ -96,18 +84,7 @@ class CommandBuilder private constructor(
// still change, if more keys are processed. E.g., it's perfectly valid to select register multiple times `"a"b`. // 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 // This doesn't cause any issues with existing extensions
val register: Char? val register: Char?
get() = selectedRegister get() = commandParts.lastOrNull { it.register != null }?.register
// 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 * The argument type for the current in-progress command part's action
@ -115,202 +92,79 @@ class CommandBuilder private constructor(
* For digraph arguments, this can fall back to [Argument.Type.CHARACTER] if there isn't a digraph match. * For digraph arguments, this can fall back to [Argument.Type.CHARACTER] if there isn't a digraph match.
*/ */
val expectedArgumentType: Argument.Type? val expectedArgumentType: Argument.Type?
get() = fallbackArgumentType get() = fallbackArgumentType ?: commandParts.lastOrNull()?.action?.argumentType
?: 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
* val isReady: Boolean get() = commandState == CurrentCommandState.READY
* The command builder might be waiting for the argument to a simple motion action such as `f`, waiting for a val isEmpty: Boolean get() = commandParts.isEmpty()
* character to move to, or it might be waiting for the argument to a motion that is itself an argument to an operator val isAtDefaultState: Boolean get() = isEmpty && count == 0 && expectedArgumentType == null
* argument. For example, the character argument to `f` in `df{character}`.
*/ val isExpectingCount: Boolean
val isAwaitingArgument: Boolean get() {
get() = expectedArgumentType != null && (motionArgument?.let { it.argument == null } ?: (argument == null)) return commandState == CurrentCommandState.NEW_COMMAND &&
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
}
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 fallbackToCharacterArgument() { fun fallbackToCharacterArgument() {
logger.trace("fallbackToCharacterArgument is executed") logger.trace { "fallbackToCharacterArgument is executed" }
// Finished handling DIGRAPH. We either succeeded, in which case handle the converted character, or failed to parse, // 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. // in which case try to handle input as a character argument.
assert(expectedArgumentType == Argument.Type.DIGRAPH) { "Cannot move state from $expectedArgumentType to CHARACTER" } assert(expectedArgumentType == Argument.Type.DIGRAPH) { "Cannot move state from $expectedArgumentType to CHARACTER" }
fallbackArgumentType = Argument.Type.CHARACTER fallbackArgumentType = Argument.Type.CHARACTER
} }
fun isAwaitingCharOrDigraphArgument(): Boolean { fun addKey(key: KeyStroke) {
val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH logger.trace { "added key to command builder" }
logger.debug { "Awaiting char or digraph: $awaiting" } keyList.add(key)
return awaiting
} }
val isExpectingCount: Boolean
get() {
return commandState == CurrentCommandState.NEW_COMMAND &&
!isRegisterPending &&
expectedArgumentType != Argument.Type.CHARACTER &&
expectedArgumentType != Argument.Type.DIGRAPH
}
/**
* 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) { fun addCountCharacter(key: KeyStroke) {
currentCount = (currentCount * 10) + (key.keyChar - '0') 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, // 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. // 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 // See https://github.com/vim/vim/blob/b376ace1aeaa7614debc725487d75c8f756dd773/src/normal.c#L631
if (currentCount < 0) { if (count < 0) {
currentCount = 999999999 count = 999999999
} }
addKey(key) addKey(key)
} }
fun deleteCountCharacter() { fun deleteCountCharacter() {
currentCount /= 10 count /= 10
keyList.removeAt(keyList.size - 1) keyList.removeAt(keyList.size - 1)
} }
var isRegisterPending: Boolean = false fun setCurrentCommandPartNode(newNode: CommandPartNode<LazyVimCommand>) {
private set logger.trace { "setCurrentCommandPartNode is executed" }
currentCommandPartNode = newNode
fun startWaitingForRegister(key: KeyStroke) {
isRegisterPending = true
addKey(key)
} }
fun selectRegister(register: Char) { fun getChildNode(key: KeyStroke): Node<LazyVimCommand>? {
logger.trace { "Selected register '$register'" } return currentCommandPartNode[key]
selectedRegister = register
isRegisterPending = false
fallbackArgumentType = null
counts.add(0)
} }
/** fun isAwaitingCharOrDigraphArgument(): Boolean {
* Adds a keystroke to the command builder val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH
* logger.debug { "Awaiting char or digraph: $awaiting" }
* Only public use is when entering a digraph/literal, where each key isn't handled by [CommandBuilder], but should return awaiting
* be added to the `'showcmd'` output.
*/
fun addKey(key: KeyStroke) {
logger.trace { "added key to command builder: $key" }
keyList.add(key)
}
/**
* 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")
}
}
// 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
}
}
/**
* 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
}
}
/**
* 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
}
/**
* 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 { fun isBuildingMultiKeyCommand(): Boolean {
@ -324,47 +178,67 @@ class CommandBuilder private constructor(
return isMultikey return isMultikey
} }
/** fun isDone(): Boolean {
* Build the command with the current counts, register, actions and arguments return commandParts.isEmpty()
* }
* The command builder is reset after the command is built.
*/ 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
}
fun buildCommand(): Command { fun buildCommand(): Command {
val rawCount = calculateCount0Snapshot() var command: Command = commandParts.removeFirst()
val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum()) while (commandParts.size > 0) {
resetAll(currentCommandPartNode.root as RootNode<LazyVimCommand>) 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
return command return command
} }
fun resetAll(rootNode: RootNode<LazyVimCommand>) { fun resetAll(commandPartNode: CommandPartNode<LazyVimCommand>) {
logger.trace("resetAll is executed") logger.trace { "resetAll is executed" }
currentCommandPartNode = rootNode resetInProgressCommandPart(commandPartNode)
commandState = CurrentCommandState.NEW_COMMAND commandState = CurrentCommandState.NEW_COMMAND
counts.clear() commandParts.clear()
counts.add(0)
isRegisterPending = false
selectedRegister = null
action = null
argument = null
keyList.clear() keyList.clear()
fallbackArgumentType = null fallbackArgumentType = null
} }
/** fun resetCount() {
* Change the command trie root node used to find commands for the current mode count = 0
* }
* 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 fun resetInProgressCommandPart(commandPartNode: CommandPartNode<LazyVimCommand>) {
* or fully resetting the command builder, such as when switching to Op-pending while entering an operator+motion. logger.trace { "resetInProgressCommandPart is executed" }
*/ count = 0
fun resetCommandTrieRootNode(rootNode: RootNode<LazyVimCommand>) { setCurrentCommandPartNode(commandPartNode)
logger.trace("resetCommandTrieRootNode is executed")
currentCommandPartNode = rootNode
} }
@TestOnly @TestOnly
fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
@ -372,12 +246,10 @@ class CommandBuilder private constructor(
other as CommandBuilder other as CommandBuilder
if (currentCommandPartNode != other.currentCommandPartNode) return false if (currentCommandPartNode != other.currentCommandPartNode) return false
if (counts != other.counts) return false if (commandParts != other.commandParts) 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 (keyList != other.keyList) return false
if (commandState != other.commandState) return false if (commandState != other.commandState) return false
if (count != other.count) return false
if (expectedArgumentType != other.expectedArgumentType) return false if (expectedArgumentType != other.expectedArgumentType) return false
if (fallbackArgumentType != other.fallbackArgumentType) return false if (fallbackArgumentType != other.fallbackArgumentType) return false
@ -386,38 +258,24 @@ class CommandBuilder private constructor(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = currentCommandPartNode.hashCode() var result = currentCommandPartNode.hashCode()
result = 31 * result + counts.hashCode() result = 31 * result + commandParts.hashCode()
result = 31 * result + selectedRegister.hashCode()
result = 31 * result + action.hashCode()
result = 31 * result + argument.hashCode()
result = 31 * result + keyList.hashCode() result = 31 * result + keyList.hashCode()
result = 31 * result + commandState.hashCode() result = 31 * result + commandState.hashCode()
result = 31 * result + expectedArgumentType.hashCode() result = 31 * result + count
result = 31 * result + fallbackArgumentType.hashCode() result = 31 * result + (expectedArgumentType?.hashCode() ?: 0)
result = 31 * result + (fallbackArgumentType?.hashCode() ?: 0)
return result return result
} }
public override fun clone(): CommandBuilder { public override fun clone(): CommandBuilder {
val result = CommandBuilder( val result = CommandBuilder(currentCommandPartNode, ArrayDeque(commandParts), keyList.toMutableList(), count)
currentCommandPartNode,
counts.toMutableList(),
keyList.toMutableList()
)
result.selectedRegister = selectedRegister
result.action = action
result.argument = argument
result.commandState = commandState result.commandState = commandState
result.fallbackArgumentType = fallbackArgumentType result.fallbackArgumentType = fallbackArgumentType
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "Command state = $commandState, " + return "Command state = $commandState, key list = ${ injector.parser.toKeyNotation(keyList) }, command parts = ${ commandParts }, count = $count\n" +
"key list = ${ injector.parser.toKeyNotation(keyList) }, " +
"selected register = $selectedRegister, " +
"counts = $counts, " +
"action = $action, " +
"argument = $argument, " +
"command part node - $currentCommandPartNode" "command part node - $currentCommandPartNode"
} }

View File

@ -68,6 +68,11 @@ enum class CommandFlags {
*/ */
FLAG_EXPECT_MORE, FLAG_EXPECT_MORE,
/**
* Indicate that the character argument may come from a digraph
*/
FLAG_ALLOW_DIGRAPH,
FLAG_START_EX, FLAG_START_EX,
FLAG_END_EX, FLAG_END_EX,

View File

@ -42,12 +42,12 @@ object MappingProcessor: KeyConsumer {
val keyState = keyProcessResultBuilder.state val keyState = keyProcessResultBuilder.state
val mappingState = keyState.mappingState val mappingState = keyState.mappingState
val commandBuilder = keyState.commandBuilder val commandBuilder = keyState.commandBuilder
if (commandBuilder.isAwaitingCharOrDigraphArgument() if (commandBuilder.isAwaitingCharOrDigraphArgument() ||
|| commandBuilder.isBuildingMultiKeyCommand() commandBuilder.isBuildingMultiKeyCommand() ||
|| commandBuilder.isRegisterPending isMappingDisabledForKey(key, keyState) ||
|| isMappingDisabledForKey(key, keyState) injector.vimState.isRegisterPending
) { ) {
log.debug("Mapping not applicable. Finish key processing, returning false") log.debug("Finish key processing, returning false")
return false return false
} }
mappingState.stopMappingTimer() 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 // "0" can be mapped, but the mapping isn't applied when entering a count. Other digits are always mapped, even when
// entering a count. // entering a count.
// See `:help :map-modes` // See `:help :map-modes`
val isMappingDisabled = key.keyChar == '0' && keyState.commandBuilder.hasCountCharacters() val isMappingDisabled = key.keyChar == '0' && keyState.commandBuilder.count > 0
log.debug { "Mapping disabled for key: $isMappingDisabled" } log.debug { "Mapping disabled for key: $isMappingDisabled" }
return isMappingDisabled return isMappingDisabled
} }

View File

@ -8,53 +8,21 @@
package com.maddyhome.idea.vim.command package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
/** /**
* Represents arguments used when executing a command - either an action, operator or motion * [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.
* TODO: Remove, rename or otherwise refactor this class * The terminology is taken directly from vim.
* * If no count is provided, [count0] defaults to zero.
* 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 data class OperatorArguments(
val isOperatorPending: Boolean,
@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, val count0: Int,
@Deprecated("Represents the mode when the OperatorArguments was created, not the current mode. Prefer editor.mode") val mode: Mode,
val mode: Mode,
) { ) {
/**
* 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) val count1: Int = count0.coerceAtLeast(1)
fun withCount0(count0: Int): OperatorArguments = this.copy(count0 = count0)
} }

View File

@ -11,4 +11,5 @@ package com.maddyhome.idea.vim.common
enum class CurrentCommandState { enum class CurrentCommandState {
NEW_COMMAND, NEW_COMMAND,
READY, READY,
BAD_COMMAND,
} }

View File

@ -1,35 +0,0 @@
/*
* 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.common
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
class VimEditorReplaceMask {
private val changedChars = mutableMapOf<LiveRange, Char>()
fun recordChangeAtCaret(editor: VimEditor) {
for (caret in editor.carets()) {
val offset = caret.offset
val marker = editor.createLiveMarker(offset, offset)
changedChars[marker] = editor.charAt(offset)
}
}
fun popChange(editor: VimEditor, offset: Int): Char? {
val marker = editor.createLiveMarker(offset, offset)
val change = changedChars[marker]
changedChars.remove(marker)
return change
}
}
fun forgetAllReplaceMasks() {
injector.editorGroup.getEditors().forEach { it.replaceMask = null }
}

Some files were not shown because too many files have changed in this diff Show More