1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-01-11 10:42:47 +01:00

Merge pull request from JetBrains/fleet

Asynchronous key processing for Fleet
This commit is contained in:
lippfi 2024-02-23 17:25:21 +02:00 committed by GitHub
commit 00808af569
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
88 changed files with 1942 additions and 1077 deletions
gradle.properties
src
tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased
vim-engine

View File

@ -28,7 +28,7 @@ publishChannels=eap
# Kotlinx serialization also uses some version of kotlin stdlib under the hood. However,
# we exclude this version from the dependency and use our own version of kotlin that is specified above
kotlinxSerializationVersion=1.5.1
kotlinxSerializationVersion=1.6.2
slackUrl=
youtrackToken=

View File

@ -211,22 +211,22 @@ public class VimPlugin implements PersistentStateComponent<Element>, Disposable
public static void setEnabled(final boolean enabled) {
if (isEnabled() == enabled) return;
if (!enabled) {
getInstance().turnOffPlugin(true);
}
getInstance().enabled = enabled;
if (enabled) {
getInstance().turnOnPlugin();
}
if (enabled) {
VimInjectorKt.getInjector().getListenersNotifier().notifyPluginTurnedOn();
} else {
VimInjectorKt.getInjector().getListenersNotifier().notifyPluginTurnedOff();
}
if (!enabled) {
getInstance().turnOffPlugin(true);
}
if (enabled) {
getInstance().turnOnPlugin();
}
StatusBarIconFactory.Util.INSTANCE.updateIcon();
}

View File

