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

Compare commits

..

61 Commits

Author SHA1 Message Date
822dad52f3
Set plugin version to chylex-41 2024-09-05 07:40:10 +02:00
bf12c98880
Exit insert mode after refactoring 2024-09-05 07:40:10 +02:00
c02bd15e4f
Add action to run last macro in all opened files 2024-09-05 07:27:53 +02:00
5957d5d681
Stop macro execution after a failed search 2024-09-05 07:27:53 +02:00
9445afb555
Revert per-caret registers 2024-09-05 07:27:53 +02:00
6e115bdf64
Revert "Factor disposable objects on editor opening"
This reverts commit 1fa78935
2024-09-05 07:27:53 +02:00
6305c412b5
Fix(VIM-3364): Exception with mapped Generate action 2024-09-05 07:27:53 +02:00
be2eabe3b9
Apply scrolloff after executing native IDEA actions 2024-09-05 07:27:53 +02:00
de13e4348a
Stay on same line after reindenting 2024-09-05 07:12:09 +02:00
1c154fdc8a
Update search register when using f/t 2024-09-05 07:12:09 +02:00
9b75931736
Automatically add unambiguous imports after running a macro 2024-09-05 07:12:09 +02:00
3fe97aa767
Fix(VIM-3179): Respect virtual space below editor (imperfectly) 2024-09-05 07:12:09 +02:00
8eb2dbebcf
Fix(VIM-3178): Workaround to support "Jump to Source" action mapping 2024-09-05 07:12:09 +02:00
cb781f9a0d
Add support for count for visual and line motion surround 2024-09-05 07:12:09 +02:00
6693755823
Fix vim-surround not working with multiple cursors
Fixes multiple cursors with vim-surround commands `cs, ds, S` (but not `ys`).
2024-09-05 07:12:09 +02:00
9a9bd335d3
Fix(VIM-696) Restore visual mode after undo/redo, and disable incompatible actions 2024-09-05 07:12:09 +02:00
91e22482c6
Respect count with <Action> mappings 2024-09-05 07:08:52 +02:00
06dc2ed75c
Change matchit plugin to use HTML patterns in unrecognized files 2024-09-05 07:02:47 +02:00
12da1ac969
Reset insert mode when switching active editor 2024-09-05 07:02:47 +02:00
5747ac0ebf
Disable switching to insert mode for some editors 2024-09-05 07:02:44 +02:00
8a47bc1840
Remove update checker 2024-09-05 07:02:36 +02:00
da4deaf698
Set custom plugin version 2024-09-05 07:02:36 +02:00
IdeaVim Bot
4c09ab4766 Add Felix Wiemuth to contributors list 2024-08-31 09:01:59 +00:00
Alex Plate
ee447bce07
Add a note to replace the raw string after the changes will be available
https://github.com/JetBrains/intellij-community/pull/2825
2024-08-30 18:57:00 +03:00
Alex Plate
5fb4c10f88
Add 2024.2 IJ for testing on TC 2024-08-30 18:42:02 +03:00
Alex Plate
ed2fcb08b0
[VIM-3620] Add a link to the usage survey 2024-08-30 18:34:10 +03:00
dedd90ce13 Fix(VIM-3615): Escape closes dialog while waiting for more keys 2024-08-30 16:46:53 +03:00
Alex Plate
73326e623e
Use the behavior form in docs 2024-08-30 16:42:03 +03:00
Alex Plate
a2bc34d6ec
[VIM-3620] Show the uninstall survey only when uninstalling IdeaVim 2024-08-30 16:41:36 +03:00
Felix Wiemuth
b652c7726a Fix typo 2024-08-30 16:41:05 +03:00
Matt Ellis
fb7a2de07b Encapsulate the command builder's state flag
This also gets rid of BAD_COMMAND, which was set but never checked - the function that set the flag would immediately reset the command builder
2024-08-30 16:36:24 +03:00
Matt Ellis
def9ca479b Ensure builder resets to a root command trie node
Also refactors command nodes a bit for better debug/trace output
2024-08-30 16:36:24 +03:00
Matt Ellis
0936e0761f Reorder CommandBuilder methods
Try to keep related functions together: awaiting arguments, count, registers, adding action/argument, processing keystrokes, build, reset.
2024-08-30 16:36:24 +03:00
Matt Ellis
09a335bcfe Start to encapsulate setting command builder state
Also rename `pushCommandPart` and `completeCommandPart`
2024-08-30 16:36:24 +03:00
Matt Ellis
37b8d69bac Remove unused EMPTY_COMMAND 2024-08-30 16:36:24 +03:00
Matt Ellis
13308050a8 Remove unnecessary OperatorArguments parameters 2024-08-30 16:36:24 +03:00
Matt Ellis
a1a553ebc9 Deprecate OperatorArguments.mode 2024-08-30 16:36:24 +03:00
Matt Ellis
5bb0c4f7cb Use editor.mode instead of OperatorArguments.mode
`OperatorArguments.mode` is the mode *before* the command was completed, rather than the current mode, which is non-obvious. E.g. for a command in Insert mode, it will still be Insert, and for a (simple) command in Normal, it will still be Normal. The only difference is that a command such as `dw` would be in Operator-pending before the command is completed. That logic is not required for this method, so it's safe to use the current mode.

This allows us to start to deprecate `OperatorArguments.mode`.
2024-08-30 16:36:24 +03:00
Matt Ellis
da6736f24a Simplify the logic when yanking during delete 2024-08-30 16:36:24 +03:00
Matt Ellis
4df1ce2ae8 Remove OperatorArguments.mode usage in block insert
`OperatorArguments.mode` is the mode *before* the command is completed, so might be Visual, Operator-pending, Insert, etc. It's not immediately obvious this is the case, so we're going to deprecate `OperatorArguments.mode` to avoid confusion with `editor.mode`.

It's not required for this method because it's only called for Visual-block mode.
2024-08-30 16:36:24 +03:00
Matt Ellis
00fd4cd491 Handle op-pending for space and backspace 2024-08-30 16:36:24 +03:00
Matt Ellis
d185672e2f Deprecate OperatorArguments.isOperatorPending
Register specific handlers for Operator-pending mode instead of relying on a runtime flag for behaviour. Also refactors and renames some arrow motion handlers.
2024-08-30 16:36:24 +03:00
Matt Ellis
55fef03a4a Add 'whichwrap' to dictionary 2024-08-30 16:36:24 +03:00
Matt Ellis
69b3e4f782 Start to refactor OperatorArguments 2024-08-30 16:36:24 +03:00
Matt Ellis
6be29b0378 Remove KeyHandler.isOperatorPending
It's easier to just look at mode. We don't need the additional check on command builder, because we can't be in OP_PENDING without pushing an operator action to the command builder
2024-08-30 16:36:24 +03:00
Matt Ellis
9965c863a6 Encapsulate command node state in builder 2024-08-30 16:36:24 +03:00
Matt Ellis
3f11ae512c Move register pending state to command builder 2024-08-30 16:36:24 +03:00
Matt Ellis
1c842eb3d8 Avoid exposing misleading count on command builder 2024-08-30 16:36:24 +03:00
Matt Ellis
c7fbce675b Update comment 2024-08-30 16:36:24 +03:00
Matt Ellis
d7e68488c8 Make Command.rawCount immutable 2024-08-30 16:36:24 +03:00
Matt Ellis
69d13a74e6 Remove unnecessary copy method 2024-08-30 16:36:24 +03:00
Matt Ellis
5a83df34a7 Replace var properties with read only 2024-08-30 16:36:24 +03:00
Matt Ellis
0a18c388e0 Simplify CommandBuilder
Fixes selecting last register if multiple registers are used in a command
2024-08-30 16:36:24 +03:00
Matt Ellis
1a3409e7df Remove count from motion argument
Only Command has a count. The motion argument is now a sealed class hierarchy, and consists only of the motion action and optional argument. This is to reduce confusion over which count to use, and potential incorrect calculation of the count
2024-08-30 16:36:24 +03:00
Matt Ellis
e93db961a0 Wrap offsets argument as an external action 2024-08-30 16:36:24 +03:00
Matt Ellis
8fd76bd08f Refactor properties for sealed Argument classes 2024-08-30 16:36:24 +03:00
Matt Ellis
0eea4a5b2c Introduce sealed classes to represent an argument 2024-08-30 16:36:24 +03:00
Matt Ellis
18a0c533e2 Remove unused OperatedRange type 2024-08-30 16:36:24 +03:00
Matt Ellis
0bd8d8f4d2 Remove unused digraph command flag 2024-08-30 16:36:24 +03:00
Alex Plate
64a89c8863
[VIM-3620] Add the uninstall survey
We're actively working on understanding the users and what we can improve in the plugin
2024-08-28 18:57:29 +03:00
Filipp Vakhitov
5b17d7740e Update generated files after merging PRs 2024-08-25 21:51:14 +03:00
126 changed files with 1606 additions and 1140 deletions

View File

@ -27,6 +27,7 @@ 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,6 +535,10 @@ 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 behavioue 'clipboard' 'cb' Defines clipboard behavior
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-40 version=chylex-41
javaVersion=17 javaVersion=17
remoteRobotVersion=0.11.23 remoteRobotVersion=0.11.23
antlrVersion=4.10.1 antlrVersion=4.10.1

View File

