Skip to content

Commit

Permalink
Maintain Visual when cancelling search entry
Browse files Browse the repository at this point in the history
  • Loading branch information
citizenmatt committed May 28, 2024
1 parent 97c9f26 commit a9111f3
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 27 deletions.
60 changes: 41 additions & 19 deletions src/main/java/com/maddyhome/idea/vim/group/ProcessGroup.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.ex.ExException
import com.maddyhome.idea.vim.ex.InvalidCommandException
import com.maddyhome.idea.vim.helper.hasVisualSelection
import com.maddyhome.idea.vim.helper.requestFocus
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.VimStateMachine.Companion.getInstance
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.returnTo
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext
import java.io.BufferedWriter
Expand All @@ -57,17 +59,31 @@ public class ProcessGroup : VimProcessGroupBase() {
// Don't allow searching in one line editors
if (editor.isOneLineMode()) return

val initText = ""
val label = leader.toString()
// Switch to Command-line mode. Unlike ex command entry, search does not switch to Normal first, and does not remove
// selection (neither does IdeaVim's ex command entry, to be honest. See startExEntry for implementation details).
// We maintain the current mode so that we can return to it correctly when search is done.
val currentMode = editor.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable command line mode $currentMode" }
editor.mode = Mode.CMD_LINE(currentMode)

val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, label, initText, count)
panel.activate(editor.ij, context.ij, leader.toString(), "", count)
}

