From 6507a44037265939da01fde84bc917f753f2925c Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 18 Dec 2024 22:28:37 +0100 Subject: [PATCH 01/16] Remove ReturnableFromCmd marker interface --- .../idea/vim/ui/ex/ExEntryPanelService.kt | 9 +++---- .../idea/vim/api/VimCommandLineServiceBase.kt | 4 --- .../com/maddyhome/idea/vim/state/mode/Mode.kt | 25 ++++++++++--------- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanelService.kt b/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanelService.kt index 7fcd073b5e..b7d2fcc5e8 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanelService.kt +++ b/src/main/java/com/maddyhome/idea/vim/ui/ex/ExEntryPanelService.kt @@ -25,7 +25,6 @@ import com.maddyhome.idea.vim.key.interceptors.VimInputInterceptor 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.ReturnableFromCmd import com.maddyhome.idea.vim.ui.ModalEntry import java.awt.event.KeyEvent import javax.swing.KeyStroke @@ -91,7 +90,7 @@ class ExEntryPanelService : VimCommandLineServiceBase(), VimModalInputService { } } if (text != null) { - Extension.addString(text!!) + Extension.addString(text) } return text } @@ -105,9 +104,6 @@ class ExEntryPanelService : VimCommandLineServiceBase(), VimModalInputService { processing: (String) -> Unit, ) { val currentMode = editor.mode - check(currentMode is ReturnableFromCmd) { - "Cannot enable cmd mode from current mode $currentMode" - } // Make sure the Visual selection marks are up to date before we use them. injector.markService.setVisualSelectionMarks(editor) @@ -136,6 +132,7 @@ class ExEntryPanelService : VimCommandLineServiceBase(), VimModalInputService { return panel } + @Deprecated("Please use ModalInputService.create()") override fun createWithoutShortcuts(editor: VimEditor, context: ExecutionContext, label: String, initText: String): VimCommandLine { val panel = ExEntryPanel.getInstanceWithoutShortcuts() panel.activate(editor.ij, context.ij, label, initText) @@ -172,4 +169,4 @@ internal class WrappedAsModalInputExEntryPanel(internal val exEntryPanel: ExEntr override fun focus() { exEntryPanel.focus() } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt index 390fe8a225..edcb460502 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLineServiceBase.kt @@ -10,7 +10,6 @@ package com.maddyhome.idea.vim.api import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.state.mode.Mode -import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd import com.maddyhome.idea.vim.state.mode.inVisualMode abstract class VimCommandLineServiceBase : VimCommandLineService { @@ -24,9 +23,6 @@ abstract class VimCommandLineServiceBase : VimCommandLineService { if (!isCommandLineSupported(editor)) throw ExException("Command line is not allowed in one line editors") val currentMode = editor.mode - check(currentMode is ReturnableFromCmd) { - "Cannot enable cmd mode from current mode $currentMode" - } if (removeSelections) { // Make sure the Visual selection marks are up to date before we use them. diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt index 29c739ea91..39dbde9912 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt @@ -20,22 +20,26 @@ import com.maddyhome.idea.vim.state.VimStateMachine * * Modes with selection have [selectionType] variable representing if the selection is character-, line-, or block-wise. * - * To update the current mode, use [VimStateMachine.setMode]. To get the current mode use [VimStateMachine.mode]. + * To update the current mode, use [VimStateMachine.mode]. To get the current mode use [VimStateMachine.mode]. * * [Mode] also has a bunch of extension functions like [Mode.isSingleModeActive]. * * Also read about how modes work in Vim: https://github.com/JetBrains/ideavim/wiki/how-many-modes-does-vim-have */ sealed interface Mode { - data class NORMAL(val returnTo: ReturnTo? = null) : Mode, ReturnableFromCmd - data class OP_PENDING(val returnTo: ReturnTo? = null, val forcedVisual: SelectionType? = null) : - Mode, ReturnableFromCmd - data class VISUAL(val selectionType: SelectionType, val returnTo: ReturnTo? = null) : Mode, - ReturnableFromCmd + data class NORMAL(val returnTo: ReturnTo? = null) : Mode + data class OP_PENDING(val returnTo: ReturnTo? = null, val forcedVisual: SelectionType? = null) : Mode + data class VISUAL(val selectionType: SelectionType, val returnTo: ReturnTo? = null) : Mode data class SELECT(val selectionType: SelectionType, val returnTo: ReturnTo? = null) : Mode - object INSERT : Mode, ReturnableFromCmd + object INSERT : Mode object REPLACE : Mode - data class CMD_LINE(val returnTo: ReturnableFromCmd) : Mode + data class CMD_LINE(val returnTo: Mode) : Mode { + init { + require(returnTo is NORMAL || returnTo is OP_PENDING || returnTo is VISUAL || returnTo is INSERT) { + "CMD_LINE mode can be active only in NORMAL, OP_PENDING, VISUAL or INSERT modes" + } + } + } } sealed interface ReturnTo { @@ -43,11 +47,8 @@ sealed interface ReturnTo { object REPLACE : ReturnTo } -// Marks modes that can we return from CMD_LINE mode -sealed interface ReturnableFromCmd - enum class SelectionType { LINE_WISE, CHARACTER_WISE, BLOCK_WISE, -} \ No newline at end of file +} From 40a6023fcb9e730b517632658d5e1a5daab65087 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Fri, 27 Dec 2024 00:37:12 +0000 Subject: [PATCH 02/16] Simplify Mode hierarchy Wanting to add `ReturnTo.SELECT` would be very tricky, as we would have to recreate the mode, but have no details about the selection type. --- .../surround/VimSurroundExtension.kt | 7 +- .../maddyhome/idea/vim/group/MotionGroup.kt | 9 +- .../vim/group/visual/IdeaSelectionControl.kt | 3 +- .../idea/vim/helper/ModeExtensions.kt | 40 ++------ .../ideavim/action/ChangeActionTest.kt | 7 +- .../motion/search/SearchEntryFwdActionTest.kt | 20 ++-- .../com/maddyhome/idea/vim/KeyHandler.kt | 10 +- .../vim/action/ex/ProcessExEntryActions.kt | 3 +- .../idea/vim/api/VimChangeGroupBase.kt | 3 +- .../maddyhome/idea/vim/api/VimCommandLine.kt | 5 +- .../com/maddyhome/idea/vim/api/VimEditor.kt | 9 +- .../idea/vim/api/VimVisualMotionGroup.kt | 4 +- .../idea/vim/api/VimVisualMotionGroupBase.kt | 4 +- .../idea/vim/handler/SpecialKeyHandlers.kt | 11 +-- .../maddyhome/idea/vim/helper/EngineHelper.kt | 5 +- .../idea/vim/helper/EngineModeExtensions.kt | 32 ++----- .../idea/vim/key/consumers/CommandConsumer.kt | 1 - .../com/maddyhome/idea/vim/state/mode/Mode.kt | 96 +++++++++++++++---- .../idea/vim/state/mode/modeExtensions.kt | 79 ++++----------- 19 files changed, 142 insertions(+), 206 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt b/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt index 6736ff5ed9..052c396c70 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt @@ -47,7 +47,6 @@ import com.maddyhome.idea.vim.state.mode.selectionType import org.jetbrains.annotations.NonNls import java.awt.event.KeyEvent import javax.swing.KeyStroke -import com.maddyhome.idea.vim.state.mode.returnTo /** * Port of vim-surround. @@ -289,7 +288,7 @@ internal class VimSurroundExtension : VimExtension { private fun getSurroundRange(caret: VimCaret): TextRange? { val editor = caret.editor if (editor.mode is Mode.CMD_LINE) { - editor.mode = (editor.mode as Mode.CMD_LINE).returnTo() + editor.mode = editor.mode.returnTo } return when (editor.mode) { is Mode.NORMAL -> injector.markService.getChangeMarks(caret) @@ -337,7 +336,7 @@ private fun getSurroundPair(c: Char): SurroundPair? = if (c in SURROUND_PAIRS) { private fun inputTagPair(editor: Editor, context: DataContext): SurroundPair? { val tagInput = inputString(editor, context, "<", '>') if (editor.vim.mode is Mode.CMD_LINE) { - editor.vim.mode = editor.vim.mode.returnTo() + editor.vim.mode = editor.vim.mode.returnTo } val matcher = tagNameAndAttributesCapturePattern.matcher(tagInput) return if (matcher.find()) { @@ -356,7 +355,7 @@ private fun inputFunctionName( ): SurroundPair? { val functionNameInput = inputString(editor, context, "function: ", null) if (editor.vim.mode is Mode.CMD_LINE) { - editor.vim.mode = editor.vim.mode.returnTo() + editor.vim.mode = editor.vim.mode.returnTo } if (functionNameInput.isEmpty()) return null return if (withInternalSpaces) { diff --git a/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt index 6ddf9890cb..bcd68927ed 100755 --- a/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/MotionGroup.kt @@ -53,8 +53,6 @@ import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext 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.ReturnTo -import com.maddyhome.idea.vim.state.mode.returnTo import org.jetbrains.annotations.Range import kotlin.math.max import kotlin.math.min @@ -332,12 +330,7 @@ internal class MotionGroup : VimMotionGroupBase() { } else { val state = injector.vimState as VimStateMachineImpl if (state.mode is Mode.VISUAL) { - val returnTo = state.mode.returnTo - when (returnTo) { - ReturnTo.INSERT -> state.mode = Mode.INSERT - ReturnTo.REPLACE -> state.mode = Mode.REPLACE - null -> state.mode = Mode.NORMAL() - } + state.mode = state.mode.returnTo } val keyHandler = KeyHandler.getInstance() KeyHandler.getInstance().reset(keyHandler.keyHandlerState, state.mode) diff --git a/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt b/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt index fc5e1e97b7..d0ccc181d3 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt @@ -33,7 +33,6 @@ import com.maddyhome.idea.vim.state.mode.inCommandLineMode import com.maddyhome.idea.vim.state.mode.inNormalMode import com.maddyhome.idea.vim.state.mode.inSelectMode import com.maddyhome.idea.vim.state.mode.inVisualMode -import com.maddyhome.idea.vim.state.mode.returnTo import com.maddyhome.idea.vim.vimscript.model.options.helpers.IdeaRefactorModeHelper import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeKeep import com.maddyhome.idea.vim.vimscript.model.options.helpers.isIdeaRefactorModeSelect @@ -75,7 +74,7 @@ internal object IdeaSelectionControl { } if (hasSelection) { - if (editor.vim.inCommandLineMode && editor.vim.mode.returnTo().hasVisualSelection) { + if (editor.vim.inCommandLineMode && editor.vim.mode.returnTo.hasVisualSelection) { logger.trace { "Modifying selection while in Command-line mode, most likely incsearch" } return@singleTask } diff --git a/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt b/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt index 89123daeea..078cba67ce 100644 --- a/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt +++ b/src/main/java/com/maddyhome/idea/vim/helper/ModeExtensions.kt @@ -21,29 +21,14 @@ import com.maddyhome.idea.vim.newapi.IjEditorExecutionContext import com.maddyhome.idea.vim.newapi.IjVimCaret import com.maddyhome.idea.vim.newapi.IjVimEditor import com.maddyhome.idea.vim.newapi.vim -import com.maddyhome.idea.vim.state.mode.Mode -import com.maddyhome.idea.vim.state.mode.ReturnTo import com.maddyhome.idea.vim.state.mode.inSelectMode -import com.maddyhome.idea.vim.state.mode.returnTo /** [adjustCaretPosition] - if true, caret will be moved one char left if it's on the line end */ internal fun Editor.exitSelectMode(adjustCaretPosition: Boolean) { - if (!this.vim.inSelectMode) return + val vimEditor = this.vim + if (!vimEditor.inSelectMode) return - val returnTo = this.vim.mode.returnTo - when (returnTo) { - ReturnTo.INSERT -> { - this.vim.mode = Mode.INSERT - } - - ReturnTo.REPLACE -> { - this.vim.mode = Mode.REPLACE - } - - null -> { - this.vim.mode = Mode.NORMAL() - } - } + vimEditor.mode = vimEditor.mode.returnTo SelectionVimListenerSuppressor.lock().use { this.caretModel.allCarets.forEach { it.removeSelection() @@ -63,28 +48,15 @@ internal fun Editor.exitSelectMode(adjustCaretPosition: Boolean) { internal fun VimEditor.exitSelectMode(adjustCaretPosition: Boolean) { if (!this.inSelectMode) return - val returnTo = this.mode.returnTo - when (returnTo) { - ReturnTo.INSERT -> { - this.mode = Mode.INSERT - } - - ReturnTo.REPLACE -> { - this.mode = Mode.REPLACE - } - - null -> { - this.mode = Mode.NORMAL() - } - } + mode = mode.returnTo SelectionVimListenerSuppressor.lock().use { - this.carets().forEach { vimCaret -> + carets().forEach { vimCaret -> val caret = (vimCaret as IjVimCaret).caret caret.removeSelection() caret.vim.vimSelectionStartClear() if (adjustCaretPosition) { val lineEnd = IjVimEditor((this as IjVimEditor).editor).getLineEndForOffset(caret.offset) - val lineStart = IjVimEditor(this.editor).getLineStartForOffset(caret.offset) + val lineStart = IjVimEditor(editor).getLineStartForOffset(caret.offset) if (caret.offset == lineEnd && caret.offset != lineStart) { caret.moveToInlayAwareOffset(caret.offset - 1) } 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 742c228a3b..4d83beb19b 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/ChangeActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/ChangeActionTest.kt @@ -10,7 +10,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 import com.maddyhome.idea.vim.state.mode.SelectionType import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.TestWithoutNeovim @@ -52,7 +51,7 @@ class ChangeActionTest : VimTestCase() { listOf("i", "", "v"), "12${c}345", "12${s}${c}3${se}45", - Mode.VISUAL(SelectionType.CHARACTER_WISE, ReturnTo.INSERT) + Mode.VISUAL(SelectionType.CHARACTER_WISE, Mode.INSERT) ) } @@ -87,7 +86,7 @@ class ChangeActionTest : VimTestCase() { listOf("i", "", "v", ""), "12${c}345", "12${s}3${c}${se}45", - Mode.SELECT(SelectionType.CHARACTER_WISE, ReturnTo.INSERT), + Mode.SELECT(SelectionType.CHARACTER_WISE, Mode.INSERT), ) } @@ -99,7 +98,7 @@ class ChangeActionTest : VimTestCase() { listOf("i", "", "gh"), "12${c}345", "12${s}3${c}${se}45", - Mode.SELECT(SelectionType.CHARACTER_WISE, ReturnTo.INSERT), + Mode.SELECT(SelectionType.CHARACTER_WISE, Mode.INSERT), ) } diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchEntryFwdActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchEntryFwdActionTest.kt index 031d2319a4..228ec56bd4 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchEntryFwdActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/search/SearchEntryFwdActionTest.kt @@ -50,13 +50,13 @@ class SearchEntryFwdActionTest : VimTestCase() { |${c}consectetur adipiscing elit |Sed in orci mauris. |Cras id tellus in ex imperdiet egestas. - """.trimMargin(), + """.trimMargin(), """Lorem ipsum dolor sit amet, - |consectetur adipiscing elit + |${s}consectetur adipiscing elit |Sed in orci mauris. - |Cras ${c}id tellus in ex imperdiet egestas. - """.trimMargin(), - Mode.INSERT, + |Cras ${c}i${se}d tellus in ex imperdiet egestas. + """.trimMargin(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), ) } @@ -68,13 +68,13 @@ class SearchEntryFwdActionTest : VimTestCase() { |${c}consectetur adipiscing elit |Sed in orci mauris. |Cras id tellus in ex imperdiet egestas. - """.trimMargin(), + """.trimMargin(), """Lorem ipsum dolor sit amet, - |consectetur adipiscing elit + |${s}consectetur adipiscing elit |Sed in orci mauris. - |Cras ${c}id tellus in ex imperdiet egestas. - """.trimMargin(), - Mode.REPLACE, + |Cras ${c}i${se}d tellus in ex imperdiet egestas. + """.trimMargin(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), ) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt index 289f55a4aa..c27883be8d 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt @@ -34,8 +34,6 @@ import com.maddyhome.idea.vim.key.consumers.SelectRegisterConsumer import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.VimStateMachine import com.maddyhome.idea.vim.state.mode.Mode -import com.maddyhome.idea.vim.state.mode.ReturnTo -import com.maddyhome.idea.vim.state.mode.returnTo import javax.swing.KeyStroke /** @@ -334,11 +332,7 @@ class KeyHandler { // 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 -> {} - } + editor.mode = editorState.mode.returnTo } instance.reset(keyState, editorState.mode) @@ -483,4 +477,4 @@ data class LastUsedEditorInfo( * If true, this editor was initialized in insert mode */ val isInsertModeForced: Boolean, -) \ No newline at end of file +) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt index 510909fbb8..53f9e56513 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/ex/ProcessExEntryActions.kt @@ -24,7 +24,6 @@ import com.maddyhome.idea.vim.ex.ExException 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.state.mode.returnTo import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext import java.util.* @@ -87,7 +86,7 @@ class ProcessExCommandEntryAction : MotionActionHandler.SingleExecution() { // startExEntry). Remember from startExEntry that we might still have selection and/or multiple carets, even // though we're in Normal. This will be handled by Command.execute once we know if we should be clearing the // selection. - editor.mode = editor.mode.returnTo() + editor.mode = editor.mode.returnTo logger.debug("processing command") 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 861b4dd4a7..0c23d0a65d 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 @@ -39,7 +39,6 @@ import com.maddyhome.idea.vim.regexp.match.VimMatchResult import com.maddyhome.idea.vim.register.RegisterConstants.LAST_INSERTED_TEXT_REGISTER import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType -import com.maddyhome.idea.vim.state.mode.toReturnTo import com.maddyhome.idea.vim.undo.VimKeyBasedUndoService import com.maddyhome.idea.vim.undo.VimTimestampBasedUndoService import com.maddyhome.idea.vim.vimscript.model.commands.SortOption @@ -660,7 +659,7 @@ abstract class VimChangeGroupBase : VimChangeGroup { * @param editor The editor to put into NORMAL mode for one command */ override fun processSingleCommand(editor: VimEditor) { - editor.mode = Mode.NORMAL(returnTo = editor.mode.toReturnTo) + editor.mode = Mode.NORMAL(editor.mode) clearStrokes(editor) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLine.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLine.kt index f18e34b0b4..daa2c3ab6e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLine.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimCommandLine.kt @@ -12,7 +12,6 @@ import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.history.HistoryEntry import com.maddyhome.idea.vim.history.VimHistory -import com.maddyhome.idea.vim.state.mode.returnTo import javax.swing.KeyStroke import kotlin.math.min @@ -111,7 +110,7 @@ interface VimCommandLine { fun close(refocusOwningEditor: Boolean, resetCaret: Boolean) { // If 'cpoptions' contains 'x', then Escape should execute the command line. This is the default for Vi but not Vim. // IdeaVim does not (currently?) support 'cpoptions', so sticks with Vim's default behaviour. Escape cancels. - editor.mode = editor.mode.returnTo() + editor.mode = editor.mode.returnTo KeyHandler.getInstance().keyHandlerState.leaveCommandLine() deactivate(refocusOwningEditor, resetCaret) } @@ -165,4 +164,4 @@ interface VimCommandLine { caret.offset = txt.length } } -} \ No newline at end of file +} 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 473f0eaadd..ae07eb1e7b 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 @@ -13,9 +13,7 @@ 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 -import com.maddyhome.idea.vim.state.mode.returnTo /** * Every line in [VimEditor] ends with a new line TODO <- this is probably not true already @@ -281,12 +279,7 @@ interface VimEditor { fun resetOpPending() { if (this.mode is Mode.OP_PENDING) { - val returnTo = this.mode.returnTo - mode = when (returnTo) { - ReturnTo.INSERT -> Mode.INSERT - ReturnTo.REPLACE -> Mode.INSERT - null -> Mode.NORMAL() - } + mode = mode.returnTo } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index e4f01ea866..fe83144532 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -8,7 +8,7 @@ package com.maddyhome.idea.vim.api -import com.maddyhome.idea.vim.state.mode.ReturnTo +import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType interface VimVisualMotionGroup { @@ -22,7 +22,7 @@ interface VimVisualMotionGroup { * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] * If visual mode is enabled with the same [selectionType], disable it */ - fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: ReturnTo? = null): Boolean + fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean fun enterSelectMode(editor: VimEditor, subMode: SelectionType): Boolean /** diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index 034be2a403..045add2be1 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -17,10 +17,8 @@ import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.pushVisualMode import com.maddyhome.idea.vim.helper.setSelectMode 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.inVisualMode -import com.maddyhome.idea.vim.state.mode.returnTo import com.maddyhome.idea.vim.state.mode.selectionType abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { @@ -47,7 +45,7 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { count: Int, rawCount: Int, selectionType: SelectionType, - returnTo: ReturnTo? + returnTo: Mode? ): Boolean { if (!editor.inVisualMode) { // Enable visual subMode diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt index 49f8480d64..e0e8064a99 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt @@ -19,7 +19,7 @@ import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.options.OptionConstants -import com.maddyhome.idea.vim.state.mode.ReturnTo +import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.isInsertionAllowed import com.maddyhome.idea.vim.state.mode.inSelectMode @@ -62,8 +62,7 @@ abstract class ShiftedSpecialKeyHandler : VimActionHandler.ConditionalMulticaret if (injector.globalOptions().selectmode.contains(OptionConstants.selectmode_key)) { injector.visualMotionGroup.enterSelectMode(editor, SelectionType.CHARACTER_WISE) } else { - injector.visualMotionGroup - .toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) + injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) } } return true @@ -98,11 +97,9 @@ abstract class ShiftedArrowKeyHandler(private val runBothCommandsAsMulticaret: B injector.visualMotionGroup.enterSelectMode(editor, SelectionType.CHARACTER_WISE) } else { if (editor.isInsertionAllowed) { - injector.visualMotionGroup - .toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE, ReturnTo.INSERT) + injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE, Mode.INSERT) } else { - injector.visualMotionGroup - .toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) + injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt index 1899d8fb0d..ad72aa6db5 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt @@ -16,7 +16,6 @@ import com.maddyhome.idea.vim.options.OptionConstants import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.isSingleModeActive -import com.maddyhome.idea.vim.state.mode.returnTo import java.util.* inline fun > noneOfEnum(): EnumSet = EnumSet.noneOf(T::class.java) @@ -50,9 +49,9 @@ inline fun > enumSetOf(vararg value: T): EnumSet = when ( } fun VimEditor.setSelectMode(submode: SelectionType) { - mode = Mode.SELECT(submode, this.mode.returnTo) + mode = Mode.SELECT(submode, mode.returnTo) } fun VimEditor.pushVisualMode(submode: SelectionType) { - mode = Mode.VISUAL(submode, this.mode.returnTo) + mode = Mode.VISUAL(submode, mode.returnTo) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt index 309153869a..f0da9d38c7 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt @@ -12,40 +12,24 @@ import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor -import com.maddyhome.idea.vim.state.mode.Mode -import com.maddyhome.idea.vim.state.mode.ReturnTo -import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE +import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.inBlockSelection import com.maddyhome.idea.vim.state.mode.inVisualMode -import com.maddyhome.idea.vim.state.mode.returnTo import com.maddyhome.idea.vim.state.mode.selectionType fun VimEditor.exitVisualMode() { - val selectionType = this.mode.selectionType ?: CHARACTER_WISE + val selectionType = mode.selectionType ?: SelectionType.CHARACTER_WISE SelectionVimListenerSuppressor.lock().use { if (inBlockSelection) { - this.removeSecondaryCarets() + removeSecondaryCarets() } - this.nativeCarets().forEach(VimCaret::removeSelection) + nativeCarets().forEach(VimCaret::removeSelection) } - if (this.inVisualMode) { - this.vimLastSelectionType = selectionType + if (inVisualMode) { + vimLastSelectionType = selectionType injector.markService.setVisualSelectionMarks(this) - this.nativeCarets().forEach { it.vimSelectionStartClear() } + nativeCarets().forEach { it.vimSelectionStartClear() } - val returnTo = this.mode.returnTo - when (returnTo) { - ReturnTo.INSERT -> { - this.mode = Mode.INSERT - } - - ReturnTo.REPLACE -> { - this.mode = Mode.REPLACE - } - - null -> { - this.mode = Mode.NORMAL() - } - } + mode = mode.returnTo } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt index b35246e348..efeaeae345 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/CommandConsumer.kt @@ -24,7 +24,6 @@ import com.maddyhome.idea.vim.key.KeyConsumer import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.VimStateMachine import com.maddyhome.idea.vim.state.mode.Mode -import com.maddyhome.idea.vim.state.mode.returnTo import javax.swing.KeyStroke class CommandConsumer : KeyConsumer { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt index 39dbde9912..df42efc3c0 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt @@ -10,41 +10,101 @@ package com.maddyhome.idea.vim.state.mode +import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.state.VimStateMachine /** * Represents a mode in IdeaVim. * - * If mode has [returnTo] variable, it can be active during the one-command-mode (':h i_Ctrl-o'). If this value - * is not null, the one-command-mode is active and we should get back to [returnTo] mode. + * IdeaVim's default mode is Normal, represented with the [NORMAL] subtype. It will enter other modes through keystrokes + * such as `i`, `v` or `R`. Leaving a mode is usually handled with ``, and the current mode will usually return + * to the previous mode. For example, hitting `` in Insert will return to Normal. * - * Modes with selection have [selectionType] variable representing if the selection is character-, line-, or block-wise. + * Modes can be nested, too. For example, `v/foo` will start in Normal, switch to Visual (with character-wise selection) + * and then switch to Command-line. Hitting `` in this nested Command-line mode will return to Visual. Hitting + * `` to accept the search result will also exit Command-line and return to Visual. Commands such as `d` can also + * end Visual mode and return to Normal. See also `id/foo`, which starts in Normal, switches to Insert, then nested + * "Insert Normal", Operator-pending and finally Command-line. + * + * Not all modes are nested. For example, Select mode can be entered from Visual (`v`), but this is a mode switch, + * rather than nesting - hitting `v` will result in Normal mode. * - * To update the current mode, use [VimStateMachine.mode]. To get the current mode use [VimStateMachine.mode]. + * Furthermore, a mode can be active for a single command, via `` in Insert or Replace mode (`:help i_CTRL-O`), or + * in Select mode (`:help v_CTRL-O`). When used in Insert or Replace mode, this enters Normal mode for a single command, + * and then returns to Insert or Replace respectively. When in Select mode, `` will enter Visual for a single + * command, and will then return to Select or Normal, depending on if the selection has been removed or not. Note that + * it is hard to know when a specific command is active for a single command - for example, `id/foo` would need to + * recursively look at the [returnTo] value until it found a [NORMAL] mode that had a non-[NORMAL] return to mode, but + * this wouldn't work for `v/foo`, which doesn't have a nested [NORMAL] mode, but a nested [VISUAL] mode. * - * [Mode] also has a bunch of extension functions like [Mode.isSingleModeActive]. + * The [VimStateMachine.mode] property can be used to set or get the current mode, but it is usually preferable to use + * the [VimEditor.mode] property. There are also several extension functions such as [Mode.isSingleModeActive] and + * [VimEditor.inVisualMode]. Setting the current selection and entering Visual or Select mode are usually two + * (programmatic) operations, although there are helper functions for this. + * + * Modes with selection have [selectionType] variable representing if the selection is character-, line-, or block-wise. * * Also read about how modes work in Vim: https://github.com/JetBrains/ideavim/wiki/how-many-modes-does-vim-have + * + * One word of warning: don't try to map all the state transitions! [vim/vim#12115](https://github.com/vim/vim/issues/12115) */ sealed interface Mode { - data class NORMAL(val returnTo: ReturnTo? = null) : Mode - data class OP_PENDING(val returnTo: ReturnTo? = null, val forcedVisual: SelectionType? = null) : Mode - data class VISUAL(val selectionType: SelectionType, val returnTo: ReturnTo? = null) : Mode - data class SELECT(val selectionType: SelectionType, val returnTo: ReturnTo? = null) : Mode - object INSERT : Mode - object REPLACE : Mode - data class CMD_LINE(val returnTo: Mode) : Mode { + /** + * The mode to return to when Escape is pressed, or the current command is finished + */ + val returnTo: Mode + + data class NORMAL(private val originalMode: Mode? = null) : Mode { + override val returnTo: Mode + get() = originalMode ?: this + } + + data class OP_PENDING(override val returnTo: Mode) : Mode { init { - require(returnTo is NORMAL || returnTo is OP_PENDING || returnTo is VISUAL || returnTo is INSERT) { - "CMD_LINE mode can be active only in NORMAL, OP_PENDING, VISUAL or INSERT modes" + // OP_PENDING will normally return to NORMAL, but can return to INSERT or REPLACE if i_CTRL-O is followed by an + // operator such as `d`. I.e. "Insert Normal mode" and "Replace Normal mode" + require(returnTo is NORMAL || returnTo is INSERT || returnTo is REPLACE) { + "OP_PENDING mode can be active only in NORMAL, INSERT or REPLACE modes, not ${returnTo.javaClass.simpleName}" + } + } + } + + data class VISUAL(val selectionType: SelectionType, override val returnTo: Mode = NORMAL()) : Mode { + init { + // VISUAL will normally return to NORMAL, but can return to INSERT or REPLACE if i_CTRL-O is followed by `v` + // I.e. "Insert Visual mode" and "Replace Visual mode" + require(returnTo is NORMAL || returnTo is INSERT || returnTo is REPLACE) { + "VISUAL mode can be active only in NORMAL, INSERT or REPLACE modes, not ${returnTo.javaClass.simpleName}" } } } -} -sealed interface ReturnTo { - object INSERT : ReturnTo - object REPLACE : ReturnTo + data class SELECT(val selectionType: SelectionType, override val returnTo: Mode = NORMAL()) : Mode { + init { + // SELECT will normally return to NORMAL, but can return to INSERT or REPLACE if v_CTRL-O is followed by a command + // that deletes the selection, e.g. `d`, or if "Insert Select mode" removes selection (`i`, ``, `ve`, `x`) + // SELECT can also be changed to VISUAL with v_CTRL-O + require(returnTo is NORMAL || returnTo is INSERT || returnTo is REPLACE) { + "SELECT mode can be active only in NORMAL, INSERT or REPLACE modes, not ${returnTo.javaClass.simpleName}" + } + } + } + + object INSERT : Mode { + override val returnTo: Mode = NORMAL() + } + + object REPLACE : Mode { + override val returnTo: Mode = NORMAL() + } + + data class CMD_LINE(override val returnTo: Mode) : Mode { + init { + require(returnTo is NORMAL || returnTo is OP_PENDING || returnTo is VISUAL || returnTo is INSERT) { + "CMD_LINE mode can be active only in NORMAL, OP_PENDING, VISUAL or INSERT modes, not ${returnTo.javaClass.simpleName}" + } + } + } } enum class SelectionType { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/modeExtensions.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/modeExtensions.kt index fadaac48e5..1c98e4da39 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/modeExtensions.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/modeExtensions.kt @@ -19,20 +19,7 @@ val Mode.selectionType: SelectionType? get() = when (this) { is Mode.VISUAL -> this.selectionType is Mode.SELECT -> this.selectionType - is Mode.CMD_LINE -> this.returnTo().selectionType - else -> null - } - -/** - * Get the mode that we need to return to if the one-command-mode (':h i_Ctrl-o') is active. - * Otherwise, returns null. - */ -val Mode.returnTo: ReturnTo? - get() = when (this) { - is Mode.NORMAL -> this.returnTo - is Mode.SELECT -> this.returnTo - is Mode.VISUAL -> this.returnTo - is Mode.OP_PENDING -> this.returnTo + is Mode.CMD_LINE -> this.returnTo.selectionType else -> null } @@ -40,7 +27,21 @@ val Mode.returnTo: ReturnTo? * Check if one-command-mode (':h i_Ctrl-o') is active. */ val Mode.isSingleModeActive: Boolean - get() = returnTo != null + get() { + // TODO: This check isn't accurate. Needs updating + // If we're in Normal and returning to something other than Normal, then we're definitely in single-execution mode + // ("Insert Normal mode"). This doesn't apply to all modes - Visual returning to Normal is fine, as is Command-line + // returning to Normal. We can even have Command-line returning to Visual returning to Normal, and this isn't + // single-execution mode. + // Single-execution mode allows changing mode further, so we can be in Visual returning to Normal returning to + // Insert (returning to Normal) with `iv` aka "Insert Visual mode". We could even be in Command-line ultimately + // returning to Replace with `Rv/foo`. But they all have Normal mode returning to non-Normal, so we could + // recursively look for this. + // We also need to check for Support mode, which also supports single-execution mode, although this switches to + // Visual mode. E.g. with `:set selectmode=key keymodel=startsel`, then `i` will be Select mode returning + // to Normal, returning to Insert (returning to Normal) aka "Insert Select mode". See also `:help v_CTRL-O`. + return (this != Mode.NORMAL() && this.returnTo != Mode.NORMAL()) || (this == Mode.NORMAL() && this.returnTo != this) + } /** * Check if the caret can be placed after the end of line. @@ -126,51 +127,3 @@ fun Mode.toVimNotation(): String { is Mode.OP_PENDING -> "no" } } - -fun Mode.returnTo(): Mode { - return when (this) { - is Mode.CMD_LINE -> { - val returnMode = returnTo as Mode - // We need to understand logic that doesn't exit visual if it's just visual, - // but exits visual if it's one-time visual - if (returnMode.returnTo != null) { - returnMode.returnTo() - } else { - returnMode - } - } - - Mode.INSERT -> Mode.NORMAL() - is Mode.NORMAL -> when (returnTo) { - ReturnTo.INSERT -> Mode.INSERT - ReturnTo.REPLACE -> Mode.REPLACE - null -> Mode.NORMAL() - } - - is Mode.OP_PENDING -> when (returnTo) { - ReturnTo.INSERT -> Mode.INSERT - ReturnTo.REPLACE -> Mode.REPLACE - null -> Mode.NORMAL() - } - - Mode.REPLACE -> Mode.NORMAL() - is Mode.SELECT -> when (returnTo) { - ReturnTo.INSERT -> Mode.INSERT - ReturnTo.REPLACE -> Mode.REPLACE - null -> Mode.NORMAL() - } - - is Mode.VISUAL -> when (returnTo) { - ReturnTo.INSERT -> Mode.INSERT - ReturnTo.REPLACE -> Mode.REPLACE - null -> Mode.NORMAL() - } - } -} - -val Mode.toReturnTo: ReturnTo - get() = when (this) { - Mode.INSERT -> ReturnTo.INSERT - Mode.REPLACE -> ReturnTo.REPLACE - else -> error("Cannot get return to from $this") - } From b446dcb457f82aa5ec194a91267d98b951f88fa6 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Fri, 27 Dec 2024 10:27:08 +0000 Subject: [PATCH 03/16] Show additional Insert modes in showmode widget --- .../idea/vim/ui/widgets/mode/VimModeWidget.kt | 45 ++++++++++++++----- .../com/maddyhome/idea/vim/state/mode/Mode.kt | 44 ++++++++++++++++++ 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt index cb093a2019..fe28dd7a06 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt +++ b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt @@ -34,7 +34,11 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW private companion object { private const val INSERT = "INSERT" private const val NORMAL = "NORMAL" + private const val INSERT_NORMAL = "(insert)" + private const val INSERT_PENDING_PREFIX = "(insert) " private const val REPLACE = "REPLACE" + private const val REPLACE_NORMAL = "(replace)" + private const val REPLACE_PENDING_PREFIX = "(replace) " private const val COMMAND = "COMMAND" private const val VISUAL = "VISUAL" private const val VISUAL_LINE = "V-LINE" @@ -43,6 +47,7 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW private const val SELECT_LINE = "S-LINE" private const val SELECT_BLOCK = "S-BLOCK" } + private val label = JBLabelWiderThan(setOf(REPLACE)).apply { isOpaque = true } init { @@ -96,7 +101,7 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW return when (mode) { Mode.INSERT -> INSERT Mode.REPLACE -> REPLACE - is Mode.NORMAL -> NORMAL + is Mode.NORMAL -> getNormalModeText(mode) is Mode.CMD_LINE -> COMMAND is Mode.VISUAL -> getVisualModeText(mode) is Mode.SELECT -> getSelectModeText(mode) @@ -104,16 +109,36 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW } } - private fun getVisualModeText(mode: Mode.VISUAL) = when (mode.selectionType) { - SelectionType.CHARACTER_WISE -> VISUAL - SelectionType.LINE_WISE -> VISUAL_LINE - SelectionType.BLOCK_WISE -> VISUAL_BLOCK + private fun getNormalModeText(mode: Mode.NORMAL) = when { + mode.isInsertPending -> INSERT_NORMAL + mode.isReplacePending -> REPLACE_NORMAL + else -> NORMAL + } + + private fun getVisualModeText(mode: Mode.VISUAL): String { + val prefix = when { + mode.isInsertPending -> INSERT_PENDING_PREFIX + mode.isReplacePending -> REPLACE_PENDING_PREFIX + else -> "" + } + return prefix + when (mode.selectionType) { + SelectionType.CHARACTER_WISE -> VISUAL + SelectionType.LINE_WISE -> VISUAL_LINE + SelectionType.BLOCK_WISE -> VISUAL_BLOCK + } } - private fun getSelectModeText(mode: Mode.SELECT) = when (mode.selectionType) { - SelectionType.CHARACTER_WISE -> SELECT - SelectionType.LINE_WISE -> SELECT_LINE - SelectionType.BLOCK_WISE -> SELECT_BLOCK + private fun getSelectModeText(mode: Mode.SELECT): String { + val prefix = when { + mode.isInsertPending -> INSERT_PENDING_PREFIX + mode.isReplacePending -> REPLACE_PENDING_PREFIX + else -> "" + } + return prefix + when (mode.selectionType) { + SelectionType.CHARACTER_WISE -> SELECT + SelectionType.LINE_WISE -> SELECT_LINE + SelectionType.BLOCK_WISE -> SELECT_BLOCK + } } private class JBLabelWiderThan(private val words: Collection): JBLabel("", CENTER) { @@ -158,4 +183,4 @@ fun repaintModeWidget() { } } } -} \ No newline at end of file +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt index df42efc3c0..9faf45c9c9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt @@ -57,6 +57,20 @@ sealed interface Mode { data class NORMAL(private val originalMode: Mode? = null) : Mode { override val returnTo: Mode get() = originalMode ?: this + + /** + * Returns true if Insert mode is pending, after the completion of the current Normal command. AKA "Insert Normal" + * + * When in Insert mode the `` keystroke will temporarily switch to Normal for the duration of a single command. + */ + val isInsertPending = originalMode is INSERT + + /** + * Returns true if Replace mode is pending, after the completion of the current Normal command. + * + * Like "Insert Normal", but with `` used in Replace mode. + */ + val isReplacePending = originalMode is REPLACE } data class OP_PENDING(override val returnTo: Mode) : Mode { @@ -77,6 +91,21 @@ sealed interface Mode { "VISUAL mode can be active only in NORMAL, INSERT or REPLACE modes, not ${returnTo.javaClass.simpleName}" } } + + /** + * Returns true if Insert mode is pending, after the completion of the current Visual command. AKA "Insert Visual" + * + * Vim can enter Visual mode from Insert mode, either using shifted keys (based on `'keymodel'` and `'selectmode'` + * values) or via "Insert Normal" (`iv`). + */ + val isInsertPending = returnTo is INSERT + + /** + * Returns true if Replace mode is pending, after the completion of the current Visual command. + * + * Like "Insert Visual", but starting from (and returning to) Replace (`Rv`). + */ + val isReplacePending = returnTo is REPLACE } data class SELECT(val selectionType: SelectionType, override val returnTo: Mode = NORMAL()) : Mode { @@ -88,6 +117,21 @@ sealed interface Mode { "SELECT mode can be active only in NORMAL, INSERT or REPLACE modes, not ${returnTo.javaClass.simpleName}" } } + + /** + * Returns true if Insert mode is pending, after the completion of the current Visual command. AKA "Insert Visual" + * + * Vim can enter Select mode from Insert mode, either using shifted keys (based on `'keymodel'` and `'selectmode'` + * values) or via "Insert Normal" (`igh`). + */ + val isInsertPending = returnTo is INSERT + + /** + * Returns true if Replace mode is pending, after the completion of the current Visual command. + * + * Like "Insert Select", but starting from (and returning to) Replace (e.g., `Rgh`). + */ + val isReplacePending = returnTo is REPLACE } object INSERT : Mode { From e5f14a5afb910d1235d443b4df16777c0e6587da Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Fri, 27 Dec 2024 23:10:43 +0000 Subject: [PATCH 04/16] Reinstate showmode tests --- .../idea/vim/ui/widgets/mode/VimModeWidget.kt | 90 ++--- .../ideavim/command/VimShowModeTest.kt | 328 ++++++++++-------- 2 files changed, 220 insertions(+), 198 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt index fe28dd7a06..ea04822eeb 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt +++ b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt @@ -31,7 +31,7 @@ import javax.swing.JComponent import kotlin.math.max class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarWidget { - private companion object { + companion object { private const val INSERT = "INSERT" private const val NORMAL = "NORMAL" private const val INSERT_NORMAL = "(insert)" @@ -46,6 +46,50 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW private const val SELECT = "SELECT" private const val SELECT_LINE = "S-LINE" private const val SELECT_BLOCK = "S-BLOCK" + + fun getModeText(mode: Mode?): String? { + return when (mode) { + Mode.INSERT -> INSERT + Mode.REPLACE -> REPLACE + is Mode.NORMAL -> getNormalModeText(mode) + is Mode.CMD_LINE -> COMMAND + is Mode.VISUAL -> getVisualModeText(mode) + is Mode.SELECT -> getSelectModeText(mode) + is Mode.OP_PENDING, null -> null + } + } + + private fun getNormalModeText(mode: Mode.NORMAL) = when { + mode.isInsertPending -> INSERT_NORMAL + mode.isReplacePending -> REPLACE_NORMAL + else -> NORMAL + } + + private fun getVisualModeText(mode: Mode.VISUAL): String { + val prefix = when { + mode.isInsertPending -> INSERT_PENDING_PREFIX + mode.isReplacePending -> REPLACE_PENDING_PREFIX + else -> "" + } + return prefix + when (mode.selectionType) { + SelectionType.CHARACTER_WISE -> VISUAL + SelectionType.LINE_WISE -> VISUAL_LINE + SelectionType.BLOCK_WISE -> VISUAL_BLOCK + } + } + + private fun getSelectModeText(mode: Mode.SELECT): String { + val prefix = when { + mode.isInsertPending -> INSERT_PENDING_PREFIX + mode.isReplacePending -> REPLACE_PENDING_PREFIX + else -> "" + } + return prefix + when (mode.selectionType) { + SelectionType.CHARACTER_WISE -> SELECT + SelectionType.LINE_WISE -> SELECT_LINE + SelectionType.BLOCK_WISE -> SELECT_BLOCK + } + } } private val label = JBLabelWiderThan(setOf(REPLACE)).apply { isOpaque = true } @@ -97,50 +141,6 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW return fileEditorManager.selectedTextEditor } - private fun getModeText(mode: Mode?): String? { - return when (mode) { - Mode.INSERT -> INSERT - Mode.REPLACE -> REPLACE - is Mode.NORMAL -> getNormalModeText(mode) - is Mode.CMD_LINE -> COMMAND - is Mode.VISUAL -> getVisualModeText(mode) - is Mode.SELECT -> getSelectModeText(mode) - is Mode.OP_PENDING, null -> null - } - } - - private fun getNormalModeText(mode: Mode.NORMAL) = when { - mode.isInsertPending -> INSERT_NORMAL - mode.isReplacePending -> REPLACE_NORMAL - else -> NORMAL - } - - private fun getVisualModeText(mode: Mode.VISUAL): String { - val prefix = when { - mode.isInsertPending -> INSERT_PENDING_PREFIX - mode.isReplacePending -> REPLACE_PENDING_PREFIX - else -> "" - } - return prefix + when (mode.selectionType) { - SelectionType.CHARACTER_WISE -> VISUAL - SelectionType.LINE_WISE -> VISUAL_LINE - SelectionType.BLOCK_WISE -> VISUAL_BLOCK - } - } - - private fun getSelectModeText(mode: Mode.SELECT): String { - val prefix = when { - mode.isInsertPending -> INSERT_PENDING_PREFIX - mode.isReplacePending -> REPLACE_PENDING_PREFIX - else -> "" - } - return prefix + when (mode.selectionType) { - SelectionType.CHARACTER_WISE -> SELECT - SelectionType.LINE_WISE -> SELECT_LINE - SelectionType.BLOCK_WISE -> SELECT_BLOCK - } - } - private class JBLabelWiderThan(private val words: Collection): JBLabel("", CENTER) { private val wordWidth: Int get() { diff --git a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt index 5852739e26..106ba728d4 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt @@ -8,162 +8,184 @@ package org.jetbrains.plugins.ideavim.command -import com.intellij.openapi.wm.WindowManager -import com.maddyhome.idea.vim.ui.widgets.mode.ModeWidgetFactory +import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.ui.widgets.mode.VimModeWidget +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals -// TODO it would be cool to test widget, but status bar is not initialized +@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) class VimShowModeTest : VimTestCase() { -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in normal`() { -// configureByText("123") -// val widget = getModeWidget() -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in insert`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("i")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- INSERT --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in replace`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("R")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- REPLACE --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in visual`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("v")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- VISUAL --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in visual line`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("V")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- VISUAL LINE --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in visual block`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- VISUAL BLOCK --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in select`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("gh")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- SELECT --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in select line`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("gH")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- SELECT LINE --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in select block`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("g")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- SELECT BLOCK --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("i")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command visual`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("iv")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) VISUAL --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command visual block`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("i")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) VISUAL BLOCK --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command visual line`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("iV")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) VISUAL LINE --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command select`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("igh")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) SELECT --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command select block`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("ig")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) SELECT BLOCK --", statusString) -// } -// -// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING) -// @Test -// fun `test status string in one command select line`() { -// configureByText("123") -// typeText(injector.parser.parseKeys("igH")) -// val statusString = fixture.editor.vim.getStatusString() -// kotlin.test.assertEquals("-- (insert) SELECT LINE --", statusString) -// } -// - - // Always return null - private fun getModeWidget(): VimModeWidget? { - val project = fixture.editor?.project ?: return null - val statusBar = WindowManager.getInstance()?.getStatusBar(project) ?: return null - return statusBar.getWidget(ModeWidgetFactory.ID) as? VimModeWidget + @Test + fun `test status string in normal`() { + configureByText("123") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("NORMAL", statusString) + } + + @Test + fun `test status string in insert`() { + configureByText("123") + typeText("i") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("INSERT", statusString) + } + + @Test + fun `test status string in replace`() { + configureByText("123") + typeText("R") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("REPLACE", statusString) + } + + @Test + fun `test status string in visual`() { + configureByText("123") + typeText("v") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("VISUAL", statusString) + } + + @Test + fun `test status string in visual line`() { + configureByText("123") + typeText("V") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("V-LINE", statusString) + } + + @Test + fun `test status string in visual block`() { + configureByText("123") + typeText("") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("V-BLOCK", statusString) + } + + @Test + fun `test status string in select`() { + configureByText("123") + typeText("gh") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("SELECT", statusString) + } + + @Test + fun `test status string in select line`() { + configureByText("123") + typeText("gH") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("S-LINE", statusString) + } + + @Test + fun `test status string in select block`() { + configureByText("123") + typeText("g") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("S-BLOCK", statusString) + } + + @Test + fun `test status string for Insert Normal mode`() { + configureByText("123") + typeText("i") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert)", statusString) + } + + @Test + fun `test status string for Replace pending Normal mode`() { + configureByText("123") + typeText("R") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(replace)", statusString) + } + + @Test + fun `test status string in Insert Visual mode`() { + configureByText("123") + typeText("iv") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) VISUAL", statusString) + } + + @Test + fun `test status string in Replace pending Visual mode`() { + configureByText("123") + typeText("Rv") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(replace) VISUAL", statusString) + } + + @Test + fun `test status string in Insert Visual block mode`() { + configureByText("123") + typeText("i") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) V-BLOCK", statusString) + } + + @Test + fun `test status string in Replace pending Visual block mode`() { + configureByText("123") + typeText("R") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(replace) V-BLOCK", statusString) + } + + @Test + fun `test status string in Insert Visual line mode`() { + configureByText("123") + typeText("iV") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) V-LINE", statusString) + } + + @Test + fun `test status string in Replace pending Visual line mode`() { + configureByText("123") + typeText("RV") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(replace) V-LINE", statusString) + } + + @Test + fun `test status string in Insert Select mode`() { + configureByText("123") + typeText("igh") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) SELECT", statusString) + } + + // TODO: Not currently working + @Disabled + @Test + fun `test status string in Insert Select mode 2`() { + configureByText("123") + enterCommand("set selectmode=key keymodel=startsel") + typeText("i") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) SELECT", statusString) + } + + @Test + fun `test status string in Insert Select block mode`() { + configureByText("123") + typeText("ig") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) S-BLOCK", statusString) + } + + @Test + fun `test status string in Insert Select line mode`() { + configureByText("123") + typeText("igH") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(insert) S-LINE", statusString) } } From efcedbcc7f418d625c8493e410d1c57408cd4201 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Fri, 27 Dec 2024 23:36:51 +0000 Subject: [PATCH 05/16] Improve isEndAllowed --- .../maddyhome/idea/vim/helper/EngineHelper.kt | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt index ad72aa6db5..b1a39ba4f4 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt @@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.options.OptionConstants import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType -import com.maddyhome.idea.vim.state.mode.isSingleModeActive import java.util.* inline fun > noneOfEnum(): EnumSet = EnumSet.noneOf(T::class.java) @@ -32,13 +31,27 @@ val VimEditor.usesVirtualSpace: Boolean val VimEditor.isEndAllowed: Boolean get() = this.isEndAllowed(this.mode) +/** + * Returns true if the end of line character is allowed as part of motion or selection + * + * This is mostly needed for the `$` motion, which can behave differently in different modes, and isn't explicitly + * documented. The motion is really only valid in Normal and Visual modes. In Normal, it moves to the last character of + * the current line, not including the end of line character. In Visual (as documented) it moves to the end of line + * char. + * + * The motion obviously doesn't work in Insert or Replace modes, but requires `` to enter "Insert Normal" mode. + * In this case, `$` should move to the end of line char, just like in insert/replace mode. AIUI, this is because Vim + * will switch to Normal mode with ``, set the current column to the "end of line" magic value, return to insert or + * replace, and then finally update the screen. Because the update happens in Insert/Replace, the "Insert Normal" + * position for "end of line" becomes the end of line char. + */ fun VimEditor.isEndAllowed(mode: Mode): Boolean { + // Technically, we should look at the "ultimate" current mode and skip anything like Command-line or Operator-pending, + // but for our usages, this isn't necessary return when (mode) { - is Mode.INSERT, is Mode.VISUAL, is Mode.SELECT -> true - is Mode.NORMAL, is Mode.CMD_LINE, Mode.REPLACE, is Mode.OP_PENDING -> { - // One day we'll use a proper insert_normal mode - if (mode.isSingleModeActive) true else usesVirtualSpace - } + Mode.INSERT, Mode.REPLACE, is Mode.VISUAL, is Mode.SELECT -> true + is Mode.NORMAL -> if (mode.isInsertPending || mode.isReplacePending) true else usesVirtualSpace + is Mode.CMD_LINE, is Mode.OP_PENDING -> usesVirtualSpace } } From 9cf2d465c9d08442ecdcb0a2e59c8a1ca13c3453 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Fri, 27 Dec 2024 23:57:02 +0000 Subject: [PATCH 06/16] Only show mode widget popup on left mouse click --- .../idea/vim/ui/widgets/mode/VimModeWidget.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt index ea04822eeb..5a0d172bb4 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt +++ b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt @@ -28,6 +28,7 @@ import java.awt.Point import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import javax.swing.JComponent +import javax.swing.SwingUtilities import kotlin.math.max class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarWidget { @@ -100,14 +101,20 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW label.addMouseListener(object : MouseAdapter() { override fun mouseClicked(e: MouseEvent) { - val popup = ModeWidgetPopup.createPopup() ?: return - val dimension = popup.content.preferredSize - - val widgetLocation = e.component.locationOnScreen - popup.show(RelativePoint(Point( - widgetLocation.x + e.component.width - dimension.width, - widgetLocation.y - dimension.height, - ))) + if (SwingUtilities.isLeftMouseButton(e)) { + val popup = ModeWidgetPopup.createPopup() ?: return + val dimension = popup.content.preferredSize + + val widgetLocation = e.component.locationOnScreen + popup.show( + RelativePoint( + Point( + widgetLocation.x + e.component.width - dimension.width, + widgetLocation.y - dimension.height, + ) + ) + ) + } } }) } From f06062fc7527e4b4acfb24ad7607e5d9dfbe53bd Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Sat, 28 Dec 2024 11:24:42 +0000 Subject: [PATCH 07/16] Return to Insert when leaving Insert Normal --- .../insert/InsertNormalExitModeActionTest.kt | 20 ++++++++++++ .../ideavim/command/VimShowModeTest.kt | 8 +++++ .../vim/key/consumers/EditorResetConsumer.kt | 31 ++++++++++++------- 3 files changed, 47 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/jetbrains/plugins/ideavim/action/change/insert/InsertNormalExitModeActionTest.kt diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/change/insert/InsertNormalExitModeActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/change/insert/InsertNormalExitModeActionTest.kt new file mode 100644 index 0000000000..65bba9ac3e --- /dev/null +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/change/insert/InsertNormalExitModeActionTest.kt @@ -0,0 +1,20 @@ +/* + * 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 org.jetbrains.plugins.ideavim.action.change.insert + +import com.maddyhome.idea.vim.state.mode.Mode +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.jupiter.api.Test + +class InsertNormalExitModeActionTest : VimTestCase() { + @Test + fun `test exit insert normal mode`() { + doTest("i", "12${c}3", "12${c}3", Mode.INSERT) + } +} diff --git a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt index 106ba728d4..929e013ea7 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt @@ -98,6 +98,14 @@ class VimShowModeTest : VimTestCase() { assertEquals("(insert)", statusString) } + @Test + fun `test status string after escape out of Insert Normal mode`() { + configureByText("123") + typeText("i") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("INSERT", statusString) + } + @Test fun `test status string for Replace pending Normal mode`() { configureByText("123") diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt index d87b81a1cd..05fdd82c14 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/EditorResetConsumer.kt @@ -60,19 +60,26 @@ class EditorResetConsumer : KeyConsumer { if (commandBuilder.isEmpty) { val register = injector.registerGroup if (register.currentRegister == register.defaultRegister) { - var indicateError = true - if (key.keyCode == KeyEvent.VK_ESCAPE) { - val executed = arrayOf(null) - injector.actionExecutor.executeCommand( - editor, - { executed[0] = injector.actionExecutor.executeEsc(editor, context) }, - "", - null, - ) - indicateError = !executed[0]!! + // Escape should exit "Insert Normal" mode. We don't have a handler for in Normal mode, so we do it here + val mode = editor.mode + if (mode is Mode.NORMAL && (mode.isInsertPending || mode.isReplacePending)) { + editor.mode = mode.returnTo } - if (indicateError) { - injector.messages.indicateError() + else { + var indicateError = true + if (key.keyCode == KeyEvent.VK_ESCAPE) { + val executed = arrayOf(null) + injector.actionExecutor.executeCommand( + editor, + { executed[0] = injector.actionExecutor.executeEsc(editor, context) }, + "", + null, + ) + indicateError = !executed[0]!! + } + if (indicateError) { + injector.messages.indicateError() + } } } } From a9bd5f0ffab52083958a87b69ebf94c16047f2a9 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Sun, 29 Dec 2024 00:59:59 +0000 Subject: [PATCH 08/16] Enter Insert Visual/Select mode with shifted key --- .../MotionShiftLeftActionHandlerTest.kt | 112 ++++++++++++++++++ .../MotionShiftRightActionHandlerTest.kt | 112 ++++++++++++++++++ .../MotionShiftDownActionHandlerTest.kt | 112 ++++++++++++++++++ .../updown/MotionShiftUpActionHandlerTest.kt | 112 ++++++++++++++++++ .../ideavim/command/VimShowModeTest.kt | 3 - .../motion/select/SelectToggleVisualMode.kt | 3 +- .../visual/VisualToggleBlockModeAction.kt | 3 +- .../visual/VisualToggleCharacterModeAction.kt | 3 +- .../idea/vim/api/VimVisualMotionGroup.kt | 22 +++- .../idea/vim/api/VimVisualMotionGroupBase.kt | 17 +-- .../idea/vim/handler/SpecialKeyHandlers.kt | 5 +- .../maddyhome/idea/vim/helper/EngineHelper.kt | 4 - 12 files changed, 481 insertions(+), 27 deletions(-) diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt index 53090b45c3..9ebd6f2bb5 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftLeftActionHandlerTest.kt @@ -79,6 +79,62 @@ class MotionShiftLeftActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test visual shift left in Insert enters Insert Visual mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I foun${c}d it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I fou${s}${c}nd${se} it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test visual shift left in Replace enters Replace Visual mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I foun${c}d it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I fou${s}${c}nd${se} it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), @@ -135,6 +191,62 @@ class MotionShiftLeftActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test select shift left in Insert enters Insert Visual mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I foun${c}d it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I fou${s}${c}n${se}d it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test select shift left in Replace enters Replace Visual mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I foun${c}d it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I fou${s}${c}n${se}d it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_continueselect]), diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftRightActionHandlerTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftRightActionHandlerTest.kt index 0c0c13ba57..92bd659943 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftRightActionHandlerTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionShiftRightActionHandlerTest.kt @@ -79,6 +79,62 @@ class MotionShiftRightActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test visual shift right in Insert mode enters Insert Visual mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}f${c}o${se}und it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test visual shift right in Replace mode enters Replace Visual mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}f${c}o${se}und it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), @@ -135,6 +191,62 @@ class MotionShiftRightActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test select shift right in Insert enters Insert Select mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}f${c}${se}ound it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test select shift right in Replace enters Replace Select mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}f${c}${se}ound it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_continueselect]), diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftDownActionHandlerTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftDownActionHandlerTest.kt index 8b043b634f..223c4d8c7f 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftDownActionHandlerTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftDownActionHandlerTest.kt @@ -107,6 +107,62 @@ class MotionShiftDownActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test Visual shift down in Insert mode enters Insert Visual mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}found it in a legendary land + al${c}l${se} rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test Visual shift down in Replace mode enters Replace Visual mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}found it in a legendary land + al${c}l${se} rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), @@ -163,6 +219,62 @@ class MotionShiftDownActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test Select shift down in Insert mode enters Insert Select mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}found it in a legendary land + al${c}${se}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test Select shift down in Replace mode enters Replace Select mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I ${c}found it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}found it in a legendary land + al${c}${se}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_continueselect]), diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftUpActionHandlerTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftUpActionHandlerTest.kt index 91e08f3fe4..ab2e870535 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftUpActionHandlerTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/updown/MotionShiftUpActionHandlerTest.kt @@ -107,6 +107,62 @@ class MotionShiftUpActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test Visual shift up in Insert mode enters Insert Visual mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I found it in a legendary land + al${c}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}${c}found it in a legendary land + all${se} rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [""]), + ) + fun `test Visual shift up in Replace mode enters Replace Visual mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I found it in a legendary land + al${c}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}${c}found it in a legendary land + all${se} rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), @@ -163,6 +219,62 @@ class MotionShiftUpActionHandlerTest : VimTestCase() { ) } + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test Select shift up in Insert mode enters Insert Select mode`() { + doTest( + listOf("i"), + """ + A Discovery + + I found it in a legendary land + al${c}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}${c}found it in a legendary land + al${se}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.INSERT), + ) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @OptionTest( + VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_startsel]), + VimOption(TestOptionConstants.selectmode, limitedValues = [OptionConstants.selectmode_key]), + ) + fun `test Select shift up in Replace mode enters Replace Select mode`() { + doTest( + listOf("R"), + """ + A Discovery + + I found it in a legendary land + al${c}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + """ + A Discovery + + I ${s}${c}found it in a legendary land + al${se}l rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent(), + Mode.SELECT(SelectionType.CHARACTER_WISE, returnTo = Mode.REPLACE), + ) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @OptionTest( VimOption(TestOptionConstants.keymodel, limitedValues = [OptionConstants.keymodel_continueselect]), diff --git a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt index 929e013ea7..917ad43221 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt @@ -13,7 +13,6 @@ import com.maddyhome.idea.vim.ui.widgets.mode.VimModeWidget import org.jetbrains.plugins.ideavim.SkipNeovimReason import org.jetbrains.plugins.ideavim.TestWithoutNeovim import org.jetbrains.plugins.ideavim.VimTestCase -import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -170,8 +169,6 @@ class VimShowModeTest : VimTestCase() { assertEquals("(insert) SELECT", statusString) } - // TODO: Not currently working - @Disabled @Test fun `test status string in Insert Select mode 2`() { configureByText("123") diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt index 48848289a6..0728a0fd52 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt @@ -17,7 +17,6 @@ import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.pushVisualMode -import com.maddyhome.idea.vim.helper.setSelectMode import com.maddyhome.idea.vim.state.mode.SelectionType /** @@ -43,7 +42,7 @@ class SelectToggleVisualMode : VimActionHandler.SingleExecution() { fun toggleMode(editor: VimEditor) { val myMode = editor.mode if (myMode is com.maddyhome.idea.vim.state.mode.Mode.VISUAL) { - editor.setSelectMode(myMode.selectionType) + injector.visualMotionGroup.enterSelectMode(editor, myMode.selectionType) if (myMode.selectionType != SelectionType.LINE_WISE) { editor.nativeCarets().forEach { if (it.offset + injector.visualMotionGroup.selectionAdj == it.selectionEnd) { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt index f468acf27c..ed5df92c16 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleBlockModeAction.kt @@ -32,8 +32,7 @@ class VisualToggleBlockModeAction : VimActionHandler.SingleExecution() { return if (injector.options(editor).selectmode.contains(OptionConstants.selectmode_cmd)) { injector.visualMotionGroup.enterSelectMode(editor, SelectionType.BLOCK_WISE) } else { - injector.visualMotionGroup - .toggleVisual(editor, cmd.count, cmd.rawCount, SelectionType.BLOCK_WISE) + injector.visualMotionGroup.toggleVisual(editor, cmd.count, cmd.rawCount, SelectionType.BLOCK_WISE) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt index deaa3e2b28..1056866990 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleCharacterModeAction.kt @@ -32,8 +32,7 @@ class VisualToggleCharacterModeAction : VimActionHandler.SingleExecution() { return if (injector.options(editor).selectmode.contains(OptionConstants.selectmode_cmd)) { injector.visualMotionGroup.enterSelectMode(editor, SelectionType.CHARACTER_WISE) } else { - injector.visualMotionGroup - .toggleVisual(editor, cmd.count, cmd.rawCount, SelectionType.CHARACTER_WISE) + injector.visualMotionGroup.toggleVisual(editor, cmd.count, cmd.rawCount, SelectionType.CHARACTER_WISE) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index fe83144532..78585ffaac 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -18,20 +18,32 @@ interface VimVisualMotionGroup { /** * This function toggles visual mode. * - * If visual mode is disabled, enable it - * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] - * If visual mode is enabled with the same [selectionType], disable it + * * If visual mode is disabled, enable it + * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] + * * If visual mode is enabled with the same [selectionType], disable it */ fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean - fun enterSelectMode(editor: VimEditor, subMode: SelectionType): Boolean + + /** + * Enter Select mode with the given selection type + * + * When used from Normal, Insert or Replace modes, it will enter Select mode using the current mode as the "return to" + * mode. I.e., if entered from Normal, will return to Normal. If entered from Insert or Replace (via shifted keys) + * will return to Insert or Replace (aka "Insert Select" mode). + * + * While it will toggle between Visual and Select modes, it doesn't update the character positions correctly. IdeaVim + * treats Select mode as exclusive and adjusts the character position when toggling modes. + */ + fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean /** * Enters visual mode based on current editor state. + * * If [subMode] is null, subMode will be detected automatically * * it: * - Updates command state - * - Updates [vimSelectionStart] property + * - Updates [VimCaret.vimSelectionStart] property * - Updates caret colors * - Updates care shape * diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index 045add2be1..034783700e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.group.visual.vimSetSelection import com.maddyhome.idea.vim.group.visual.vimUpdateEditorSelection import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.helper.pushVisualMode -import com.maddyhome.idea.vim.helper.setSelectMode import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.inVisualMode @@ -27,8 +26,15 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { override val selectionAdj: Int get() = if (exclusiveSelection) 0 else 1 - override fun enterSelectMode(editor: VimEditor, subMode: SelectionType): Boolean { - editor.setSelectMode(subMode) + override fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean { + // If we're already in Select or toggling from Visual, replace the current mode (keep the existing returnTo), + // otherwise push Select, using the current mode as returnTo. + // If we're entering from Normal, use its own returnTo, as this will handle both Normal and "Internal Normal" + editor.mode = when (editor.mode) { + is Mode.SELECT, is Mode.VISUAL -> Mode.SELECT(selectionType, editor.mode.returnTo) + is Mode.NORMAL -> Mode.SELECT(selectionType, editor.mode.returnTo) + else -> Mode.SELECT(selectionType, editor.mode) + } editor.forEachCaret { it.vimSelectionStart = it.vimLeadSelectionOffset } return true } @@ -67,10 +73,7 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { it.vimLastColumn = intendedColumn } } else { - editor.mode = Mode.VISUAL( - selectionType, - returnTo ?: editor.mode.returnTo - ) + editor.mode = Mode.VISUAL(selectionType, returnTo ?: editor.mode.returnTo) editor.forEachCaret { it.vimSetSelection(it.offset) } } return true diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt index e0e8064a99..79a83f9b12 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt @@ -19,7 +19,6 @@ import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.helper.exitVisualMode import com.maddyhome.idea.vim.options.OptionConstants -import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.isInsertionAllowed import com.maddyhome.idea.vim.state.mode.inSelectMode @@ -94,10 +93,12 @@ abstract class ShiftedArrowKeyHandler(private val runBothCommandsAsMulticaret: B if (withKey) { if (!inVisualMode && !inSelectMode) { if (injector.globalOptions().selectmode.contains(OptionConstants.selectmode_key)) { + // Note that this will correctly choose either Select or Insert Select modes injector.visualMotionGroup.enterSelectMode(editor, SelectionType.CHARACTER_WISE) } else { if (editor.isInsertionAllowed) { - injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE, Mode.INSERT) + // Enter Insert/Replace Visual mode + injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE, editor.mode) } else { injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt index b1a39ba4f4..131392d95b 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt @@ -61,10 +61,6 @@ inline fun > enumSetOf(vararg value: T): EnumSet = when ( else -> EnumSet.of(value[0], *value.slice(1..value.lastIndex).toTypedArray()) } -fun VimEditor.setSelectMode(submode: SelectionType) { - mode = Mode.SELECT(submode, mode.returnTo) -} - fun VimEditor.pushVisualMode(submode: SelectionType) { mode = Mode.VISUAL(submode, mode.returnTo) } From a17b1993f9264e9828db0c3ae27312ac7a632a90 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Sun, 29 Dec 2024 23:58:08 +0000 Subject: [PATCH 09/16] Rename submode to selectionType --- .../maddyhome/idea/vim/group/copy/PutGroup.kt | 4 ++-- .../vim/group/visual/IdeaSelectionControl.kt | 10 ++++----- .../vim/group/visual/VisualMotionGroup.kt | 4 ++-- .../options/helpers/IdeaRefactorModeHelper.kt | 9 ++++---- .../idea/vim/api/VimVisualMotionGroup.kt | 6 ++--- .../idea/vim/api/VimVisualMotionGroupBase.kt | 22 +++++++++---------- .../vim/group/visual/EngineVisualGroup.kt | 4 ++-- .../idea/vim/group/visual/VisualChange.kt | 8 +++---- .../idea/vim/handler/MotionActionHandler.kt | 4 ++-- .../maddyhome/idea/vim/helper/EngineHelper.kt | 4 ++-- .../com/maddyhome/idea/vim/put/VimPut.kt | 2 +- .../com/maddyhome/idea/vim/put/VimPutBase.kt | 10 ++++----- 12 files changed, 42 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/group/copy/PutGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/copy/PutGroup.kt index 40c752bc20..6203e26dd4 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/copy/PutGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/copy/PutGroup.kt @@ -80,7 +80,7 @@ internal class PutGroup : VimPutBase() { vimEditor: VimEditor, vimContext: ExecutionContext, text: ProcessedTextData, - subMode: SelectionType, + selectionType: SelectionType, data: PutData, additionalData: Map, ) { @@ -148,7 +148,7 @@ internal class PutGroup : VimPutBase() { startOffset, endOffset, text.typeInRegister, - subMode, + selectionType, data.caretAfterInsertedText, ) } diff --git a/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt b/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt index d0ccc181d3..07e4a881c4 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/visual/IdeaSelectionControl.kt @@ -157,23 +157,23 @@ internal object IdeaSelectionControl { return when { editor.isOneLineMode -> { if (logReason) logger.debug("Enter select mode. Reason: one line mode") - Mode.SELECT(VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)) + Mode.SELECT(VimPlugin.getVisualMotion().detectSelectionType(editor.vim)) } selectionSource == VimListenerManager.SelectionSource.MOUSE && OptionConstants.selectmode_mouse in selectmode -> { if (logReason) logger.debug("Enter select mode. Selection source is mouse and selectMode option has mouse") - Mode.SELECT(VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)) + Mode.SELECT(VimPlugin.getVisualMotion().detectSelectionType(editor.vim)) } editor.isTemplateActive() && editor.vim.isIdeaRefactorModeSelect -> { if (logReason) logger.debug("Enter select mode. Template is active and selectMode has template") - Mode.SELECT(VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)) + Mode.SELECT(VimPlugin.getVisualMotion().detectSelectionType(editor.vim)) } selectionSource == VimListenerManager.SelectionSource.OTHER && OptionConstants.selectmode_ideaselection in selectmode -> { if (logReason) logger.debug("Enter select mode. Selection source is OTHER and selectMode has refactoring") - Mode.SELECT(VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)) + Mode.SELECT(VimPlugin.getVisualMotion().detectSelectionType(editor.vim)) } else -> { if (logReason) logger.debug("Enter visual mode") - Mode.VISUAL(VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim)) + Mode.VISUAL(VimPlugin.getVisualMotion().detectSelectionType(editor.vim)) } } } diff --git a/src/main/java/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt index 798dd5a8a3..02f185fb76 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/visual/VisualMotionGroup.kt @@ -18,13 +18,13 @@ import com.maddyhome.idea.vim.state.mode.SelectionType * @author Alex Plate */ internal class VisualMotionGroup : VimVisualMotionGroupBase() { - override fun autodetectVisualSubmode(editor: VimEditor): SelectionType { + override fun detectSelectionType(editor: VimEditor): SelectionType { // IJ specific. See https://youtrack.jetbrains.com/issue/VIM-1924. val project = editor.ij.project if (project != null && FindManager.getInstance(project).selectNextOccurrenceWasPerformed()) { return SelectionType.CHARACTER_WISE } - return super.autodetectVisualSubmode(editor) + return super.detectSelectionType(editor) } } diff --git a/src/main/java/com/maddyhome/idea/vim/vimscript/model/options/helpers/IdeaRefactorModeHelper.kt b/src/main/java/com/maddyhome/idea/vim/vimscript/model/options/helpers/IdeaRefactorModeHelper.kt index 358a17fe4d..ee1d2dbcfc 100644 --- a/src/main/java/com/maddyhome/idea/vim/vimscript/model/options/helpers/IdeaRefactorModeHelper.kt +++ b/src/main/java/com/maddyhome/idea/vim/vimscript/model/options/helpers/IdeaRefactorModeHelper.kt @@ -90,12 +90,11 @@ internal object IdeaRefactorModeHelper { corrections.add(Action.RemoveSelection) } if (mode.hasVisualSelection && editor.selectionModel.hasSelection()) { - val autodetectedSubmode = VimPlugin.getVisualMotion().autodetectVisualSubmode(editor.vim) - if (mode.selectionType != autodetectedSubmode) { - // Update the submode + val selectionType = VimPlugin.getVisualMotion().detectSelectionType(editor.vim) + if (mode.selectionType != selectionType) { val newMode = when (mode) { - is Mode.SELECT -> mode.copy(selectionType = autodetectedSubmode) - is Mode.VISUAL -> mode.copy(selectionType = autodetectedSubmode) + is Mode.SELECT -> mode.copy(selectionType) + is Mode.VISUAL -> mode.copy(selectionType) else -> error("IdeaVim should be either in visual or select modes") } corrections.add(Action.SetMode(newMode)) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index 78585ffaac..eddbe2db66 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -39,7 +39,7 @@ interface VimVisualMotionGroup { /** * Enters visual mode based on current editor state. * - * If [subMode] is null, subMode will be detected automatically + * If [selectionType] is null, it will be detected automatically * * it: * - Updates command state @@ -51,6 +51,6 @@ interface VimVisualMotionGroup { * - DOES NOT move caret * - DOES NOT check if carets actually have any selection */ - fun enterVisualMode(editor: VimEditor, subMode: SelectionType? = null): Boolean - fun autodetectVisualSubmode(editor: VimEditor): SelectionType + fun enterVisualMode(editor: VimEditor, selectionType: SelectionType? = null): Boolean + fun detectSelectionType(editor: VimEditor): SelectionType } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index 034783700e..3c8cbdea25 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -54,10 +54,9 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { returnTo: Mode? ): Boolean { if (!editor.inVisualMode) { - // Enable visual subMode if (rawCount > 0) { - val primarySubMode = editor.primaryCaret().vimLastVisualOperatorRange?.type ?: selectionType - editor.pushVisualMode(primarySubMode) + val primarySelectionType = editor.primaryCaret().vimLastVisualOperatorRange?.type ?: selectionType + editor.pushVisualMode(primarySelectionType) editor.forEachCaret { val range = it.vimLastVisualOperatorRange ?: VisualChange.default(selectionType) @@ -80,7 +79,6 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { } if (selectionType == editor.mode.selectionType) { - // Disable visual subMode editor.exitVisualMode() return true } @@ -88,7 +86,6 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { val mode = editor.mode check(mode is Mode.VISUAL) - // Update visual subMode with new sub subMode editor.mode = mode.copy(selectionType = selectionType) for (caret in editor.carets()) { if (!caret.isValid) continue @@ -126,7 +123,7 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { return true } - override fun autodetectVisualSubmode(editor: VimEditor): SelectionType { + override fun detectSelectionType(editor: VimEditor): SelectionType { if (editor.carets().size > 1 && seemsLikeBlockMode(editor)) { return SelectionType.BLOCK_WISE } @@ -147,7 +144,8 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { /** * Enters visual mode based on current editor state. - * If [subMode] is null, subMode will be detected automatically + * + * If [selectionType] is null, it will be detected automatically * * it: * - Updates command state @@ -159,14 +157,14 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { * - DOES NOT move caret * - DOES NOT check if carets actually have any selection */ - override fun enterVisualMode(editor: VimEditor, subMode: SelectionType?): Boolean { - val autodetectedSubMode = subMode ?: autodetectVisualSubmode(editor) - editor.mode = Mode.VISUAL(autodetectedSubMode) - // editor.vimStateMachine.setMode(VimStateMachine.Mode.VISUAL, autodetectedSubMode) + override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType?): Boolean { + val newSelectionType = selectionType ?: detectSelectionType(editor) + + editor.mode = Mode.VISUAL(newSelectionType) // vimLeadSelectionOffset requires read action injector.application.runReadAction { - if (autodetectedSubMode == SelectionType.BLOCK_WISE) { + if (newSelectionType == SelectionType.BLOCK_WISE) { editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset } } else { editor.nativeCarets().forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt index 306ab35c29..a7dccc9078 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/EngineVisualGroup.kt @@ -27,9 +27,9 @@ import com.maddyhome.idea.vim.state.mode.selectionType fun setVisualSelection(selectionStart: Int, selectionEnd: Int, caret: VimCaret) { val (start, end) = if (selectionStart > selectionEnd) selectionEnd to selectionStart else selectionStart to selectionEnd val editor = caret.editor - val subMode = editor.mode.selectionType ?: SelectionType.CHARACTER_WISE + val selectionType = editor.mode.selectionType ?: SelectionType.CHARACTER_WISE val mode = editor.mode - when (subMode) { + when (selectionType) { SelectionType.CHARACTER_WISE -> { val (nativeStart, nativeEnd) = charToNativeSelection(editor, start, end, mode) caret.vimSetSystemSelectionSilently(nativeStart, nativeEnd) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/VisualChange.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/VisualChange.kt index 2cbe6096ec..2217a7a460 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/VisualChange.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/group/visual/VisualChange.kt @@ -12,10 +12,10 @@ import com.maddyhome.idea.vim.state.mode.SelectionType data class VisualChange(val lines: Int, val columns: Int, val type: SelectionType) { companion object { - fun default(subMode: SelectionType): VisualChange = - when (subMode) { - SelectionType.LINE_WISE, SelectionType.CHARACTER_WISE -> VisualChange(1, 1, subMode) - SelectionType.BLOCK_WISE -> VisualChange(0, 1, subMode) + fun default(selectionType: SelectionType): VisualChange = + when (selectionType) { + SelectionType.LINE_WISE, SelectionType.CHARACTER_WISE -> VisualChange(1, 1, selectionType) + SelectionType.BLOCK_WISE -> VisualChange(0, 1, selectionType) } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt index e927bfeaff..14f0bb3945 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt @@ -132,7 +132,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) { cmd: Command, operatorArguments: OperatorArguments, ): Boolean { - val blockSubmodeActive = editor.inBlockSelection + val blockSelectionActive = editor.inBlockSelection val handler = if (this is AmbiguousExecution) this.getMotionActionHandler(cmd.argument) else this when (handler) { @@ -159,7 +159,7 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) { } is ForEachCaret -> run { when { - blockSubmodeActive || editor.carets().size == 1 -> { + blockSelectionActive || editor.carets().size == 1 -> { val primaryCaret = editor.primaryCaret() handler.doExecuteForEach(editor, primaryCaret, context, cmd, operatorArguments) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt index 131392d95b..b967e0a438 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt @@ -61,6 +61,6 @@ inline fun > enumSetOf(vararg value: T): EnumSet = when ( else -> EnumSet.of(value[0], *value.slice(1..value.lastIndex).toTypedArray()) } -fun VimEditor.pushVisualMode(submode: SelectionType) { - mode = Mode.VISUAL(submode, mode.returnTo) +fun VimEditor.pushVisualMode(selectionType: SelectionType) { + mode = Mode.VISUAL(selectionType, mode.returnTo) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt index bc2f0139be..2db30f07d8 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPut.kt @@ -46,7 +46,7 @@ interface VimPut { vimEditor: VimEditor, vimContext: ExecutionContext, text: ProcessedTextData, - subMode: SelectionType, + selectionType: SelectionType, data: PutData, additionalData: Map, ) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt index 4783f9a9d0..f91b3b6ccc 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/put/VimPutBase.kt @@ -491,9 +491,9 @@ abstract class VimPutBase : VimPut { val startOffsets = prepareDocumentAndGetStartOffsets(editor, updated, text.typeInRegister, data, additionalData) startOffsets.forEach { startOffset -> - val subMode = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE + val selectionType = data.visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE val (endOffset, updatedCaret) = putTextInternal( - editor, updated, context, text.copiedText.text, text.typeInRegister, subMode, + editor, updated, context, text.copiedText.text, text.typeInRegister, selectionType, startOffset, data.count, data.indent, data.caretAfterInsertedText, ) updated = updatedCaret @@ -504,7 +504,7 @@ abstract class VimPutBase : VimPut { startOffset, endOffset, text.typeInRegister, - subMode, + selectionType, data.caretAfterInsertedText, ) } @@ -541,12 +541,12 @@ abstract class VimPutBase : VimPut { additionalData: Map, ) { val visualSelection = data.visualSelection - val subMode = visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE + val selectionType = visualSelection?.typeInEditor ?: SelectionType.CHARACTER_WISE if (injector.globalOptions().clipboard.contains(OptionConstants.clipboard_ideaput)) { val idePasteProvider = getProviderForPasteViaIde(editor, text.typeInRegister, data) if (idePasteProvider != null) { logger.debug("Perform put via idea paste") - putTextViaIde(idePasteProvider, editor, context, text, subMode, data, additionalData) + putTextViaIde(idePasteProvider, editor, context, text, selectionType, data, additionalData) return } } From 03985a3a97d862ae4cf1d69ba26dbe769dd311cf Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Mon, 30 Dec 2024 14:25:37 +0000 Subject: [PATCH 10/16] Implement v_CTRL-O From Select mode, enters Visual for a single command --- ...lectToggleSingleVisualCommandActionTest.kt | 72 +++++++++++++++++++ .../insert/InsertSingleCommandAction.kt | 1 + .../SelectToggleSingleVisualCommandAction.kt | 34 +++++++++ .../motion/select/SelectToggleVisualMode.kt | 3 +- .../idea/vim/api/VimVisualMotionGroup.kt | 12 ++++ .../idea/vim/api/VimVisualMotionGroupBase.kt | 45 ++++++++++-- .../idea/vim/handler/MotionActionHandler.kt | 12 ++++ .../idea/vim/helper/EngineModeExtensions.kt | 10 ++- .../com/maddyhome/idea/vim/state/mode/Mode.kt | 12 +++- .../ksp-generated/engine_commands.json | 5 ++ 10 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 src/test/java/org/jetbrains/plugins/ideavim/action/motion/select/SelectToggleSingleVisualCommandActionTest.kt create mode 100644 vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleSingleVisualCommandAction.kt diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/motion/select/SelectToggleSingleVisualCommandActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/select/SelectToggleSingleVisualCommandActionTest.kt new file mode 100644 index 0000000000..149598a521 --- /dev/null +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/motion/select/SelectToggleSingleVisualCommandActionTest.kt @@ -0,0 +1,72 @@ +/* + * 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 org.jetbrains.plugins.ideavim.action.motion.select + +import com.maddyhome.idea.vim.state.mode.Mode +import com.maddyhome.idea.vim.state.mode.SelectionType +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.jupiter.api.Test + +@Suppress("SpellCheckingInspection") +@TestWithoutNeovim(SkipNeovimReason.SELECT_MODE) +class SelectToggleSingleVisualCommandActionTest : VimTestCase() { + @Test + fun `test enter Visual mode for single command`() { + doTest( + "gh", + "Lorem ${c}ipsum dolor sit amet", + "Lorem ${s}${c}i${se}psum dolor sit amet", + Mode.VISUAL(SelectionType.CHARACTER_WISE, returnTo = Mode.SELECT(SelectionType.CHARACTER_WISE)), + ) + } + + @Test + fun `test enter Visual mode for single motion`() { + doTest( + "ghe", + "Lorem ${c}ipsum dolor sit amet", + "Lorem ${s}ipsum${se}${c} dolor sit amet", + Mode.SELECT(SelectionType.CHARACTER_WISE), + ) + } + + // AFAICT, all Visual operators remove the selection + @Test + fun `test returns to Normal if Visual operator removes selection`() { + doTest( + "ghU", + "Lorem ${c}ipsum dolor sit amet", + "Lorem ${c}Ipsum dolor sit amet", + Mode.NORMAL(), + ) + } + + @Test + fun `test Escape returns to Normal after entering Visual for a single command`() { + // Escape doesn't "pop the stack", but returns to Normal + doTest( + "gh", + "Lorem ${c}ipsum dolor sit amet", + "Lorem ${c}ipsum dolor sit amet", + Mode.NORMAL(), + ) + } + + @Test + fun `test exit Visual mode with same shortcut`() { + doTest( + "gh", + "Lorem ${c}ipsum dolor sit amet", + "Lorem ${s}i${c}${se}psum dolor sit amet", + Mode.SELECT(SelectionType.CHARACTER_WISE), + ) + } +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertSingleCommandAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertSingleCommandAction.kt index 77c0b29cf8..7906cdc0f0 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertSingleCommandAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/insert/InsertSingleCommandAction.kt @@ -19,6 +19,7 @@ import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* +// Remember that Insert mode mappings also apply to Replace @CommandOrMotion(keys = [""], modes = [Mode.INSERT]) class InsertSingleCommandAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.INSERT diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleSingleVisualCommandAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleSingleVisualCommandAction.kt new file mode 100644 index 0000000000..ad51126c55 --- /dev/null +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleSingleVisualCommandAction.kt @@ -0,0 +1,34 @@ +/* + * 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.select + +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.injector +import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.OperatorArguments +import com.maddyhome.idea.vim.handler.VimActionHandler + +// See `:help v_CTRL-O` +@CommandOrMotion(keys = [""], modes = [Mode.SELECT, Mode.VISUAL]) +class SelectToggleSingleVisualCommandAction : VimActionHandler.SingleExecution() { + override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED + + override fun execute( + editor: VimEditor, + context: ExecutionContext, + cmd: Command, + operatorArguments: OperatorArguments, + ): Boolean { + injector.visualMotionGroup.processSingleVisualCommand(editor) + return true + } +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt index 0728a0fd52..321fb6e197 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt @@ -16,7 +16,6 @@ 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 -import com.maddyhome.idea.vim.helper.pushVisualMode import com.maddyhome.idea.vim.state.mode.SelectionType /** @@ -51,7 +50,7 @@ class SelectToggleVisualMode : VimActionHandler.SingleExecution() { } } } else if (myMode is com.maddyhome.idea.vim.state.mode.Mode.SELECT) { - editor.pushVisualMode(myMode.selectionType) + injector.visualMotionGroup.enterVisualMode(editor, myMode.selectionType) if (myMode.selectionType != SelectionType.LINE_WISE) { editor.nativeCarets().forEach { if (it.offset == it.selectionEnd && it.visualLineStart <= it.offset - injector.visualMotionGroup.selectionAdj) { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index eddbe2db66..a16b322437 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -53,4 +53,16 @@ interface VimVisualMotionGroup { */ fun enterVisualMode(editor: VimEditor, selectionType: SelectionType? = null): Boolean fun detectSelectionType(editor: VimEditor): SelectionType + + /** + * When in Select mode, enter Visual mode for a single command + * + * While the Vim docs state that this is for the duration of a single Visual command, it also includes motions. This + * is different to "Insert Visual" mode (`iv`) which allows multiple motions until an operator is invoked. + * + * If already in Visual, this function will return to Select. + * + * See `:help v_CTRL-O`. + */ + fun processSingleVisualCommand(editor: VimEditor) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index 3c8cbdea25..c0fad6d41c 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -8,6 +8,7 @@ package com.maddyhome.idea.vim.api +import com.maddyhome.idea.vim.action.motion.select.SelectToggleVisualMode import com.maddyhome.idea.vim.group.visual.VisualChange import com.maddyhome.idea.vim.group.visual.VisualOperation import com.maddyhome.idea.vim.group.visual.vimLeadSelectionOffset @@ -29,11 +30,14 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { override fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean { // If we're already in Select or toggling from Visual, replace the current mode (keep the existing returnTo), // otherwise push Select, using the current mode as returnTo. - // If we're entering from Normal, use its own returnTo, as this will handle both Normal and "Internal Normal" - editor.mode = when (editor.mode) { - is Mode.SELECT, is Mode.VISUAL -> Mode.SELECT(selectionType, editor.mode.returnTo) - is Mode.NORMAL -> Mode.SELECT(selectionType, editor.mode.returnTo) - else -> Mode.SELECT(selectionType, editor.mode) + // If we're entering from Normal, use its own returnTo, as this will handle both Normal and "Internal Normal". + // And return back to Select if we were originally in Select and entered Visual for a single command (eg `ghe`) + val mode = editor.mode + editor.mode = when { + mode is Mode.VISUAL && mode.isSelectPending -> mode.returnTo + mode is Mode.VISUAL || mode is Mode.SELECT -> Mode.SELECT(selectionType, mode.returnTo) + mode is Mode.NORMAL -> Mode.SELECT(selectionType, mode.returnTo) + else -> Mode.SELECT(selectionType, mode) } editor.forEachCaret { it.vimSelectionStart = it.vimLeadSelectionOffset } return true @@ -172,4 +176,35 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { } return true } + + /** + * When in Select mode, enter Visual mode for a single command + * + * While the Vim docs state that this is for the duration of a single Visual command, it also includes motions. This + * is different to "Insert Visual" mode (`iv`) which allows multiple motions until an operator is invoked. + * + * If already in Visual, this function will return to Select. + * + * See `:help v_CTRL-O`. + */ + override fun processSingleVisualCommand(editor: VimEditor) { + val mode = editor.mode + if (mode is Mode.SELECT) { + editor.mode = Mode.VISUAL(mode.selectionType, returnTo = mode) + // TODO: This is a copy of code from SelectToggleVisualMode.toggleMode. It should be moved to VimVisualMotionGroup + // IdeaVim always treats Select mode as exclusive. This will adjust the caret from exclusive to (potentially) + // inclusive, depending on the value of 'selection' + if (mode.selectionType != SelectionType.LINE_WISE) { + editor.nativeCarets().forEach { + if (it.offset == it.selectionEnd && it.visualLineStart <= it.offset - injector.visualMotionGroup.selectionAdj) { + it.moveToInlayAwareOffset(it.offset - injector.visualMotionGroup.selectionAdj) + } + } + } + } + else if (mode is Mode.VISUAL && mode.isSelectPending) { + // TODO: It would be better to move this to VimVisualMotionGroup + SelectToggleVisualMode.toggleMode(editor) + } + } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt index 14f0bb3945..50b7c60835 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/MotionActionHandler.kt @@ -275,6 +275,18 @@ sealed class MotionActionHandler : EditorActionHandlerBase(false) { return resultOffset } + override fun postExecute( + editor: VimEditor, + context: ExecutionContext, + cmd: Command, + operatorArguments: OperatorArguments + ) { + // If we're in single-execution Visual mode, return to Select. See `:help v_CTRL-O` + if ((editor.mode as? Mode.VISUAL)?.isSelectPending == true) { + injector.visualMotionGroup.processSingleVisualCommand(editor) + } + } + private object CaretMergingWatcher : VimCaretListener { override fun caretRemoved(caret: ImmutableVimCaret?) { caret ?: return diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt index f0da9d38c7..6a116408c9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineModeExtensions.kt @@ -12,6 +12,7 @@ import com.maddyhome.idea.vim.api.VimCaret import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor +import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.inBlockSelection import com.maddyhome.idea.vim.state.mode.inVisualMode @@ -30,6 +31,13 @@ fun VimEditor.exitVisualMode() { injector.markService.setVisualSelectionMarks(this) nativeCarets().forEach { it.vimSelectionStartClear() } - mode = mode.returnTo + // We usually want to return to the mode that we were in before we started Visual. Typically, this will be NORMAL, + // but can be INSERT for "Insert Visual" (`iv`). For "Select Visual" (`gh`) we can't return to SELECT, + // because we've just removed the selection. We have to return to NORMAL. + val mode = this.mode + this.mode = when { + mode is Mode.VISUAL && mode.isSelectPending -> Mode.NORMAL() + else -> mode.returnTo + } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt index 9faf45c9c9..444fc10510 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/mode/Mode.kt @@ -87,8 +87,9 @@ sealed interface Mode { init { // VISUAL will normally return to NORMAL, but can return to INSERT or REPLACE if i_CTRL-O is followed by `v` // I.e. "Insert Visual mode" and "Replace Visual mode" - require(returnTo is NORMAL || returnTo is INSERT || returnTo is REPLACE) { - "VISUAL mode can be active only in NORMAL, INSERT or REPLACE modes, not ${returnTo.javaClass.simpleName}" + // VISUAL can return to SELECT after `` + require(returnTo is NORMAL || returnTo is INSERT || returnTo is REPLACE || returnTo is SELECT) { + "VISUAL mode can be active only in NORMAL, INSERT, REPLACE or SELECT modes, not ${returnTo.javaClass.simpleName}" } } @@ -106,6 +107,13 @@ sealed interface Mode { * Like "Insert Visual", but starting from (and returning to) Replace (`Rv`). */ val isReplacePending = returnTo is REPLACE + + /** + * Returns true if the mode is temporarily switched from Select to Visual for the duration of one command + * + * See `:help v_CTRL-O` + */ + val isSelectPending = returnTo is SELECT } data class SELECT(val selectionType: SelectionType, override val returnTo: Mode = NORMAL()) : Mode { diff --git a/vim-engine/src/main/resources/ksp-generated/engine_commands.json b/vim-engine/src/main/resources/ksp-generated/engine_commands.json index b157eeeb8b..281d3dfc66 100644 --- a/vim-engine/src/main/resources/ksp-generated/engine_commands.json +++ b/vim-engine/src/main/resources/ksp-generated/engine_commands.json @@ -344,6 +344,11 @@ "class": "com.maddyhome.idea.vim.action.motion.mark.MotionJumpPreviousAction", "modes": "N" }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.select.SelectToggleSingleVisualCommandAction", + "modes": "SX" + }, { "keys": "", "class": "com.maddyhome.idea.vim.action.ex.HistoryUpAction", From 9e6f5820b3230fb401d64689474513bf0941b83a Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 31 Dec 2024 01:58:50 +0000 Subject: [PATCH 11/16] Update mode widget text for Select pending --- .../idea/vim/ui/widgets/mode/VimModeWidget.kt | 21 +++++++++++++++---- .../ideavim/command/VimShowModeTest.kt | 13 ++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt index 5a0d172bb4..67d603ae42 100644 --- a/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt +++ b/src/main/java/com/maddyhome/idea/vim/ui/widgets/mode/VimModeWidget.kt @@ -35,10 +35,8 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW companion object { private const val INSERT = "INSERT" private const val NORMAL = "NORMAL" - private const val INSERT_NORMAL = "(insert)" private const val INSERT_PENDING_PREFIX = "(insert) " private const val REPLACE = "REPLACE" - private const val REPLACE_NORMAL = "(replace)" private const val REPLACE_PENDING_PREFIX = "(replace) " private const val COMMAND = "COMMAND" private const val VISUAL = "VISUAL" @@ -47,6 +45,7 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW private const val SELECT = "SELECT" private const val SELECT_LINE = "S-LINE" private const val SELECT_BLOCK = "S-BLOCK" + private const val SELECT_PENDING_PREFIX = "(select) " fun getModeText(mode: Mode?): String? { return when (mode) { @@ -60,16 +59,30 @@ class VimModeWidget(val project: Project) : CustomStatusBarWidget, VimStatusBarW } } + /** + * Return the text to show in Normal mode + * + * Vim doesn't show any text for Normal, but that doesn't work for IdeaVim's status widget. We also show "NORMAL" + * when Insert or Replace is pending. I.e. "(insert) NORMAL" and "(replace) NORMAL". Vim only shows the pending mode + */ private fun getNormalModeText(mode: Mode.NORMAL) = when { - mode.isInsertPending -> INSERT_NORMAL - mode.isReplacePending -> REPLACE_NORMAL + mode.isInsertPending -> INSERT_PENDING_PREFIX + NORMAL + mode.isReplacePending -> REPLACE_PENDING_PREFIX + NORMAL else -> NORMAL } + /** + * Return the text to show in Visual mode + * + * IdeaVim shows the Insert and Replace pending modes, but we also show Select pending - "(select) VISUAL". Vim + * doesn't show this, but IdeaVim's status widget isn't like Vim's status bar, and this addition keeps things a + * little more consistent. + */ private fun getVisualModeText(mode: Mode.VISUAL): String { val prefix = when { mode.isInsertPending -> INSERT_PENDING_PREFIX mode.isReplacePending -> REPLACE_PENDING_PREFIX + mode.isSelectPending -> SELECT_PENDING_PREFIX else -> "" } return prefix + when (mode.selectionType) { diff --git a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt index 917ad43221..a83ffa07ce 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/command/VimShowModeTest.kt @@ -94,7 +94,7 @@ class VimShowModeTest : VimTestCase() { configureByText("123") typeText("i") val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) - assertEquals("(insert)", statusString) + assertEquals("(insert) NORMAL", statusString) } @Test @@ -110,7 +110,7 @@ class VimShowModeTest : VimTestCase() { configureByText("123") typeText("R") val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) - assertEquals("(replace)", statusString) + assertEquals("(replace) NORMAL", statusString) } @Test @@ -193,4 +193,13 @@ class VimShowModeTest : VimTestCase() { val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) assertEquals("(insert) S-LINE", statusString) } + + @Test + fun `test status string in Visual with Select pending`() { + configureByText("123") + enterCommand("set selectmode=key keymodel=startsel") + typeText("") + val statusString = VimModeWidget.getModeText(fixture.editor.vim.mode) + assertEquals("(select) VISUAL", statusString) + } } From 2cd000c6f8b0b28ede23e6407d9258e46f101f5d Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 31 Dec 2024 02:09:37 +0000 Subject: [PATCH 12/16] Remove unnecessary pushVisualMode helper function --- .../com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt | 3 +-- .../kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index c0fad6d41c..d794a536e3 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -15,7 +15,6 @@ import com.maddyhome.idea.vim.group.visual.vimLeadSelectionOffset import com.maddyhome.idea.vim.group.visual.vimSetSelection import com.maddyhome.idea.vim.group.visual.vimUpdateEditorSelection import com.maddyhome.idea.vim.helper.exitVisualMode -import com.maddyhome.idea.vim.helper.pushVisualMode import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.inVisualMode @@ -60,7 +59,7 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { if (!editor.inVisualMode) { if (rawCount > 0) { val primarySelectionType = editor.primaryCaret().vimLastVisualOperatorRange?.type ?: selectionType - editor.pushVisualMode(primarySelectionType) + editor.mode = Mode.VISUAL(primarySelectionType, editor.mode.returnTo) editor.forEachCaret { val range = it.vimLastVisualOperatorRange ?: VisualChange.default(selectionType) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt index b967e0a438..56e84f3949 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/helper/EngineHelper.kt @@ -14,7 +14,6 @@ import com.maddyhome.idea.vim.api.options import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.options.OptionConstants import com.maddyhome.idea.vim.state.mode.Mode -import com.maddyhome.idea.vim.state.mode.SelectionType import java.util.* inline fun > noneOfEnum(): EnumSet = EnumSet.noneOf(T::class.java) @@ -60,7 +59,3 @@ inline fun > enumSetOf(vararg value: T): EnumSet = when ( 1 -> EnumSet.of(value[0]) else -> EnumSet.of(value[0], *value.slice(1..value.lastIndex).toTypedArray()) } - -fun VimEditor.pushVisualMode(selectionType: SelectionType) { - mode = Mode.VISUAL(selectionType, mode.returnTo) -} From 14c2a64b460a2cb83fc51cb28e6fc0fd884ad224 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 31 Dec 2024 10:05:56 +0000 Subject: [PATCH 13/16] Update docs and remove unnecessary null parameter --- .../visual/VisualToggleLineModeAction.kt | 5 ++- .../idea/vim/api/VimVisualMotionGroup.kt | 21 ++++-------- .../idea/vim/api/VimVisualMotionGroupBase.kt | 32 +++++++------------ 3 files changed, 20 insertions(+), 38 deletions(-) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleLineModeAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleLineModeAction.kt index 470756769a..89a7319638 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleLineModeAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/visual/VisualToggleLineModeAction.kt @@ -24,8 +24,8 @@ import com.maddyhome.idea.vim.options.OptionConstants @CommandOrMotion(keys = ["V"], modes = [Mode.NORMAL, Mode.VISUAL]) class VisualToggleLineModeAction : VimActionHandler.ConditionalMulticaret() { - override val type: Command.Type = Command.Type.OTHER_READONLY + override fun runAsMulticaret( editor: VimEditor, context: ExecutionContext, @@ -57,7 +57,6 @@ class VisualToggleLineModeAction : VimActionHandler.ConditionalMulticaret() { cmd: Command, operatorArguments: OperatorArguments, ): Boolean { - return injector.visualMotionGroup - .toggleVisual(editor, cmd.count, cmd.rawCount, SelectionType.LINE_WISE) + return injector.visualMotionGroup.toggleVisual(editor, cmd.count, cmd.rawCount, SelectionType.LINE_WISE) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index a16b322437..f744f75fa5 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -16,7 +16,9 @@ interface VimVisualMotionGroup { val selectionAdj: Int /** - * This function toggles visual mode. + * This function toggles visual mode according to the logic required for `v`, `V` and `` + * + * This is the implementation for `v`, `V` and ``. If you need to enter Visual mode, use [enterVisualMode]. * * * If visual mode is disabled, enable it * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] @@ -37,21 +39,12 @@ interface VimVisualMotionGroup { fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean /** - * Enters visual mode based on current editor state. - * - * If [selectionType] is null, it will be detected automatically + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set * - * it: - * - Updates command state - * - Updates [VimCaret.vimSelectionStart] property - * - Updates caret colors - * - Updates care shape - * - * - DOES NOT change selection - * - DOES NOT move caret - * - DOES NOT check if carets actually have any selection + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. */ - fun enterVisualMode(editor: VimEditor, selectionType: SelectionType? = null): Boolean + fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean + fun detectSelectionType(editor: VimEditor): SelectionType /** diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index d794a536e3..ace4f570b1 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -43,11 +43,13 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { } /** - * This function toggles visual mode. + * This function toggles visual mode according to the logic required for `v`, `V` and `` * - * If visual mode is disabled, enable it - * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] - * If visual mode is enabled with the same [selectionType], disable it + * This is the implementation for `v`, `V` and ``. If you need to enter Visual mode, use [enterVisualMode]. + * + * * If visual mode is disabled, enable it + * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] + * * If visual mode is enabled with the same [selectionType], disable it */ override fun toggleVisual( editor: VimEditor, @@ -146,28 +148,16 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { } /** - * Enters visual mode based on current editor state. - * - * If [selectionType] is null, it will be detected automatically + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set * - * it: - * - Updates command state - * - Updates [ImmutableVimCaret.vimSelectionStart] property - * - Updates caret colors - * - Updates care shape - * - * - DOES NOT change selection - * - DOES NOT move caret - * - DOES NOT check if carets actually have any selection + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. */ - override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType?): Boolean { - val newSelectionType = selectionType ?: detectSelectionType(editor) - - editor.mode = Mode.VISUAL(newSelectionType) + override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { + editor.mode = Mode.VISUAL(selectionType) // vimLeadSelectionOffset requires read action injector.application.runReadAction { - if (newSelectionType == SelectionType.BLOCK_WISE) { + if (selectionType == SelectionType.BLOCK_WISE) { editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset } } else { editor.nativeCarets().forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } From b2a0962dc6b73697059ed0f03f2da2766c32dbc8 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Tue, 31 Dec 2024 12:18:40 +0000 Subject: [PATCH 14/16] Reorder functions in file. No code changes --- .../idea/vim/api/VimVisualMotionGroup.kt | 29 ++-- .../idea/vim/api/VimVisualMotionGroupBase.kt | 132 +++++++++--------- 2 files changed, 83 insertions(+), 78 deletions(-) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index f744f75fa5..36d79169a5 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -16,15 +16,11 @@ interface VimVisualMotionGroup { val selectionAdj: Int /** - * This function toggles visual mode according to the logic required for `v`, `V` and `` - * - * This is the implementation for `v`, `V` and ``. If you need to enter Visual mode, use [enterVisualMode]. + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set * - * * If visual mode is disabled, enable it - * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] - * * If visual mode is enabled with the same [selectionType], disable it + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. */ - fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean + fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean /** * Enter Select mode with the given selection type @@ -39,13 +35,15 @@ interface VimVisualMotionGroup { fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean /** - * Enters Visual mode, ensuring that the caret's selection start offset is correctly set + * This function toggles visual mode according to the logic required for `v`, `V` and `` * - * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. + * This is the implementation for `v`, `V` and ``. If you need to enter Visual mode, use [enterVisualMode]. + * + * * If visual mode is disabled, enable it + * * If visual mode is enabled, but [selectionType] differs, update visual according to new [selectionType] + * * If visual mode is enabled with the same [selectionType], disable it */ - fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean - - fun detectSelectionType(editor: VimEditor): SelectionType + fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean /** * When in Select mode, enter Visual mode for a single command @@ -58,4 +56,11 @@ interface VimVisualMotionGroup { * See `:help v_CTRL-O`. */ fun processSingleVisualCommand(editor: VimEditor) + + /** + * Detect the current selection type based on the editor's current selection state + * + * If the IDE changes the selection, this function can be used to understand what the current selection type is. + */ + fun detectSelectionType(editor: VimEditor): SelectionType } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index ace4f570b1..572e92b2a4 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -26,6 +26,25 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { override val selectionAdj: Int get() = if (exclusiveSelection) 0 else 1 + /** + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set + * + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. + */ + override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { + editor.mode = Mode.VISUAL(selectionType) + + // vimLeadSelectionOffset requires read action + injector.application.runReadAction { + if (selectionType == SelectionType.BLOCK_WISE) { + editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset } + } else { + editor.nativeCarets().forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } + } + } + return true + } + override fun enterSelectMode(editor: VimEditor, selectionType: SelectionType): Boolean { // If we're already in Select or toggling from Visual, replace the current mode (keep the existing returnTo), // otherwise push Select, using the current mode as returnTo. @@ -100,72 +119,6 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { return true } - protected fun seemsLikeBlockMode(editor: VimEditor): Boolean { - val selections = editor.nativeCarets().map { - val adj = if (editor.offsetToBufferPosition(it.selectionEnd).column == 0) 1 else 0 - it.selectionStart to (it.selectionEnd - adj).coerceAtLeast(0) - }.sortedBy { it.first } - val selectionStartColumn = editor.offsetToBufferPosition(selections.first().first).column - val selectionStartLine = editor.offsetToBufferPosition(selections.first().first).line - - val maxColumn = selections.maxOfOrNull { editor.offsetToBufferPosition(it.second).column } ?: return false - selections.forEachIndexed { i, it -> - if (editor.offsetToBufferPosition(it.first).line != editor.offsetToBufferPosition(it.second).line) { - return false - } - if (editor.offsetToBufferPosition(it.first).column != selectionStartColumn) { - return false - } - val lineEnd = - editor.offsetToBufferPosition(editor.getLineEndForOffset(it.second)).column - if (editor.offsetToBufferPosition(it.second).column != maxColumn.coerceAtMost(lineEnd)) { - return false - } - if (editor.offsetToBufferPosition(it.first).line != selectionStartLine + i) { - return false - } - } - return true - } - - override fun detectSelectionType(editor: VimEditor): SelectionType { - if (editor.carets().size > 1 && seemsLikeBlockMode(editor)) { - return SelectionType.BLOCK_WISE - } - val all = editor.nativeCarets().all { caret -> - // Detect if visual mode is character wise or line wise - val selectionStart = caret.selectionStart - val selectionEnd = caret.selectionEnd - val startLine = editor.offsetToBufferPosition(selectionStart).line - val endPosition = editor.offsetToBufferPosition(selectionEnd) - val endLine = if (endPosition.column == 0) (endPosition.line - 1).coerceAtLeast(0) else endPosition.line - val lineStartOfSelectionStart = editor.getLineStartOffset(startLine) - val lineEndOfSelectionEnd = editor.getLineEndOffset(endLine, true) - lineStartOfSelectionStart == selectionStart && (lineEndOfSelectionEnd + 1 == selectionEnd || lineEndOfSelectionEnd == selectionEnd) - } - if (all) return SelectionType.LINE_WISE - return SelectionType.CHARACTER_WISE - } - - /** - * Enters Visual mode, ensuring that the caret's selection start offset is correctly set - * - * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. - */ - override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { - editor.mode = Mode.VISUAL(selectionType) - - // vimLeadSelectionOffset requires read action - injector.application.runReadAction { - if (selectionType == SelectionType.BLOCK_WISE) { - editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset } - } else { - editor.nativeCarets().forEach { it.vimSelectionStart = it.vimLeadSelectionOffset } - } - } - return true - } - /** * When in Select mode, enter Visual mode for a single command * @@ -196,4 +149,51 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { SelectToggleVisualMode.toggleMode(editor) } } + + override fun detectSelectionType(editor: VimEditor): SelectionType { + if (editor.carets().size > 1 && seemsLikeBlockMode(editor)) { + return SelectionType.BLOCK_WISE + } + val all = editor.nativeCarets().all { caret -> + // Detect if visual mode is character wise or line wise + val selectionStart = caret.selectionStart + val selectionEnd = caret.selectionEnd + val startLine = editor.offsetToBufferPosition(selectionStart).line + val endPosition = editor.offsetToBufferPosition(selectionEnd) + val endLine = if (endPosition.column == 0) (endPosition.line - 1).coerceAtLeast(0) else endPosition.line + val lineStartOfSelectionStart = editor.getLineStartOffset(startLine) + val lineEndOfSelectionEnd = editor.getLineEndOffset(endLine, true) + lineStartOfSelectionStart == selectionStart && (lineEndOfSelectionEnd + 1 == selectionEnd || lineEndOfSelectionEnd == selectionEnd) + } + if (all) return SelectionType.LINE_WISE + return SelectionType.CHARACTER_WISE + } + + protected fun seemsLikeBlockMode(editor: VimEditor): Boolean { + val selections = editor.nativeCarets().map { + val adj = if (editor.offsetToBufferPosition(it.selectionEnd).column == 0) 1 else 0 + it.selectionStart to (it.selectionEnd - adj).coerceAtLeast(0) + }.sortedBy { it.first } + val selectionStartColumn = editor.offsetToBufferPosition(selections.first().first).column + val selectionStartLine = editor.offsetToBufferPosition(selections.first().first).line + + val maxColumn = selections.maxOfOrNull { editor.offsetToBufferPosition(it.second).column } ?: return false + selections.forEachIndexed { i, it -> + if (editor.offsetToBufferPosition(it.first).line != editor.offsetToBufferPosition(it.second).line) { + return false + } + if (editor.offsetToBufferPosition(it.first).column != selectionStartColumn) { + return false + } + val lineEnd = + editor.offsetToBufferPosition(editor.getLineEndForOffset(it.second)).column + if (editor.offsetToBufferPosition(it.second).column != maxColumn.coerceAtMost(lineEnd)) { + return false + } + if (editor.offsetToBufferPosition(it.first).line != selectionStartLine + i) { + return false + } + } + return true + } } From bdb12259995ed0e79cc6f718e701996129137087 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 1 Jan 2025 11:05:46 +0000 Subject: [PATCH 15/16] Refactor improper usages of toggleVisual --- .../idea/vim/api/VimVisualMotionGroup.kt | 15 ++++++++++++++- .../idea/vim/api/VimVisualMotionGroupBase.kt | 4 ++-- .../idea/vim/handler/SpecialKeyHandlers.kt | 8 ++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index 36d79169a5..085d33712d 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -19,8 +19,21 @@ interface VimVisualMotionGroup { * Enters Visual mode, ensuring that the caret's selection start offset is correctly set * * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. + * + * This overload needs to remain for compatibility with external IdeaVim extensions + */ + fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { + return enterVisualMode(editor, selectionType, Mode.NORMAL()) + } + + /** + * Enters Visual mode, ensuring that the caret's selection start offset is correctly set + * + * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. You can + * specify the mode to return to when Visual command exits. Typically, this is [Mode.NORMAL], but can be [Mode.INSERT] + * or [Mode.REPLACE] for "Insert Visual" or [Mode.SELECT] when processing a single Visual command in Select mode. */ - fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean + fun enterVisualMode(editor: VimEditor, selectionType: SelectionType, returnTo: Mode): Boolean /** * Enter Select mode with the given selection type diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index 572e92b2a4..1056ba04fa 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -31,8 +31,8 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { * * Use this to programmatically enter Visual mode. Note that it does not modify the editor's selection. */ - override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType): Boolean { - editor.mode = Mode.VISUAL(selectionType) + override fun enterVisualMode(editor: VimEditor, selectionType: SelectionType, returnTo: Mode): Boolean { + editor.mode = Mode.VISUAL(selectionType, returnTo) // vimLeadSelectionOffset requires read action injector.application.runReadAction { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt index 79a83f9b12..6a4f301700 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/handler/SpecialKeyHandlers.kt @@ -61,7 +61,7 @@ abstract class ShiftedSpecialKeyHandler : VimActionHandler.ConditionalMulticaret if (injector.globalOptions().selectmode.contains(OptionConstants.selectmode_key)) { injector.visualMotionGroup.enterSelectMode(editor, SelectionType.CHARACTER_WISE) } else { - injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) + injector.visualMotionGroup.enterVisualMode(editor, SelectionType.CHARACTER_WISE) } } return true @@ -97,10 +97,10 @@ abstract class ShiftedArrowKeyHandler(private val runBothCommandsAsMulticaret: B injector.visualMotionGroup.enterSelectMode(editor, SelectionType.CHARACTER_WISE) } else { if (editor.isInsertionAllowed) { - // Enter Insert/Replace Visual mode - injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE, editor.mode) + // Enter Insert/Replace Visual mode, passing in the current Insert/Replace mode as pending + injector.visualMotionGroup.enterVisualMode(editor, SelectionType.CHARACTER_WISE, editor.mode) } else { - injector.visualMotionGroup.toggleVisual(editor, 1, 0, SelectionType.CHARACTER_WISE) + injector.visualMotionGroup.enterVisualMode(editor, SelectionType.CHARACTER_WISE) } } } From 272bea02f2d40a9d259298f955c11fa46c477856 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 1 Jan 2025 12:08:27 +0000 Subject: [PATCH 16/16] Move select toggle implementation --- .../idea/vim/listener/AppCodeTemplates.kt | 4 +- .../motion/select/SelectToggleVisualMode.kt | 28 +------ .../idea/vim/api/VimVisualMotionGroup.kt | 11 +++ .../idea/vim/api/VimVisualMotionGroupBase.kt | 80 ++++++++++++++++--- 4 files changed, 81 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/maddyhome/idea/vim/listener/AppCodeTemplates.kt b/src/main/java/com/maddyhome/idea/vim/listener/AppCodeTemplates.kt index 7df5c7233f..6700ce8a6e 100644 --- a/src/main/java/com/maddyhome/idea/vim/listener/AppCodeTemplates.kt +++ b/src/main/java/com/maddyhome/idea/vim/listener/AppCodeTemplates.kt @@ -20,7 +20,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.util.Key import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.VimPlugin -import com.maddyhome.idea.vim.action.motion.select.SelectToggleVisualMode +import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.group.visual.VimVisualTimer import com.maddyhome.idea.vim.helper.fileSize import com.maddyhome.idea.vim.helper.inVisualMode @@ -56,7 +56,7 @@ internal object AppCodeTemplates { if (myEditor != null) { VimVisualTimer.doNow() if (myEditor.inVisualMode) { - SelectToggleVisualMode.toggleMode(myEditor.vim) + injector.visualMotionGroup.toggleSelectVisual(myEditor.vim) KeyHandler.getInstance().partialReset(myEditor.vim) } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt index 321fb6e197..bd85c9991e 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt @@ -16,7 +16,6 @@ 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 -import com.maddyhome.idea.vim.state.mode.SelectionType /** * @author Alex Plate @@ -33,32 +32,7 @@ class SelectToggleVisualMode : VimActionHandler.SingleExecution() { cmd: Command, operatorArguments: OperatorArguments, ): Boolean { - toggleMode(editor) + injector.visualMotionGroup.toggleSelectVisual(editor) return true } - - companion object { - fun toggleMode(editor: VimEditor) { - val myMode = editor.mode - if (myMode is com.maddyhome.idea.vim.state.mode.Mode.VISUAL) { - injector.visualMotionGroup.enterSelectMode(editor, myMode.selectionType) - if (myMode.selectionType != SelectionType.LINE_WISE) { - editor.nativeCarets().forEach { - if (it.offset + injector.visualMotionGroup.selectionAdj == it.selectionEnd) { - it.moveToInlayAwareOffset(it.offset + injector.visualMotionGroup.selectionAdj) - } - } - } - } else if (myMode is com.maddyhome.idea.vim.state.mode.Mode.SELECT) { - injector.visualMotionGroup.enterVisualMode(editor, myMode.selectionType) - if (myMode.selectionType != SelectionType.LINE_WISE) { - editor.nativeCarets().forEach { - if (it.offset == it.selectionEnd && it.visualLineStart <= it.offset - injector.visualMotionGroup.selectionAdj) { - it.moveToInlayAwareOffset(it.offset - injector.visualMotionGroup.selectionAdj) - } - } - } - } - } - } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt index 085d33712d..de1bd80690 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroup.kt @@ -58,6 +58,17 @@ interface VimVisualMotionGroup { */ fun toggleVisual(editor: VimEditor, count: Int, rawCount: Int, selectionType: SelectionType, returnTo: Mode? = null): Boolean + /** + * Toggles between Select and Visual modes + * + * IdeaVim treats Select mode as always exclusive, regardless of the value in `'selection'`. As such, when toggling + * between Visual and Select, the caret is adjusted to be more natural for exclusive selection. Specifically, when + * toggling from Visual with inclusive selection to Select (always exclusive), the caret is adjusted one character to + * the right, to put it as exclusive to the current selection. When toggling from Select (exclusive) to Visual with + * inclusive selection, the caret is adjusted one character to the left from exclusive position to inclusive. + */ + fun toggleSelectVisual(editor: VimEditor) + /** * When in Select mode, enter Visual mode for a single command * diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt index 1056ba04fa..a558bd52ee 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimVisualMotionGroupBase.kt @@ -8,7 +8,6 @@ package com.maddyhome.idea.vim.api -import com.maddyhome.idea.vim.action.motion.select.SelectToggleVisualMode import com.maddyhome.idea.vim.group.visual.VisualChange import com.maddyhome.idea.vim.group.visual.VisualOperation import com.maddyhome.idea.vim.group.visual.vimLeadSelectionOffset @@ -119,6 +118,66 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { return true } + override fun toggleSelectVisual(editor: VimEditor) { + val mode = editor.mode + if (mode is Mode.VISUAL) { + val previouslyExclusive = exclusiveSelection + enterSelectMode(editor, mode.selectionType) + adjustCaretsForSelectionPolicy(editor, previouslyExclusive) + } + else if (mode is Mode.SELECT) { + val previouslyExclusive = true // IdeaVim treats Select as always exclusive + enterVisualMode(editor, mode.selectionType) + adjustCaretsForSelectionPolicy(editor, previouslyExclusive) + } + } + + /** + * Move the caret after toggling Visual/Select mode, if this also toggles selection policy (inclusive/exclusive) + * + * Toggling Visual/Select mode does not update the caret position in Vim. This is because the inclusive/exclusive + * state of the selection does not change. It is the value of the `'selection'` option, and the same for Visual and + * Select. + * + * IdeaVim can have a different selection policy for Visual and Select; it is more intuitive for Select mode to use + * "exclusive", but more familiar for Visual to be inclusive (see [VimEditor.isSelectionExclusive] for more + * background). IdeaVim will therefore try to update the caret position to match what the selection would be if it + * has just been created with the new selection policy, rather than toggled after the selection was made. + * + * In other words, Vim places the caret at the end of the selection when creating an inclusive selection, and + * _after_ the end of selection when creating an exclusive selection, and IdeaVim updates to match this. + * + * Specifically, when switching from inclusive to exclusive, and the caret is at the end of the selection, the caret + * is moved to be after the selection. And when switching from exclusive to inclusive and the caret is after the end + * of the selection, it is moved to be at the end of selection. + */ + private fun adjustCaretsForSelectionPolicy(editor: VimEditor, previouslyExclusive: Boolean) { + val mode = editor.mode + // TODO: Improve handling of this.exclusiveSelection + val isSelectionExclusive = this.exclusiveSelection || mode is Mode.SELECT + if (mode.selectionType != SelectionType.LINE_WISE) { + // Remember that VimCaret.selectionEnd is exclusive! + if (isSelectionExclusive && !previouslyExclusive) { + // Inclusive -> exclusive + editor.nativeCarets().forEach { + if (it.offset == it.selectionEnd - 1) { + it.moveToInlayAwareOffset(it.selectionEnd) // Caret is on the selection end, move to after + } + } + } + else if (!isSelectionExclusive && previouslyExclusive) { + // Exclusive -> inclusive + editor.nativeCarets().forEach { + // If caret offset matches the exclusive selection end offset, then it's positioned after the selection, so + // move it to the actual end of the selection. Make sure there's enough room on this line to do so + if (it.offset == it.selectionEnd && it.visualLineStart < it.offset) { + it.moveToInlayAwareOffset(it.selectionEnd - 1) + } + } + } + } + } + /** * When in Select mode, enter Visual mode for a single command * @@ -132,21 +191,16 @@ abstract class VimVisualMotionGroupBase : VimVisualMotionGroup { override fun processSingleVisualCommand(editor: VimEditor) { val mode = editor.mode if (mode is Mode.SELECT) { + // We can't use toggleSelectVisual because when toggling between Select and Visual, we replace the current mode, + // but here we want to "push" Visual, so we return to Select. + val previouslyExclusive = true // Select is always exclusive editor.mode = Mode.VISUAL(mode.selectionType, returnTo = mode) - // TODO: This is a copy of code from SelectToggleVisualMode.toggleMode. It should be moved to VimVisualMotionGroup - // IdeaVim always treats Select mode as exclusive. This will adjust the caret from exclusive to (potentially) - // inclusive, depending on the value of 'selection' - if (mode.selectionType != SelectionType.LINE_WISE) { - editor.nativeCarets().forEach { - if (it.offset == it.selectionEnd && it.visualLineStart <= it.offset - injector.visualMotionGroup.selectionAdj) { - it.moveToInlayAwareOffset(it.offset - injector.visualMotionGroup.selectionAdj) - } - } - } + adjustCaretsForSelectionPolicy(editor, previouslyExclusive) } else if (mode is Mode.VISUAL && mode.isSelectPending) { - // TODO: It would be better to move this to VimVisualMotionGroup - SelectToggleVisualMode.toggleMode(editor) + // We can use toggleSelectVisual because we're replacing the "pushed" Visual mode with a simple Select, which is + // the same as when we toggle from Visual to Select + toggleSelectVisual(editor) } }