diff --git a/src/test/java/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt index 1986ff3b9c..d37b50fb8e 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt @@ -56,6 +56,16 @@ class ScrollPageDownActionTest : VimTestCase() { assertVisibleArea(33, 67) } + @TestWithoutNeovim(SkipNeovimReason.SCROLL) + @Test + fun `test scroll single page down with S-Enter`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText("") + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + @TestWithoutNeovim(SkipNeovimReason.SCROLL) @Test fun `test scroll page down in insert mode with S-Down`() { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt index 28a3cfcb17..7aeb633f00 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt @@ -22,7 +22,24 @@ import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.enumSetOf import java.util.* -@CommandOrMotion(keys = ["", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) +// The mapping is interesting. Vim has multiple mappings to move the caret down: , obviously, but also +// , , `+` and `j`. While there are some differences ( and `+` have a flag that moves the caret to +// the start of the line), there is just one function to handle all of these keys and move the caret down. +// This function checks if Shift is held down, in which case it will scroll the page forward, because behaves +// the same as . The side effect is that all shift+"down" shortcuts will now scroll forward, including . +// However, Vim does not support shifted ctrl shortcuts because terminals only support simple ascii control characters. +// So doesn't scroll forward. Shift+j becomes `J`, which joins multiple lines. And on a typical US/UK keyboard, +// `+` requires shift to type, so can only be typed with the numpad. Vim does not allow remapping . +// (IdeaVim does not get shift+numpadPlus, only "typed +". We might get it for a keypress, but by the time it's +// converted to a typed char, we lose the modifier). +// The same logic holds for and shift - , `k` and `-` should scroll backwards, but isn't valid, `K` is +// a different action, and shift+numpadMinus works but can't be remapped (or handled by IdeaVim). +// Ironically, Vim registers separate functions for and , so the non-shifted functions don't actually +// need to check for shift. So this is all side effect... +// See https://github.com/vim/vim/issues/15107 +// Note that IdeaVim handles separately because it behaves differently based on 'keymodel' +// TODO: Is there any way for IdeaVim to handle shift+numpadPlus? +@CommandOrMotion(keys = ["", "", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) class MotionScrollPageDownAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/NextTabAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/NextTabAction.kt index 1bbabc5b65..610c7dd762 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/NextTabAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/NextTabAction.kt @@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.VimActionHandler -@CommandOrMotion(keys = ["gt"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) +@CommandOrMotion(keys = ["gt", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) class NextTabAction : VimActionHandler.SingleExecution() { override fun execute( editor: VimEditor, @@ -30,3 +30,19 @@ class NextTabAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED } + +@CommandOrMotion(keys = [""], modes = [Mode.INSERT]) +class InsertNextTabAction : VimActionHandler.SingleExecution() { + override fun execute( + editor: VimEditor, + context: ExecutionContext, + cmd: Command, + operatorArguments: OperatorArguments, + ): Boolean { + // Vim doesn't change mode + injector.motion.moveCaretGotoNextTab(editor, context, cmd.rawCount) + return true + } + + override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/PreviousTabAction.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/PreviousTabAction.kt index eeb034b4a7..e44ab3ce2b 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/PreviousTabAction.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/window/tabs/PreviousTabAction.kt @@ -16,7 +16,7 @@ import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.handler.VimActionHandler -@CommandOrMotion(keys = ["gT"], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) +@CommandOrMotion(keys = ["gT", ""], modes = [Mode.NORMAL, Mode.VISUAL, Mode.OP_PENDING]) class PreviousTabAction : VimActionHandler.SingleExecution() { override fun execute( editor: VimEditor, @@ -30,3 +30,18 @@ class PreviousTabAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED } + +@CommandOrMotion(keys = [""], modes = [Mode.INSERT]) +class InsertPreviousTabAction : VimActionHandler.SingleExecution() { + override fun execute( + editor: VimEditor, + context: ExecutionContext, + cmd: Command, + operatorArguments: OperatorArguments, + ): Boolean { + injector.motion.moveCaretGotoPreviousTab(editor, context, cmd.rawCount) + return true + } + + override val type: Command.Type = Command.Type.OTHER_SELF_SYNCHRONIZED +} diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimStringParserBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimStringParserBase.kt index 7327cff082..bf0b4fc458 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimStringParserBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimStringParserBase.kt @@ -93,101 +93,94 @@ abstract class VimStringParserBase : VimStringParser { } try { name = String(Character.toChars(keyCode)) - } catch (ignored: IllegalArgumentException) { + } catch (_: IllegalArgumentException) { } } return if (name != null) "<$prefix$name>" else "<<$keyStroke>>" } - override fun parseKeys(string: String): List { - val result: MutableList = ArrayList() - var specialKeyStart = '<' + override fun parseKeys(string: String): List = buildList { + val specialKeyBuilder = StringBuilder() var state = KeyParserState.INIT - var specialKeyBuilder = StringBuilder() - for (element in string) { + + for (c in string) { when (state) { - KeyParserState.INIT -> when (element) { + KeyParserState.INIT -> when (c) { '\\' -> state = KeyParserState.ESCAPE - '<', '«' -> { - specialKeyStart = element + '<' -> { state = KeyParserState.SPECIAL - specialKeyBuilder = StringBuilder() + specialKeyBuilder.clear() } else -> { - val stroke: KeyStroke = if (element == '\t' || element == '\n') { - KeyStroke.getKeyStroke(element.code, 0) - } else if (isControlCharacter(element)) { - KeyStroke.getKeyStroke(element.code + 'A'.code - 1, InputEvent.CTRL_DOWN_MASK) + val stroke: KeyStroke = if (c == '\t' || c == '\n') { + KeyStroke.getKeyStroke(c.code, 0) + } else if (isControlCharacter(c)) { + KeyStroke.getKeyStroke(c.code + 'A'.code - 1, InputEvent.CTRL_DOWN_MASK) } else { - KeyStroke.getKeyStroke(element) + KeyStroke.getKeyStroke(c) } - result.add(stroke) + add(stroke) } } + KeyParserState.ESCAPE -> { state = KeyParserState.INIT - if (element != '\\') { - result.add(KeyStroke.getKeyStroke('\\')) + if (c != '\\') { + add(KeyStroke.getKeyStroke('\\')) } - result.add(KeyStroke.getKeyStroke(element)) + add(KeyStroke.getKeyStroke(c)) } - KeyParserState.SPECIAL -> if (element == '>' || element == '»') { - state = KeyParserState.INIT - val specialKeyName = specialKeyBuilder.toString() - val lower = specialKeyName.lowercase(Locale.getDefault()) - require("sid" != lower) { "<$specialKeyName> is not supported" } - if ("comma" == lower) { - result.add(KeyStroke.getKeyStroke(',')) - } else if ("nop" != lower) { - val leader = parseMapLeader(specialKeyName) - val specialKey = parseSpecialKey(specialKeyName, 0) - if (leader != null) { - result.addAll(leader) - } else if (specialKey != null && specialKeyName.length > 1) { - result.add(specialKey) - } else { - result.add(KeyStroke.getKeyStroke('<')) - result.addAll(stringToKeys(specialKeyName)) - result.add(KeyStroke.getKeyStroke('>')) + + KeyParserState.SPECIAL -> { + if (c == '>') { + state = KeyParserState.INIT + val specialKeyName = specialKeyBuilder.toString() + val lower = specialKeyName.lowercase(Locale.getDefault()) + require("sid" != lower) { "<$specialKeyName> is not supported" } + + if ("leader" == lower) { + addAll(getMapLeader()) + } else if ("nop" != lower) { + val specialKey = parseSpecialKey(specialKeyName, 0) + if (specialKey != null && specialKeyName.length > 1) { + add(specialKey) + } else { + add(KeyStroke.getKeyStroke('<')) + addAll(stringToKeys(specialKeyName)) + add(KeyStroke.getKeyStroke('>')) + } } - } - } else { - // e.g. move '<-2 - the first part does not belong to any special key - if (element == '<' || element == '«') { - result.add(KeyStroke.getKeyStroke(specialKeyStart)) - result.addAll(stringToKeys(specialKeyBuilder.toString())) - specialKeyBuilder = StringBuilder() } else { - specialKeyBuilder.append(element) + // e.g. move '<-2 - the first part does not belong to any special key + if (c == '<') { + add(KeyStroke.getKeyStroke('<')) + addAll(stringToKeys(specialKeyBuilder.toString())) + specialKeyBuilder.clear() + } else { + specialKeyBuilder.append(c) + } } } } } + if (state == KeyParserState.ESCAPE) { - result.add(KeyStroke.getKeyStroke('\\')) + add(KeyStroke.getKeyStroke('\\')) } else if (state == KeyParserState.SPECIAL) { - result.add(KeyStroke.getKeyStroke(specialKeyStart)) - result.addAll(stringToKeys(specialKeyBuilder.toString())) + add(KeyStroke.getKeyStroke('<')) + addAll(stringToKeys(specialKeyBuilder.toString())) } - return result } - private fun parseMapLeader(s: String): List? { - if ("leader".equals(s, ignoreCase = true)) { - val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader") - return if (mapLeader is VimString) { - stringToKeys(mapLeader.value) - } else { - stringToKeys("\\") - } + private fun getMapLeader(): List { + val mapLeader: Any? = injector.variableService.getGlobalVariableValue("mapleader") + return if (mapLeader is VimString) { + stringToKeys(mapLeader.value) + } else { + stringToKeys("\\") } - return null } -// override fun parseKeysSet(@NonNls vararg keys: String): Set> = List(keys.size) { -// injector.parser.parseKeys(keys[it]) -// }.toSet() - override fun stringToKeys(string: @NonNls String): List { val res: MutableList = ArrayList() for (element in string) { @@ -211,6 +204,7 @@ abstract class VimStringParserBase : VimStringParser { return c < '\u0020' } + @Suppress("SpellCheckingInspection") private fun getVimKeyValue(c: Int): @NonNls String? { return when (c) { KeyEvent.VK_ENTER -> "cr" @@ -498,6 +492,7 @@ abstract class VimStringParserBase : VimStringParser { return null } + @Suppress("SpellCheckingInspection") private fun getVimKeyName(lower: @NonNls String?): Int? { return when (lower) { "cr", "enter", "return" -> KeyEvent.VK_ENTER @@ -549,6 +544,7 @@ abstract class VimStringParserBase : VimStringParser { } } + @Suppress("SpellCheckingInspection") private fun getVimTypedKeyName(lower: String): Char? { return when (lower) { "space" -> ' ' diff --git a/vim-engine/src/main/resources/ksp-generated/engine_commands.json b/vim-engine/src/main/resources/ksp-generated/engine_commands.json index 6e7008f7e8..b157eeeb8b 100644 --- a/vim-engine/src/main/resources/ksp-generated/engine_commands.json +++ b/vim-engine/src/main/resources/ksp-generated/engine_commands.json @@ -359,6 +359,26 @@ "class": "com.maddyhome.idea.vim.action.window.LookupUpAction", "modes": "I" }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.window.tabs.InsertNextTabAction", + "modes": "I" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.window.tabs.NextTabAction", + "modes": "NXO" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.window.tabs.InsertPreviousTabAction", + "modes": "I" + }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.window.tabs.PreviousTabAction", + "modes": "NXO" + }, { "keys": "", "class": "com.maddyhome.idea.vim.action.change.insert.InsertCompletedLiteralAction", @@ -844,6 +864,11 @@ "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionShiftEndAction", "modes": "INXS" }, + { + "keys": "", + "class": "com.maddyhome.idea.vim.action.motion.scroll.MotionScrollPageDownAction", + "modes": "NXO" + }, { "keys": "", "class": "com.maddyhome.idea.vim.action.motion.leftright.MotionShiftHomeAction",