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

Compare commits

..

23 Commits

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

View File

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

View File

@ -8,10 +8,6 @@
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
@ -44,18 +40,6 @@ 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, motionType: SelectionType): Boolean {
private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textRange: TextRange, selectionType: 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 (_: ExException) {
} catch (ex: ExException) {
// Get the argument for function('...') or funcref('...') for the error message
val functionName = if (expression is FunctionCallExpression && expression.arguments.isNotEmpty()) {
val functionName = if (expression is FunctionCallExpression && expression.arguments.size > 0) {
expression.arguments[0].evaluate(editor, context, scriptContext).toString()
}
else {
@ -77,7 +77,7 @@ private fun doOperatorAction(editor: VimEditor, context: ExecutionContext, textR
return false
}
val arg = when (motionType) {
val arg = when (selectionType) {
SelectionType.LINE_WISE -> "line"
SelectionType.CHARACTER_WISE -> "char"
SelectionType.BLOCK_WISE -> "block"
@ -101,13 +101,19 @@ 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 as? Argument.Motion ?: return false
val argument = cmd.argument ?: return false
if (!editor.inRepeatMode) {
argumentCaptured = argument
}
val range = getMotionRange(editor, context, argument, operatorArguments)
if (range != null) {
return doOperatorAction(editor, context, range, argument.getMotionType())
val selectionType = if (argument.motion.isLinewiseMotion()) {
SelectionType.LINE_WISE
} else {
SelectionType.CHARACTER_WISE
}
return doOperatorAction(editor, context, range, selectionType)
}
return false
}
@ -115,7 +121,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
private fun getMotionRange(
editor: VimEditor,
context: ExecutionContext,
argument: Argument.Motion,
argument: Argument,
operatorArguments: OperatorArguments,
): TextRange? {
// Note that we're using getMotionRange2 in order to avoid normalising the linewise range into line start
@ -130,7 +136,7 @@ internal class OperatorAction : VimActionHandler.SingleExecution() {
operatorArguments,
)?.normalize()?.let {
// If we're linewise, make sure the end offset isn't just the EOL char
if (argument.getMotionType() == SelectionType.LINE_WISE && it.endOffset < editor.fileSize()) {
if (argument.motion.isLinewiseMotion() && it.endOffset < editor.fileSize()) {
TextRange(it.startOffset, it.endOffset + 1)
} 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
var lastCommand = VimRepeater.lastChangeCommand
val lastCommand = VimRepeater.lastChangeCommand
if (lastCommand == null && Extension.lastExtensionHandler == null) return false
@ -57,7 +57,12 @@ internal class RepeatChangeAction : VimActionHandler.SingleExecution() {
)
} else if (!repeatHandler && lastCommand != null) {
if (cmd.rawCount > 0) {
lastCommand = lastCommand.copy(rawCount = cmd.rawCount)
lastCommand.rawCount = cmd.count
val arg = lastCommand.argument
if (arg != null) {
val mot = arg.motion
mot.rawCount = 0
}
}
state.executingCommand = lastCommand

View File

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

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)) {
if (!injector.changeGroup.deleteJoinLines(editor, caret, operatorArguments.count1, true, operatorArguments)) {
res = false
}
}

View File

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

View File

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

View File

@ -44,7 +44,6 @@ import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import java.util.*
import java.util.regex.Pattern
@ -94,29 +93,34 @@ 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.
if (editor.mode is Mode.OP_PENDING) {
val isInOpPending = keyHandler.isOperatorPending(editor.mode, keyState)
if (isInOpPending) {
val matchitAction = MatchitAction()
matchitAction.reverse = reverse
matchitAction.isInOpPending = true
keyState.commandBuilder.addAction(matchitAction)
keyState.commandBuilder.completeCommandPart(
Argument(
Command(
count,
matchitAction,
Command.Type.MOTION,
EnumSet.noneOf(CommandFlags::class.java),
),
),
)
} else {
editor.sortedCarets().forEach { caret ->
injector.jumpService.saveJumpLocation(editor)
caret.moveToOffset(
getMatchitOffset(
editor.ij,
caret.ij,
operatorArguments.count0,
isInOpPending = false,
reverse
))
caret.moveToOffset(getMatchitOffset(editor.ij, caret.ij, count, isInOpPending, reverse))
}
}
}
@ -350,7 +354,7 @@ private object FileTypePatterns {
private val DEFAULT_PAIRS = setOf('(', ')', '[', ']', '{', '}')
private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPending: Boolean, reverse: Boolean): Int {
private fun getMatchitOffset(editor: Editor, caret: Caret, count: Int, isInOpPending: Boolean, reverse: Boolean): Int {
val virtualFile = EditorHelper.getVirtualFile(editor)
var caretOffset = caret.offset
@ -363,9 +367,9 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPe
val currentChar = editor.document.charsSequence[caretOffset]
var motionOffset: Int? = null
if (count0 > 0) {
if (count > 0) {
// Matchit doesn't affect the percent motion, so we fall back to the default behavior.
motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count0)
motionOffset = VimPlugin.getMotion().moveCaretToLinePercent(editor.vim, caret.vim, count)
} else {
// Check the simplest case first.
if (DEFAULT_PAIRS.contains(currentChar)) {
@ -396,7 +400,8 @@ private fun getMatchitOffset(editor: Editor, caret: Caret, count0: Int, isInOpPe
private fun getMotionOffset(motion: Motion): Int? {
return when (motion) {
is Motion.AdjustedOffset, is Motion.AbsoluteOffset -> motion.offset
is Motion.AbsoluteOffset -> motion.offset
is Motion.AdjustedOffset -> motion.offset
is Motion.Error, is Motion.NoMotion -> null
}
}

View File

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

View File

@ -10,6 +10,7 @@ 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
@ -165,11 +166,17 @@ 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,12 +29,14 @@ 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:
* <a href="https://github.com/kana/vim-textobj-entire">vim-textobj-entire</a>
* https://github.com/kana/vim-textobj-entire
*
* <p>
* vim-textobj-entire provides two text objects:
@ -49,7 +51,7 @@ import static com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingI
* </ul>
*
* See also the reference manual for more details at:
* <a href="https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt">text-obj-entire.txt</a>
* https://github.com/kana/vim-textobj-entire/blob/master/doc/textobj-entire.txt
*
* @author Alexandre Grison (@agrison)
*/
@ -92,8 +94,9 @@ public class VimTextObjEntireExtension implements VimExtension {
this.ignoreLeadingAndTrailing = ignoreLeadingAndTrailing;
}
@Nullable
@Override
public @Nullable TextRange getRange(@NotNull VimEditor editor,
public TextRange getRange(@NotNull VimEditor editor,
@NotNull ImmutableVimCaret caret,
@NotNull ExecutionContext context,
int count,
@ -122,22 +125,24 @@ public class VimTextObjEntireExtension implements VimExtension {
return new TextRange(start, end);
}
@NotNull
@Override
public @NotNull TextObjectVisualType getVisualType() {
public 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 (!(editor.getMode() instanceof Mode.OP_PENDING)) {
int count0 = operatorArguments.getCount0();
if (!keyHandler.isOperatorPending(editor.getMode(), keyHandlerState)) {
((IjVimEditor) editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> {
final TextRange range = textObjectHandler.getRange(editor, new IjVimCaret(caret), context, Math.max(1, count0), count0);
final TextRange range = textObjectHandler.getRange(editor, new IjVimCaret(caret), context, count, 0);
if (range != null) {
try (VimListenerSuppressor.Locked ignored = SelectionVimListenerSuppressor.INSTANCE.lock()) {
if (editor.getMode() instanceof Mode.VISUAL) {
@ -150,7 +155,9 @@ public class VimTextObjEntireExtension implements VimExtension {
});
} else {
keyHandlerState.getCommandBuilder().addAction(textObjectHandler);
keyHandlerState.getCommandBuilder().completeCommandPart(new Argument(new Command(count,
textObjectHandler, Command.Type.MOTION,
EnumSet.noneOf(CommandFlags.class))));
}
}
}

View File

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

View File

@ -103,11 +103,6 @@ class ChangeGroup : VimChangeGroupBase() {
}
}
override fun processBackspace(editor: VimEditor, context: ExecutionContext) {
injector.actionExecutor.executeAction(editor, name = IdeActions.ACTION_EDITOR_BACKSPACE, context = context)
injector.scroll.scrollCaretIntoView(editor)
}
private fun restoreCursor(editor: VimEditor, caret: VimCaret, startLine: Int) {
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.handler.ExternalActionHandler
import com.maddyhome.idea.vim.ex.ExOutputModel
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.Motion.AbsoluteOffset
import com.maddyhome.idea.vim.handler.MotionActionHandler
@ -193,16 +193,21 @@ 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
@ -211,8 +216,8 @@ internal class MotionGroup : VimMotionGroupBase() {
editor.vim,
caret.vim,
IjEditorExecutionContext(context!!),
argument.argument,
operatorArguments
cmd.argument,
operatorArguments.withCount0(raw),
)
// Invalid motion
@ -228,32 +233,22 @@ internal class MotionGroup : VimMotionGroupBase() {
end++
}
}
}
is TextObjectActionHandler -> {
val range = action.getRange(
editor.vim,
caret.vim,
IjEditorExecutionContext(context!!),
operatorArguments.count1,
operatorArguments.count0
) ?: return null
} 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
start = range.startOffset
end = range.endOffset
if (argument.isLinewiseMotion()) end--
if (cmd.isLinewiseMotion()) end--
} else {
throw RuntimeException(
"Commands doesn't take " + cmd.action.javaClass.simpleName + " as an operator",
)
}
is ExternalActionHandler -> {
val range = action.getRange(caret.vim) ?: return null
start = range.startOffset
end = range.endOffset
}
else -> throw RuntimeException("Commands doesn't take " + action.javaClass.simpleName + " as an operator")
}
// This is a kludge for dw, dW, and d[w. Without this kludge, an extra newline is operated when it shouldn't be.
val id = argument.motion.id
val id = argument.motion.action.id
if (id == VimChangeGroupBase.VIM_MOTION_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_BIG_WORD_RIGHT || id == VimChangeGroupBase.VIM_MOTION_CAMEL_RIGHT) {
val text = editor.document.charsSequence.subSequence(start, end).toString()
val lastNewLine = text.lastIndexOf('\n')
@ -263,7 +258,6 @@ internal class MotionGroup : VimMotionGroupBase() {
}
}
}
return TextRange(start, end)
}

View File

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

View File

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

View File

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

View File

@ -17,24 +17,6 @@ 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,7 +53,6 @@ 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
@ -76,11 +75,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
// TBH, I don't like the names. Need to think a bit more about this
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
@ -97,7 +91,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
editor.vimChangeActionSwitchMode = value
}
override val indentConfig: VimIndentConfig
get() = IndentConfig.create(editor)
get() = create(editor)
override fun fileSize(): Long = editor.fileSize.toLong()
@ -404,8 +398,8 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase(
return editor.visualPositionToOffset(VisualPosition(position.line, position.column, position.leansRight))
}
override fun exitInsertMode(context: ExecutionContext) {
editor.exitInsertMode(context.ij)
override fun exitInsertMode(context: ExecutionContext, operatorArguments: OperatorArguments) {
editor.exitInsertMode(context.ij, operatorArguments)
}
override fun exitSelectModeNative(adjustCaret: Boolean) {
@ -477,7 +471,6 @@ 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,13 +192,6 @@ 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 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());
// 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();
if ((labelText.equals("/") || labelText.equals("?") || searchCommand) && !injector.getMacro().isExecutingMacro()) {
final boolean forwards = !labelText.equals("?"); // :s, :g, :v are treated as forwards
@ -531,8 +531,9 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
private static final Logger logger = Logger.getInstance(ExEntryPanel.class.getName());
@NotNull
@Override
public @NotNull VimCommandLineCaret getCaret() {
public VimCommandLineCaret getCaret() {
return (VimCommandLineCaret) entry.getCaret();
}
@ -550,8 +551,9 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
entry.clearCurrentAction();
}
@Nullable
@Override
public @Nullable Integer getPromptCharacterOffset() {
public Integer getPromptCharacterOffset() {
int offset = entry.currentActionPromptCharacterOffset;
return offset == -1 ? null : offset;
}
@ -571,7 +573,8 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
IdeFocusManager.findInstance().requestFocus(entry, true);
}
public @Nullable VimInputInterceptor<?> getInputInterceptor() {
@Nullable
public VimInputInterceptor<?> getInputInterceptor() {
return myInputInterceptor;
}
@ -584,13 +587,15 @@ public class ExEntryPanel extends JPanel implements VimCommandLine {
myInputInterceptor = vimInputInterceptor;
}
@Nullable
@Override
public @Nullable Function1<String, Unit> getInputProcessing() {
public Function1<String, Unit> getInputProcessing() {
return inputProcessing;
}
@Nullable
@Override
public @Nullable Character getFinishOn() {
public Character getFinishOn() {
return finishOn;
}

View File

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

View File

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

View File

@ -85,33 +85,4 @@ 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,35 +85,4 @@ 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,31 +23,15 @@ 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")
}
@ -55,27 +39,11 @@ 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,16 +141,6 @@ Mode.INSERT,
)
}
@Test
fun testDeleteWithMultipleCounts() {
doTest(
"2d2aa",
"function(int <caret>arg1, char* arg<caret>2=\"a,b,c(d,e)\", bool arg3, string arg4, int arg5)",
"function(<caret>)",
Mode.NORMAL(),
)
}
@Test
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.children.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) }
val possibleKeys = (currentNode.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,13 +17,14 @@ 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
@ -196,9 +197,11 @@ 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)
}
@ -207,6 +210,14 @@ 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,
@ -215,7 +226,11 @@ class KeyHandler {
) {
logger.trace("Command execution")
val command = keyState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(command.rawCount, editorState.mode)
val operatorArguments = OperatorArguments(
editor.mode is Mode.OP_PENDING,
command.rawCount,
editorState.mode,
)
// If we were in "operator pending" mode, reset back to normal mode.
// But opening command line should not reset operator pending mode (e.g. `d/foo`
@ -280,7 +295,7 @@ class KeyHandler {
keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode()))
}
private fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> {
private fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> {
return injector.keyGroup.getKeyRoot(mappingMode)
}
@ -326,7 +341,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)
@ -346,15 +361,22 @@ 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"
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 -> {}
}
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
}
instance.reset(keyState, editorState.mode)
ReturnTo.REPLACE -> {
editor.mode = Mode.REPLACE
}
}
}
if (keyState.commandBuilder.isDone()) {
getInstance().reset(keyState, editorState.mode)
}
}
}

View File

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

View File

@ -37,11 +37,12 @@ class ChangeLineAction : ChangeInInsertSequenceAction() {
): Boolean {
// `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.Motion(motion, null),
Argument(command),
operatorArguments,
)
}

View File

@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.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])
@ -40,5 +39,4 @@ 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,6 +39,7 @@ class ChangeVisualAction : VisualOperatorActionHandler.ForEachCaret() {
range.toVimTextRange(false),
range.type,
context,
operatorArguments,
)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,40 +0,0 @@
/*
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.action.change.insert
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.getLineStartForOffset
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
@CommandOrMotion(keys = ["<C-H>", "<BS>"], modes = [Mode.INSERT])
internal class InsertBackspaceAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_WRITABLE
override fun execute( editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments, ): Boolean {
if (editor.insertMode) {
injector.changeGroup.processBackspace(editor, context)
} else {
for (caret in editor.carets()) {
val offset = (caret.offset - 1).takeIf { it > 0 } ?: continue
val oldChar = editor.replaceMask?.popChange(editor, offset)
if (oldChar != null) {
injector.changeGroup.replaceText(editor, caret, offset, offset + 1, oldChar.toString())
}
caret.moveToOffset(offset)
}
}
return true
}
}

View File

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

View File

@ -52,8 +52,7 @@ 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 argument = cmd.argument as? Argument.Character ?: return false
val keyStroke = KeyStroke.getKeyStroke(argument.character)
val keyStroke = KeyStroke.getKeyStroke(cmd.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)
return insertDeleteInsertedText(editor, caret, operatorArguments)
}
}
@ -48,7 +48,11 @@ 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): Boolean {
private fun insertDeleteInsertedText(
editor: VimEditor,
caret: VimCaret,
operatorArguments: OperatorArguments,
): Boolean {
var deleteTo = caret.vimInsertStart.startOffset
val offset = caret.offset
if (offset == deleteTo) {
@ -61,6 +65,7 @@ private fun insertDeleteInsertedText(editor: VimEditor, caret: VimCaret): Boolea
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)
return insertDeletePreviousWord(editor, caret, operatorArguments)
}
}
@ -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): Boolean {
private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret, operatorArguments: OperatorArguments): Boolean {
val deleteTo: Int = if (caret.getBufferPosition().column == 0) {
caret.offset - 1
} else {
@ -74,6 +74,6 @@ private fun insertDeletePreviousWord(editor: VimEditor, caret: VimCaret): Boolea
return false
}
val range = TextRange(deleteTo, caret.offset)
injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true)
injector.changeGroup.deleteRange(editor, caret, range, SelectionType.CHARACTER_WISE, true, operatorArguments)
return true
}

View File

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.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
@ -28,8 +27,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 as? Argument.ExString ?: return true
val historyType = VimHistory.Type.getTypeByLabel(argument.label.toString())
val argument = cmd.argument ?: return true
val historyType = VimHistory.Type.getTypeByLabel(argument.character.toString())
injector.historyGroup.addEntry(historyType, argument.string)
return true
}

View File

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

View File

@ -19,21 +19,10 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.handler.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() {
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionArrowLeftAction : NonShiftedSpecialKeyHandler() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion(
@ -43,38 +32,8 @@ abstract class MotionNonShiftedArrowLeftBaseAction() : NonShiftedSpecialKeyHandl
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
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)
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)
}
}

View File

@ -19,23 +19,12 @@ import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.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
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() {
@CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionArrowRightAction : NonShiftedSpecialKeyHandler() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun motion(
@ -45,38 +34,9 @@ abstract class MotionNonShiftedArrowRightBaseAction() : NonShiftedSpecialKeyHand
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
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)
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)
}
}

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])
open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() {
@CommandOrMotion(keys = ["<BS>", "<C-H>"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionBackspaceAction : MotionActionHandler.ForEachCaret() {
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
@ -30,15 +30,24 @@ open class MotionBackspaceAction(private val allowPastEnd: Boolean = false) : Mo
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("b")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap)
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd = false, allowWrap)
}
override val motionType: MotionType = MotionType.EXCLUSIVE
}
// 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)
@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
}

View File

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

View File

@ -21,7 +21,8 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler
abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionActionHandler.ForEachCaret() {
@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionLeftAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset(
@ -32,16 +33,23 @@ abstract class MotionLeftBaseAction(private val allowPastEnd: Boolean) : MotionA
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("h")
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowPastEnd, allowWrap)
val allowEnd = operatorArguments.isOperatorPending // dh deletes \n with wrap enabled
return injector.motion.getHorizontalMotion(editor, caret, -operatorArguments.count1, allowEnd, allowWrap)
}
}
@CommandOrMotion(keys = ["h"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionLeftAction : MotionLeftBaseAction(allowPastEnd = false)
@CommandOrMotion(keys = ["<Left>", "<kLeft>"], modes = [Mode.INSERT])
class MotionLeftInsertModeAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
// 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)
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)
}
}

View File

@ -23,7 +23,8 @@ import com.maddyhome.idea.vim.handler.MotionActionHandler
import com.maddyhome.idea.vim.helper.isEndAllowed
import com.maddyhome.idea.vim.helper.usesVirtualSpace
abstract class MotionRightBaseAction() : MotionActionHandler.ForEachCaret() {
@CommandOrMotion(keys = ["l"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
class MotionRightAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset(
@ -34,20 +35,24 @@ abstract class MotionRightBaseAction() : MotionActionHandler.ForEachCaret() {
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("l")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd(editor), allowWrap)
val allowEnd = editor.usesVirtualSpace || editor.isEndAllowed ||
operatorArguments.isOperatorPending // because of `dl` removing the last character
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd = allowEnd, allowWrap)
}
protected open fun allowPastEnd(editor: VimEditor) = editor.usesVirtualSpace || editor.isEndAllowed
}
@CommandOrMotion(keys = ["l"], modes = [Mode.NORMAL, Mode.VISUAL])
class MotionRightAction : MotionRightBaseAction()
@CommandOrMotion(keys = ["<Right>", "<kRight>"], modes = [Mode.INSERT])
class MotionRightInsertAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
@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
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)
}
}

View File

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

View File

@ -16,21 +16,28 @@ import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.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 MotionShiftArrowRightAction : ShiftedArrowKeyHandler(true) {
class MotionShiftRightAction : ShiftedArrowKeyHandler(true) {
override val type: Command.Type = Command.Type.OTHER_READONLY
override fun motionWithKeyModel(editor: VimEditor, caret: VimCaret, context: ExecutionContext, cmd: Command) {
val motion = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true)
caret.moveToMotion(motion)
val vertical = injector.motion.getHorizontalMotion(editor, caret, cmd.count, true)
caret.moveToMotion(vertical)
}
override fun motionWithoutKeyModel(editor: VimEditor, context: ExecutionContext, cmd: Command) {
val caret = editor.currentCaret()
val motion = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false)
caret.moveToMotion(motion)
val newOffset = injector.motion.findOffsetOfNextWord(editor, caret.offset, cmd.count, false)
if (newOffset is Motion.AbsoluteOffset) {
caret.moveToOffset(newOffset.offset)
}
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.action.motion.leftright
import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.options
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.Motion
import com.maddyhome.idea.vim.handler.MotionActionHandler
@CommandOrMotion(keys = ["<Space>"], modes = [Mode.NORMAL, Mode.VISUAL])
open class MotionSpaceAction(private val allowPastEnd: Boolean = false) : MotionActionHandler.ForEachCaret() {
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
val allowWrap = injector.options(editor).whichwrap.contains("s")
return injector.motion.getHorizontalMotion(editor, caret, operatorArguments.count1, allowPastEnd, allowWrap)
}
override val motionType: MotionType = MotionType.EXCLUSIVE
}
@CommandOrMotion(keys = ["<Space>"], modes = [Mode.OP_PENDING])
class MotionSpaceOpPendingModeAction : MotionSpaceAction(allowPastEnd = true)

View File

@ -15,12 +15,15 @@ import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.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,
@ -48,6 +51,9 @@ 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
@ -58,7 +64,7 @@ sealed class TillCharacterMotion(
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
if (argument !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) 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 !is Argument.Character) return Motion.Error
if (argument == null) return Motion.Error
val mark = argument.character
return injector.motion.moveCaretToMark(caret, mark, true)

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.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
@ -23,7 +24,6 @@ import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.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,6 +111,7 @@ 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)
@ -161,21 +162,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 = editor.mode
val mode = operatorArguments.mode
if (type == null ||
(mode == Mode.INSERT || mode == Mode.REPLACE) ||
!saveToRegister ||
@ -238,10 +239,11 @@ abstract class VimChangeGroupBase : VimChangeGroup {
editor: VimEditor,
context: ExecutionContext,
count: Int,
operatorArguments: OperatorArguments,
) {
val myLastStrokes = lastStrokes ?: return
for (caret in editor.nativeCarets()) {
repeat(count) {
for (i in 0 until count) {
for (lastStroke in myLastStrokes) {
when (lastStroke) {
is NativeAction -> {
@ -250,7 +252,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
}
is EditorActionHandlerBase -> {
injector.actionExecutor.executeVimAction(editor, lastStroke, context, OperatorArguments(0, editor.mode))
injector.actionExecutor.executeVimAction(editor, lastStroke, context, operatorArguments)
strokes.add(lastStroke)
}
@ -279,6 +281,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
context: ExecutionContext,
count: Int,
started: Boolean,
operatorArguments: OperatorArguments,
) {
for (caret in editor.nativeCarets()) {
if (repeatLines > 0) {
@ -299,17 +302,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)
repeatInsertText(editor, context, updatedCount, operatorArguments)
} 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)
repeatInsertText(editor, context, updatedCount, operatorArguments)
}
}
caret.moveToOffset(position)
} else {
repeatInsertText(editor, context, count)
repeatInsertText(editor, context, count, operatorArguments)
val position = injector.motion.getHorizontalMotion(editor, caret, -1, false)
caret.moveToMotion(position)
}
@ -327,7 +330,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val oldFragmentLength = oldFragment.length
// Repeat buffer limits
if (repeatCharsCount > MAX_REPEAT_CHARS_COUNT) {
if (repeatCharsCount > Companion.MAX_REPEAT_CHARS_COUNT) {
return
}
@ -348,7 +351,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (oldFragmentLength > 0) {
val editorDelete = injector.nativeActionManager.deleteAction
if (editorDelete != null) {
repeat(oldFragmentLength) {
for (i in 0 until oldFragmentLength) {
strokes.add(editorDelete)
}
}
@ -367,7 +370,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
val motionName = if (delta < 0) "VimMotionLeftAction" else "VimMotionRightAction"
val action = injector.actionExecutor.findVimAction(motionName)!!
val count = abs(delta)
repeat(count) {
for (i in 0 until count) {
positionCaretActions.add(action)
}
return positionCaretActions
@ -446,8 +449,25 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (mode == Mode.REPLACE) {
editor.insertMode = false
}
val count = if (cmd.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) 1 else cmd.count
repeatInsert(editor, context, count, 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),
)
}
if (mode == Mode.REPLACE) {
editor.insertMode = true
}
@ -512,9 +532,9 @@ abstract class VimChangeGroupBase : VimChangeGroup {
exit: Boolean,
operatorArguments: OperatorArguments,
) {
repeatInsertText(editor, context, 1)
repeatInsertText(editor, context, 1, operatorArguments)
if (exit) {
editor.exitInsertMode(context)
editor.exitInsertMode(context, operatorArguments)
}
}
@ -524,7 +544,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
*
* DEPRECATED. Please, don't use this function directly. Use ModeHelper.exitInsertMode in file ModeExtensions.kt
*/
override fun processEscape(editor: VimEditor, context: ExecutionContext?) {
override fun processEscape(editor: VimEditor, context: ExecutionContext?, operatorArguments: OperatorArguments) {
// Get the offset for marks before we exit insert mode - switching from insert to overtype subtracts one from the
// column offset.
val markGroup = injector.markService
@ -533,24 +553,17 @@ abstract class VimChangeGroupBase : VimChangeGroup {
if (editor.mode is Mode.REPLACE) {
editor.insertMode = true
}
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
var cnt = if (lastInsert != null) lastInsert!!.count else 0
if (lastInsert != null && lastInsert!!.flags.contains(CommandFlags.FLAG_NO_REPEAT_INSERT)) {
cnt = 1
}
} ?: 0
if (vimDocument != null && vimDocumentListener != null) {
vimDocument!!.removeChangeListener(vimDocumentListener!!)
vimDocumentListener = null
}
lastStrokes = ArrayList(strokes)
if (context != null) {
injector.changeGroup.repeatInsert(editor, context, repeatCount0, true)
injector.changeGroup.repeatInsert(editor, context, if (cnt == 0) 0 else cnt - 1, true, operatorArguments)
}
if (editor.mode is Mode.INSERT) {
updateLastInsertedTextRegister()
@ -664,7 +677,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)
val res = deleteText(editor, rangeToDelete, SelectionType.CHARACTER_WISE, caret, operatorArguments)
if (editor.usesVirtualSpace) {
caret.moveToOffset(startOffset)
} else {
@ -691,6 +704,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
caret: VimCaret,
count: Int,
spaces: Boolean,
operatorArguments: OperatorArguments,
): Boolean {
var myCount = count
if (myCount < 2) myCount = 2
@ -699,7 +713,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
return if (lline + myCount > total) {
false
} else {
deleteJoinNLines(editor, caret, lline, myCount, spaces)
deleteJoinNLines(editor, caret, lline, myCount, spaces, operatorArguments)
}
}
@ -717,14 +731,12 @@ 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
}
@ -772,7 +784,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
logger.debug("offset=$offset")
}
if (offset != -1) {
val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret)
val res = deleteText(editor, TextRange(start, offset), SelectionType.LINE_WISE, caret, operatorArguments)
if (res && caret.offset >= editor.fileSize() && caret.offset != 0) {
caret.moveToOffset(
injector.motion.moveCaretToRelativeLineStartSkipLeading(
@ -795,7 +807,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
lline + count <= total
}
if (!allowedExecution) return false
repeat(executions) {
for (i in 0 until executions) {
val joinLinesAction = injector.nativeActionManager.joinLines
if (joinLinesAction != null) {
injector.actionExecutor.executeAction(editor, joinLinesAction, context)
@ -825,7 +837,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)
return deleteJoinNLines(editor, caret, startLine, count, spaces, operatorArguments)
}
override fun joinViaIdeaBySelections(
@ -862,25 +874,28 @@ 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
if (!isChange && motionType != SelectionType.LINE_WISE) {
var type: SelectionType = if (argument.motion.isLinewiseMotion()) {
SelectionType.LINE_WISE
} else {
SelectionType.CHARACTER_WISE
}
val motion = argument.motion
if (!isChange && !motion.isLinewiseMotion()) {
val start = editor.offsetToBufferPosition(range.startOffset)
val end = editor.offsetToBufferPosition(range.endOffset)
if (start.line != end.line
&& !editor.anyNonWhitespace(range.startOffset, -1)
&& !editor.anyNonWhitespace(range.endOffset, 1)) {
motionType = SelectionType.LINE_WISE
if (start.line != end.line) {
if (!editor.anyNonWhitespace(range.startOffset, -1) && !editor.anyNonWhitespace(range.endOffset, 1)) {
type = SelectionType.LINE_WISE
}
}
return Pair(range, motionType)
}
return Pair(range, type)
}
/**
@ -900,12 +915,13 @@ 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, saveToRegister)
val res = deleteText(editor, range, type, caret, operatorArguments, saveToRegister)
var processedCaret = editor.findLastVersionOfCaret(caret) ?: caret
if (removeLastNewLine) {
val textLength = editor.fileSize().toInt()
@ -1024,6 +1040,7 @@ 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
@ -1040,7 +1057,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)
deleteText(editor, TextRange(startOffset, endOffset), null, caret, operatorArguments)
if (spaces && !hasTrailingWhitespace) {
insertText(editor, caret, startOffset, " ")
}
@ -1127,8 +1144,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
var motionArgument = argument as? Argument.Motion ?: return false
val id = motionArgument.motion.id
val motion = argument.motion
val id = motion.action.id
var kludge = false
val bigWord = id == VIM_MOTION_BIG_WORD_RIGHT
val chars = editor.text()
@ -1138,7 +1155,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 && operatorArguments.count1 == 1) {
if (wordMotions.contains(id) && lastWordChar && motion.count == 1) {
val res = deleteCharacter(editor, caret, 1, true, operatorArguments)
if (res) {
editor.vimChangeActionSwitchMode = Mode.INSERT
@ -1148,50 +1165,49 @@ abstract class VimChangeGroupBase : VimChangeGroup {
when (id) {
VIM_MOTION_WORD_RIGHT -> {
kludge = true
motionArgument = Argument.Motion(
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_WORD_END_RIGHT)
}
VIM_MOTION_BIG_WORD_RIGHT -> {
kludge = true
motionArgument = Argument.Motion(
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_BIG_WORD_END_RIGHT)
}
VIM_MOTION_CAMEL_RIGHT -> {
kludge = true
motionArgument = Argument.Motion(
injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT) as MotionActionHandler,
motionArgument.argument
)
motion.action = injector.actionExecutor.findVimActionOrDie(VIM_MOTION_CAMEL_END_RIGHT)
}
}
}
}
if (kludge) {
val pos1 = injector.searchHelper.findNextWordEnd(editor, offset, operatorArguments.count1, bigWord, false)
val pos2 = injector.searchHelper.findNextWordEnd(editor, pos1, -operatorArguments.count1, bigWord, false)
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)
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 && operatorArguments.count1 > 1) {
if (pos2 == offset) {
if (operatorArguments.count1 > 1) {
count0--
} else if (motion.count > 1) {
motion.rawCount = motion.count - 1
} else {
motion.flags = EnumSet.noneOf(CommandFlags::class.java)
}
}
}
val (first, second) = getDeleteRangeAndType(
editor,
caret,
context,
motionArgument,
argument,
true,
operatorArguments.copy(count0 = count0),
operatorArguments.withCount0(count0),
) ?: return false
return changeRange(
editor,
@ -1199,6 +1215,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
first,
second,
context,
operatorArguments,
)
}
@ -1223,6 +1240,7 @@ 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(
@ -1231,6 +1249,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
range: TextRange,
type: SelectionType,
context: ExecutionContext,
operatorArguments: OperatorArguments,
): Boolean {
var col = 0
var lines = 0
@ -1243,7 +1262,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)
val res = deleteRange(editor, caret, range, type, true, operatorArguments)
val updatedCaret = editor.findLastVersionOfCaret(caret) ?: caret
if (res) {
if (type === SelectionType.LINE_WISE) {
@ -1905,7 +1924,7 @@ abstract class VimChangeGroupBase : VimChangeGroup {
pos++
}
if (pos > wsoff) {
deleteText(editor, TextRange(wsoff, pos), null, caret, true)
deleteText(editor, TextRange(wsoff, pos), null, caret, operatorArguments, true)
}
}
}
@ -1937,21 +1956,23 @@ abstract class VimChangeGroupBase : VimChangeGroup {
}
}
override fun initBlockInsert(
override fun blockInsert(
editor: VimEditor,
context: ExecutionContext,
range: TextRange,
append: Boolean,
operatorArguments: OperatorArguments,
): Boolean {
val lines = getLinesCountInVisualBlock(editor, range)
val startPosition = editor.offsetToBufferPosition(range.startOffset)
// Note that when called, we're likely to have moved from Visual (block) to Normal, which means all secondary carets
// 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.
val mode = operatorArguments.mode
val visualBlockMode = mode is Mode.VISUAL && mode.selectionType === SelectionType.BLOCK_WISE
for (caret in editor.carets()) {
val line = startPosition.line
var column = startPosition.column
if (append) {
if (!visualBlockMode) {
column = 0
} else if (append) {
column += range.maxLength
if (caret.vimLastColumn == VimMotionGroupBase.LAST_COLUMN) {
column = VimMotionGroupBase.LAST_COLUMN
@ -1963,10 +1984,18 @@ 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
}
@ -2039,3 +2068,9 @@ 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,6 +8,8 @@
package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.command.Command
interface VimCommandLineService {
fun isCommandLineSupported(editor: VimEditor): Boolean
@ -24,7 +26,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, count0: Int, initialText: String): VimCommandLine
fun createCommandPrompt(editor: VimEditor, context: ExecutionContext, command: Command, initialText: String): VimCommandLine
@Deprecated("Please use ModalInputService.create()")
fun createWithoutShortcuts(editor: VimEditor, context: ExecutionContext, label: String, initText: String): VimCommandLine

View File

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

View File

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

View File

@ -8,7 +8,6 @@
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 {
@ -19,9 +18,6 @@ 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): RootNode<LazyVimCommand>
fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand>
fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer
fun getActions(editor: VimEditor, keyStroke: KeyStroke): List<NativeAction>
fun getKeymapConflicts(keyStroke: KeyStroke): List<NativeAction>

View File

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

View File

@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.Graphemes
import com.maddyhome.idea.vim.common.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
@ -117,9 +116,9 @@ abstract class VimMotionGroupBase : VimMotionGroup {
val text = editor.text()
val oldOffset = caret.offset
var current = oldOffset
repeat(count.absoluteValue) {
for (i in 0 until count.absoluteValue) {
val newOffset = if (count > 0) Graphemes.next(text, current) else Graphemes.prev(text, current)
current = newOffset ?: return@repeat
current = newOffset ?: break
}
val offset = if (allowWrap) {
@ -314,51 +313,50 @@ 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
val action = argument.motion
when (action) {
is MotionActionHandler -> {
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) {
// This is where we are now
start = caret.offset
// Execute the motion (without moving the cursor) and get where we end
val motion = action.getHandlerOffset(editor, caret, context, argument.argument, operatorArguments)
if (Motion.Error == motion || Motion.NoMotion == motion) return null
val motion =
cmdAction.getHandlerOffset(editor, caret, context, cmd.argument, operatorArguments.withCount0(raw))
// Invalid motion
if (Motion.Error == motion) return null
if (Motion.NoMotion == motion) return null
end = (motion as AbsoluteOffset).offset
// If inclusive, add the last character to the range
if (action.motionType === MotionType.INCLUSIVE) {
if (cmdAction.motionType === MotionType.INCLUSIVE) {
if (start > end) {
if (start < editor.fileSize()) start++
} else {
if (end < editor.fileSize()) end++
}
}
}
is TextObjectActionHandler -> {
val range: TextRange = action.getRange(editor, caret, context, operatorArguments.count1, operatorArguments.count0)
} else if (cmdAction is TextObjectActionHandler) {
val range: TextRange = cmdAction.getRange(editor, caret, context, cnt, raw)
?: return null
start = range.startOffset
end = range.endOffset
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")
if (cmd.isLinewiseMotion()) end--
} else {
throw RuntimeException(
"Commands doesn't take " + cmdAction.javaClass.simpleName + " as an operator",
)
}
// Normalize the range
@ -370,7 +368,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 (argument.isLinewiseMotion()) {
if (cmd.isLinewiseMotion()) {
if (caret.getBufferPosition().line != editor.lineCount() - 1) {
start = editor.getLineStartForOffset(start)
end = min((editor.getLineEndForOffset(end) + 1).toLong(), editor.fileSize()).toInt()
@ -379,19 +377,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 = action.id
val id = argument.motion.action.id
if (id == "VimMotionWordRightAction" || id == "VimMotionBigWordRightAction" || id == "VimMotionCamelRightAction") {
if (!editor.anyNonWhitespace(end, -1)) {
end = start + lastNewLine
}
}
}
return TextRange(start, end)
}

View File

@ -8,88 +8,51 @@
package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.handler.ExternalActionHandler
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.MotionActionHandler
import com.maddyhome.idea.vim.handler.TextObjectActionHandler
import com.maddyhome.idea.vim.state.mode.SelectionType
import java.util.*
/**
* 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`).
* 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
*/
sealed class Argument {
/** A simple character argument */
class Character(val character: Char) : Argument()
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)
/** 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
}
/**
* 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,
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 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
override val motionType: MotionType = MotionType.EXCLUSIVE
},
Command.Type.MOTION,
EnumSet.noneOf(CommandFlags::class.java),
)
}
}

View File

@ -8,47 +8,34 @@
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 (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
* 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.
*/
data class Command(
val register: Char?,
val rawCount: Int,
val action: EditorActionHandlerBase,
val argument: Argument?,
var rawCount: Int,
var action: EditorActionHandlerBase,
val type: Type,
val flags: EnumSet<CommandFlags>,
var flags: EnumSet<CommandFlags>,
) {
constructor(rawCount: Int, register: Char) : this(
rawCount,
NonExecutableActionHandler,
Type.SELECT_REGISTER,
EnumSet.of(CommandFlags.FLAG_EXPECT_MORE),
) {
this.register = register
}
init {
action.process(this)
}
@ -56,7 +43,20 @@ data class Command(
val count: Int
get() = rawCount.coerceAtLeast(1)
override fun toString() = "Action = ${action.id}"
var argument: Argument? = null
var register: Char? = null
fun isLinewiseMotion(): Boolean {
return when (action) {
is TextObjectActionHandler -> (action as TextObjectActionHandler).visualType == TextObjectVisualType.LINE_WISE
is MotionActionHandler -> (action as MotionActionHandler).motionType == MotionType.LINE_WISE
else -> error("Command is not a motion: $action")
}
}
override fun toString(): String {
return "Action = ${action.id}"
}
enum class Type {
/**
@ -85,6 +85,10 @@ data class Command(
COPY,
PASTE,
/**
* Represents commands that select the register.
*/
SELECT_REGISTER,
OTHER_READONLY,
OTHER_WRITABLE,
@ -108,3 +112,18 @@ data class Command(
}
}
}
private object NonExecutableActionHandler : EditorActionHandlerBase(false) {
override val type: Command.Type
get() = error("This action should not be executed")
override fun baseExecute(
editor: VimEditor,
caret: VimCaret,
context: ExecutionContext,
cmd: Command,
operatorArguments: OperatorArguments,
): Boolean {
error("This action should not be executed")
}
}

View File

@ -15,78 +15,66 @@ import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.trace
import com.maddyhome.idea.vim.diagnostic.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 private constructor(
class CommandBuilder(
private var currentCommandPartNode: CommandPartNode<LazyVimCommand>,
private val counts: MutableList<Int>,
private val commandParts: ArrayDeque<Command>,
private val keyList: MutableList<KeyStroke>,
initialUncommittedRawCount: Int,
) : Cloneable {
constructor(rootNode: RootNode<LazyVimCommand>, initialUncommittedRawCount: Int = 0)
: this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf())
constructor(
currentCommandPartNode: CommandPartNode<LazyVimCommand>,
initialUncommittedRawCount: Int = 0
) : this(
currentCommandPartNode,
ArrayDeque(),
mutableListOf(),
initialUncommittedRawCount
)
private var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND
private var selectedRegister: Char? = null
private var action: EditorActionHandlerBase? = null
private var argument: Argument? = null
private var fallbackArgumentType: Argument.Type? = null
private val motionArgument
get() = argument as? Argument.Motion
private var currentCount: Int
get() = counts.last()
set(value) {
counts[counts.size - 1] = value
}
/** Provide the typed keys for `'showcmd'` */
val keys: Iterable<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
var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND
/**
* Returns the current total count, as the product of all entered count components. The value is not coerced.
* The current uncommitted count for the currently in-progress command part
*
* This value is not reliable! Please use [Command.rawCount] or [Command.count] instead of this function.
* TODO: Investigate usages. This value cannot be trusted
* TODO: Rename to uncommittedRawCount
*
* This value is a snapshot of the count for a currently in-progress command, and should not be used for anything
* 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.
* This value is not coerced, and can be 0.
*
* 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`.
* There are very few reasons for using this value. It is incomplete (the user could type another digit), and there
* can be other committed command parts, such as operator and multiple register selections, each of which will can a
* count (e.g., `2"a3"b4"c5d6` waiting for a motion). The count is only final after [buildCommand], and then only via
* [Command.count] or [Command.rawCount].
*
* The returned value is 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.
* 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.
*/
fun calculateCount0Snapshot(): Int {
return if (counts.all { it == 0 }) 0 else counts.map { it.coerceAtLeast(1) }.reduce { acc, i -> acc * i }
}
var count: Int = initialUncommittedRawCount
private set
/**
* The current aggregated, but uncommitted count for all command parts in the command builder, coerced to 1
*
* This value multiplies together the count for command parts currently committed, such as operator and multiple
* register selections, as well as the current uncommitted count for the next command part. E.g., `2"a3"b4"c5d6` will
* multiply each count together to get what would be the final count. All counts are coerced to at least 1 before
* multiplying, which means the result will also be at least 1.
*
* Note that there are very few uses for this value. The final value should be retrieved from [Command.count] or
* [Command.rawCount] after a call to [buildCommand]. This value is expected to be used for `'incsearch'`
* highlighting.
*/
val aggregatedUncommittedCount: Int
get() = (commandParts.map { it.count }.reduceOrNull { acc, i -> acc * i } ?: 1) * count.coerceAtLeast(1)
val keys: Iterable<KeyStroke> get() = keyList
// 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
@ -96,18 +84,7 @@ class CommandBuilder private constructor(
// still change, if more keys are processed. E.g., it's perfectly valid to select register multiple times `"a"b`.
// This doesn't cause any issues with existing extensions
val register: Char?
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
}
get() = commandParts.lastOrNull { it.register != null }?.register
/**
* The argument type for the current in-progress command part's action
@ -115,202 +92,79 @@ class CommandBuilder private constructor(
* For digraph arguments, this can fall back to [Argument.Type.CHARACTER] if there isn't a digraph match.
*/
val expectedArgumentType: Argument.Type?
get() = fallbackArgumentType
?: motionArgument?.let { return it.motion.argumentType }
?: action?.argumentType
get() = fallbackArgumentType ?: commandParts.lastOrNull()?.action?.argumentType
/**
* 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))
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
}
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 isAwaitingCharOrDigraphArgument(): Boolean {
val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH
logger.debug { "Awaiting char or digraph: $awaiting" }
return awaiting
fun addKey(key: KeyStroke) {
logger.trace { "added key to command builder" }
keyList.add(key)
}
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) {
currentCount = (currentCount * 10) + (key.keyChar - '0')
count = (count * 10) + (key.keyChar - '0')
// If count overflows and flips negative, reset to 999999999L. In Vim, count is a long, which is *usually* 32 bits,
// so will flip at 2147483648. We store count as an Int, which is also 32 bit.
// See https://github.com/vim/vim/blob/b376ace1aeaa7614debc725487d75c8f756dd773/src/normal.c#L631
if (currentCount < 0) {
currentCount = 999999999
if (count < 0) {
count = 999999999
}
addKey(key)
}
fun deleteCountCharacter() {
currentCount /= 10
count /= 10
keyList.removeAt(keyList.size - 1)
}
var isRegisterPending: Boolean = false
private set
fun startWaitingForRegister(key: KeyStroke) {
isRegisterPending = true
addKey(key)
fun setCurrentCommandPartNode(newNode: CommandPartNode<LazyVimCommand>) {
logger.trace { "setCurrentCommandPartNode is executed" }
currentCommandPartNode = newNode
}
fun selectRegister(register: Char) {
logger.trace { "Selected register '$register'" }
selectedRegister = register
isRegisterPending = false
fallbackArgumentType = null
counts.add(0)
fun getChildNode(key: KeyStroke): Node<LazyVimCommand>? {
return currentCommandPartNode[key]
}
/**
* 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 isAwaitingCharOrDigraphArgument(): Boolean {
val awaiting = expectedArgumentType == Argument.Type.CHARACTER || expectedArgumentType == Argument.Type.DIGRAPH
logger.debug { "Awaiting char or digraph: $awaiting" }
return awaiting
}
fun isBuildingMultiKeyCommand(): Boolean {
@ -324,47 +178,67 @@ class CommandBuilder private constructor(
return isMultikey
}
/**
* Build the command with the current counts, register, actions and arguments
*
* The command builder is reset after the command is built.
*/
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
}
fun buildCommand(): Command {
val rawCount = calculateCount0Snapshot()
val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum())
resetAll(currentCommandPartNode.root as RootNode<LazyVimCommand>)
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
return command
}
fun resetAll(rootNode: RootNode<LazyVimCommand>) {
logger.trace("resetAll is executed")
currentCommandPartNode = rootNode
fun resetAll(commandPartNode: CommandPartNode<LazyVimCommand>) {
logger.trace { "resetAll is executed" }
resetInProgressCommandPart(commandPartNode)
commandState = CurrentCommandState.NEW_COMMAND
counts.clear()
counts.add(0)
isRegisterPending = false
selectedRegister = null
action = null
argument = null
commandParts.clear()
keyList.clear()
fallbackArgumentType = null
}
/**
* 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
fun resetCount() {
count = 0
}
fun resetInProgressCommandPart(commandPartNode: CommandPartNode<LazyVimCommand>) {
logger.trace { "resetInProgressCommandPart is executed" }
count = 0
setCurrentCommandPartNode(commandPartNode)
}
@TestOnly
fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@ -372,12 +246,10 @@ class CommandBuilder private constructor(
other as CommandBuilder
if (currentCommandPartNode != other.currentCommandPartNode) 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 (commandParts != other.commandParts) 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
@ -386,38 +258,24 @@ class CommandBuilder private constructor(
override fun hashCode(): Int {
var result = currentCommandPartNode.hashCode()
result = 31 * result + counts.hashCode()
result = 31 * result + selectedRegister.hashCode()
result = 31 * result + action.hashCode()
result = 31 * result + argument.hashCode()
result = 31 * result + commandParts.hashCode()
result = 31 * result + keyList.hashCode()
result = 31 * result + commandState.hashCode()
result = 31 * result + expectedArgumentType.hashCode()
result = 31 * result + fallbackArgumentType.hashCode()
result = 31 * result + count
result = 31 * result + (expectedArgumentType?.hashCode() ?: 0)
result = 31 * result + (fallbackArgumentType?.hashCode() ?: 0)
return result
}
public override fun clone(): CommandBuilder {
val result = CommandBuilder(
currentCommandPartNode,
counts.toMutableList(),
keyList.toMutableList()
)
result.selectedRegister = selectedRegister
result.action = action
result.argument = argument
val result = CommandBuilder(currentCommandPartNode, ArrayDeque(commandParts), keyList.toMutableList(), count)
result.commandState = commandState
result.fallbackArgumentType = fallbackArgumentType
return result
}
override fun toString(): String {
return "Command state = $commandState, " +
"key list = ${ injector.parser.toKeyNotation(keyList) }, " +
"selected register = $selectedRegister, " +
"counts = $counts, " +
"action = $action, " +
"argument = $argument, " +
return "Command state = $commandState, key list = ${ injector.parser.toKeyNotation(keyList) }, command parts = ${ commandParts }, count = $count\n" +
"command part node - $currentCommandPartNode"
}

View File

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

View File

@ -8,53 +8,21 @@
package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.state.mode.Mode
/**
* 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.
* [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.
*/
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,
data class OperatorArguments(
val isOperatorPending: Boolean,
val count0: Int,
@Deprecated("Represents the mode when the OperatorArguments was created, not the current mode. Prefer editor.mode") val mode: Mode,
val mode: Mode,
) {
/**
* Create a new instance of [OperatorArguments]
*
* @param count0 The 0-based count for the whole command
* @param mode Only used for the deprecated [OperatorArguments.mode] property
*/
@Suppress("DEPRECATION")
constructor(count0: Int, mode: Mode) : this(mode is Mode.OP_PENDING, count0, mode)
val count1: Int = count0.coerceAtLeast(1)
fun withCount0(count0: Int): OperatorArguments = this.copy(count0 = count0)
}

View File

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

View File

@ -1,35 +0,0 @@
/*
* Copyright 2003-2024 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package com.maddyhome.idea.vim.common
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
class VimEditorReplaceMask {
private val changedChars = mutableMapOf<LiveRange, Char>()
fun recordChangeAtCaret(editor: VimEditor) {
for (caret in editor.carets()) {
val offset = caret.offset
val marker = editor.createLiveMarker(offset, offset)
changedChars[marker] = editor.charAt(offset)
}
}
fun popChange(editor: VimEditor, offset: Int): Char? {
val marker = editor.createLiveMarker(offset, offset)
val change = changedChars[marker]
changedChars.remove(marker)
return change
}
}
fun forgetAllReplaceMasks() {
injector.editorGroup.getEditors().forEach { it.replaceMask = null }
}

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