@ -8,6 +8,10 @@
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
@ -40,6 +44,18 @@ 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, selectionType: SelectionType): Boolean { private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, motionType: 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 (ex: ExException) { } catch (_: 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.size > 0) { val functionName = if (expression is FunctionCallExpression && expression.arguments.isNotEmpty()) {
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 (selectionType) { val arg = when (motionType) {
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,19 +101,13 @@ 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 ?: return false val argument = cmd.argument as? Argument.Motion ?: 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) {
val selectionType = if (argument.motion.isLinewiseMotion()) { return doOperatorAction(editor, context, range, argument.getMotionType())
SelectionType.LINE_WISE
} else {
SelectionType.CHARACTER_WISE
}
return doOperatorAction(editor, context, range, selectionType)
} }
return false return false
} }
@ -121,7 +115,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
private fun getMotionRange( private fun getMotionRange(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
argument: Argument, argument: Argument.Motion,
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
@ -136,7 +130,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.motion.isLinewiseMotion() && it.endOffset < editor.fileSize()) { if (argument.getMotionType() == SelectionType.LINE_WISE && 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
val lastCommand = VimRepeater.lastChangeCommand var lastCommand = VimRepeater.lastChangeCommand
if (lastCommand == null && Extension.lastExtensionHandler == null) return false if (lastCommand == null && Extension.lastExtensionHandler == null) return false
@ -57,12 +57,7 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
) )
} else if (!repeatHandler && lastCommand != null) { } else if (!repeatHandler && lastCommand != null) {
if (cmd.rawCount > 0) { if (cmd.rawCount > 0) {
lastCommand.rawCount = cmd.count lastCommand = lastCommand.copy(rawCount = cmd.rawCount)
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, operatorArguments) return injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, false)
} }
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, operatorArguments)) { if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true)) {
res = false res = false
} }
} }

View File

@ -22,11 +22,6 @@ 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,7 +33,6 @@ 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;
@ -64,8 +63,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).
@NotNull private final String openBrackets; private final @NotNull String openBrackets;
@NotNull private final String closeBrackets; private final @NotNull String closeBrackets;
static class ParseException extends Exception { static class ParseException extends Exception {
public ParseException(@NotNull String message) { public ParseException(@NotNull String message) {
@ -87,8 +86,7 @@ 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.
*/ */
@NotNull static @NotNull BracketPairs fromBracketPairList(final @NotNull String bracketPairs) throws ParseException {
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;
@ -128,7 +126,7 @@ public class VimArgTextObjExtension implements VimExtension {
return new BracketPairs(openBrackets.toString(), closeBrackets.toString()); return new BracketPairs(openBrackets.toString(), closeBrackets.toString());
} }
BracketPairs(@NotNull final String openBrackets, @NotNull final String closeBrackets) { BracketPairs(final @NotNull String openBrackets, final @NotNull String closeBrackets) {
assert openBrackets.length() == closeBrackets.length(); assert openBrackets.length() == closeBrackets.length();
this.openBrackets = openBrackets; this.openBrackets = openBrackets;
this.closeBrackets = closeBrackets; this.closeBrackets = closeBrackets;
@ -158,10 +156,9 @@ public class VimArgTextObjExtension implements VimExtension {
} }
} }
public static final BracketPairs DEFAULT_BRACKET_PAIRS = new BracketPairs("(", ")"); private static final BracketPairs DEFAULT_BRACKET_PAIRS = new BracketPairs("(", ")");
@Nullable private static @Nullable String bracketPairsVariable() {
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();
@ -192,9 +189,8 @@ public class VimArgTextObjExtension implements VimExtension {
this.isInner = isInner; this.isInner = isInner;
} }
@Nullable
@Override @Override
public TextRange getRange(@NotNull VimEditor editor, public @Nullable TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret, @NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context, @NotNull ExecutionContext context,
int count, int count,
@ -236,24 +232,22 @@ public class VimArgTextObjExtension implements VimExtension {
return new TextRange(finder.getLeftBound(), finder.getRightBound()); return new TextRange(finder.getLeftBound(), finder.getRightBound());
} }
@NotNull
@Override @Override
public TextObjectVisualType getVisualType() { public @NotNull 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 (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) { if (!(editor.getMode() instanceof Mode.OP_PENDING)) {
int count0 = operatorArguments.getCount0();
editor.nativeCarets().forEach((VimCaret caret) -> { editor.nativeCarets().forEach((VimCaret caret) -> {
final TextRange range = textObjectHandler.getRange(editor, caret, context, count, 0); final TextRange range = textObjectHandler.getRange(editor, caret, context, Math.max(1, count0), count0);
if (range != null) { 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) {
@ -265,8 +259,7 @@ public class VimArgTextObjExtension implements VimExtension {
} }
}); });
} else { } else {
keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count, keyHandlerState.getCommandBuilder().addAction(textObjectHandler);
textObjectHandler, Command.Type.MOTION, EnumSet.noneOf(CommandFlags.class))));
} }
} }
} }
@ -276,9 +269,9 @@ public class VimArgTextObjExtension implements VimExtension {
* position * position
*/ */
private static class ArgBoundsFinder { private static class ArgBoundsFinder {
@NotNull private final CharSequence text; private final @NotNull CharSequence text;
@NotNull private final Document document; private final @NotNull Document document;
@NotNull private final BracketPairs brackets; private final @NotNull 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;
@ -305,7 +298,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.length() == 0) { if (text.isEmpty()) {
error = "empty document"; error = "empty document";
return false; return false;
} }

View File

@ -25,9 +25,6 @@ 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
@ -52,7 +49,6 @@ 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 {
@ -184,10 +180,8 @@ 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.completeCommandPart(Argument(command)) keyState.commandBuilder.addAction(CommentaryTextObjectMotionHandler)
} }
} }

View File

@ -44,6 +44,7 @@ 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
@ -93,34 +94,29 @@ 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.
val isInOpPending = keyHandler.isOperatorPending(editor.mode, keyState) if (editor.mode is Mode.OP_PENDING) {
if (isInOpPending) {
val matchitAction = MatchitAction() val matchitAction = MatchitAction()
matchitAction.reverse = reverse matchitAction.reverse = reverse
matchitAction.isInOpPending = true matchitAction.isInOpPending = true
keyState.commandBuilder.completeCommandPart( keyState.commandBuilder.addAction(matchitAction)
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(getMatchitOffset(editor.ij, caret.ij, count, isInOpPending, reverse)) caret.moveToOffset(
getMatchitOffset(
editor.ij,
caret.ij,
operatorArguments.count0,
isInOpPending = false,
reverse
))
} }
} }
} }
@ -354,7 +350,7 @@ private object FileTypePatterns {
private val DEFAULT_PAIRS = setOf('(', ')', '[', ']', '{', '}') private val DEFAULT_PAIRS = setOf('(', ')', '[', ']', '{', '}')
private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPending: Boolean, reverse: Boolean): Int { private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPending: Boolean, reverse: Boolean): Int {
val virtualFile = EditorHelper.getVirtualFile(editor) val virtualFile = EditorHelper.getVirtualFile(editor)
var caretOffset = caret.offset var caretOffset = caret.offset
@ -367,9 +363,9 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPen
val currentChar = editor.document.charsSequence[caretOffset] val currentChar = editor.document.charsSequence[caretOffset]
var motionOffset: Int? = null var motionOffset: Int? = null
if (count > 0) { if (count0 > 0) {
// Matchit doesn't affect the percent motion, so we fall back to the default behavior. // Matchit doesn't affect the percent motion, so we fall back to the default behavior.
motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count) motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count0)
} else { } else {
// Check the simplest case first. // Check the simplest case first.
if (DEFAULT_PAIRS.contains(currentChar)) { if (DEFAULT_PAIRS.contains(currentChar)) {
@ -400,8 +396,7 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPen
private fun getMotionOffset(motion: Motion): Int? { private fun getMotionOffset(motion: Motion): Int? {
return when (motion) { return when (motion) {
is Motion.AbsoluteOffset -> motion.offset is Motion.AdjustedOffset, 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,12 +555,13 @@ private fun registerCommand(default: String, action: NerdAction) {
} }
private val actionsRoot: RootNode<NerdAction> = RootNode() private val actionsRoot: RootNode<NerdAction> = RootNode("NERDTree")
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.keys.toMutableSet() val res = node.children.keys.toMutableSet()
res += node.values.map { collectShortcuts(it) }.flatten() res += node.children.values.map { collectShortcuts(it) }.flatten()
res res
} else { } else {
emptySet() emptySet()

View File

@ -10,7 +10,6 @@ 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
@ -166,17 +165,11 @@ 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,14 +29,12 @@ 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:
* https://github.com/kana/vim-textobj-entire * <a href="https://github.com/kana/vim-textobj-entire">vim-textobj-entire</a>
* *
* <p> * <p>
* vim-textobj-entire provides two text objects: * vim-textobj-entire provides two text objects:
@ -51,7 +49,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:
* https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt * <a href="https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt">text-obj-entire.txt</a>
* *
* @author Alexandre Grison (@agrison) * @author Alexandre Grison (@agrison)
*/ */
@ -94,9 +92,8 @@ public class VimTextObjEntireExtension implements VimExtension {
this.ignoreLeadingAndTrailing = ignoreLeadingAndTrailing; this.ignoreLeadingAndTrailing = ignoreLeadingAndTrailing;
} }
@Nullable
@Override @Override
public TextRange getRange(@NotNull VimEditor editor, public @Nullable TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret, @NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context, @NotNull ExecutionContext context,
int count, int count,
@ -125,24 +122,22 @@ public class VimTextObjEntireExtension implements VimExtension {
return new TextRange(start, end); return new TextRange(start, end);
} }
@NotNull
@Override @Override
public TextObjectVisualType getVisualType() { public @NotNull 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 (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) { if (!(editor.getMode() instanceof Mode.OP_PENDING)) {
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, count, 0); final TextRange range = textObjectHandler.getRange(editor, new IjVimCaret(caret), context, Math.max(1, count0), count0);
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) {
@ -155,9 +150,7 @@ public class VimTextObjEntireExtension implements VimExtension {
}); });
} else { } else {
keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count, keyHandlerState.getCommandBuilder().addAction(textObjectHandler);
textObjectHandler, Command.Type.MOTION,
EnumSet.noneOf(CommandFlags.class))));
} }
} }
} }