public override fun endSearchCommand(): String {
public override fun endSearchCommand(editor: VimEditor): String {
val panel = ExEntryPanel.getInstance()
panel.deactivate(true)

// Restore our previous mode. If it's a Visual mode, make sure we've still got selection. There is no legitimate
// (Vim) way for this to happen, but make sure we're consistent
val mode = editor.mode.returnTo()
if (mode.hasVisualSelection && !editor.nativeCarets().any { it.hasSelection() }) {
editor.mode = Mode.NORMAL()
}
else {
editor.mode = mode
}

return panel.text
}

Expand Down Expand Up @@ -100,10 +116,11 @@ public class ProcessGroup : VimProcessGroupBase() {
panel.deactivate(true)
var res = true
try {
// Exit Command-line mode and return to Normal before executing the command. Remember from startExCommand 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 = Mode.NORMAL()
// Exit Command-line mode and return to the previous mode before executing the command (this is set to Normal in
// 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()

logger.debug("processing command")

Expand Down Expand Up @@ -143,8 +160,9 @@ public class ProcessGroup : VimProcessGroupBase() {
}

public override fun cancelExEntry(editor: VimEditor, resetCaret: Boolean) {
// TODO: Clear the selection. It's supposed to happen when we start ex entry
editor.mode = Mode.NORMAL()
// 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()
getInstance().reset(editor)
val panel = ExEntryPanel.getInstance()
panel.deactivate(true, resetCaret)
Expand Down Expand Up @@ -177,16 +195,20 @@ public class ProcessGroup : VimProcessGroupBase() {

val rangeText = cmd?.let { getRange(editor, cmd) } ?: ""

// Note that we should remove selection and reset caret offset before we switch back to Normal mode and then enter
// Command-line mode. However, some IdeaVim commands can handle multiple carets, including multiple carets with
// selection (which might or might not be a block selection). Unfortunately, because we're just entering
// Command-line mode, we don't know which command is going to be entered, so we can't remove selection here.
// Therefore, we switch to Normal and then Command-line even though we might still have a Visual selection...
// On the plus side, it means we still show selection while editing the command line, which Vim also does
// (Normal then Command-line is not strictly necessary, but done for completeness and autocmd)
// Switch to Normal mode before we enter Command-line mode. This gives us the correct mode to switch back to when
// ex entry is finished.
// Note that we should remove selection and reset caret offset at this point. However, some IdeaVim commands can
// handle multiple carets, including multiple carets with selection (which might or might not be a block selection).
// Unfortunately, since we don't know which command will be executed, we can't handle this yet. It might also mess
// with regex matching of visual area, which would have to rely on last selection info.
// Therefore, we change modes away from Visual, even though we might still have a Visual selection...
// On the plus side, it means we still show selection while editing the command line, which Vim also does.
// Caret selection is finally handled in Command.execute
editor.mode = Mode.NORMAL()
editor.mode = Mode.CMD_LINE(currentMode)
val normalMode = Mode.NORMAL()
if (currentMode !is Mode.NORMAL) {
editor.mode = normalMode
}
editor.mode = Mode.CMD_LINE(normalMode)

val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, label, rangeText + initialCommandText, 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2190,6 +2190,66 @@ class SearchGroupTest : VimTestCase() {
}
}

@Test
fun `test cancelling search restores Visual (characterwise)`() {
doTest(
listOf("v", "e", "/amet", "<Esc>"),
"""
|${c}Lorem ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin(),
"""
|${s}Lore${c}m${se} ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin(),
Mode.VISUAL(SelectionType.CHARACTER_WISE)
)
}

@Test
fun `test cancelling search restores Visual (linewise)`() {
doTest(
listOf("V", "2j", "/mauris", "<Esc>"),
"""
|${c}Lorem ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin(),
"""
|${s}Lorem ipsum dolor sit amet,
|consectetur adipiscing elit
|${c}Sed in orci mauris.
|${se}Cras id tellus in ex imperdiet egestas.
""".trimMargin(),
Mode.VISUAL(SelectionType.LINE_WISE)
)
}

@Test
fun `test cancelling search restores Visual (blockwise)`() {
doTest(
listOf("<C-V>", "2j", "/mauris", "<Esc>"),
"""
|Lo${c}rem ipsum dolor sit amet,
|consectetur adipiscing elit
|Sed in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin(),
"""
|Lo${s}${c}r${se}em ipsum dolor sit amet,
|co${s}${c}n${se}sectetur adipiscing elit
|Se${s}${c}d${se} in orci mauris.
|Cras id tellus in ex imperdiet egestas.
""".trimMargin(),
Mode.VISUAL(SelectionType.BLOCK_WISE)
)
}

@Test
fun `test highlight search results`() {
configureByText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public interface VimProcessGroup {
public val modeBeforeCommandProcessing: Mode?

public fun startSearchCommand(editor: VimEditor, context: ExecutionContext, count: Int, leader: Char)
public fun endSearchCommand(): String
public fun endSearchCommand(editor: VimEditor): String
public fun processExKey(editor: VimEditor, stroke: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean
public fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command)
public fun startExCommand(editor: VimEditor, context: ExecutionContext, cmd: Command)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class VimProcessGroupStub : VimProcessGroupBase() {
TODO("Not yet implemented")
}

override fun endSearchCommand(): String {
override fun endSearchCommand(editor: VimEditor): String {
TODO("Not yet implemented")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import com.maddyhome.idea.vim.key.Node
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.ReturnableFromCmd
import com.maddyhome.idea.vim.state.mode.returnTo
import javax.swing.KeyStroke

Expand Down Expand Up @@ -142,10 +141,9 @@ public class CommandConsumer : KeyConsumer {
operator, which would be invoked first (e.g. 'd' in "d/foo").
*/
logger.trace("Processing ex_string")
val text = injector.processGroup.endSearchCommand()
val text = injector.processGroup.endSearchCommand(lambdaEditor)
commandBuilder.popCommandPart() // Pop ProcessExEntryAction
commandBuilder.completeCommandPart(Argument(text)) // Set search text on SearchEntry(Fwd|Rev)Action
lambdaEditor.mode = lambdaEditor.mode.returnTo()
}
}
}
Expand Down Expand Up @@ -193,9 +191,6 @@ public class CommandConsumer : KeyConsumer {
injector.redrawService.redrawStatusLine()
injector.processGroup.startSearchCommand(editor, context, commandBuilder.count, key)
commandBuilder.commandState = CurrentCommandState.NEW_COMMAND
val currentMode = editorState.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable command line mode $currentMode" }
editor.mode = Mode.CMD_LINE(currentMode)
}

else -> Unit
Expand Down

0 comments on commit a9111f3

Please sign in to comment.