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
buildType(TestingBuildType("Latest EAP", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(TestingBuildType("2024.1.1", "<default>"))
buildType(TestingBuildType("2024.2", "<default>"))
buildType(TestingBuildType("Latest EAP With Xorg", "<default>", version = "LATEST-EAP-SNAPSHOT"))
buildType(PropertyBased)

View File

@ -535,6 +535,10 @@ Contributors:
[![icon][github]](https://github.com/igorbabko)
&nbsp;
Igor Babko
* [![icon][mail]](mailto:533601+felixwiemuth@users.noreply.github.com)
[![icon][github]](https://github.com/felixwiemuth)
&nbsp;
Felix Wiemuth
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.
```
'clipboard' 'cb' Defines clipboard behavioue
'clipboard' 'cb' Defines clipboard behavior
A comma-separated list of words to control clipboard behaviour:
unnamed The clipboard register '*' is used instead of the
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
ideaType=IC
instrumentPluginCode=true
version=chylex-40
version=chylex-41
javaVersion=17
remoteRobotVersion=0.11.23
antlrVersion=4.10.1

View File

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

View File

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

View File

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

View File

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

View File

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

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

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 fun collectShortcuts(node: Node<NerdAction>): Set<KeyStroke> {
return if (node is CommandPartNode<NerdAction>) {
val res = node.keys.toMutableSet()
res += node.values.map { collectShortcuts(it) }.flatten()
val res = node.children.keys.toMutableSet()
res += node.children.values.map { collectShortcuts(it) }.flatten()
res
} else {
emptySet()

View File

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

View File

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

View File

@ -30,14 +30,12 @@ import com.maddyhome.idea.vim.state.mode.Mode;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.EnumSet;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putExtensionHandlerMapping;
import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMapping;
/**
* Port of vim-indent-object:
* https://github.com/michaeljsmith/vim-indent-object
* <a href="https://github.com/michaeljsmith/vim-indent-object">vim-indent-object</a>
*
* <p>
* 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>
*
* 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)
*/
@ -98,9 +96,8 @@ public class VimIndentObject implements VimExtension {
this.includeBelow = includeBelow;
}
@Nullable
@Override
public TextRange getRange(@NotNull VimEditor editor,
public @Nullable TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context,
int count,
@ -249,9 +246,8 @@ public class VimIndentObject implements VimExtension {
return new TextRange(upperBoundaryOffset, lowerBoundaryOffset);
}
@NotNull
@Override
public TextObjectVisualType getVisualType() {
public @NotNull TextObjectVisualType getVisualType() {
return TextObjectVisualType.LINE_WISE;
}
@ -264,15 +260,14 @@ public class VimIndentObject implements VimExtension {
@Override
public void execute(@NotNull VimEditor editor, @NotNull ExecutionContext context, @NotNull OperatorArguments operatorArguments) {
IjVimEditor vimEditor = (IjVimEditor)editor;
@NotNull KeyHandler keyHandler = KeyHandler.getInstance();
@NotNull KeyHandlerState keyHandlerState = KeyHandler.getInstance().getKeyHandlerState();
int count = Math.max(1, keyHandlerState.getCommandBuilder().getCount());
final IndentObjectHandler textObjectHandler = new IndentObjectHandler(includeAbove, includeBelow);
if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) {
if (!(editor.getMode() instanceof Mode.OP_PENDING)) {
int count0 = operatorArguments.getCount0();
((IjVimEditor)editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> {
final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, count, 0);
final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, Math.max(1, count0), count0);
if (range != null) {
try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) {
if (editor.getMode() instanceof Mode.VISUAL) {
@ -285,9 +280,7 @@ public class VimIndentObject implements VimExtension {
});
} else {
keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count,
textObjectHandler, Command.Type.MOTION,
EnumSet.noneOf(CommandFlags.class))));
keyHandlerState.getCommandBuilder().addAction(textObjectHandler);
}
}
}

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) {
if (caret != editor.primaryCaret()) {
(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.OperatorArguments
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.handler.ExternalActionHandler
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
import com.maddyhome.idea.vim.handler.MotionActionHandler
@ -193,21 +193,16 @@ internal class MotionGroup : VimMotionGroupBase() {
argument: Argument,
operatorArguments: OperatorArguments,
): TextRange? {
if (argument !is Argument.Motion) {
throw RuntimeException("Unexpected argument passed to getMotionRange2: $argument")
}
var start: Int
var end: Int
if (argument.type === Argument.Type.OFFSETS) {
val offsets = argument.offsets[caret.vim] ?: return null
val (first, second) = offsets.getNativeStartAndEnd()
start = first
end = second
} else {
val cmd = argument.motion
// Normalize the counts between the command and the motion argument
val cnt = cmd.count * operatorArguments.count1
val raw = if (operatorArguments.count0 == 0 && cmd.rawCount == 0) 0 else cnt
if (cmd.action is MotionActionHandler) {
val action = cmd.action as MotionActionHandler
val action = argument.motion
when (action) {
is MotionActionHandler -> {
// This is where we are now
start = caret.offset
@ -216,8 +211,8 @@ internal class MotionGroup : VimMotionGroupBase() {
editor.vim,
caret.vim,
IjEditorExecutionContext(context!!),
cmd.argument,
operatorArguments.withCount0(raw),
argument.argument,
operatorArguments
)
// Invalid motion
@ -233,22 +228,32 @@ internal class MotionGroup : VimMotionGroupBase() {
end++
}
}
} else if (cmd.action is TextObjectActionHandler) {
val action = cmd.action as TextObjectActionHandler
val range =
action.getRange(editor.vim, caret.vim, IjEditorExecutionContext(context!!), cnt, raw) ?: return null
}
is TextObjectActionHandler -> {
val range = action.getRange(
editor.vim,
caret.vim,
IjEditorExecutionContext(context!!),
operatorArguments.count1,
operatorArguments.count0
) ?: return null
start = range.startOffset
end = range.endOffset
if (cmd.isLinewiseMotion()) end--
} else {
throw RuntimeException(
"Commands doesn't take " + cmd.action.javaClass.simpleName + " as an operator",
)
if (argument.isLinewiseMotion()) end--
}
is ExternalActionHandler -> {
val range = action.getRange(caret.vim) ?: return null
start = range.startOffset
end = range.endOffset
}
else -> throw RuntimeException("Commands doesn't take " + action.javaClass.simpleName + " as an operator")
}
// This is a kludge for dw, dW, and d[w. Without this kludge, an extra newline is operated when it shouldn't be.
val id = argument.motion.action.id
val id = argument.motion.id
if (id == VimChangeGroupBase.VIM_MOTION_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_BIG_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_CAMEL_RIGHT) {
val text = editor.document.charsSequence.subSequence(start, end).toString()
val lastNewLine = text.lastIndexOf('\n')
@ -258,6 +263,7 @@ internal class MotionGroup : VimMotionGroupBase() {
}
}
}
return TextRange(start, end)
}

View File

@ -62,6 +62,7 @@ internal class IjActionExecutor : VimActionExecutor {
override val ACTION_EXPAND_REGION_RECURSIVELY: String
get() = IdeActions.ACTION_EXPAND_REGION_RECURSIVELY
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"
/**

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

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.group.visual.VisualChange
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.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
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.vimChangeActionSwitchMode: Mode? by userData()
internal var Editor.replaceMask: VimEditorReplaceMask? by userData()
internal var Caret.currentInsert: InsertSequence? by userData()
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.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.EditorListener
import com.maddyhome.idea.vim.helper.inInsertMode
import com.maddyhome.idea.vim.newapi.ij
@ -65,7 +64,7 @@ class IJEditorFocusListener : EditorListener {
val context: ExecutionContext = injector.executionContextManager.getEditorExecutionContext(editor)
val mode = injector.vimState.mode
when (mode) {
is Mode.INSERT -> editor.exitInsertMode(context, OperatorArguments(false, 0, mode))
is Mode.INSERT -> editor.exitInsertMode(context)
else -> {}
}
}
@ -79,3 +78,4 @@ class IJEditorFocusListener : EditorListener {
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.TemplateEditingAdapter
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.actions.NextVariableAction
import com.intellij.find.FindModelListener
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionUpdateThread
@ -152,6 +154,10 @@ internal object IdeaSpecifics {
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
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.getLineStartForOffset
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.FileGroup
import com.maddyhome.idea.vim.group.IjOptions
@ -395,7 +394,8 @@ internal object VimListenerManager {
editor.vim.mode = Mode.NORMAL()
KeyHandler.getInstance().reset(editor.vim)
}
injector.scroll.scrollCaretIntoView(editor.vim)
// Breaks relativenumber for some reason
// injector.scroll.scrollCaretIntoView(editor.vim)
}
MotionGroup.fileEditorManagerSelectionChangedCallback(event)

View File

@ -17,6 +17,24 @@ internal class IjLiveRange(val marker: RangeMarker) : LiveRange {
override val endOffset: Int
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

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.VirtualFile
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.IndentConfig
import com.maddyhome.idea.vim.common.IndentConfig.Companion.create
import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.common.TextRange
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.helper.EditorHelper
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.inExMode
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.vimLastSelectionType
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
val editor = editor.getTopLevelEditor()
val originalEditor = editor
override var replaceMask: VimEditorReplaceMask?
get() = editor.replaceMask
set(value) {
editor.replaceMask = value
}
override fun updateMode(mode: Mode) {
(injector.vimState as VimStateMachineImpl).mode = mode
@ -91,7 +97,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
editor.vimChangeActionSwitchMode = value
}
override val indentConfig: VimIndentConfig
get() = create(editor)
get() = IndentConfig.create(editor)
override fun fileSize(): Long = editor.fileSize.toLong()
@ -398,8 +404,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight))
}
override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) {
editor.exitInsertMode(context.ij, operatorArguments)
override fun exitInsertMode(context: ExecutionContext) {
editor.exitInsertMode(context.ij)
}
override fun exitSelectModeNative(adjustCaret: Boolean) {
@ -471,6 +477,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
get() = (editor as? EditorEx)?.isInsertMode ?: false
set(value) {
(editor as? EditorEx)?.isInsertMode = value
forgetAllReplaceMasks()
}
override val document: VimDocument

View File

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

View File

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

View File

@ -4,16 +4,6 @@
"class": "com.maddyhome.idea.vim.action.change.RepeatChangeAction",
"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>",
"class": "com.maddyhome.idea.vim.action.editor.VimEditorTab",

View File

@ -7,6 +7,7 @@
*/
package org.jetbrains.plugins.ideavim.action
import com.intellij.idea.TestFor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo
@ -1068,4 +1069,14 @@ foobaz
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)
@Test
fun testYankRegisterUsesLastEnteredRegister() {
typeTextInFile("\"a\"byl" + "\"ap", "hel<caret>lo world\n")
typeTextInFile("\"a\"byl" + "\"bp", "hel<caret>lo world\n")
assertState("helllo world\n")
}

View File

@ -85,4 +85,33 @@ class MotionBackspaceActionTest : VimTestCase() {
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")
}
}
@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
fun `test v count variable without count specified`() {
configureByText("\n")
enterCommand("nnoremap <expr> n ':echo ' .. v:count .. \"\\<CR>\"")
enterCommand("""nnoremap <expr> n ':echo ' .. v:count .. "\<CR>"""")
typeText("n")
assertExOutput("0")
}
@ -23,15 +23,31 @@ class VimVariableServiceTest : VimTestCase() {
@Test
fun `test v count variable`() {
configureByText("\n")
enterCommand("nnoremap <expr> n ':' .. \"\\<C-u>\" .. 'echo ' .. v:count .. \"\\<CR>\"")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count .. "\<CR>"""")
typeText("5n")
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
fun `test v count1 variable without count specified`() {
configureByText("\n")
enterCommand("nnoremap <expr> n ':echo ' .. v:count1 .. \"\\<CR>\"")
enterCommand("""nnoremap <expr> n ':echo ' .. v:count1 .. "\<CR>"""")
typeText("n")
assertExOutput("1")
}
@ -39,11 +55,27 @@ class VimVariableServiceTest : VimTestCase() {
@Test
fun `test v count1 variable`() {
configureByText("\n")
enterCommand("nnoremap <expr> n ':' .. \"\\<C-u>\" .. 'echo ' .. v:count1 .. \"\\<CR>\"")
enterCommand("""nnoremap <expr> n ':' .. "\<C-u>" .. 'echo ' .. v:count1 .. "\<CR>"""")
typeText("5n")
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
fun `test mapping with updating jumplist`() {
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
fun testSelectTwoArguments() {
doTest(

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 {
// The converted digraph character has been captured as an argument, push it back through key handler
val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character)
val argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState)
return true

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -52,7 +52,7 @@ sealed class PutTextBaseAction(
}
result
} 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 {
if (editor.isOneLineMode()) return false
injector.commandLine.createCommandPrompt(editor, context, cmd, initialText = "")
injector.commandLine.createCommandPrompt(editor, context, cmd.rawCount, initialText = "")
return true
}
}

View File

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

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

View File

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

View File

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

View File

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

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.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.NonShiftedSpecialKeyHandler
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() {
private fun doMotion(
editor: VimEditor,
caret: ImmutableVimCaret,
count1: Int,
whichwrapKey: String,
allowPastEnd: Boolean,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains(whichwrapKey)
return injector.motion.getHorizontalMotion(editor, caret, count1, allowPastEnd, allowWrap)
}
abstract class MotionNonShiftedArrowLeftBaseAction() : NonShiftedSpecialKeyHandler() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion(
@ -32,8 +43,38 @@ class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("<")
val allowEnd = operatorArguments.isOperatorPending // d<Left> deletes \n with wrap enabled
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap)
return doMotion(editor, caret, -operatorArguments.count1, "<", allowPastEnd)
}
protected open val allowPastEnd: Boolean = false
}
// Note that Select mode is handled in [SelectMotionArrowLeftAction]
@CommandOrMotion(keys = ["<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.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.NonShiftedSpecialKeyHandler
import com.maddyhome.idea.vim.helper.isEndAllowed
import com.maddyhome.idea.vim.helper.usesVirtualSpace
@CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionArrowRightAction : NonShiftedSpecialKeyHandler() {
private fun doMotion(
editor: VimEditor,
caret: ImmutableVimCaret,
count1: Int,
whichwrapKey: String,
allowPastEnd: Boolean,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains(whichwrapKey)
return injector.motion.getHorizontalMotion(editor, caret, count1, allowPastEnd, allowWrap)
}
abstract class MotionNonShiftedArrowRightBaseAction() : NonShiftedSpecialKeyHandler() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion(
@ -34,9 +45,38 @@ class MotionArrowRightAction : NonShiftedSpecialKeyHandler() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
val allowPastEnd = editor.usesVirtualSpace || editor.isEndAllowed ||
operatorArguments.isOperatorPending // because of `d<Right>` removing the last character
val allowWrap = injector.options(editor).whichwrap.contains(">")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd, allowWrap)
return doMotion(editor, caret, operatorArguments.count1, ">", allowPastEnd(editor))
}
protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed
}
// Note that Select mode is handled with [SelectMotionArrowRightAction]
@CommandOrMotion(keys = ["<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.MotionActionHandler
@CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionBackspaceAction : MotionActionHandler.ForEachCaret() {
@CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.NORMAL, Mode.VISUAL])
open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() {
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
@ -30,24 +30,15 @@ class MotionBackspaceAction : MotionActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("b")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd = false, allowWrap)
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap)
}
override val motionType: MotionType = MotionType.EXCLUSIVE
}
@CommandOrMotion(keys = ["<Space>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionSpaceAction : MotionActionHandler.ForEachCaret() {
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("s")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd = false, allowWrap)
}
override val motionType: MotionType = MotionType.EXCLUSIVE
}
// When the motion is used with an operator, the EOL character is counted.
// This allows e.g., `d<BS>` to delete the end of line character on the previous line when wrap is active
// ('whichwrap' contains "b")
// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators
@CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.OP_PENDING])
class MotionBackspaceOpPendingModeAction : MotionBackspaceAction(allowPastEnd = true)

View File

@ -27,13 +27,9 @@ import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.helper.isEndAllowed
import java.util.*
@CommandOrMotion(keys = ["<End>"], modes = [Mode.INSERT])
class MotionLastColumnInsertAction : MotionLastColumnAction() {
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_SAVE_STROKE)
}
abstract class MotionLastColumnBaseAction(private val isMotionForOperator: Boolean = false)
: MotionActionHandler.ForEachCaret() {
@CommandOrMotion(keys = ["$"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
open class MotionLastColumnAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.INCLUSIVE
override fun getOffset(
@ -43,13 +39,26 @@ open class MotionLastColumnAction : MotionActionHandler.ForEachCaret() {
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
val allow = if (editor.inVisualMode) {
val allowPastEnd = if (editor.inVisualMode) {
injector.options(editor).selection != "old"
} else {
if (operatorArguments.isOperatorPending) false else editor.isEndAllowed
// Don't allow past end if this motion is for an operator. I.e., for something like `d$`, we don't want to delete
// the end of line character
if (isMotionForOperator) false else editor.isEndAllowed
}
val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allow)
val offset = injector.motion.moveCaretToRelativeLineEnd(editor, caret, operatorArguments.count1 - 1, allowPastEnd)
return Motion.AdjustedOffset(offset, VimMotionGroupBase.LAST_COLUMN)
}
}
@CommandOrMotion(keys = ["$"], modes = [Mode.NORMAL, Mode.VISUAL])
open class MotionLastColumnAction : MotionLastColumnBaseAction()
@CommandOrMotion(keys = ["$"], modes = [Mode.OP_PENDING])
class MotionLastColumnOpPendingAction : MotionLastColumnBaseAction(isMotionForOperator = true)
@CommandOrMotion(keys = ["<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.MotionActionHandler
@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionLeftAction : MotionActionHandler.ForEachCaret() {
abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset(
@ -33,23 +32,16 @@ class MotionLeftAction : MotionActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("h")
val allowEnd = operatorArguments.isOperatorPending // dh deletes \n with wrap enabled
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap)
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap)
}
}
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.INSERT])
class MotionLeftInsertModeAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionLeftAction : MotionLeftBaseAction(allowPastEnd = false)
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("[")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, true, allowWrap)
}
}
// When the motion is used with an operator, the EOL character is counted.
// This allows e.g., `dh` to delete the end of line character on the previous line when wrap is active
// ('whichwrap' contains "h")
// See `:help whichwrap`. This says a delete or change operator, but it appears to apply to all operators
@CommandOrMotion(keys = ["h"], modes = [Mode.OP_PENDING])
class MotionLeftOpPendingModeAction : MotionLeftBaseAction(allowPastEnd = true)

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

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.handler.ShiftedArrowKeyHandler
/**
* @author Alex Plate
*/
@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 fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) {
val vertical = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true)
caret.moveToMotion(vertical)
val motion = injector.motion.getHorizontalMotion(editor, caret, -cmd.count, true)
caret.moveToMotion(motion)
}
override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) {
val caret = editor.currentCaret()
val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false)
caret.moveToMotion(newOffset)
val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, -cmd.count, false)
caret.moveToMotion(motion)
}
}

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.moveToMotion
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.ShiftedArrowKeyHandler
/**
* @author Alex Plate
*/
@CommandOrMotion(keys = ["<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 fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) {
val vertical = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true)
caret.moveToMotion(vertical)
val motion = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true)
caret.moveToMotion(motion)
}
override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) {
val caret = editor.currentCaret()
val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false)
if (newOffset is Motion.AbsoluteOffset) {
caret.moveToOffset(newOffset.offset)
}
val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false)
caret.moveToMotion(motion)
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.common.forgetAllReplaceMasks
import com.maddyhome.idea.vim.state.mode.Mode
abstract class VimEditorBase : VimEditor {
@ -18,6 +19,9 @@ abstract class VimEditorBase : VimEditor {
if (vimState.mode == value) return
val oldValue = vimState.mode
if (oldValue == Mode.REPLACE) {
forgetAllReplaceMasks()
}
updateMode(value)
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.command.MappingMode
import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.key.KeyMapping
import com.maddyhome.idea.vim.key.KeyMappingLayer
import com.maddyhome.idea.vim.key.MappingInfo
import com.maddyhome.idea.vim.key.MappingOwner
import com.maddyhome.idea.vim.key.RootNode
import com.maddyhome.idea.vim.key.ShortcutOwnerInfo
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
import javax.swing.KeyStroke
interface VimKeyGroup {
fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand>
fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand>
fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer
fun getActions(editor: VimEditor, 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.extension.ExtensionHandler
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.key.KeyMapping
import com.maddyhome.idea.vim.key.KeyMappingLayer
import com.maddyhome.idea.vim.key.MappingInfo
@ -30,7 +29,7 @@ abstract class VimKeyGroupBase : VimKeyGroup {
@JvmField
val myShortcutConflicts: MutableMap<KeyStroke, ShortcutOwnerInfo> = LinkedHashMap()
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)
override fun removeKeyMapping(modes: Set<MappingMode>, keys: List<KeyStroke>) {
@ -63,7 +62,7 @@ abstract class VimKeyGroupBase : VimKeyGroup {
* @param mappingMode The mapping mode
* @return The key mapping tree root
*/
override fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<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)

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

View File

@ -8,51 +8,88 @@
package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.ExternalActionHandler
import com.maddyhome.idea.vim.handler.MotionActionHandler
import java.util.*
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import com.maddyhome.idea.vim.state.mode.SelectionType
/**
* This represents a command argument.
* TODO please make it a sealed class and not a giant collection of fields with default values, it's not safe
* Represents an argument to a command's action
*
* A [Command] is made up of an optional register and count, and an action. That action might be a simple command such
* as `i` to start Insert mode, or a motion `w` to move to the next word. Or it might require an argument, such as a
* character like in the motion `fx` or an ex-string in the command `d/foo`. Or it might be another action, representing
* a motion, such as `dw`. That motion argument's action might itself have an action (`dfx`).
*/
class Argument private constructor(
val character: Char = 0.toChar(),
val motion: Command = EMPTY_COMMAND,
val offsets: Map<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)
sealed class Argument {
/** A simple character argument */
class Character(val character: Char) : Argument()
/** 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 {
MOTION, CHARACTER, DIGRAPH, EX_STRING, OFFSETS
}
companion object {
@JvmField
val EMPTY_COMMAND: Command = Command(
0,
object : MotionActionHandler.SingleExecution() {
override fun getOffset(
editor: VimEditor,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
) = Motion.NoMotion
/**
* A motion argument used to complete an operator, such as `dw` or `diw`
*
* A motion argument will often have its own argument, such as when deleting up to the next occurrence of a
* character, as in `dfx`.
*/
MOTION,
override val motionType: MotionType = MotionType.EXCLUSIVE
},
Command.Type.MOTION,
EnumSet.noneOf(CommandFlags::class.java),
)
/** A character argument, such as the character to move to with the `f` command. */
CHARACTER,
/**
* Used to represent an expected argument type rather than an actual argument type
*
* When building a command, an operator can say that it expects a digraph or literal argument, in which case the key
* handler will allow `<C-K>`, `<C-V>` and `<C-Q>`, and start the digraph state machine. The finished digraph is
* converted into a character, and a character argument is added to the operator action.
*/
DIGRAPH
}
}

View File

@ -8,34 +8,47 @@
package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import java.util.*
/**
* This represents a single Vim command to be executed (operator, motion, text object, etc.). It may optionally include
* an argument if appropriate for the command. The command has a count and a type.
* This represents a single Vim command to be executed (action, motion, operator+motion, v_textobject, etc.)
*
* A command is an action, with a type that determines how it is handled, such as [Type.MOTION], [Type.CHANGE],
* [Type.OTHER_SELF_SYNCHRONIZED], etc. It also exposes the action's [CommandFlags] which are also used to help execute
* the action.
*
* A command's action can require an argument, which can be either a character (e.g., `fx`) or the input from the Ex
* command line. It can also be a motion, in which case the command is an operator+motion, such as `dw`. The motion
* argument is an action that might also have an argument, such as `dfx` or `d/foo`.
*
* A command can optionally include a count and a register. More than one count can be entered, before an operator and
* then before the motion argument, e.g. `2d3w`. This is intuitively "delete the next three words, twice", which is the
* same as "delete the next six words". While both the operator and motion have a count while being built, the final
* command has a single count that is the product of all count components. In this example, the command would have a
* final count of `6`.
*
* Note that for a command that is an operator+motion command, the count applies to the motion, rather than the
* operator. For example, `3i` will insert the following typed text three times, while `3cw` will change the next three
* words with the following typed text, rather than changing the next word with the typed text three times. The command
* still has a single count, and to handle this, the operator action should ignore the count, while the motion action
* should use it when calculating the movement.
*
* As an additional interesting pathological edge case, it's possible to enter a count when selecting a register, and
* it's possible to select multiple registers while building a command; the last register wins. This means that
* `2"a3"b4"c5d6w` will delete 720 words and store the text in register `c`.
*
* @see OperatorArguments
*/
data class Command(
var rawCount: Int,
var action: EditorActionHandlerBase,
val register: Char?,
val rawCount: Int,
val action: EditorActionHandlerBase,
val argument: Argument?,
val type: Type,
var flags: EnumSet<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 {
action.process(this)
}
@ -43,20 +56,7 @@ data class Command(
val count: Int
get() = rawCount.coerceAtLeast(1)
var argument: Argument? = null
var register: Char? = null
fun isLinewiseMotion(): Boolean {
return when (action) {
is TextObjectActionHandler -> (action as TextObjectActionHandler).visualType == TextObjectVisualType.LINE_WISE
is MotionActionHandler -> (action as MotionActionHandler).motionType == MotionType.LINE_WISE
else -> error("Command is not a motion: $action")
}
}
override fun toString(): String {
return "Action = ${action.id}"
}
override fun toString() = "Action = ${action.id}"
enum class Type {
/**
@ -85,10 +85,6 @@ data class Command(
COPY,
PASTE,
/**
* Represents commands that select the register.
*/
SELECT_REGISTER,
OTHER_READONLY,
OTHER_WRITABLE,
@ -112,18 +108,3 @@ data class Command(
}
}
}
private object NonExecutableActionHandler : EditorActionHandlerBase(false) {
override val type: Command.Type
get() = error("This action should not be executed")
override fun baseExecute(
editor: VimEditor,
caret: VimCaret,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
error("This action should not be executed")
}
}

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.vimLogger
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.ExternalActionHandler
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.noneOfEnum
import com.maddyhome.idea.vim.key.CommandNode
import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.key.Node
import com.maddyhome.idea.vim.key.RootNode
import org.jetbrains.annotations.TestOnly
import javax.swing.KeyStroke
class CommandBuilder(
class CommandBuilder private constructor(
private var currentCommandPartNode: CommandPartNode<LazyVimCommand>,
private val commandParts: ArrayDeque<Command>,
private val counts: MutableList<Int>,
private val keyList: MutableList<KeyStroke>,
initialUncommittedRawCount: Int,
) : Cloneable {
constructor(
currentCommandPartNode: CommandPartNode<LazyVimCommand>,
initialUncommittedRawCount: Int = 0
) : this(
currentCommandPartNode,
ArrayDeque(),
mutableListOf(),
initialUncommittedRawCount
)
constructor(rootNode: RootNode<LazyVimCommand>, initialUncommittedRawCount: Int = 0)
: this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf())
var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND
private var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND
private var selectedRegister: Char? = null
private var action: EditorActionHandlerBase? = null
private var argument: Argument? = null
private var fallbackArgumentType: Argument.Type? = null
/**
* The current uncommitted count for the currently in-progress command part
*
* 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 val motionArgument
get() = argument as? Argument.Motion
/**
* The current aggregated, but uncommitted count for all command parts in the command builder, coerced to 1
*
* This value multiplies together the count for command parts currently committed, such as operator and multiple
* register selections, as well as the current uncommitted count for the next command part. E.g., `2"a3"b4"c5d6` will
* multiply each count together to get what would be the final count. All counts are coerced to at least 1 before
* multiplying, which means the result will also be at least 1.
*
* Note that there are very few uses for this value. The final value should be retrieved from [Command.count] or
* [Command.rawCount] after a call to [buildCommand]. This value is expected to be used for `'incsearch'`
* highlighting.
*/
val aggregatedUncommittedCount: Int
get() = (commandParts.map { it.count }.reduceOrNull { acc, i -> acc * i } ?: 1) * count.coerceAtLeast(1)
private var currentCount: Int
get() = counts.last()
set(value) {
counts[counts.size - 1] = value
}
/** Provide the typed keys for `'showcmd'` */
val keys: Iterable<KeyStroke> get() = keyList
/** Returns true if the command builder is clean and ready to start building */
val isEmpty
get() = commandState == CurrentCommandState.NEW_COMMAND
&& selectedRegister == null
&& counts.size == 1
&& action == null
&& argument == null
&& fallbackArgumentType == null
/** Returns true if the command is ready to be built and executed */
val isReady
get() = commandState == CurrentCommandState.READY
/**
* Returns the current total count, as the product of all entered count components. The value is not coerced.
*
* 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
// 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
@ -84,7 +96,18 @@ class CommandBuilder(
// still change, if more keys are processed. E.g., it's perfectly valid to select register multiple times `"a"b`.
// This doesn't cause any issues with existing extensions
val register: Char?
get() = commandParts.lastOrNull { it.register != null }?.register
get() = selectedRegister
// TODO: Try to remove this too. Also used by extension handling
fun hasCurrentCommandPartArgument() = motionArgument != null || argument != null
// TODO: And remove this too. More extension special case code
// It's used by the Matchit extension to incorrectly reset the command builder. Extensions need a way to properly
// handle the command builder. I.e., they should act like expression mappings, which return keys to evaluate, or an
// empty string to leave state as it is - either way, it's an explicit choice. Currently, extensions mostly ignore it
fun resetCount() {
counts[counts.size - 1] = 0
}
/**
* The argument type for the current in-progress command part's action
@ -92,79 +115,202 @@ class CommandBuilder(
* For digraph arguments, this can fall back to [Argument.Type.CHARACTER] if there isn't a digraph match.
*/
val expectedArgumentType: Argument.Type?
get() = fallbackArgumentType ?: commandParts.lastOrNull()?.action?.argumentType
get() = fallbackArgumentType
?: motionArgument?.let { return it.motion.argumentType }
?: action?.argumentType
private var fallbackArgumentType: Argument.Type? = null
val isReady: Boolean get() = commandState == CurrentCommandState.READY
val isEmpty: Boolean get() = commandParts.isEmpty()
val isAtDefaultState: Boolean get() = isEmpty && count == 0 && expectedArgumentType == null
val isExpectingCount: Boolean
get() {
return commandState == CurrentCommandState.NEW_COMMAND &&
expectedArgumentType != Argument.Type.CHARACTER &&
expectedArgumentType != Argument.Type.DIGRAPH
}
fun pushCommandPart(action: EditorActionHandlerBase) {
logger.trace { "pushCommandPart is executed. action = $action" }
commandParts.add(Command(count, action, action.type, action.flags))
fallbackArgumentType = null
count = 0
}
fun pushCommandPart(register: Char) {
logger.trace { "pushCommandPart is executed. register = $register" }
// We will never execute this command, but we need to push something to correctly handle counts on either side of a
// select register command part. e.g. 2"a2d2w or even crazier 2"a2"a2"a2"a2"a2d2w
commandParts.add(Command(count, register))
fallbackArgumentType = null
count = 0
}
/**
* Returns true if the command builder is waiting for an argument
*
* The command builder might be waiting for the argument to a simple motion action such as `f`, waiting for a
* character to move to, or it might be waiting for the argument to a motion that is itself an argument to an operator
* argument. For example, the character argument to `f` in `df{character}`.
*/
val isAwaitingArgument: Boolean
get() = expectedArgumentType != null && (motionArgument?.let { it.argument == null } ?: (argument == null))
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,
// in which case try to handle input as a character argument.
assert(expectedArgumentType == Argument.Type.DIGRAPH) { "Cannot move state from $expectedArgumentType to CHARACTER" }
fallbackArgumentType = Argument.Type.CHARACTER
}
fun addKey(key: KeyStroke) {
logger.trace { "added key to command builder" }
keyList.add(key)
fun isAwaitingCharOrDigraphArgument(): Boolean {
val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH
logger.debug { "Awaiting char or digraph: $awaiting" }
return awaiting
}
val isExpectingCount: Boolean
get() {
return commandState == CurrentCommandState.NEW_COMMAND &&
!isRegisterPending &&
expectedArgumentType != Argument.Type.CHARACTER &&
expectedArgumentType != Argument.Type.DIGRAPH
}
/**
* 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) {
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,
// so will flip at 2147483648. We store count as an Int, which is also 32 bit.
// See https://github.com/vim/vim/blob/b376ace1aeaa7614debc725487d75c8f756dd773/src/normal.c#L631
if (count < 0) {
count = 999999999
if (currentCount < 0) {
currentCount = 999999999
}
addKey(key)
}
fun deleteCountCharacter() {
count /= 10
currentCount /= 10
keyList.removeAt(keyList.size - 1)
}
fun setCurrentCommandPartNode(newNode: CommandPartNode<LazyVimCommand>) {
logger.trace { "setCurrentCommandPartNode is executed" }
currentCommandPartNode = newNode
var isRegisterPending: Boolean = false
private set
fun startWaitingForRegister(key: KeyStroke) {
isRegisterPending = true
addKey(key)
}
fun getChildNode(key: KeyStroke): Node<LazyVimCommand>? {
return currentCommandPartNode[key]
fun selectRegister(register: Char) {
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
logger.debug { "Awaiting char or digraph: $awaiting" }
return awaiting
/**
* Adds a keystroke to the command builder
*
* Only public use is when entering a digraph/literal, where each key isn't handled by [CommandBuilder], but should
* be added to the `'showcmd'` output.
*/
fun addKey(key: KeyStroke) {
logger.trace { "added key to command builder: $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 {
@ -178,67 +324,47 @@ class CommandBuilder(
return isMultikey
}
fun isDone(): Boolean {
return commandParts.isEmpty()
}
fun completeCommandPart(argument: Argument) {
logger.trace { "completeCommandPart is executed" }
commandParts.last().argument = argument
commandState = CurrentCommandState.READY
}
fun isDuplicateOperatorKeyStroke(key: KeyStroke): Boolean {
logger.trace { "entered isDuplicateOperatorKeyStroke" }
val action = commandParts.last().action as? DuplicableOperatorAction
logger.trace { "action = $action" }
return action?.duplicateWith == key.keyChar
}
fun hasCurrentCommandPartArgument(): Boolean {
return commandParts.lastOrNull()?.argument != null
}
/**
* Build the command with the current counts, register, actions and arguments
*
* The command builder is reset after the command is built.
*/
fun buildCommand(): Command {
var command: Command = commandParts.removeFirst()
while (commandParts.size > 0) {
val next = commandParts.removeFirst()
next.rawCount = if (command.rawCount == 0 && next.rawCount == 0) 0 else command.count * next.count
command.rawCount = 0
if (command.type == Command.Type.SELECT_REGISTER) {
next.register = command.register
command.register = null
command = next
} else {
command.argument = Argument(next)
assert(commandParts.size == 0)
}
}
fallbackArgumentType = null
val rawCount = calculateCount0Snapshot()
val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum())
resetAll(currentCommandPartNode.root as RootNode<LazyVimCommand>)
return command
}
fun resetAll(commandPartNode: CommandPartNode<LazyVimCommand>) {
logger.trace { "resetAll is executed" }
resetInProgressCommandPart(commandPartNode)
fun resetAll(rootNode: RootNode<LazyVimCommand>) {
logger.trace("resetAll is executed")
currentCommandPartNode = rootNode
commandState = CurrentCommandState.NEW_COMMAND
commandParts.clear()
counts.clear()
counts.add(0)
isRegisterPending = false
selectedRegister = null
action = null
argument = null
keyList.clear()
fallbackArgumentType = null
}
fun resetCount() {
count = 0
}
fun resetInProgressCommandPart(commandPartNode: CommandPartNode<LazyVimCommand>) {
logger.trace { "resetInProgressCommandPart is executed" }
count = 0
setCurrentCommandPartNode(commandPartNode)
/**
* Change the command trie root node used to find commands for the current mode
*
* Typically, we reset the command trie root node after a command is executed, using the root node of the current
* mode - this is handled by [resetAll]. This function allows us to change the root node without executing a command
* or fully resetting the command builder, such as when switching to Op-pending while entering an operator+motion.
*/
fun resetCommandTrieRootNode(rootNode: RootNode<LazyVimCommand>) {
logger.trace("resetCommandTrieRootNode is executed")
currentCommandPartNode = rootNode
}
@TestOnly
fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -246,10 +372,12 @@ class CommandBuilder(
other as CommandBuilder
if (currentCommandPartNode != other.currentCommandPartNode) return false
if (commandParts != other.commandParts) return false
if (counts != other.counts) return false
if (selectedRegister != other.selectedRegister) return false
if (action != other.action) return false
if (argument != other.argument) return false
if (keyList != other.keyList) return false
if (commandState != other.commandState) return false
if (count != other.count) return false
if (expectedArgumentType != other.expectedArgumentType) return false
if (fallbackArgumentType != other.fallbackArgumentType) return false
@ -258,24 +386,38 @@ class CommandBuilder(
override fun hashCode(): Int {
var result = currentCommandPartNode.hashCode()
result = 31 * result + commandParts.hashCode()
result = 31 * result + counts.hashCode()
result = 31 * result + selectedRegister.hashCode()
result = 31 * result + action.hashCode()
result = 31 * result + argument.hashCode()
result = 31 * result + keyList.hashCode()
result = 31 * result + commandState.hashCode()
result = 31 * result + count
result = 31 * result + (expectedArgumentType?.hashCode() ?: 0)
result = 31 * result + (fallbackArgumentType?.hashCode() ?: 0)
result = 31 * result + expectedArgumentType.hashCode()
result = 31 * result + fallbackArgumentType.hashCode()
return result
}
public override fun clone(): CommandBuilder {
val result = CommandBuilder(currentCommandPartNode, ArrayDeque(commandParts), keyList.toMutableList(), count)
val result = CommandBuilder(
currentCommandPartNode,
counts.toMutableList(),
keyList.toMutableList()
)
result.selectedRegister = selectedRegister
result.action = action
result.argument = argument
result.commandState = commandState
result.fallbackArgumentType = fallbackArgumentType
return result
}
override fun toString(): String {
return "Command state = $commandState, key list = ${ injector.parser.toKeyNotation(keyList) }, command parts = ${ commandParts }, count = $count\n" +
return "Command state = $commandState, " +
"key list = ${ injector.parser.toKeyNotation(keyList) }, " +
"selected register = $selectedRegister, " +
"counts = $counts, " +
"action = $action, " +
"argument = $argument, " +
"command part node - $currentCommandPartNode"
}

View File

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

View File

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

View File

@ -8,21 +8,53 @@
package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.state.mode.Mode
/**
* [count0] is a raw count entered by user. May be zero.
* [count1] is the same count, but 1-based. If [count0] is zero, [count1] is one.
* The terminology is taken directly from vim.
* If no count is provided, [count0] defaults to zero.
* Represents arguments used when executing a command - either an action, operator or motion
*
* TODO: Remove, rename or otherwise refactor this class
*
* Problems with this class:
* * The name is misleading, as it is used when executing motions that do not have an operator, as well as when
* executing the operator itself. Or even when executing actions that are neither operators nor motions
* * It is not clear if this represents the arguments to an operator (while the operator's [Command] also has arguments)
* * [mode] is the mode _before_ the command is completed, which is not guaranteed to be the same as the mode once the
* command completes. There is no indication of this difference, which could lead to confusion
* * The count is (correctly) the count for the whole command, rather than the operator, or the operator's arguments
* (the in-progress motion)
*
* @param isOperatorPending Deprecated. The value is used to indicate that a command is operator+motion and was
* previously used to change the behaviour of the motion (the EOL character is counted in this scenario - see
* `:help whichwrap`). It is better to register a separate action for [Mode.OP_PENDING] rather than expect a runtime
* flag for something that can be handled statically.
* @param count0 The raw count of the entire command. E.g., if the command is `2d3w`, then this count will be `6`, even
* when this class is passed to the `d` operator action (the count applies to the motion).
* @param mode Deprecated. The mode of the editor at the time that the [OperatorArguments] is created, which is _before_
* the command is completed. This was previously used to check for [Mode.OP_PENDING], but is no longer required. Prefer
* [VimEditor.mode] instead.
*/
data class OperatorArguments(
val isOperatorPending: Boolean,
data class OperatorArguments
@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 mode: Mode,
@Deprecated("Represents the mode when the OperatorArguments was created, not the current mode. Prefer editor.mode") val mode: Mode,
) {
val count1: Int = count0.coerceAtLeast(1)
fun withCount0(count0: Int): OperatorArguments = this.copy(count0 = count0)
/**
* Create a new instance of [OperatorArguments]
*
* @param count0 The 0-based count for the whole command
* @param mode Only used for the deprecated [OperatorArguments.mode] property
*/
@Suppress("DEPRECATION")
constructor(count0: Int, mode: Mode) : this(mode is Mode.OP_PENDING, count0, mode)
val count1: Int = count0.coerceAtLeast(1)
}

View File

@ -11,5 +11,4 @@ package com.maddyhome.idea.vim.common
enum class CurrentCommandState {
NEW_COMMAND,
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