1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-02-24 08:46:00 +01:00

More obvious processing of ex-commands.

1. Now we have two parallel commandBuilders: for the editor and for the command prompt. It's done for sequence of keys like `d/foo<C-R>"<CR>` where we have two different commands that are built at the same time.
2. We simplified the CommandConsumer and made the logic more straightforward. `/`, `?` and `:` enter the command mode, while pressing final `<CR>` fires the command execution.
This commit is contained in:
Filipp Vakhitov 2024-05-22 22:04:32 +03:00
parent a6994a09c3
commit 726b885b60
15 changed files with 188 additions and 94 deletions

View File

@ -11,25 +11,60 @@ import com.intellij.vim.annotations.CommandOrMotion
import com.intellij.vim.annotations.Mode
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.common.Direction
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 java.util.*
/**
* Called by KeyHandler to process the contents of the ex entry panel
*
* The mapping for this action means that the ex command is executed as a write action
*/
@CommandOrMotion(keys = ["<CR>", "<C-M>", "<C-J>"], modes = [Mode.CMD_LINE])
public class ProcessExEntryAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
public class ProcessExEntryAction : MotionActionHandler.AmbiguousExecution() {
override val flags: EnumSet<CommandFlags> = EnumSet.of(CommandFlags.FLAG_SAVE_JUMP, CommandFlags.FLAG_END_EX)
override val motionType: MotionType = MotionType.EXCLUSIVE
override val flags: EnumSet<CommandFlags> = EnumSet.of(CommandFlags.FLAG_COMPLETE_EX)
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
return VimPlugin.getProcess().processExEntry(editor, context)
override fun getMotionActionHandler(argument: Argument?): MotionActionHandler {
return if (argument?.character == ':') ProcessExCommandEntryAction() else ProcessSearchEntryAction()
}
}
public class ProcessSearchEntryAction : MotionActionHandler.ForEachCaret() {
override val motionType: MotionType = MotionType.EXCLUSIVE
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
if (argument == null) return Motion.Error
return when (argument.character) {
'/' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, Direction.FORWARDS).toMotionOrError()
'?' -> injector.searchGroup.processSearchCommand(editor, argument.string, caret.offset, Direction.BACKWARDS).toMotionOrError()
else -> throw ExException("Unexpected search label ${argument.character}")
}
}
}
public class ProcessExCommandEntryAction : MotionActionHandler.SingleExecution() {
override val motionType: MotionType = MotionType.LINE_WISE
override fun getOffset(
editor: VimEditor,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
VimPlugin.getProcess().processExEntry(editor, context)
// TODO support motions for commands
return Motion.NoMotion
}
}

View File

