diff --git a/src/main/java/com/maddyhome/idea/vim/group/ProcessGroup.kt b/src/main/java/com/maddyhome/idea/vim/group/ProcessGroup.kt index 07e3788ca..e45bb0adc 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/ProcessGroup.kt +++ b/src/main/java/com/maddyhome/idea/vim/group/ProcessGroup.kt @@ -20,6 +20,7 @@ import com.intellij.openapi.progress.ProgressManager import com.intellij.util.execution.ParametersListUtil import com.intellij.util.text.CharSequenceReader 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.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor @@ -90,19 +91,22 @@ public class ProcessGroup : VimProcessGroupBase() { 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 // is open. So I'll put focus back in the editor and process the key. val panel = ExEntryPanel.getInstance() if (panel.isActive) { - requestFocus(panel.entry) - panel.handleKey(stroke) - + processResultBuilder.addExecutionStep { _, _, _ -> + requestFocus(panel.entry) + panel.handleKey(stroke) + } return true } else { - editor.mode = NORMAL() - getInstance().reset(editor) + processResultBuilder.addExecutionStep { _, lambdaEditor, _ -> + lambdaEditor.mode = NORMAL() + getInstance().reset(lambdaEditor) + } return false } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt index 7e6448c0e..9cf87a638 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt @@ -86,6 +86,19 @@ public class KeyHandler { mappingCompleted: Boolean, 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 { """ ------- Key Handler ------- @@ -95,59 +108,69 @@ public class KeyHandler { } val maxMapDepth = injector.globalOptions().maxmapdepth if (handleKeyRecursionCount >= maxMapDepth) { - injector.messages.showStatusBarMessage(editor, injector.messages.message("E223")) - injector.messages.indicateError() - LOG.warn("Key handling, maximum recursion of the key received. maxdepth=$maxMapDepth") - return + processBuilder.addExecutionStep { _, lambdaEditor, _ -> + LOG.warn("Key handling, maximum recursion of the key received. maxdepth=$maxMapDepth") + injector.messages.showStatusBarMessage(lambdaEditor, injector.messages.message("E223")) + injector.messages.indicateError() + } + return processBuilder.build() } - val newState = keyState ?: this.keyHandlerState injector.messages.clearError() val editorState = editor.vimStateMachine - val commandBuilder = newState.commandBuilder + val commandBuilder = processBuilder.state.commandBuilder // If this is a "regular" character keystroke, get the character 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. var shouldRecord = handleKeyRecursionCount == 0 && injector.registerGroup.isRecording + var isProcessed = false handleKeyRecursionCount++ try { 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.") - if (isCommandCountKey(chKey, newState, editorState)) { + if (isCommandCountKey(chKey, processBuilder.state, editorState)) { commandBuilder.addCountCharacter(key) - } else if (isDeleteCommandCountKey(key, newState, editorState.mode)) { + isProcessed = true + } else if (isDeleteCommandCountKey(key, processBuilder.state, editorState.mode)) { commandBuilder.deleteCountCharacter() + isProcessed = true } 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)) { - handleCharArgument(key, chKey, newState, editor) + handleCharArgument(key, chKey, processBuilder.state, editor) + isProcessed = true } else if (editorState.isRegisterPending) { LOG.trace("Pending mode.") commandBuilder.addKey(key) - handleSelectRegister(editorState, chKey, newState) - } else if (!handleDigraph(editor, key, newState, context)) { + handleSelectRegister(editorState, chKey, processBuilder.state) + isProcessed = true + } else if (!handleDigraph(editor, key, processBuilder)) { 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, // 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") if (node is CommandNode<LazyVimCommand>) { LOG.trace("Node is a command node") - handleCommandNode(editor, context, key, node, newState, editorState) + handleCommandNode(key, node, processBuilder) commandBuilder.addKey(key) + isProcessed = true } else if (node is CommandPartNode<LazyVimCommand>) { LOG.trace("Node is a command part node") commandBuilder.setCurrentCommandPartNode(node) commandBuilder.addKey(key) - } else if (isSelectRegister(key, newState, editorState)) { + isProcessed = true + } else if (isSelectRegister(key, processBuilder.state, editorState)) { LOG.trace("Select register") editorState.isRegisterPending = true commandBuilder.addKey(key) + isProcessed = true } else { // node == null 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 (editorState.mode == Mode.INSERT || editorState.mode == Mode.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) { 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) { LOG.trace("Process cmd line") - shouldRecord = injector.processGroup.processExKey(editor, key) && shouldRecord + shouldRecord = injector.processGroup.processExKey(editor, key, processBuilder) && shouldRecord + isProcessed = true } else { LOG.trace("Set command state to 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 { - handleKeyRecursionCount-- + processBuilder.onFinish = { handleKeyRecursionCount-- } } - finishedCommandPreparation(editor, context, editorState, key, shouldRecord, newState) - updateState(newState) + return processBuilder.build() } internal fun finishedCommandPreparation( editor: VimEditor, context: ExecutionContext, - editorState: VimStateMachine, key: KeyStroke?, shouldRecord: Boolean, keyState: KeyHandlerState, ) { // Do we have a fully entered command at this point? If so, let's execute it. + val editorState = editor.vimStateMachine val commandBuilder = keyState.commandBuilder if (commandBuilder.isReady) { LOG.trace("Ready command builder. Execute command.") @@ -211,6 +252,21 @@ public class KeyHandler { 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] */ @@ -357,18 +413,16 @@ public class KeyHandler { private fun handleDigraph( editor: VimEditor, key: KeyStroke, - keyState: KeyHandlerState, - context: ExecutionContext, + keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder, ): Boolean { - LOG.debug("Handling digraph") // 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 // 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. + val keyState = keyProcessResultBuilder.state val commandBuilder = keyState.commandBuilder val digraphSequence = keyState.digraphSequence if (commandBuilder.expectedArgumentType == Argument.Type.DIGRAPH) { - LOG.trace("Expected argument is digraph") if (digraphSequence.isDigraphStart(key)) { digraphSequence.startDigraphSequence() commandBuilder.addKey(key) @@ -381,34 +435,54 @@ public class KeyHandler { } } 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) { 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 } DigraphResult.RES_DONE -> { - if (commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) { - commandBuilder.fallbackToCharacterArgument() + if (injector.exEntryPanel.isActive()) { + 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 - commandBuilder.addKey(key) - handleKey(editor, stroke, context, keyState) + keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditorState, lambdaContext -> + lambdaKeyState.commandBuilder.addKey(key) + handleKey(lambdaEditorState, stroke, lambdaContext, lambdaKeyState) + } return true } DigraphResult.RES_BAD -> { - // BAD is an error. We were expecting a valid character, and we didn't get it. - if (commandBuilder.expectedArgumentType != null) { - commandBuilder.commandState = CurrentCommandState.BAD_COMMAND + if (injector.exEntryPanel.isActive()) { + 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, 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 } @@ -417,7 +491,9 @@ public class KeyHandler { // state. E.g. waiting for {char} <BS> {char}. Let the key handler have a go at it. if (commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) { commandBuilder.fallbackToCharacterArgument() - handleKey(editor, key, context, keyState) + keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext -> + handleKey(lambdaEditor, key, lambdaContext, lambdaKeyState) + } return true } return false @@ -469,58 +545,63 @@ public class KeyHandler { } private fun handleCommandNode( - editor: VimEditor, - context: ExecutionContext, key: KeyStroke, node: CommandNode<LazyVimCommand>, - keyState: KeyHandlerState, - editorState: VimStateMachine, + processBuilder: KeyProcessResult.KeyProcessResultBuilder, ) { LOG.trace("Handle command node") // The user entered a valid command. Create the command and add it to the stack. val action = node.actionHolder.instance + val keyState = processBuilder.state val commandBuilder = keyState.commandBuilder val expectedArgumentType = commandBuilder.expectedArgumentType commandBuilder.pushCommandPart(action) if (!checkArgumentCompatibility(expectedArgumentType, action)) { LOG.trace("Return from command node handling") - commandBuilder.commandState = CurrentCommandState.BAD_COMMAND + processBuilder.addExecutionStep { lamdaKeyState, lambdaEditor, _ -> + setBadCommand(lambdaEditor, lamdaKeyState) + } return } if (action.argumentType == null || stopMacroRecord(node)) { LOG.trace("Set command state to READY") commandBuilder.commandState = CurrentCommandState.READY } else { - LOG.trace("Set waiting for the argument") - val argumentType = action.argumentType - startWaitingForArgument(editor, context, key.keyChar, action, argumentType!!, keyState, editorState) - partialReset(keyState, editorState.mode) + processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext -> + LOG.trace("Set waiting for the argument") + val argumentType = action.argumentType + 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 - 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. + 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"). -*/ - LOG.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 - editor.mode = editorState.mode.returnTo() + 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"). + */ + LOG.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 + lambdaEditor.mode = lambdaEditor.mode.returnTo() + } } } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt index 6e097661c..1e102d28f 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroup.kt @@ -7,6 +7,7 @@ */ 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.Command import com.maddyhome.idea.vim.command.OperatorArguments @@ -65,9 +66,9 @@ public interface VimChangeGroup { operatorArguments: OperatorArguments, ): 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 diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt index 20c960802..7ea3e8a24 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimChangeGroupBase.kt @@ -9,6 +9,7 @@ package com.maddyhome.idea.vim.api 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.Command import com.maddyhome.idea.vim.command.CommandFlags @@ -715,18 +716,18 @@ public abstract class VimChangeGroupBase : VimChangeGroup { */ override fun processKey( editor: VimEditor, - context: ExecutionContext, key: KeyStroke, + processResultBuilder: KeyProcessResult.KeyProcessResultBuilder, ): Boolean { logger.debug { "processKey($key)" } if (key.keyChar != KeyEvent.CHAR_UNDEFINED) { - type(editor, context, key.keyChar) + processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> type(lambdaEditor, lambdaContext, key.keyChar) } return true } // Shift-space 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 false @@ -734,16 +735,18 @@ public abstract class VimChangeGroupBase : VimChangeGroup { override fun processKeyInSelectMode( editor: VimEditor, - context: ExecutionContext, key: KeyStroke, + processResultBuilder: KeyProcessResult.KeyProcessResultBuilder ): Boolean { var res: Boolean SelectionVimListenerSuppressor.lock().use { - res = processKey(editor, context, key) - editor.exitSelectModeNative(false) - KeyHandler.getInstance().reset(editor) - if (isPrintableChar(key.keyChar) || activeTemplateWithLeftRightMotion(editor, key)) { - injector.changeGroup.insertBeforeCursor(editor, context) + res = processKey(editor, key, processResultBuilder) + processResultBuilder.addExecutionStep { _, lambdaEditor, lambdaContext -> + lambdaEditor.exitSelectModeNative(false) + KeyHandler.getInstance().reset(lambdaEditor) + if (isPrintableChar(key.keyChar) || activeTemplateWithLeftRightMotion(lambdaEditor, key)) { + injector.changeGroup.insertBeforeCursor(lambdaEditor, lambdaContext) + } } } return res diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimProcessGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimProcessGroup.kt index 4f02e8c98..954619b3c 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimProcessGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimProcessGroup.kt @@ -7,6 +7,7 @@ */ package com.maddyhome.idea.vim.api +import com.maddyhome.idea.vim.KeyProcessResult import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.state.mode.Mode import javax.swing.KeyStroke @@ -18,7 +19,7 @@ public interface VimProcessGroup { public fun startSearchCommand(editor: VimEditor, context: ExecutionContext, count: Int, leader: Char) 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 startExCommand(editor: VimEditor, context: ExecutionContext, cmd: Command) public fun processExEntry(editor: VimEditor, context: ExecutionContext): Boolean diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/stubs/VimProcessGroupStub.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/stubs/VimProcessGroupStub.kt index baebd1ac3..9af96ff86 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/stubs/VimProcessGroupStub.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/stubs/VimProcessGroupStub.kt @@ -8,6 +8,7 @@ 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.VimEditor import com.maddyhome.idea.vim.api.VimProcessGroupBase @@ -36,7 +37,7 @@ public class VimProcessGroupStub : VimProcessGroupBase() { 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") } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt index 722476ae4..2095c05fb 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt @@ -9,6 +9,7 @@ package com.maddyhome.idea.vim.command 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.VimEditor 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.impl.state.toMappingMode 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.VimStateMachine import javax.swing.KeyStroke public object MappingProcessor { @@ -28,13 +29,16 @@ public object MappingProcessor { private val log = vimLogger<MappingProcessor>() internal fun handleKeyMapping( - editor: VimEditor, key: KeyStroke, - keyState: KeyHandlerState, - context: ExecutionContext, + editor: VimEditor, + allowKeyMappings: Boolean, mappingCompleted: Boolean, + keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder, ): Boolean { + if (!allowKeyMappings) return false + log.debug("Start processing key mappings.") + val keyState = keyProcessResultBuilder.state val commandState = editor.vimStateMachine val mappingState = keyState.mappingState 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 // be processed as normal. val mappingProcessed = - handleUnfinishedMappingSequence(editor, keyState, mappingState, mapping, mappingCompleted) || - handleCompleteMappingSequence(editor, keyState, context, mappingState, mapping, key) || - handleAbandonedMappingSequence(editor, keyState, mappingState, context) + handleUnfinishedMappingSequence(keyProcessResultBuilder, mapping, mappingCompleted) || + handleCompleteMappingSequence(keyProcessResultBuilder, mapping, key) || + handleAbandonedMappingSequence(keyProcessResultBuilder) log.debug { "Finish mapping processing. Return $mappingProcessed" } return mappingProcessed @@ -76,9 +80,7 @@ public object MappingProcessor { } private fun handleUnfinishedMappingSequence( - editor: VimEditor, - keyState: KeyHandlerState, - mappingState: MappingState, + processBuilder: KeyProcessResult.KeyProcessResultBuilder, mapping: KeyMappingLayer, mappingCompleted: Boolean, ): Boolean { @@ -93,7 +95,7 @@ public object MappingProcessor { // 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 // 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.") 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 // user has typed "dw" wait for the timeout, and then replay "d" and "w" without any mapping (which will of course // delete a word) + processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ -> processUnfinishedMappingSequence(lambdaEditor, lambdaKeyState) } + return true + } + + private fun processUnfinishedMappingSequence(editor: VimEditor, keyState: KeyHandlerState) { if (injector.options(editor).timeout) { 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. @@ -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 this "unexpected behaviour" exists, and it would be better not to relay on mutable state with delays. // https://youtrack.jetbrains.com/issue/VIM-2392 + val mappingState = keyState.mappingState mappingState.startMappingTimer { injector.application.invokeLater( { @@ -127,7 +135,8 @@ public object MappingProcessor { // of waiting for `abc` mapping. val lastKeyInSequence = index == unhandledKeys.lastIndex - KeyHandler.getInstance().handleKey( + val keyHandler = KeyHandler.getInstance() + keyHandler.handleKey( editor, keyStroke, injector.executionContextManager.onEditor(editor), @@ -142,19 +151,16 @@ public object MappingProcessor { } } log.trace("Unfinished mapping processing finished") - return true } private fun handleCompleteMappingSequence( - editor: VimEditor, - keyState: KeyHandlerState, - context: ExecutionContext, - mappingState: MappingState, + processBuilder: KeyProcessResult.KeyProcessResultBuilder, mapping: KeyMappingLayer, key: KeyStroke, ): Boolean { log.trace("Processing complete mapping 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) var mappingInfo = currentMappingInfo if (mappingInfo == null) { @@ -180,6 +186,19 @@ public object MappingProcessor { log.trace("Cannot find any mapping info for the sequence. 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() val currentContext = context.updateEditor(editor) log.trace("Executing mapping info") @@ -216,20 +235,14 @@ public object MappingProcessor { KeyHandler.getInstance().handleKey(editor, key, currentContext, allowKeyMappings = true, false, keyState) } log.trace("Success processing of mapping") - return true } - private fun handleAbandonedMappingSequence( - editor: VimEditor, - keyState: KeyHandlerState, - mappingState: MappingState, - context: ExecutionContext, - ): Boolean { + private fun handleAbandonedMappingSequence(processBuilder: KeyProcessResult.KeyProcessResultBuilder): Boolean { log.debug("Processing abandoned mapping sequence") // 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 // 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 (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. // 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) + 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)) { log.trace("This is a plugin mapping, process it") KeyHandler.getInstance().handleKey( @@ -264,7 +283,6 @@ public object MappingProcessor { } } log.trace("Return true from abandoned keys processing.") - return true } // The <Plug>mappings are not executed if they fail to map to something.