View File

@ -30,14 +30,12 @@ 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:
* https://github.com/michaeljsmith/vim-indent-object * <a href="https://github.com/michaeljsmith/vim-indent-object">vim-indent-object</a>
* *
* <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:
@ -49,7 +47,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:
* https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt * <a href="https://github.com/michaeljsmith/vim-indent-object/blob/master/doc/indent-object.txt">indent-object.txt</a>
* *
* @author Shrikant Kandula (@sharat87) * @author Shrikant Kandula (@sharat87)
*/ */
@ -98,9 +96,8 @@ public class VimIndentObject implements VimExtension {
this.includeBelow = includeBelow; this.includeBelow = includeBelow;
} }
@Nullable
@Override @Override
public TextRange getRange(@NotNull VimEditor editor, public @Nullable TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret, @NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context, @NotNull ExecutionContext context,
int count, int count,
@ -249,9 +246,8 @@ public class VimIndentObject implements VimExtension {
return new TextRange(upperBoundaryOffset, lowerBoundaryOffset); return new TextRange(upperBoundaryOffset, lowerBoundaryOffset);
} }
@NotNull
@Override @Override
public TextObjectVisualType getVisualType() { public @NotNull TextObjectVisualType getVisualType() {
return TextObjectVisualType.LINE_WISE; return TextObjectVisualType.LINE_WISE;
} }
@ -264,15 +260,14 @@ 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 (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) { if (!(editor.getMode() instanceof Mode.OP_PENDING)) {
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, count, 0); final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, Math.max(1, count0), count0);
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) {
@ -285,9 +280,7 @@ public class VimIndentObject implements VimExtension {
}); });
} else { } else {
keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count, keyHandlerState.getCommandBuilder().addAction(textObjectHandler);
textObjectHandler, Command.Type.MOTION,
EnumSet.noneOf(CommandFlags.class))));
} }
} }
} }

View File

@ -103,6 +103,11 @@ 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.ex.ExOutputModel 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
@ -193,21 +193,16 @@ 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
@ -216,8 +211,8 @@ internal class MotionGroup : VimMotionGroupBase() {
editor.vim, editor.vim,
caret.vim, caret.vim,
IjEditorExecutionContext(context!!), IjEditorExecutionContext(context!!),
cmd.argument, argument.argument,
operatorArguments.withCount0(raw), operatorArguments
) )
// Invalid motion // Invalid motion
@ -233,22 +228,32 @@ internal class MotionGroup : VimMotionGroupBase() {
end++ end++
} }
} }
} else if (cmd.action is TextObjectActionHandler) { }
val action = cmd.action as TextObjectActionHandler
val range = is TextObjectActionHandler -> {
action.getRange(editor.vim, caret.vim, IjEditorExecutionContext(context!!), cnt, raw) ?: return null val range = action.getRange(
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 (cmd.isLinewiseMotion()) end-- if (argument.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.action.id val id = argument.motion.id
if (id == VimChangeGroupBase.VIM_MOTION_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_BIG_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_CAMEL_RIGHT) { 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')
@ -258,6 +263,7 @@ internal class MotionGroup : VimMotionGroupBase() {
} }
} }
} }
return TextRange(start, end) return TextRange(start, end)
} }

View File

@ -62,6 +62,7 @@ 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,7 +16,6 @@ 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
@ -94,6 +93,6 @@ internal fun VimEditor.exitSelectMode(adjustCaretPosition: Boolean) {
} }
} }
internal fun Editor.exitInsertMode(context: DataContext, operatorArguments: OperatorArguments) { internal fun Editor.exitInsertMode(context: DataContext) {
VimPlugin.getChange().processEscape(IjVimEditor(this), IjEditorExecutionContext(context), operatorArguments) VimPlugin.getChange().processEscape(IjVimEditor(this), IjEditorExecutionContext(context))
} }

View File

@ -24,7 +24,9 @@ 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
@ -123,6 +125,7 @@ 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,7 +15,6 @@ 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
@ -65,7 +64,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, OperatorArguments(false, 0, mode)) is Mode.INSERT -> editor.exitInsertMode(context)
else -> {} else -> {}
} }
} }
@ -79,3 +78,4 @@ class IJEditorFocusListener : EditorListener {
KeyHandler.getInstance().reset(editor) KeyHandler.getInstance().reset(editor)
} }
} }

View File

@ -16,7 +16,9 @@ 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
@ -152,6 +154,10 @@ 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,7 +66,6 @@ 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
@ -395,7 +394,8 @@ internal object VimListenerManager {
editor.vim.mode = Mode.NORMAL() editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim) KeyHandler.getInstance().reset(editor.vim)
} }
injector.scroll.scrollCaretIntoView(editor.vim) // Breaks relativenumber for some reason
// injector.scroll.scrollCaretIntoView(editor.vim)
} }
MotionGroup.fileEditorManagerSelectionChangedCallback(event) MotionGroup.fileEditorManagerSelectionChangedCallback(event)

View File

@ -17,6 +17,24 @@ 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,6 +53,7 @@ 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
@ -75,6 +76,11 @@ 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
@ -91,7 +97,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
editor.vimChangeActionSwitchMode = value editor.vimChangeActionSwitchMode = value
} }
override val indentConfig: VimIndentConfig override val indentConfig: VimIndentConfig
get() = create(editor) get() = IndentConfig.create(editor)
override fun fileSize(): Long = editor.fileSize.toLong() override fun fileSize(): Long = editor.fileSize.toLong()
@ -398,8 +404,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, operatorArguments: OperatorArguments) { override fun exitInsertMode(context: ExecutionContext) {
editor.exitInsertMode(context.ij, operatorArguments) editor.exitInsertMode(context.ij)
} }
override fun exitSelectModeNative(adjustCaret: Boolean) { override fun exitSelectModeNative(adjustCaret: Boolean) {
@ -471,6 +477,7 @@ 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,6 +192,13 @@ 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 the current count from the command builder. This value is coerced to at least 1, so will always be valid. // Get a snapshot of the count for the in progress command, and coerce it to 1. This value will include all
// The aggregated value includes any counts for operator and register selections, and the uncommitted count for // count components - selecting register(s), operator and motions. E.g. `2"a3"b4"c5d6/` will return 720.
// the search command (`/` or `?`). E.g., `2"a3"b4"c5d6/` would return 720. // If we're showing highlights for an Ex command like `:s`, the command builder will be empty, but we'll still
// If we're showing highlights for an ex command like `:s`, there won't be a command, but the value is already // get a valid value.
// coerced to at least 1. int count1 = Math.max(1, KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder()
int count1 = KeyHandler.getInstance().getKeyHandlerState().getEditorCommandBuilder().getAggregatedUncommittedCount(); .calculateCount0Snapshot());
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,9 +531,8 @@ 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 VimCommandLineCaret getCaret() { public @NotNull VimCommandLineCaret getCaret() {
return (VimCommandLineCaret) entry.getCaret(); return (VimCommandLineCaret) entry.getCaret();
} }
@ -551,9 +550,8 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
entry.clearCurrentAction(); entry.clearCurrentAction();
} }
@Nullable
@Override @Override
public Integer getPromptCharacterOffset() { public @Nullable Integer getPromptCharacterOffset() {
int offset = entry.currentActionPromptCharacterOffset; int offset = entry.currentActionPromptCharacterOffset;
return offset == -1 ? null : offset; return offset == -1 ? null : offset;
} }
@ -573,8 +571,7 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
IdeFocusManager.findInstance().requestFocus(entry, true); IdeFocusManager.findInstance().requestFocus(entry, true);
} }
@Nullable public @Nullable VimInputInterceptor<?> getInputInterceptor() {
public VimInputInterceptor<?> getInputInterceptor() {
return myInputInterceptor; return myInputInterceptor;
} }
@ -587,15 +584,13 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
myInputInterceptor = vimInputInterceptor; myInputInterceptor = vimInputInterceptor;
} }
@Nullable
@Override @Override
public Function1<String, Unit> getInputProcessing() { public @Nullable Function1<String, Unit> getInputProcessing() {
return inputProcessing; return inputProcessing;
} }
@Nullable
@Override @Override
public Character getFinishOn() { public @Nullable Character getFinishOn() {
return finishOn; return finishOn;
} }

View File

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

View File

@ -4,16 +4,6 @@
"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,6 +7,7 @@
*/ */
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
@ -1068,4 +1069,14 @@ 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" + "\"ap", "hel<caret>lo world\n") typeTextInFile("\"a\"byl" + "\"bp", "hel<caret>lo world\n")
assertState("helllo world\n") assertState("helllo world\n")
} }

View File

