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

Use KeyProcessResultBuilder

It will help us to build the KeyProcessResult that we need for asynchronous key processing
This commit is contained in:
filipp 2024-02-23 13:02:24 +02:00
parent 275c5d28e1
commit 19fa00837c
7 changed files with 233 additions and 124 deletions
src/main/java/com/maddyhome/idea/vim/group
vim-engine/src/main/kotlin/com/maddyhome/idea/vim

View File

@ -20,6 +20,7 @@ import com.intellij.openapi.progress.ProgressManager
import com.intellij.util.execution.ParametersListUtil import com.intellij.util.execution.ParametersListUtil
import com.intellij.util.text.CharSequenceReader import com.intellij.util.text.CharSequenceReader
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
@ -90,19 +91,22 @@ public class ProcessGroup : VimProcessGroupBase() {
panel.activate(editor.ij, context.ij, ":", initText, 1) panel.activate(editor.ij, context.ij, ":", initText, 1)
} }
public override fun processExKey(editor: VimEditor, stroke: KeyStroke): Boolean { public override fun processExKey(editor: VimEditor, stroke: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean {
// This will only get called if somehow the key focus ended up in the editor while the ex entry window // This will only get called if somehow the key focus ended up in the editor while the ex entry window
// is open. So I'll put focus back in the editor and process the key. // is open. So I'll put focus back in the editor and process the key.
val panel = ExEntryPanel.getInstance() val panel = ExEntryPanel.getInstance()
if (panel.isActive) { if (panel.isActive) {
requestFocus(panel.entry) processResultBuilder.addExecutionStep { _, _, _ ->
panel.handleKey(stroke) requestFocus(panel.entry)
panel.handleKey(stroke)
}
return true return true
} else { } else {
editor.mode = NORMAL() processResultBuilder.addExecutionStep { _, lambdaEditor, _ ->
getInstance().reset(editor) lambdaEditor.mode = NORMAL()
getInstance().reset(lambdaEditor)
}
return false return false
} }
} }

View File

@ -86,6 +86,19 @@ public class KeyHandler {
mappingCompleted: Boolean, mappingCompleted: Boolean,
keyState: KeyHandlerState, keyState: KeyHandlerState,
) { ) {
val result = processKey(key, editor, allowKeyMappings, mappingCompleted, KeyProcessResult.SynchronousKeyProcessBuilder(keyState))
if (result is KeyProcessResult.Executable) {
result.execute(editor, context)
}
}
private fun processKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
processBuilder: KeyProcessResult.KeyProcessResultBuilder,
): KeyProcessResult {
LOG.trace { LOG.trace {
""" """
------- Key Handler ------- ------- Key Handler -------
@ -95,59 +108,69 @@ public class KeyHandler {
} }
val maxMapDepth = injector.globalOptions().maxmapdepth val maxMapDepth = injector.globalOptions().maxmapdepth
if (handleKeyRecursionCount >= maxMapDepth) { if (handleKeyRecursionCount >= maxMapDepth) {
injector.messages.showStatusBarMessage(editor, injector.messages.message("E223")) processBuilder.addExecutionStep { _, lambdaEditor, _ ->
injector.messages.indicateError() LOG.warn("Key handling, maximum recursion of the key received. maxdepth=$maxMapDepth")
LOG.warn("Key handling, maximum recursion of the key received. maxdepth=$maxMapDepth") injector.messages.showStatusBarMessage(lambdaEditor, injector.messages.message("E223"))
return injector.messages.indicateError()
}
return processBuilder.build()
} }
val newState = keyState ?: this.keyHandlerState
injector.messages.clearError() injector.messages.clearError()
val editorState = editor.vimStateMachine val editorState = editor.vimStateMachine
val commandBuilder = newState.commandBuilder val commandBuilder = processBuilder.state.commandBuilder
// If this is a "regular" character keystroke, get the character // If this is a "regular" character keystroke, get the character
val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar
// We only record unmapped keystrokes. If we've recursed to handle mapping, don't record anything. // We only record unmapped keystrokes. If we've recursed to handle mapping, don't record anything.
var shouldRecord = handleKeyRecursionCount == 0 && injector.registerGroup.isRecording var shouldRecord = handleKeyRecursionCount == 0 && injector.registerGroup.isRecording
var isProcessed = false
handleKeyRecursionCount++ handleKeyRecursionCount++
try { try {
LOG.trace("Start key processing...") LOG.trace("Start key processing...")
if (!allowKeyMappings || !MappingProcessor.handleKeyMapping(editor, key, newState, context, mappingCompleted)) { if (!MappingProcessor.handleKeyMapping(key, editor, allowKeyMappings, mappingCompleted, processBuilder)) {
LOG.trace("Mappings processed, continue processing key.") LOG.trace("Mappings processed, continue processing key.")
if (isCommandCountKey(chKey, newState, editorState)) { if (isCommandCountKey(chKey, processBuilder.state, editorState)) {
commandBuilder.addCountCharacter(key) commandBuilder.addCountCharacter(key)
} else if (isDeleteCommandCountKey(key, newState, editorState.mode)) { isProcessed = true
} else if (isDeleteCommandCountKey(key, processBuilder.state, editorState.mode)) {
commandBuilder.deleteCountCharacter() commandBuilder.deleteCountCharacter()
isProcessed = true
} else if (isEditorReset(key, editorState)) { } else if (isEditorReset(key, editorState)) {
handleEditorReset(editor, key, newState, context) processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext -> handleEditorReset(lambdaEditor, key, lambdaKeyState, lambdaContext) }
isProcessed = true
} else if (isExpectingCharArgument(commandBuilder)) { } else if (isExpectingCharArgument(commandBuilder)) {
handleCharArgument(key, chKey, newState, editor) handleCharArgument(key, chKey, processBuilder.state, editor)
isProcessed = true
} else if (editorState.isRegisterPending) { } else if (editorState.isRegisterPending) {
LOG.trace("Pending mode.") LOG.trace("Pending mode.")
commandBuilder.addKey(key) commandBuilder.addKey(key)
handleSelectRegister(editorState, chKey, newState) handleSelectRegister(editorState, chKey, processBuilder.state)
} else if (!handleDigraph(editor, key, newState, context)) { isProcessed = true
} else if (!handleDigraph(editor, key, processBuilder)) {
LOG.debug("Digraph is NOT processed") LOG.debug("Digraph is NOT processed")
// Ask the key/action tree if this is an appropriate key at this point in the command and if so, // Ask the key/action tree if this is an appropriate key at this point in the command and if so,
// return the node matching this keystroke // return the node matching this keystroke
val node: Node<LazyVimCommand>? = mapOpCommand(key, commandBuilder.getChildNode(key), editorState.mode, newState) val node: Node<LazyVimCommand>? = mapOpCommand(key, commandBuilder.getChildNode(key), editorState.mode, processBuilder.state)
LOG.trace("Get the node for the current mode") LOG.trace("Get the node for the current mode")
if (node is CommandNode<LazyVimCommand>) { if (node is CommandNode<LazyVimCommand>) {
LOG.trace("Node is a command node") LOG.trace("Node is a command node")
handleCommandNode(editor, context, key, node, newState, editorState) handleCommandNode(key, node, processBuilder)
commandBuilder.addKey(key) commandBuilder.addKey(key)
isProcessed = true
} else if (node is CommandPartNode<LazyVimCommand>) { } else if (node is CommandPartNode<LazyVimCommand>) {
LOG.trace("Node is a command part node") LOG.trace("Node is a command part node")
commandBuilder.setCurrentCommandPartNode(node) commandBuilder.setCurrentCommandPartNode(node)
commandBuilder.addKey(key) commandBuilder.addKey(key)
} else if (isSelectRegister(key, newState, editorState)) { isProcessed = true
} else if (isSelectRegister(key, processBuilder.state, editorState)) {
LOG.trace("Select register") LOG.trace("Select register")
editorState.isRegisterPending = true editorState.isRegisterPending = true
commandBuilder.addKey(key) commandBuilder.addKey(key)
isProcessed = true
} else { } else {
// node == null // node == null
LOG.trace("We are not able to find a node for this key") LOG.trace("We are not able to find a node for this key")
@ -155,37 +178,55 @@ public class KeyHandler {
// If we are in insert/replace mode send this key in for processing // If we are in insert/replace mode send this key in for processing
if (editorState.mode == Mode.INSERT || editorState.mode == Mode.REPLACE) { if (editorState.mode == Mode.INSERT || editorState.mode == Mode.REPLACE) {
LOG.trace("Process insert or replace") LOG.trace("Process insert or replace")
shouldRecord = injector.changeGroup.processKey(editor, context, key) && shouldRecord shouldRecord = injector.changeGroup.processKey(editor, key, processBuilder) && shouldRecord
isProcessed = true
} else if (editorState.mode is Mode.SELECT) { } else if (editorState.mode is Mode.SELECT) {
LOG.trace("Process select") LOG.trace("Process select")
shouldRecord = injector.changeGroup.processKeyInSelectMode(editor, context, key) && shouldRecord shouldRecord = injector.changeGroup.processKeyInSelectMode(editor, key, processBuilder) && shouldRecord
isProcessed = true
} else if (editor.mode is Mode.CMD_LINE) { } else if (editor.mode is Mode.CMD_LINE) {
LOG.trace("Process cmd line") LOG.trace("Process cmd line")
shouldRecord = injector.processGroup.processExKey(editor, key) && shouldRecord shouldRecord = injector.processGroup.processExKey(editor, key, processBuilder) && shouldRecord
isProcessed = true
} else { } else {
LOG.trace("Set command state to bad_command") LOG.trace("Set command state to bad_command")
commandBuilder.commandState = CurrentCommandState.BAD_COMMAND commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
} }
partialReset(newState, editorState.mode) partialReset(processBuilder.state, editorState.mode)
} }
} else {
isProcessed = true
}
} else {
isProcessed = true
}
if (isProcessed) {
processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext ->
finishedCommandPreparation(lambdaEditor, lambdaContext, key, shouldRecord, lambdaKeyState)
}
} else {
// Key wasn't processed by any of the consumers, so we reset our key state
// and tell IDE that the key is Unknown (handle key for us)
onUnknownKey(editor, processBuilder.state)
return KeyProcessResult.Unknown.apply {
handleKeyRecursionCount-- // because onFinish will now be executed for unknown
} }
} }
} finally { } finally {
handleKeyRecursionCount-- processBuilder.onFinish = { handleKeyRecursionCount-- }
} }
finishedCommandPreparation(editor, context, editorState, key, shouldRecord, newState) return processBuilder.build()
updateState(newState)
} }
internal fun finishedCommandPreparation( internal fun finishedCommandPreparation(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext, context: ExecutionContext,
editorState: VimStateMachine,
key: KeyStroke?, key: KeyStroke?,
shouldRecord: Boolean, shouldRecord: Boolean,
keyState: KeyHandlerState, keyState: KeyHandlerState,
) { ) {
// Do we have a fully entered command at this point? If so, let's execute it. // Do we have a fully entered command at this point? If so, let's execute it.
val editorState = editor.vimStateMachine
val commandBuilder = keyState.commandBuilder val commandBuilder = keyState.commandBuilder
if (commandBuilder.isReady) { if (commandBuilder.isReady) {
LOG.trace("Ready command builder. Execute command.") LOG.trace("Ready command builder. Execute command.")
@ -211,6 +252,21 @@ public class KeyHandler {
LOG.trace("----------- Key Handler Finished -----------") LOG.trace("----------- Key Handler Finished -----------")
} }
private fun onUnknownKey(editor: VimEditor, keyState: KeyHandlerState) {
keyState.commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
LOG.trace("Command builder is set to BAD")
editor.resetOpPending()
editor.vimStateMachine.resetRegisterPending()
editor.isReplaceCharacter = false
reset(keyState, editor.mode)
}
public fun setBadCommand(editor: VimEditor, keyState: KeyHandlerState) {
onUnknownKey(editor, keyState)
injector.messages.indicateError()
}
/** /**
* See the description for [com.maddyhome.idea.vim.command.DuplicableOperatorAction] * See the description for [com.maddyhome.idea.vim.command.DuplicableOperatorAction]
*/ */
@ -357,18 +413,16 @@ public class KeyHandler {
private fun handleDigraph( private fun handleDigraph(
editor: VimEditor, editor: VimEditor,
key: KeyStroke, key: KeyStroke,
keyState: KeyHandlerState, keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
context: ExecutionContext,
): Boolean { ): Boolean {
LOG.debug("Handling digraph")
// Support starting a digraph/literal sequence if the operator accepts one as an argument, e.g. 'r' or 'f'. // Support starting a digraph/literal sequence if the operator accepts one as an argument, e.g. 'r' or 'f'.
// Normally, we start the sequence (in Insert or CmdLine mode) through a VimAction that can be mapped. Our // Normally, we start the sequence (in Insert or CmdLine mode) through a VimAction that can be mapped. Our
// VimActions don't work as arguments for operators, so we have to special case here. Helpfully, Vim appears to // VimActions don't work as arguments for operators, so we have to special case here. Helpfully, Vim appears to
// hardcode the shortcuts, and doesn't support mapping, so everything works nicely. // hardcode the shortcuts, and doesn't support mapping, so everything works nicely.
val keyState = keyProcessResultBuilder.state
val commandBuilder = keyState.commandBuilder val commandBuilder = keyState.commandBuilder
val digraphSequence = keyState.digraphSequence val digraphSequence = keyState.digraphSequence
if (commandBuilder.expectedArgumentType == Argument.Type.DIGRAPH) { if (commandBuilder.expectedArgumentType == Argument.Type.DIGRAPH) {
LOG.trace("Expected argument is digraph")
if (digraphSequence.isDigraphStart(key)) { if (digraphSequence.isDigraphStart(key)) {
digraphSequence.startDigraphSequence() digraphSequence.startDigraphSequence()
commandBuilder.addKey(key) commandBuilder.addKey(key)
@ -381,34 +435,54 @@ public class KeyHandler {
} }
} }
val res = digraphSequence.processKey(key, editor) val res = digraphSequence.processKey(key, editor)
if (injector.exEntryPanel.isActive()) {
when (res.result) {
DigraphResult.RES_HANDLED -> setPromptCharacterEx(if (commandBuilder.isPuttingLiteral()) '^' else key.keyChar)
DigraphResult.RES_DONE, DigraphResult.RES_BAD -> if (key.keyCode == KeyEvent.VK_C && key.modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
return false
} else {
injector.exEntryPanel.clearCurrentAction()
}
}
}
when (res.result) { when (res.result) {
DigraphResult.RES_HANDLED -> { DigraphResult.RES_HANDLED -> {
commandBuilder.addKey(key) keyProcessResultBuilder.addExecutionStep { lambdaKeyState, _, _ ->
if (injector.exEntryPanel.isActive()) {
setPromptCharacterEx(if (lambdaKeyState.commandBuilder.isPuttingLiteral()) '^' else key.keyChar)
}
lambdaKeyState.commandBuilder.addKey(key)
}
return true return true
} }
DigraphResult.RES_DONE -> { DigraphResult.RES_DONE -> {
if (commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) { if (injector.exEntryPanel.isActive()) {
commandBuilder.fallbackToCharacterArgument() if (key.keyCode == KeyEvent.VK_C && key.modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
return false
} else {
keyProcessResultBuilder.addExecutionStep { _, _, _ ->
injector.exEntryPanel.clearCurrentAction()
}
}
}
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, _, _ ->
if (lambdaKeyState.commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) {
lambdaKeyState.commandBuilder.fallbackToCharacterArgument()
}
} }
val stroke = res.stroke ?: return false val stroke = res.stroke ?: return false
commandBuilder.addKey(key) keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditorState, lambdaContext ->
handleKey(editor, stroke, context, keyState) lambdaKeyState.commandBuilder.addKey(key)
handleKey(lambdaEditorState, stroke, lambdaContext, lambdaKeyState)
}
return true return true
} }
DigraphResult.RES_BAD -> { DigraphResult.RES_BAD -> {
// BAD is an error. We were expecting a valid character, and we didn't get it. if (injector.exEntryPanel.isActive()) {
if (commandBuilder.expectedArgumentType != null) { if (key.keyCode == KeyEvent.VK_C && key.modifiers and InputEvent.CTRL_DOWN_MASK != 0) {
commandBuilder.commandState = CurrentCommandState.BAD_COMMAND return false
} else {
keyProcessResultBuilder.addExecutionStep { _, _, _ ->
injector.exEntryPanel.clearCurrentAction()
}
}
}
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ ->
// BAD is an error. We were expecting a valid character, and we didn't get it.
if (lambdaKeyState.commandBuilder.expectedArgumentType != null) {
setBadCommand(lambdaEditor, lambdaKeyState)
}
} }
return true return true
} }
@ -417,7 +491,9 @@ public class KeyHandler {
// state. E.g. waiting for {char} <BS> {char}. Let the key handler have a go at it. // state. E.g. waiting for {char} <BS> {char}. Let the key handler have a go at it.
if (commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) { if (commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) {
commandBuilder.fallbackToCharacterArgument() commandBuilder.fallbackToCharacterArgument()
handleKey(editor, key, context, keyState) keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext ->
handleKey(lambdaEditor, key, lambdaContext, lambdaKeyState)
}
return true return true
} }
return false return false
@ -469,58 +545,63 @@ public class KeyHandler {
} }
private fun handleCommandNode( private fun handleCommandNode(
editor: VimEditor,
context: ExecutionContext,
key: KeyStroke, key: KeyStroke,
node: CommandNode<LazyVimCommand>, node: CommandNode<LazyVimCommand>,
keyState: KeyHandlerState, processBuilder: KeyProcessResult.KeyProcessResultBuilder,
editorState: VimStateMachine,
) { ) {
LOG.trace("Handle command node") LOG.trace("Handle command node")
// The user entered a valid command. Create the command and add it to the stack. // The user entered a valid command. Create the command and add it to the stack.
val action = node.actionHolder.instance val action = node.actionHolder.instance
val keyState = processBuilder.state
val commandBuilder = keyState.commandBuilder val commandBuilder = keyState.commandBuilder
val expectedArgumentType = commandBuilder.expectedArgumentType val expectedArgumentType = commandBuilder.expectedArgumentType
commandBuilder.pushCommandPart(action) commandBuilder.pushCommandPart(action)
if (!checkArgumentCompatibility(expectedArgumentType, action)) { if (!checkArgumentCompatibility(expectedArgumentType, action)) {
LOG.trace("Return from command node handling") LOG.trace("Return from command node handling")
commandBuilder.commandState = CurrentCommandState.BAD_COMMAND processBuilder.addExecutionStep { lamdaKeyState, lambdaEditor, _ ->
setBadCommand(lambdaEditor, lamdaKeyState)
}
return return
} }
if (action.argumentType == null || stopMacroRecord(node)) { if (action.argumentType == null || stopMacroRecord(node)) {
LOG.trace("Set command state to READY") LOG.trace("Set command state to READY")
commandBuilder.commandState = CurrentCommandState.READY commandBuilder.commandState = CurrentCommandState.READY
} else { } else {
LOG.trace("Set waiting for the argument") processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext ->
val argumentType = action.argumentType LOG.trace("Set waiting for the argument")
startWaitingForArgument(editor, context, key.keyChar, action, argumentType!!, keyState, editorState) val argumentType = action.argumentType
partialReset(keyState, editorState.mode) val editorState = lambdaEditor.vimStateMachine
startWaitingForArgument(lambdaEditor, lambdaContext, key.keyChar, action, argumentType!!, lambdaKeyState, editorState)
lambdaKeyState.partialReset(editorState.mode)
}
} }
// TODO In the name of God, get rid of EX_STRING, FLAG_COMPLETE_EX and all the related staff processBuilder.addExecutionStep { _, lambdaEditor, _ ->
if (expectedArgumentType === Argument.Type.EX_STRING && action.flags.contains(CommandFlags.FLAG_COMPLETE_EX)) { // TODO In the name of God, get rid of EX_STRING, FLAG_COMPLETE_EX and all the related staff
/* The only action that implements FLAG_COMPLETE_EX is ProcessExEntryAction. if (expectedArgumentType === Argument.Type.EX_STRING && action.flags.contains(CommandFlags.FLAG_COMPLETE_EX)) {
* When pressing ':', ExEntryAction is chosen as the command. Since it expects no arguments, it is invoked and /* The only action that implements FLAG_COMPLETE_EX is ProcessExEntryAction.
calls ProcessGroup#startExCommand, pushes CMD_LINE mode, and the action is popped. The ex handler will push * When pressing ':', ExEntryAction is chosen as the command. Since it expects no arguments, it is invoked and
the final <CR> through handleKey, which chooses ProcessExEntryAction. Because we're not expecting EX_STRING, calls ProcessGroup#startExCommand, pushes CMD_LINE mode, and the action is popped. The ex handler will push
this branch does NOT fire, and ProcessExEntryAction handles the ex cmd line entry. the final <CR> through handleKey, which chooses ProcessExEntryAction. Because we're not expecting EX_STRING,
* When pressing '/' or '?', SearchEntry(Fwd|Rev)Action is chosen as the command. This expects an argument of this branch does NOT fire, and ProcessExEntryAction handles the ex cmd line entry.
EX_STRING, so startWaitingForArgument calls ProcessGroup#startSearchCommand. The ex handler pushes the final * When pressing '/' or '?', SearchEntry(Fwd|Rev)Action is chosen as the command. This expects an argument of
<CR> through handleKey, which chooses ProcessExEntryAction, and we hit this branch. We don't invoke EX_STRING, so startWaitingForArgument calls ProcessGroup#startSearchCommand. The ex handler pushes the final
ProcessExEntryAction, but pop it, set the search text as an argument on SearchEntry(Fwd|Rev)Action and invoke <CR> through handleKey, which chooses ProcessExEntryAction, and we hit this branch. We don't invoke
that instead. ProcessExEntryAction, but pop it, set the search text as an argument on SearchEntry(Fwd|Rev)Action and invoke
* When using '/' or '?' as part of a motion (e.g. "d/foo"), the above happens again, and all is good. Because that instead.
the text has been applied as an argument on the last command, '.' will correctly repeat it. * 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 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 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"). operator, which would be invoked first (e.g. 'd' in "d/foo").
*/ */
LOG.trace("Processing ex_string") LOG.trace("Processing ex_string")
val text = injector.processGroup.endSearchCommand() val text = injector.processGroup.endSearchCommand()
commandBuilder.popCommandPart() // Pop ProcessExEntryAction commandBuilder.popCommandPart() // Pop ProcessExEntryAction
commandBuilder.completeCommandPart(Argument(text)) // Set search text on SearchEntry(Fwd|Rev)Action commandBuilder.completeCommandPart(Argument(text)) // Set search text on SearchEntry(Fwd|Rev)Action
editor.mode = editorState.mode.returnTo() lambdaEditor.mode = lambdaEditor.mode.returnTo()
}
} }
} }

View File

@ -7,6 +7,7 @@
*/ */
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
@ -65,9 +66,9 @@ public interface VimChangeGroup {
operatorArguments: OperatorArguments, operatorArguments: OperatorArguments,
): Boolean ): Boolean
public fun processKey(editor: VimEditor, context: ExecutionContext, key: KeyStroke): Boolean public fun processKey(editor: VimEditor, key: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean
public fun processKeyInSelectMode(editor: VimEditor, context: ExecutionContext, key: KeyStroke): Boolean public fun processKeyInSelectMode(editor: VimEditor, key: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean
public fun deleteLine(editor: VimEditor, caret: VimCaret, count: Int, operatorArguments: OperatorArguments): Boolean public fun deleteLine(editor: VimEditor, caret: VimCaret, count: Int, operatorArguments: OperatorArguments): Boolean

View File

@ -9,6 +9,7 @@
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.command.Argument import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.command.CommandFlags
@ -715,18 +716,18 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
*/ */
override fun processKey( override fun processKey(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext,
key: KeyStroke, key: KeyStroke,
processResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
): Boolean { ): Boolean {
logger.debug { "processKey($key)" } logger.debug { "processKey($key)" }
if (key.keyChar != KeyEvent.CHAR_UNDEFINED) { if (key.keyChar != KeyEvent.CHAR_UNDEFINED) {
type(editor, context, key.keyChar) processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, key.keyChar) }
return true return true
} }
// Shift-space // Shift-space
if (key.keyCode == 32 && key.modifiers and KeyEvent.SHIFT_DOWN_MASK != 0) { if (key.keyCode == 32 && key.modifiers and KeyEvent.SHIFT_DOWN_MASK != 0) {
type(editor, context, ' ') processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, ' ') }
return true return true
} }
return false return false
@ -734,16 +735,18 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
override fun processKeyInSelectMode( override fun processKeyInSelectMode(
editor: VimEditor, editor: VimEditor,
context: ExecutionContext,
key: KeyStroke, key: KeyStroke,
processResultBuilder: KeyProcessResult.KeyProcessResultBuilder
): Boolean { ): Boolean {
var res: Boolean var res: Boolean
SelectionVimListenerSuppressor.lock().use { SelectionVimListenerSuppressor.lock().use {
res = processKey(editor, context, key) res = processKey(editor, key, processResultBuilder)
editor.exitSelectModeNative(false) processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext ->
KeyHandler.getInstance().reset(editor) lambdaEditor.exitSelectModeNative(false)
if (isPrintableChar(key.keyChar) || activeTemplateWithLeftRightMotion(editor, key)) { KeyHandler.getInstance().reset(lambdaEditor)
injector.changeGroup.insertBeforeCursor(editor, context) if (isPrintableChar(key.keyChar) || activeTemplateWithLeftRightMotion(lambdaEditor, key)) {
injector.changeGroup.insertBeforeCursor(lambdaEditor, lambdaContext)
}
} }
} }
return res return res

View File

@ -7,6 +7,7 @@
*/ */
package com.maddyhome.idea.vim.api package com.maddyhome.idea.vim.api
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.state.mode.Mode import com.maddyhome.idea.vim.state.mode.Mode
import javax.swing.KeyStroke import javax.swing.KeyStroke
@ -18,7 +19,7 @@ public interface VimProcessGroup {
public fun startSearchCommand(editor: VimEditor, context: ExecutionContext, count: Int, leader: Char) public fun startSearchCommand(editor: VimEditor, context: ExecutionContext, count: Int, leader: Char)
public fun endSearchCommand(): String public fun endSearchCommand(): String
public fun processExKey(editor: VimEditor, stroke: KeyStroke): Boolean public fun processExKey(editor: VimEditor, stroke: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean
public fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) public fun startFilterCommand(editor: VimEditor, context: ExecutionContext, cmd: Command)
public fun startExCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) public fun startExCommand(editor: VimEditor, context: ExecutionContext, cmd: Command)
public fun processExEntry(editor: VimEditor, context: ExecutionContext): Boolean public fun processExEntry(editor: VimEditor, context: ExecutionContext): Boolean

View File

@ -8,6 +8,7 @@
package com.maddyhome.idea.vim.api.stubs package com.maddyhome.idea.vim.api.stubs
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.VimProcessGroupBase import com.maddyhome.idea.vim.api.VimProcessGroupBase
@ -36,7 +37,7 @@ public class VimProcessGroupStub : VimProcessGroupBase() {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
override fun processExKey(editor: VimEditor, stroke: KeyStroke): Boolean { override fun processExKey(editor: VimEditor, stroke: KeyStroke, processResultBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean {
TODO("Not yet implemented") TODO("Not yet implemented")
} }

View File

@ -9,6 +9,7 @@
package com.maddyhome.idea.vim.command package com.maddyhome.idea.vim.command
import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
@ -19,8 +20,8 @@ import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.helper.vimStateMachine import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.impl.state.toMappingMode import com.maddyhome.idea.vim.impl.state.toMappingMode
import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.KeyMappingLayer
import com.maddyhome.idea.vim.key.MappingInfoLayer
import com.maddyhome.idea.vim.state.KeyHandlerState import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.VimStateMachine
import javax.swing.KeyStroke import javax.swing.KeyStroke
public object MappingProcessor { public object MappingProcessor {
@ -28,13 +29,16 @@ public object MappingProcessor {
private val log = vimLogger<MappingProcessor>() private val log = vimLogger<MappingProcessor>()
internal fun handleKeyMapping( internal fun handleKeyMapping(
editor: VimEditor,
key: KeyStroke, key: KeyStroke,
keyState: KeyHandlerState, editor: VimEditor,
context: ExecutionContext, allowKeyMappings: Boolean,
mappingCompleted: Boolean, mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
): Boolean { ): Boolean {
if (!allowKeyMappings) return false
log.debug("Start processing key mappings.") log.debug("Start processing key mappings.")
val keyState = keyProcessResultBuilder.state
val commandState = editor.vimStateMachine val commandState = editor.vimStateMachine
val mappingState = keyState.mappingState val mappingState = keyState.mappingState
val commandBuilder = keyState.commandBuilder val commandBuilder = keyState.commandBuilder
@ -58,9 +62,9 @@ public object MappingProcessor {
// Returns true if any of these methods handle the key. False means that the key is unrelated to mapping and should // Returns true if any of these methods handle the key. False means that the key is unrelated to mapping and should
// be processed as normal. // be processed as normal.
val mappingProcessed = val mappingProcessed =
handleUnfinishedMappingSequence(editor, keyState, mappingState, mapping, mappingCompleted) || handleUnfinishedMappingSequence(keyProcessResultBuilder, mapping, mappingCompleted) ||
handleCompleteMappingSequence(editor, keyState, context, mappingState, mapping, key) || handleCompleteMappingSequence(keyProcessResultBuilder, mapping, key) ||
handleAbandonedMappingSequence(editor, keyState, mappingState, context) handleAbandonedMappingSequence(keyProcessResultBuilder)
log.debug { "Finish mapping processing. Return $mappingProcessed" } log.debug { "Finish mapping processing. Return $mappingProcessed" }
return mappingProcessed return mappingProcessed
@ -76,9 +80,7 @@ public object MappingProcessor {
} }
private fun handleUnfinishedMappingSequence( private fun handleUnfinishedMappingSequence(
editor: VimEditor, processBuilder: KeyProcessResult.KeyProcessResultBuilder,
keyState: KeyHandlerState,
mappingState: MappingState,
mapping: KeyMappingLayer, mapping: KeyMappingLayer,
mappingCompleted: Boolean, mappingCompleted: Boolean,
): Boolean { ): Boolean {
@ -93,7 +95,7 @@ public object MappingProcessor {
// mapping is a prefix, it will get evaluated when the next character is entered. // mapping is a prefix, it will get evaluated when the next character is entered.
// Note that currentlyUnhandledKeySequence is the same as the state after commandState.getMappingKeys().add(key). It // Note that currentlyUnhandledKeySequence is the same as the state after commandState.getMappingKeys().add(key). It
// would be nice to tidy ths up // would be nice to tidy ths up
if (!mapping.isPrefix(mappingState.keys)) { if (!mapping.isPrefix(processBuilder.state.mappingState.keys)) {
log.debug("There are no mappings that start with the current sequence. Returning false.") log.debug("There are no mappings that start with the current sequence. Returning false.")
return false return false
} }
@ -102,6 +104,11 @@ public object MappingProcessor {
// Every time a key is pressed and handled, the timer is stopped. E.g. if there is a mapping for "dweri", and the // Every time a key is pressed and handled, the timer is stopped. E.g. if there is a mapping for "dweri", and the
// user has typed "dw" wait for the timeout, and then replay "d" and "w" without any mapping (which will of course // user has typed "dw" wait for the timeout, and then replay "d" and "w" without any mapping (which will of course
// delete a word) // delete a word)
processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ -> processUnfinishedMappingSequence(lambdaEditor, lambdaKeyState) }
return true
}
private fun processUnfinishedMappingSequence(editor: VimEditor, keyState: KeyHandlerState) {
if (injector.options(editor).timeout) { if (injector.options(editor).timeout) {
log.trace("timeout is set. schedule a mapping timer") log.trace("timeout is set. schedule a mapping timer")
// XXX There is a strange issue that reports that mapping state is empty at the moment of the function call. // XXX There is a strange issue that reports that mapping state is empty at the moment of the function call.
@ -109,6 +116,7 @@ public object MappingProcessor {
// but before invoke later is handled. This is a rare case, so I'll just add a check to isPluginMapping. // but before invoke later is handled. This is a rare case, so I'll just add a check to isPluginMapping.
// But this "unexpected behaviour" exists, and it would be better not to relay on mutable state with delays. // But this "unexpected behaviour" exists, and it would be better not to relay on mutable state with delays.
// https://youtrack.jetbrains.com/issue/VIM-2392 // https://youtrack.jetbrains.com/issue/VIM-2392
val mappingState = keyState.mappingState
mappingState.startMappingTimer { mappingState.startMappingTimer {
injector.application.invokeLater( injector.application.invokeLater(
{ {
@ -127,7 +135,8 @@ public object MappingProcessor {
// of waiting for `abc` mapping. // of waiting for `abc` mapping.
val lastKeyInSequence = index == unhandledKeys.lastIndex val lastKeyInSequence = index == unhandledKeys.lastIndex
KeyHandler.getInstance().handleKey( val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor, editor,
keyStroke, keyStroke,
injector.executionContextManager.onEditor(editor), injector.executionContextManager.onEditor(editor),
@ -142,19 +151,16 @@ public object MappingProcessor {
} }
} }
log.trace("Unfinished mapping processing finished") log.trace("Unfinished mapping processing finished")
return true
} }
private fun handleCompleteMappingSequence( private fun handleCompleteMappingSequence(
editor: VimEditor, processBuilder: KeyProcessResult.KeyProcessResultBuilder,
keyState: KeyHandlerState,
context: ExecutionContext,
mappingState: MappingState,
mapping: KeyMappingLayer, mapping: KeyMappingLayer,
key: KeyStroke, key: KeyStroke,
): Boolean { ): Boolean {
log.trace("Processing complete mapping sequence...") log.trace("Processing complete mapping sequence...")
// The current sequence isn't a prefix, check to see if it's a completed sequence. // The current sequence isn't a prefix, check to see if it's a completed sequence.
val mappingState = processBuilder.state.mappingState
val currentMappingInfo = mapping.getLayer(mappingState.keys) val currentMappingInfo = mapping.getLayer(mappingState.keys)
var mappingInfo = currentMappingInfo var mappingInfo = currentMappingInfo
if (mappingInfo == null) { if (mappingInfo == null) {
@ -180,6 +186,19 @@ public object MappingProcessor {
log.trace("Cannot find any mapping info for the sequence. Return false.") log.trace("Cannot find any mapping info for the sequence. Return false.")
return false return false
} }
processBuilder.addExecutionStep { b, c, d -> processCompleteMappingSequence(key, b, c, d, mappingInfo, currentMappingInfo) }
return true
}
private fun processCompleteMappingSequence(
key: KeyStroke,
keyState: KeyHandlerState,
editor: VimEditor,
context: ExecutionContext,
mappingInfo: MappingInfoLayer,
currentMappingInfo: MappingInfoLayer?,
) {
val mappingState = keyState.mappingState
mappingState.resetMappingSequence() mappingState.resetMappingSequence()
val currentContext = context.updateEditor(editor) val currentContext = context.updateEditor(editor)
log.trace("Executing mapping info") log.trace("Executing mapping info")
@ -216,20 +235,14 @@ public object MappingProcessor {
KeyHandler.getInstance().handleKey(editor, key, currentContext, allowKeyMappings = true, false, keyState) KeyHandler.getInstance().handleKey(editor, key, currentContext, allowKeyMappings = true, false, keyState)
} }
log.trace("Success processing of mapping") log.trace("Success processing of mapping")
return true
} }
private fun handleAbandonedMappingSequence( private fun handleAbandonedMappingSequence(processBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean {
editor: VimEditor,
keyState: KeyHandlerState,
mappingState: MappingState,
context: ExecutionContext,
): Boolean {
log.debug("Processing abandoned mapping sequence") log.debug("Processing abandoned mapping sequence")
// The user has terminated a mapping sequence with an unexpected key // The user has terminated a mapping sequence with an unexpected key
// E.g. if there is a mapping for "hello" and user enters command "help" the processing of "h", "e" and "l" will be // E.g. if there is a mapping for "hello" and user enters command "help" the processing of "h", "e" and "l" will be
// prevented by this handler. Make sure the currently unhandled keys are processed as normal. // prevented by this handler. Make sure the currently unhandled keys are processed as normal.
val unhandledKeyStrokes = mappingState.detachKeys() val unhandledKeyStrokes = processBuilder.state.mappingState.detachKeys()
// If there is only the current key to handle, do nothing // If there is only the current key to handle, do nothing
if (unhandledKeyStrokes.size == 1) { if (unhandledKeyStrokes.size == 1) {
@ -244,6 +257,12 @@ public object MappingProcessor {
// If user enters `dI`, the first `d` will be caught be this handler because it's a prefix for `ds` command. // If user enters `dI`, the first `d` will be caught be this handler because it's a prefix for `ds` command.
// After the user enters `I`, the caught `d` should be processed without mapping, and the rest of keys // After the user enters `I`, the caught `d` should be processed without mapping, and the rest of keys
// should be processed with mappings (to make I work) // should be processed with mappings (to make I work)
processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext ->
processAbondonedMappingSequence(unhandledKeyStrokes, lambdaEditor, lambdaContext, lambdaKeyState) }
return true
}
private fun processAbondonedMappingSequence(unhandledKeyStrokes: List<KeyStroke>, editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
if (isPluginMapping(unhandledKeyStrokes)) { if (isPluginMapping(unhandledKeyStrokes)) {
log.trace("This is a plugin mapping, process it") log.trace("This is a plugin mapping, process it")
KeyHandler.getInstance().handleKey( KeyHandler.getInstance().handleKey(
@ -264,7 +283,6 @@ public object MappingProcessor {
} }
} }
log.trace("Return true from abandoned keys processing.") log.trace("Return true from abandoned keys processing.")
return true
} }
// The <Plug>mappings are not executed if they fail to map to something. // The <Plug>mappings are not executed if they fail to map to something.