1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-09 00:34:07 +02:00

Implement v_CTRL-O

From Select mode, enters Visual for a single command
This commit is contained in:
Matt Ellis 2024-12-30 14:25:37 +00:00 committed by Alex Pláte
parent fcc234c4fe
commit 63b3af3f65
10 changed files with 196 additions and 10 deletions
src/test/java/org/jetbrains/plugins/ideavim/action/motion/select
vim-engine/src/main

View File

@ -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<C-O>",
"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(
"gh<C-O>e",
"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(
"gh<C-O>U",
"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<C-O><Esc>",
"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<C-O><C-O>",
"Lorem ${c}ipsum dolor sit amet",
"Lorem ${s}i${c}${se}psum dolor sit amet",
Mode.SELECT(SelectionType.CHARACTER_WISE),
)
}
}

View File

@ -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 = ["<C-O>"], modes = [Mode.INSERT])
class InsertSingleCommandAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.INSERT

View File

@ -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 = ["<C-O>"], 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
}
}

View File

@ -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) {

View File

@ -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 (`i<C-O>v`) 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)
}

View File

@ -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 `gh<C-O>e`)
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 (`i<C-O>v`) 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)
}
}
}

View File

@ -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

View File

@ -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" (`i<C-O>v`). For "Select Visual" (`gh<C-O>`) 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
}
}
}

View File

@ -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 `<C-O>`
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 (`R<C-O>v`).
*/
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 {

View File

@ -364,6 +364,11 @@
"class": "com.maddyhome.idea.vim.action.motion.mark.MotionJumpPreviousAction",
"modes": "N"
},
{
"keys": "<C-O>",
"class": "com.maddyhome.idea.vim.action.motion.select.SelectToggleSingleVisualCommandAction",
"modes": "SX"
},
{
"keys": "<C-P>",
"class": "com.maddyhome.idea.vim.action.ex.HistoryUpAction",