@ -85,4 +85,33 @@ 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,4 +85,35 @@ 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,15 +23,31 @@ 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")
} }
@ -39,11 +55,27 @@ 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,6 +141,16 @@ 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.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } val possibleKeys = (currentNode.children.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,14 +17,13 @@ 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
@ -197,11 +196,9 @@ 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)
} }
@ -210,14 +207,6 @@ 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,
@ -226,11 +215,7 @@ class KeyHandler {
) { ) {
logger.trace("Command execution") logger.trace("Command execution")
val command = keyState.commandBuilder.buildCommand() val command = keyState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments( val operatorArguments = OperatorArguments(command.rawCount, editorState.mode)
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`
@ -295,7 +280,7 @@ class KeyHandler {
keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode())) keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode()))
} }
private fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> { private fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> {
return injector.keyGroup.getKeyRoot(mappingMode) return injector.keyGroup.getKeyRoot(mappingMode)
} }
@ -341,7 +326,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)
@ -361,22 +346,15 @@ 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"
val myMode = editorState.mode if (editorState.mode is Mode.NORMAL && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) {
val returnTo = myMode.returnTo when (editorState.mode.returnTo) {
if (myMode is Mode.NORMAL && returnTo != null && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) { ReturnTo.INSERT -> editor.mode = Mode.INSERT
when (returnTo) { ReturnTo.REPLACE -> editor.mode = Mode.REPLACE
ReturnTo.INSERT -> { null -> {}
editor.mode = Mode.INSERT }
} }
ReturnTo.REPLACE -> { instance.reset(keyState, editorState.mode)
editor.mode = Mode.REPLACE
}
}
}
if (keyState.commandBuilder.isDone()) {
getInstance().reset(keyState, editorState.mode)
}
} }
} }

View File

@ -17,23 +17,17 @@ 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
} }
@ -45,7 +39,7 @@ class ChangeCharacterAction : ChangeEditorActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
return argument != null && changeCharacter(editor, caret, operatorArguments.count1, argument.character) return argument is Argument.Character && changeCharacter(editor, caret, operatorArguments.count1, argument.character)
} }
} }

View File

@ -37,12 +37,11 @@ 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(command), Argument.Motion(motion, null),
operatorArguments, operatorArguments,
) )
} }

View File

@ -15,6 +15,7 @@ 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])
@ -39,4 +40,5 @@ 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,7 +39,6 @@ class ChangeVisualAction : VisualOperatorActionHandler.ForEachCaret() {
range.toVimTextRange(false), range.toVimTextRange(false),
range.type, range.type,
context, context,
operatorArguments,
) )
} }
} }

View File

@ -15,16 +15,13 @@ 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
@ -32,11 +29,8 @@ import java.util.*
@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
} }
@ -50,7 +44,7 @@ class ChangeVisualCharacterAction : VisualOperatorActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val argument = cmd.argument val argument = cmd.argument
return argument != null && return argument is Argument.Character &&
changeCharacterRange(editor, caret, range.toVimTextRange(false), argument.character) changeCharacterRange(editor, caret, range.toVimTextRange(false), argument.character)
} }
} }

View File

@ -56,7 +56,6 @@ 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, operatorArguments) injector.changeGroup.changeRange(editor, caret, blockRange, SelectionType.BLOCK_WISE, context)
} 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, operatorArguments) injector.changeGroup.changeRange(editor, caret, lineRange, SelectionType.LINE_WISE, context)
} }
} }
} }

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) startFilterCommand(editor, context, cmd.rawCount)
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, Argument.EMPTY_COMMAND.copy(rawCount = count)) startFilterCommand(editor, context, count)
return true return true
} }
} }
interface FilterCommand { interface FilterCommand {
fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) { fun startFilterCommand(editor: VimEditor, context: ExecutionContext, count0: Int) {
injector.commandLine.createCommandPrompt(editor, context, cmd, initialText = "!") injector.commandLine.createCommandPrompt(editor, context, count0, 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, operatorArguments) return injector.changeGroup.deleteRange(editor, caret, range, selectionType, false)
} }
} }

View File

@ -40,7 +40,6 @@ 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, operatorArguments) return injector.changeGroup.deleteRange(editor, usedCaret, usedRange, usedType, false)
} }
} }

View File

@ -58,7 +58,6 @@ 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)
@ -67,7 +66,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, operatorArguments) injector.changeGroup.deleteRange(editor, caret, lineRange, SelectionType.LINE_WISE, false)
} }
} }
} }

View File

@ -0,0 +1,40 @@
/*
* 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,7 +52,8 @@ 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 keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character) val argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance() 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,7 +52,8 @@ 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 keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character) val argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance() 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, operatorArguments) return insertDeleteInsertedText(editor, caret)
} }
} }
@ -48,11 +48,7 @@ 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( private fun insertDeleteInsertedText(editor: VimEditor, caret: VimCaret): Boolean {
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) {
@ -65,7 +61,6 @@ private fun insertDeleteInsertedText(
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, operatorArguments) return insertDeletePreviousWord(editor, caret)
} }
} }
@ -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, operatorArguments: OperatorArguments): Boolean { private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret): 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, operato
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, operatorArguments) injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true)
return true return true
} }

View File

@ -18,14 +18,10 @@ 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() {
@ -39,9 +35,8 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
cmd: Command, cmd: Command,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean { ): Boolean {
val argument = cmd.argument val argument = cmd.argument as? Argument.Character ?: return false
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()) {
@ -50,7 +45,7 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
val textToStore = expression.toInsertableString() val textToStore = expression.toInsertableString()
injector.registerGroup.storeTextSpecial('=', textToStore) injector.registerGroup.storeTextSpecial('=', textToStore)
} }
insertRegister(editor, context, '=', operatorArguments) insertRegister(editor, context, '=')
} catch (e: ExException) { } catch (e: ExException) {
injector.messages.indicateError() injector.messages.indicateError()
injector.messages.showStatusBarMessage(editor, e.message) injector.messages.showStatusBarMessage(editor, e.message)
@ -58,7 +53,7 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
} }
return true return true
} else { } else {
return argument != null && insertRegister(editor, context, argument.character, operatorArguments) return insertRegister(editor, context, argument.character)
} }
} }
} }
@ -72,18 +67,13 @@ class InsertRegisterAction : VimActionHandler.SingleExecution() {
* @return true if able to insert the register contents, false if not * @return true if able to insert the register contents, false if not
*/ */
@RWLockLabel.SelfSynchronized @RWLockLabel.SelfSynchronized
private fun insertRegister( private fun insertRegister(editor: VimEditor, context: ExecutionContext, key: Char): Boolean {
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, operatorArguments = operatorArguments) injector.put.putText(editor, context, putData)
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.blockInsert(editor, context, range.toVimTextRange(false), true, operatorArguments) injector.changeGroup.initBlockInsert(editor, context, range.toVimTextRange(false), true)
} 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.blockInsert(editor, context, vimSelection.toVimTextRange(false), false, operatorArguments) injector.changeGroup.initBlockInsert(editor, context, vimSelection.toVimTextRange(false), false)
} 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), operatorArguments) injector.put.putText(editor, context, getPutData(count))
} }
} }

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, initialText = "") injector.commandLine.createCommandPrompt(editor, context, cmd.rawCount, initialText = "")
return true return true
} }
} }

View File

@ -38,7 +38,8 @@ class InsertRegisterAction: VimActionHandler.SingleExecution() {
val caretOffset = cmdLine.caret.offset val caretOffset = cmdLine.caret.offset
val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character) val argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val pasteContent = if ((keyStroke.modifiers and KeyEvent.CTRL_DOWN_MASK) == 0) { 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,6 +13,7 @@ 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
@ -27,8 +28,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 ?: return true val argument = cmd.argument as? Argument.ExString ?: return true
val historyType = VimHistory.Type.getTypeByLabel(argument.character.toString()) val historyType = VimHistory.Type.getTypeByLabel(argument.label.toString())
injector.historyGroup.addEntry(historyType, argument.string) injector.historyGroup.addEntry(historyType, argument.string)
return true return true
} }

View File

@ -34,8 +34,9 @@ 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 {
if (argument?.processing != null) return ExecuteDefinedInputProcessingAction() check(argument is Argument.ExString)
return if (argument?.character == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction(this) if (argument.processing != null) return ExecuteDefinedInputProcessingAction()
return if (argument.label == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction(this)
} }
} }
@ -48,7 +49,7 @@ class ExecuteDefinedInputProcessingAction : MotionActionHandler.SingleExecution(
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument == null) return Motion.Error if (argument !is Argument.ExString) return Motion.Error
val input = argument.string val input = argument.string
val processing = argument.processing!! val processing = argument.processing!!
@ -62,11 +63,11 @@ class ProcessSearchEntryAction(private val parentAction: ProcessExEntryAction) :
get() = throw RuntimeException("Parent motion type should be used, as only it is accessed by other code") 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 == null) return Motion.Error if (argument !is Argument.ExString) return Motion.Error
val offsetAndMotion = when (argument.character) { val offsetAndMotion = when (argument.label) {
'/' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.FORWARDS) '/' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, operatorArguments.count1, Direction.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.character}") else -> throw ExException("Unexpected search label ${argument.label}")
} }
if (offsetAndMotion == null) return Motion.Error if (offsetAndMotion == null) return Motion.Error
parentAction.motionType = offsetAndMotion.second parentAction.motionType = offsetAndMotion.second
@ -78,7 +79,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 == null) return Motion.Error if (argument !is Argument.ExString) 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 ?: return false val argument = cmd.argument as? Argument.Character ?: 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 (e: ExException) { } catch (_: 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 ?: return false val argument = cmd.argument as? Argument.Character ?: return false
val reg = argument.character val reg = argument.character
injector.registerGroup.startRecording(reg) injector.registerGroup.startRecording(reg)
} else { } else {

View File

@ -19,10 +19,21 @@ 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
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) private fun doMotion(
class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() { editor: VimEditor,
caret: ImmutableVimCaret,
count1: Int,
whichwrapKey: String,
allowPastEnd: Boolean,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains(whichwrapKey)
return injector.motion.getHorizontalMotion(editor, caret, count1, allowPastEnd, allowWrap)
}
abstract class MotionNonShiftedArrowLeftBaseAction() : NonShiftedSpecialKeyHandler() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion( override fun motion(
@ -32,8 +43,38 @@ class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("<") return doMotion(editor, caret, -operatorArguments.count1, "<", allowPastEnd)
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,12 +19,23 @@ 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
@CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) private fun doMotion(
class MotionArrowRightAction : NonShiftedSpecialKeyHandler() { editor: VimEditor,
caret: ImmutableVimCaret,
count1: Int,
whichwrapKey: String,
allowPastEnd: Boolean,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains(whichwrapKey)
return injector.motion.getHorizontalMotion(editor, caret, count1, allowPastEnd, allowWrap)
}
abstract class MotionNonShiftedArrowRightBaseAction() : NonShiftedSpecialKeyHandler() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion( override fun motion(
@ -34,9 +45,38 @@ class MotionArrowRightAction : NonShiftedSpecialKeyHandler() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowPastEnd = editor.usesVirtualSpace || editor.isEndAllowed || return doMotion(editor, caret, operatorArguments.count1, ">", allowPastEnd(editor))
operatorArguments.isOperatorPending // because of `d<Right>` removing the last character }
val allowWrap = injector.options(editor).whichwrap.contains(">")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd, allowWrap) protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed
}
// 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, Mode.OP_PENDING]) @CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionBackspaceAction : MotionActionHandler.ForEachCaret() { open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() {
override fun getOffset( override fun getOffset(
editor: VimEditor, editor: VimEditor,
caret: ImmutableVimCaret, caret: ImmutableVimCaret,
@ -30,24 +30,15 @@ class MotionBackspaceAction : MotionActionHandler.ForEachCaret() {
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 = false, allowWrap) return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap)
} }
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
} }
@CommandOrMotion(keys = ["<Space>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) // When the motion is used with an operator, the EOL character is counted.
class MotionSpaceAction : MotionActionHandler.ForEachCaret() { // This allows e.g., `d<BS>` to delete the end of line character on the previous line when wrap is active
override fun getOffset( // ('whichwrap' contains "b")
editor: VimEditor, // See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators
caret: ImmutableVimCaret, @CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.OP_PENDING])
context: ExecutionContext, class MotionBackspaceOpPendingModeAction : MotionBackspaceAction(allowPastEnd = true)
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,13 +27,9 @@ 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.*
@CommandOrMotion(keys = ["<End>"], modes = [Mode.INSERT]) abstract class MotionLastColumnBaseAction(private val isMotionForOperator: Boolean = false)
class MotionLastColumnInsertAction : MotionLastColumnAction() { : MotionActionHandler.ForEachCaret() {
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(
@ -43,13 +39,26 @@ open class MotionLastColumnAction : MotionActionHandler.ForEachCaret() {
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allow = if (editor.inVisualMode) { val allowPastEnd = if (editor.inVisualMode) {
injector.options(editor).selection != "old" injector.options(editor).selection != "old"
} else { } else {
if (operatorArguments.isOperatorPending) false else editor.isEndAllowed // Don't allow past end if this motion is for an operator. I.e., for something like `d$`, we don't want to delete
// the end of line character
if (isMotionForOperator) false else editor.isEndAllowed
} }
val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allow) val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allowPastEnd)
return Motion.AdjustedOffset(offset, VimMotionGroupBase.LAST_COLUMN) 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,8 +21,7 @@ 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 = ["h"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionActionHandler.ForEachCaret() {
class MotionLeftAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset( override fun getOffset(
@ -33,23 +32,16 @@ class MotionLeftAction : MotionActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("h") val allowWrap = injector.options(editor).whichwrap.contains("h")
val allowEnd = operatorArguments.isOperatorPending // dh deletes \n with wrap enabled return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap)
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap)
} }
} }
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.INSERT]) @CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionLeftInsertModeAction : MotionActionHandler.ForEachCaret() { class MotionLeftAction : MotionLeftBaseAction(allowPastEnd = false)
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset( // When the motion is used with an operator, the EOL character is counted.
editor: VimEditor, // This allows e.g., `dh` to delete the end of line character on the previous line when wrap is active
caret: ImmutableVimCaret, // ('whichwrap' contains "h")
context: ExecutionContext, // See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators
argument: Argument?, @CommandOrMotion(keys = ["h"], modes = [Mode.OP_PENDING])
operatorArguments: OperatorArguments, class MotionLeftOpPendingModeAction : MotionLeftBaseAction(allowPastEnd = true)
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("[")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, true, allowWrap)
}
}

View File

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

View File

@ -18,23 +18,19 @@ 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 MotionShiftLeftAction : ShiftedArrowKeyHandler(true) { class MotionShiftArrowLeftAction : 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 vertical = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true) val motion = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true)
caret.moveToMotion(vertical) caret.moveToMotion(motion)
} }
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 newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false) val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false)
caret.moveToMotion(newOffset) caret.moveToMotion(motion)
} }
} }

View File

@ -16,28 +16,21 @@ 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 MotionShiftRightAction : ShiftedArrowKeyHandler(true) { class MotionShiftArrowRightAction : 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 vertical = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true) val motion = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true)
caret.moveToMotion(vertical) caret.moveToMotion(motion)
} }
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 newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false) val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false)
if (newOffset is Motion.AbsoluteOffset) { caret.moveToMotion(motion)
caret.moveToOffset(newOffset.offset)
}
} }
} }

View File

@ -0,0 +1,41 @@
/*
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.action.motion.leftright
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler
@CommandOrMotion(keys = ["<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,15 +15,12 @@ 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,
@ -51,9 +48,6 @@ 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
@ -64,7 +58,7 @@ sealed class TillCharacterMotion(
argument: Argument?, argument: Argument?,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Motion { ): Motion {
if (argument == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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 == null) return Motion.Error if (argument !is Argument.Character) 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,7 +24,6 @@ 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 {
val argument = cmd.argument return cmd.argument.let { it is Argument.Character && injector.markService.setMark(editor, it.character) }
return argument != null && injector.markService.setMark(editor, argument.character)
} }
} }

View File

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

View File

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

View File

@ -37,10 +37,26 @@ 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)
@ -58,13 +74,7 @@ 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( fun deleteJoinLines(editor: VimEditor, caret: VimCaret, count: Int, spaces: Boolean): Boolean
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
@ -92,7 +102,6 @@ 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
@ -112,8 +121,6 @@ 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(
@ -122,7 +129,6 @@ 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
@ -187,7 +193,6 @@ 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,7 +15,6 @@ 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
@ -24,6 +23,7 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.handler.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,7 +111,6 @@ 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)
@ -162,21 +161,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 && range.startOffset == editor.fileSize() updatedRange = if (range.startOffset == range.endOffset
.toInt() && range.startOffset != 0 && range.startOffset == editor.fileSize().toInt()
) { && range.startOffset != 0) {
TextRange(range.startOffset - 1, range.endOffset) TextRange(range.startOffset - 1, range.endOffset)
} else { } else {
return false return false
} }
} }
val mode = operatorArguments.mode val mode = editor.mode
if (type == null || if (type == null ||
(mode == Mode.INSERT || mode == Mode.REPLACE) || (mode == Mode.INSERT || mode == Mode.REPLACE) ||
!saveToRegister || !saveToRegister ||
@ -239,11 +238,10 @@ 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()) {
for (i in 0 until count) { repeat(count) {
for (lastStroke in myLastStrokes) { for (lastStroke in myLastStrokes) {
when (lastStroke) { when (lastStroke) {
is NativeAction -> { is NativeAction -> {
@ -252,7 +250,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
} }
is EditorActionHandlerBase -> { is EditorActionHandlerBase -> {
injector.actionExecutor.executeVimAction(editor, lastStroke, context, operatorArguments) injector.actionExecutor.executeVimAction(editor, lastStroke, context, OperatorArguments(0, editor.mode))
strokes.add(lastStroke) strokes.add(lastStroke)
} }
@ -281,7 +279,6 @@ 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) {
@ -302,17 +299,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, operatorArguments) repeatInsertText(editor, context, updatedCount)
} 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, operatorArguments) repeatInsertText(editor, context, updatedCount)
} }
} }
caret.moveToOffset(position) caret.moveToOffset(position)
} else { } else {
repeatInsertText(editor, context, count, operatorArguments) repeatInsertText(editor, context, count)
val position = injector.motion.getHorizontalMotion(editor, caret, -1, false) val position = injector.motion.getHorizontalMotion(editor, caret, -1, false)
caret.moveToMotion(position) caret.moveToMotion(position)
} }
@ -330,7 +327,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val oldFragmentLength = oldFragment.length val oldFragmentLength = oldFragment.length
// Repeat buffer limits // Repeat buffer limits
if (repeatCharsCount > Companion.MAX_REPEAT_CHARS_COUNT) { if (repeatCharsCount > MAX_REPEAT_CHARS_COUNT) {
return return
} }
@ -351,7 +348,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) {
for (i in 0 until oldFragmentLength) { repeat(oldFragmentLength) {
strokes.add(editorDelete) strokes.add(editorDelete)
} }
} }
@ -370,7 +367,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)
for (i in 0 until count) { repeat(count) {
positionCaretActions.add(action) positionCaretActions.add(action)
} }
return positionCaretActions return positionCaretActions
@ -449,25 +446,8 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (mode == Mode.REPLACE) { if (mode == Mode.REPLACE) {
editor.insertMode = false editor.insertMode = false
} }
if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) { val count = if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) 1 else cmd.count
val commandState = injector.vimState repeatInsert(editor, context, count, false)
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
} }
@ -532,9 +512,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
exit: Boolean, exit: Boolean,
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
) { ) {
repeatInsertText(editor, context, 1, operatorArguments) repeatInsertText(editor, context, 1)
if (exit) { if (exit) {
editor.exitInsertMode(context, operatorArguments) editor.exitInsertMode(context)
} }
} }
@ -544,7 +524,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?, operatorArguments: OperatorArguments) { override fun processEscape(editor: VimEditor, context: ExecutionContext?) {
// Get the offset for marks before we exit insert mode - switching from insert to overtype subtracts one from the // 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
@ -553,17 +533,24 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (editor.mode is Mode.REPLACE) { if (editor.mode is Mode.REPLACE) {
editor.insertMode = true editor.insertMode = true
} }
var cnt = if (lastInsert != null) lastInsert!!.count else 0 val repeatCount0 = lastInsert?.let {
if (lastInsert != null && lastInsert!!.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) { // How many times do we want to *repeat* the insert? For a simple insert or change action, this is count-1. But if
cnt = 1 // the command is an operator+motion, then the count applies to the motion, not the insert/change. I.e., `2cw`
// changes two words, rather than inserting the change twice. This is the only place where we need to know who the
// count applies to
if (CommandFlags.FLAG_NO_REPEAT_INSERT in it.flags || it.action.argumentType == Argument.Type.MOTION) {
0
} else {
it.count - 1
} }
} ?: 0
if (vimDocument != null && vimDocumentListener != null) { 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, if (cnt == 0) 0 else cnt - 1, true, operatorArguments) injector.changeGroup.repeatInsert(editor, context, repeatCount0, true)
} }
if (editor.mode is Mode.INSERT) { if (editor.mode is Mode.INSERT) {
updateLastInsertedTextRegister() updateLastInsertedTextRegister()
@ -677,7 +664,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, operatorArguments) val res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE, caret)
if (editor.usesVirtualSpace) { if (editor.usesVirtualSpace) {
caret.moveToOffset(startOffset) caret.moveToOffset(startOffset)
} else { } else {
@ -704,7 +691,6 @@ 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
@ -713,7 +699,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, operatorArguments) deleteJoinNLines(editor, caret, lline, myCount, spaces)
} }
} }
@ -731,12 +717,14 @@ 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
} }
@ -784,7 +772,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, operatorArguments) val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret)
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(
@ -807,7 +795,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
lline + count <= total lline + count <= total
} }
if (!allowedExecution) return false if (!allowedExecution) return false
for (i in 0 until executions) { repeat(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)
@ -837,7 +825,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, operatorArguments) return deleteJoinNLines(editor, caret, startLine, count, spaces)
} }
override fun joinViaIdeaBySelections( override fun joinViaIdeaBySelections(
@ -874,28 +862,25 @@ 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
var type: SelectionType = if (argument.motion.isLinewiseMotion()) { if (!isChange && motionType != SelectionType.LINE_WISE) {
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
if (!editor.anyNonWhitespace(range.startOffset, -1) && !editor.anyNonWhitespace(range.endOffset, 1)) { && !editor.anyNonWhitespace(range.startOffset, -1)
type = SelectionType.LINE_WISE && !editor.anyNonWhitespace(range.endOffset, 1)) {
motionType = SelectionType.LINE_WISE
} }
} }
} return Pair(range, motionType)
return Pair(range, type)
} }
/** /**
@ -915,13 +900,12 @@ 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, operatorArguments, saveToRegister) val res = deleteText(editor, range, type, caret, 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()
@ -1040,7 +1024,6 @@ 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
@ -1057,7 +1040,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, operatorArguments) deleteText(editor, TextRange(startOffset, endOffset), null, caret)
if (spaces && !hasTrailingWhitespace) { if (spaces && !hasTrailingWhitespace) {
insertText(editor, caret, startOffset, " ") insertText(editor, caret, startOffset, " ")
} }
@ -1144,8 +1127,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
val motion = argument.motion var motionArgument = argument as? Argument.Motion ?: return false
val id = motion.action.id val id = motionArgument.motion.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()
@ -1155,7 +1138,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 && motion.count == 1) { if (wordMotions.contains(id) && lastWordChar && operatorArguments.count1 == 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
@ -1165,49 +1148,50 @@ abstract class VimChangeGroupBase : VimChangeGroup {
when (id) { when (id) {
VIM_MOTION_WORD_RIGHT -> { VIM_MOTION_WORD_RIGHT -> {
kludge = true kludge = true
motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT) motionArgument = Argument.Motion(
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
} }
VIM_MOTION_BIG_WORD_RIGHT -> { VIM_MOTION_BIG_WORD_RIGHT -> {
kludge = true kludge = true
motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT) motionArgument = Argument.Motion(
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
} }
VIM_MOTION_CAMEL_RIGHT -> { VIM_MOTION_CAMEL_RIGHT -> {
kludge = true kludge = true
motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT) motionArgument = Argument.Motion(
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
} }
} }
} }
} }
if (kludge) { if (kludge) {
val cnt = operatorArguments.count1 * motion.count val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, operatorArguments.count1, bigWord, false)
val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, cnt, bigWord, false) val pos2 = injector.searchHelper.findNextWordEnd(editor, pos1, -operatorArguments.count1, 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) { if (pos2 == offset && operatorArguments.count1 > 1) {
if (operatorArguments.count1 > 1) {
count0-- 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,
argument, motionArgument,
true, true,
operatorArguments.withCount0(count0), operatorArguments.copy(count0 = count0),
) ?: return false ) ?: return false
return changeRange( return changeRange(
editor, editor,
@ -1215,7 +1199,6 @@ abstract class VimChangeGroupBase : VimChangeGroup {
first, first,
second, second,
context, context,
operatorArguments,
) )
} }
@ -1240,7 +1223,6 @@ 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(
@ -1249,7 +1231,6 @@ 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
@ -1262,7 +1243,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, operatorArguments) val res = deleteRange(editor, caret, range, type, true)
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) {
@ -1924,7 +1905,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
pos++ pos++
} }
if (pos > wsoff) { if (pos > wsoff) {
deleteText(editor, TextRange(wsoff, pos), null, caret, operatorArguments, true) deleteText(editor, TextRange(wsoff, pos), null, caret, true)
} }
} }
} }
@ -1956,23 +1937,21 @@ abstract class VimChangeGroupBase : VimChangeGroup {
} }
} }
override fun blockInsert( override fun initBlockInsert(
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)
val mode = operatorArguments.mode // Note that when called, we're likely to have moved from Visual (block) to Normal, which means all secondary carets
val visualBlockMode = mode is Mode.VISUAL && mode.selectionType === SelectionType.BLOCK_WISE // will have been removed. Even if not, this would move them all to the same location, which would remove them and
// leave only the primary caret.
for (caret in editor.carets()) { for (caret in editor.carets()) {
val line = startPosition.line val line = startPosition.line
var column = startPosition.column var column = startPosition.column
if (!visualBlockMode) { if (append) {
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
@ -1984,18 +1963,10 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val offset = editor.getLineEndOffset(line) val offset = editor.getLineEndOffset(line)
insertText(editor, caret, offset, pad) insertText(editor, caret, offset, pad)
} }
if (visualBlockMode || !append) {
caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column))) caret.moveToInlayAwareOffset(editor.bufferPositionToOffset(BufferPosition(line, column)))
}
if (visualBlockMode) {
setInsertRepeat(lines, column, append) setInsertRepeat(lines, column, append)
} }
}
if (visualBlockMode || !append) {
insertBeforeCursor(editor, context) insertBeforeCursor(editor, context)
} else {
insertAfterCursor(editor, context)
}
return true return true
} }
@ -2068,9 +2039,3 @@ 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,8 +8,6 @@
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
@ -26,7 +24,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, command: Command, initialText: String): VimCommandLine fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, count0: Int, 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,7 +8,6 @@
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
@ -50,15 +49,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, command: Command, initialText: String): VimCommandLine { override fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, count0: Int, initialText: String): VimCommandLine {
val rangeText = getRange(editor, command) val rangeText = getRange(editor, count0)
return createCommandLinePrompt(editor, context, removeSelections = true, label = ":", rangeText + initialText) return createCommandLinePrompt(editor, context, removeSelections = true, label = ":", rangeText + initialText)
} }
protected fun getRange(editor: VimEditor, cmd: Command) = when { protected fun getRange(editor: VimEditor, count0: Int) = when {
editor.inVisualMode -> "'<,'>" editor.inVisualMode -> "'<,'>"
cmd.rawCount == 1 -> "." count0 == 1 -> "."
cmd.rawCount > 1 -> ".,.+" + (cmd.count - 1) count0 > 1 -> ".,.+" + (count0 - 1)
else -> "" else -> ""
} }
} }

View File

@ -11,6 +11,7 @@ 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
@ -128,6 +129,7 @@ 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
@ -244,7 +246,9 @@ 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
fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) @Deprecated("Use overload without OperatorArguments", replaceWith = ReplaceWith("exitInsertMode(context)"))
fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) { exitInsertMode(context) }
fun exitInsertMode(context: ExecutionContext)
fun exitSelectModeNative(adjustCaret: Boolean) fun exitSelectModeNative(adjustCaret: Boolean)
var vimLastSelectionType: SelectionType? var vimLastSelectionType: SelectionType?

View File

@ -8,6 +8,7 @@
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 {
@ -18,6 +19,9 @@ 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): CommandPartNode<LazyVimCommand> fun getKeyRoot(mappingMode: MappingMode): RootNode<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,7 +12,6 @@ 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
@ -30,7 +29,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, CommandPartNode<LazyVimCommand>> = EnumMap(MappingMode::class.java) val keyRoots: MutableMap<MappingMode, RootNode<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>) {
@ -63,7 +62,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): CommandPartNode<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode() } override fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode(mappingMode.name.get(0).lowercase()) }
override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode) override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode)

View File

@ -15,6 +15,7 @@ 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
@ -116,9 +117,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
for (i in 0 until count.absoluteValue) { repeat(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 ?: break current = newOffset ?: return@repeat
} }
val offset = if (allowWrap) { val offset = if (allowWrap) {
@ -313,50 +314,51 @@ 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 offsets = argument.offsets[caret] ?: return null val action = argument.motion
val (first, second) = offsets.getNativeStartAndEnd() when (action) {
start = first is MotionActionHandler -> {
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 = val motion = action.getHandlerOffset(editor, caret, context, argument.argument, operatorArguments)
cmdAction.getHandlerOffset(editor, caret, context, cmd.argument, operatorArguments.withCount0(raw)) if (Motion.Error == motion || Motion.NoMotion == motion) return null
// Invalid motion
if (Motion.Error == motion) return null
if (Motion.NoMotion == motion) return null
end = (motion as AbsoluteOffset).offset end = (motion as AbsoluteOffset).offset
// If inclusive, add the last character to the range // If inclusive, add the last character to the range
if (cmdAction.motionType === MotionType.INCLUSIVE) { if (action.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 (cmd.isLinewiseMotion()) end-- if (argument.isLinewiseMotion()) end--
} else { }
throw RuntimeException(
"Commands doesn't take " + cmdAction.javaClass.simpleName + " as an operator", 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 // Normalize the range
@ -368,7 +370,7 @@ abstract class VimMotionGroupBase : VimMotionGroup {
// If we are a linewise motion we need to normalize the start and stop then move the start to the beginning // 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. // of the line and move the end to the end of the line.
if (cmd.isLinewiseMotion()) { if (argument.isLinewiseMotion()) {
if (caret.getBufferPosition().line != editor.lineCount() - 1) { if (caret.getBufferPosition().line != editor.lineCount() - 1) {
start = editor.getLineStartForOffset(start) start = editor.getLineStartForOffset(start)
end = min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt() end = min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt()
@ -377,19 +379,19 @@ abstract class VimMotionGroupBase : VimMotionGroup {
end = editor.getLineEndForOffset(end) end = editor.getLineEndForOffset(end)
} }
} }
}
// 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 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 = argument.motion.action.id val id = 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,51 +8,88 @@
package com.maddyhome.idea.vim.command package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.api.ImmutableVimCaret import com.maddyhome.idea.vim.handler.ExternalActionHandler
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 java.util.* import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import com.maddyhome.idea.vim.state.mode.SelectionType
/** /**
* This represents a command argument. * Represents an argument to a command's action
* 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`).
*/ */
class Argument private constructor( sealed class Argument {
val character: Char = 0.toChar(), /** A simple character argument */
val motion: Command = EMPTY_COMMAND, class Character(val character: Char) : Argument()
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 */
class ExString(val label: Char, val string: String, val processing: ((String) -> Unit)?) : Argument()
/**
* Represents an argument that is a motion. Used by operator commands
*
* A command is either an action (like `i`), a motion (like `w`) or an operator that takes a motion as an argument
* (like `dw`). A motion argument is a motion action handler with its own optional argument. The motion action handler
* could be a [MotionActionHandler] or [TextObjectActionHandler], or even the [ExternalActionHandler] that tracks the
* caret moves from an external action such as EasyMotion/AceJump. A motion might be a simple motion such as `w` to
* move a word, or require a character argument (`f`), or even an ex-string (`/foo`).
*
* Note that a motion argument does not have a count - that is owned by the fully built command. When executing the
* command, the count applies to the motion action, not the operator action. This just means the operator action
* does not use the count. (`3i` means insert the following typed text three times. But `3cw` means change the next
* three words with the following inserted text, rather than change the next word by inserting the following text
* three times.)
*
* @see Command
*/
class Motion private constructor(val motion: EditorActionHandlerBase, val argument: Argument? = null) : Argument() {
constructor(motion: MotionActionHandler, argument: Argument?) : this(motion as EditorActionHandlerBase, argument)
constructor(motion: TextObjectActionHandler) : this(motion as EditorActionHandlerBase)
constructor(motion: ExternalActionHandler) : this (motion as EditorActionHandlerBase)
fun getMotionType() = if (isLinewiseMotion()) SelectionType.LINE_WISE else SelectionType.CHARACTER_WISE
fun isLinewiseMotion(): Boolean {
return motion.let {
when (it) {
is TextObjectActionHandler -> it.visualType == TextObjectVisualType.LINE_WISE
is MotionActionHandler -> it.motionType == MotionType.LINE_WISE
is ExternalActionHandler -> it.isLinewiseMotion
else -> error("Command is not a motion: $motion")
}
}
}
fun withArgument(argument: Argument) = Motion(motion, argument)
}
/**
* Represents the type of argument, or the type of an expected argument while entering a command
*/
enum class Type { enum class Type {
MOTION, CHARACTER, DIGRAPH, EX_STRING, OFFSETS
}
companion object { /**
@JvmField * A motion argument used to complete an operator, such as `dw` or `diw`
val EMPTY_COMMAND: Command = Command( *
0, * A motion argument will often have its own argument, such as when deleting up to the next occurrence of a
object : MotionActionHandler.SingleExecution() { * character, as in `dfx`.
override fun getOffset( */
editor: VimEditor, MOTION,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
) = Motion.NoMotion
override val motionType: MotionType = MotionType.EXCLUSIVE /** A character argument, such as the character to move to with the `f` command. */
}, CHARACTER,
Command.Type.MOTION,
EnumSet.noneOf(CommandFlags::class.java), /**
) * 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,34 +8,47 @@
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 (operator, motion, text object, etc.). It may optionally include * This represents a single Vim command to be executed (action, motion, operator+motion, v_textobject, etc.)
* 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(
var rawCount: Int, val register: Char?,
var action: EditorActionHandlerBase, val rawCount: Int,
val action: EditorActionHandlerBase,
val argument: Argument?,
val type: Type, val type: Type,
var flags: EnumSet<CommandFlags>, val 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)
} }
@ -43,20 +56,7 @@ data class Command(
val count: Int val count: Int
get() = rawCount.coerceAtLeast(1) get() = rawCount.coerceAtLeast(1)
var argument: Argument? = null override fun toString() = "Action = ${action.id}"
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,10 +85,6 @@ data class Command(
COPY, COPY,
PASTE, PASTE,
/**
* Represents commands that select the register.
*/
SELECT_REGISTER,
OTHER_READONLY, OTHER_READONLY,
OTHER_WRITABLE, OTHER_WRITABLE,
@ -112,18 +108,3 @@ data class Command(
} }
} }
} }
private object NonExecutableActionHandler : EditorActionHandlerBase(false) {
override val type: Command.Type
get() = error("This action should not be executed")
override fun baseExecute(
editor: VimEditor,
caret: VimCaret,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
error("This action should not be executed")
}
}

