From 9bbeab80628220a1b07c48efc73d30933ffdac56 Mon Sep 17 00:00:00 2001 From: filipp Date: Fri, 23 Aug 2024 13:03:53 +0300 Subject: [PATCH] VIM-2074 Backspace behaviour is incorrect in Replace mode --- .../vim/action/editor/VimEditorActions.kt | 5 --- .../maddyhome/idea/vim/group/ChangeGroup.kt | 5 +++ .../idea/vim/helper/UserDataManager.kt | 2 + .../maddyhome/idea/vim/newapi/IjLiveRange.kt | 18 +++++++++ .../maddyhome/idea/vim/newapi/IjVimEditor.kt | 9 +++++ .../ideavim/action/ChangeActionTest.kt | 11 +++++ .../change/change/ChangeReplaceAction.kt | 2 + .../change/insert/InsertBackspaceAction.kt | 40 +++++++++++++++++++ .../maddyhome/idea/vim/api/VimChangeGroup.kt | 1 + .../idea/vim/api/VimChangeGroupBase.kt | 2 + .../com/maddyhome/idea/vim/api/VimEditor.kt | 4 +- .../maddyhome/idea/vim/api/VimEditorBase.kt | 4 ++ .../idea/vim/common/VimEditorReplaceMask.kt | 35 ++++++++++++++++ .../com/maddyhome/idea/vim/regexp/VimRegex.kt | 2 + 14 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertBackspaceAction.kt create mode 100644 vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimEditorReplaceMask.kt diff --git a/src/main/java/com/maddyhome/idea/vim/action/editor/VimEditorActions.kt b/src/main/java/com/maddyhome/idea/vim/action/editor/VimEditorActions.kt index 4bd7a885b4..a7bcb07294 100644 --- a/src/main/java/com/maddyhome/idea/vim/action/editor/VimEditorActions.kt +++ b/src/main/java/com/maddyhome/idea/vim/action/editor/VimEditorActions.kt @@ -22,11 +22,6 @@ import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* -@CommandOrMotion(keys = ["", ""], modes = [Mode.INSERT]) -internal class VimEditorBackSpace : IdeActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE) { - override val type: Command.Type = Command.Type.DELETE -} - @CommandOrMotion(keys = [""], modes = [Mode.INSERT]) internal class VimEditorDelete : IdeActionHandler(IdeActions.ACTION_EDITOR_DELETE) { override val type: Command.Type = Command.Type.DELETE diff --git a/src/main/java/com/maddyhome/idea/vim/group/ChangeGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/ChangeGroup.kt index 31c2519850..270fafc7e1 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/ChangeGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/ChangeGroup.kt @@ -104,6 +104,11 @@ class ChangeGroup : VimChangeGroupBase() { } } + override fun processBackspace(editor: VimEditor, context: ExecutionContext) { + injector.actionExecutor.executeAction(editor, name = IdeActions.ACTION_EDITOR_BACKSPACE, context = context) + injector.scroll.scrollCaretIntoView(editor) + } + private fun restoreCursor(editor: VimEditor, caret: VimCaret, startLine: Int) { if (caret != editor.primaryCaret()) { (editor as IjVimEditor).editor.caretModel.addCaret( diff --git a/src/main/java/com/maddyhome/idea/vim/helper/UserDataManager.kt b/src/main/java/com/maddyhome/idea/vim/helper/UserDataManager.kt index 983fad8df3..39e3c7d8c9 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/UserDataManager.kt +++ b/src/main/java/com/maddyhome/idea/vim/helper/UserDataManager.kt @@ -25,6 +25,7 @@ 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.InsertSequence +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 @@ -126,6 +127,7 @@ internal var Editor.vimExOutput: ExOutputModel? by userData() internal var Editor.vimTestInputModel: TestInputModel? by userData() internal var Editor.vimChangeActionSwitchMode: Mode? by userData() +internal var Editor.replaceMask: VimEditorReplaceMask? by userData() internal var Caret.currentInsert: InsertSequence? by userData() internal val Caret.insertHistory: MutableList by userDataOr { mutableListOf() } diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjLiveRange.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjLiveRange.kt index fa0af6dd70..69ec85f157 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjLiveRange.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjLiveRange.kt @@ -17,6 +17,24 @@ internal class IjLiveRange(val marker: RangeMarker) : LiveRange { override val endOffset: Int get() = marker.endOffset + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as IjLiveRange + + if (startOffset != other.startOffset) return false + if (endOffset != other.endOffset) return false + + return true + } + + override fun hashCode(): Int { + var result = startOffset + result = 31 * result + endOffset + return result + } } val RangeMarker.vim: LiveRange diff --git a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt index d8a1a0819b..f1c8420435 100644 --- a/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt +++ b/src/main/java/com/maddyhome/idea/vim/newapi/IjVimEditor.kt @@ -44,6 +44,8 @@ import com.maddyhome.idea.vim.common.IndentConfig.Companion.create import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.ModeChangeListener import com.maddyhome.idea.vim.common.TextRange +import com.maddyhome.idea.vim.common.VimEditorReplaceMask +import com.maddyhome.idea.vim.common.forgetAllReplaceMasks import com.maddyhome.idea.vim.group.visual.vimSetSystemBlockSelectionSilently import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.StrictMode @@ -53,6 +55,7 @@ import com.maddyhome.idea.vim.helper.fileSize import com.maddyhome.idea.vim.helper.getTopLevelEditor import com.maddyhome.idea.vim.helper.inExMode import com.maddyhome.idea.vim.helper.isTemplateActive +import com.maddyhome.idea.vim.helper.replaceMask import com.maddyhome.idea.vim.helper.vimChangeActionSwitchMode import com.maddyhome.idea.vim.helper.vimLastSelectionType import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl @@ -75,6 +78,11 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase( // TBH, I don't like the names. Need to think a bit more about this val editor = editor.getTopLevelEditor() val originalEditor = editor + override var replaceMask: VimEditorReplaceMask? + get() = editor.replaceMask + set(value) { + editor.replaceMask = value + } override fun updateMode(mode: Mode) { (injector.vimState as VimStateMachineImpl).mode = mode @@ -454,6 +462,7 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor, VimEditorBase( get() = (editor as? EditorEx)?.isInsertMode ?: false set(value) { (editor as? EditorEx)?.isInsertMode = value + forgetAllReplaceMasks() } override val document: VimDocument diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/ChangeActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/ChangeActionTest.kt index f388be1013..af70055877 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/ChangeActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/ChangeActionTest.kt @@ -7,6 +7,7 @@ */ package org.jetbrains.plugins.ideavim.action +import com.intellij.idea.TestFor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.ReturnTo @@ -1068,4 +1069,14 @@ foobaz Mode.NORMAL(), ) } + + @Test + @TestFor(issues = ["VIM-2074"]) + fun `backspace with replace mode`() { + configureByText("${c}Hello world") + typeText("R1111") + assertState("1111o world") + typeText("") + assertState("1ello world") + } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeReplaceAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeReplaceAction.kt index 171785a028..3b7e87a914 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeReplaceAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/change/ChangeReplaceAction.kt @@ -15,6 +15,7 @@ import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.common.VimEditorReplaceMask import com.maddyhome.idea.vim.handler.ChangeEditorActionHandler @CommandOrMotion(keys = ["R"], modes = [Mode.NORMAL]) @@ -39,4 +40,5 @@ class ChangeReplaceAction : ChangeEditorActionHandler.SingleExecution() { */ private fun changeReplace(editor: VimEditor, context: ExecutionContext) { injector.changeGroup.initInsert(editor, context, com.maddyhome.idea.vim.state.mode.Mode.REPLACE) + editor.replaceMask = VimEditorReplaceMask() } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertBackspaceAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertBackspaceAction.kt new file mode 100644 index 0000000000..f1537e9329 --- /dev/null +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertBackspaceAction.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2003-2024 The IdeaVim authors + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE.txt file or at + * https://opensource.org/licenses/MIT. + */ + +package com.maddyhome.idea.vim.action.change.insert + +import com.intellij.vim.annotations.CommandOrMotion +import com.intellij.vim.annotations.Mode +import com.maddyhome.idea.vim.api.ExecutionContext +import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.api.getLineStartForOffset +import com.maddyhome.idea.vim.api.injector +import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.handler.VimActionHandler + +@CommandOrMotion(keys = ["", ""], 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 + } +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt index fe3cecccca..68bcaa8a57 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt @@ -41,6 +41,7 @@ interface VimChangeGroup { 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) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt index 47b52f2add..96984703c2 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt @@ -731,12 +731,14 @@ abstract class VimChangeGroupBase : VimChangeGroup { ): Boolean { logger.debug { "processKey($key)" } if (key.keyChar != KeyEvent.CHAR_UNDEFINED) { + editor.replaceMask?.recordChangeAtCaret(editor) processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, key.keyChar) } return true } // Shift-space if (key.keyCode == 32 && key.modifiers and KeyEvent.SHIFT_DOWN_MASK != 0) { + editor.replaceMask?.recordChangeAtCaret(editor) processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, ' ') } return true } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt index 659f34f80e..004de0583a 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditor.kt @@ -11,12 +11,11 @@ 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.impl.state.VimStateMachineImpl +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 import com.maddyhome.idea.vim.state.mode.returnTo -import org.jetbrains.annotations.ApiStatus.Internal /** * Every line in [VimEditor] ends with a new line TODO <- this is probably not true already @@ -130,6 +129,7 @@ interface VimEditor { val lfMakesNewLine: Boolean var vimChangeActionSwitchMode: Mode? val indentConfig: VimIndentConfig + var replaceMask: VimEditorReplaceMask? fun fileSize(): Long diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditorBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditorBase.kt index a4018dd2f6..e3c85459bc 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditorBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimEditorBase.kt @@ -8,6 +8,7 @@ package com.maddyhome.idea.vim.api +import com.maddyhome.idea.vim.common.forgetAllReplaceMasks import com.maddyhome.idea.vim.state.mode.Mode abstract class VimEditorBase : VimEditor { @@ -18,6 +19,9 @@ abstract class VimEditorBase : VimEditor { if (vimState.mode == value) return val oldValue = vimState.mode + if (oldValue == Mode.REPLACE) { + forgetAllReplaceMasks() + } updateMode(value) injector.listenersNotifier.notifyModeChanged(this, oldValue) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimEditorReplaceMask.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimEditorReplaceMask.kt new file mode 100644 index 0000000000..33b9a7e279 --- /dev/null +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/VimEditorReplaceMask.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2003-2024 The IdeaVim authors + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE.txt file or at + * https://opensource.org/licenses/MIT. + */ + +package com.maddyhome.idea.vim.common + +import com.maddyhome.idea.vim.api.VimEditor +import com.maddyhome.idea.vim.api.injector + +class VimEditorReplaceMask { + private val changedChars = mutableMapOf() + + 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 } +} \ No newline at end of file diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt index 580a7fdb19..12776aee07 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/regexp/VimRegex.kt @@ -26,6 +26,7 @@ import com.maddyhome.idea.vim.api.VirtualFile import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.common.LiveRange import com.maddyhome.idea.vim.common.TextRange +import com.maddyhome.idea.vim.common.VimEditorReplaceMask import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.helper.noneOfEnum import com.maddyhome.idea.vim.regexp.engine.VimRegexEngine @@ -606,6 +607,7 @@ class VimRegex(pattern: String) { override var vimChangeActionSwitchMode: Mode? = null override val indentConfig: VimIndentConfig get() = TODO("Not yet implemented") + override var replaceMask: VimEditorReplaceMask? = null override fun fileSize(): Long { return text.length.toLong()