@ -24,6 +24,8 @@ import com.maddyhome.idea.vim.api.VimApplication
import com.maddyhome.idea.vim.api.VimChangeGroup
import com.maddyhome.idea.vim.api.VimClipboardManager
import com.maddyhome.idea.vim.api.VimCommandGroup
import com.maddyhome.idea.vim.api.VimCommandLine
import com.maddyhome.idea.vim.api.VimCommandLineService
import com.maddyhome.idea.vim.api.VimDigraphGroup
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimEditorGroup
@ -197,6 +199,11 @@ internal class IjVimInjector : VimInjectorBase() {
get() = service<Executor>()
override val vimscriptParser: VimscriptParser
get() = com.maddyhome.idea.vim.vimscript.parser.VimscriptParser
override val commandLine: VimCommandLineService = object : VimCommandLineService {
override fun getActiveCommandLine(): VimCommandLine? {
return com.maddyhome.idea.vim.ui.ex.ExEntryPanel.getInstance()
}
}
override val optionGroup: VimOptionGroup
get() = service()

View File

@ -19,6 +19,7 @@ import com.intellij.openapi.editor.ScrollingModel;
import com.intellij.ui.DocumentAdapter;
import com.intellij.util.IJSwingUtilities;
import com.maddyhome.idea.vim.VimPlugin;
import com.maddyhome.idea.vim.api.VimCommandLine;
import com.maddyhome.idea.vim.ex.ranges.LineRange;
import com.maddyhome.idea.vim.helper.SearchHighlightsHelper;
import com.maddyhome.idea.vim.helper.UiHelper;
@ -48,7 +49,7 @@ import static com.maddyhome.idea.vim.api.VimInjectorKt.injector;
/**
* This is used to enter ex commands such as searches and "colon" commands
*/
public class ExEntryPanel extends JPanel {
public class ExEntryPanel extends JPanel implements VimCommandLine {
private static ExEntryPanel instance;
private static ExEntryPanel instanceWithoutShortcuts;

View File

@ -0,0 +1,30 @@
/*
* 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.ex
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.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import java.util.*
@CommandOrMotion(keys = ["<Esc>", "<C-[>", "<C-C>"], modes = [Mode.CMD_LINE])
public class LeaveCommandLineAction : VimActionHandler.SingleExecution() {
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_END_EX)
override val type: Command.Type = Command.Type.OTHER_READONLY
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
return true
}
}

View File

@ -10,37 +10,29 @@ package com.maddyhome.idea.vim.action.motion.search
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.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.Direction
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.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import java.util.*
@CommandOrMotion(keys = ["/"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
public class SearchEntryFwdAction : MotionActionHandler.ForEachCaret() {
public class SearchEntryFwdAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override val argumentType: Argument.Type = Argument.Type.EX_STRING
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_START_EX, CommandFlags.FLAG_SAVE_JUMP)
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_SAVE_JUMP)
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
if (argument == null) return Motion.Error
return injector.searchGroup
.processSearchCommand(editor, argument.string, caret.offset, Direction.FORWARDS).toMotionOrError()
override fun execute( editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
injector.processGroup.startSearchCommand(editor, context, cmd.count, '/')
val currentMode = editor.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable command line mode $currentMode" }
editor.mode = com.maddyhome.idea.vim.state.mode.Mode.CMD_LINE(currentMode)
return true
}
override val motionType: MotionType = MotionType.EXCLUSIVE
}

View File

@ -10,37 +10,29 @@ package com.maddyhome.idea.vim.action.motion.search
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.ImmutableVimCaret
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.MotionType
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.Direction
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.handler.VimActionHandler
import com.maddyhome.idea.vim.helper.enumSetOf
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import java.util.*
@CommandOrMotion(keys = ["?"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING])
public class SearchEntryRevAction : MotionActionHandler.ForEachCaret() {
public class SearchEntryRevAction : VimActionHandler.SingleExecution() {
override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED
override val argumentType: Argument.Type = Argument.Type.EX_STRING
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_START_EX, CommandFlags.FLAG_SAVE_JUMP)
override val flags: EnumSet<CommandFlags> = enumSetOf(CommandFlags.FLAG_SAVE_JUMP)
override fun getOffset(
editor: VimEditor,
caret: ImmutableVimCaret,
context: ExecutionContext,
argument: Argument?,
operatorArguments: OperatorArguments,
): Motion {
if (argument == null) return Motion.Error
return injector.searchGroup
.processSearchCommand(editor, argument.string, caret.offset, Direction.BACKWARDS).toMotionOrError()
override fun execute( editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
injector.processGroup.startSearchCommand(editor, context, cmd.count, '?')
val currentMode = editor.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable command line mode $currentMode" }
editor.mode = com.maddyhome.idea.vim.state.mode.Mode.CMD_LINE(currentMode)
return true
}
override val motionType: MotionType = MotionType.EXCLUSIVE
}
}

View File

@ -0,0 +1,16 @@
/*
* 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.api
public interface VimCommandLine {
public val label: String
public val text: String
public fun deactivate(refocusOwningEditor: Boolean)
}

View File

@ -0,0 +1,13 @@
/*
* 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.api
public interface VimCommandLineService {
public fun getActiveCommandLine(): VimCommandLine?
}

View File

@ -181,6 +181,8 @@ public interface VimInjector {
// !! in progress
public val variableService: VariableService
public val commandLine: VimCommandLineService
// !! in progress
public val functionService: VimscriptFunctionService

View File

@ -17,10 +17,14 @@ public interface VimProcessGroup {
public val isCommandProcessing: Boolean
public val modeBeforeCommandProcessing: Mode?
// TODO remove me
public fun startSearchCommand(editor: VimEditor, context: ExecutionContext, count: Int, leader: Char)
// TODO remove me
public fun endSearchCommand(): String
// TODO remove me
public fun processExKey(editor: VimEditor, stroke: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean
public fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command)
// TODO remove me
public fun startExCommand(editor: VimEditor, context: ExecutionContext, cmd: Command)
public fun processExEntry(editor: VimEditor, context: ExecutionContext): Boolean
public fun cancelExEntry(editor: VimEditor, resetCaret: Boolean)

View File

@ -18,6 +18,7 @@ import java.util.*
/**
* This represents a command argument.
* TODO please make it a sealed class and not a giant collection of fields with default values, it's not safe
*/
public class Argument private constructor(
public val character: Char = 0.toChar(),
@ -28,7 +29,7 @@ public class Argument private constructor(
) {
public constructor(motionArg: Command) : this(motion = motionArg, type = Type.MOTION)
public constructor(charArg: Char) : this(character = charArg, type = Type.CHARACTER)
public constructor(strArg: String) : this(string = strArg, type = Type.EX_STRING)
public constructor(label: Char, strArg: String) : this(character = label, string = strArg, type = Type.EX_STRING)
public constructor(offsets: Map<ImmutableVimCaret, VimSelection>) : this(offsets = offsets, type = Type.OFFSETS)
public enum class Type {

View File

@ -73,14 +73,8 @@ public enum class CommandFlags {
*/
FLAG_ALLOW_DIGRAPH,
/**
* Indicates that a command handles completing ex input.
*
* When performing a search, the search action command requires an EX_STRING as input. This is completed by a command
* that has FLAG_COMPLETE_EX. That command isn't called and the ex string becomes an argument for the previous command
* that started the EX_STRING.
*/
FLAG_COMPLETE_EX,
FLAG_START_EX,
FLAG_END_EX,
FLAG_TEXT_BLOCK,
}

View File

@ -98,6 +98,14 @@ public class CommandConsumer : KeyConsumer {
// The user entered a valid command. Create the command and add it to the stack.
val action = node.actionHolder.instance
val keyState = processBuilder.state
if (action.flags.contains(CommandFlags.FLAG_START_EX)) {
keyState.enterCommandLine()
}
if (action.flags.contains(CommandFlags.FLAG_END_EX)) {
keyState.leaveCommandLine()
}
val commandBuilder = keyState.commandBuilder
val expectedArgumentType = commandBuilder.expectedArgumentType
commandBuilder.pushCommandPart(action)
@ -122,29 +130,14 @@ public class CommandConsumer : KeyConsumer {
}
processBuilder.addExecutionStep { _, lambdaEditor, _ ->
// TODO In the name of God, get rid of EX_STRING, FLAG_COMPLETE_EX and all the related staff
if (expectedArgumentType === Argument.Type.EX_STRING && action.flags.contains(CommandFlags.FLAG_COMPLETE_EX)) {
/* The only action that implements FLAG_COMPLETE_EX is ProcessExEntryAction.
* When pressing ':', ExEntryAction is chosen as the command. Since it expects no arguments, it is invoked and
calls ProcessGroup#startExCommand, pushes CMD_LINE mode, and the action is popped. The ex handler will push
the final <CR> through handleKey, which chooses ProcessExEntryAction. Because we're not expecting EX_STRING,
this branch does NOT fire, and ProcessExEntryAction handles the ex cmd line entry.
* When pressing '/' or '?', SearchEntry(Fwd|Rev)Action is chosen as the command. This expects an argument of
EX_STRING, so startWaitingForArgument calls ProcessGroup#startSearchCommand. The ex handler pushes the final
<CR> through handleKey, which chooses ProcessExEntryAction, and we hit this branch. We don't invoke
ProcessExEntryAction, but pop it, set the search text as an argument on SearchEntry(Fwd|Rev)Action and invoke
that instead.
* When using '/' or '?' as part of a motion (e.g. "d/foo"), the above happens again, and all is good. Because
the text has been applied as an argument on the last command, '.' will correctly repeat it.
It's hard to see how to improve this. Removing EX_STRING means starting ex input has to happen in ExEntryAction
and SearchEntry(Fwd|Rev)Action, and the ex command invoked in ProcessExEntryAction, but that breaks any initial
operator, which would be invoked first (e.g. 'd' in "d/foo").
*/
if (action.flags.contains(CommandFlags.FLAG_END_EX)) {
logger.trace("Processing ex_string")
val text = injector.processGroup.endSearchCommand()
commandBuilder.popCommandPart() // Pop ProcessExEntryAction
commandBuilder.completeCommandPart(Argument(text)) // Set search text on SearchEntry(Fwd|Rev)Action
val commandLine = injector.commandLine.getActiveCommandLine()!!
val label = commandLine.label
val text = commandLine.text
commandLine.deactivate(true)
commandBuilder.completeCommandPart(Argument(label[0], text))
lambdaEditor.mode = lambdaEditor.mode.returnTo()
}
}

View File

@ -159,8 +159,7 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
action.id == "VimMotionSentencePreviousStartAction" ||
action.id == "VimMotionSentenceNextStartAction" ||
action.id == "VimMotionGotoFileMarkAction" ||
action.id == "VimSearchEntryFwdAction" ||
action.id == "VimSearchEntryRevAction" ||
action.id == "VimProcessExEntryAction" ||
action.id == "VimSearchAgainNextAction" ||
action.id == "VimSearchAgainPreviousAction" ||
action.id == "VimMotionParagraphNextAction" ||

View File

@ -19,9 +19,21 @@ import com.maddyhome.idea.vim.state.mode.Mode
public data class KeyHandlerState(
public val mappingState: MappingState,
public val digraphSequence: DigraphSequence,
public val commandBuilder: CommandBuilder,
public val editorCommandBuilder: CommandBuilder,
public var commandLineCommandBuilder: CommandBuilder?,
): Cloneable {
public constructor() : this(MappingState(), DigraphSequence(), CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.NORMAL)))
public constructor() : this(MappingState(), DigraphSequence(), CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.NORMAL)), null)
public val commandBuilder: CommandBuilder
get() = commandLineCommandBuilder ?: editorCommandBuilder
public fun enterCommandLine() {
commandLineCommandBuilder = CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.CMD_LINE))
}
public fun leaveCommandLine() {
commandLineCommandBuilder = null
}
public fun partialReset(mode: Mode) {
mappingState.resetMappingSequence()
@ -31,14 +43,17 @@ public data class KeyHandlerState(
public fun reset(mode: Mode) {
digraphSequence.reset()
mappingState.resetMappingSequence()
commandBuilder.resetAll(injector.keyGroup.getKeyRoot(mode.toMappingMode()))
commandLineCommandBuilder = null
editorCommandBuilder.resetAll(injector.keyGroup.getKeyRoot(mode.toMappingMode()))
}
public override fun clone(): KeyHandlerState {
return KeyHandlerState(
mappingState.clone(),
digraphSequence.clone(),
commandBuilder.clone()
editorCommandBuilder.clone(),
commandLineCommandBuilder?.clone(),
)
}
}