View File

@ -15,67 +15,79 @@ 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( class CommandBuilder private constructor(
private var currentCommandPartNode: CommandPartNode<LazyVimCommand>, private var currentCommandPartNode: CommandPartNode<LazyVimCommand>,
private val commandParts: ArrayDeque<Command>, private val counts: MutableList<Int>,
private val keyList: MutableList<KeyStroke>, private val keyList: MutableList<KeyStroke>,
initialUncommittedRawCount: Int,
) : Cloneable { ) : Cloneable {
constructor( constructor(rootNode: RootNode<LazyVimCommand>, initialUncommittedRawCount: Int = 0)
currentCommandPartNode: CommandPartNode<LazyVimCommand>, : this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf())
initialUncommittedRawCount: Int = 0
) : this(
currentCommandPartNode,
ArrayDeque(),
mutableListOf(),
initialUncommittedRawCount
)
var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND private var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND
private var selectedRegister: Char? = null
private var action: EditorActionHandlerBase? = null
private var argument: Argument? = null
private var fallbackArgumentType: Argument.Type? = null
/** private val motionArgument
* The current uncommitted count for the currently in-progress command part get() = argument as? Argument.Motion
*
* TODO: Investigate usages. This value cannot be trusted
* TODO: Rename to uncommittedRawCount
*
* This value is not coerced, and can be 0.
*
* There are very few reasons for using this value. It is incomplete (the user could type another digit), and there
* can be other committed command parts, such as operator and multiple register selections, each of which will can a
* count (e.g., `2"a3"b4"c5d6` waiting for a motion). The count is only final after [buildCommand], and then only via
* [Command.count] or [Command.rawCount].
*
* The [aggregatedUncommittedCount] property can be used to get the current total count across all command parts,
* although this value is also not guaranteed to be final.
*/
var count: Int = initialUncommittedRawCount
private set
/** private var currentCount: Int
* The current aggregated, but uncommitted count for all command parts in the command builder, coerced to 1 get() = counts.last()
* set(value) {
* This value multiplies together the count for command parts currently committed, such as operator and multiple counts[counts.size - 1] = value
* 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)
/** Provide the typed keys for `'showcmd'` */
val keys: Iterable<KeyStroke> get() = keyList 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.
*
* This value is not reliable! Please use [Command.rawCount] or [Command.count] instead of this function.
*
* This value is a snapshot of the count for a currently in-progress command, and should not be used for anything
* other than reporting on the state of the command. This value is likely to change as the user continues entering the
* command. There are very few expected uses of this value. Examples include calculating `'incsearch'` highlighting
* for an in-progress search command, or the `v:count` and `v:count1` variables used during an expression mapping.
*
* The returned value is the product of all count components. In other words, given a command that is an
* operator+motion, both the operator and motion can have a count, such as `2d3w`, which means delete the next six
* words. Furthermore, Vim allows a count when selecting register, and it is valid to select register multiple times.
* E.g., `2"a3"b4"c5d6w` will delete the next 720 words and save the text to the register `c`.
*
* The returned value is not coerced. If no count components are specified, the returned value is 0. If any components
* are specified, the value will naturally be greater than 0.
*/
fun calculateCount0Snapshot(): Int {
return if (counts.all { it == 0 }) 0 else counts.map { it.coerceAtLeast(1) }.reduce { acc, i -> acc * i }
}
// TODO: Try to remove this. We shouldn't be looking at the unbuilt command // 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
// need better handling of extensions so that they integrate better with half-built commands, either by finishing or // need better handling of extensions so that they integrate better with half-built commands, either by finishing or
@ -84,7 +96,18 @@ class CommandBuilder(
// still change, if more keys are processed. E.g., it's perfectly valid to select register multiple times `"a"b`. // 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() = commandParts.lastOrNull { it.register != null }?.register get() = selectedRegister
// TODO: Try to remove this too. Also used by extension handling
fun hasCurrentCommandPartArgument() = motionArgument != null || argument != null
// TODO: And remove this too. More extension special case code
// It's used by the Matchit extension to incorrectly reset the command builder. Extensions need a way to properly
// handle the command builder. I.e., they should act like expression mappings, which return keys to evaluate, or an
// empty string to leave state as it is - either way, it's an explicit choice. Currently, extensions mostly ignore it
fun resetCount() {
counts[counts.size - 1] = 0
}
/** /**
* The argument type for the current in-progress command part's action * The argument type for the current in-progress command part's action
@ -92,79 +115,202 @@ class CommandBuilder(
* For digraph arguments, this can fall back to [Argument.Type.CHARACTER] if there isn't a digraph match. * 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 ?: commandParts.lastOrNull()?.action?.argumentType get() = fallbackArgumentType
?: motionArgument?.let { return it.motion.argumentType }
?: action?.argumentType
private var fallbackArgumentType: Argument.Type? = null /**
* Returns true if the command builder is waiting for an argument
val isReady: Boolean get() = commandState == CurrentCommandState.READY *
val isEmpty: Boolean get() = commandParts.isEmpty() * The command builder might be waiting for the argument to a simple motion action such as `f`, waiting for a
val isAtDefaultState: Boolean get() = isEmpty && count == 0 && expectedArgumentType == null * character to move to, or it might be waiting for the argument to a motion that is itself an argument to an operator
* argument. For example, the character argument to `f` in `df{character}`.
val isExpectingCount: Boolean */
get() { val isAwaitingArgument: Boolean
return commandState == CurrentCommandState.NEW_COMMAND && get() = expectedArgumentType != null && (motionArgument?.let { it.argument == null } ?: (argument == null))
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 addKey(key: KeyStroke) { fun isAwaitingCharOrDigraphArgument(): Boolean {
logger.trace { "added key to command builder" } val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH
keyList.add(key) logger.debug { "Awaiting char or digraph: $awaiting" }
return awaiting
} }
val isExpectingCount: Boolean
get() {
return commandState == CurrentCommandState.NEW_COMMAND &&
!isRegisterPending &&
expectedArgumentType != Argument.Type.CHARACTER &&
expectedArgumentType != Argument.Type.DIGRAPH
}
/**
* 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) {
count = (count * 10) + (key.keyChar - '0') currentCount = (currentCount * 10) + (key.keyChar - '0')
// If count overflows and flips negative, reset to 999999999L. In Vim, count is a long, which is *usually* 32 bits, // 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 (count < 0) { if (currentCount < 0) {
count = 999999999 currentCount = 999999999
} }
addKey(key) addKey(key)
} }
fun deleteCountCharacter() { fun deleteCountCharacter() {
count /= 10 currentCount /= 10
keyList.removeAt(keyList.size - 1) keyList.removeAt(keyList.size - 1)
} }
fun setCurrentCommandPartNode(newNode: CommandPartNode<LazyVimCommand>) { var isRegisterPending: Boolean = false
logger.trace { "setCurrentCommandPartNode is executed" } private set
currentCommandPartNode = newNode
fun startWaitingForRegister(key: KeyStroke) {
isRegisterPending = true
addKey(key)
} }
fun getChildNode(key: KeyStroke): Node<LazyVimCommand>? { fun selectRegister(register: Char) {
return currentCommandPartNode[key] logger.trace { "Selected register '$register'" }
selectedRegister = register
isRegisterPending = false
fallbackArgumentType = null
counts.add(0)
} }
fun isAwaitingCharOrDigraphArgument(): Boolean { /**
val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH * Adds a keystroke to the command builder
logger.debug { "Awaiting char or digraph: $awaiting" } *
return awaiting * Only public use is when entering a digraph/literal, where each key isn't handled by [CommandBuilder], but should
* be added to the `'showcmd'` output.
*/
fun addKey(key: KeyStroke) {
logger.trace { "added key to command builder: $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 {
@ -178,67 +324,47 @@ class CommandBuilder(
return isMultikey return isMultikey
} }
fun isDone(): Boolean { /**
return commandParts.isEmpty() * Build the command with the current counts, register, actions and arguments
} *
* 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 {
var command: Command = commandParts.removeFirst() val rawCount = calculateCount0Snapshot()
while (commandParts.size > 0) { val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum())
val next = commandParts.removeFirst() resetAll(currentCommandPartNode.root as RootNode<LazyVimCommand>)
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(commandPartNode: CommandPartNode<LazyVimCommand>) { fun resetAll(rootNode: RootNode<LazyVimCommand>) {
logger.trace { "resetAll is executed" } logger.trace("resetAll is executed")
resetInProgressCommandPart(commandPartNode) currentCommandPartNode = rootNode
commandState = CurrentCommandState.NEW_COMMAND commandState = CurrentCommandState.NEW_COMMAND
commandParts.clear() counts.clear()
counts.add(0)
isRegisterPending = false
selectedRegister = null
action = null
argument = null
keyList.clear() keyList.clear()
fallbackArgumentType = null fallbackArgumentType = null
} }
fun resetCount() { /**
count = 0 * Change the command trie root node used to find commands for the current mode
} *
* Typically, we reset the command trie root node after a command is executed, using the root node of the current
fun resetInProgressCommandPart(commandPartNode: CommandPartNode<LazyVimCommand>) { * mode - this is handled by [resetAll]. This function allows us to change the root node without executing a command
logger.trace { "resetInProgressCommandPart is executed" } * or fully resetting the command builder, such as when switching to Op-pending while entering an operator+motion.
count = 0 */
setCurrentCommandPartNode(commandPartNode) fun resetCommandTrieRootNode(rootNode: RootNode<LazyVimCommand>) {
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
@ -246,10 +372,12 @@ class CommandBuilder(
other as CommandBuilder other as CommandBuilder
if (currentCommandPartNode != other.currentCommandPartNode) return false if (currentCommandPartNode != other.currentCommandPartNode) return false
if (commandParts != other.commandParts) return false if (counts != other.counts) return false
if (selectedRegister != other.selectedRegister) return false
if (action != other.action) return false
if (argument != other.argument) return false
if (keyList != other.keyList) return false if (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
@ -258,24 +386,38 @@ class CommandBuilder(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = currentCommandPartNode.hashCode() var result = currentCommandPartNode.hashCode()
result = 31 * result + commandParts.hashCode() result = 31 * result + counts.hashCode()
result = 31 * result + selectedRegister.hashCode()
result = 31 * result + action.hashCode()
result = 31 * result + argument.hashCode()
result = 31 * result + keyList.hashCode() result = 31 * result + keyList.hashCode()
result = 31 * result + commandState.hashCode() result = 31 * result + commandState.hashCode()
result = 31 * result + count result = 31 * result + expectedArgumentType.hashCode()
result = 31 * result + (expectedArgumentType?.hashCode() ?: 0) result = 31 * result + fallbackArgumentType.hashCode()
result = 31 * result + (fallbackArgumentType?.hashCode() ?: 0)
return result return result
} }
public override fun clone(): CommandBuilder { public override fun clone(): CommandBuilder {
val result = CommandBuilder(currentCommandPartNode, ArrayDeque(commandParts), keyList.toMutableList(), count) val result = CommandBuilder(
currentCommandPartNode,
counts.toMutableList(),
keyList.toMutableList()
)
result.selectedRegister = selectedRegister
result.action = action
result.argument = argument
result.commandState = commandState result.commandState = commandState
result.fallbackArgumentType = fallbackArgumentType result.fallbackArgumentType = fallbackArgumentType
return result return result
} }
override fun toString(): String { override fun toString(): String {
return "Command state = $commandState, key list = ${ injector.parser.toKeyNotation(keyList) }, command parts = ${ commandParts }, count = $count\n" + return "Command state = $commandState, " +
"key list = ${ injector.parser.toKeyNotation(keyList) }, " +
"selected register = $selectedRegister, " +
"counts = $counts, " +
"action = $action, " +
"argument = $argument, " +
"command part node - $currentCommandPartNode" "command part node - $currentCommandPartNode"
} }

View File

@ -68,11 +68,6 @@ 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()
isMappingDisabledForKey(key, keyState) || || commandBuilder.isRegisterPending
injector.vimState.isRegisterPending || isMappingDisabledForKey(key, keyState)
) { ) {
log.debug("Finish key processing, returning false") log.debug("Mapping not applicable. 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.count > 0 val isMappingDisabled = key.keyChar == '0' && keyState.commandBuilder.hasCountCharacters()
log.debug { "Mapping disabled for key: $isMappingDisabled" } log.debug { "Mapping disabled for key: $isMappingDisabled" }
return isMappingDisabled return isMappingDisabled
} }

View File

@ -8,21 +8,53 @@
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
/** /**
* [count0] is a raw count entered by user. May be zero. * Represents arguments used when executing a command - either an action, operator or motion
* [count1] is the same count, but 1-based. If [count0] is zero, [count1] is one. *
* The terminology is taken directly from vim. * TODO: Remove, rename or otherwise refactor this class
* 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,
) { ) {
val count1: Int = count0.coerceAtLeast(1)
fun withCount0(count0: Int): OperatorArguments = this.copy(count0 = count0) /**
* Create a new instance of [OperatorArguments]
*
* @param count0 The 0-based count for the whole command
* @param mode Only used for the deprecated [OperatorArguments.mode] property
*/
@Suppress("DEPRECATION")
constructor(count0: Int, mode: Mode) : this(mode is Mode.OP_PENDING, count0, mode)
val count1: Int = count0.coerceAtLeast(1)
} }

View File

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

View File

@ -0,0 +1,35 @@
/*
* 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