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:
parent
fcc234c4fe
commit
63b3af3f65
src/test/java/org/jetbrains/plugins/ideavim/action/motion/select
vim-engine/src/main
kotlin/com/maddyhome/idea/vim
action
change/insert
motion/select
api
handler
helper
state/mode
resources/ksp-generated
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user