@ -77,7 +77,7 @@ public class VimTypedActionHandler(origHandler: TypedActionHandler) : TypedActio
val modifiers = if (charTyped == ' ' && VimKeyListener.isSpaceShift) KeyEvent.SHIFT_DOWN_MASK else 0
val keyStroke = KeyStroke.getKeyStroke(charTyped, modifiers)
val startTime = if (traceTime) System.currentTimeMillis() else null
handler.handleKey(editor.vim, keyStroke, injector.executionContextManager.onEditor(editor.vim, context.vim))
handler.handleKey(editor.vim, keyStroke, injector.executionContextManager.onEditor(editor.vim, context.vim), handler.keyHandlerState)
if (startTime != null) {
val duration = System.currentTimeMillis() - startTime
LOG.info("VimTypedAction '$charTyped': $duration ms")

View File

@ -80,10 +80,12 @@ public class VimShortcutKeyAction : AnAction(), DumbAware, ActionRemoteBehaviorS
// Should we use HelperKt.getTopLevelEditor(editor) here, as we did in former EditorKeyHandler?
try {
val start = if (traceTime) System.currentTimeMillis() else null
KeyHandler.getInstance().handleKey(
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor.vim,
keyStroke,
injector.executionContextManager.onEditor(editor.vim, e.dataContext.vim),
keyHandler.keyHandlerState,
)
if (start != null) {
val duration = System.currentTimeMillis() - start

View File

@ -21,7 +21,7 @@ import com.maddyhome.idea.vim.state.mode.SelectionType
public class CommandState(private val machine: VimStateMachine) {
public val isOperatorPending: Boolean
get() = machine.isOperatorPending
get() = machine.isOperatorPending(machine.mode)
public val mode: Mode
get() {

View File

@ -143,7 +143,8 @@ public object VimExtensionFacade {
@JvmStatic
public fun executeNormalWithoutMapping(keys: List<KeyStroke>, editor: Editor) {
val context = injector.executionContextManager.onEditor(editor.vim)
keys.forEach { KeyHandler.getInstance().handleKey(editor.vim, it, context, false, false) }
val keyHandler = KeyHandler.getInstance()
keys.forEach { keyHandler.handleKey(editor.vim, it, context, false, false, keyHandler.keyHandlerState) }
}
/** Returns a single key stroke from the user input similar to 'getchar()'. */
@ -159,7 +160,7 @@ public object VimExtensionFacade {
LOG.trace("Unit test mode is active")
val mappingStack = KeyHandler.getInstance().keyStack
mappingStack.feedSomeStroke() ?: TestInputModel.getInstance(editor).nextKeyStroke()?.also {
if (editor.vim.vimStateMachine.isRecording) {
if (injector.registerGroup.isRecording) {
KeyHandler.getInstance().modalEntryKeys += it
}
}

View File

@ -251,7 +251,7 @@ public class VimArgTextObjExtension implements VimExtension {
final ArgumentTextObjectHandler textObjectHandler = new ArgumentTextObjectHandler(isInner);
//noinspection DuplicatedCode
if (!vimStateMachine.isOperatorPending()) {
if (!vimStateMachine.isOperatorPending(editor.getMode())) {
editor.nativeCarets().forEach((VimCaret caret) -> {
final TextRange range = textObjectHandler.getRange(editor, caret, context, count, 0);
if (range != null) {

View File

@ -99,7 +99,7 @@ internal class Matchit : VimExtension {
// Normally we want to jump to the start of the matching pair. But when moving forward in operator
// pending mode, we want to include the entire match. isInOpPending makes that distinction.
val isInOpPending = commandState.isOperatorPending
val isInOpPending = commandState.isOperatorPending(editor.mode)
if (isInOpPending) {
val matchitAction = MatchitAction()

View File

@ -31,7 +31,6 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.putKeyMappingIfMissin
import com.maddyhome.idea.vim.extension.exportOperatorFunction
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.helper.exitVisualMode
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.OperatorFunction
import com.maddyhome.idea.vim.newapi.IjVimEditor
@ -163,13 +162,14 @@ internal class ReplaceWithRegister : VimExtension {
caretAfterInsertedText = false,
putToLine = -1,
)
val vimEditor = editor.vim
ClipboardOptionHelper.IdeaputDisabler().use {
VimPlugin.getPut().putText(
IjVimEditor(editor),
vimEditor,
injector.executionContextManager.onEditor(editor.vim),
putData,
operatorArguments = OperatorArguments(
editor.vimStateMachine?.isOperatorPending ?: false,
editor.vimStateMachine?.isOperatorPending(vimEditor.mode) ?: false,
0,
editor.vim.mode,
),

View File

@ -138,7 +138,7 @@ public class VimTextObjEntireExtension implements VimExtension {
final EntireTextObjectHandler textObjectHandler = new EntireTextObjectHandler(ignoreLeadingAndTrailing);
//noinspection DuplicatedCode
if (!vimStateMachine.isOperatorPending()) {
if (!vimStateMachine.isOperatorPending(editor.getMode())) {
((IjVimEditor) editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> {
final TextRange range = textObjectHandler.getRange(editor, new IjVimCaret(caret), context, count, 0);
if (range != null) {

View File

@ -267,7 +267,7 @@ public class VimIndentObject implements VimExtension {
final IndentObjectHandler textObjectHandler = new IndentObjectHandler(includeAbove, includeBelow);
if (!vimStateMachine.isOperatorPending()) {
if (!vimStateMachine.isOperatorPending(editor.getMode())) {
((IjVimEditor)editor).getEditor().getCaretModel().runForEachCaret((Caret caret) -> {
final TextRange range = textObjectHandler.getRange(vimEditor, new IjVimCaret(caret), context, count, 0);
if (range != null) {

View File

@ -251,7 +251,7 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
switchToInsertMode.run();
}
});
updateCaretsVisualAttributes(editor);
updateCaretsVisualAttributes(new IjVimEditor(editor));
}
public void editorDeinit(@NotNull Editor editor, boolean isReleased) {
@ -288,6 +288,18 @@ public class EditorGroup implements PersistentStateComponent<Element>, VimEditor
notifyIdeaJoin(((IjVimEditor) editor).getEditor().getProject(), editor);
}
@Override
public void updateCaretsVisualAttributes(@NotNull VimEditor editor) {
Editor ijEditor = ((IjVimEditor) editor).getEditor();
CaretVisualAttributesHelperKt.updateCaretsVisualAttributes(ijEditor);
}
@Override
public void updateCaretsVisualPosition(@NotNull VimEditor editor) {
Editor ijEditor = ((IjVimEditor) editor).getEditor();
CaretVisualAttributesHelperKt.updateCaretsVisualAttributes(ijEditor);
}
public static class NumberChangeListener implements EffectiveOptionValueChangeListener {
public static NumberChangeListener INSTANCE = new NumberChangeListener();

View File

@ -15,6 +15,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.progress.util.PotemkinProgress
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyHandler.Companion.getInstance
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
@ -77,11 +78,12 @@ internal class MacroGroup : VimMacroBase() {
} catch (e: ProcessCanceledException) {
return@runnable
}
val keyHandler = KeyHandler.getInstance()
ProgressManager.getInstance().executeNonCancelableSection {
// Prevent autocompletion during macros.
// See https://github.com/JetBrains/ideavim/pull/772 for details
CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion)
getInstance().handleKey(editor, key, context)
keyHandler.handleKey(editor, key, context, keyHandler.keyHandlerState)
}
if (injector.messages.isError()) return@runnable
}

View File

@ -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
@ -37,7 +38,6 @@ import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.Mode.NORMAL
import com.maddyhome.idea.vim.state.mode.Mode.VISUAL
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext
import java.io.BufferedWriter
@ -85,24 +85,27 @@ public class ProcessGroup : VimProcessGroupBase() {
modeBeforeCommandProcessing = currentMode
val initText = getRange(editor, cmd)
injector.markService.setVisualSelectionMarks(editor)
editor.vimStateMachine.mode = Mode.CMD_LINE(currentMode)
editor.mode = Mode.CMD_LINE(currentMode)
val panel = ExEntryPanel.getInstance()
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 {
getInstance(editor).mode = NORMAL()
getInstance().reset(editor)
processResultBuilder.addExecutionStep { _, lambdaEditor, _ ->
lambdaEditor.mode = NORMAL()
getInstance().reset(lambdaEditor)
}
return false
}
}
@ -112,7 +115,7 @@ public class ProcessGroup : VimProcessGroupBase() {
panel.deactivate(true)
var res = true
try {
getInstance(editor).mode = NORMAL()
editor.mode = NORMAL()
logger.debug("processing command")
@ -152,7 +155,7 @@ public class ProcessGroup : VimProcessGroupBase() {
}
public override fun cancelExEntry(editor: VimEditor, resetCaret: Boolean) {
editor.vimStateMachine.mode = NORMAL()
editor.mode = NORMAL()
getInstance().reset(editor)
val panel = ExEntryPanel.getInstance()
panel.deactivate(true, resetCaret)
@ -162,7 +165,7 @@ public class ProcessGroup : VimProcessGroupBase() {
val initText = getRange(editor, cmd) + "!"
val currentMode = editor.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable cmd mode from $currentMode" }
editor.vimStateMachine.mode = Mode.CMD_LINE(currentMode)
editor.mode = Mode.CMD_LINE(currentMode)
val panel = ExEntryPanel.getInstance()
panel.activate(editor.ij, context.ij, ":", initText, 1)
}

View File

@ -77,7 +77,7 @@ internal object IdeaSelectionControl {
logger.debug("Some carets have selection. State before adjustment: ${editor.vim.mode}")
editor.vim.vimStateMachine.mode = Mode.NORMAL()
editor.vim.mode = Mode.NORMAL()
activateMode(editor, chooseSelectionMode(editor, selectionSource, true))
} else {

View File

@ -339,7 +339,8 @@ internal abstract class VimKeyHandler(nextHandler: EditorActionHandler?) : Octop
override fun executeHandler(editor: Editor, caret: Caret?, dataContext: DataContext?) {
val enterKey = key(key)
val context = injector.executionContextManager.onEditor(editor.vim, dataContext?.vim)
KeyHandler.getInstance().handleKey(editor.vim, enterKey, context)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor.vim, enterKey, context, keyHandler.keyHandlerState)
}
override fun isHandlerEnabled(editor: Editor, dataContext: DataContext?): Boolean {

View File

@ -19,14 +19,17 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.IsReplaceCharListener
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.options.EffectiveOptionValueChangeListener
import com.maddyhome.idea.vim.options.helpers.GuiCursorMode
import com.maddyhome.idea.vim.options.helpers.GuiCursorOptionHelper
import com.maddyhome.idea.vim.options.helpers.GuiCursorType
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.mode
import org.jetbrains.annotations.TestOnly
import java.awt.Color
@ -138,3 +141,31 @@ private object AttributesCache {
@TestOnly
internal fun getGuiCursorMode(editor: Editor) = editor.guicursorMode()
public class CaretVisualAttributesListener : IsReplaceCharListener, ModeChangeListener {
override fun isReplaceCharChanged(editor: VimEditor) {
updateCaretsVisual(editor)
}
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
updateCaretsVisual(editor)
}
private fun updateCaretsVisual(editor: VimEditor) {
if (injector.globalOptions().ideaglobalmode) {
updateAllEditorsCaretsVisual()
} else {
val ijEditor = (editor as IjVimEditor).editor
ijEditor.updateCaretsVisualAttributes()
ijEditor.updateCaretsVisualPosition()
}
}
public fun updateAllEditorsCaretsVisual() {
injector.editorGroup.getEditors().forEach { editor ->
val ijEditor = (editor as IjVimEditor).editor
ijEditor.updateCaretsVisualAttributes()
ijEditor.updateCaretsVisualPosition()
}
}
}

View File

@ -34,15 +34,15 @@ internal fun Editor.exitSelectMode(adjustCaretPosition: Boolean) {
val returnTo = this.vim.vimStateMachine.mode.returnTo
when (returnTo) {
ReturnTo.INSERT -> {
this.vim.vimStateMachine.mode = Mode.INSERT
this.vim.mode = Mode.INSERT
}
ReturnTo.REPLACE -> {
this.vim.vimStateMachine.mode = Mode.REPLACE
this.vim.mode = Mode.REPLACE
}
null -> {
this.vim.vimStateMachine.mode = Mode.NORMAL()
this.vim.mode = Mode.NORMAL()
}
}
SelectionVimListenerSuppressor.lock().use {
@ -67,15 +67,15 @@ internal fun VimEditor.exitSelectMode(adjustCaretPosition: Boolean) {
val returnTo = this.vimStateMachine.mode.returnTo
when (returnTo) {
ReturnTo.INSERT -> {
this.vimStateMachine.mode = Mode.INSERT
this.mode = Mode.INSERT
}
ReturnTo.REPLACE -> {
this.vimStateMachine.mode = Mode.REPLACE
this.mode = Mode.REPLACE
}
null -> {
this.vimStateMachine.mode = Mode.NORMAL()
this.mode = Mode.NORMAL()
}
}
SelectionVimListenerSuppressor.lock().use {

View File

@ -75,7 +75,7 @@ internal object IdeaSpecifics {
}
}
if (hostEditor != null && action is ChooseItemAction && hostEditor.vimStateMachine?.isRecording == true) {
if (hostEditor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
val lookup = LookupManager.getActiveLookup(hostEditor)
if (lookup != null) {
val charsToRemove = hostEditor.caretModel.primaryCaret.offset - lookup.lookupStart
@ -96,7 +96,7 @@ internal object IdeaSpecifics {
if (VimPlugin.isNotEnabled()) return
val editor = editor
if (editor != null && action is ChooseItemAction && editor.vimStateMachine?.isRecording == true) {
if (editor != null && action is ChooseItemAction && injector.registerGroup.isRecording) {
val prevDocumentLength = completionPrevDocumentLength
val prevDocumentOffset = completionPrevDocumentOffset
@ -125,7 +125,7 @@ internal object IdeaSpecifics {
) {
editor?.let {
val commandState = it.vim.vimStateMachine
commandState.mode = Mode.NORMAL()
it.vim.mode = Mode.NORMAL()
VimPlugin.getChange().insertBeforeCursor(it.vim, event.dataContext.vim)
KeyHandler.getInstance().reset(it.vim)
}

View File

@ -76,6 +76,7 @@ import com.maddyhome.idea.vim.group.visual.VimVisualTimer
import com.maddyhome.idea.vim.group.visual.moveCaretOneCharLeftFromSelectionEnd
import com.maddyhome.idea.vim.handler.correctorRequester
import com.maddyhome.idea.vim.handler.keyCheckRequests
import com.maddyhome.idea.vim.helper.CaretVisualAttributesListener
import com.maddyhome.idea.vim.helper.GuicursorChangeListener
import com.maddyhome.idea.vim.helper.StrictMode
import com.maddyhome.idea.vim.helper.VimStandalonePluginUpdateChecker
@ -93,11 +94,12 @@ import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.inSelectMode
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.ui.ShowCmdOptionChangeListener
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import com.maddyhome.idea.vim.ui.widgets.macro.MacroWidgetListener
import com.maddyhome.idea.vim.ui.widgets.macro.macroWidgetOptionListener
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetListener
import com.maddyhome.idea.vim.ui.widgets.mode.modeWidgetOptionListener
import com.maddyhome.idea.vim.vimDisposable
import java.awt.event.MouseAdapter
@ -137,11 +139,27 @@ internal object VimListenerManager {
EditorListeners.addAll()
check(correctorRequester.tryEmit(Unit))
check(keyCheckRequests.tryEmit(Unit))
val caretVisualAttributesListener = CaretVisualAttributesListener()
injector.listenersNotifier.modeChangeListeners.add(caretVisualAttributesListener)
injector.listenersNotifier.isReplaceCharListeners.add(caretVisualAttributesListener)
caretVisualAttributesListener.updateAllEditorsCaretsVisual()
val modeWidgetListener = ModeWidgetListener()
injector.listenersNotifier.modeChangeListeners.add(modeWidgetListener)
injector.listenersNotifier.myEditorListeners.add(modeWidgetListener)
injector.listenersNotifier.vimPluginListeners.add(modeWidgetListener)
val macroWidgetListener = MacroWidgetListener()
injector.listenersNotifier.macroRecordingListeners.add(macroWidgetListener)
injector.listenersNotifier.vimPluginListeners.add(macroWidgetListener)
}
fun turnOff() {
GlobalListeners.disable()
EditorListeners.removeAll()
injector.listenersNotifier.reset()
check(correctorRequester.tryEmit(Unit))
}

View File

@ -35,6 +35,7 @@ import com.maddyhome.idea.vim.api.VimScrollingModel
import com.maddyhome.idea.vim.api.VimSelectionModel
import com.maddyhome.idea.vim.api.VimVisualPosition
import com.maddyhome.idea.vim.api.VirtualFile
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.EditorLine
import com.maddyhome.idea.vim.common.IndentConfig
@ -51,13 +52,16 @@ import com.maddyhome.idea.vim.helper.fileSize
import com.maddyhome.idea.vim.helper.getTopLevelEditor
import com.maddyhome.idea.vim.helper.inExMode
import com.maddyhome.idea.vim.helper.isTemplateActive
import com.maddyhome.idea.vim.helper.updateCaretsVisualAttributes
import com.maddyhome.idea.vim.helper.updateCaretsVisualPosition
import com.maddyhome.idea.vim.helper.vimChangeActionSwitchMode
import com.maddyhome.idea.vim.helper.vimLastSelectionType
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl
import com.maddyhome.idea.vim.impl.state.toMappingMode
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.returnTo
import org.jetbrains.annotations.ApiStatus
import java.lang.System.identityHashCode
@ -243,14 +247,6 @@ internal class IjVimEditor(editor: Editor) : MutableLinearEditor() {
}
}
override fun updateCaretsVisualAttributes() {
editor.updateCaretsVisualAttributes()
}
override fun updateCaretsVisualPosition() {
editor.updateCaretsVisualPosition()
}
override fun offsetToVisualPosition(offset: Int): VimVisualPosition {
return editor.offsetToVisualPosition(offset).let { VimVisualPosition(it.line, it.column, it.leansRight) }
}

View File

@ -207,7 +207,7 @@ internal class IjVimInjector : VimInjectorBase() {
override fun commandStateFor(editor: VimEditor): VimStateMachine {
var res = editor.ij.vimStateMachine
if (res == null) {
res = VimStateMachineImpl(editor)
res = VimStateMachineImpl()
editor.ij.vimStateMachine = res
}
return res

View File

@ -13,6 +13,7 @@ import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.diagnostic.trace
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.isCloseKeyStroke
import com.maddyhome.idea.vim.helper.vimStateMachine
import java.awt.KeyEventDispatcher
@ -60,7 +61,7 @@ public object ModalEntry {
} else {
return true
}
if (editor.vimStateMachine.isRecording) {
if (injector.registerGroup.isRecording) {
KeyHandler.getInstance().modalEntryKeys += stroke
}
if (!processor(stroke)) {

View File

@ -114,7 +114,8 @@ internal class CompleteEntryAction : TextAction(ExEditorKit.CompleteEntry) {
// write action
// * The key handler routines get the chance to clean up and reset state
val entry = ExEntryPanel.getInstance().entry
KeyHandler.getInstance().handleKey(entry.editor.vim, stroke, entry.context.vim)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(entry.editor.vim, stroke, entry.context.vim, keyHandler.keyHandlerState)
}
companion object {

View File

@ -125,10 +125,12 @@ internal object ExEditorKit : DefaultEditorKit() {
if (target.useHandleKeyFromEx) {
val entry = ExEntryPanel.getInstance().entry
val editor = entry.editor
KeyHandler.getInstance().handleKey(
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor.vim,
key,
injector.executionContextManager.onEditor(editor.vim, entry.context.vim),
keyHandler.keyHandlerState,
)
} else {
val event = ActionEvent(e.source, e.id, c.toString(), e.getWhen(), e.modifiers)

View File

@ -36,10 +36,12 @@ internal class ExShortcutKeyAction(private val exEntryPanel: ExEntryPanel) : Dum
val keyStroke = getKeyStroke(e)
if (keyStroke != null) {
val editor = exEntryPanel.entry.editor
KeyHandler.getInstance().handleKey(
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(
editor.vim,
keyStroke,
injector.executionContextManager.onEditor(editor.vim, e.dataContext.vim),
keyHandler.keyHandlerState
)
}
}

View File

@ -8,15 +8,10 @@
package com.maddyhome.idea.vim.ui.widgets
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.VimPluginListener
import com.maddyhome.idea.vim.options.GlobalOptionChangeListener
public class VimWidgetListener(private val updateWidget: Runnable) : GlobalOptionChangeListener, VimPluginListener {
init {
injector.listenersNotifier.vimPluginListeners.add(this)
}
public open class VimWidgetListener(private val updateWidget: Runnable) : GlobalOptionChangeListener, VimPluginListener {
override fun onGlobalOptionChanged() {
updateWidget.run()
}

View File

@ -13,11 +13,13 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.wm.StatusBarWidget
import com.intellij.openapi.wm.StatusBarWidgetFactory
import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.common.MacroRecordingListener
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.ui.widgets.VimWidgetListener
@ -26,21 +28,7 @@ import java.awt.Component
private const val ID = "IdeaVimMacro"
internal class MacroWidgetFactory : StatusBarWidgetFactory, VimStatusBarWidget {
private var content: String = ""
private val macroRecordingListener = object : MacroRecordingListener {
override fun recordingStarted(editor: VimEditor, register: Char) {
content = "recording @$register"
updateWidgetInStatusBar(ID, editor.ij.project)
}
override fun recordingFinished(editor: VimEditor, register: Char) {
content = ""
updateWidgetInStatusBar(ID, editor.ij.project)
}
}
internal class MacroWidgetFactory : StatusBarWidgetFactory {
override fun getId(): String {
return ID
}
@ -50,22 +38,23 @@ internal class MacroWidgetFactory : StatusBarWidgetFactory, VimStatusBarWidget {
}
override fun createWidget(project: Project): StatusBarWidget {
injector.listenersNotifier.macroRecordingListeners.add(macroRecordingListener)
return VimMacroWidget()
}
override fun isAvailable(project: Project): Boolean {
return VimPlugin.isEnabled() && injector.globalIjOptions().showmodewidget
}
}
private inner class VimMacroWidget : StatusBarWidget {
override fun ID(): String {
return ID
}
public class VimMacroWidget : StatusBarWidget, VimStatusBarWidget {
public var content: String = ""
override fun getPresentation(): StatusBarWidget.WidgetPresentation {
return VimModeWidgetPresentation()
}
override fun ID(): String {
return ID
}
override fun getPresentation(): StatusBarWidget.WidgetPresentation {
return VimModeWidgetPresentation()
}
private inner class VimModeWidgetPresentation : StatusBarWidget.TextPresentation {
@ -91,4 +80,30 @@ public fun updateMacroWidget() {
}
}
public val macroWidgetOptionListener: VimWidgetListener = VimWidgetListener { updateMacroWidget() }
// TODO: At the moment recording macro & RegisterGroup is bound to application, so macro will be recorded even if we
// move between projects. BUT it's not a good idea. Maybe RegisterGroup should have it's own project scope instances
public class MacroWidgetListener : MacroRecordingListener, VimWidgetListener({ updateMacroWidget() }) {
override fun recordingStarted() {
for (project in ProjectManager.getInstance().openProjects) {
val macroWidget = getWidget(project) ?: continue
val register = injector.registerGroup.recordRegister
macroWidget.content = "recording @$register"
macroWidget.updateWidgetInStatusBar(ID, project)
}
}
override fun recordingFinished() {
for (project in ProjectManager.getInstance().openProjects) {
val macroWidget = getWidget(project) ?: continue
macroWidget.content = ""
macroWidget.updateWidgetInStatusBar(ID, project)
}
}
private fun getWidget(project: Project): VimMacroWidget? {
val statusBar = WindowManager.getInstance()?.getStatusBar(project) ?: return null
return statusBar.getWidget(ID) as? VimMacroWidget
}
}
public val macroWidgetOptionListener: VimWidgetListener = VimWidgetListener { updateMacroWidget() }

View File

@ -20,14 +20,9 @@ import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBLabel
import com.intellij.ui.util.width
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.newapi.globalIjOptions
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetFocusListener
import com.maddyhome.idea.vim.ui.widgets.mode.listeners.ModeWidgetModeListener
import java.awt.Dimension
import java.awt.Point
import java.awt.event.MouseAdapter
@ -54,11 +49,6 @@ public class VimModeWidget(public val project: Project) : CustomStatusBarWidget,
val mode = getFocusedEditor(project)?.vim?.mode
updateLabel(mode)
injector.listenersNotifier.apply {
modeChangeListeners.add(ModeWidgetModeListener(this@VimModeWidget))
myEditorListeners.add(ModeWidgetFocusListener(this@VimModeWidget))
}
label.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
val popup = ModeWidgetPopup.createPopup() ?: return

View File

@ -11,23 +11,48 @@ package com.maddyhome.idea.vim.ui.widgets.mode.listeners
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.common.EditorListener
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.newapi.IjVimEditor
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.ui.widgets.VimWidgetListener
import com.maddyhome.idea.vim.ui.widgets.mode.ModeWidgetFactory
import com.maddyhome.idea.vim.ui.widgets.mode.VimModeWidget
import com.maddyhome.idea.vim.ui.widgets.mode.updateModeWidget
internal class ModeWidgetFocusListener(private val modeWidget: VimModeWidget): EditorListener {
internal class ModeWidgetListener: ModeChangeListener, EditorListener, VimWidgetListener({ updateModeWidget()}) {
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
val modeWidget = getWidget(editor) ?: return
val editorMode = editor.mode
if (editorMode !is Mode.OP_PENDING) {
modeWidget.updateWidget(editorMode)
}
}
private fun getWidget(editor: VimEditor): VimModeWidget? {
val project = (editor as IjVimEditor).editor.project ?: return null
return getWidget(project)
}
private fun getWidget(project: Project): VimModeWidget? {
val statusBar = WindowManager.getInstance()?.getStatusBar(project) ?: return null
return statusBar.getWidget(ModeWidgetFactory.ID) as? VimModeWidget
}
override fun created(editor: VimEditor) {
updateModeWidget()
val modeWidget = getWidget(editor) ?: return
val mode = getFocusedEditorForProject(editor.ij.project)?.vim?.mode
modeWidget.updateWidget(mode)
}
override fun released(editor: VimEditor) {
updateModeWidget()
val modeWidget = getWidget(editor) ?: return
val focusedEditor = getFocusedEditorForProject(editor.ij.project)
if (focusedEditor == null || focusedEditor == editor.ij) {
modeWidget.updateWidget(null)
@ -35,18 +60,19 @@ internal class ModeWidgetFocusListener(private val modeWidget: VimModeWidget): E
}
override fun focusGained(editor: VimEditor) {
if (editor.ij.project != modeWidget.project) return
val modeWidget = getWidget(editor) ?: return
val mode = editor.mode
modeWidget.updateWidget(mode)
}
override fun focusLost(editor: VimEditor) {
val modeWidget = getWidget(editor) ?: return
val mode = getFocusedEditorForProject(editor.ij.project)?.vim?.mode
modeWidget.updateWidget(mode)
}
private fun getFocusedEditorForProject(editorProject: Project?): Editor? {
if (editorProject != modeWidget.project) return null
if (editorProject == null) return null
val fileEditorManager = FileEditorManager.getInstance(editorProject)
return fileEditorManager.selectedTextEditor
}

View File

@ -1,25 +0,0 @@
/*
* Copyright 2003-2023 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.ui.widgets.mode.listeners
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.common.ModeChangeListener
import com.maddyhome.idea.vim.newapi.ij
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.ui.widgets.mode.VimModeWidget
internal class ModeWidgetModeListener(private val modeWidget: VimModeWidget): ModeChangeListener {
override fun modeChanged(editor: VimEditor, oldMode: Mode) {
val editorMode = editor.mode
if (editorMode !is Mode.OP_PENDING && editor.ij.project == modeWidget.project) {
modeWidget.updateWidget(editorMode)
}
}
}

View File

@ -57,7 +57,7 @@ internal object IdeaRefactorModeHelper {
}
is Action.SetMode -> {
editor.vim.vimStateMachine.mode = correction.newMode
editor.vim.mode = correction.newMode
}
}
}

View File

@ -10,6 +10,8 @@ package org.jetbrains.plugins.ideavim.action
import com.intellij.idea.TestFor
import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
@ -17,6 +19,7 @@ import org.jetbrains.plugins.ideavim.VimTestCase
import org.jetbrains.plugins.ideavim.waitAndAssert
import org.junit.jupiter.api.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* @author vlan
@ -144,7 +147,7 @@ class CopyActionTest : VimTestCase() {
""".trimIndent(),
)
assertPluginError(true)
assertTrue(fixture.editor.vim.vimStateMachine.commandBuilder.isEmpty)
}
@Test

View File

@ -14,8 +14,6 @@ import com.maddyhome.idea.vim.VimPlugin
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.api.keys
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.plugins.ideavim.ExceptionHandler
import org.jetbrains.plugins.ideavim.OnlyThrowLoggedErrorProcessor
import org.jetbrains.plugins.ideavim.SkipNeovimReason
@ -44,9 +42,8 @@ class MacroActionTest : VimTestCase() {
// |q|
@Test
fun testRecordMacro() {
val editor = typeTextInFile(injector.parser.parseKeys("qa" + "3l" + "q"), "on<caret>e two three\n")
val commandState = editor.vim.vimStateMachine
kotlin.test.assertFalse(commandState.isRecording)
typeTextInFile(injector.parser.parseKeys("qa" + "3l" + "q"), "on<caret>e two three\n")
kotlin.test.assertFalse(injector.registerGroup.isRecording)
assertRegister('a', "3l")
}
@ -101,7 +98,7 @@ class MacroActionTest : VimTestCase() {
val register = VimPlugin.getRegister().getRegister('a')
val registerSize = register!!.keys.size
kotlin.test.assertEquals(9, registerSize)
assertEquals(9, registerSize)
}
@Test

View File

@ -0,0 +1,169 @@
/*
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package org.jetbrains.plugins.ideavim.command
import com.intellij.openapi.wm.WindowManager
import com.maddyhome.idea.vim.ui.widgets.mode.ModeWidgetFactory
import com.maddyhome.idea.vim.ui.widgets.mode.VimModeWidget
import org.jetbrains.plugins.ideavim.VimTestCase
// TODO it would be cool to test widget, but status bar is not initialized
class VimShowModeTest : VimTestCase() {
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in normal`() {
// configureByText("123")
// val widget = getModeWidget()
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in insert`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- INSERT --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in replace`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("R"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- REPLACE --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in visual`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("v"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- VISUAL --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in visual line`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("V"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- VISUAL LINE --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in visual block`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("<C-V>"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- VISUAL BLOCK --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in select`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("gh"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- SELECT --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in select line`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("gH"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- SELECT LINE --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in select block`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("g<C-H>"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- SELECT BLOCK --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O>"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command visual`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O>v"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) VISUAL --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command visual block`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O><C-V>"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) VISUAL BLOCK --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command visual line`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O>V"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) VISUAL LINE --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command select`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O>gh"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) SELECT --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command select block`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O>g<C-H>"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) SELECT BLOCK --", statusString)
// }
//
// @TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
// @Test
// fun `test status string in one command select line`() {
// configureByText("123")
// typeText(injector.parser.parseKeys("i<C-O>gH"))
// val statusString = fixture.editor.vim.getStatusString()
// kotlin.test.assertEquals("-- (insert) SELECT LINE --", statusString)
// }
//
// Always return null
private fun getModeWidget(): VimModeWidget? {
val project = fixture.editor?.project ?: return null
val statusBar = WindowManager.getInstance()?.getStatusBar(project) ?: return null
return statusBar.getWidget(ModeWidgetFactory.ID) as? VimModeWidget
}
}

View File

@ -1,162 +0,0 @@
/*
* Copyright 2003-2023 The IdeaVim authors
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE.txt file or at
* https://opensource.org/licenses/MIT.
*/
package org.jetbrains.plugins.ideavim.command
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim
import org.jetbrains.plugins.ideavim.SkipNeovimReason
import org.jetbrains.plugins.ideavim.TestWithoutNeovim
import org.jetbrains.plugins.ideavim.VimTestCase
import org.junit.jupiter.api.Test
class VimStateMachineTest : VimTestCase() {
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in normal`() {
configureByText("123")
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in insert`() {
configureByText("123")
typeText(injector.parser.parseKeys("i"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- INSERT --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in replace`() {
configureByText("123")
typeText(injector.parser.parseKeys("R"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- REPLACE --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in visual`() {
configureByText("123")
typeText(injector.parser.parseKeys("v"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- VISUAL --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in visual line`() {
configureByText("123")
typeText(injector.parser.parseKeys("V"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- VISUAL LINE --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in visual block`() {
configureByText("123")
typeText(injector.parser.parseKeys("<C-V>"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- VISUAL BLOCK --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in select`() {
configureByText("123")
typeText(injector.parser.parseKeys("gh"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- SELECT --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in select line`() {
configureByText("123")
typeText(injector.parser.parseKeys("gH"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- SELECT LINE --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in select block`() {
configureByText("123")
typeText(injector.parser.parseKeys("g<C-H>"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- SELECT BLOCK --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O>"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command visual`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O>v"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) VISUAL --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command visual block`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O><C-V>"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) VISUAL BLOCK --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command visual line`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O>V"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) VISUAL LINE --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command select`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O>gh"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) SELECT --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command select block`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O>g<C-H>"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) SELECT BLOCK --", statusString)
}
@TestWithoutNeovim(reason = SkipNeovimReason.NOT_VIM_TESTING)
@Test
fun `test status string in one command select line`() {
configureByText("123")
typeText(injector.parser.parseKeys("i<C-O>gH"))
val statusString = fixture.editor.vim.vimStateMachine.getStatusString()
kotlin.test.assertEquals("-- (insert) SELECT LINE --", statusString)
}
}

View File

@ -84,7 +84,6 @@ Mode.INSERT,
}
}
private val code = """
fun ${c}sum(x: Int, y: Int, z: Int): Int {
return x + y + z

View File

@ -190,7 +190,7 @@ class VimMultipleCursorsExtensionTest : VimTestCase() {
|dfkjsg
""".trimMargin()
val editor = configureByText(before)
editor.vim.vimStateMachine.mode = Mode.VISUAL(SelectionType.CHARACTER_WISE)
editor.vim.mode = Mode.VISUAL(SelectionType.CHARACTER_WISE)
typeText(injector.parser.parseKeys("<A-p>"))
@ -273,7 +273,7 @@ class VimMultipleCursorsExtensionTest : VimTestCase() {
|dfkjsg
""".trimMargin()
val editor = configureByText(before)
editor.vim.vimStateMachine.mode = Mode.VISUAL(SelectionType.CHARACTER_WISE)
editor.vim.mode = Mode.VISUAL(SelectionType.CHARACTER_WISE)
typeText(injector.parser.parseKeys("<A-x>"))
assertMode(Mode.VISUAL(SelectionType.CHARACTER_WISE))
@ -363,7 +363,7 @@ fun getCellType(${s}pos$se: VisualPosition): CellType {
fun `test ignores regex in search pattern`() {
val before = "test ${s}t.*st${c}$se toast tallest t.*st"
val editor = configureByText(before)
editor.vim.vimStateMachine.mode = Mode.VISUAL(SelectionType.CHARACTER_WISE)
editor.vim.mode = Mode.VISUAL(SelectionType.CHARACTER_WISE)
typeText(injector.parser.parseKeys("<A-n><A-n>"))
val after = "test ${s}t.*st$se toast tallest ${s}t.*st$se"

View File

@ -73,7 +73,6 @@ import com.maddyhome.idea.vim.options.helpers.GuiCursorOptionHelper
import com.maddyhome.idea.vim.options.helpers.GuiCursorType
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.ui.ex.ExEntryPanel
import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext
import com.maddyhome.idea.vim.vimscript.model.datatypes.VimFuncref
@ -119,6 +118,7 @@ abstract class VimTestCase {
if (editor != null) {
KeyHandler.getInstance().fullReset(editor.vim)
}
KeyHandler.getInstance().keyHandlerState.reset(Mode.NORMAL())
VimPlugin.getOptionGroup().resetAllOptionsForTesting()
VimPlugin.getKey().resetKeyMappings()
VimPlugin.getSearch().resetState()
@ -157,6 +157,7 @@ abstract class VimTestCase {
bookmarksManager?.bookmarks?.forEach { bookmark ->
bookmarksManager.remove(bookmark)
}
fixture.editor?.let { injector.messages.showStatusBarMessage(it.vim, "") }
SelectionVimListenerSuppressor.lock().use { fixture.tearDown() }
ExEntryPanel.getInstance().deactivate(false)
VimPlugin.getVariableService().clear()
@ -817,7 +818,7 @@ abstract class VimTestCase {
val inputModel = TestInputModel.getInstance(editor)
var key = inputModel.nextKeyStroke()
while (key != null) {
keyHandler.handleKey(editor.vim, key, dataContext)
keyHandler.handleKey(editor.vim, key, dataContext, keyHandler.keyHandlerState)
key = inputModel.nextKeyStroke()
}
},

View File

@ -32,7 +32,6 @@ abstract class VimPropertyTestBase : VimTestCase() {
VimPlugin.getRegister().resetRegisters()
editor.caretModel.runForEachCaret { it.moveToOffset(0) }
editor.vim.vimStateMachine.resetDigraph()
VimPlugin.getSearch().resetState()
VimPlugin.getChange().reset()
}

View File

@ -11,6 +11,7 @@ plugins {
kotlin("jvm")
// id("org.jlleitschuh.gradle.ktlint")
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
kotlin("plugin.serialization") version "1.9.22"
`maven-publish`
antlr
}
@ -51,7 +52,7 @@ dependencies {
antlr("org.antlr:antlr4:4.13.1")
ksp(project(":annotation-processors"))
implementation(project(":annotation-processors"))
compileOnly(project(":annotation-processors"))
compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:$kotlinxSerializationVersion")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")

View File

@ -12,44 +12,50 @@ import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
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.CommandBuilder
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.command.MappingProcessor
import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.common.CurrentCommandState
import com.maddyhome.idea.vim.common.DigraphResult
import com.maddyhome.idea.vim.common.argumentCaptured
import com.maddyhome.idea.vim.diagnostic.VimLogger
import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.trace
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.helper.isCloseKeyStroke
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.CommandNode
import com.maddyhome.idea.vim.impl.state.toMappingMode
import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.key.KeyStack
import com.maddyhome.idea.vim.key.Node
import com.maddyhome.idea.vim.key.consumers.CharArgumentConsumer
import com.maddyhome.idea.vim.key.consumers.CommandConsumer
import com.maddyhome.idea.vim.key.consumers.CommandCountConsumer
import com.maddyhome.idea.vim.key.consumers.DeleteCommandConsumer
import com.maddyhome.idea.vim.key.consumers.DigraphConsumer
import com.maddyhome.idea.vim.key.consumers.EditorResetConsumer
import com.maddyhome.idea.vim.key.consumers.ModeInputConsumer
import com.maddyhome.idea.vim.key.consumers.RegisterConsumer
import com.maddyhome.idea.vim.key.consumers.SelectRegisterConsumer
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import com.maddyhome.idea.vim.state.mode.returnTo
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
/**
* This handles every keystroke that the user can argType except those that are still valid hotkeys for various Idea
* actions. This is a singleton.
*/
// TODO for future refactorings (PRs are welcome)
// 1. avoid using handleKeyRecursionCount & shouldRecord
// 2. maybe we can live without allowKeyMappings: Boolean & mappingCompleted: Boolean
public class KeyHandler {
private val keyConsumers: List<KeyConsumer> = listOf(MappingProcessor, CommandCountConsumer(), DeleteCommandConsumer(), EditorResetConsumer(), CharArgumentConsumer(), RegisterConsumer(), DigraphConsumer(), CommandConsumer(), SelectRegisterConsumer(), ModeInputConsumer())
private var handleKeyRecursionCount = 0
public var keyHandlerState: KeyHandlerState = KeyHandlerState()
private set
public val keyStack: KeyStack = KeyStack()
public val modalEntryKeys: MutableList<KeyStroke> = ArrayList()
@ -61,8 +67,8 @@ public class KeyHandler {
* @param key The keystroke typed by the user
* @param context The data context
*/
public fun handleKey(editor: VimEditor, key: KeyStroke, context: ExecutionContext) {
handleKey(editor, key, context, allowKeyMappings = true, mappingCompleted = false)
public fun handleKey(editor: VimEditor, key: KeyStroke, context: ExecutionContext, keyState: KeyHandlerState) {
handleKey(editor, key, context, allowKeyMappings = true, mappingCompleted = false, keyState)
}
/**
@ -70,8 +76,6 @@ public class KeyHandler {
*
* @param allowKeyMappings - If we allow key mappings or not
* @param mappingCompleted - if true, we don't check if the mapping is incomplete
*
* TODO mappingCompleted and recursionCounter - we should find a more beautiful way to use them
*/
public fun handleKey(
editor: VimEditor,
@ -79,119 +83,91 @@ public class KeyHandler {
context: ExecutionContext,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyState: KeyHandlerState,
) {
LOG.trace {
"""
val result = processKey(key, editor, allowKeyMappings, mappingCompleted, KeyProcessResult.SynchronousKeyProcessBuilder(keyState))
if (result is KeyProcessResult.Executable) {
result.execute(editor, context)
}
}
/**
* This method determines whether IdeaVim can handle the passed key or not.
* For instance, if there is no mapping for <F5>, we should return 'KeyProcessResult.Unknown' to inform the IDE that
* we did not process the keypress, and therefore need to propagate it further.
* Alternatively, if we understand the key, we return a 'KeyProcessResult.Executable', which contains a runnable that
* could execute the key if needed.
*/
public fun processKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
processBuilder: KeyProcessResult.KeyProcessResultBuilder,
): KeyProcessResult {
synchronized(lock) {
LOG.trace {
"""
------- Key Handler -------
Start key processing. allowKeyMappings: $allowKeyMappings, mappingCompleted: $mappingCompleted
Key: $key
""".trimIndent()
}
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
}
}
val maxMapDepth = injector.globalOptions().maxmapdepth
if (handleKeyRecursionCount >= maxMapDepth) {
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()
}
injector.messages.clearError()
val editorState = editor.vimStateMachine
val commandBuilder = editorState.commandBuilder
injector.messages.clearError()
// We only record unmapped keystrokes. If we've recursed to handle mapping, don't record anything.
val shouldRecord = MutableBoolean(handleKeyRecursionCount == 0 && injector.registerGroup.isRecording)
// 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 && editorState.isRecording
handleKeyRecursionCount++
try {
LOG.trace("Start key processing...")
if (!allowKeyMappings || !MappingProcessor.handleKeyMapping(editor, key, context, mappingCompleted)) {
LOG.trace("Mappings processed, continue processing key.")
if (isCommandCountKey(chKey, editorState)) {
commandBuilder.addCountCharacter(key)
} else if (isDeleteCommandCountKey(key, editorState)) {
commandBuilder.deleteCountCharacter()
} else if (isEditorReset(key, editorState)) {
handleEditorReset(editor, key, context, editorState)
} else if (isExpectingCharArgument(commandBuilder)) {
handleCharArgument(key, chKey, editorState)
} else if (editorState.isRegisterPending) {
LOG.trace("Pending mode.")
commandBuilder.addKey(key)
handleSelectRegister(editorState, chKey)
} else if (!handleDigraph(editor, key, context, editorState)) {
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)
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, editorState)
commandBuilder.addKey(key)
} else if (node is CommandPartNode<LazyVimCommand>) {
LOG.trace("Node is a command part node")
commandBuilder.setCurrentCommandPartNode(node)
commandBuilder.addKey(key)
} else if (isSelectRegister(key, editorState)) {
LOG.trace("Select register")
editorState.isRegisterPending = true
commandBuilder.addKey(key)
} else {
// node == null
LOG.trace("We are not able to find a node for this key")
// 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
} else if (editorState.mode is Mode.SELECT) {
LOG.trace("Process select")
shouldRecord = injector.changeGroup.processKeyInSelectMode(editor, context, key) && shouldRecord
} else if (editorState.mappingState.mappingMode == MappingMode.CMD_LINE) {
LOG.trace("Process cmd line")
shouldRecord = injector.processGroup.processExKey(editor, key) && shouldRecord
} else {
LOG.trace("Set command state to bad_command")
commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
}
partialReset(editor)
handleKeyRecursionCount++
try {
val isProcessed = keyConsumers.any {
it.consumeKey(key, editor, allowKeyMappings, mappingCompleted, processBuilder, shouldRecord)
}
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
onUnknownKey(editor, processBuilder.state)
updateState(processBuilder.state)
return KeyProcessResult.Unknown.apply {
handleKeyRecursionCount-- // because onFinish will now be executed for unknown
}
}
} finally {
processBuilder.onFinish = { handleKeyRecursionCount-- }
}
finishedCommandPreparation(editor, context, editorState, commandBuilder, key, shouldRecord)
} finally {
handleKeyRecursionCount--
return processBuilder.build()
}
}
internal fun finishedCommandPreparation(
editor: VimEditor,
context: ExecutionContext,
editorState: VimStateMachine,
commandBuilder: CommandBuilder,
key: KeyStroke?,
shouldRecord: Boolean,
shouldRecord: MutableBoolean,
keyState: KeyHandlerState,
) {
// Do we have a fully entered command at this point? If so, let's execute it.
val commandBuilder = keyState.commandBuilder
if (commandBuilder.isReady) {
LOG.trace("Ready command builder. Execute command.")
executeCommand(editor, context, editorState)
} else if (commandBuilder.isBad) {
LOG.trace("Command builder is set to BAD")
editorState.resetOpPending()
editorState.resetRegisterPending()
editorState.resetReplaceCharacter()
injector.messages.indicateError()
reset(editor)
executeCommand(editor, context, editor.vimStateMachine, keyState)
}
// Don't record the keystroke that stops the recording (unmapped this is `q`)
if (shouldRecord && editorState.isRecording && key != null) {
if (shouldRecord.value && injector.registerGroup.isRecording && key != null) {
injector.registerGroup.recordKeyStroke(key)
modalEntryKeys.forEach { injector.registerGroup.recordKeyStroke(it) }
modalEntryKeys.clear()
@ -202,226 +178,44 @@ public class KeyHandler {
LOG.trace("----------- Key Handler Finished -----------")
}
/**
* See the description for [com.maddyhome.idea.vim.command.DuplicableOperatorAction]
*/
private fun mapOpCommand(
key: KeyStroke,
node: Node<LazyVimCommand>?,
editorState: VimStateMachine,
): Node<LazyVimCommand>? {
return if (editorState.isDuplicateOperatorKeyStroke(key)) {
editorState.commandBuilder.getChildNode(KeyStroke.getKeyStroke('_'))
} else {
node
}
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)
}
private fun handleEditorReset(
editor: VimEditor,
key: KeyStroke,
context: ExecutionContext,
editorState: VimStateMachine,
) {
val commandBuilder = editorState.commandBuilder
if (commandBuilder.isAwaitingCharOrDigraphArgument()) {
editorState.resetReplaceCharacter()
}
if (commandBuilder.isAtDefaultState) {
val register = injector.registerGroup
if (register.currentRegister == register.defaultRegister) {
var indicateError = true
if (key.keyCode == KeyEvent.VK_ESCAPE) {
val executed = arrayOf<Boolean?>(null)
injector.actionExecutor.executeCommand(
editor,
{ executed[0] = injector.actionExecutor.executeEsc(context) },
"",
null,
)
indicateError = !executed[0]!!
}
if (indicateError) {
injector.messages.indicateError()
}
}
}
reset(editor)
public fun setBadCommand(editor: VimEditor, keyState: KeyHandlerState) {
onUnknownKey(editor, keyState)
injector.messages.indicateError()
}
private fun isCommandCountKey(chKey: Char, editorState: VimStateMachine): Boolean {
// Make sure to avoid handling '0' as the start of a count.
val commandBuilder = editorState.commandBuilder
val notRegisterPendingCommand = editorState.mode is Mode.NORMAL && !editorState.isRegisterPending
val visualMode = editorState.mode is Mode.VISUAL && !editorState.isRegisterPending
val opPendingMode = editorState.mode is Mode.OP_PENDING
if (notRegisterPendingCommand || visualMode || opPendingMode) {
if (commandBuilder.isExpectingCount && Character.isDigit(chKey) && (commandBuilder.count > 0 || chKey != '0')) {
LOG.debug("This is a command key count")
return true
}
}
LOG.debug("This is NOT a command key count")
return false
public fun isDuplicateOperatorKeyStroke(key: KeyStroke, mode: Mode, keyState: KeyHandlerState): Boolean {
return isOperatorPending(mode, keyState) && keyState.commandBuilder.isDuplicateOperatorKeyStroke(key)
}
private fun isDeleteCommandCountKey(key: KeyStroke, editorState: VimStateMachine): Boolean {
// See `:help N<Del>`
val commandBuilder = editorState.commandBuilder
val isDeleteCommandKeyCount =
(editorState.mode is Mode.NORMAL || editorState.mode is Mode.VISUAL || editorState.mode is Mode.OP_PENDING) &&
commandBuilder.isExpectingCount && commandBuilder.count > 0 && key.keyCode == KeyEvent.VK_DELETE
LOG.debug { "This is a delete command key count: $isDeleteCommandKeyCount" }
return isDeleteCommandKeyCount
}
private fun isEditorReset(key: KeyStroke, editorState: VimStateMachine): Boolean {
val editorReset = editorState.mode is Mode.NORMAL && key.isCloseKeyStroke()
LOG.debug { "This is editor reset: $editorReset" }
return editorReset
}
private fun isSelectRegister(key: KeyStroke, editorState: VimStateMachine): Boolean {
if (editorState.mode !is Mode.NORMAL && editorState.mode !is Mode.VISUAL) {
return false
}
return if (editorState.isRegisterPending) {
true
} else {
key.keyChar == '"' && !editorState.isOperatorPending && editorState.commandBuilder.expectedArgumentType == null
}
}
private fun handleSelectRegister(vimStateMachine: VimStateMachine, chKey: Char) {
LOG.trace("Handle select register")
vimStateMachine.resetRegisterPending()
if (injector.registerGroup.isValid(chKey)) {
LOG.trace("Valid register")
vimStateMachine.commandBuilder.pushCommandPart(chKey)
} else {
LOG.trace("Invalid register, set command state to BAD_COMMAND")
vimStateMachine.commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
}
}
private fun isExpectingCharArgument(commandBuilder: CommandBuilder): Boolean {
val expectingCharArgument = commandBuilder.expectedArgumentType === Argument.Type.CHARACTER
LOG.debug { "Expecting char argument: $expectingCharArgument" }
return expectingCharArgument
}
private fun handleCharArgument(key: KeyStroke, chKey: Char, vimStateMachine: VimStateMachine) {
var mutableChKey = chKey
LOG.trace("Handling char argument")
// We are expecting a character argument - is this a regular character the user typed?
// Some special keys can be handled as character arguments - let's check for them here.
if (mutableChKey.code == 0) {
when (key.keyCode) {
KeyEvent.VK_TAB -> mutableChKey = '\t'
KeyEvent.VK_ENTER -> mutableChKey = '\n'
}
}
val commandBuilder = vimStateMachine.commandBuilder
if (mutableChKey.code != 0) {
LOG.trace("Add character argument to the current command")
// Create the character argument, add it to the current command, and signal we are ready to process the command
commandBuilder.completeCommandPart(Argument(mutableChKey))
} else {
LOG.trace("This is not a valid character argument. Set command state to BAD_COMMAND")
// Oops - this isn't a valid character argument
commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
}
vimStateMachine.resetReplaceCharacter()
}
private fun handleDigraph(
editor: VimEditor,
key: KeyStroke,
context: ExecutionContext,
editorState: VimStateMachine,
): 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 commandBuilder = editorState.commandBuilder
if (commandBuilder.expectedArgumentType == Argument.Type.DIGRAPH) {
LOG.trace("Expected argument is digraph")
if (editorState.digraphSequence.isDigraphStart(key)) {
editorState.startDigraphSequence()
editorState.commandBuilder.addKey(key)
return true
}
if (editorState.digraphSequence.isLiteralStart(key)) {
editorState.startLiteralSequence()
editorState.commandBuilder.addKey(key)
return true
}
}
val res = editorState.processDigraphKey(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 -> {
editorState.commandBuilder.addKey(key)
return true
}
DigraphResult.RES_DONE -> {
if (commandBuilder.expectedArgumentType === Argument.Type.DIGRAPH) {
commandBuilder.fallbackToCharacterArgument()
}
val stroke = res.stroke ?: return false
editorState.commandBuilder.addKey(key)
handleKey(editor, stroke, context)
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
}
return true
}
DigraphResult.RES_UNHANDLED -> {
// UNHANDLED means the keystroke made no sense in the context of a digraph, but isn't an error in the current
// 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)
return true
}
return false
}
}
return false
public fun isOperatorPending(mode: Mode, keyState: KeyHandlerState): Boolean {
return mode is Mode.OP_PENDING && !keyState.commandBuilder.isEmpty
}
private fun executeCommand(
editor: VimEditor,
context: ExecutionContext,
editorState: VimStateMachine,
keyState: KeyHandlerState,
) {
LOG.trace("Command execution")
val command = editorState.commandBuilder.buildCommand()
val command = keyState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(
editorState.mappingState.mappingMode == MappingMode.OP_PENDING,
editor.mode is Mode.OP_PENDING,
command.rawCount,
editorState.mode,
)
// If we were in "operator pending" mode, reset back to normal mode.
editorState.resetOpPending()
editor.resetOpPending()
// Save off the command we are about to execute
editorState.executingCommand = command
@ -429,13 +223,13 @@ public class KeyHandler {
if (type.isWrite) {
if (!editor.isWritable()) {
injector.messages.indicateError()
reset(editor)
reset(keyState, editorState.mode)
LOG.warn("File is not writable")
return
}
}
if (injector.application.isMainThread()) {
val action: Runnable = ActionRunner(editor, context, command, operatorArguments)
val action: Runnable = ActionRunner(editor, context, command, keyState, operatorArguments)
val cmdAction = command.action
val name = cmdAction.id
if (type.isWrite) {
@ -448,127 +242,6 @@ public class KeyHandler {
}
}
private fun handleCommandNode(
editor: VimEditor,
context: ExecutionContext,
key: KeyStroke,
node: CommandNode<LazyVimCommand>,
editorState: VimStateMachine,
) {
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 commandBuilder = editorState.commandBuilder
val expectedArgumentType = commandBuilder.expectedArgumentType
commandBuilder.pushCommandPart(action)
if (!checkArgumentCompatibility(expectedArgumentType, action)) {
LOG.trace("Return from command node handling")
commandBuilder.commandState = CurrentCommandState.BAD_COMMAND
return
}
if (action.argumentType == null || stopMacroRecord(node, editorState)) {
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!!, editorState)
partialReset(editor)
}
// 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
editorState.mode = editorState.mode.returnTo()
}
}
private fun stopMacroRecord(node: CommandNode<LazyVimCommand>, editorState: VimStateMachine): Boolean {
// TODO
// return editorState.isRecording && node.actionHolder.getInstance() is ToggleRecordingAction
return editorState.isRecording && node.actionHolder.instance.id == "VimToggleRecordingAction"
}
private fun startWaitingForArgument(
editor: VimEditor,
context: ExecutionContext,
key: Char,
action: EditorActionHandlerBase,
argument: Argument.Type,
editorState: VimStateMachine,
) {
val commandBuilder = editorState.commandBuilder
when (argument) {
Argument.Type.MOTION -> {
if (editorState.isDotRepeatInProgress && argumentCaptured != null) {
commandBuilder.completeCommandPart(argumentCaptured!!)
}
editorState.mode = Mode.OP_PENDING(editorState.mode.returnTo)
}
Argument.Type.DIGRAPH -> // Command actions represent the completion of a command. Showcmd relies on this - if the action represents a
// part of a command, the showcmd output is reset part way through. This means we need to special case entering
// digraph/literal input mode. We have an action that takes a digraph as an argument, and pushes it back through
// the key handler when it's complete.
// TODO
// if (action is InsertCompletedDigraphAction) {
if (action.id == "VimInsertCompletedDigraphAction") {
editorState.startDigraphSequence()
setPromptCharacterEx('?')
} else if (action.id == "VimInsertCompletedLiteralAction") {
editorState.startLiteralSequence()
setPromptCharacterEx('^')
}
Argument.Type.EX_STRING -> {
// The current Command expects an EX_STRING argument. E.g. SearchEntry(Fwd|Rev)Action. This won't execute until
// state hits READY. Start the ex input field, push CMD_LINE mode and wait for the argument.
injector.processGroup.startSearchCommand(editor, context, commandBuilder.count, key)
commandBuilder.commandState = CurrentCommandState.NEW_COMMAND
val currentMode = editorState.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable command line mode $currentMode" }
editorState.mode = Mode.CMD_LINE(currentMode)
}
else -> Unit
}
// Another special case. Force a mode change to update the caret shape
// This was a typed solution
// if (action is ChangeCharacterAction || action is ChangeVisualCharacterAction)
if (action.id == "VimChangeCharacterAction" || action.id == "VimChangeVisualCharacterAction") {
editorState.isReplaceCharacter = true
}
}
private fun checkArgumentCompatibility(
expectedArgumentType: Argument.Type?,
action: EditorActionHandlerBase,
): Boolean {
return !(expectedArgumentType === Argument.Type.MOTION && action.type !== Command.Type.MOTION)
}
/**
* Partially resets the state of this handler. Resets the command count, clears the key list, resets the key tree
* node to the root for the current mode we are in.
@ -576,9 +249,7 @@ public class KeyHandler {
* @param editor The editor to reset.
*/
public fun partialReset(editor: VimEditor) {
val editorState = VimStateMachine.getInstance(editor)
editorState.mappingState.resetMappingSequence()
editorState.commandBuilder.resetInProgressCommandPart(getKeyRoot(editorState.mappingState.mappingMode))
keyHandlerState.partialReset(editor.mode)
}
/**
@ -587,15 +258,23 @@ public class KeyHandler {
* @param editor The editor to reset.
*/
public fun reset(editor: VimEditor) {
partialReset(editor)
val editorState = VimStateMachine.getInstance(editor)
editorState.commandBuilder.resetAll(getKeyRoot(editorState.mappingState.mappingMode))
keyHandlerState.partialReset(editor.mode)
keyHandlerState.commandBuilder.resetAll(getKeyRoot(editor.mode.toMappingMode()))
}
public fun reset(keyState: KeyHandlerState, mode: Mode) {
keyHandlerState.partialReset(mode)
keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode()))
}
private fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> {
return injector.keyGroup.getKeyRoot(mappingMode)
}
public fun updateState(keyState: KeyHandlerState) {
this.keyHandlerState = keyState
}
/**
* Completely resets the state of this handler. Resets the command mode to normal, resets, and clears the selected
* register.
@ -604,13 +283,13 @@ public class KeyHandler {
*/
public fun fullReset(editor: VimEditor) {
injector.messages.clearError()
VimStateMachine.getInstance(editor).reset()
reset(editor)
editor.resetState()
reset(keyHandlerState, editor.mode)
injector.registerGroupIfCreated?.resetRegister()
editor.removeSelection()
}
private fun setPromptCharacterEx(promptCharacter: Char) {
public fun setPromptCharacterEx(promptCharacter: Char) {
val exEntryPanel = injector.exEntryPanel
if (exEntryPanel.isActive()) {
exEntryPanel.setCurrentActionPromptCharacter(promptCharacter)
@ -624,11 +303,12 @@ public class KeyHandler {
val editor: VimEditor,
val context: ExecutionContext,
val cmd: Command,
val keyState: KeyHandlerState,
val operatorArguments: OperatorArguments,
) : Runnable {
override fun run() {
val editorState = VimStateMachine.getInstance(editor)
editorState.commandBuilder.commandState = CurrentCommandState.NEW_COMMAND
keyState.commandBuilder.commandState = CurrentCommandState.NEW_COMMAND
val register = cmd.register
if (register != null) {
injector.registerGroup.selectRegister(register)
@ -653,21 +333,22 @@ public class KeyHandler {
if (myMode is Mode.NORMAL && returnTo != null && !cmd.flags.contains(CommandFlags.FLAG_EXPECT_MORE)) {
when (returnTo) {
ReturnTo.INSERT -> {
editorState.mode = Mode.INSERT
editor.mode = Mode.INSERT
}
ReturnTo.REPLACE -> {
editorState.mode = Mode.REPLACE
editor.mode = Mode.REPLACE
}
}
}
if (editorState.commandBuilder.isDone()) {
getInstance().reset(editor)
if (keyState.commandBuilder.isDone()) {
getInstance().reset(keyState, editorState.mode)
}
}
}
public companion object {
public val lock: Any = Object()
private val LOG: VimLogger = vimLogger<KeyHandler>()
internal fun <T> isPrefix(list1: List<T>, list2: List<T>): Boolean {
@ -687,4 +368,113 @@ public class KeyHandler {
@JvmStatic
public fun getInstance(): KeyHandler = instance
}
public data class MutableBoolean(public var value: Boolean)
}
/**
* This class was created to manage Fleet input processing.
* Fleet needs to synchronously determine if the key will be handled by the plugin or should be passed elsewhere.
* The key processing itself will be executed asynchronously at a later time.
*/
public sealed interface KeyProcessResult {
/**
* Key input that is not recognized by IdeaVim and should be passed to IDE.
*/
public object Unknown: KeyProcessResult
/**
* Key input that is recognized by IdeaVim and can be executed.
* Key handling is a two-step process:
* 1. Determine if the key should be processed and how (is it a command, mapping, or something else).
* 2. Execute the recognized command.
* This class should be returned after the first step is complete.
* It will continue the key handling and finish the process.
*/
public class Executable(
private val originalState: KeyHandlerState,
private val preProcessState: KeyHandlerState,
private val processing: KeyProcessing,
): KeyProcessResult {
public companion object {
private val logger = vimLogger<KeyProcessResult>()
}
public fun execute(editor: VimEditor, context: ExecutionContext) {
synchronized(KeyHandler.lock) {
val keyHandler = KeyHandler.getInstance()
if (keyHandler.keyHandlerState != originalState) {
logger.error("Unexpected editor state. Aborting command execution.")
}
processing(preProcessState, editor, context)
keyHandler.updateState(preProcessState)
}
}
}
/**
* This class serves as a wrapper around the key handling algorithm and should be used with care:
* We process keys in two steps:
* 1. We first determine if IdeaVim can handle the key or not. At this stage, you should avoid modifying anything
* except state: KeyHandlerState. This is because it is not guaranteed that the key will be handled by IdeaVim at
* all, and we want to minimize possible side effects.
* 2. If it's confirmed that the key will be handled, add all the key handling processes as execution steps,
* slated for later execution.
*
* Please note that execution steps could depend on KeyHandlerState, and because of that we cannot change the state
* after adding an execution step. This is because an execution step does not anticipate changes to the state.
* If there's need to alter the state following any of the execution steps, wrap the state modification as an
* execution step. This will allow state modification to occur later rather than immediately.
*/
public abstract class KeyProcessResultBuilder {
public abstract val state: KeyHandlerState
protected val processings: MutableList<KeyProcessing> = mutableListOf()
public var onFinish: (() -> Unit)? = null // FIXME I'm a dirty hack to support recursion counter
public fun addExecutionStep(keyProcessing: KeyProcessing) {
processings.add(keyProcessing)
}
public abstract fun build(): KeyProcessResult
}
// Works with existing state and modifies it during execution
// It's the way IdeaVim worked for the long time and for this class we do not create
// unnecessary objects and assume that the code will be executed immediately
public class SynchronousKeyProcessBuilder(public override val state: KeyHandlerState): KeyProcessResultBuilder() {
public override fun build(): KeyProcessResult {
return Executable(state, state) { keyHandlerState, vimEditor, executionContext ->
try {
for (processing in processings) {
processing(keyHandlerState, vimEditor, executionContext)
}
} finally {
onFinish?.let { it() }
}
}
}
}
// Works with a clone of current state, nothing is modified during the builder work (key processing)
// The new state will be applied later, when we run Executable.execute() (it may not be run at all)
public class AsyncKeyProcessBuilder(originalState: KeyHandlerState): KeyProcessResultBuilder() {
private val originalState: KeyHandlerState = KeyHandler.getInstance().keyHandlerState
public override val state: KeyHandlerState = originalState.clone()
public override fun build(): KeyProcessResult {
return Executable(originalState, state) { keyHandlerState, vimEditor, executionContext ->
try {
for (processing in processings) {
processing(keyHandlerState, vimEditor, executionContext)
}
} finally {
onFinish?.let { it() }
KeyHandler.getInstance().updateState(state)
}
}
}
}
}
public typealias KeyProcessing = (KeyHandlerState, VimEditor, ExecutionContext) -> Unit

View File

@ -8,11 +8,11 @@
package com.maddyhome.idea.vim.action
import com.intellij.vim.processors.CommandBean
import com.maddyhome.idea.vim.action.change.LazyVimCommand
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.InputStream
@ -44,3 +44,6 @@ public interface CommandProvider {
?: throw RuntimeException("Failed to fetch ex commands from ${javaClass.name}")
}
}
@Serializable
public data class CommandBean(val keys: String, val `class`: String, val modes: String)

View File

@ -13,11 +13,31 @@ import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.vimscript.model.LazyInstance
import javax.swing.KeyStroke
public class LazyVimCommand(
public open class LazyVimCommand(
public val keys: Set<List<KeyStroke>>,
public val modes: Set<MappingMode>,
className: String,
classLoader: ClassLoader,
) : LazyInstance<EditorActionHandlerBase>(className, classLoader) {
public val actionId: String = EditorActionHandlerBase.getActionId(className)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LazyVimCommand
if (keys != other.keys) return false
if (modes != other.modes) return false
if (actionId != other.actionId) return false
return true
}
override fun hashCode(): Int {
var result = keys.hashCode()
result = 31 * result + modes.hashCode()
result = 31 * result + actionId.hashCode()
return result
}
}

View File

@ -27,7 +27,8 @@ public class InsertCompletedDigraphAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
// The converted digraph character has been captured as an argument, push it back through key handler
val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character)
KeyHandler.getInstance().handleKey(editor, keyStroke, context)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState)
return true
}
}

View File

@ -27,7 +27,8 @@ public class InsertCompletedLiteralAction : VimActionHandler.SingleExecution() {
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
// The converted literal character has been captured as an argument, push it back through key handler
val keyStroke = KeyStroke.getKeyStroke(cmd.argument!!.character)
KeyHandler.getInstance().handleKey(editor, keyStroke, context)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor, keyStroke, context, keyHandler.keyHandlerState)
return true
}
}

View File

@ -25,12 +25,12 @@ public class ToggleRecordingAction : VimActionHandler.SingleExecution() {
override val argumentType: Argument.Type = Argument.Type.CHARACTER
override fun execute(editor: VimEditor, context: ExecutionContext, cmd: Command, operatorArguments: OperatorArguments): Boolean {
return if (!editor.vimStateMachine.isRecording) {
return if (!injector.registerGroup.isRecording) {
val argument = cmd.argument ?: return false
val reg = argument.character
injector.registerGroup.startRecording(editor, reg)
injector.registerGroup.startRecording(reg)
} else {
injector.registerGroup.finishRecording(editor)
injector.registerGroup.finishRecording()
true
}
}

View File

@ -45,7 +45,7 @@ public class SelectToggleVisualMode : VimActionHandler.SingleExecution() {
val commandState = editor.vimStateMachine
val myMode = commandState.mode
if (myMode is com.maddyhome.idea.vim.state.mode.Mode.VISUAL) {
commandState.setSelectMode(myMode.selectionType)
editor.setSelectMode(myMode.selectionType)
if (myMode.selectionType != SelectionType.LINE_WISE) {
editor.nativeCarets().forEach {
if (it.offset.point + injector.visualMotionGroup.selectionAdj == it.selectionEnd) {
@ -54,7 +54,7 @@ public class SelectToggleVisualMode : VimActionHandler.SingleExecution() {
}
}
} else if (myMode is com.maddyhome.idea.vim.state.mode.Mode.SELECT) {
commandState.pushVisualMode(myMode.selectionType)
editor.pushVisualMode(myMode.selectionType)
if (myMode.selectionType != SelectionType.LINE_WISE) {
editor.nativeCarets().forEach {
if (it.offset.point == it.selectionEnd && it.visualLineStart <= it.offset.point - injector.visualMotionGroup.selectionAdj) {

View File

@ -37,7 +37,7 @@ public class VisualSelectPreviousAction : VimActionHandler.SingleExecution() {
if (caretToSelectionInfo.any { it.second.start == null || it.second.end == null }) return false
editor.vimStateMachine.mode = com.maddyhome.idea.vim.state.mode.Mode.VISUAL(selectionType)
editor.mode = com.maddyhome.idea.vim.state.mode.Mode.VISUAL(selectionType)
for ((caret, selectionInfo) in caretToSelectionInfo) {
val startOffset = editor.bufferPositionToOffset(selectionInfo.start!!)

View File

@ -53,7 +53,7 @@ private fun swapVisualSelections(editor: VimEditor): Boolean {
editor.vimLastSelectionType = mode.selectionType
injector.markService.setVisualSelectionMarks(primaryCaret, TextRange(vimSelectionStart, primaryCaret.offset.point))
editor.vimStateMachine.mode = mode.copy(selectionType = lastSelectionType)
editor.mode = mode.copy(selectionType = lastSelectionType)
primaryCaret.vimSetSelection(lastVisualRange.startOffset, lastVisualRange.endOffset, true)
injector.scroll.scrollCaretIntoView(editor)

View File

@ -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

View File

@ -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
@ -36,7 +37,6 @@ import com.maddyhome.idea.vim.register.RegisterConstants.LAST_INSERTED_TEXT_REGI
import com.maddyhome.idea.vim.state.VimStateMachine.Companion.getInstance
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.toReturnTo
import org.jetbrains.annotations.NonNls
import java.awt.event.KeyEvent
@ -439,7 +439,7 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
}
val cmd = state.executingCommand
if (cmd != null && state.isDotRepeatInProgress) {
state.mode = mode
editor.mode = mode
if (mode == Mode.REPLACE) {
editor.insertMode = false
}
@ -465,7 +465,7 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
if (mode == Mode.REPLACE) {
editor.insertMode = true
}
state.mode = Mode.NORMAL()
editor.mode = Mode.NORMAL()
} else {
lastInsert = cmd
strokes.clear()
@ -480,7 +480,7 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
vimDocument!!.addChangeListener(myChangeListener)
oldOffset = editor.currentCaret().offset.point
editor.insertMode = mode == Mode.INSERT
state.mode = mode
editor.mode = mode
}
notifyListeners(editor)
}
@ -560,8 +560,7 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
// The change pos '.' mark is the offset AFTER processing escape, and after switching to overtype
markGroup.setMark(editor, MARK_CHANGE_POS)
getInstance(editor).mode = Mode.NORMAL()
editor.vimStateMachine.mode = Mode.NORMAL()
editor.mode = Mode.NORMAL()
}
private fun updateLastInsertedTextRegister() {
@ -640,7 +639,7 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
* @param editor The editor to put into NORMAL mode for one command
*/
override fun processSingleCommand(editor: VimEditor) {
getInstance(editor).mode = Mode.NORMAL(returnTo = editor.mode.toReturnTo)
editor.mode = Mode.NORMAL(returnTo = editor.mode.toReturnTo)
clearStrokes(editor)
}
@ -711,24 +710,23 @@ public abstract class VimChangeGroupBase : VimChangeGroup {
* This processes all "regular" keystrokes entered while in insert/replace mode
*
* @param editor The editor the character was typed into
* @param context The data context
* @param key The user entered keystroke
* @return true if this was a regular character, false if not
*/
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
@ -736,16 +734,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

View File

@ -14,8 +14,13 @@ import com.maddyhome.idea.vim.common.LiveRange
import com.maddyhome.idea.vim.common.Offset
import com.maddyhome.idea.vim.common.Pointer
import com.maddyhome.idea.vim.common.TextRange
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.impl.state.VimStateMachineImpl
import com.maddyhome.idea.vim.impl.state.toMappingMode
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.returnTo
/**
* Every line in [VimEditor] ends with a new line TODO <- this is probably not true already
@ -124,6 +129,24 @@ import com.maddyhome.idea.vim.state.mode.SelectionType
* ([VimVisualPosition] should be phased out if possible, as it is an IntelliJ concept, not a Vim concept.)
*/
public interface VimEditor {
public var mode: Mode
get() = vimStateMachine.mode
set(value) {
if (vimStateMachine.mode == value) return
val oldValue = vimStateMachine.mode
(vimStateMachine as VimStateMachineImpl).mode = value
injector.listenersNotifier.notifyModeChanged(this, oldValue)
}
public var isReplaceCharacter: Boolean
get() = vimStateMachine.isReplaceCharacter
set(value) {
if (value != vimStateMachine.isReplaceCharacter) {
(vimStateMachine as VimStateMachineImpl).isReplaceCharacter = value
injector.listenersNotifier.notifyIsReplaceCharChanged(this)
}
}
public val lfMakesNewLine: Boolean
public var vimChangeActionSwitchMode: Mode?
@ -196,9 +219,6 @@ public interface VimEditor {
shiftType: LineDeleteShift,
): Pair<Pair<Offset, Offset>, LineDeleteShift>?
public fun updateCaretsVisualAttributes()
public fun updateCaretsVisualPosition()
public fun offsetToBufferPosition(offset: Int): BufferPosition
public fun bufferPositionToOffset(position: BufferPosition): Int
@ -276,6 +296,29 @@ public interface VimEditor {
* instance and need to search for a new version.
*/
public fun <T : ImmutableVimCaret> findLastVersionOfCaret(caret: T): T?
/**
* Resets the command, mode, visual mode, and mapping mode to initial values.
*/
public fun resetState() {
mode = Mode.NORMAL()
vimStateMachine.executingCommand = null
vimStateMachine.digraphSequence.reset()
vimStateMachine.commandBuilder.resetInProgressCommandPart(
injector.keyGroup.getKeyRoot(mode.toMappingMode())
)
}
public fun resetOpPending() {
if (this.mode is Mode.OP_PENDING) {
val returnTo = this.mode.returnTo
mode = when (returnTo) {
ReturnTo.INSERT -> Mode.INSERT
ReturnTo.REPLACE -> Mode.INSERT
null -> Mode.NORMAL()
}
}
}
}
public interface MutableVimEditor : VimEditor {

View File

@ -55,4 +55,8 @@ public interface VimEditorGroup {
* hidden editors that are used to handle requests from Code With Me guests.
*/
public fun getEditors(buffer: VimDocument): Collection<VimEditor>
// TODO find a better place for methods below. Maybe make CaretVisualAttributesHelper abstract?
public fun updateCaretsVisualAttributes(editor: VimEditor)
public fun updateCaretsVisualPosition(editor: VimEditor)
}

View File

@ -17,8 +17,4 @@ public interface VimMessages {
public fun message(key: String, vararg params: Any): String
public fun updateStatusBar(editor: VimEditor)
public fun showMode(editor: VimEditor?, msg: String) {
showStatusBarMessage(editor, msg)
}
}

View File

@ -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

View File

@ -32,7 +32,7 @@ public abstract class VimVisualMotionGroupBase : VimVisualMotionGroup {
get() = if (exclusiveSelection) 0 else 1
override fun enterSelectMode(editor: VimEditor, subMode: SelectionType): Boolean {
editor.vimStateMachine.setSelectMode(subMode)
editor.setSelectMode(subMode)
editor.forEachCaret { it.vimSelectionStart = it.vimLeadSelectionOffset }
return true
}
@ -55,7 +55,7 @@ public abstract class VimVisualMotionGroupBase : VimVisualMotionGroup {
// Enable visual subMode
if (rawCount > 0) {
val primarySubMode = editor.primaryCaret().vimLastVisualOperatorRange?.type ?: selectionType
editor.vimStateMachine.pushVisualMode(primarySubMode)
editor.pushVisualMode(primarySubMode)
editor.forEachCaret {
val range = it.vimLastVisualOperatorRange ?: VisualChange.default(selectionType)
@ -71,7 +71,7 @@ public abstract class VimVisualMotionGroupBase : VimVisualMotionGroup {
it.vimLastColumn = intendedColumn
}
} else {
editor.vimStateMachine.mode = Mode.VISUAL(
editor.mode = Mode.VISUAL(
selectionType,
returnTo ?: editor.vimStateMachine.mode.returnTo
)
@ -90,7 +90,7 @@ public abstract class VimVisualMotionGroupBase : VimVisualMotionGroup {
check(mode is Mode.VISUAL)
// Update visual subMode with new sub subMode
editor.vimStateMachine.mode = mode.copy(selectionType = selectionType)
editor.mode = mode.copy(selectionType = selectionType)
for (caret in editor.carets()) {
if (!caret.isValid) continue
caret.vimUpdateEditorSelection()
@ -162,7 +162,7 @@ public abstract class VimVisualMotionGroupBase : VimVisualMotionGroup {
*/
override fun enterVisualMode(editor: VimEditor, subMode: SelectionType?): Boolean {
val autodetectedSubMode = subMode ?: autodetectVisualSubmode(editor)
editor.vimStateMachine.mode = Mode.VISUAL(autodetectedSubMode)
editor.mode = Mode.VISUAL(autodetectedSubMode)
// editor.vimStateMachine.setMode(VimStateMachine.Mode.VISUAL, autodetectedSubMode)
if (autodetectedSubMode == SelectionType.BLOCK_WISE) {
editor.primaryCaret().run { vimSelectionStart = vimLeadSelectionOffset }

View File

@ -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")
}

View File

@ -17,11 +17,10 @@ import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.key.Node
import com.maddyhome.idea.vim.key.RootNode
import org.jetbrains.annotations.TestOnly
import java.util.*
import javax.swing.KeyStroke
public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<LazyVimCommand>) {
private val commandParts = ArrayDeque<Command>()
public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<LazyVimCommand>): Cloneable {
private var commandParts = ArrayDeque<Command>()
private var keyList = mutableListOf<KeyStroke>()
public var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND
@ -66,7 +65,7 @@ public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<
public fun popCommandPart(): Command {
val command = commandParts.removeLast()
expectedArgumentType = if (commandParts.size > 0) commandParts.peekLast().action.argumentType else null
expectedArgumentType = if (commandParts.size > 0) commandParts.last().action.argumentType else null
return command
}
@ -107,7 +106,7 @@ public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<
public fun isAwaitingCharOrDigraphArgument(): Boolean {
if (commandParts.size == 0) return false
val argumentType = commandParts.peekLast().action.argumentType
val argumentType = commandParts.last().action.argumentType
val awaiting = argumentType == Argument.Type.CHARACTER || argumentType == Argument.Type.DIGRAPH
LOG.debug { "Awaiting char of digraph: $awaiting" }
return awaiting
@ -125,7 +124,7 @@ public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<
}
public fun isPuttingLiteral(): Boolean {
return !commandParts.isEmpty() && commandParts.last.action.id == "VimInsertCompletedLiteralAction"
return !commandParts.isEmpty() && commandParts.last().action.id == "VimInsertCompletedLiteralAction"
}
public fun isDone(): Boolean {
@ -133,21 +132,21 @@ public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<
}
public fun completeCommandPart(argument: Argument) {
commandParts.peekLast().argument = argument
commandParts.last().argument = argument
commandState = CurrentCommandState.READY
}
public fun isDuplicateOperatorKeyStroke(key: KeyStroke): Boolean {
val action = commandParts.peekLast().action as? DuplicableOperatorAction
val action = commandParts.last().action as? DuplicableOperatorAction
return action?.duplicateWith == key.keyChar
}
public fun hasCurrentCommandPartArgument(): Boolean {
return commandParts.peek()?.argument != null
return commandParts.firstOrNull()?.argument != null
}
public fun buildCommand(): Command {
if (commandParts.last.action.id == "VimInsertCompletedDigraphAction" || commandParts.last.action.id == "VimResetModeAction") {
if (commandParts.last().action.id == "VimInsertCompletedDigraphAction" || commandParts.last().action.id == "VimResetModeAction") {
expectedArgumentType = prevExpectedArgumentType
prevExpectedArgumentType = null
return commandParts.removeLast()
@ -191,6 +190,45 @@ public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<
@TestOnly
public fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as CommandBuilder
if (currentCommandPartNode != other.currentCommandPartNode) return false
if (commandParts != other.commandParts) return false
if (keyList != other.keyList) return false
if (commandState != other.commandState) return false
if (count != other.count) return false
if (expectedArgumentType != other.expectedArgumentType) return false
if (prevExpectedArgumentType != other.prevExpectedArgumentType) return false
return true
}
override fun hashCode(): Int {
var result = currentCommandPartNode.hashCode()
result = 31 * result + commandParts.hashCode()
result = 31 * result + keyList.hashCode()
result = 31 * result + commandState.hashCode()
result = 31 * result + count
result = 31 * result + (expectedArgumentType?.hashCode() ?: 0)
result = 31 * result + (prevExpectedArgumentType?.hashCode() ?: 0)
return result
}
public override fun clone(): CommandBuilder {
val result = CommandBuilder(currentCommandPartNode)
result.commandParts = ArrayDeque(commandParts)
result.keyList = keyList.toMutableList()
result.commandState = commandState
result.count = count
result.expectedArgumentType = expectedArgumentType
result.prevExpectedArgumentType = prevExpectedArgumentType
return result
}
public companion object {
private val LOG = vimLogger<CommandBuilder>()

View File

@ -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
@ -17,27 +18,35 @@ import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.trace
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.KeyConsumer
import com.maddyhome.idea.vim.key.KeyMappingLayer
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.key.MappingInfoLayer
import com.maddyhome.idea.vim.state.KeyHandlerState
import javax.swing.KeyStroke
public object MappingProcessor {
public object MappingProcessor: KeyConsumer {
private val log = vimLogger<MappingProcessor>()
internal fun handleKeyMapping(
editor: VimEditor,
public override fun consumeKey(
key: KeyStroke,
context: ExecutionContext,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
if (!allowKeyMappings) return false
log.debug("Start processing key mappings.")
val keyState = keyProcessResultBuilder.state
val commandState = editor.vimStateMachine
val mappingState = commandState.mappingState
val commandBuilder = commandState.commandBuilder
val mappingState = keyState.mappingState
val commandBuilder = keyState.commandBuilder
if (commandBuilder.isAwaitingCharOrDigraphArgument() ||
commandBuilder.isBuildingMultiKeyCommand() ||
isMappingDisabledForKey(key, commandState) ||
isMappingDisabledForKey(key, keyState) ||
commandState.isRegisterPending
) {
log.debug("Finish key processing, returning false")
@ -48,32 +57,32 @@ public object MappingProcessor {
// Save the unhandled keystrokes until we either complete or abandon the sequence.
log.trace("Add key to mapping state")
mappingState.addKey(key)
val mapping = injector.keyGroup.getKeyMappingLayer(mappingState.mappingMode)
log.trace { "Get keys for mapping mode. mode = " + mappingState.mappingMode }
val mappingMode = editor.mode.toMappingMode()
val mapping = injector.keyGroup.getKeyMappingLayer(mappingMode)
log.trace { "Get keys for mapping mode. mode = $mappingMode" }
// 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, mappingState, mapping, mappingCompleted) ||
handleCompleteMappingSequence(editor, context, mappingState, mapping, key) ||
handleAbandonedMappingSequence(editor, mappingState, context)
handleUnfinishedMappingSequence(keyProcessResultBuilder, mapping, mappingCompleted) ||
handleCompleteMappingSequence(keyProcessResultBuilder, mapping, key) ||
handleAbandonedMappingSequence(keyProcessResultBuilder)
log.debug { "Finish mapping processing. Return $mappingProcessed" }
return mappingProcessed
}
private fun isMappingDisabledForKey(key: KeyStroke, vimStateMachine: VimStateMachine): Boolean {
private fun isMappingDisabledForKey(key: KeyStroke, keyState: KeyHandlerState): Boolean {
// "0" can be mapped, but the mapping isn't applied when entering a count. Other digits are always mapped, even when
// entering a count.
// See `:help :map-modes`
val isMappingDisabled = key.keyChar == '0' && vimStateMachine.commandBuilder.count > 0
val isMappingDisabled = key.keyChar == '0' && keyState.commandBuilder.count > 0
log.debug { "Mapping disabled for key: $isMappingDisabled" }
return isMappingDisabled
}
private fun handleUnfinishedMappingSequence(
editor: VimEditor,
mappingState: MappingState,
processBuilder: KeyProcessResult.KeyProcessResultBuilder,
mapping: KeyMappingLayer,
mappingCompleted: Boolean,
): Boolean {
@ -88,7 +97,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
}
@ -97,6 +106,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.
@ -104,6 +118,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(
{
@ -122,12 +137,14 @@ 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),
allowKeyMappings = true,
mappingCompleted = lastKeyInSequence,
keyState,
)
}
},
@ -136,18 +153,16 @@ public object MappingProcessor {
}
}
log.trace("Unfinished mapping processing finished")
return true
}
private fun handleCompleteMappingSequence(
editor: VimEditor,
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) {
@ -173,12 +188,25 @@ 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")
try {
mappingState.startMapExecution()
mappingInfo.execute(editor, context)
mappingInfo.execute(editor, context, keyState)
} catch (e: Exception) {
injector.messages.showStatusBarMessage(editor, e.message)
injector.messages.indicateError()
@ -206,22 +234,17 @@ public object MappingProcessor {
// If we've just evaluated the previous key sequence, make sure to also handle the current key
if (mappingInfo !== currentMappingInfo) {
log.trace("Evaluating the current key")
KeyHandler.getInstance().handleKey(editor, key, currentContext, allowKeyMappings = true, false)
KeyHandler.getInstance().handleKey(editor, key, currentContext, allowKeyMappings = true, false, keyState)
}
log.trace("Success processing of mapping")
return true
}
private fun handleAbandonedMappingSequence(
editor: VimEditor,
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) {
@ -236,6 +259,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(
@ -244,18 +273,18 @@ public object MappingProcessor {
context,
allowKeyMappings = true,
mappingCompleted = false,
keyState,
)
} else {
log.trace("Process abandoned keys.")
KeyHandler.getInstance()
.handleKey(editor, unhandledKeyStrokes[0], context, allowKeyMappings = false, mappingCompleted = false)
.handleKey(editor, unhandledKeyStrokes[0], context, allowKeyMappings = false, mappingCompleted = false, keyState)
for (keyStroke in unhandledKeyStrokes.subList(1, unhandledKeyStrokes.size)) {
KeyHandler.getInstance()
.handleKey(editor, keyStroke, context, allowKeyMappings = true, mappingCompleted = false)
.handleKey(editor, keyStroke, context, allowKeyMappings = true, mappingCompleted = false, keyState)
}
}
log.trace("Return true from abandoned keys processing.")
return true
}
// The <Plug>mappings are not executed if they fail to map to something.

View File

@ -16,7 +16,7 @@ import java.awt.event.ActionListener
import javax.swing.KeyStroke
import javax.swing.Timer
public class MappingState {
public class MappingState: Cloneable {
// Map command depth. 0 - if it is not a map command. 1 - regular map command. 2+ - nested map commands
private var mapDepth = 0
@ -35,12 +35,7 @@ public class MappingState {
public val keys: Iterable<KeyStroke>
get() = keyList
public var mappingMode: MappingMode = MappingMode.NORMAL
set(value) {
field = value
}
private val timer = Timer(injector.globalOptions().timeoutlen, null)
private var timer = VimTimer(injector.globalOptions().timeoutlen)
private var keyList = mutableListOf<KeyStroke>()
init {
@ -77,7 +72,54 @@ public class MappingState {
// NOTE: We intentionally don't reset mapping mode here
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MappingState
if (mapDepth != other.mapDepth) return false
if (timer != other.timer) return false
if (keyList != other.keyList) return false
return true
}
override fun hashCode(): Int {
var result = mapDepth
result = 31 * result + timer.hashCode()
result = 31 * result + keyList.hashCode()
return result
}
public override fun clone(): MappingState {
val result = MappingState()
result.timer = timer
result.mapDepth = mapDepth
result.keyList = keyList.toMutableList()
return result
}
public companion object {
private val LOG = vimLogger<MappingState>()
}
}
public class VimTimer(delay: Int) : Timer(delay, null) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as VimTimer
if (delay != other.delay) return false
if (initialDelay != other.initialDelay) return false
if (isRunning != other.isRunning) return false
return true
}
override fun hashCode(): Int {
return javaClass.hashCode()
}
}
}

View File

@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.diagnostic.vimLogger
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class DigraphSequence {
public class DigraphSequence: Cloneable {
private var digraphState = DIG_STATE_PENDING
private var digraphChar = 0.toChar()
private lateinit var codeChars: CharArray
@ -222,6 +222,46 @@ public class DigraphSequence {
codeChars = CharArray(8)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DigraphSequence
if (digraphState != other.digraphState) return false
if (digraphChar != other.digraphChar) return false
if (!codeChars.contentEquals(other.codeChars)) return false
if (codeCnt != other.codeCnt) return false
if (codeType != other.codeType) return false
if (codeMax != other.codeMax) return false
return true
}
override fun hashCode(): Int {
var result = digraphState
result = 31 * result + digraphChar.hashCode()
result = 31 * result + codeChars.contentHashCode()
result = 31 * result + codeCnt
result = 31 * result + codeType
result = 31 * result + codeMax
return result
}
public override fun clone(): DigraphSequence {
val result = DigraphSequence()
result.digraphState = digraphState
result.digraphChar = digraphChar
if (::codeChars.isInitialized) {
result.codeChars = codeChars.copyOf()
}
result.codeCnt = codeCnt
result.codeType = codeType
result.codeMax = codeMax
return result
}
public companion object {
private const val DIG_STATE_PENDING = 1
private const val DIG_STATE_DIG_ONE = 2

View File

@ -0,0 +1,15 @@
/*
* 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.common
import com.maddyhome.idea.vim.api.VimEditor
public interface IsReplaceCharListener {
public fun isReplaceCharChanged(editor: VimEditor)
}

View File

@ -8,9 +8,7 @@
package com.maddyhome.idea.vim.common
import com.maddyhome.idea.vim.api.VimEditor
public interface MacroRecordingListener {
public fun recordingStarted(editor: VimEditor, register: Char)
public fun recordingFinished(editor: VimEditor, register: Char)
public fun recordingStarted()
public fun recordingFinished()
}

View File

@ -19,6 +19,7 @@ public class VimListenersNotifier {
public val myEditorListeners: MutableCollection<EditorListener> = ConcurrentLinkedDeque()
public val macroRecordingListeners: MutableCollection<MacroRecordingListener> = ConcurrentLinkedDeque()
public val vimPluginListeners: MutableCollection<VimPluginListener> = ConcurrentLinkedDeque()
public val isReplaceCharListeners: MutableCollection<IsReplaceCharListener> = ConcurrentLinkedDeque()
public fun notifyModeChanged(editor: VimEditor, oldMode: Mode) {
modeChangeListeners.forEach { it.modeChanged(editor, oldMode) }
@ -40,12 +41,12 @@ public class VimListenersNotifier {
myEditorListeners.forEach { it.focusLost(editor) }
}
public fun notifyMacroRecordingStarted(editor: VimEditor, register: Char) {
macroRecordingListeners.forEach { it.recordingStarted(editor, register) }
public fun notifyMacroRecordingStarted() {
macroRecordingListeners.forEach { it.recordingStarted() }
}
public fun notifyMacroRecordingFinished(editor: VimEditor, register: Char) {
macroRecordingListeners.forEach { it.recordingFinished(editor, register) }
public fun notifyMacroRecordingFinished() {
macroRecordingListeners.forEach { it.recordingFinished() }
}
public fun notifyPluginTurnedOn() {
@ -55,4 +56,16 @@ public class VimListenersNotifier {
public fun notifyPluginTurnedOff() {
vimPluginListeners.forEach { it.turnedOff() }
}
public fun notifyIsReplaceCharChanged(editor: VimEditor) {
isReplaceCharListeners.forEach { it.isReplaceCharChanged(editor) }
}
public fun reset() {
modeChangeListeners.clear()
myEditorListeners.clear()
macroRecordingListeners.clear()
vimPluginListeners.clear()
isReplaceCharListeners.clear()
}
}

View File

@ -51,7 +51,7 @@ public fun setVisualSelection(selectionStart: Int, selectionEnd: Int, caret: Vim
editor.vimSetSystemBlockSelectionSilently(blockStart, blockEnd)
// We've just added secondary carets again, hide them to better emulate block selection
editor.updateCaretsVisualAttributes()
injector.editorGroup.updateCaretsVisualAttributes(editor)
for (aCaret in editor.nativeCarets()) {
if (!aCaret.isValid) continue

View File

@ -54,10 +54,10 @@ public inline fun <reified T : Enum<T>> enumSetOf(vararg value: T): EnumSet<T> =
else -> EnumSet.of(value[0], *value.slice(1..value.lastIndex).toTypedArray())
}
public fun VimStateMachine.setSelectMode(submode: SelectionType) {
public fun VimEditor.setSelectMode(submode: SelectionType) {
mode = Mode.SELECT(submode, this.mode.returnTo)
}
public fun VimStateMachine.pushVisualMode(submode: SelectionType) {
public fun VimEditor.pushVisualMode(submode: SelectionType) {
mode = Mode.VISUAL(submode, this.mode.returnTo)
}

View File

@ -17,7 +17,6 @@ import com.maddyhome.idea.vim.state.mode.ReturnTo
import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE
import com.maddyhome.idea.vim.state.mode.inBlockSelection
import com.maddyhome.idea.vim.state.mode.inVisualMode
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.returnTo
import com.maddyhome.idea.vim.state.mode.selectionType
@ -37,15 +36,15 @@ public fun VimEditor.exitVisualMode() {
val returnTo = this.vimStateMachine.mode.returnTo
when (returnTo) {
ReturnTo.INSERT -> {
this.vimStateMachine.mode = Mode.INSERT
this.mode = Mode.INSERT
}
ReturnTo.REPLACE -> {
this.vimStateMachine.mode = Mode.REPLACE
this.mode = Mode.REPLACE
}
null -> {
this.vimStateMachine.mode = Mode.NORMAL()
this.mode = Mode.NORMAL()
}
}
}

View File

@ -7,10 +7,8 @@
*/
package com.maddyhome.idea.vim.impl.state
import com.maddyhome.idea.vim.action.change.LazyVimCommand
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.globalOptions
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandBuilder
import com.maddyhome.idea.vim.command.CommandFlags
@ -18,51 +16,29 @@ import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.command.MappingState
import com.maddyhome.idea.vim.common.DigraphResult
import com.maddyhome.idea.vim.common.DigraphSequence
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.helper.noneOfEnum
import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnTo
import com.maddyhome.idea.vim.state.mode.SelectionType
import com.maddyhome.idea.vim.state.mode.returnTo
import org.jetbrains.annotations.Contract
import java.util.*
import javax.swing.KeyStroke
/**
* Used to maintain state before and while entering a Vim command (operator, motion, text object, etc.)
*
* // TODO: 21.02.2022 This constructor should be empty
*/
public class VimStateMachineImpl(private val editor: VimEditor?) : VimStateMachine {
override val commandBuilder: CommandBuilder = CommandBuilder(getKeyRootNode(MappingMode.NORMAL))
override var mode: Mode = Mode.NORMAL()
set(value) {
if (field == value) return
public class VimStateMachineImpl : VimStateMachine {
@Deprecated("Please use KeyHandlerState instead")
override val commandBuilder: CommandBuilder = KeyHandler.getInstance().keyHandlerState.commandBuilder
@Deprecated("Please use KeyHandlerState instead")
override val mappingState: MappingState = KeyHandler.getInstance().keyHandlerState.mappingState
@Deprecated("Please use KeyHandlerState instead")
override val digraphSequence: DigraphSequence = KeyHandler.getInstance().keyHandlerState.digraphSequence
val oldValue = field
field = value
setMappingMode()
if (editor != null) {
injector.listenersNotifier.notifyModeChanged(editor, oldValue)
}
onModeChanged()
}
override val mappingState: MappingState = MappingState()
override val digraphSequence: DigraphSequence = DigraphSequence()
override var isRecording: Boolean = false
set(value) {
field = value
doShowMode()
}
override var mode: Mode = Mode.NORMAL()
override var isDotRepeatInProgress: Boolean = false
override var isRegisterPending: Boolean = false
override var isReplaceCharacter: Boolean = false
set(value) {
field = value
onModeChanged()
}
/**
* The currently executing command
@ -76,32 +52,19 @@ public class VimStateMachineImpl(private val editor: VimEditor?) : VimStateMachi
*/
override var executingCommand: Command? = null
override val isOperatorPending: Boolean
get() = mappingState.mappingMode == MappingMode.OP_PENDING && !commandBuilder.isEmpty
override fun isOperatorPending(mode: Mode): Boolean {
val keyHandler = KeyHandler.getInstance()
return keyHandler.isOperatorPending(mode, keyHandler.keyHandlerState)
}
override fun isDuplicateOperatorKeyStroke(key: KeyStroke?): Boolean {
return isOperatorPending && commandBuilder.isDuplicateOperatorKeyStroke(key!!)
override fun isDuplicateOperatorKeyStroke(key: KeyStroke, mode: Mode): Boolean {
val keyHandler = KeyHandler.getInstance()
return keyHandler.isDuplicateOperatorKeyStroke(key, mode, keyHandler.keyHandlerState)
}
override val executingCommandFlags: EnumSet<CommandFlags>
get() = executingCommand?.flags ?: noneOfEnum()
override fun resetOpPending() {
if (this.mode is Mode.OP_PENDING) {
val returnTo = this.mode.returnTo
mode = when (returnTo) {
ReturnTo.INSERT -> Mode.INSERT
ReturnTo.REPLACE -> Mode.INSERT
null -> Mode.NORMAL()
}
}
}
override fun resetReplaceCharacter() {
if (isReplaceCharacter) {
isReplaceCharacter = false
}
}
override fun resetRegisterPending() {
if (isRegisterPending) {
@ -109,44 +72,19 @@ public class VimStateMachineImpl(private val editor: VimEditor?) : VimStateMachi
}
}
private fun resetModes() {
// modeStates.clear()
mode = Mode.NORMAL()
onModeChanged()
setMappingMode()
}
private fun onModeChanged() {
if (editor != null) {
editor.updateCaretsVisualAttributes()
editor.updateCaretsVisualPosition()
} else {
injector.editorGroup.getEditors().forEach { editor ->
editor.updateCaretsVisualAttributes()
editor.updateCaretsVisualPosition()
}
}
doShowMode()
}
private fun setMappingMode() {
mappingState.mappingMode = modeToMappingMode(this.mode)
}
override fun startDigraphSequence() {
digraphSequence.startDigraphSequence()
val keyHandler = KeyHandler.getInstance()
keyHandler.keyHandlerState.digraphSequence.startDigraphSequence()
}
override fun startLiteralSequence() {
digraphSequence.startLiteralSequence()
val keyHandler = KeyHandler.getInstance()
keyHandler.keyHandlerState.digraphSequence.startLiteralSequence()
}
override fun processDigraphKey(key: KeyStroke, editor: VimEditor): DigraphResult {
return digraphSequence.processKey(key, editor)
}
override fun resetDigraph() {
digraphSequence.reset()
val keyHandler = KeyHandler.getInstance()
return keyHandler.keyHandlerState.digraphSequence.processKey(key, editor)
}
/**
@ -166,74 +104,7 @@ public class VimStateMachineImpl(private val editor: VimEditor?) : VimStateMachi
}
}
/**
* Resets the command, mode, visual mode, and mapping mode to initial values.
*/
override fun reset() {
executingCommand = null
resetModes()
commandBuilder.resetInProgressCommandPart(getKeyRootNode(mappingState.mappingMode))
digraphSequence.reset()
}
private fun doShowMode() {
val msg = StringBuilder()
if (injector.globalOptions().showmode) {
msg.append(getStatusString())
}
if (isRecording) {
if (msg.isNotEmpty()) {
msg.append(" - ")
}
msg.append(injector.messages.message("show.mode.recording"))
}
injector.messages.showMode(editor, msg.toString())
}
override fun getStatusString(): String {
val modeState = this.mode
return buildString {
when (modeState) {
is Mode.NORMAL -> {
if (modeState.returnTo != null) append("-- (insert) --")
}
Mode.INSERT -> append("-- INSERT --")
Mode.REPLACE -> append("-- REPLACE --")
is Mode.VISUAL -> {
val inInsert = if (modeState.returnTo != null) "(insert) " else ""
append("-- ${inInsert}VISUAL")
when (modeState.selectionType) {
SelectionType.LINE_WISE -> append(" LINE")
SelectionType.BLOCK_WISE -> append(" BLOCK")
else -> Unit
}
append(" --")
}
is Mode.SELECT -> {
val inInsert = if (modeState.returnTo != null) "(insert) " else ""
append("-- ${inInsert}SELECT")
when (modeState.selectionType) {
SelectionType.LINE_WISE -> append(" LINE")
SelectionType.BLOCK_WISE -> append(" BLOCK")
else -> Unit
}
append(" --")
}
else -> Unit
}
}
}
public companion object {
private val logger = vimLogger<VimStateMachine>()
private fun getKeyRootNode(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> {
return injector.keyGroup.getKeyRoot(mappingMode)
}
@Contract(pure = true)
public fun modeToMappingMode(mode: Mode): MappingMode {
return when (mode) {
@ -247,3 +118,14 @@ public class VimStateMachineImpl(private val editor: VimEditor?) : VimStateMachi
}
}
}
public fun Mode.toMappingMode(): MappingMode {
return when (this) {
is Mode.NORMAL -> MappingMode.NORMAL
Mode.INSERT, Mode.REPLACE -> MappingMode.INSERT
is Mode.VISUAL -> MappingMode.VISUAL
is Mode.SELECT -> MappingMode.SELECT
is Mode.CMD_LINE -> MappingMode.CMD_LINE
is Mode.OP_PENDING -> MappingMode.OP_PENDING
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.key
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import javax.swing.KeyStroke
public interface KeyConsumer {
/**
* @return true if consumed key and could do something meaningful wit it
*/
public fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean
}

View File

@ -26,11 +26,10 @@ import com.maddyhome.idea.vim.extension.ExtensionHandler
import com.maddyhome.idea.vim.group.visual.VimSelection
import com.maddyhome.idea.vim.group.visual.VimSelection.Companion.create
import com.maddyhome.idea.vim.helper.VimNlsSafe
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType.CHARACTER_WISE
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.state.mode.selectionType
import com.maddyhome.idea.vim.vimscript.model.CommandLineVimLContext
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
@ -50,7 +49,7 @@ public sealed class MappingInfo(
@VimNlsSafe
abstract override fun getPresentableString(): String
abstract override fun execute(editor: VimEditor, context: ExecutionContext)
abstract override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState)
override fun compareTo(other: MappingInfo): Int {
val size = fromKeys.size
@ -86,7 +85,7 @@ public class ToKeysMappingInfo(
) : MappingInfo(fromKeys, isRecursive, owner) {
override fun getPresentableString(): String = injector.parser.toKeyNotation(toKeys)
override fun execute(editor: VimEditor, context: ExecutionContext) {
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToKeys' mapping info...")
val editorDataContext = injector.executionContextManager.onEditor(editor, context)
val fromIsPrefix = KeyHandler.isPrefix(fromKeys, toKeys)
@ -98,7 +97,7 @@ public class ToKeysMappingInfo(
while (keyHandler.keyStack.hasStroke()) {
val keyStroke = keyHandler.keyStack.feedStroke()
val recursive = isRecursive && !(first && fromIsPrefix)
keyHandler.handleKey(editor, keyStroke, editorDataContext, recursive, false)
keyHandler.handleKey(editor, keyStroke, editorDataContext, recursive, false, keyState)
first = false
}
} finally {
@ -124,7 +123,7 @@ public class ToExpressionMappingInfo(
) : MappingInfo(fromKeys, isRecursive, owner) {
override fun getPresentableString(): String = originalString
override fun execute(editor: VimEditor, context: ExecutionContext) {
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToExpression' mapping info...")
val editorDataContext = injector.executionContextManager.onEditor(editor, context)
val toKeys = injector.parser.parseKeys(toExpression.evaluate(editor, context, CommandLineVimLContext).toString())
@ -133,7 +132,7 @@ public class ToExpressionMappingInfo(
for (keyStroke in toKeys) {
val recursive = isRecursive && !(first && fromIsPrefix)
val keyHandler = KeyHandler.getInstance()
keyHandler.handleKey(editor, keyStroke, editorDataContext, recursive, false)
keyHandler.handleKey(editor, keyStroke, editorDataContext, recursive, false, keyState)
first = false
}
}
@ -151,14 +150,14 @@ public class ToHandlerMappingInfo(
) : MappingInfo(fromKeys, isRecursive, owner) {
override fun getPresentableString(): String = "call ${extensionHandler.javaClass.canonicalName}"
override fun execute(editor: VimEditor, context: ExecutionContext) {
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToHandler' mapping info...")
val vimStateMachine = VimStateMachine.getInstance(editor)
// Cache isOperatorPending in case the extension changes the mode while moving the caret
// See CommonExtensionTest
// TODO: Is this legal? Should we assert in this case?
val shouldCalculateOffsets: Boolean = vimStateMachine.isOperatorPending
val shouldCalculateOffsets: Boolean = vimStateMachine.isOperatorPending(editor.mode)
val startOffsets: Map<ImmutableVimCaret, Offset> = editor.carets().associateWith { it.offset }
@ -169,24 +168,24 @@ public class ToHandlerMappingInfo(
val handler = extensionHandler
if (handler is ExtensionHandler.WithCallback) {
handler._backingFunction = Runnable {
myFun(shouldCalculateOffsets, editor, startOffsets)
myFun(shouldCalculateOffsets, editor, startOffsets, keyState)
if (shouldCalculateOffsets) {
injector.application.invokeLater {
KeyHandler.getInstance().finishedCommandPreparation(
val keyHandler = KeyHandler.getInstance()
keyHandler.finishedCommandPreparation(
editor,
context,
VimStateMachine.getInstance(editor),
VimStateMachine.getInstance(editor).commandBuilder,
null,
false,
KeyHandler.MutableBoolean(false),
keyState,
)
}
}
}
}
val operatorArguments = OperatorArguments(vimStateMachine.isOperatorPending, vimStateMachine.commandBuilder.count, vimStateMachine.mode)
val operatorArguments = OperatorArguments(vimStateMachine.isOperatorPending(editor.mode), keyState.commandBuilder.count, vimStateMachine.mode)
injector.actionExecutor.executeCommand(
editor,
{ extensionHandler.execute(editor, context, operatorArguments) },
@ -201,7 +200,7 @@ public class ToHandlerMappingInfo(
}
if (handler !is ExtensionHandler.WithCallback) {
myFun(shouldCalculateOffsets, editor, startOffsets)
myFun(shouldCalculateOffsets, editor, startOffsets, keyState)
}
}
@ -212,9 +211,9 @@ public class ToHandlerMappingInfo(
shouldCalculateOffsets: Boolean,
editor: VimEditor,
startOffsets: Map<ImmutableVimCaret, Offset>,
keyState: KeyHandlerState,
) {
val commandState = editor.vimStateMachine
if (shouldCalculateOffsets && !commandState.commandBuilder.hasCurrentCommandPartArgument()) {
if (shouldCalculateOffsets && !keyState.commandBuilder.hasCurrentCommandPartArgument()) {
val offsets: MutableMap<ImmutableVimCaret, VimSelection> = HashMap()
for (caret in editor.carets()) {
var startOffset = startOffsets[caret]
@ -222,7 +221,7 @@ public class ToHandlerMappingInfo(
val vimSelection =
create(caret.vimSelectionStart, caret.offset.point, editor.mode.selectionType ?: CHARACTER_WISE, editor)
offsets[caret] = vimSelection
commandState.mode = Mode.NORMAL()
editor.mode = Mode.NORMAL()
} else if (startOffset != null && startOffset.point != caret.offset.point) {
// Command line motions are always characterwise exclusive
var endOffset = caret.offset
@ -240,7 +239,7 @@ public class ToHandlerMappingInfo(
}
}
if (offsets.isNotEmpty()) {
commandState.commandBuilder.completeCommandPart(Argument(offsets))
keyState.commandBuilder.completeCommandPart(Argument(offsets))
}
}
}
@ -255,7 +254,7 @@ public class ToActionMappingInfo(
) : MappingInfo(fromKeys, isRecursive, owner) {
override fun getPresentableString(): String = "action $action"
override fun execute(editor: VimEditor, context: ExecutionContext) {
override fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState) {
LOG.debug("Executing 'ToAction' mapping...")
val editorDataContext = injector.executionContextManager.onEditor(editor, context)
val dataContext = injector.executionContextManager.onCaret(editor.currentCaret(), editorDataContext)

View File

@ -10,8 +10,9 @@ package com.maddyhome.idea.vim.key
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.state.KeyHandlerState
public interface MappingInfoLayer {
public fun getPresentableString(): String
public fun execute(editor: VimEditor, context: ExecutionContext)
public fun execute(editor: VimEditor, context: ExecutionContext, keyState: KeyHandlerState)
}

View File

@ -42,10 +42,21 @@ import javax.swing.KeyStroke
public interface Node<T>
/** Represents a complete command */
public class CommandNode<T>(public val actionHolder: T) : Node<T>
public data class CommandNode<T>(public val actionHolder: T) : Node<T>
/** Represents a part of the command */
public open class CommandPartNode<T> : Node<T>, HashMap<KeyStroke, Node<T>>()
public open class CommandPartNode<T> : Node<T>, HashMap<KeyStroke, Node<T>>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false
return true
}
override fun hashCode(): Int {
return super.hashCode()
}
}
/** Represents a root node for the mode */
public class RootNode<T> : CommandPartNode<T>()

View File

@ -0,0 +1,75 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.CommandBuilder
import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.key.KeyConsumer
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class CharArgumentConsumer: KeyConsumer {
private companion object {
private val logger = vimLogger<CharArgumentConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
if (!isExpectingCharArgument(keyProcessResultBuilder.state.commandBuilder)) return false
val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar
handleCharArgument(key, chKey, keyProcessResultBuilder)
return true
}
private fun isExpectingCharArgument(commandBuilder: CommandBuilder): Boolean {
val expectingCharArgument = commandBuilder.expectedArgumentType === Argument.Type.CHARACTER
logger.debug { "Expecting char argument: $expectingCharArgument" }
return expectingCharArgument
}
private fun handleCharArgument(key: KeyStroke, chKey: Char, processBuilder: KeyProcessResult.KeyProcessResultBuilder) {
var mutableChKey = chKey
logger.trace("Handling char argument")
// We are expecting a character argument - is this a regular character the user typed?
// Some special keys can be handled as character arguments - let's check for them here.
if (mutableChKey.code == 0) {
when (key.keyCode) {
KeyEvent.VK_TAB -> mutableChKey = '\t'
KeyEvent.VK_ENTER -> mutableChKey = '\n'
}
}
val commandBuilder = processBuilder.state.commandBuilder
if (mutableChKey.code != 0) {
processBuilder.addExecutionStep { _, lambdaEditor, _ ->
// Create the character argument, add it to the current command, and signal we are ready to process the command
logger.trace("Add character argument to the current command")
commandBuilder.completeCommandPart(Argument(mutableChKey))
lambdaEditor.isReplaceCharacter = false
}
} else {
logger.trace("This is not a valid character argument. Set command state to BAD_COMMAND")
// Oops - this isn't a valid character argument
processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ ->
KeyHandler.getInstance().setBadCommand(lambdaEditor, lambdaKeyState)
}
}
}
}

View File

@ -0,0 +1,217 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.action.change.LazyVimCommand
import com.maddyhome.idea.vim.api.ExecutionContext
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.Argument
import com.maddyhome.idea.vim.command.Command
import com.maddyhome.idea.vim.command.CommandFlags
import com.maddyhome.idea.vim.common.CurrentCommandState
import com.maddyhome.idea.vim.common.argumentCaptured
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.handler.EditorActionHandlerBase
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.CommandNode
import com.maddyhome.idea.vim.key.CommandPartNode
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.key.Node
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.ReturnableFromCmd
import com.maddyhome.idea.vim.state.mode.returnTo
import javax.swing.KeyStroke
public class CommandConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<CommandConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
val commandBuilder = keyProcessResultBuilder.state.commandBuilder
// 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), editor.mode, keyProcessResultBuilder.state)
logger.trace("Get the node for the current mode")
when (node) {
is CommandNode<LazyVimCommand> -> {
logger.trace("Node is a command node")
handleCommandNode(key, node, keyProcessResultBuilder)
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, _, _ -> lambdaKeyState.commandBuilder.addKey(key) }
return true
}
is CommandPartNode<LazyVimCommand> -> {
logger.trace("Node is a command part node")
commandBuilder.setCurrentCommandPartNode(node)
commandBuilder.addKey(key)
return true
}
else -> {
return false
}
}
}
/**
* See the description for [com.maddyhome.idea.vim.command.DuplicableOperatorAction]
*/
private fun mapOpCommand(
key: KeyStroke,
node: Node<LazyVimCommand>?,
mode: Mode,
keyState: KeyHandlerState,
): Node<LazyVimCommand>? {
return if (KeyHandler.getInstance().isDuplicateOperatorKeyStroke(key, mode, keyState)) {
keyState.commandBuilder.getChildNode(KeyStroke.getKeyStroke('_'))
} else {
node
}
}
private fun handleCommandNode(
key: KeyStroke,
node: CommandNode<LazyVimCommand>,
processBuilder: KeyProcessResult.KeyProcessResultBuilder,
) {
logger.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)) {
logger.trace("Return from command node handling")
processBuilder.addExecutionStep { lamdaKeyState, lambdaEditor, _ ->
KeyHandler.getInstance().setBadCommand(lambdaEditor, lamdaKeyState)
}
return
}
if (action.argumentType == null || stopMacroRecord(node)) {
logger.trace("Set command state to READY")
commandBuilder.commandState = CurrentCommandState.READY
} else {
processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext ->
logger.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)
}
}
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").
*/
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
lambdaEditor.mode = lambdaEditor.mode.returnTo()
}
}
}
private fun stopMacroRecord(node: CommandNode<LazyVimCommand>): Boolean {
return injector.registerGroup.isRecording && node.actionHolder.instance.id == "VimToggleRecordingAction"
}
private fun startWaitingForArgument(
editor: VimEditor,
context: ExecutionContext,
key: Char,
action: EditorActionHandlerBase,
argument: Argument.Type,
keyState: KeyHandlerState,
editorState: VimStateMachine,
) {
val commandBuilder = keyState.commandBuilder
when (argument) {
Argument.Type.MOTION -> {
if (editorState.isDotRepeatInProgress && argumentCaptured != null) {
commandBuilder.completeCommandPart(argumentCaptured!!)
}
editor.mode = Mode.OP_PENDING(editorState.mode.returnTo)
}
Argument.Type.DIGRAPH -> // Command actions represent the completion of a command. Showcmd relies on this - if the action represents a
// part of a command, the showcmd output is reset part way through. This means we need to special case entering
// digraph/literal input mode. We have an action that takes a digraph as an argument, and pushes it back through
// the key handler when it's complete.
// TODO
// if (action is InsertCompletedDigraphAction) {
if (action.id == "VimInsertCompletedDigraphAction") {
keyState.digraphSequence.startDigraphSequence()
KeyHandler.getInstance().setPromptCharacterEx('?')
} else if (action.id == "VimInsertCompletedLiteralAction") {
keyState.digraphSequence.startLiteralSequence()
KeyHandler.getInstance().setPromptCharacterEx('^')
}
Argument.Type.EX_STRING -> {
// The current Command expects an EX_STRING argument. E.g. SearchEntry(Fwd|Rev)Action. This won't execute until
// state hits READY. Start the ex input field, push CMD_LINE mode and wait for the argument.
injector.processGroup.startSearchCommand(editor, context, commandBuilder.count, key)
commandBuilder.commandState = CurrentCommandState.NEW_COMMAND
val currentMode = editorState.mode
check(currentMode is ReturnableFromCmd) { "Cannot enable command line mode $currentMode" }
editor.mode = Mode.CMD_LINE(currentMode)
}
else -> Unit
}
// Another special case. Force a mode change to update the caret shape
// This was a typed solution
// if (action is ChangeCharacterAction || action is ChangeVisualCharacterAction)
if (action.id == "VimChangeCharacterAction" || action.id == "VimChangeVisualCharacterAction") {
editor.isReplaceCharacter = true
}
}
private fun checkArgumentCompatibility(
expectedArgumentType: Argument.Type?,
action: EditorActionHandlerBase,
): Boolean {
return !(expectedArgumentType === Argument.Type.MOTION && action.type !== Command.Type.MOTION)
}
}

View File

@ -0,0 +1,59 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.mode.Mode
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class CommandCountConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<CommandCountConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar
if (!isCommandCountKey(chKey, keyProcessResultBuilder.state, editor)) return false
keyProcessResultBuilder.state.commandBuilder.addCountCharacter(key)
return true
}
private fun isCommandCountKey(chKey: Char, keyState: KeyHandlerState, editor: VimEditor): Boolean {
// Make sure to avoid handling '0' as the start of a count.
val editorState = editor.vimStateMachine
val commandBuilder = keyState.commandBuilder
val notRegisterPendingCommand = editorState.mode is Mode.NORMAL && !editorState.isRegisterPending
val visualMode = editorState.mode is Mode.VISUAL && !editorState.isRegisterPending
val opPendingMode = editorState.mode is Mode.OP_PENDING
if (notRegisterPendingCommand || visualMode || opPendingMode) {
if (commandBuilder.isExpectingCount && Character.isDigit(chKey) && (commandBuilder.count > 0 || chKey != '0')) {
logger.debug("This is a command key count")
return true
}
}
logger.debug("This is NOT a command key count")
return false
}
}

View File

@ -0,0 +1,50 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.mode.Mode
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class DeleteCommandConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<DeleteCommandConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
if (!isDeleteCommandCountKey(key, keyProcessResultBuilder.state, editor.mode)) return false
keyProcessResultBuilder.state.commandBuilder.deleteCountCharacter()
return true
}
private fun isDeleteCommandCountKey(key: KeyStroke, keyState: KeyHandlerState, mode: Mode): Boolean {
// See `:help N<Del>`
val commandBuilder = keyState.commandBuilder
val isDeleteCommandKeyCount =
(mode is Mode.NORMAL || mode is Mode.VISUAL || mode is Mode.OP_PENDING) &&
commandBuilder.isExpectingCount && commandBuilder.count > 0 && key.keyCode == KeyEvent.VK_DELETE
logger.debug { "This is a delete command key count: $isDeleteCommandKeyCount" }
return isDeleteCommandKeyCount
}
}

View File

@ -0,0 +1,125 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
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.common.DigraphResult
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.key.KeyConsumer
import java.awt.event.InputEvent
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class DigraphConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<DigraphConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
logger.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) {
logger.trace("Expected argument is digraph")
if (digraphSequence.isDigraphStart(key)) {
digraphSequence.startDigraphSequence()
commandBuilder.addKey(key)
return true
}
if (digraphSequence.isLiteralStart(key)) {
digraphSequence.startLiteralSequence()
commandBuilder.addKey(key)
return true
}
}
val res = digraphSequence.processKey(key, editor)
val keyHandler = KeyHandler.getInstance()
when (res.result) {
DigraphResult.RES_HANDLED -> {
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, _, _ ->
if (injector.exEntryPanel.isActive()) {
keyHandler.setPromptCharacterEx(if (lambdaKeyState.commandBuilder.isPuttingLiteral()) '^' else key.keyChar)
}
lambdaKeyState.commandBuilder.addKey(key)
}
return true
}
DigraphResult.RES_DONE -> {
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
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditorState, lambdaContext ->
lambdaKeyState.commandBuilder.addKey(key)
keyHandler.handleKey(lambdaEditorState, stroke, lambdaContext, lambdaKeyState)
}
return true
}
DigraphResult.RES_BAD -> {
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) {
KeyHandler.getInstance().setBadCommand(lambdaEditor, lambdaKeyState)
}
}
return true
}
DigraphResult.RES_UNHANDLED -> {
// UNHANDLED means the keystroke made no sense in the context of a digraph, but isn't an error in the current
// 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()
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext ->
keyHandler.handleKey(lambdaEditor, key, lambdaContext, lambdaKeyState)
}
return true
}
return false
}
}
return false
}
}

View File

@ -0,0 +1,80 @@
/*
* 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.key.consumers
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
import com.maddyhome.idea.vim.diagnostic.debug
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.helper.isCloseKeyStroke
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.mode.Mode
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class EditorResetConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<EditorResetConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
if (!isEditorReset(key, editor)) return false
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, lambdaContext -> handleEditorReset(lambdaEditor, key, lambdaKeyState, lambdaContext) }
return true
}
private fun isEditorReset(key: KeyStroke, editor: VimEditor): Boolean {
val editorReset = editor.mode is Mode.NORMAL && key.isCloseKeyStroke()
logger.debug { "This is editor reset: $editorReset" }
return editorReset
}
private fun handleEditorReset(
editor: VimEditor,
key: KeyStroke,
keyState: KeyHandlerState,
context: ExecutionContext,
) {
val commandBuilder = keyState.commandBuilder
if (commandBuilder.isAwaitingCharOrDigraphArgument()) {
editor.isReplaceCharacter = false
}
if (commandBuilder.isAtDefaultState) {
val register = injector.registerGroup
if (register.currentRegister == register.defaultRegister) {
var indicateError = true
if (key.keyCode == KeyEvent.VK_ESCAPE) {
val executed = arrayOf<Boolean?>(null)
injector.actionExecutor.executeCommand(
editor,
{ executed[0] = injector.actionExecutor.executeEsc(context) },
"",
null,
)
indicateError = !executed[0]!!
}
if (indicateError) {
injector.messages.indicateError()
}
}
}
KeyHandler.getInstance().reset(keyState, editor.mode)
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.state.mode.Mode
import javax.swing.KeyStroke
public class ModeInputConsumer: KeyConsumer {
private companion object {
private val logger = vimLogger<ModeInputConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
val isProcessed = when (editor.mode) {
Mode.INSERT, Mode.REPLACE -> {
logger.trace("Process insert or replace")
val keyProcessed = injector.changeGroup.processKey(editor, key, keyProcessResultBuilder)
shouldRecord.value = keyProcessed && shouldRecord.value
keyProcessed
}
is Mode.SELECT -> {
logger.trace("Process select")
val keyProcessed = injector.changeGroup.processKeyInSelectMode(editor, key, keyProcessResultBuilder)
shouldRecord.value = keyProcessed && shouldRecord.value
keyProcessed
}
is Mode.CMD_LINE -> {
logger.trace("Process cmd line")
val keyProcessed = injector.processGroup.processExKey(editor, key, keyProcessResultBuilder)
shouldRecord.value = keyProcessed && shouldRecord.value
keyProcessed
}
else -> {
false
}
}
if (isProcessed) {
keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ ->
lambdaKeyState.partialReset(lambdaEditor.mode)
}
}
return isProcessed
}
}

View File

@ -0,0 +1,57 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.KeyConsumer
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
public class RegisterConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<CharArgumentConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
if (!editor.vimStateMachine.isRegisterPending) return false
logger.trace("Pending mode.")
keyProcessResultBuilder.state.commandBuilder.addKey(key)
val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar
handleSelectRegister(editor, chKey, keyProcessResultBuilder)
return true
}
private fun handleSelectRegister(editor: VimEditor, chKey: Char, processBuilder: KeyProcessResult.KeyProcessResultBuilder) {
logger.trace("Handle select register")
editor.vimStateMachine.resetRegisterPending()
if (injector.registerGroup.isValid(chKey)) {
logger.trace("Valid register")
processBuilder.state.commandBuilder.pushCommandPart(chKey)
} else {
processBuilder.addExecutionStep { lambdaKeyState, lambdaEditor, _ ->
logger.trace("Invalid register, set command state to BAD_COMMAND")
KeyHandler.getInstance().setBadCommand(lambdaEditor, lambdaKeyState)
}
}
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.key.consumers
import com.maddyhome.idea.vim.KeyHandler
import com.maddyhome.idea.vim.KeyProcessResult
import com.maddyhome.idea.vim.api.VimEditor
import com.maddyhome.idea.vim.diagnostic.vimLogger
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.key.KeyConsumer
import com.maddyhome.idea.vim.state.KeyHandlerState
import com.maddyhome.idea.vim.state.VimStateMachine
import com.maddyhome.idea.vim.state.mode.Mode
import javax.swing.KeyStroke
public class SelectRegisterConsumer : KeyConsumer {
private companion object {
private val logger = vimLogger<SelectRegisterConsumer>()
}
override fun consumeKey(
key: KeyStroke,
editor: VimEditor,
allowKeyMappings: Boolean,
mappingCompleted: Boolean,
keyProcessResultBuilder: KeyProcessResult.KeyProcessResultBuilder,
shouldRecord: KeyHandler.MutableBoolean,
): Boolean {
val state = keyProcessResultBuilder.state
if (!isSelectRegister(key, state, editor.vimStateMachine)) return false
logger.trace("Select register")
state.commandBuilder.addKey(key)
keyProcessResultBuilder.addExecutionStep { _, lambdaEditor, _ ->
lambdaEditor.vimStateMachine.isRegisterPending = true
}
return true
}
private fun isSelectRegister(key: KeyStroke, keyState: KeyHandlerState, editorState: VimStateMachine): Boolean {
if (editorState.mode !is Mode.NORMAL && editorState.mode !is Mode.VISUAL) {
return false
}
return if (editorState.isRegisterPending) {
true
} else {
key.keyChar == '"' && !KeyHandler.getInstance().isOperatorPending(editorState.mode, keyState) && keyState.commandBuilder.expectedArgumentType == null
}
}
}

View File

@ -25,6 +25,9 @@ public interface VimRegisterGroup {
public var lastRegisterChar: Char
public val currentRegister: Char
public val isRecording: Boolean
public val recordRegister: Char?
/**
* When we access last register, it can be e.g. " because of two reasons:
* 1. Because the default register value was used
@ -79,13 +82,13 @@ public interface VimRegisterGroup {
public fun getRegister(r: Char): Register?
public fun getRegisters(): List<Register>
public fun saveRegister(r: Char, register: Register)
public fun startRecording(editor: VimEditor, register: Char): Boolean
public fun startRecording(register: Char): Boolean
public fun getPlaybackRegister(r: Char): Register?
public fun recordText(text: String)
public fun setKeys(register: Char, keys: List<KeyStroke>)
public fun setKeys(register: Char, keys: List<KeyStroke>, type: SelectionType)
public fun finishRecording(editor: VimEditor)
public fun finishRecording()
public fun getCurrentRegisterForMulticaret(): Char // `set clipbaard+=unnamedplus` should not make system register the default one when working with multiple carets VIM-2804
public fun isSystemClipboard(register: Char): Boolean
public fun isPrimaryRegisterSupported(): Boolean

View File

@ -36,9 +36,18 @@ import com.maddyhome.idea.vim.register.RegisterConstants.WRITABLE_REGISTERS
import javax.swing.KeyStroke
public abstract class VimRegisterGroupBase : VimRegisterGroup {
override val isRecording: Boolean
get() = recordRegister != null
@JvmField
protected var recordRegister: Char = 0.toChar()
public override var recordRegister: Char? = null
set(value) {
field = value
if (value != null) {
injector.listenersNotifier.notifyMacroRecordingStarted()
} else {
injector.listenersNotifier.notifyMacroRecordingFinished()
}
}
@JvmField
protected var recordList: MutableList<KeyStroke>? = null
@ -127,7 +136,7 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
override fun recordKeyStroke(key: KeyStroke) {
val myRecordList = recordList
if (recordRegister != 0.toChar() && myRecordList != null) {
if (isRecording && myRecordList != null) {
myRecordList.add(key)
}
}
@ -475,12 +484,10 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
myRegisters[myR] = register
}
override fun startRecording(editor: VimEditor, register: Char): Boolean {
override fun startRecording(register: Char): Boolean {
return if (RECORDABLE_REGISTERS.indexOf(register) != -1) {
VimStateMachine.getInstance(editor).isRecording = true
recordRegister = register
recordList = ArrayList()
injector.listenersNotifier.notifyMacroRecordingStarted(editor, register)
true
} else {
false
@ -493,7 +500,7 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
override fun recordText(text: String) {
val myRecordList = recordList
if (recordRegister != 0.toChar() && myRecordList != null) {
if (isRecording && myRecordList != null) {
myRecordList.addAll(injector.parser.stringToKeys(text))
}
}
@ -506,27 +513,25 @@ public abstract class VimRegisterGroupBase : VimRegisterGroup {
myRegisters[register] = Register(register, type, keys.toMutableList())
}
override fun finishRecording(editor: VimEditor) {
if (recordRegister != 0.toChar()) {
override fun finishRecording() {
val register = recordRegister
if (register != null) {
var reg: Register? = null
if (Character.isUpperCase(recordRegister)) {
reg = getRegister(recordRegister)
if (Character.isUpperCase(register)) {
reg = getRegister(register)
}
val myRecordList = recordList
if (myRecordList != null) {
if (reg == null) {
reg = Register(Character.toLowerCase(recordRegister), SelectionType.CHARACTER_WISE, myRecordList)
myRegisters[Character.toLowerCase(recordRegister)] = reg
reg = Register(Character.toLowerCase(register), SelectionType.CHARACTER_WISE, myRecordList)
myRegisters[Character.toLowerCase(register)] = reg
} else {
reg.addKeys(myRecordList)
}
}
VimStateMachine.getInstance(editor).isRecording = false
injector.listenersNotifier.notifyMacroRecordingFinished(editor, recordRegister)
}
recordRegister = 0.toChar()
recordRegister = null
}
public companion object {

View File

@ -0,0 +1,44 @@
/*
* 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.state
import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.CommandBuilder
import com.maddyhome.idea.vim.command.MappingMode
import com.maddyhome.idea.vim.command.MappingState
import com.maddyhome.idea.vim.common.DigraphSequence
import com.maddyhome.idea.vim.impl.state.toMappingMode
import com.maddyhome.idea.vim.state.mode.Mode
public data class KeyHandlerState(
public val mappingState: MappingState,
public val digraphSequence: DigraphSequence,
public val commandBuilder: CommandBuilder,
): Cloneable {
public constructor() : this(MappingState(), DigraphSequence(), CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.NORMAL)))
public fun partialReset(mode: Mode) {
mappingState.resetMappingSequence()
commandBuilder.resetInProgressCommandPart(injector.keyGroup.getKeyRoot(mode.toMappingMode()))
}
public fun reset(mode: Mode) {
digraphSequence.reset()
mappingState.resetMappingSequence()
commandBuilder.resetAll(injector.keyGroup.getKeyRoot(mode.toMappingMode()))
}
public override fun clone(): KeyHandlerState {
return KeyHandlerState(
mappingState.clone(),
digraphSequence.clone(),
commandBuilder.clone()
)
}
}

View File

@ -26,14 +26,17 @@ import javax.swing.KeyStroke
* Used to maintain state before and while entering a Vim command (operator, motion, text object, etc.)
*/
public interface VimStateMachine {
@Deprecated("Please use KeyHandlerState instead")
public val commandBuilder: CommandBuilder
public var mode: Mode
@Deprecated("Please use KeyHandlerState instead")
public val mappingState: MappingState
@Deprecated("Please use KeyHandlerState instead")
public val digraphSequence: DigraphSequence
public var isRecording: Boolean
public val mode: Mode
public var isDotRepeatInProgress: Boolean
public var isRegisterPending: Boolean
public var isReplaceCharacter: Boolean
public val isReplaceCharacter: Boolean
/**
* The currently executing command
@ -46,17 +49,14 @@ public interface VimStateMachine {
* This field is reset after the command has been executed.
*/
public var executingCommand: Command?
public val isOperatorPending: Boolean
public fun isOperatorPending(mode: Mode): Boolean
public val executingCommandFlags: EnumSet<CommandFlags>
public fun isDuplicateOperatorKeyStroke(key: KeyStroke?): Boolean
public fun isDuplicateOperatorKeyStroke(key: KeyStroke, mode: Mode): Boolean
public fun resetOpPending()
public fun resetReplaceCharacter()
public fun resetRegisterPending()
public fun startLiteralSequence()
public fun processDigraphKey(key: KeyStroke, editor: VimEditor): DigraphResult
public fun resetDigraph()
/**
* Toggles the insert/overwrite state. If currently insert, goto replace mode. If currently replace, goto insert
@ -64,16 +64,12 @@ public interface VimStateMachine {
*/
public fun toggleInsertOverwrite()
/**
* Resets the command, mode, visual mode, and mapping mode to initial values.
*/
public fun reset()
public fun getStatusString(): String
public fun startDigraphSequence()
public companion object {
private val globalState = VimStateMachineImpl(null)
private val globalState = VimStateMachineImpl()
// TODO do we really need this method? Can't we use editor.vimStateMachine?
public fun getInstance(editor: Any?): VimStateMachine {
return if (editor == null || injector.globalOptions().ideaglobalmode) {
globalState

View File

@ -24,7 +24,6 @@ import com.maddyhome.idea.vim.ex.NoRangeAllowedException
import com.maddyhome.idea.vim.ex.ranges.LineRange
import com.maddyhome.idea.vim.ex.ranges.Ranges
import com.maddyhome.idea.vim.helper.Msg
import com.maddyhome.idea.vim.state.mode.mode
import com.maddyhome.idea.vim.helper.noneOfEnum
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.vimscript.model.Executable
@ -71,7 +70,7 @@ public sealed class Command(public var commandRanges: Ranges, public val command
}
val operatorArguments = OperatorArguments(
editor.vimStateMachine.isOperatorPending,
editor.vimStateMachine.isOperatorPending(editor.mode),
0,
editor.mode,
)

View File

@ -73,7 +73,7 @@ public data class NormalCommand(val ranges: Ranges, val argument: String) : Comm
val keyHandler = KeyHandler.getInstance()
keyHandler.reset(editor)
for (key in keys) {
keyHandler.handleKey(editor, key, context, useMappings, true)
keyHandler.handleKey(editor, key, context, useMappings, true, keyHandler.keyHandlerState)
}
// Exit if state leaves as insert or cmd_line