diff --git a/resources/META-INF/includes/VimActions.xml b/resources/META-INF/includes/VimActions.xml index ce6b6ea360..b193bcd501 100644 --- a/resources/META-INF/includes/VimActions.xml +++ b/resources/META-INF/includes/VimActions.xml @@ -150,6 +150,8 @@ + + diff --git a/resources/META-INF/plugin.xml b/resources/META-INF/plugin.xml index 480ce3e6f9..fa6fd3f76b 100644 --- a/resources/META-INF/plugin.xml +++ b/resources/META-INF/plugin.xml @@ -75,7 +75,8 @@ - + + diff --git a/src/com/maddyhome/idea/vim/action/internal/AddInlaysAction.kt b/src/com/maddyhome/idea/vim/action/internal/AddBlockInlaysAction.kt similarity index 97% rename from src/com/maddyhome/idea/vim/action/internal/AddInlaysAction.kt rename to src/com/maddyhome/idea/vim/action/internal/AddBlockInlaysAction.kt index a19fd26eb8..e87c3a1e2b 100644 --- a/src/com/maddyhome/idea/vim/action/internal/AddInlaysAction.kt +++ b/src/com/maddyhome/idea/vim/action/internal/AddBlockInlaysAction.kt @@ -41,7 +41,7 @@ import java.util.* import javax.swing.UIManager import kotlin.math.max -class AddInlaysAction : AnAction() { +class AddBlockInlaysAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { val dataContext = e.dataContext val editor = getEditor(dataContext) ?: return @@ -111,7 +111,7 @@ class AddInlaysAction : AnAction() { return if (text == null) 0 else fontMetrics.stringWidth(text) } - private inner class MyFontMetrics internal constructor(editor: Editor, familyName: String?, size: Int) { + private inner class MyFontMetrics(editor: Editor, familyName: String?, size: Int) { val metrics: FontMetrics fun isActual(editor: Editor, familyName: String, size: Int): Boolean { val font = metrics.font diff --git a/src/com/maddyhome/idea/vim/action/internal/AddInlineInlaysAction.kt b/src/com/maddyhome/idea/vim/action/internal/AddInlineInlaysAction.kt new file mode 100644 index 0000000000..ecf96e5a89 --- /dev/null +++ b/src/com/maddyhome/idea/vim/action/internal/AddInlineInlaysAction.kt @@ -0,0 +1,58 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.action.internal + +import com.intellij.codeInsight.daemon.impl.HintRenderer +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.VisualPosition +import com.maddyhome.idea.vim.helper.EditorHelper +import java.util.* +import kotlin.math.max + +class AddInlineInlaysAction : AnAction() { + companion object { + private val random = Random() + } + + override fun actionPerformed(e: AnActionEvent) { + val dataContext = e.dataContext + val editor = getEditor(dataContext) ?: return + val inlayModel = editor.inlayModel + val currentVisualLine = editor.caretModel.primaryCaret.visualPosition.line + var i = random.nextInt(10) + val lineLength = EditorHelper.getLineLength(editor, EditorHelper.visualLineToLogicalLine(editor, currentVisualLine)) + while (i < lineLength) { + val relatesToPrecedingText = random.nextInt(10) > 7 + val text = "a".repeat(max(1, random.nextInt(7))) + val offset = EditorHelper.visualPositionToOffset(editor, VisualPosition(currentVisualLine, i)) + // We don't need a custom renderer, just use the standard parameter hint renderer + inlayModel.addInlineElement(offset, relatesToPrecedingText, HintRenderer(if (relatesToPrecedingText) ":$text" else "$text:")) + // Every 20 chars +/- 5 chars + i += 20 + (random.nextInt(10) - 5) + } + } + + private fun getEditor(dataContext: DataContext): Editor? { + return CommonDataKeys.EDITOR.getData(dataContext) + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnLeftAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnLeftAction.kt index 0f01583900..532dd78885 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnLeftAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnLeftAction.kt @@ -32,6 +32,6 @@ class MotionScrollColumnLeftAction : VimActionHandler.SingleExecution() { override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SIDE_SCROLL_JUMP) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollColumn(editor, cmd.count) + return VimPlugin.getMotion().scrollColumns(editor, cmd.count) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnRightAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnRightAction.kt index 75c9320030..9545bc38d2 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnRightAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollColumnRightAction.kt @@ -31,6 +31,6 @@ class MotionScrollColumnRightAction : VimActionHandler.SingleExecution() { override val flags: EnumSet = EnumSet.of(CommandFlags.FLAG_IGNORE_SIDE_SCROLL_JUMP) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollColumn(editor, -cmd.count) + return VimPlugin.getMotion().scrollColumns(editor, -cmd.count) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenColumnAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenColumnAction.kt index 59e5ba5048..cfc7624582 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenColumnAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenColumnAction.kt @@ -27,6 +27,6 @@ class MotionScrollFirstScreenColumnAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollColumnToFirstScreenColumn(editor) + return VimPlugin.getMotion().scrollCaretColumnToFirstScreenColumn(editor) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineAction.kt index d13c22dd1e..0d2b66939c 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollFirstScreenLineAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, cmd.rawCount, false) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLinePageStartAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLinePageStartAction.kt index 6ec3f6c3a8..38bdaf755b 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLinePageStartAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLinePageStartAction.kt @@ -21,18 +21,24 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollFirstScreenLinePageStartAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - var line = cmd.rawCount - if (line == 0) { - val nextVisualLine = EditorHelper.getVisualLineAtBottomOfScreen(editor) + 1 - line = EditorHelper.visualLineToLogicalLine(editor, nextVisualLine) + 1 // rawCount is 1 based + var rawCount = cmd.rawCount + if (rawCount == 0) { + val nextVisualLine = EditorHelper.normalizeVisualLine(editor, + EditorHelper.getVisualLineAtBottomOfScreen(editor) + 1) + rawCount = EditorHelper.visualLineToLogicalLine(editor, nextVisualLine) + 1 // rawCount is 1 based } - return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, line, true) + return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, rawCount, true) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineStartAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineStartAction.kt index af1303391f..d5ba56de57 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineStartAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollFirstScreenLineStartAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollFirstScreenLineStartAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLineToFirstScreenLine(editor, cmd.rawCount, true) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageDownAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageDownAction.kt index 7e87228644..7e8b93a60c 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageDownAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfPageDownAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollHalfPageDownAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollScreen(editor, cmd.rawCount, true) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfWidthLeftAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfWidthLeftAction.kt new file mode 100644 index 0000000000..e6dffc4242 --- /dev/null +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfWidthLeftAction.kt @@ -0,0 +1,53 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.action.motion.scroll + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags +import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* + +/* +For the following four commands the cursor follows the screen. If the +character that the cursor is on is moved off the screen, the cursor is moved +to the closest character that is on the screen. The value of 'sidescroll' is +not used. + + *zH* +zH Move the view on the text half a screenwidth to the + left, thus scroll the text half a screenwidth to the + right. This only works when 'wrap' is off. + +[count] is used but undocumented. + */ +class MotionScrollHalfWidthLeftAction : VimActionHandler.SingleExecution() { + override val type: Command.Type = Command.Type.OTHER_READONLY + + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SIDE_SCROLL_JUMP) + + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { + // Vim's screen width is the full screen width, including columns used for gutters. + return VimPlugin.getMotion().scrollColumns(editor, cmd.count * (EditorHelper.getApproximateScreenWidth(editor) / 2)); + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfWidthRightAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfWidthRightAction.kt new file mode 100644 index 0000000000..902f14156b --- /dev/null +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollHalfWidthRightAction.kt @@ -0,0 +1,53 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.action.motion.scroll + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.editor.Editor +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags +import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* + +/* +For the following four commands the cursor follows the screen. If the +character that the cursor is on is moved off the screen, the cursor is moved +to the closest character that is on the screen. The value of 'sidescroll' is +not used. + + *zH* +zH Move the view on the text half a screenwidth to the + left, thus scroll the text half a screenwidth to the + right. This only works when 'wrap' is off. + +[count] is used but undocumented. + */ +class MotionScrollHalfWidthRightAction : VimActionHandler.SingleExecution() { + override val type: Command.Type = Command.Type.OTHER_READONLY + + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SIDE_SCROLL_JUMP) + + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { + // Vim's screen width is the full screen width, including columns used for gutters. + return VimPlugin.getMotion().scrollColumns(editor, -cmd.count * (EditorHelper.getApproximateScreenWidth(editor) / 2)); + } +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenColumnAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenColumnAction.kt index 8bc00d6953..47d9697314 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenColumnAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenColumnAction.kt @@ -27,6 +27,6 @@ class MotionScrollLastScreenColumnAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { - return VimPlugin.getMotion().scrollColumnToLastScreenColumn(editor) + return VimPlugin.getMotion().scrollCaretColumnToLastScreenColumn(editor) } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineAction.kt index 589f8cb4cf..2a46649c29 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollLastScreenLineAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, cmd.rawCount, false) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLinePageStartAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLinePageStartAction.kt index eea5392857..5892d0749e 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLinePageStartAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLinePageStartAction.kt @@ -21,28 +21,37 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollLastScreenLinePageStartAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { val motion = VimPlugin.getMotion() - var line = cmd.rawCount - if (line == 0) { - val prevVisualLine = EditorHelper.getVisualLineAtTopOfScreen(editor) - 1 - line = EditorHelper.visualLineToLogicalLine(editor, prevVisualLine) + 1 // rawCount is 1 based - return motion.scrollLineToLastScreenLine(editor, line, true) + + // Without [count]: Redraw with the line just above the window at the bottom of the window. Put the cursor in that + // line, at the first non-blank in the line. + if (cmd.rawCount == 0) { + val prevVisualLine = EditorHelper.normalizeVisualLine(editor, + EditorHelper.getVisualLineAtTopOfScreen(editor) - 1) + val logicalLine = EditorHelper.visualLineToLogicalLine(editor, prevVisualLine) + return motion.scrollLineToLastScreenLine(editor, logicalLine + 1, true) } + // [count]z^ first scrolls [count] to the bottom of the window, then moves the caret to the line that is now at // the top, and then move that line to the bottom of the window - line = EditorHelper.normalizeLine(editor, line) - if (motion.scrollLineToLastScreenLine(editor, line, true)) { - line = EditorHelper.getVisualLineAtTopOfScreen(editor) - line = EditorHelper.visualLineToLogicalLine(editor, line) + 1 // rawCount is 1 based - return motion.scrollLineToLastScreenLine(editor, line, true) + var logicalLine = EditorHelper.normalizeLine(editor, cmd.rawCount - 1) + if (motion.scrollLineToLastScreenLine(editor, logicalLine + 1, false)) { + logicalLine = EditorHelper.visualLineToLogicalLine(editor, EditorHelper.getVisualLineAtTopOfScreen(editor)) + return motion.scrollLineToLastScreenLine(editor, logicalLine + 1, true) } + return false } } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineStartAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineStartAction.kt index 833a22e548..f7e233c884 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineStartAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLastScreenLineStartAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollLastScreenLineStartAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLineToLastScreenLine(editor, cmd.rawCount, true) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineDownAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineDownAction.kt index 97ab6183a7..6403301408 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineDownAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineDownAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollLineDownAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLine(editor, cmd.count) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineUpAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineUpAction.kt index 95de6cdbc4..85c0561495 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineUpAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollLineUpAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollLineUpAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLine(editor, -cmd.count) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineAction.kt index e88f83fc8a..d8e58cc46a 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollMiddleScreenLineAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLineToMiddleScreenLine(editor, cmd.rawCount, false) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineStartAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineStartAction.kt index 9978b8ba76..7119192aaf 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineStartAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollMiddleScreenLineStartAction.kt @@ -21,11 +21,16 @@ import com.intellij.openapi.actionSystem.DataContext import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command +import com.maddyhome.idea.vim.command.CommandFlags import com.maddyhome.idea.vim.handler.VimActionHandler +import com.maddyhome.idea.vim.helper.enumSetOf +import java.util.* class MotionScrollMiddleScreenLineStartAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollLineToMiddleScreenLine(editor, cmd.rawCount, true) } diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt index d51f7debc7..c7daf16ee0 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageDownAction.kt @@ -35,6 +35,8 @@ class MotionScrollPageDownAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollFullPage(editor, cmd.count) } @@ -44,15 +46,13 @@ class MotionScrollPageDownInsertModeAction : VimActionHandler.SingleExecution(), override val keyStrokesSet: Set> = setOf( listOf(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0)), - listOf(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.CTRL_DOWN_MASK)), - listOf(KeyStroke.getKeyStroke(KeyEvent.VK_KP_DOWN, KeyEvent.CTRL_DOWN_MASK)), listOf(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, KeyEvent.SHIFT_DOWN_MASK)), listOf(KeyStroke.getKeyStroke(KeyEvent.VK_KP_DOWN, KeyEvent.SHIFT_DOWN_MASK)) ) override val type: Command.Type = Command.Type.OTHER_READONLY - override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_CLEAR_STROKES) + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP, CommandFlags.FLAG_CLEAR_STROKES) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollFullPage(editor, cmd.count) diff --git a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageUpAction.kt b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageUpAction.kt index 2a5dbc3942..0dcc267e46 100644 --- a/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageUpAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/scroll/MotionScrollPageUpAction.kt @@ -35,6 +35,8 @@ class MotionScrollPageUpAction : VimActionHandler.SingleExecution() { override val type: Command.Type = Command.Type.OTHER_READONLY + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP) + override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollFullPage(editor, -cmd.count) } @@ -44,15 +46,13 @@ class MotionScrollPageUpInsertModeAction : VimActionHandler.SingleExecution(), C override val keyStrokesSet: Set> = setOf( listOf(KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0)), - listOf(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.CTRL_DOWN_MASK)), - listOf(KeyStroke.getKeyStroke(KeyEvent.VK_KP_UP, KeyEvent.CTRL_DOWN_MASK)), listOf(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.SHIFT_DOWN_MASK)), listOf(KeyStroke.getKeyStroke(KeyEvent.VK_KP_UP, KeyEvent.SHIFT_DOWN_MASK)) ) override val type: Command.Type = Command.Type.OTHER_READONLY - override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_CLEAR_STROKES) + override val flags: EnumSet = enumSetOf(CommandFlags.FLAG_IGNORE_SCROLL_JUMP, CommandFlags.FLAG_CLEAR_STROKES) override fun execute(editor: Editor, context: DataContext, cmd: Command): Boolean { return VimPlugin.getMotion().scrollFullPage(editor, -cmd.count) diff --git a/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableBlockModeAction.kt b/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableBlockModeAction.kt index ed8774f722..a82f976f87 100644 --- a/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableBlockModeAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableBlockModeAction.kt @@ -23,6 +23,7 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.group.visual.vimSetSystemSelectionSilently import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.EditorHelper @@ -41,7 +42,7 @@ class SelectEnableBlockModeAction : VimActionHandler.SingleExecution() { val lineEnd = EditorHelper.getLineEndForOffset(editor, editor.caretModel.primaryCaret.offset) editor.caretModel.primaryCaret.run { vimSetSystemSelectionSilently(offset, (offset + 1).coerceAtMost(lineEnd)) - moveToOffset((offset + 1).coerceAtMost(lineEnd)) + moveToInlayAwareOffset((offset + 1).coerceAtMost(lineEnd)) vimLastColumn = visualPosition.column } return VimPlugin.getVisualMotion().enterSelectMode(editor, CommandState.SubMode.VISUAL_BLOCK) diff --git a/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableCharacterModeAction.kt b/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableCharacterModeAction.kt index 220ce3aa17..9826b61251 100644 --- a/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableCharacterModeAction.kt +++ b/src/com/maddyhome/idea/vim/action/motion/select/SelectEnableCharacterModeAction.kt @@ -23,6 +23,7 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.group.visual.vimSetSystemSelectionSilently import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.EditorHelper @@ -41,7 +42,7 @@ class SelectEnableCharacterModeAction : VimActionHandler.SingleExecution() { val lineEnd = EditorHelper.getLineEndForOffset(editor, caret.offset) caret.run { vimSetSystemSelectionSilently(offset, (offset + 1).coerceAtMost(lineEnd)) - moveToOffset((offset + 1).coerceAtMost(lineEnd)) + moveToInlayAwareOffset((offset + 1).coerceAtMost(lineEnd)) vimLastColumn = visualPosition.column } } diff --git a/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt b/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt index 81772d6932..0d92f72287 100644 --- a/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt +++ b/src/com/maddyhome/idea/vim/action/motion/select/SelectToggleVisualMode.kt @@ -23,6 +23,7 @@ import com.intellij.openapi.editor.Editor import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.group.visual.updateCaretState import com.maddyhome.idea.vim.handler.VimActionHandler import com.maddyhome.idea.vim.helper.commandState @@ -45,7 +46,7 @@ class SelectToggleVisualMode : VimActionHandler.SingleExecution() { if (subMode != CommandState.SubMode.VISUAL_LINE) { editor.caretModel.runForEachCaret { if (it.offset + VimPlugin.getVisualMotion().selectionAdj == it.selectionEnd) { - it.moveToOffset(it.offset + VimPlugin.getVisualMotion().selectionAdj) + it.moveToInlayAwareOffset(it.offset + VimPlugin.getVisualMotion().selectionAdj) } } } @@ -54,7 +55,7 @@ class SelectToggleVisualMode : VimActionHandler.SingleExecution() { if (subMode != CommandState.SubMode.VISUAL_LINE) { editor.caretModel.runForEachCaret { if (it.offset == it.selectionEnd && it.visualLineStart <= it.offset - VimPlugin.getVisualMotion().selectionAdj) { - it.moveToOffset(it.offset - VimPlugin.getVisualMotion().selectionAdj) + it.moveToInlayAwareOffset(it.offset - VimPlugin.getVisualMotion().selectionAdj) } } } diff --git a/src/com/maddyhome/idea/vim/command/CommandFlags.kt b/src/com/maddyhome/idea/vim/command/CommandFlags.kt index b157d4061c..9262947a0f 100644 --- a/src/com/maddyhome/idea/vim/command/CommandFlags.kt +++ b/src/com/maddyhome/idea/vim/command/CommandFlags.kt @@ -53,6 +53,15 @@ enum class CommandFlags { * This keystroke should be saved as part of the current insert */ FLAG_SAVE_STROKE, + + /** + * Don't include scrolljump when adjusting the scroll area to ensure the current cursor position is visible. + * Should be used for commands that adjust the scroll area (such as or ). + * Technically, the current implementation doesn't need these flags, as these commands adjust the scroll area + * according to their own rules and then move the cursor to fit (e.g. move cursor down a line with ). Moving the + * cursor always tries to adjust the scroll area to ensure it's visible, which in this case is always a no-op. + * This is an implementation detail, so keep the flags for both documentation and in case of refactoring. + */ FLAG_IGNORE_SCROLL_JUMP, FLAG_IGNORE_SIDE_SCROLL_JUMP, diff --git a/src/com/maddyhome/idea/vim/ex/handler/SortHandler.kt b/src/com/maddyhome/idea/vim/ex/handler/SortHandler.kt index 0a46061559..9e39c44a29 100644 --- a/src/com/maddyhome/idea/vim/ex/handler/SortHandler.kt +++ b/src/com/maddyhome/idea/vim/ex/handler/SortHandler.kt @@ -28,6 +28,7 @@ import com.maddyhome.idea.vim.ex.ExCommand import com.maddyhome.idea.vim.ex.ExException import com.maddyhome.idea.vim.ex.flags import com.maddyhome.idea.vim.ex.ranges.LineRange +import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.inBlockSubMode import java.util.* @@ -51,7 +52,7 @@ class SortHandler : CommandHandler.SingleExecution() { val primaryCaret = editor.caretModel.primaryCaret val range = getLineRange(editor, primaryCaret, cmd) val worked = VimPlugin.getChange().sortRange(editor, range, lineComparator) - primaryCaret.moveToOffset(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, range.startLine)) + primaryCaret.moveToInlayAwareOffset(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, range.startLine)) return worked } @@ -61,7 +62,7 @@ class SortHandler : CommandHandler.SingleExecution() { if (!VimPlugin.getChange().sortRange(editor, range, lineComparator)) { worked = false } - caret.moveToOffset(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, range.startLine)) + caret.moveToInlayAwareOffset(VimPlugin.getMotion().moveCaretToLineStartSkipLeading(editor, range.startLine)) } return worked diff --git a/src/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java b/src/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java index b48ce34fb9..298a54963b 100644 --- a/src/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java +++ b/src/com/maddyhome/idea/vim/extension/argtextobj/VimArgTextObjExtension.java @@ -11,6 +11,7 @@ import com.maddyhome.idea.vim.extension.VimExtension; import com.maddyhome.idea.vim.extension.VimExtensionHandler; import com.maddyhome.idea.vim.handler.TextObjectActionHandler; +import com.maddyhome.idea.vim.helper.InlayHelperKt; import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor; import com.maddyhome.idea.vim.listener.VimListenerSuppressor; import org.jetbrains.annotations.NotNull; @@ -234,7 +235,7 @@ public void execute(@NotNull Editor editor, @NotNull DataContext context) { if (commandState.getMode() == CommandState.Mode.VISUAL) { vimSetSelection(caret, range.getStartOffset(), range.getEndOffset() - 1, true); } else { - caret.moveToOffset(range.getStartOffset()); + InlayHelperKt.moveToInlayAwareOffset(caret, range.getStartOffset()); } } } diff --git a/src/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt b/src/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt index 21e2951eeb..e2ad96e6e8 100644 --- a/src/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt +++ b/src/com/maddyhome/idea/vim/extension/exchange/VimExchangeExtension.kt @@ -43,11 +43,9 @@ import com.maddyhome.idea.vim.extension.VimExtensionFacade.setOperatorFunction import com.maddyhome.idea.vim.extension.VimExtensionFacade.setRegister import com.maddyhome.idea.vim.extension.VimExtensionHandler import com.maddyhome.idea.vim.group.MarkGroup -import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.* import com.maddyhome.idea.vim.helper.StringHelper.parseKeys import com.maddyhome.idea.vim.helper.StringHelper.stringToKeys -import com.maddyhome.idea.vim.helper.fileSize -import com.maddyhome.idea.vim.helper.subMode import com.maddyhome.idea.vim.key.OperatorFunction /** @@ -199,14 +197,14 @@ class VimExchangeExtension: VimExtension { fun fixCursor(ex1: Exchange, ex2: Exchange, reverse: Boolean) { val primaryCaret = editor.caretModel.primaryCaret if(reverse) { - primaryCaret.moveToOffset(editor.getMarkOffset(ex1.start)) + primaryCaret.moveToInlayAwareOffset(editor.getMarkOffset(ex1.start)) } else { if (ex1.start.logicalLine == ex2.start.logicalLine) { val horizontalOffset = ex1.end.col - ex2.end.col - primaryCaret.moveToLogicalPosition(LogicalPosition(ex1.start.logicalLine, ex1.start.col - horizontalOffset)) + primaryCaret.moveToInlayAwareLogicalPosition(LogicalPosition(ex1.start.logicalLine, ex1.start.col - horizontalOffset)) } else if(ex1.end.logicalLine - ex1.start.logicalLine != ex2.end.logicalLine - ex2.start.logicalLine) { val verticalOffset = ex1.end.logicalLine - ex2.end.logicalLine - primaryCaret.moveToLogicalPosition(LogicalPosition(ex1.start.logicalLine - verticalOffset, ex1.start.col)) + primaryCaret.moveToInlayAwareLogicalPosition(LogicalPosition(ex1.start.logicalLine - verticalOffset, ex1.start.col)) } } } diff --git a/src/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt b/src/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt index 1d2bc43e81..e32f273e28 100644 --- a/src/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt +++ b/src/com/maddyhome/idea/vim/extension/surround/VimSurroundExtension.kt @@ -175,10 +175,8 @@ class VimSurroundExtension : VimExtension { val change = VimPlugin.getChange() val leftSurround = pair.first val primaryCaret = editor.caretModel.primaryCaret - primaryCaret.moveToOffset(range.startOffset) - change.insertText(editor, primaryCaret, leftSurround) - primaryCaret.moveToOffset(range.endOffset + leftSurround.length) - change.insertText(editor, primaryCaret, pair.second) + change.insertText(editor, primaryCaret, range.startOffset, leftSurround) + change.insertText(editor, primaryCaret, range.endOffset + leftSurround.length, pair.second) // Jump back to start executeNormalWithoutMapping(StringHelper.parseKeys("`["), editor) } diff --git a/src/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java b/src/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java index 23d997a444..db51c2e2df 100644 --- a/src/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java +++ b/src/com/maddyhome/idea/vim/extension/textobjentire/VimTextObjEntireExtension.java @@ -26,6 +26,7 @@ import com.maddyhome.idea.vim.extension.VimExtension; import com.maddyhome.idea.vim.extension.VimExtensionHandler; import com.maddyhome.idea.vim.handler.TextObjectActionHandler; +import com.maddyhome.idea.vim.helper.InlayHelperKt; import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor; import com.maddyhome.idea.vim.listener.VimListenerSuppressor; import org.jetbrains.annotations.NotNull; @@ -140,7 +141,7 @@ public void execute(@NotNull Editor editor, @NotNull DataContext context) { if (commandState.getMode() == CommandState.Mode.VISUAL) { vimSetSelection(caret, range.getStartOffset(), range.getEndOffset() - 1, true); } else { - caret.moveToOffset(range.getStartOffset()); + InlayHelperKt.moveToInlayAwareOffset(caret, range.getStartOffset()); } } } diff --git a/src/com/maddyhome/idea/vim/group/ChangeGroup.java b/src/com/maddyhome/idea/vim/group/ChangeGroup.java index 5b37a7a604..37c8f0dd9c 100644 --- a/src/com/maddyhome/idea/vim/group/ChangeGroup.java +++ b/src/com/maddyhome/idea/vim/group/ChangeGroup.java @@ -37,6 +37,7 @@ import com.intellij.openapi.editor.event.EditorMouseEvent; import com.intellij.openapi.editor.event.EditorMouseListener; import com.intellij.openapi.editor.ex.EditorEx; +import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.TextRangeInterval; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; @@ -627,8 +628,7 @@ private void repeatInsert(@NotNull Editor editor, @NotNull DataContext context, final String pad = EditorHelper.pad(editor, context, logicalLine + i, repeatColumn); if (pad.length() > 0) { final int offset = editor.getDocument().getLineEndOffset(logicalLine + i); - caret.moveToOffset(offset); - insertText(editor, caret, pad); + insertText(editor, caret, offset, pad); } } if (repeatColumn >= MotionGroup.LAST_COLUMN) { @@ -714,7 +714,7 @@ public boolean deleteCharacter(@NotNull Editor editor, @NotNull Caret caret, int final boolean res = deleteText(editor, new TextRange(caret.getOffset(), endOffset), SelectionType.CHARACTER_WISE); final int pos = caret.getOffset(); final int norm = EditorHelper.normalizeOffset(editor, caret.getLogicalPosition().line, pos, isChange); - if (norm != pos) { + if (norm != pos || editor.offsetToVisualPosition(norm) != EditorUtil.inlayAwareOffsetToVisualPosition(editor, norm)) { MotionGroup.moveCaret(editor, caret, norm); } @@ -1044,13 +1044,12 @@ public boolean changeCharacter(@NotNull Editor editor, @NotNull Caret caret, int // Indent new line if we replaced with a newline if (ch == '\n') { - caret.moveToOffset(offset + 1); - insertText(editor, caret, space); + insertText(editor, caret, offset + 1, space); int slen = space.length(); if (slen == 0) { slen++; } - caret.moveToOffset(offset + slen); + InlayHelperKt.moveToInlayAwareOffset(caret, offset + slen); } return true; @@ -1343,12 +1342,11 @@ else if (append) { if (column < MotionGroup.LAST_COLUMN && lineLength < column) { final String pad = EditorHelper.pad(editor, context, line, column); final int offset = editor.getDocument().getLineEndOffset(line); - caret.moveToOffset(offset); - insertText(editor, caret, pad); + insertText(editor, caret, offset, pad); } if (range.isMultiple() || !append) { - caret.moveToOffset(editor.logicalPositionToOffset(new LogicalPosition(line, column))); + InlayHelperKt.moveToInlayAwareLogicalPosition(caret, new LogicalPosition(line, column)); } if (range.isMultiple()) { setInsertRepeat(lines, column, append); @@ -1587,12 +1585,19 @@ public void indentLines(@NotNull Editor editor, * @param caret The caret to start insertion in * @param str The text to insert */ + public void insertText(@NotNull Editor editor, @NotNull Caret caret, int offset, @NotNull String str) { + editor.getDocument().insertString(offset, str); + InlayHelperKt.moveToInlayAwareOffset(caret, offset + str.length()); + + VimPlugin.getMark().setMark(editor, MarkGroup.MARK_CHANGE_POS, offset); + } + public void insertText(@NotNull Editor editor, @NotNull Caret caret, @NotNull String str) { - int start = caret.getOffset(); - editor.getDocument().insertString(start, str); - caret.moveToOffset(start + str.length()); + insertText(editor, caret, caret.getOffset(), str); + } - VimPlugin.getMark().setMark(editor, MarkGroup.MARK_CHANGE_POS, start); + public void insertText(@NotNull Editor editor, @NotNull Caret caret, @NotNull LogicalPosition start, @NotNull String str) { + insertText(editor, caret, editor.logicalPositionToOffset(start), str); } public void indentMotion(@NotNull Editor editor, @@ -1651,8 +1656,7 @@ public void indentRange(@NotNull Editor editor, int len = EditorHelper.getLineLength(editor, l); if (len > from) { LogicalPosition spos = new LogicalPosition(l, from); - caret.moveToOffset(editor.logicalPositionToOffset(spos)); - insertText(editor, caret, indent); + insertText(editor, caret, spos, indent); } } } @@ -1861,7 +1865,7 @@ public boolean changeNumberVisualMode(final @NotNull Editor editor, replaceText(editor, rangeToReplace.getFirst().getStartOffset(), rangeToReplace.getFirst().getEndOffset(), newNumber); } - caret.moveToOffset(selectedRange.getStartOffset()); + InlayHelperKt.moveToInlayAwareOffset(caret, selectedRange.getStartOffset()); return true; } @@ -1896,7 +1900,7 @@ public boolean changeNumber(final @NotNull Editor editor, @NotNull Caret caret, } else { replaceText(editor, range.getFirst().getStartOffset(), range.getFirst().getEndOffset(), newNumber); - caret.moveToOffset(range.getFirst().getStartOffset() + newNumber.length() - 1); + InlayHelperKt.moveToInlayAwareOffset(caret, range.getFirst().getStartOffset() + newNumber.length() - 1); return true; } } diff --git a/src/com/maddyhome/idea/vim/group/DigraphGroup.java b/src/com/maddyhome/idea/vim/group/DigraphGroup.java index 9a186ac775..2495a0fb99 100644 --- a/src/com/maddyhome/idea/vim/group/DigraphGroup.java +++ b/src/com/maddyhome/idea/vim/group/DigraphGroup.java @@ -81,7 +81,7 @@ public boolean parseCommandLine(@NotNull Editor editor, @NotNull String args) { } private void showDigraphs(@NotNull Editor editor) { - int width = EditorHelper.getScreenWidth(editor); + int width = EditorHelper.getApproximateScreenWidth(editor); if (width < 10) { width = 80; } diff --git a/src/com/maddyhome/idea/vim/group/MotionGroup.java b/src/com/maddyhome/idea/vim/group/MotionGroup.java index 24ca52e542..5d19f6022a 100755 --- a/src/com/maddyhome/idea/vim/group/MotionGroup.java +++ b/src/com/maddyhome/idea/vim/group/MotionGroup.java @@ -145,7 +145,6 @@ else if (cmd.getAction() instanceof TextObjectActionHandler) { // If we are a linewise motion we need to normalize the start and stop then move the start to the beginning // of the line and move the end to the end of the line. - EnumSet flags = cmd.getFlags(); if (cmd.isLinewiseMotion()) { if (caret.getLogicalPosition().line != getLineCount(editor) - 1) { start = getLineStartForOffset(editor, start); @@ -178,46 +177,49 @@ else if (cmd.getAction() instanceof TextObjectActionHandler) { private static void moveCaretToView(@NotNull Editor editor) { final int scrollOffset = getNormalizedScrollOffset(editor); - int topVisualLine = getVisualLineAtTopOfScreen(editor); - int bottomVisualLine = getVisualLineAtBottomOfScreen(editor); - int caretVisualLine = editor.getCaretModel().getVisualPosition().line; - int newline = caretVisualLine; + final int topVisualLine = getVisualLineAtTopOfScreen(editor); + final int bottomVisualLine = getVisualLineAtBottomOfScreen(editor); + final int caretVisualLine = editor.getCaretModel().getVisualPosition().line; + final int newVisualLine; if (caretVisualLine < topVisualLine + scrollOffset) { - newline = normalizeVisualLine(editor, topVisualLine + scrollOffset); + newVisualLine = normalizeVisualLine(editor, topVisualLine + scrollOffset); } else if (caretVisualLine >= bottomVisualLine - scrollOffset) { - newline = normalizeVisualLine(editor, bottomVisualLine - scrollOffset); + newVisualLine = normalizeVisualLine(editor, bottomVisualLine - scrollOffset); } - - int sideScrollOffset = OptionsManager.INSTANCE.getSidescrolloff().value(); - int width = getScreenWidth(editor); - if (sideScrollOffset > width / 2) { - sideScrollOffset = width / 2; + else { + newVisualLine = caretVisualLine; } - int col = editor.getCaretModel().getVisualPosition().column; - int oldColumn = col; + final int sideScrollOffset = getNormalizedSideScrollOffset(editor); + + final int oldColumn = editor.getCaretModel().getVisualPosition().column; + int col = oldColumn; if (col >= getLineLength(editor) - 1) { col = UserDataManager.getVimLastColumn(editor.getCaretModel().getPrimaryCaret()); } - int visualColumn = getVisualColumnAtLeftOfScreen(editor); + + final int leftVisualColumn = getVisualColumnAtLeftOfScreen(editor, newVisualLine); + final int rightVisualColumn = getVisualColumnAtRightOfScreen(editor, newVisualLine); int caretColumn = col; int newColumn = caretColumn; - if (caretColumn < visualColumn + sideScrollOffset) { - newColumn = visualColumn + sideScrollOffset; + + // TODO: Visual column arithmetic will be inaccurate as it include columns for inlays and folds + if (caretColumn < leftVisualColumn + sideScrollOffset) { + newColumn = leftVisualColumn + sideScrollOffset; } - else if (caretColumn >= visualColumn + width - sideScrollOffset) { - newColumn = visualColumn + width - sideScrollOffset - 1; + else if (caretColumn > rightVisualColumn - sideScrollOffset) { + newColumn = rightVisualColumn - sideScrollOffset; } - if (newline == caretVisualLine && newColumn != caretColumn) { + if (newVisualLine == caretVisualLine && newColumn != caretColumn) { col = newColumn; } - newColumn = normalizeVisualColumn(editor, newline, newColumn, CommandStateHelper.isEndAllowed(CommandStateHelper.getMode(editor))); + newColumn = normalizeVisualColumn(editor, newVisualLine, newColumn, CommandStateHelper.isEndAllowed(CommandStateHelper.getMode(editor))); - if (newline != caretVisualLine || newColumn != oldColumn) { - int offset = visualPositionToOffset(editor, new VisualPosition(newline, newColumn)); + if (newVisualLine != caretVisualLine || newColumn != oldColumn) { + int offset = visualPositionToOffset(editor, new VisualPosition(newVisualLine, newColumn)); moveCaret(editor, editor.getCaretModel().getPrimaryCaret(), offset); UserDataManager.setVimLastColumn(editor.getCaretModel().getPrimaryCaret(), col); @@ -277,10 +279,15 @@ private static int getScrollOption(int rawCount) { } private static int getNormalizedScrollOffset(final @NotNull Editor editor) { - int scrollOffset = OptionsManager.INSTANCE.getScrolloff().value(); + final int scrollOffset = OptionsManager.INSTANCE.getScrolloff().value(); return normalizeScrollOffset(editor, scrollOffset); } + private static int getNormalizedSideScrollOffset(final @NotNull Editor editor) { + final int sideScrollOffset = OptionsManager.INSTANCE.getSidescrolloff().value(); + return normalizeSideScrollOffset(editor, sideScrollOffset); + } + public static void moveCaret(@NotNull Editor editor, @NotNull Caret caret, int offset) { if (offset < 0 || offset > editor.getDocument().getTextLength() || !caret.isValid()) return; @@ -292,8 +299,11 @@ public static void moveCaret(@NotNull Editor editor, @NotNull Caret caret, int o return; } - if (caret.getOffset() != offset) { - caret.moveToOffset(offset); + // Always move the caret. It will be smart enough to not do anything if the offsets are the same, but it will also + // ensure that it's in the correct location relative to any inline inlays + final int oldOffset = caret.getOffset(); + InlayHelperKt.moveToInlayAwareOffset(caret, offset); + if (oldOffset != offset) { UserDataManager.setVimLastColumn(caret, caret.getVisualPosition().column); if (caret == editor.getCaretModel().getPrimaryCaret()) { scrollCaretIntoView(editor); @@ -579,148 +589,208 @@ public int moveCaretToBeforeNextCharacterOnLine(@NotNull Editor editor, @NotNull public boolean scrollLineToFirstScreenLine(@NotNull Editor editor, int rawCount, boolean start) { scrollLineToScreenLocation(editor, ScreenLocation.TOP, rawCount, start); - return true; } public boolean scrollLineToMiddleScreenLine(@NotNull Editor editor, int rawCount, boolean start) { scrollLineToScreenLocation(editor, ScreenLocation.MIDDLE, rawCount, start); - return true; } public boolean scrollLineToLastScreenLine(@NotNull Editor editor, int rawCount, boolean start) { scrollLineToScreenLocation(editor, ScreenLocation.BOTTOM, rawCount, start); - return true; } - public boolean scrollColumnToFirstScreenColumn(@NotNull Editor editor) { - scrollColumnToScreenColumn(editor, 0); - + public boolean scrollCaretColumnToFirstScreenColumn(@NotNull Editor editor) { + final VisualPosition caretVisualPosition = editor.getCaretModel().getVisualPosition(); + final int scrollOffset = getNormalizedSideScrollOffset(editor); + // TODO: Should the offset be applied to visual columns? This includes inline inlays and folds + final int column = Math.max(0, caretVisualPosition.column - scrollOffset); + scrollColumnToLeftOfScreen(editor, caretVisualPosition.line, column); return true; } - public boolean scrollColumnToLastScreenColumn(@NotNull Editor editor) { - scrollColumnToScreenColumn(editor, getScreenWidth(editor)); - + public boolean scrollCaretColumnToLastScreenColumn(@NotNull Editor editor) { + final VisualPosition caretVisualPosition = editor.getCaretModel().getVisualPosition(); + final int scrollOffset = getNormalizedSideScrollOffset(editor); + // TODO: Should the offset be applied to visual columns? This includes inline inlays and folds + final int column = normalizeVisualColumn(editor, caretVisualPosition.line, caretVisualPosition.column + scrollOffset, false); + scrollColumnToRightOfScreen(editor, caretVisualPosition.line, column); return true; } public static void scrollCaretIntoView(@NotNull Editor editor) { - final EnumSet flags = CommandState.getInstance(editor).getExecutingCommandFlags(); - final boolean scrollJump = flags.contains(CommandFlags.FLAG_IGNORE_SCROLL_JUMP); - scrollPositionIntoView(editor, editor.getCaretModel().getVisualPosition(), scrollJump); + final VisualPosition position = editor.getCaretModel().getVisualPosition(); + scrollCaretIntoViewVertically(editor, position.line); + scrollCaretIntoViewHorizontally(editor, position); } - public static void scrollPositionIntoView(@NotNull Editor editor, - @NotNull VisualPosition position, - boolean scrollJump) { - final int topVisualLine = getVisualLineAtTopOfScreen(editor); - final int bottomVisualLine = getVisualLineAtBottomOfScreen(editor); - final int visualLine = position.line; - final int column = position.column; - - // We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred - int scrollOffset = OptionsManager.INSTANCE.getScrolloff().value(); + // Vim's version of this method is move.c:update_topline, which will first scroll to fit the current line number at + // the top of the window and then ensure that the current line fits at the bottom of the window + private static void scrollCaretIntoViewVertically(@NotNull Editor editor, final int caretLine) { - int scrollJumpSize = 0; - if (scrollJump) { - scrollJumpSize = Math.max(0, OptionsManager.INSTANCE.getScrolljump().value() - 1); - } - - int visualTop = topVisualLine + scrollOffset; - int visualBottom = bottomVisualLine - scrollOffset + 1; - if (visualTop == visualBottom) { - visualBottom++; - } - - int diff; - if (visualLine < visualTop) { - diff = visualLine - visualTop; - scrollJumpSize = -scrollJumpSize; - } - else { - diff = Math.max(0, visualLine - visualBottom + 1); - } + // TODO: Make this work with soft wraps + // Vim's algorithm works counts line heights for wrapped lines. We're using visual lines, which handles collapsed + // folds, but treats soft wrapped lines as individual lines. + // Ironically, after figuring out how Vim's algorithm works (although not *why*), it looks likely to be rewritten as + // a dumb line for line reimplementation. - if (diff != 0) { + final int topLine = getVisualLineAtTopOfScreen(editor); + final int bottomLine = getVisualLineAtBottomOfScreen(editor); - // If we need to scroll the current line more than half a screen worth of lines then we just centre the new - // current line. This mimics vim behavior of e.g. 100G in a 300 line file with a screen size of 25 centering line - // 100. It also handles so=999 keeping the current line centred. - // It doesn't handle keeping the line centred when scroll offset is less than a full page height, as the new line - // might be within e.g. top + scroll offset, so we test for that separately. - // Note that block inlays means that the pixel height we are scrolling can be larger than half the screen, even if - // the number of lines is less. I'm not sure what impact this has. - int height = bottomVisualLine - topVisualLine + 1; - if (Math.abs(diff) > height / 2 || scrollOffset > height / 2) { - scrollVisualLineToMiddleOfScreen(editor, visualLine); + // We need the non-normalised value here, so we can handle cases such as so=999 to keep the current line centred + final int scrollOffset = OptionsManager.INSTANCE.getScrolloff().value(); + final int topBound = topLine + scrollOffset; + final int bottomBound = Math.max(topBound + 1, bottomLine - scrollOffset); + + // If we need to scroll the current line more than half a screen worth of lines then we just centre the new + // current line. This mimics vim behavior of e.g. 100G in a 300 line file with a screen size of 25 centering line + // 100. It also handles so=999 keeping the current line centred. + // Note that block inlays means that the pixel height we are scrolling can be larger than half the screen, even if + // the number of lines is less. I'm not sure what impact this has. + final int height = bottomLine - topLine + 1; + final int halfHeight = Math.max(2, (height / 2) - 1); + + // Scrolljump isn't handled as you might expect. It is the minimal number of lines to scroll, but that doesn't mean + // newLine = caretLine +/- MAX(sj, so) + // + // When scrolling up (`k` - scrolling window up in the buffer; more lines are visible at the top of the window), Vim + // will start at the new cursor line and repeatedly advance lines above and below. The new top line must be at least + // scrolloff above caretLine. If this takes the new top line above the current top line, we must scroll at least + // scrolljump. If the new caret line was already above the current top line, this counts as one scroll, and we + // scroll from the caret line. Otherwise, we scroll from the current top line. + // (See move.c:scroll_cursor_top) + // + // When scrolling down (`j` - scrolling window down in the buffer; more lines are visible at the bottom), Vim again + // expands lines above and below the new bottom line, but calcualtes things a little differently. The total number + // of lines expanded is at least scrolljump and there must be at least scrolloff lines below. + // Since the lines are advancing simultaneously, it is only possible to get scrolljump/2 above the new cursor line. + // If there are fewer than scrolljump/2 lines between the current bottom line and the new cursor line, the extra + // lines are pushed below the new cursor line. Due to the algorithm advancing the "above" line before the "below" + // line, we can end up with more than just scrolljump/2 lines on the top (hence the sj+1). + // Therefore, the new top line is (cln + max(so, sj - min(cln-bl, ceiling((sj + 1)/2)))) + // (where cln is caretLine, bl is bottomLine, so is scrolloff and sj is scrolljump) + // (See move.c:scroll_cursor_bot) + // + // On top of that, if the scroll distance is "too large", the new cursor line is positioned in the centre of the + // screen. What "too large" means depends on scroll direction. There is an initial approximate check before working + // out correct scroll locations + final int scrollJump = getScrollJump(editor, height); + + if (caretLine < topBound) { + // Scrolling up, put the cursor at the top of the window (minus scrolloff) + // Initial approximation in move.c:update_topline + if (topLine + scrollOffset - caretLine >= halfHeight) { + scrollVisualLineToMiddleOfScreen(editor, caretLine); } else { - // Put the new cursor line "scrolljump" lines from the top/bottom. Ensure that the line is fully visible, - // including block inlays above/below the line - if (diff > 0) { - int resLine = bottomVisualLine + diff + scrollJumpSize; - scrollVisualLineToBottomOfScreen(editor, resLine); + // New top line must be at least scrolloff above caretLine. If this is above current top line, we must scroll + // at least scrolljump. If caretLine was already above topLine, this counts as one scroll, and we scroll from + // here. Otherwise, we scroll from topLine + final int scrollJumpTopLine = Math.max(0, (caretLine < topLine) ? caretLine - scrollJump + 1 : topLine - scrollJump); + final int scrollOffsetTopLine = Math.max(0, caretLine - scrollOffset); + final int newTopLine = Math.min(scrollOffsetTopLine, scrollJumpTopLine); + + // Used is set to the line height of caretLine, and then incremented by line height of the lines above and + // below caretLine (up to scrolloff or end of file) + final int used = 1 + (newTopLine - topLine) + Math.min(scrollOffset, getVisualLineCount(editor) - topLine); + if (used > height) { + scrollVisualLineToMiddleOfScreen(editor, caretLine); } else { - int resLine = topVisualLine + diff + scrollJumpSize; - resLine = Math.min(resLine, getVisualLineCount(editor) - height); - resLine = Math.max(0, resLine); - scrollVisualLineToTopOfScreen(editor, resLine); + scrollVisualLineToTopOfScreen(editor, newTopLine); } } } + else if (caretLine > bottomBound) { + // Scrolling down, put the cursor at the bottom of the window (minus scrolloff) + // Vim does a quick approximation before going through the full algorithm. It checks the line below the bottom + // line in the window (bottomLine + 1). See move.c:update_topline + int lineCount = caretLine - (bottomLine + 1) + 1 + scrollOffset; + if (lineCount > height) { + scrollVisualLineToMiddleOfScreen(editor, caretLine); + } else { + // Vim expands out from caretLine at least scrolljump lines. It stops expanding above when it hits the + // current bottom line, or (because it's expanding above and below) when it's scrolled scrolljump/2. It expands + // above first, and the initial scroll count is 1, so we used (scrolljump+1)/2 + final int scrolledAbove = caretLine - bottomLine; + final int extra = Math.max(scrollOffset, scrollJump - Math.min(scrolledAbove, Math.round((scrollJump + 1) / 2.0f))); + final int scrolled = scrolledAbove + extra; + + // "used" is the count of lines expanded above and below. We expand below until we hit EOF (or when we've + // expanded over a screen full) or until we've scrolled enough and we've expanded at least linesAbove + // We expand above until usedAbove + usedBelow >= height. Or until we've scrolled enough (scrolled > sj and extra > so) + // and we've expanded at least linesAbove (and at most, linesAbove - scrolled - scrolledAbove - 1) + // The minus one is for the current line + //noinspection UnnecessaryLocalVariable + final int usedAbove = scrolledAbove; + final int usedBelow = Math.min(getVisualLineCount(editor) - caretLine, usedAbove - 1); + final int used = Math.min(height + 1, usedAbove + usedBelow); + + // If we've expanded more than a screen full, redraw with the cursor in the middle of the screen. If we're going + // scroll more than a screen full or more than scrolloff, redraw with the cursor in the middle of the screen. + lineCount = used > height ? used : scrolled; + if (lineCount >= height && lineCount > scrollOffset) { + scrollVisualLineToMiddleOfScreen(editor, caretLine); + } + else { + scrollVisualLineToBottomOfScreen(editor, caretLine + extra); + } + } + } + } - int visualColumn = getVisualColumnAtLeftOfScreen(editor); - int width = getScreenWidth(editor); + private static int getScrollJump(@NotNull Editor editor, int height) { final EnumSet flags = CommandState.getInstance(editor).getExecutingCommandFlags(); - scrollJump = !flags.contains(CommandFlags.FLAG_IGNORE_SIDE_SCROLL_JUMP); - scrollOffset = OptionsManager.INSTANCE.getScrolloff().value(); - scrollJumpSize = 0; + final boolean scrollJump = !flags.contains(CommandFlags.FLAG_IGNORE_SCROLL_JUMP); + + // Default value is 1. Zero is a valid value, but we normalise to 1 - we always want to scroll at least one line + // If the value is negative, it's a percentage of the height. if (scrollJump) { - scrollJumpSize = Math.max(0, OptionsManager.INSTANCE.getSidescroll().value() - 1); - if (scrollJumpSize == 0) { - scrollJumpSize = width / 2; + final int scrollJumpSize = OptionsManager.INSTANCE.getScrolljump().value(); + if (scrollJumpSize < 0) { + return (int) (height * (Math.min(100, -scrollJumpSize) / 100.0)); } - } - - int visualLeft = visualColumn + scrollOffset; - int visualRight = visualColumn + width - scrollOffset; - if (scrollOffset >= width / 2) { - scrollOffset = width / 2; - visualLeft = visualColumn + scrollOffset; - visualRight = visualColumn + width - scrollOffset; - if (visualLeft == visualRight) { - visualRight++; + else { + return Math.max(1, scrollJumpSize); } } + return 1; + } - scrollJumpSize = Math.min(scrollJumpSize, width / 2 - scrollOffset); + private static void scrollCaretIntoViewHorizontally(@NotNull Editor editor, + @NotNull VisualPosition position) { + final int currentVisualLeftColumn = getVisualColumnAtLeftOfScreen(editor, position.line); + final int currentVisualRightColumn = getVisualColumnAtRightOfScreen(editor, position.line); + final int caretColumn = position.column; - if (column < visualLeft) { - diff = column - visualLeft + 1; - scrollJumpSize = -scrollJumpSize; - } - else { - diff = column - visualRight + 1; - if (diff < 0) { - diff = 0; - } - } + final int halfWidth = getApproximateScreenWidth(editor) / 2; + final int scrollOffset = getNormalizedSideScrollOffset(editor); + + final EnumSet flags = CommandState.getInstance(editor).getExecutingCommandFlags(); + final boolean allowSidescroll = !flags.contains(CommandFlags.FLAG_IGNORE_SIDE_SCROLL_JUMP); + int sidescroll = OptionsManager.INSTANCE.getSidescroll().value(); + + final int offsetLeft = caretColumn - currentVisualLeftColumn - scrollOffset; + final int offsetRight = caretColumn - (currentVisualRightColumn - scrollOffset); + if (offsetLeft < 0 || offsetRight > 0) { + int diff = offsetLeft < 0 ? -offsetLeft : offsetRight; - if (diff != 0) { - int col; - if (Math.abs(diff) > width / 2) { - col = column - width / 2 - 1; + if ((allowSidescroll && sidescroll == 0) || diff >= halfWidth || offsetRight >= offsetLeft) { + scrollColumnToMiddleOfScreen(editor, position.line, caretColumn); } else { - col = visualColumn + diff + scrollJumpSize; + if (allowSidescroll && diff < sidescroll) { + diff = sidescroll; + } + if (offsetLeft < 0) { + scrollColumnToLeftOfScreen(editor, position.line, Math.max(0, currentVisualLeftColumn - diff)); + } else { + scrollColumnToRightOfScreen(editor, position.line, + normalizeVisualColumn(editor, position.line, currentVisualRightColumn + diff, false)); + } } - - col = Math.max(0, col); - scrollColumnToLeftOfScreen(editor, col); } } @@ -740,14 +810,12 @@ public boolean scrollLine(@NotNull Editor editor, int lines) { assert lines != 0 : "lines cannot be 0"; if (lines > 0) { - int visualLine = getVisualLineAtTopOfScreen(editor); - visualLine = normalizeVisualLine(editor, visualLine + lines); - scrollVisualLineToTopOfScreen(editor, visualLine); + final int visualLine = getVisualLineAtTopOfScreen(editor); + scrollVisualLineToTopOfScreen(editor, visualLine + lines); } else { - int visualLine = getVisualLineAtBottomOfScreen(editor); - visualLine = normalizeVisualLine(editor, visualLine + lines); - scrollVisualLineToBottomOfScreen(editor, visualLine); + final int visualLine = getVisualLineAtBottomOfScreen(editor); + scrollVisualLineToBottomOfScreen(editor, visualLine + lines); } moveCaretToView(editor); @@ -852,34 +920,8 @@ public int moveCaretToJump(@NotNull Editor editor, int count) { } } - private void scrollColumnToScreenColumn(@NotNull Editor editor, int column) { - int scrollOffset = OptionsManager.INSTANCE.getSidescrolloff().value(); - int width = getScreenWidth(editor); - if (scrollOffset > width / 2) { - scrollOffset = width / 2; - } - if (column <= width / 2) { - if (column < scrollOffset + 1) { - column = scrollOffset + 1; - } - } - else { - if (column > width - scrollOffset) { - column = width - scrollOffset; - } - } - - int visualColumn = editor.getCaretModel().getVisualPosition().column; - scrollColumnToLeftOfScreen(editor, normalizeVisualColumn(editor, editor.getCaretModel().getVisualPosition().line, visualColumn - column + 1, - false)); - } - - private static void scrollColumnToLeftOfScreen(@NotNull Editor editor, int column) { - scrollHorizontally(editor, column * getColumnWidth(editor)); - } - public int moveCaretToMiddleColumn(@NotNull Editor editor, @NotNull Caret caret) { - final int width = getScreenWidth(editor) / 2; + final int width = getApproximateScreenWidth(editor) / 2; final int len = getLineLength(editor); return moveCaretToColumn(editor, caret, Math.max(0, Math.min(len - 1, width)), false); @@ -913,14 +955,30 @@ public int moveCaretToLineEnd(@NotNull Editor editor, @NotNull Caret caret) { return moveCaretToLineEnd(editor, editor.visualToLogicalPosition(visualEndOfLine).line, true); } - public boolean scrollColumn(@NotNull Editor editor, int columns) { - int visualColumn = getVisualColumnAtLeftOfScreen(editor); - visualColumn = normalizeVisualColumn(editor, editor.getCaretModel().getVisualPosition().line, visualColumn + columns, false); - - scrollColumnToLeftOfScreen(editor, visualColumn); + public boolean scrollColumns(@NotNull Editor editor, int columns) { + final VisualPosition caretVisualPosition = editor.getCaretModel().getVisualPosition(); + if (columns > 0) { + // TODO: Don't add columns to visual position. This includes inlays and folds + int visualColumn = normalizeVisualColumn(editor, caretVisualPosition.line, + getVisualColumnAtLeftOfScreen(editor, caretVisualPosition.line) + columns, false); + + // If the target column has an inlay preceding it, move passed it. This inlay will have been (incorrectly) + // included in the simple visual position, so it's ok to step over. If we don't do this, scrollColumnToLeftOfScreen + // can get stuck trying to make sure the inlay is visible. + // A better solution is to not use VisualPosition everywhere, especially for arithmetic + final Inlay inlay = editor.getInlayModel().getInlineElementAt(new VisualPosition(caretVisualPosition.line, visualColumn - 1)); + if (inlay != null && !inlay.isRelatedToPrecedingText()) { + visualColumn++; + } + scrollColumnToLeftOfScreen(editor, caretVisualPosition.line, visualColumn); + } + else { + // Don't normalise the rightmost column, or we break virtual space + final int visualColumn = getVisualColumnAtRightOfScreen(editor, caretVisualPosition.line) + columns; + scrollColumnToRightOfScreen(editor, caretVisualPosition.line, visualColumn); + } moveCaretToView(editor); - return true; } @@ -937,18 +995,18 @@ public int moveCaretToLineStart(@NotNull Editor editor, int line) { } public int moveCaretToLineScreenStart(@NotNull Editor editor, @NotNull Caret caret) { - final int col = getVisualColumnAtLeftOfScreen(editor); + final int col = getVisualColumnAtLeftOfScreen(editor, caret.getVisualPosition().line); return moveCaretToColumn(editor, caret, col, false); } public int moveCaretToLineScreenStartSkipLeading(@NotNull Editor editor, @NotNull Caret caret) { - final int col = getVisualColumnAtLeftOfScreen(editor); + final int col = getVisualColumnAtLeftOfScreen(editor, caret.getVisualPosition().line); final int logicalLine = caret.getLogicalPosition().line; return getLeadingCharacterOffset(editor, logicalLine, col); } public int moveCaretToLineScreenEnd(@NotNull Editor editor, @NotNull Caret caret, boolean allowEnd) { - final int col = getVisualColumnAtLeftOfScreen(editor) + getScreenWidth(editor) - 1; + final int col = getVisualColumnAtRightOfScreen(editor, caret.getVisualPosition().line); return moveCaretToColumn(editor, caret, col, allowEnd); } @@ -1119,17 +1177,16 @@ public int moveCaretGotoLineFirst(@NotNull Editor editor, int line) { } // Scrolls current or [count] line to given screen location - // In Vim, [count] refers to a file line, so it's a logical line + // In Vim, [count] refers to a file line, so it's a one-based logical line private void scrollLineToScreenLocation(@NotNull Editor editor, @NotNull ScreenLocation screenLocation, - int line, + int rawCount, boolean start) { final int scrollOffset = getNormalizedScrollOffset(editor); - line = normalizeLine(editor, line); - int visualLine = line == 0 - ? editor.getCaretModel().getVisualPosition().line - : logicalLineToVisualLine(editor, line - 1); + int visualLine = rawCount == 0 + ? editor.getCaretModel().getVisualPosition().line + : logicalLineToVisualLine(editor, normalizeLine(editor, rawCount - 1)); // This method moves the current (or [count]) line to the specified screen location // Scroll offset is applicable, but scroll jump isn't. Offset is applied to screen lines (visual lines) @@ -1144,6 +1201,7 @@ private void scrollLineToScreenLocation(@NotNull Editor editor, scrollVisualLineToBottomOfScreen(editor, visualLine + scrollOffset); break; } + if (visualLine != editor.getCaretModel().getVisualPosition().line || start) { int offset; if (start) { @@ -1316,5 +1374,4 @@ public int moveCaretToLineEndOffset(@NotNull Editor editor, return moveCaretToLineEnd(editor, visualLineToLogicalLine(editor, line), allowPastEnd); } } - } diff --git a/src/com/maddyhome/idea/vim/group/copy/PutGroup.kt b/src/com/maddyhome/idea/vim/group/copy/PutGroup.kt index 46a4e658f1..1a343cf88d 100644 --- a/src/com/maddyhome/idea/vim/group/copy/PutGroup.kt +++ b/src/com/maddyhome/idea/vim/group/copy/PutGroup.kt @@ -46,6 +46,7 @@ import com.maddyhome.idea.vim.common.TextRange import com.maddyhome.idea.vim.group.MarkGroup import com.maddyhome.idea.vim.group.MotionGroup import com.maddyhome.idea.vim.group.visual.VimSelection +import com.maddyhome.idea.vim.helper.moveToInlayAwareOffset import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.fileSize import com.maddyhome.idea.vim.option.ClipboardOptionsData @@ -127,7 +128,7 @@ class PutGroup { ApplicationManager.getApplication().runWriteAction { VimPlugin.getChange().deleteRange(editor, caret, range, selection.type, false) } - caret.moveToOffset(range.startOffset) + caret.moveToInlayAwareOffset(range.startOffset) } } @@ -259,7 +260,7 @@ class PutGroup { EditorHelper.getOrderedCaretsList(editor).forEach { caret -> val startOffset = prepareDocumentAndGetStartOffsets(editor, caret, text.typeInRegister, data, additionalData).first() val pointMarker = editor.document.createRangeMarker(startOffset, startOffset) - caret.moveToOffset(startOffset) + caret.moveToInlayAwareOffset(startOffset) carets[caret] = pointMarker } diff --git a/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt b/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt index dc935071ba..a3d7d857fe 100644 --- a/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt +++ b/src/com/maddyhome/idea/vim/group/visual/VisualGroup.kt @@ -28,17 +28,7 @@ import com.maddyhome.idea.vim.VimPlugin import com.maddyhome.idea.vim.command.CommandState import com.maddyhome.idea.vim.group.ChangeGroup import com.maddyhome.idea.vim.group.MotionGroup -import com.maddyhome.idea.vim.helper.EditorHelper -import com.maddyhome.idea.vim.helper.fileSize -import com.maddyhome.idea.vim.helper.inBlockSubMode -import com.maddyhome.idea.vim.helper.inSelectMode -import com.maddyhome.idea.vim.helper.inVisualMode -import com.maddyhome.idea.vim.helper.isEndAllowed -import com.maddyhome.idea.vim.helper.mode -import com.maddyhome.idea.vim.helper.sort -import com.maddyhome.idea.vim.helper.subMode -import com.maddyhome.idea.vim.helper.vimLastColumn -import com.maddyhome.idea.vim.helper.vimSelectionStart +import com.maddyhome.idea.vim.helper.* /** * @author Alex Plate @@ -52,7 +42,7 @@ import com.maddyhome.idea.vim.helper.vimSelectionStart fun Caret.vimSetSelection(start: Int, end: Int = start, moveCaretToSelectionEnd: Boolean = false) { vimSelectionStart = start setVisualSelection(start, end, this) - if (moveCaretToSelectionEnd && !editor.inBlockSubMode) moveToOffset(end) + if (moveCaretToSelectionEnd && !editor.inBlockSubMode) moveToInlayAwareOffset(end) } /** @@ -213,7 +203,7 @@ fun moveCaretOneCharLeftFromSelectionEnd(editor: Editor, predictedMode: CommandS editor.caretModel.allCarets.forEach { caret -> val lineEnd = EditorHelper.getLineEndForOffset(editor, caret.offset) val lineStart = EditorHelper.getLineStartForOffset(editor, caret.offset) - if (caret.offset == lineEnd && lineEnd != lineStart) caret.moveToOffset(caret.offset - 1) + if (caret.offset == lineEnd && lineEnd != lineStart) caret.moveToInlayAwareOffset(caret.offset - 1) } } return @@ -223,9 +213,9 @@ fun moveCaretOneCharLeftFromSelectionEnd(editor: Editor, predictedMode: CommandS if (caret.selectionEnd <= 0) return@forEach if (EditorHelper.getLineStartForOffset(editor, caret.selectionEnd - 1) != caret.selectionEnd - 1 && caret.selectionEnd > 1 && editor.document.text[caret.selectionEnd - 1] == '\n') { - caret.moveToOffset(caret.selectionEnd - 2) + caret.moveToInlayAwareOffset(caret.selectionEnd - 2) } else { - caret.moveToOffset(caret.selectionEnd - 1) + caret.moveToInlayAwareOffset(caret.selectionEnd - 1) } } } @@ -263,7 +253,7 @@ private fun setVisualSelection(selectionStart: Int, selectionEnd: Int, caret: Ca if (lastColumn >= MotionGroup.LAST_COLUMN) { aCaret.vimSetSystemSelectionSilently(aCaret.selectionStart, lineEndOffset) val newOffset = (lineEndOffset - VimPlugin.getVisualMotion().selectionAdj).coerceAtLeast(lineStartOffset) - aCaret.moveToOffset(newOffset) + aCaret.moveToInlayAwareOffset(newOffset) } val visualPosition = editor.offsetToVisualPosition(aCaret.selectionEnd) if (aCaret.offset == aCaret.selectionEnd && visualPosition != aCaret.visualPosition) { @@ -280,7 +270,7 @@ private fun setVisualSelection(selectionStart: Int, selectionEnd: Int, caret: Ca } } - editor.caretModel.primaryCaret.moveToOffset(selectionEnd) + editor.caretModel.primaryCaret.moveToInlayAwareOffset(selectionEnd) } else -> Unit } diff --git a/src/com/maddyhome/idea/vim/helper/EditorHelper.java b/src/com/maddyhome/idea/vim/helper/EditorHelper.java index a01d73d045..ebf8ad04d3 100644 --- a/src/com/maddyhome/idea/vim/helper/EditorHelper.java +++ b/src/com/maddyhome/idea/vim/helper/EditorHelper.java @@ -20,6 +20,7 @@ import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.editor.*; +import com.intellij.openapi.editor.ex.util.EditorUtil; import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.vfs.VirtualFile; @@ -90,15 +91,15 @@ public static int getLineLength(final @NotNull Editor editor) { * characters if there are "real" tabs in the line. * * @param editor The editor - * @param line The logical line within the file + * @param logicalLine The logical line within the file * @return The number of characters in the specified line */ - public static int getLineLength(final @NotNull Editor editor, final int line) { + public static int getLineLength(final @NotNull Editor editor, final int logicalLine) { if (getLineCount(editor) == 0) { return 0; } else { - return Math.max(0, editor.offsetToLogicalPosition(editor.getDocument().getLineEndOffset(line)).column); + return Math.max(0, editor.offsetToLogicalPosition(editor.getDocument().getLineEndOffset(logicalLine)).column); } } @@ -182,8 +183,18 @@ public static int normalizeScrollOffset(final @NotNull Editor editor, int scroll } /** - * Gets the number of lines than can be displayed on the screen at one time. This is rounded down to the - * nearest whole line if there is a partial line visible at the bottom of the screen. + * Best efforts to ensure the side scroll offset doesn't overlap itself and remains a sensible value. Inline inlays + * can cause this to work incorrectly. + * @param editor The editor to use to normalize the side scroll offset + * @param sideScrollOffset The value of the 'sidescroll' option + * @return The side scroll offset value to use + */ + public static int normalizeSideScrollOffset(final @NotNull Editor editor, int sideScrollOffset) { + return Math.min(sideScrollOffset, getApproximateScreenWidth(editor) / 2); + } + + /** + * Gets the number of lines than can be displayed on the screen at one time. * * Note that this value is only approximate and should be avoided whenever possible! * @@ -191,52 +202,42 @@ public static int normalizeScrollOffset(final @NotNull Editor editor, int scroll * @return The number of screen lines */ private static int getApproximateScreenHeight(final @NotNull Editor editor) { - int lh = editor.getLineHeight(); - Rectangle area = getVisibleArea(editor); - int height = area.y + area.height - getVisualLineAtTopOfScreen(editor) * lh; - return height / lh; + return getVisibleArea(editor).height / editor.getLineHeight(); } /** - * Gets the number of characters that are visible on a screen line + * Gets the number of characters that are visible on a screen line, based on screen width and assuming a fixed width + * font. It does not include inlays or folds. + * + * Note that this value is only approximate and should be avoided whenever possible! * * @param editor The editor * @return The number of screen columns */ - public static int getScreenWidth(final @NotNull Editor editor) { - Rectangle rect = getVisibleArea(editor); - Point pt = new Point(rect.width, 0); - VisualPosition vp = editor.xyToVisualPosition(pt); - - return vp.column; + public static int getApproximateScreenWidth(final @NotNull Editor editor) { + return getVisibleArea(editor).width / EditorUtil.getPlainSpaceWidth(editor); } /** - * Gets the number of pixels per column of text. - * + * Gets the visual column at the left of the screen for the given visual line. * @param editor The editor - * @return The number of pixels + * @param visualLine The visual line to use to check for inlays and support non-proportional fonts + * @return The visual column number */ - public static int getColumnWidth(final @NotNull Editor editor) { - Rectangle rect = getVisibleArea(editor); - if (rect.width == 0) return 0; - Point pt = new Point(rect.width, 0); - VisualPosition vp = editor.xyToVisualPosition(pt); - if (vp.column == 0) return 0; - - return rect.width / vp.column; + public static int getVisualColumnAtLeftOfScreen(final @NotNull Editor editor, int visualLine) { + final Rectangle area = getVisibleArea(editor); + return getFullVisualColumn(editor, area.x, editor.visualLineToY(visualLine), area.x, area.x + area.width); } /** - * Gets the column currently displayed at the left edge of the editor. - * + * Gets the visual column at the right of the screen for the given visual line. * @param editor The editor - * @return The column number + * @param visualLine The visual line to use to check for inlays and support non-proportional fonts + * @return The visual column number */ - public static int getVisualColumnAtLeftOfScreen(final @NotNull Editor editor) { - int cw = getColumnWidth(editor); - if (cw == 0) return 0; - return (getVisibleArea(editor).x + cw - 1) / cw; + public static int getVisualColumnAtRightOfScreen(final @NotNull Editor editor, int visualLine) { + final Rectangle area = getVisibleArea(editor); + return getFullVisualColumn(editor, area.x + area.width - 1, editor.visualLineToY(visualLine), area.x, area.x + area.width); } /** @@ -462,6 +463,7 @@ public static int getLeadingCharacterOffset(final @NotNull Editor editor, final * @return The file offset of the visual position */ public static int visualPositionToOffset(final @NotNull Editor editor, final @NotNull VisualPosition pos) { + // [202] return editor.visualPositionToOffset(pos); return editor.logicalPositionToOffset(editor.visualToLogicalPosition(pos)); } @@ -617,8 +619,8 @@ public static void scrollVisualLineToCaretLocation(final @NotNull Editor editor, // We try to keep the caret in the same location, but only if there's enough space all around for the line's // inlays. E.g. caret on top screen line and the line has inlays above, or caret on bottom screen line and has // inlays below - final int topInlayHeight = EditorHelper.getHeightOfVisualLineInlays(editor, visualLine, true); - final int bottomInlayHeight = EditorHelper.getHeightOfVisualLineInlays(editor, visualLine, false); + final int topInlayHeight = EditorUtil.getInlaysHeight(editor, visualLine, true); + final int bottomInlayHeight = EditorUtil.getInlaysHeight(editor, visualLine, false); int inlayOffset = 0; if (topInlayHeight > caretScreenOffset) { @@ -639,8 +641,21 @@ public static void scrollVisualLineToCaretLocation(final @NotNull Editor editor, * @return Returns true if the window was moved */ public static boolean scrollVisualLineToTopOfScreen(final @NotNull Editor editor, int visualLine) { - int inlayHeight = getHeightOfVisualLineInlays(editor, visualLine, true); - int y = editor.visualLineToY(visualLine) - inlayHeight; + int y = EditorUtil.getVisualLineAreaStartY(editor, normalizeVisualLine(editor, visualLine)); + + // Normalise Y so that we don't try to scroll the editor to a location it can't reach. The editor will handle this, + // but when we ask for the target location to move the caret to match, we'll get the incorrect value. + // E.g. from line 100 of a 175 line, with line 100 at the top of screen, hit 100. This should scroll line 175 + // to the top of the screen. With virtual space enabled, this is fine. If it's not enabled, we end up scrolling line + // 146 to the top of the screen, but the caret thinks we're going to 175, and the caret is put in the wrong location + // (To complicate things, this issue doesn't show up when running headless for tests) + if (!editor.getSettings().isAdditionalPageAtBottom()) { + // Get the max line number that can sit at the top of the screen + final int editorHeight = getVisibleArea(editor).height; + final int virtualSpaceHeight = editor.getSettings().getAdditionalLinesCount() * editor.getLineHeight(); + final int yLastLine = editor.visualLineToY(EditorHelper.getLineCount(editor)); // last line + 1 + y = Math.min(y, yLastLine + virtualSpaceHeight - editorHeight); + } return scrollVertically(editor, y); } @@ -651,7 +666,7 @@ public static boolean scrollVisualLineToTopOfScreen(final @NotNull Editor editor * @param visualLine The visual line to place in the middle of the current window */ public static void scrollVisualLineToMiddleOfScreen(@NotNull Editor editor, int visualLine) { - int y = editor.visualLineToY(visualLine); + int y = editor.visualLineToY(normalizeVisualLine(editor, visualLine)); int lineHeight = editor.getLineHeight(); int height = getVisibleArea(editor).height; scrollVertically(editor, y - ((height - lineHeight) / 2)); @@ -668,19 +683,76 @@ public static void scrollVisualLineToMiddleOfScreen(@NotNull Editor editor, int * @return True if the editor was scrolled */ public static boolean scrollVisualLineToBottomOfScreen(@NotNull Editor editor, int visualLine) { - int inlayHeight = getHeightOfVisualLineInlays(editor, visualLine, false); int exPanelHeight = 0; - int exPanelWithoutShortcutsHeight = 0; if (ExEntryPanel.getInstance().isActive()) { exPanelHeight = ExEntryPanel.getInstance().getHeight(); } if (ExEntryPanel.getInstanceWithoutShortcuts().isActive()) { - exPanelWithoutShortcutsHeight = ExEntryPanel.getInstanceWithoutShortcuts().getHeight(); + exPanelHeight += ExEntryPanel.getInstanceWithoutShortcuts().getHeight(); + } + final int y = EditorUtil.getVisualLineAreaEndY(editor, normalizeVisualLine(editor, visualLine)) + exPanelHeight; + final Rectangle visibleArea = getVisibleArea(editor); + return scrollVertically(editor, max(0, y - visibleArea.height)); + } + + public static void scrollColumnToLeftOfScreen(@NotNull Editor editor, int visualLine, int visualColumn) { + int targetVisualColumn = visualColumn; + + // Requested column might be an inlay (because we do simple arithmetic on visual position, and inlays and folds have + // a visual position). If it is an inlay and is related to following text, we want to display it, so use it as the + // target column. If it's an inlay related to preceding text, we don't want to display it at the left of the screen, + // show the next column instead + Inlay inlay = editor.getInlayModel().getInlineElementAt(new VisualPosition(visualLine, visualColumn)); + if (inlay != null && inlay.isRelatedToPrecedingText()) { + targetVisualColumn = visualColumn + 1; + } + else if (visualColumn > 0) { + inlay = editor.getInlayModel().getInlineElementAt(new VisualPosition(visualLine, visualColumn - 1)); + if (inlay != null && !inlay.isRelatedToPrecedingText()) { + targetVisualColumn = visualColumn - 1; + } + } + + final int columnLeftX = editor.visualPositionToXY(new VisualPosition(visualLine, targetVisualColumn)).x; + EditorHelper.scrollHorizontally(editor, columnLeftX); + } + + public static void scrollColumnToMiddleOfScreen(@NotNull Editor editor, int visualLine, int visualColumn) { + final Point point = editor.visualPositionToXY(new VisualPosition(visualLine, visualColumn)); + final int screenWidth = EditorHelper.getVisibleArea(editor).width; + + // Snap the column to the nearest standard column grid. This positions us nicely if there are an odd or even number + // of columns. It also works with inline inlays and folds. It is slightly inaccurate for proportional fonts, but is + // still a good solution. Besides, what kind of monster uses Vim with proportional fonts? + final int standardColumnWidth = EditorUtil.getPlainSpaceWidth(editor); + final int x = point.x - (screenWidth / standardColumnWidth / 2 * standardColumnWidth); + EditorHelper.scrollHorizontally(editor, x); + } + + public static void scrollColumnToRightOfScreen(@NotNull Editor editor, int visualLine, int visualColumn) { + int targetVisualColumn = visualColumn; + + // Requested column might be an inlay (because we do simple arithmetic on visual position, and inlays and folds have + // a visual position). If it is an inlay and is related to preceding text, we want to display it, so use it as the + // target column. If it's an inlay related to following text, we don't want to display it at the right of the + // screen, show the previous column + var inlay = editor.getInlayModel().getInlineElementAt(new VisualPosition(visualLine, visualColumn)); + if (inlay != null && !inlay.isRelatedToPrecedingText()) { + targetVisualColumn = visualColumn - 1; } - int y = editor.visualLineToY(visualLine); - int height = inlayHeight + editor.getLineHeight() + exPanelHeight + exPanelWithoutShortcutsHeight; - Rectangle visibleArea = getVisibleArea(editor); - return scrollVertically(editor, y - visibleArea.height + height); + else { + // If the target column is followed by an inlay which is associated with it, make the inlay the target column so + // it is visible + inlay = editor.getInlayModel().getInlineElementAt(new VisualPosition(visualLine, visualColumn + 1)); + if (inlay != null && inlay.isRelatedToPrecedingText()) { + targetVisualColumn = visualColumn + 1; + } + } + + // Scroll to the left edge of the target column, minus a screenwidth, and adjusted for inlays + final int targetColumnRightX = editor.visualPositionToXY(new VisualPosition(visualLine, targetVisualColumn + 1)).x; + final int screenWidth = EditorHelper.getVisibleArea(editor).width; + EditorHelper.scrollHorizontally(editor, targetColumnRightX - screenWidth); } /** @@ -813,6 +885,8 @@ private static int scrollFullPageUp(final @NotNull Editor editor, int pages) { } private static int getFullVisualLine(final @NotNull Editor editor, int y, int topBound, int bottomBound) { + // Note that we ignore inlays here. We're interested in the bounds of the text line. Scrolling will handle inlays as + // it sees fit (e.g. scrolling a line to the bottom will make sure inlays below the line are visible). int line = editor.yToVisualLine(y); int yActual = editor.visualLineToY(line); if (yActual < topBound) { @@ -824,15 +898,55 @@ else if (yActual + editor.getLineHeight() > bottomBound) { return line; } - private static int getHeightOfVisualLineInlays(final @NotNull Editor editor, int visualLine, boolean above) { - InlayModel inlayModel = editor.getInlayModel(); - int inlayHeight = 0; - // [Version Update] 202+ Inlay is parametrized - //noinspection rawtypes - for (Inlay inlay : inlayModel.getBlockElementsForVisualLine(visualLine, above)) { - inlayHeight += inlay.getHeightInPixels(); + private static int getFullVisualColumn(final @NotNull Editor editor, int x, int y, int leftBound, int rightBound) { + // Mapping XY to a visual position will return the position of the closest character, rather than the position of + // the character grid that contains the XY. This means two things. Firstly, we don't get back the visual position of + // an inline inlay, and secondly, we can get the character to the left or right of X. This is the same logic for + // positioning the caret when you click in the editor. + // Note that visualPos.leansRight will be true for the right half side of the character grid + VisualPosition closestVisualPosition = editor.xyToVisualPosition(new Point(x, y)); + + // Make sure we get the character that contains this XY, not the editor's decision about closest character. The + // editor will give us the next character if X is over half way through the character grid. + int xActualLeft = editor.visualPositionToXY(closestVisualPosition).x; + if (xActualLeft > x) { + closestVisualPosition = getPreviousNonInlayVisualPosition(editor, closestVisualPosition); + xActualLeft = editor.visualPositionToXY(closestVisualPosition).x; + } + + if (xActualLeft >= leftBound) { + final int xActualRight = editor.visualPositionToXY(new VisualPosition(closestVisualPosition.line, closestVisualPosition.column + 1)).x - 1; + if (xActualRight <= rightBound) { + return closestVisualPosition.column; + } + + return getPreviousNonInlayVisualPosition(editor, closestVisualPosition).column; + } + else { + return getNextNonInlayVisualPosition(editor, closestVisualPosition).column; + } + } + + private static VisualPosition getNextNonInlayVisualPosition(@NotNull Editor editor, VisualPosition position) { + final InlayModel inlayModel = editor.getInlayModel(); + final int lineLength = EditorHelper.getVisualLineLength(editor, position.line); + position = new VisualPosition(position.line, position.column + 1); + while (position.column < lineLength && inlayModel.hasInlineElementAt(position)) { + position = new VisualPosition(position.line, position.column + 1); + } + return position; + } + + private static VisualPosition getPreviousNonInlayVisualPosition(@NotNull Editor editor, VisualPosition position) { + if (position.column == 0) { + return position; + } + final InlayModel inlayModel = editor.getInlayModel(); + position = new VisualPosition(position.line, position.column - 1); + while (position.column >= 0 && inlayModel.hasInlineElementAt(position)) { + position = new VisualPosition(position.line, position.column - 1); } - return inlayHeight; + return position; } /** diff --git a/src/com/maddyhome/idea/vim/helper/InlayHelper.kt b/src/com/maddyhome/idea/vim/helper/InlayHelper.kt new file mode 100644 index 0000000000..bc56a30f5b --- /dev/null +++ b/src/com/maddyhome/idea/vim/helper/InlayHelper.kt @@ -0,0 +1,74 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.maddyhome.idea.vim.helper + +import com.intellij.injected.editor.EditorWindow +import com.intellij.openapi.editor.* + +/** + * Move the caret to the given offset, handling inline inlays + * + * Inline inlays take up a single visual column. The caret can be positioned on the visual column of the inlay or the + * text. For Vim, we always want to position the caret before the text (when rendered as a block, this means over the + * text, and not over the inlay). Caret.moveToOffset will position itself correctly when an inlay relates to following + * text - it correctly adds one to the visual column. However, it does not add one if the inlay relates to preceding + * text. + * + * I believe this is an incorrect implementation of EditorUtil.inlayAwareOffsetToVisualPosition. When adding an + * inlay, it is added at an offset, and a new visual column is inserted there. When moving to an offset, that visual + * column is always there, regardless of whether the inlay relates to preceding or following text. + * + * It is safe to call this method if the caret hasn't actually moved. In fact, it is a good idea to do so, as it will + * make sure that if the document has changed to place an inlay at the caret position, the caret is re-positioned + * appropriately + */ +fun Caret.moveToInlayAwareOffset(offset: Int) { + // If the target offset is collapsed inside a fold, move directly to the offset, expanding the fold + if (editor.foldingModel.isOffsetCollapsed(offset)) { + moveToOffset(offset) + } + else { + val newVisualPosition = inlayAwareOffsetToVisualPosition(editor, offset) + if (newVisualPosition != visualPosition) { + moveToVisualPosition(newVisualPosition) + } + } +} + +fun Caret.moveToInlayAwareLogicalPosition(pos: LogicalPosition) { + moveToInlayAwareOffset(editor.logicalPositionToOffset(pos)) +} + +// This is the same as EditorUtil.inlayAwareOffsetToVisualPosition, except it always skips the inlay, regardless of +// its "relates to preceding text" state +private fun inlayAwareOffsetToVisualPosition(editor: Editor, offset: Int): VisualPosition { + var logicalPosition = editor.offsetToLogicalPosition(offset) + val e = if (editor is EditorWindow) { + logicalPosition = editor.injectedToHost(logicalPosition) + editor.delegate + } + else { + editor + } + var pos = e.logicalToVisualPosition(logicalPosition) + while (editor.inlayModel.getInlineElementAt(pos) != null) { + pos = VisualPosition(pos.line, pos.column + 1) + } + return pos +} \ No newline at end of file diff --git a/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt b/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt index 46998150af..95bfccea3a 100644 --- a/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt +++ b/src/com/maddyhome/idea/vim/helper/ModeExtensions.kt @@ -78,7 +78,7 @@ fun Editor.exitSelectMode(adjustCaretPosition: Boolean) { val lineEnd = EditorHelper.getLineEndForOffset(this, it.offset) val lineStart = EditorHelper.getLineStartForOffset(this, it.offset) if (it.offset == lineEnd && it.offset != lineStart) { - it.moveToOffset(it.offset - 1) + it.moveToInlayAwareOffset(it.offset - 1) } } } diff --git a/src/com/maddyhome/idea/vim/listener/ListenerManager.kt b/src/com/maddyhome/idea/vim/listener/ListenerManager.kt index 4f7cc31277..79b3a043b9 100644 --- a/src/com/maddyhome/idea/vim/listener/ListenerManager.kt +++ b/src/com/maddyhome/idea/vim/listener/ListenerManager.kt @@ -49,21 +49,8 @@ import com.maddyhome.idea.vim.group.EditorGroup import com.maddyhome.idea.vim.group.FileGroup import com.maddyhome.idea.vim.group.MotionGroup import com.maddyhome.idea.vim.group.SearchGroup -import com.maddyhome.idea.vim.group.visual.IdeaSelectionControl -import com.maddyhome.idea.vim.group.visual.VimVisualTimer -import com.maddyhome.idea.vim.group.visual.moveCaretOneCharLeftFromSelectionEnd -import com.maddyhome.idea.vim.group.visual.vimSetSystemSelectionSilently -import com.maddyhome.idea.vim.helper.EditorHelper -import com.maddyhome.idea.vim.helper.StatisticReporter -import com.maddyhome.idea.vim.helper.disabledForThisEditor -import com.maddyhome.idea.vim.helper.exitSelectMode -import com.maddyhome.idea.vim.helper.exitVisualMode -import com.maddyhome.idea.vim.helper.inSelectMode -import com.maddyhome.idea.vim.helper.inVisualMode -import com.maddyhome.idea.vim.helper.isEndAllowed -import com.maddyhome.idea.vim.helper.isIdeaVimDisabledHere -import com.maddyhome.idea.vim.helper.subMode -import com.maddyhome.idea.vim.helper.vimLastColumn +import com.maddyhome.idea.vim.group.visual.* +import com.maddyhome.idea.vim.helper.* import com.maddyhome.idea.vim.listener.VimListenerManager.EditorListeners.add import com.maddyhome.idea.vim.listener.VimListenerManager.EditorListeners.remove import com.maddyhome.idea.vim.option.OptionsManager @@ -359,7 +346,7 @@ object VimListenerManager { val lineEnd = EditorHelper.getLineEndForOffset(editor, caret.offset) val lineStart = EditorHelper.getLineStartForOffset(editor, caret.offset) cutOffEnd = if (caret.offset == lineEnd && lineEnd != lineStart) { - caret.moveToOffset(caret.offset - 1) + caret.moveToInlayAwareOffset(caret.offset - 1) true } else { false diff --git a/src/com/maddyhome/idea/vim/option/OptionsManager.kt b/src/com/maddyhome/idea/vim/option/OptionsManager.kt index f778f0de4e..44606ee944 100644 --- a/src/com/maddyhome/idea/vim/option/OptionsManager.kt +++ b/src/com/maddyhome/idea/vim/option/OptionsManager.kt @@ -66,8 +66,8 @@ object OptionsManager { val number = addOption(ToggleOption("number", "nu", false)) val relativenumber = addOption(ToggleOption("relativenumber", "rnu", false)) val scroll = addOption(NumberOption("scroll", "scr", 0)) - val scrolljump = addOption(NumberOption("scrolljump", "sj", 1)) - val scrolloff = addOption(NumberOption("scrolloff", "so", 0)) + val scrolljump = addOption(NumberOption(ScrollJumpData.name, "sj", 1, -100, Integer.MAX_VALUE)) + val scrolloff = addOption(NumberOption(ScrollOffData.name, "so", 0)) val selection = addOption(BoundStringOption("selection", "sel", "inclusive", arrayOf("old", "inclusive", "exclusive"))) val selectmode = addOption(SelectModeOptionData.option) val showcmd = addOption(ToggleOption("showcmd", "sc", true)) // Vim: Off by default on platforms with possibly slow tty. On by default elsewhere. @@ -316,7 +316,7 @@ object OptionsManager { cols.sortBy { it.name } extra.sortBy { it.name } - var width = EditorHelper.getScreenWidth(editor) + var width = EditorHelper.getApproximateScreenWidth(editor) if (width < 20) { width = 80 } @@ -552,6 +552,14 @@ object IdeaStatusIcon { val allValues = arrayOf(enabled, gray, disabled) } +object ScrollOffData { + const val name = "scrolloff" +} + +object ScrollJumpData { + const val name = "scrolljump" +} + object StrictMode { val on: Boolean get() = OptionsManager.ideastrictmode.isSet @@ -572,4 +580,4 @@ object IdeaWriteData { const val all = "all" val allValues = arrayOf(all, "file") -} \ No newline at end of file +} diff --git a/src/com/maddyhome/idea/vim/package-info.java b/src/com/maddyhome/idea/vim/package-info.java index 9072e4b257..77858793db 100644 --- a/src/com/maddyhome/idea/vim/package-info.java +++ b/src/com/maddyhome/idea/vim/package-info.java @@ -459,8 +459,8 @@ * |zE| TO BE IMPLEMENTED * |zF| TO BE IMPLEMENTED * |zG| TO BE IMPLEMENTED - * |zH| TO BE IMPLEMENTED - * |zL| TO BE IMPLEMENTED + * |zH| {@link com.maddyhome.idea.vim.action.motion.scroll.MotionScrollHalfWidthLeftAction} + * |zL| {@link com.maddyhome.idea.vim.action.motion.scroll.MotionScrollHalfWidthRightAction} * |zM| {@link com.maddyhome.idea.vim.action.fold.VimCollapseAllRegions} * |zN| TO BE IMPLEMENTED * |zO| {@link com.maddyhome.idea.vim.action.fold.VimExpandRegionRecursively} diff --git a/test/org/jetbrains/plugins/ideavim/NeovimTesting.kt b/test/org/jetbrains/plugins/ideavim/NeovimTesting.kt index 867328df39..89bfa331ce 100644 --- a/test/org/jetbrains/plugins/ideavim/NeovimTesting.kt +++ b/test/org/jetbrains/plugins/ideavim/NeovimTesting.kt @@ -104,6 +104,7 @@ annotation class TestWithoutNeovim(val reason: SkipNeovimReason, val description enum class SkipNeovimReason { PLUGIN, MULTICARET, + INLAYS, OPTION, UNCLEAR, NON_ASCII, diff --git a/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt index 09519f74b1..6650d26531 100644 --- a/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimOptionTestCase.kt @@ -20,6 +20,7 @@ package org.jetbrains.plugins.ideavim import com.maddyhome.idea.vim.option.BoundStringOption import com.maddyhome.idea.vim.option.ListOption +import com.maddyhome.idea.vim.option.NumberOption import com.maddyhome.idea.vim.option.OptionsManager import com.maddyhome.idea.vim.option.ToggleOption @@ -80,6 +81,11 @@ abstract class VimOptionTestCase(option: String, vararg otherOptions: String) : option.set(it.values.first()) } + VimTestOptionType.NUMBER -> { + if (option !is NumberOption) kotlin.test.fail("${it.option} is not a number option. Change it for method `${testMethod.name}`") + + option.set(it.values.first().toInt()) + } } } } @@ -106,5 +112,6 @@ annotation class VimTestOption( enum class VimTestOptionType { LIST, TOGGLE, - VALUE + VALUE, + NUMBER } diff --git a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt index 3c0a7d2e1c..7cc8d27be8 100644 --- a/test/org/jetbrains/plugins/ideavim/VimTestCase.kt +++ b/test/org/jetbrains/plugins/ideavim/VimTestCase.kt @@ -23,11 +23,11 @@ import com.intellij.ide.highlighter.JavaFileType import com.intellij.ide.highlighter.XmlFileType import com.intellij.openapi.application.PathManager import com.intellij.openapi.application.WriteAction -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.editor.* import com.intellij.openapi.editor.colors.EditorColors +import com.intellij.openapi.editor.ex.util.EditorUtil import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx +import com.intellij.openapi.fileTypes.FileType import com.intellij.openapi.fileTypes.PlainTextFileType import com.intellij.openapi.project.Project import com.intellij.testFramework.EditorTestUtil @@ -43,16 +43,22 @@ import com.maddyhome.idea.vim.command.CommandState.SubMode import com.maddyhome.idea.vim.ex.ExOutputModel.Companion.getInstance import com.maddyhome.idea.vim.ex.vimscript.VimScriptGlobalEnvironment import com.maddyhome.idea.vim.group.visual.VimVisualTimer.swingTimer -import com.maddyhome.idea.vim.helper.* +import com.maddyhome.idea.vim.helper.EditorDataContext +import com.maddyhome.idea.vim.helper.EditorHelper import com.maddyhome.idea.vim.helper.RunnableHelper.runWriteCommand +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys import com.maddyhome.idea.vim.helper.StringHelper.stringToKeys +import com.maddyhome.idea.vim.helper.TestInputModel +import com.maddyhome.idea.vim.helper.inBlockSubMode import com.maddyhome.idea.vim.listener.SelectionVimListenerSuppressor +import com.maddyhome.idea.vim.option.OptionsManager import com.maddyhome.idea.vim.option.OptionsManager.getOption import com.maddyhome.idea.vim.option.OptionsManager.ideastrictmode import com.maddyhome.idea.vim.option.OptionsManager.resetAllOptions import com.maddyhome.idea.vim.option.ToggleOption import com.maddyhome.idea.vim.ui.ExEntryPanel -import junit.framework.Assert +import org.junit.Assert +import java.lang.Integer.min import java.util.* import java.util.function.Consumer import javax.swing.KeyStroke @@ -74,6 +80,7 @@ abstract class VimTestCase : UsefulTestCase() { LightTempDirTestFixtureImpl(true)) myFixture.setUp() myFixture.testDataPath = testDataPath + // Note that myFixture.editor is usually null here. It's only set once configureByText has been called KeyHandler.getInstance().fullReset(myFixture.editor) resetAllOptions() VimPlugin.getKey().resetKeyMappings() @@ -120,24 +127,83 @@ abstract class VimTestCase : UsefulTestCase() { return typeText(keys) } - protected fun configureByText(content: String): Editor { - myFixture.configureByText(PlainTextFileType.INSTANCE, content) + protected val screenWidth: Int + get() = 80 + protected val screenHeight: Int + get() = 35 + + protected fun setEditorVisibleSize(width: Int, height: Int) { + EditorTestUtil.setEditorVisibleSize(myFixture.editor, width, height) + } + + protected fun configureByText(content: String) = configureByText(PlainTextFileType.INSTANCE, content) + protected fun configureByJavaText(content: String) = configureByText(JavaFileType.INSTANCE, content) + protected fun configureByXmlText(content: String) = configureByText(XmlFileType.INSTANCE, content) + + private fun configureByText(fileType: FileType, content: String): Editor { + myFixture.configureByText(fileType, content) + setEditorVisibleSize(screenWidth, screenHeight) return myFixture.editor } protected fun configureByFileName(fileName: String): Editor { myFixture.configureByText(fileName, "\n") + setEditorVisibleSize(screenWidth, screenHeight) return myFixture.editor } - protected fun configureByJavaText(content: String): Editor { - myFixture.configureByText(JavaFileType.INSTANCE, content) - return myFixture.editor + @Suppress("SameParameterValue") + protected fun configureByPages(pageCount: Int) { + val stringBuilder = StringBuilder() + repeat(pageCount * screenHeight) { + stringBuilder.appendln("I found it in a legendary land") + } + configureByText(stringBuilder.toString()) } - protected fun configureByXmlText(content: String): Editor { - myFixture.configureByText(XmlFileType.INSTANCE, content) - return myFixture.editor + protected fun configureByLines(lineCount: Int, line: String) { + val stringBuilder = StringBuilder() + repeat(lineCount) { + stringBuilder.appendln(line) + } + configureByText(stringBuilder.toString()) + } + + protected fun configureByColumns(columnCount: Int) { + val content = buildString { + repeat(columnCount) { + append('0' + (it % 10)) + } + } + configureByText(content) + } + + @JvmOverloads + protected fun setPositionAndScroll(scrollToLogicalLine: Int, caretLogicalLine: Int, caretLogicalColumn: Int = 0) { + val scrolloff = min(OptionsManager.scrolloff.value(), screenHeight / 2) + val scrolljump = OptionsManager.scrolljump.value() + OptionsManager.scrolljump.set(1) + + // Convert to visual lines to handle any collapsed folds + val scrollToVisualLine = EditorHelper.logicalLineToVisualLine(myFixture.editor, scrollToLogicalLine) + val bottomVisualLine = scrollToVisualLine + screenHeight - 1 + val bottomLogicalLine = EditorHelper.visualLineToLogicalLine(myFixture.editor, bottomVisualLine) + + // Make sure we're not trying to put caret in an invalid location + val boundsTop = EditorHelper.visualLineToLogicalLine(myFixture.editor, + if (scrollToVisualLine > scrolloff) scrollToVisualLine + scrolloff else scrollToVisualLine) + val boundsBottom = EditorHelper.visualLineToLogicalLine(myFixture.editor, + if (bottomVisualLine > EditorHelper.getVisualLineCount(myFixture.editor) - scrolloff - 1) bottomVisualLine - scrolloff else bottomVisualLine) + Assert.assertTrue("Caret line $caretLogicalLine not inside legal screen bounds (${boundsTop} - ${boundsBottom})", + caretLogicalLine in boundsTop..boundsBottom) + + typeText(parseKeys("${scrollToLogicalLine+scrolloff+1}z", "${caretLogicalLine+1}G", "${caretLogicalColumn+1}|")) + + OptionsManager.scrolljump.set(scrolljump) + + // Make sure we're where we want to be + assertVisibleArea(scrollToLogicalLine, bottomLogicalLine) + assertPosition(caretLogicalLine, caretLogicalColumn) } protected fun typeText(keys: List): Editor { @@ -168,6 +234,13 @@ abstract class VimTestCase : UsefulTestCase() { Assert.assertEquals(LogicalPosition(line, column), actualPosition) } + fun assertVisualPosition(visualLine: Int, visualColumn: Int) { + val carets = myFixture.editor.caretModel.allCarets + Assert.assertEquals("Wrong amount of carets", 1, carets.size) + val actualPosition = carets[0].visualPosition + Assert.assertEquals(VisualPosition(visualLine, visualColumn), actualPosition) + } + fun assertOffset(vararg expectedOffsets: Int) { val carets = myFixture.editor.caretModel.allCarets if (expectedOffsets.size == 2 && carets.size == 1) { @@ -179,6 +252,35 @@ abstract class VimTestCase : UsefulTestCase() { } } + // Use logical rather than visual lines, so we can correctly test handling of collapsed folds and soft wraps + fun assertVisibleArea(topLogicalLine: Int, bottomLogicalLine: Int) { + val actualVisualTop = EditorHelper.getVisualLineAtTopOfScreen(myFixture.editor) + val actualLogicalTop = EditorHelper.visualLineToLogicalLine(myFixture.editor, actualVisualTop) + val actualVisualBottom = EditorHelper.getVisualLineAtBottomOfScreen(myFixture.editor) + val actualLogicalBottom = EditorHelper.visualLineToLogicalLine(myFixture.editor, actualVisualBottom) + + Assert.assertEquals("Top logical lines don't match", topLogicalLine, actualLogicalTop) + Assert.assertEquals("Bottom logical lines don't match", bottomLogicalLine, actualLogicalBottom) + } + + fun assertVisibleLineBounds(logicalLine: Int, leftLogicalColumn: Int, rightLogicalColumn: Int) { + val visualLine = EditorHelper.logicalLineToVisualLine(myFixture.editor, logicalLine) + val actualLeftVisualColumn = EditorHelper.getVisualColumnAtLeftOfScreen(myFixture.editor, visualLine) + val actualLeftLogicalColumn = myFixture.editor.visualToLogicalPosition(VisualPosition(visualLine, actualLeftVisualColumn)).column + val actualRightVisualColumn = EditorHelper.getVisualColumnAtRightOfScreen(myFixture.editor, visualLine) + val actualRightLogicalColumn = myFixture.editor.visualToLogicalPosition(VisualPosition(visualLine, actualRightVisualColumn)).column + + val expected = ScreenBounds(leftLogicalColumn, rightLogicalColumn) + val actual = ScreenBounds(actualLeftLogicalColumn, actualRightLogicalColumn) + Assert.assertEquals(expected, actual) + } + + private data class ScreenBounds(val leftLogicalColumn: Int, val rightLogicalColumn: Int) { + override fun toString(): String { + return "[$leftLogicalColumn-$rightLogicalColumn]" + } + } + fun assertMode(expectedMode: CommandState.Mode) { val mode = CommandState.getInstance(myFixture.editor).mode Assert.assertEquals(expectedMode, mode) @@ -253,7 +355,7 @@ abstract class VimTestCase : UsefulTestCase() { } private fun performTest(keys: String, after: String, modeAfter: CommandState.Mode, subModeAfter: SubMode) { - typeText(StringHelper.parseKeys(keys)) + typeText(parseKeys(keys)) myFixture.checkResult(after) assertState(modeAfter, subModeAfter) } @@ -284,6 +386,14 @@ abstract class VimTestCase : UsefulTestCase() { protected val fileManager: FileEditorManagerEx get() = FileEditorManagerEx.getInstanceEx(myFixture.project) + protected fun addInlay(offset: Int, relatesToPrecedingText: Boolean, widthInColumns: Int): Inlay<*> { + // Enforce deterministic tests for inlays. Default text char width is different per platform (e.g. Windows is 7 and + // Mac is 8) and using the same inlay width on all platforms can cause columns to be on or off screen unexpectedly. + // If inlay width is related to character width, we will scale correctly across different platforms + val columnWidth = EditorUtil.getPlainSpaceWidth(myFixture.editor) + return EditorTestUtil.addInlay(myFixture.editor, offset, relatesToPrecedingText, widthInColumns * columnWidth)!! + } + companion object { const val c = EditorTestUtil.CARET_TAG const val s = EditorTestUtil.SELECTION_START_TAG @@ -311,9 +421,9 @@ abstract class VimTestCase : UsefulTestCase() { @JvmStatic fun commandToKeys(command: String): List { val keys: MutableList = ArrayList() - keys.addAll(StringHelper.parseKeys(":")) + keys.addAll(parseKeys(":")) keys.addAll(stringToKeys(command)) - keys.addAll(StringHelper.parseKeys("")) + keys.addAll(parseKeys("")) return keys } @@ -321,9 +431,9 @@ abstract class VimTestCase : UsefulTestCase() { fun searchToKeys(pattern: String, forwards: Boolean): List { val keys: MutableList = ArrayList() - keys.addAll(StringHelper.parseKeys(if (forwards) "/" else "?")) + keys.addAll(parseKeys(if (forwards) "/" else "?")) keys.addAll(stringToKeys(pattern)) - keys.addAll(StringHelper.parseKeys("")) + keys.addAll(parseKeys("")) return keys } diff --git a/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteCharacterLeftActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteCharacterLeftActionTest.kt new file mode 100644 index 0000000000..406f523b1b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteCharacterLeftActionTest.kt @@ -0,0 +1,116 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.change.delete + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +// |X| +class DeleteCharacterLeftActionTest : VimTestCase() { + fun `test delete single character`() { + val keys = parseKeys("X") + val before = "I f${c}ound it in a legendary land" + val after = "I ${c}ound it in a legendary land" + configureByText(before) + typeText(keys) + myFixture.checkResult(after) + } + + fun `test delete multiple characters`() { + val keys = parseKeys("5X") + val before = "I found$c it in a legendary land" + val after = "I $c it in a legendary land" + configureByText(before) + typeText(keys) + myFixture.checkResult(after) + } + + fun `test deletes min of count and start of line`() { + val keys = parseKeys("25X") + val before = """ + A Discovery + + I found$c it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent() + val after = """ + A Discovery + + $c it in a legendary land + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent() + configureByText(before) + typeText(keys) + myFixture.checkResult(after) + } + + fun `test delete with inlay relating to preceding text`() { + val keys = parseKeys("X") + val before = "I fo${c}und it in a legendary land" + val after = "I f${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 3 ('o'). The 'u' is moved to the right one visual column, and now lives at offset + // 4, visual column 5. + // Kotlin type annotations are a real world example of inlays related to preceding text. + // Hitting 'X' on the character before the inlay should place the cursor after the inlay + // Before: "I fo«:test»|u|nd it in a legendary land." + // After: "I f«:test»|u|nd it in a legendary land." + addInlay(4, true, 5) + + typeText(keys) + myFixture.checkResult(after) + + // It doesn't matter if the inlay is related to preceding or following text. Deleting visual column 3 moves the + // inlay one visual column to the left, from column 4 to 3. The cursor starts at offset 4, pushed to 5 by the inlay. + // 'X' moves the cursor one column to the left (along with the text), which puts it at offset 4. But offset 4 can + // now mean visual column 3 or 4 - the inlay or the text. Make sure the cursor is positioned on the text. + assertVisualPosition(0, 4) + } + + fun `test delete with inlay relating to following text`() { + // This should have the same behaviour as related to preceding text + val keys = parseKeys("X") + val before = "I fo${c}und it in a legendary land" + val after = "I f${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 5 ('u' - because the inlay pushes it one visual column to the right). + // Kotlin parameter hints are a real world example of inlays related to following text. + // Hitting 'X' on the character before the inlay should place the cursor after the inlay + // Before: "I fo«test:»|u|nd it in a legendary land." + // After: "I f«test:»|u|nd it in a legendary land." + addInlay(4, true, 5) + + typeText(keys) + myFixture.checkResult(after) + + // It doesn't matter if the inlay is related to preceding or following text. Deleting visual column 3 moves the + // inlay one visual column to the left, from column 4 to 3. The cursor starts at offset 4, pushed to 5 by the inlay. + // 'X' moves the cursor one column to the left (along with the text), which puts it at offset 4. But offset 4 can + // now mean visual column 3 or 4 - the inlay or the text. Make sure the cursor is positioned on the text. + assertVisualPosition(0, 4) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteCharacterRightActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteCharacterRightActionTest.kt new file mode 100644 index 0000000000..934b14cb5b --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/change/delete/DeleteCharacterRightActionTest.kt @@ -0,0 +1,120 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.change.delete + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import org.jetbrains.plugins.ideavim.VimTestCase + +// |x| +class DeleteCharacterRightActionTest : VimTestCase() { + fun `test delete single character`() { + val keys = parseKeys("x") + val before = "I ${c}found it in a legendary land" + val after = "I ${c}ound it in a legendary land" + configureByText(before) + typeText(keys) + myFixture.checkResult(after) + } + + fun `test delete multiple characters`() { + val keys = parseKeys("5x") + val before = "I ${c}found it in a legendary land" + val after = "I $c it in a legendary land" + configureByText(before) + typeText(keys) + myFixture.checkResult(after) + } + + fun `test deletes min of count and end of line`() { + val keys = parseKeys("20x") + val before = """ + A Discovery + + I found it in a legendary l${c}and + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent() + val after = """ + A Discovery + + I found it in a legendary ${c}l + all rocks and lavender and tufted grass, + where it was settled on some sodden sand + hard by the torrent of a mountain pass. + """.trimIndent() + configureByText(before) + typeText(keys) + myFixture.checkResult(after) + } + + fun `test delete with inlay relating to preceding text`() { + val keys = parseKeys("x") + val before = "I f${c}ound it in a legendary land" + val after = "I f${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 3 ('o'). The 'u' is moved to the right one visual column, and now lives at offset + // 4, visual column 5. + // Kotlin type annotations are a real world example of inlays related to preceding text. + // Hitting 'x' on the character before the inlay should place the cursor after the inlay + // Before: "I f|o|«:test»und it in a legendary land." + // After: "I f«:test»|u|nd it in a legendary land." + addInlay(4, true, 5) + + typeText(keys) + myFixture.checkResult(after) + + // It doesn't matter if the inlay is related to preceding or following text. Deleting visual column 3 moves the + // inlay one visual column to the left, from column 4 to 3. 'x' doesn't move the logical position/offset of the + // cursor, but offset 3 can now refer to the inlay as well as text - visual column 3 and 4. Make sure the cursor is + // positioned on the text, not the inlay. + // Note that the inlay isn't deleted - deleting a character from the end of a variable name shouldn't delete the + // type annotation + assertVisualPosition(0, 4) + } + + fun `test delete with inlay relating to following text`() { + // This should have the same behaviour as related to preceding text + val keys = parseKeys("x") + val before = "I f${c}ound it in a legendary land" + val after = "I f${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 5 ('u' - because the inlay pushes it one visual column to the right). + // Kotlin parameter hints are a real world example of inlays related to following text. + // Hitting 'x' on the character before the inlay should place the cursor after the inlay + // Before: "I f|o|«test:»und it in a legendary land." + // After: "I f«test:»|u|nd it in a legendary land." + addInlay(4, true, 5) + + typeText(keys) + myFixture.checkResult(after) + + // It doesn't matter if the inlay is related to preceding or following text. Deleting visual column 3 moves the + // inlay one visual column to the left, from column 4 to 3. 'x' doesn't move the logical position/offset of the + // cursor, but offset 3 can now refer to the inlay as well as text - visual column 3 and 4. Make sure the cursor is + // positioned on the text, not the inlay. + // Note that the inlay isn't deleted - deleting a character from the end of a variable name shouldn't delete the + // type annotation + assertVisualPosition(0, 4) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowLeftActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowLeftActionTest.kt index 2ed6aeb74e..41c2cd9a80 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowLeftActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowLeftActionTest.kt @@ -21,16 +21,57 @@ package org.jetbrains.plugins.ideavim.action.motion.leftright import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys import com.maddyhome.idea.vim.option.KeyModelOptionData -import org.jetbrains.plugins.ideavim.SkipNeovimReason -import org.jetbrains.plugins.ideavim.TestWithoutNeovim -import org.jetbrains.plugins.ideavim.VimOptionDefaultAll -import org.jetbrains.plugins.ideavim.VimOptionTestCase -import org.jetbrains.plugins.ideavim.VimOptionTestConfiguration -import org.jetbrains.plugins.ideavim.VimTestOption -import org.jetbrains.plugins.ideavim.VimTestOptionType +import org.jetbrains.plugins.ideavim.* class MotionArrowLeftActionTest : VimOptionTestCase(KeyModelOptionData.name) { + @VimOptionDefaultAll + fun `test with inlay related to preceding text`() { + val keys = parseKeys("h") + val before = "I fou${c}nd it in a legendary land" + val after = "I fo${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 5 ('u' - because the inlay pushes it one visual column to the right). + // Kotlin parameter hints are a real world example of inlays related to following text. + // Hitting 'l' on the character before the inlay should place the cursor after the inlay + // Before: "I f|o|«test:»und it in a legendary land." + // After: "I f«test:»|u|nd it in a legendary land." + addInlay(4, true, 5) + + typeText(keys) + myFixture.checkResult(after) + + // The cursor starts at offset 5 and moves to offset 4. Offset 4 contains both the inlay and the next character, at + // visual positions 4 and 5 respectively. We always want the cursor to move to the next character, not the inlay. + assertVisualPosition(0, 5) + } + + @VimOptionDefaultAll + fun `test with inlay related to following text`() { + val keys = parseKeys("h") + val before = "I fou${c}nd it in a legendary land" + val after = "I fo${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 5 ('u' - because the inlay pushes it one visual column to the right). + // Kotlin parameter hints are a real world example of inlays related to following text. + // Hitting 'l' on the character before the inlay should place the cursor after the inlay + // Before: "I f|o|«test:»und it in a legendary land." + // After: "I fo«test:»|u|nd it in a legendary land." + addInlay(4, false, 5) + + typeText(keys) + myFixture.checkResult(after) + + // The cursor starts at offset 5 and moves to offset 4. Offset 4 contains both the inlay and the next character, at + // visual positions 4 and 5 respectively. We always want the cursor to move to the next character, not the inlay. + assertVisualPosition(0, 5) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @VimOptionDefaultAll fun `test visual default options`() { diff --git a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt index 2fec62c7dd..7d12fcc75b 100644 --- a/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt +++ b/test/org/jetbrains/plugins/ideavim/action/motion/leftright/MotionArrowRightActionTest.kt @@ -21,16 +21,57 @@ package org.jetbrains.plugins.ideavim.action.motion.leftright import com.maddyhome.idea.vim.command.CommandState +import com.maddyhome.idea.vim.helper.StringHelper import com.maddyhome.idea.vim.option.KeyModelOptionData -import org.jetbrains.plugins.ideavim.SkipNeovimReason -import org.jetbrains.plugins.ideavim.TestWithoutNeovim -import org.jetbrains.plugins.ideavim.VimOptionDefaultAll -import org.jetbrains.plugins.ideavim.VimOptionTestCase -import org.jetbrains.plugins.ideavim.VimOptionTestConfiguration -import org.jetbrains.plugins.ideavim.VimTestOption -import org.jetbrains.plugins.ideavim.VimTestOptionType +import org.jetbrains.plugins.ideavim.* class MotionArrowRightActionTest : VimOptionTestCase(KeyModelOptionData.name) { + @VimOptionDefaultAll + fun `test with inlay related to preceding text`() { + val keys = StringHelper.parseKeys("l") + val before = "I f${c}ound it in a legendary land" + val after = "I fo${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 5 ('u' - because the inlay pushes it one visual column to the right). + // Kotlin parameter hints are a real world example of inlays related to following text. + // Hitting 'l' on the character before the inlay should place the cursor after the inlay + // Before: "I f|o|«test:»und it in a legendary land." + // After: "I f«test:»|u|nd it in a legendary land." + addInlay(4, true, 5) + + typeText(keys) + myFixture.checkResult(after) + + // The cursor starts at offset 3 and moves to offset 4. Offset 4 contains both the inlay and the next character, at + // visual positions 4 and 5 respectively. We always want the cursor to move to the next character, not the inlay. + assertVisualPosition(0, 5) + } + + @VimOptionDefaultAll + fun `test with inlay related to following text`() { + val keys = StringHelper.parseKeys("l") + val before = "I f${c}ound it in a legendary land" + val after = "I fo${c}und it in a legendary land" + configureByText(before) + + // The inlay is inserted at offset 4 (0 based) - the 'u' in "found". It occupies visual column 4, and is associated + // with the text in visual column 5 ('u' - because the inlay pushes it one visual column to the right). + // Kotlin parameter hints are a real world example of inlays related to following text. + // Hitting 'l' on the character before the inlay should place the cursor after the inlay + // Before: "I f|o|«test:»und it in a legendary land." + // After: "I fo«test:»|u|nd it in a legendary land." + addInlay(4, false, 5) + + typeText(keys) + myFixture.checkResult(after) + + // The cursor starts at offset 3 and moves to offset 4. Offset 4 contains both the inlay and the next character, at + // visual positions 4 and 5 respectively. We always want the cursor to move to the next character, not the inlay. + assertVisualPosition(0, 5) + } + @TestWithoutNeovim(SkipNeovimReason.OPTION) @VimOptionDefaultAll fun `test visual default options`() { diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollColumnLeftActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollColumnLeftActionTest.kt new file mode 100644 index 0000000000..e7c8f3e670 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollColumnLeftActionTest.kt @@ -0,0 +1,139 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* +z or *zl* *z* +zl Move the view on the text [count] characters to the + right, thus scroll the text [count] characters to the + left. This only works when 'wrap' is off. + */ +class ScrollColumnLeftActionTest : VimTestCase() { + fun `test scrolls column to left`() { + configureByColumns(200) + typeText(parseKeys("100|", "zl")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 60, 139) + } + + fun `test scrolls column to left with zRight`() { + configureByColumns(200) + typeText(parseKeys("100|", "z")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 60, 139) + } + + fun `test scroll first column to left moves cursor`() { + configureByColumns(200) + typeText(parseKeys("100|", "zs", "zl")) + assertPosition(0, 100) + assertVisibleLineBounds(0, 100, 179) + } + + fun `test scrolls count columns to left`() { + configureByColumns(200) + typeText(parseKeys("100|", "10zl")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 69, 148) + } + + fun `test scrolls count columns to left with zRight`() { + configureByColumns(200) + typeText(parseKeys("100|", "10z")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 69, 148) + } + + fun `test scrolls column to left with sidescrolloff moves cursor`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("100|", "zs", "zl")) + assertPosition(0, 100) + assertVisibleLineBounds(0, 90, 169) + } + + fun `test scroll column to left ignores sidescroll`() { + OptionsManager.sidescroll.set(10) + configureByColumns(200) + typeText(parseKeys("100|")) + // Assert we got initial scroll correct + // sidescroll=10 means we don't get the sidescroll jump of half a screen and the cursor is positioned at the right edge + assertPosition(0, 99) + assertVisibleLineBounds(0, 20, 99) + + // Scrolls, but doesn't use sidescroll jump + typeText(parseKeys("zl")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 21, 100) + } + + fun `test scroll column to left on last page enters virtual space`() { + configureByColumns(200) + typeText(parseKeys("200|", "ze", "zl")) + assertPosition(0, 199) + assertVisibleLineBounds(0, 121, 200) + typeText(parseKeys("zl")) + assertPosition(0, 199) + assertVisibleLineBounds(0, 122, 201) + typeText(parseKeys("zl")) + assertPosition(0, 199) + assertVisibleLineBounds(0, 123, 202) + } + + @VimBehaviorDiffers(description = "Vim has virtual space at end of line") + fun `test scroll columns to left on last page does not have full virtual space`() { + configureByColumns(200) + typeText(parseKeys("200|", "ze", "50zl")) + assertPosition(0, 199) + // Vim is 179-258 + // See also editor.settings.additionalColumnCount + assertVisibleLineBounds(0, 123, 202) + } + + fun `test scroll column to left correctly scrolls inline inlay associated with preceding text`() { + configureByColumns(200) + addInlay(67, true, 5) + typeText(parseKeys("100|")) + // Text at start of line is: 456:test7 + assertVisibleLineBounds(0, 64, 138) + typeText(parseKeys("2zl")) // 6:test7 + assertVisibleLineBounds(0, 66, 140) + typeText(parseKeys("zl")) // 7 + assertVisibleLineBounds(0, 67, 146) + } + + fun `test scroll column to left correctly scrolls inline inlay associated with following text`() { + configureByColumns(200) + addInlay(67, false, 5) + typeText(parseKeys("100|")) + // Text at start of line is: 456test:78 + assertVisibleLineBounds(0, 64, 138) + typeText(parseKeys("2zl")) // 6test:78 + assertVisibleLineBounds(0, 66, 140) + typeText(parseKeys("zl")) // test:78 + assertVisibleLineBounds(0, 67, 141) + typeText(parseKeys("zl")) // 8 + assertVisibleLineBounds(0, 68, 147) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollColumnRightActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollColumnRightActionTest.kt new file mode 100644 index 0000000000..2114420e76 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollColumnRightActionTest.kt @@ -0,0 +1,165 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* +z or *zh* *z* +zh Move the view on the text [count] characters to the + left, thus scroll the text [count] characters to the + right. This only works when 'wrap' is off. + */ +class ScrollColumnRightActionTest : VimTestCase() { + fun `test scrolls column to right`() { + configureByColumns(200) + typeText(parseKeys("100|", "zh")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 58, 137) + } + + fun `test scrolls column to right with zLeft`() { + configureByColumns(200) + typeText(parseKeys("100|", "z")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 58, 137) + } + + @VimBehaviorDiffers(description = "Vim has virtual space at the end of line. IdeaVim will scroll up to length of longest line") + fun `test scroll last column to right moves cursor 1`() { + configureByColumns(200) + typeText(parseKeys("$")) + // Assert we got initial scroll correct + // We'd need virtual space to scroll this. We're over 200 due to editor.settings.additionalColumnsCount + assertVisibleLineBounds(0, 123, 202) + + typeText(parseKeys("zh")) + assertPosition(0, 199) + assertVisibleLineBounds(0, 122, 201) + } + + @VimBehaviorDiffers(description = "Vim has virtual space at the end of line. IdeaVim will scroll up to length of longest line") + fun `test scroll last column to right moves cursor 2`() { + configureByText(buildString { + repeat(300) { append("0") } + appendln() + repeat(200) { append("0") } + }) + typeText(parseKeys("j$")) + // Assert we got initial scroll correct + // Note, this matches Vim - we've scrolled to centre (but only because the line above allows us to scroll without + // virtual space) + assertVisibleLineBounds(1, 159, 238) + + typeText(parseKeys("zh")) + assertPosition(1, 199) + assertVisibleLineBounds(1, 158, 237) + } + + fun `test scrolls count columns to right`() { + configureByColumns(200) + typeText(parseKeys("100|", "10zh")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 49, 128) + } + + fun `test scrolls count columns to right with zLeft`() { + configureByColumns(200) + typeText(parseKeys("100|", "10z")) + assertPosition(0, 99) + assertVisibleLineBounds(0, 49, 128) + } + + fun `test scrolls column to right with sidescrolloff moves cursor`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("100|", "ze", "zh")) + assertPosition(0, 98) + assertVisibleLineBounds(0, 29, 108) + } + + fun `test scroll column to right ignores sidescroll`() { + OptionsManager.sidescroll.set(10) + configureByColumns(200) + typeText(parseKeys("100|")) + // Assert we got initial scroll correct + // sidescroll=10 means we don't get the sidescroll jump of half a screen and the cursor is positioned at the right edge + assertPosition(0, 99) + assertVisibleLineBounds(0, 20, 99) + + typeText(parseKeys("zh")) // Moves cursor, but not by sidescroll jump + assertPosition(0, 98) + assertVisibleLineBounds(0, 19, 98) + } + + fun `test scroll column to right on first page does nothing`() { + configureByColumns(200) + typeText(parseKeys("10|", "zh")) + assertPosition(0, 9) + assertVisibleLineBounds(0, 0, 79) + } + + fun `test scroll column to right correctly scrolls inline inlay associated with preceding text`() { + configureByColumns(200) + addInlay(130, true, 5) + typeText(parseKeys("100|")) + // Text at end of line is: 89:inlay0123 + assertVisibleLineBounds(0, 59, 133) // 75 characters wide + typeText(parseKeys("3zh")) // 89:inlay0 + assertVisibleLineBounds(0, 56, 130) // 75 characters + typeText(parseKeys("zh")) // 89:inlay + assertVisibleLineBounds(0, 55, 129) // 75 characters + typeText(parseKeys("zh")) // 8 + assertVisibleLineBounds(0, 49, 128) // 80 characters + } + + fun `test scroll column to right correctly scrolls inline inlay associated with following text`() { + configureByColumns(200) + addInlay(130, false, 5) + typeText(parseKeys("100|")) + // Text at end of line is: 89inlay:0123 + assertVisibleLineBounds(0, 59, 133) // 75 characters wide + typeText(parseKeys("3zh")) // 89inlay:0 + assertVisibleLineBounds(0, 56, 130) // 75 characters + typeText(parseKeys("zh")) // 89 + assertVisibleLineBounds(0, 50, 129) // 80 characters + typeText(parseKeys("zh")) // 9 + assertVisibleLineBounds(0, 49, 128) // 80 characters + } + + fun `test scroll column to right with preceding inline inlay moves cursor at end of screen`() { + configureByColumns(200) + addInlay(90, false, 5) + typeText(parseKeys("100|", "ze", "zh")) + assertPosition(0, 98) + assertVisibleLineBounds(0, 24, 98) + typeText(parseKeys("zh")) + assertPosition(0, 97) + assertVisibleLineBounds(0, 23, 97) + typeText(parseKeys("zh")) + assertPosition(0, 96) + assertVisibleLineBounds(0, 22, 96) + typeText(parseKeys("zh")) + assertPosition(0, 95) + assertVisibleLineBounds(0, 21, 95) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenColumnActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenColumnActionTest.kt new file mode 100644 index 0000000000..772fa5bfe0 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenColumnActionTest.kt @@ -0,0 +1,114 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.ex.util.EditorUtil +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.Assert + +/* + *zs* +zs Scroll the text horizontally to position the cursor + at the start (left side) of the screen. This only + works when 'wrap' is off. + */ +class ScrollFirstScreenColumnActionTest : VimTestCase() { + fun `test scroll caret column to first screen column`() { + configureByColumns(200) + typeText(parseKeys("100|", "zs")) + assertVisibleLineBounds(0, 99, 178) + } + + fun `test scroll caret column to first screen column with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("100|", "zs")) + assertVisibleLineBounds(0, 89, 168) + } + + fun `test scroll at or near start of line`() { + configureByColumns(200) + typeText(parseKeys("5|", "zs")) + assertVisibleLineBounds(0, 4, 83) + } + + fun `test scroll at or near start of line with sidescrolloff does nothing`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("5|", "zs")) + assertVisibleLineBounds(0, 0, 79) + } + + @VimBehaviorDiffers(description = "Vim scrolls caret to first screen column, filling with virtual space") + fun `test scroll end of line to first screen column`() { + configureByColumns(200) + typeText(parseKeys("$", "zs")) + // See also editor.settings.isVirtualSpace and editor.settings.additionalColumnsCount + assertVisibleLineBounds(0, 123, 202) + } + + fun `test first screen column includes previous inline inlay associated with following text`() { + // The inlay is associated with the caret, on the left, so should appear before it when scrolling columns + configureByColumns(200) + val inlay = addInlay(99, false, 5) + typeText(parseKeys("100|", "zs")) + val visibleArea = myFixture.editor.scrollingModel.visibleArea + val textWidth = visibleArea.width - inlay.widthInPixels + val availableColumns = textWidth / EditorUtil.getPlainSpaceWidth(myFixture.editor) + + // The first visible text column will be 99, with the inlay positioned to the left of it + assertVisibleLineBounds(0, 99, 99 + availableColumns - 1) + Assert.assertEquals(visibleArea.x, inlay.bounds!!.x) + } + + fun `test first screen column does not include previous inline inlay associated with preceding text`() { + // The inlay is associated with the column before the caret, so should not affect scrolling + configureByColumns(200) + addInlay(99, true, 5) + typeText(parseKeys("100|", "zs")) + assertVisibleLineBounds(0, 99, 178) + } + + fun `test first screen column does not include subsequent inline inlay associated with following text`() { + // The inlay is associated with the column after the caret, so should not affect scrolling + configureByColumns(200) + val inlay = addInlay(100, false, 5) + typeText(parseKeys("100|", "zs")) + val availableColumns = getAvailableColumns(inlay) + assertVisibleLineBounds(0, 99, 99 + availableColumns - 1) + } + + fun `test first screen column does not include subsequent inline inlay associated with preceding text`() { + // The inlay is associated with the caret column, but appears to the right of the column, so does not affect scrolling + configureByColumns(200) + val inlay = addInlay(100, true, 5) + typeText(parseKeys("100|", "zs")) + val availableColumns = getAvailableColumns(inlay) + assertVisibleLineBounds(0, 99, 99 + availableColumns - 1) + } + + private fun getAvailableColumns(inlay: Inlay<*>): Int { + val textWidth = myFixture.editor.scrollingModel.visibleArea.width - inlay.widthInPixels + return textWidth / EditorUtil.getPlainSpaceWidth(myFixture.editor) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLineActionTest.kt new file mode 100644 index 0000000000..65108e8d22 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLineActionTest.kt @@ -0,0 +1,91 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *zt* +zt Like "z", but leave the cursor in the same + column. + */ +class ScrollFirstScreenLineActionTest : VimTestCase() { + fun `test scroll current line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("zt")) + assertPosition(19, 0) + assertVisibleArea(19, 53) + } + + fun `test scroll current line to top of screen and leave cursor in current column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(0, 19, 14) + typeText(StringHelper.parseKeys("zt")) + assertPosition(19, 14) + assertVisibleArea(19, 53) + } + + fun `test scroll current line to top of screen minus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("zt")) + assertPosition(19, 0) + assertVisibleArea(9, 43) + } + + fun `test scrolls count line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("100zt")) + assertPosition(99, 0) + assertVisibleArea(99, 133) + } + + fun `test scrolls count line to top of screen minus scrolloff`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("zt")) + assertPosition(19, 0) + assertVisibleArea(19, 53) + } + + @VimBehaviorDiffers(description = "Virtual space at end of file") + fun `test invalid count scrolls last line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("1000zt")) + assertPosition(175, 0) + assertVisibleArea(146, 175) + } + + fun `test scroll current line to top of screen ignoring scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("zt")) + assertPosition(19, 0) + assertVisibleArea(19, 53) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLinePageStartActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLinePageStartActionTest.kt new file mode 100644 index 0000000000..0fca21c145 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLinePageStartActionTest.kt @@ -0,0 +1,93 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *z+* +z+ Without [count]: Redraw with the line just below the + window at the top of the window. Put the cursor in + that line, at the first non-blank in the line. + With [count]: just like "z". + */ +class ScrollFirstScreenLinePageStartActionTest : VimTestCase() { + fun `test scrolls first line on next page to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("z+")) + assertPosition(35, 0) + assertVisibleArea(35, 69) + } + + fun `test scrolls to first non-blank in line`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(0, 20) + typeText(parseKeys("z+")) + assertPosition(35, 4) + assertVisibleArea(35, 69) + } + + fun `test scrolls first line on next page to scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("z+")) + assertPosition(35, 0) + assertVisibleArea(25, 59) + } + + fun `test scrolls first line on next page ignores scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("z+")) + assertPosition(35, 0) + assertVisibleArea(35, 69) + } + + fun `test count z+ scrolls count line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("100z+")) + assertPosition(99, 0) + assertVisibleArea(99, 133) + } + + fun `test count z+ scrolls count line to top of screen plus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("100z+")) + assertPosition(99, 0) + assertVisibleArea(89, 123) + } + + @VimBehaviorDiffers(description = "Requires virtual space support") + fun `test scroll on penultimate page`() { + configureByPages(5) + setPositionAndScroll(130, 145) + typeText(parseKeys("z+")) + assertPosition(165, 0) + assertVisibleArea(146, 175) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLineStartActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLineStartActionTest.kt new file mode 100644 index 0000000000..1d8f4b262d --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollFirstScreenLineStartActionTest.kt @@ -0,0 +1,92 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *z* +z Redraw, line [count] at top of window (default + cursor line). Put cursor at first non-blank in the + line. + */ +class ScrollFirstScreenLineStartActionTest : VimTestCase() { + fun `test scroll current line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("z")) + assertPosition(19, 0) + assertVisibleArea(19, 53) + } + + fun `test scroll current line to top of screen and move to first non-blank`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(0, 19, 0) + typeText(StringHelper.parseKeys("z")) + assertPosition(19, 4) + assertVisibleArea(19, 53) + } + + fun `test scroll current line to top of screen minus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("z")) + assertPosition(19, 0) + assertVisibleArea(9, 43) + } + + fun `test scrolls count line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("100z")) + assertPosition(99, 0) + assertVisibleArea(99, 133) + } + + fun `test scrolls count line to top of screen minus scrolloff`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("z")) + assertPosition(19, 0) + assertVisibleArea(19, 53) + } + + @VimBehaviorDiffers(description = "Virtual space at end of file") + fun `test invalid count scrolls last line to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("1000z")) + assertPosition(175, 0) + assertVisibleArea(146, 175) + } + + fun `test scroll current line to top of screen ignoring scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + typeText(StringHelper.parseKeys("z")) + assertPosition(19, 0) + assertVisibleArea(19, 53) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt new file mode 100644 index 0000000000..c20cadce11 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageDownActionTest.kt @@ -0,0 +1,114 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import junit.framework.Assert +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *CTRL-D* +CTRL-D Scroll window Downwards in the buffer. The number of + lines comes from the 'scroll' option (default: half a + screen). If [count] given, first set 'scroll' option + to [count]. The cursor is moved the same number of + lines down in the file (if possible; when lines wrap + and when hitting the end of the file there may be a + difference). When the cursor is on the last line of + the buffer nothing happens and a beep is produced. + See also 'startofline' option. + */ +class ScrollHalfPageDownActionTest : VimTestCase() { + fun `test scroll half window downwards keeps cursor on same relative line`() { + configureByPages(5) + setPositionAndScroll(20, 25) + typeText(parseKeys("")) + assertPosition(42, 0) + assertVisibleArea(37, 71) + } + + fun `test scroll downwards on last line causes beep`() { + configureByPages(5) + setPositionAndScroll(146, 175) + typeText(parseKeys("")) + assertPosition(175, 0) + assertVisibleArea(146, 175) + assertTrue(VimPlugin.isError()) + } + + fun `test scroll downwards in bottom half of last page moves to the last line`() { + configureByPages(5) + setPositionAndScroll(146, 165) + typeText(parseKeys("")) + assertPosition(175, 0) + assertVisibleArea(146, 175) + } + + fun `test scroll downwards in top half of last page moves cursor down half a page`() { + configureByPages(5) + setPositionAndScroll(146, 150) + typeText(parseKeys("")) + assertPosition(167, 0) + assertVisibleArea(146, 175) + } + + fun `test scroll count lines downwards`() { + configureByPages(5) + setPositionAndScroll(100, 130) + typeText(parseKeys("10")) + assertPosition(140, 0) + assertVisibleArea(110, 144) + } + + fun `test scroll count downwards modifies scroll option`() { + configureByPages(5) + setPositionAndScroll(100, 110) + typeText(parseKeys("10")) + Assert.assertEquals(OptionsManager.scroll.value(), 10) + } + + fun `test scroll downwards uses scroll option`() { + OptionsManager.scroll.set(10) + configureByPages(5) + setPositionAndScroll(100, 110) + typeText(parseKeys("")) + assertPosition(120, 0) + assertVisibleArea(110, 144) + } + + fun `test count scroll downwards is limited to single page`() { + configureByPages(5) + setPositionAndScroll(100, 110) + typeText(parseKeys("1000")) + assertPosition(145, 0) + assertVisibleArea(135, 169) + } + + @VimBehaviorDiffers(description = "IdeaVim does not support the 'startofline' options") + fun `test scroll downwards puts cursor on first non-blank column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(20, 25, 14) + typeText(parseKeys("")) + assertPosition(42, 4) + assertVisibleArea(37, 71) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt new file mode 100644 index 0000000000..c9cc5bd3a1 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfPageUpActionTest.kt @@ -0,0 +1,107 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import junit.framework.Assert +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *CTRL-U* +CTRL-U Scroll window Upwards in the buffer. The number of + lines comes from the 'scroll' option (default: half a + screen). If [count] given, first set the 'scroll' + option to [count]. The cursor is moved the same + number of lines up in the file (if possible; when + lines wrap and when hitting the end of the file there + may be a difference). When the cursor is on the first + line of the buffer nothing happens and a beep is + produced. See also 'startofline' option. + */ +class ScrollHalfPageUpActionTest : VimTestCase() { + fun `test scroll half window upwards keeps cursor on same relative line`() { + configureByPages(5) + setPositionAndScroll(50, 60) + typeText(parseKeys("")) + assertPosition(43, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll upwards on first line causes beep`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("")) + assertPosition(0, 0) + assertVisibleArea(0, 34) + assertTrue(VimPlugin.isError()) + } + + fun `test scroll upwards in first half of first page moves to first line`() { + configureByPages(5) + setPositionAndScroll(5, 10) + typeText(parseKeys("")) + assertPosition(0, 0) + assertVisibleArea(0, 34) + } + + fun `test scroll count lines upwards`() { + configureByPages(5) + setPositionAndScroll(50, 53) + typeText(parseKeys("10")) + assertPosition(43, 0) + assertVisibleArea(40, 74) + } + + fun `test scroll count modifies scroll option`() { + configureByPages(5) + setPositionAndScroll(50, 53) + typeText(parseKeys("10")) + Assert.assertEquals(OptionsManager.scroll.value(), 10) + } + + fun `test scroll upwards uses scroll option`() { + OptionsManager.scroll.set(10) + configureByPages(5) + setPositionAndScroll(50, 53) + typeText(parseKeys("")) + assertPosition(43, 0) + assertVisibleArea(40, 74) + } + + fun `test count scroll upwards is limited to a single page`() { + configureByPages(5) + setPositionAndScroll(100, 134) + typeText(parseKeys("50")) + assertPosition(99, 0) + assertVisibleArea(65, 99) + } + + @VimBehaviorDiffers(description = "IdeaVim does not support the 'startofline' options") + fun `test scroll up puts cursor on first non-blank column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(50, 60, 14) + typeText(parseKeys("")) + assertPosition(43, 4) + assertVisibleArea(33, 67) + } +} + diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfWidthLeftActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfWidthLeftActionTest.kt new file mode 100644 index 0000000000..2339373e8f --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfWidthLeftActionTest.kt @@ -0,0 +1,123 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* +For the following four commands the cursor follows the screen. If the +character that the cursor is on is moved off the screen, the cursor is moved +to the closest character that is on the screen. The value of 'sidescroll' is +not used. + + *zH* +zH Move the view on the text half a screenwidth to the + left, thus scroll the text half a screenwidth to the + right. This only works when 'wrap' is off. + +[count] is used but undocumented. + */ +class ScrollHalfWidthLeftActionTest : VimTestCase() { + fun `test scroll half page width`() { + configureByColumns(200) + typeText(parseKeys("zL")) + assertVisibleLineBounds(0, 40, 119) + } + + fun `test scroll keeps cursor in place if already in scrolled area`() { + configureByColumns(200) + typeText(parseKeys("50|", "zL")) + assertPosition(0, 49) + assertVisibleLineBounds(0, 40, 119) + } + + fun `test scroll moves cursor if moves off screen 1`() { + configureByColumns(200) + typeText(parseKeys("zL")) + assertPosition(0, 40) + assertVisibleLineBounds(0, 40, 119) + } + + fun `test scroll moves cursor if moves off screen 2`() { + configureByColumns(200) + typeText(parseKeys("10|", "zL")) + assertPosition(0, 40) + assertVisibleLineBounds(0, 40, 119) + } + + fun `test scroll count half page widths`() { + configureByColumns(300) + typeText(parseKeys("3zL")) + assertPosition(0, 120) + assertVisibleLineBounds(0, 120, 199) + } + + fun `test scroll half page width with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("zL")) + assertPosition(0, 50) + assertVisibleLineBounds(0, 40, 119) + } + + fun `test scroll half page width ignores sidescroll`() { + OptionsManager.sidescroll.set(10) + configureByColumns(200) + typeText(parseKeys("zL")) + assertPosition(0, 40) + assertVisibleLineBounds(0, 40, 119) + } + + @VimBehaviorDiffers(description = "Vim has virtual space at end of line") + fun `test scroll at end of line does not use full virtual space`() { + configureByColumns(200) + typeText(parseKeys("200|", "ze", "zL")) + assertPosition(0, 199) + assertVisibleLineBounds(0, 123, 202) + } + + @VimBehaviorDiffers(description = "Vim has virtual space at end of line") + fun `test scroll near end of line does not use full virtual space`() { + configureByColumns(200) + typeText(parseKeys("190|", "ze", "zL")) + assertPosition(0, 189) + assertVisibleLineBounds(0, 123, 202) + } + + fun `test scroll includes inlay visual column in half page width`() { + configureByColumns(200) + addInlay(20, true, 5) + typeText(parseKeys("zL")) + // The inlay is included in the count of scrolled visual columns + assertPosition(0, 39) + assertVisibleLineBounds(0, 39, 118) + } + + fun `test scroll with inlay in scrolled area and left of the cursor`() { + configureByColumns(200) + addInlay(20, true, 5) + typeText(parseKeys("30|", "zL")) + // The inlay is included in the count of scrolled visual columns + assertPosition(0, 39) + assertVisibleLineBounds(0, 39, 118) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfWidthRightActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfWidthRightActionTest.kt new file mode 100644 index 0000000000..2e3eb23282 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollHalfWidthRightActionTest.kt @@ -0,0 +1,115 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* +For the following four commands the cursor follows the screen. If the +character that the cursor is on is moved off the screen, the cursor is moved +to the closest character that is on the screen. The value of 'sidescroll' is +not used. + + *zH* +zH Move the view on the text half a screenwidth to the + left, thus scroll the text half a screenwidth to the + right. This only works when 'wrap' is off. + +[count] is used but undocumented. + */ +class ScrollHalfWidthRightActionTest : VimTestCase() { + fun `test scroll half page width`() { + configureByColumns(200) + typeText(parseKeys("200|", "ze", "zH")) + assertPosition(0, 159) + assertVisibleLineBounds(0, 80, 159) + } + + fun `test scroll keeps cursor in place if already in scrolled area`() { + configureByColumns(200) + typeText(parseKeys("100|", "zs", "zH")) + assertPosition(0, 99) + // Scroll right 40 characters 99 -> 59 + assertVisibleLineBounds(0, 59, 138) + } + + fun `test scroll moves cursor if moves off screen`() { + configureByColumns(200) + typeText(parseKeys("100|", "ze", "zH")) + assertPosition(0, 79) + assertVisibleLineBounds(0, 0, 79) + } + + fun `test scroll count half page widths`() { + configureByColumns(400) + typeText(parseKeys("350|", "ze", "3zH")) + assertPosition(0, 229) + assertVisibleLineBounds(0, 150, 229) + } + + fun `test scroll half page width with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("150|", "ze", "zH")) + assertPosition(0, 109) + assertVisibleLineBounds(0, 40, 119) + } + + fun `test scroll half page width ignores sidescroll`() { + OptionsManager.sidescroll.set(10) + configureByColumns(200) + typeText(parseKeys("200|", "ze", "zH")) + assertPosition(0, 159) + assertVisibleLineBounds(0, 80, 159) + } + + fun `test scroll at start of line does nothing`() { + configureByColumns(200) + typeText(parseKeys("zH")) + assertPosition(0, 0) + assertVisibleLineBounds(0, 0, 79) + } + + fun `test scroll near start of line does nothing`() { + configureByColumns(200) + typeText(parseKeys("10|", "zH")) + assertPosition(0, 9) + assertVisibleLineBounds(0, 0, 79) + } + + fun `test scroll includes inlay visual column in half page width`() { + configureByColumns(200) + addInlay(180, true, 5) + typeText(parseKeys("190|", "ze", "zH")) + // The inlay is included in the count of scrolled visual columns + assertPosition(0, 150) + assertVisibleLineBounds(0, 71, 150) + } + + fun `test scroll with inlay and cursor in scrolled area`() { + configureByColumns(200) + addInlay(180, true, 5) + typeText(parseKeys("170|", "ze", "zH")) + // The inlay is after the cursor, and does not affect scrolling + assertPosition(0, 129) + assertVisibleLineBounds(0, 50, 129) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenColumnActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenColumnActionTest.kt new file mode 100644 index 0000000000..fc48c5ec3c --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenColumnActionTest.kt @@ -0,0 +1,132 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.intellij.openapi.editor.Inlay +import com.intellij.openapi.editor.ex.util.EditorUtil +import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.Assert + +/* + *ze* +ze Scroll the text horizontally to position the cursor + at the end (right side) of the screen. This only + works when 'wrap' is off. + */ +class ScrollLastScreenColumnActionTest : VimTestCase() { + fun `test scroll caret column to last screen column`() { + configureByColumns(200) + typeText(StringHelper.parseKeys("100|", "ze")) + assertVisibleLineBounds(0, 20, 99) + } + + fun `test scroll caret column to last screen column with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(StringHelper.parseKeys("100|", "ze")) + assertVisibleLineBounds(0, 30, 109) + } + + fun `test scroll at or near start of line does nothing`() { + configureByColumns(200) + typeText(StringHelper.parseKeys("10|", "ze")) + assertVisibleLineBounds(0, 0, 79) + } + + fun `test scroll end of line to last screen column`() { + configureByColumns(200) + typeText(StringHelper.parseKeys("$", "ze")) + assertVisibleLineBounds(0, 120, 199) + } + + fun `test scroll end of line to last screen column with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(StringHelper.parseKeys("$", "ze")) + // See myFixture.editor.settings.additionalColumnsCount + assertVisibleLineBounds(0, 120, 199) + } + + fun `test scroll caret column to last screen column with sidescrolloff containing an inline inlay`() { + // The offset should include space for the inlay + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + val inlay = addInlay(101, true, 5) + typeText(StringHelper.parseKeys("100|", "ze")) + val availableColumns = getAvailableColumns(inlay) + // Rightmost text column will still be the same, even if it's offset by an inlay + // TODO: Should the offset include the visual column taken up by the inlay? + // Note that the values for this test are -1 when compared to other tests. That's because the inlay takes up a + // visual column, and scrolling doesn't distinguish the type of visual column + // We need to decide if folds and/or inlays should be included in offsets, and figure out how to reasonably implement it + assertVisibleLineBounds(0, 108 - availableColumns + 1, 108) + } + + fun `test last screen column does not include previous inline inlay associated with preceding text`() { + // The inlay is associated with the column before the caret, appears on the left of the caret, so does not affect + // the last visible column + configureByColumns(200) + val inlay = addInlay(99, true, 5) + typeText(StringHelper.parseKeys("100|", "ze")) + val availableColumns = getAvailableColumns(inlay) + assertVisibleLineBounds(0, 99 - availableColumns + 1, 99) + } + + fun `test last screen column does not include previous inline inlay associated with following text`() { + // The inlay is associated with the caret, but appears on the left, so does not affect the last visible column + configureByColumns(200) + val inlay = addInlay(99, false, 5) + typeText(StringHelper.parseKeys("100|", "ze")) + val availableColumns = getAvailableColumns(inlay) + assertVisibleLineBounds(0, 99 - availableColumns + 1, 99) + } + + fun `test last screen column includes subsequent inline inlay associated with preceding text`() { + // The inlay is inserted after the caret and relates to the caret column. It should still be visible + configureByColumns(200) + val inlay = addInlay(100, true, 5) + typeText(StringHelper.parseKeys("100|", "ze")) + val visibleArea = myFixture.editor.scrollingModel.visibleArea + val textWidth = visibleArea.width - inlay.widthInPixels + val availableColumns = textWidth / EditorUtil.getPlainSpaceWidth(myFixture.editor) + + // The last visible text column will be 99, but it will be positioned before the inlay + assertVisibleLineBounds(0, 99 - availableColumns + 1, 99) + + // We have to assert the location of the inlay + Assert.assertEquals(visibleArea.x + textWidth, inlay.bounds!!.x) + Assert.assertEquals(visibleArea.x + visibleArea.width, inlay.bounds!!.x + inlay.bounds!!.width) + } + + fun `test last screen column does not include subsequent inline inlay associated with following text`() { + // The inlay is inserted after the caret, and relates to text after the caret. It should not affect the last visible + // column + configureByColumns(200) + addInlay(100, false, 5) + typeText(StringHelper.parseKeys("100|", "ze")) + assertVisibleLineBounds(0, 20, 99) + } + + private fun getAvailableColumns(inlay: Inlay<*>): Int { + val textWidth = myFixture.editor.scrollingModel.visibleArea.width - inlay.widthInPixels + return textWidth / EditorUtil.getPlainSpaceWidth(myFixture.editor) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLineActionTest.kt new file mode 100644 index 0000000000..394a7cb24c --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLineActionTest.kt @@ -0,0 +1,88 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *zb* +zb Like "z-", but leave the cursor in the same column. + */ +class ScrollLastScreenLineActionTest : VimTestCase() { + fun `test scroll current line to bottom of screen`() { + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("zb")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + fun `test scroll current line to bottom of screen and leave cursor in current column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 60, 14) + typeText(StringHelper.parseKeys("zb")) + assertPosition(60, 14) + assertVisibleArea(26, 60) + } + + fun `test scroll current line to bottom of screen minus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("zb")) + assertPosition(60, 0) + assertVisibleArea(36, 70) + } + + fun `test scrolls count line to bottom of screen`() { + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("100zb")) + assertPosition(99, 0) + assertVisibleArea(65, 99) + } + + fun `test scrolls count line to bottom of screen minus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("100zb")) + assertPosition(99, 0) + assertVisibleArea(75, 109) + } + + fun `test scrolls current line to bottom of screen ignoring scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("zb")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + fun `test scrolls correctly when less than a page to scroll`() { + configureByPages(5) + setPositionAndScroll(5, 15) + typeText(StringHelper.parseKeys("zb")) + assertPosition(15, 0) + assertVisibleArea(0, 34) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLinePageStartActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLinePageStartActionTest.kt new file mode 100644 index 0000000000..92a4e5a04f --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLinePageStartActionTest.kt @@ -0,0 +1,112 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *z^* +z^ Without [count]: Redraw with the line just above the + window at the bottom of the window. Put the cursor in + that line, at the first non-blank in the line. + With [count]: First scroll the text to put the [count] + line at the bottom of the window, then redraw with the + line which is now at the top of the window at the + bottom of the window. Put the cursor in that line, at + the first non-blank in the line. + */ +class ScrollLastScreenLinePageStartActionTest : VimTestCase() { + fun `test scrolls last line on previous page to bottom of screen`() { + configureByPages(5) + setPositionAndScroll(99, 119) + typeText(parseKeys("z^")) + assertPosition(98, 0) + assertVisibleArea(64, 98) + } + + fun `test scrolls to first non-blank in line`() { + configureByLines(200, " I found it in a legendary land") + setPositionAndScroll(99, 119) + typeText(parseKeys("z^")) + assertPosition(98, 4) + assertVisibleArea(64, 98) + } + + fun `test scrolls last line on previous page to scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(99, 119) + typeText(parseKeys("z^")) + assertPosition(98, 0) + assertVisibleArea(74, 108) + } + + fun `test scrolls last line on previous page ignores scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(99, 119) + typeText(parseKeys("z^")) + assertPosition(98, 0) + assertVisibleArea(64, 98) + } + + fun `test count z^ puts count line at bottom of screen then scrolls back a page`() { + configureByPages(5) + setPositionAndScroll(140, 150) + typeText(parseKeys("100z^")) + // Put 100 at the bottom of the page. Top is 66. Scroll back a page so 66 is at bottom of page + assertPosition(65, 0) + assertVisibleArea(31, 65) + } + + fun `test z^ on first page puts cursor on first line 1`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 25) + typeText(parseKeys("z^")) + assertPosition(0, 4) + assertVisibleArea(0, 34) + } + + fun `test z^ on first page puts cursor on first line 2`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 6) + typeText(parseKeys("z^")) + assertPosition(0, 4) + assertVisibleArea(0, 34) + } + + fun `test z^ on first page ignores scrolloff and puts cursor on last line of previous page`() { + OptionsManager.scrolloff.set(10) + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(0, 6) + typeText(parseKeys("z^")) + assertPosition(0, 4) + assertVisibleArea(0, 34) + } + + fun `test z^ on second page puts cursor on previous last line`() { + configureByLines(50, " I found it in a legendary land") + setPositionAndScroll(19, 39) + typeText(parseKeys("z^")) + assertPosition(18, 4) + assertVisibleArea(0, 34) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLineStartActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLineStartActionTest.kt new file mode 100644 index 0000000000..c49d3c3069 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLastScreenLineStartActionTest.kt @@ -0,0 +1,90 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *z-* +z- Redraw, line [count] at bottom of window (default + cursor line). Put cursor at first non-blank in the + line. + */ +class ScrollLastScreenLineStartActionTest : VimTestCase() { + fun `test scroll current line to bottom of screen`() { + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("z-")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + fun `test scroll current line to bottom of screen and move cursor to first non-blank`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 60, 14) + typeText(StringHelper.parseKeys("z-")) + assertPosition(60, 4) + assertVisibleArea(26, 60) + } + + fun `test scroll current line to bottom of screen minus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("z-")) + assertPosition(60, 0) + assertVisibleArea(36, 70) + } + + fun `test scrolls count line to bottom of screen`() { + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("100z-")) + assertPosition(99, 0) + assertVisibleArea(65, 99) + } + + fun `test scrolls count line to bottom of screen minus scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("100z-")) + assertPosition(99, 0) + assertVisibleArea(75, 109) + } + + fun `test scrolls current line to bottom of screen ignoring scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(40, 60) + typeText(StringHelper.parseKeys("z-")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + fun `test scrolls correctly when less than a page to scroll`() { + configureByPages(5) + setPositionAndScroll(5, 15) + typeText(StringHelper.parseKeys("z-")) + assertPosition(15, 0) + assertVisibleArea(0, 34) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLineDownActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLineDownActionTest.kt new file mode 100644 index 0000000000..1ad857bff2 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLineDownActionTest.kt @@ -0,0 +1,102 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *CTRL-E* +CTRL-E Scroll window [count] lines downwards in the buffer. + The text moves upwards on the screen. + Mnemonic: Extra lines. + */ +class ScrollLineDownActionTest : VimTestCase() { + fun `test scroll single line down`() { + configureByPages(5) + setPositionAndScroll(0, 34) + typeText(parseKeys("")) + assertPosition(34, 0) + assertVisibleArea(1, 35) + } + + fun `test scroll line down will keep cursor on screen`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("")) + assertPosition(1, 0) + assertVisibleArea(1, 35) + } + + fun `test scroll count lines down`() { + configureByPages(5) + setPositionAndScroll(0, 34) + typeText(parseKeys("10")) + assertPosition(34, 0) + assertVisibleArea(10, 44) + } + + fun `test scroll count lines down will keep cursor on screen`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("10")) + assertPosition(10, 0) + assertVisibleArea(10, 44) + } + + @VimBehaviorDiffers(description = "Vim has virtual space at the end of the file, IntelliJ (by default) does not") + fun `test too many lines down stops at last line`() { + configureByPages(5) // 5 * 35 = 175 + setPositionAndScroll(100, 100) + typeText(parseKeys("100")) + + // TODO: Enforce virtual space + // Vim will put the caret on line 174, and put that line at the top of the screen + // See com.maddyhome.idea.vim.helper.EditorHelper.scrollVisualLineToTopOfScreen + assertPosition(146, 0) + assertVisibleArea(146, 175) + } + + fun `test scroll down uses scrolloff and moves cursor`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(20, 30) + typeText(parseKeys("")) + assertPosition(31, 0) + assertVisibleArea(21, 55) + } + + fun `test scroll down is not affected by scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(20, 20) + typeText(parseKeys("")) + assertPosition(21, 0) + assertVisibleArea(21, 55) + } + + fun `test scroll down in visual mode`() { + configureByPages(5) + setPositionAndScroll(20, 30) + typeText(parseKeys("Vjjjj", "")) + assertVisibleArea(21, 55) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLineUpActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLineUpActionTest.kt new file mode 100644 index 0000000000..15a415de0f --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollLineUpActionTest.kt @@ -0,0 +1,105 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *CTRL-Y* +CTRL-Y Scroll window [count] lines upwards in the buffer. + The text moves downwards on the screen. + Note: When using the MS-Windows key bindings CTRL-Y is + remapped to redo. + */ +class ScrollLineUpActionTest : VimTestCase() { + fun `test scroll single line up`() { + configureByPages(5) + setPositionAndScroll(29, 29) + typeText(parseKeys("")) + assertPosition(29, 0) + assertVisibleArea(28, 62) + } + + fun `test scroll line up will keep cursor on screen`() { + configureByPages(5) + setPositionAndScroll(29, 63) + typeText(parseKeys("")) + assertPosition(62, 0) + assertVisibleArea(28, 62) + } + + fun `test scroll count lines up`() { + configureByPages(5) + setPositionAndScroll(29, 29) + typeText(parseKeys("10")) + assertPosition(29, 0) + assertVisibleArea(19, 53) + } + + fun `test scroll count lines up will keep cursor on screen`() { + configureByPages(5) + setPositionAndScroll(29, 63) + typeText(parseKeys("10")) + assertPosition(53, 0) + assertVisibleArea(19, 53) + } + + fun `test too many lines up stops at zero`() { + configureByPages(5) + setPositionAndScroll(29, 29) + typeText(parseKeys("100")) + assertPosition(29, 0) + assertVisibleArea(0, 34) + } + + fun `test too many lines up stops at zero and keeps cursor on screen`() { + configureByPages(5) + setPositionAndScroll(59, 59) + typeText(parseKeys("100")) + assertPosition(34, 0) + assertVisibleArea(0, 34) + } + + fun `test scroll up uses scrolloff and moves cursor`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(20, 44) + typeText(parseKeys("")) + assertPosition(43, 0) + assertVisibleArea(19, 53) + } + + fun `test scroll up is not affected by scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(29, 63) + typeText(parseKeys("")) + assertPosition(62, 0) + assertVisibleArea(28, 62) + } + + fun `test scroll line up in visual mode`() { + configureByPages(5) + setPositionAndScroll(29, 29) + typeText(parseKeys("Vjjjj", "")) + assertVisibleArea(28, 62) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollMiddleScreenLineActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollMiddleScreenLineActionTest.kt new file mode 100644 index 0000000000..8eb9e572fc --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollMiddleScreenLineActionTest.kt @@ -0,0 +1,82 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *zz* +zz Like "z.", but leave the cursor in the same column. + Careful: If caps-lock is on, this command becomes + "ZZ": write buffer and exit! + */ +class ScrollMiddleScreenLineActionTest : VimTestCase() { + fun `test scrolls current line to middle of screen`() { + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("zz")) + assertPosition(45, 0) + assertVisibleArea(28, 62) + } + + fun `test scrolls current line to middle of screen and keeps cursor in the same column`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 45, 14) + typeText(parseKeys("zz")) + assertPosition(45, 14) + assertVisibleArea(28, 62) + } + + fun `test scrolls count line to the middle of the screen`() { + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("100zz")) + assertPosition(99, 0) + assertVisibleArea(82, 116) + } + + fun `test scrolls count line ignoring scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("100zz")) + assertPosition(99, 0) + assertVisibleArea(82, 116) + } + + fun `test scrolls correctly when count line is in first half of first page`() { + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("10zz")) + assertPosition(9, 0) + assertVisibleArea(0, 34) + } + + @VimBehaviorDiffers(description = "Virtual space at end of file") + fun `test scrolls last line of file correctly`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("175zz")) + assertPosition(174, 0) + assertVisibleArea(146, 175) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollMiddleScreenLineStartActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollMiddleScreenLineStartActionTest.kt new file mode 100644 index 0000000000..4f0795457c --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollMiddleScreenLineStartActionTest.kt @@ -0,0 +1,82 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + *z.* +z. Redraw, line [count] at center of window (default + cursor line). Put cursor at first non-blank in the + line. + */ +class ScrollMiddleScreenLineStartActionTest : VimTestCase() { + fun `test scrolls current line to middle of screen`() { + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("z.")) + assertPosition(45, 0) + assertVisibleArea(28, 62) + } + + fun `test scrolls current line to middle of screen and moves cursor to first non-blank`() { + configureByLines(100, " I found it in a legendary land") + setPositionAndScroll(40, 45, 14) + typeText(parseKeys("z.")) + assertPosition(45, 4) + assertVisibleArea(28, 62) + } + + fun `test scrolls count line to the middle of the screen`() { + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("100z.")) + assertPosition(99, 0) + assertVisibleArea(82, 116) + } + + fun `test scrolls count line ignoring scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("100z.")) + assertPosition(99, 0) + assertVisibleArea(82, 116) + } + + fun `test scrolls correctly when count line is in first half of first page`() { + configureByPages(5) + setPositionAndScroll(40, 45) + typeText(parseKeys("10z.")) + assertPosition(9, 0) + assertVisibleArea(0, 34) + } + + @VimBehaviorDiffers(description = "Virtual space at end of file") + fun `test scrolls last line of file correctly`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("175z.")) + assertPosition(174, 0) + assertVisibleArea(146, 175) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt new file mode 100644 index 0000000000..1a9a653f40 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageDownActionTest.kt @@ -0,0 +1,169 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.helper.VimBehaviorDiffers +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + or ** ** + or ** *CTRL-F* +CTRL-F Scroll window [count] pages Forwards (downwards) in + the buffer. See also 'startofline' option. + When there is only one window the 'window' option + might be used. + + move window one page down *i_* + move window one page down *i_* + */ +class ScrollPageDownActionTest : VimTestCase() { + fun `test scroll single page down with S-Down`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll single page down with PageDown`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll single page down with CTRL-F`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll page down in insert mode with S-Down`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("i", "")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll page down in insert mode with PageDown`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("i", "")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll count pages down with S-Down`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("3")) + assertPosition(99, 0) + assertVisibleArea(99, 133) + } + + fun `test scroll count pages down with PageDown`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("3")) + assertPosition(99, 0) + assertVisibleArea(99, 133) + } + + fun `test scroll count pages down with CTRL-F`() { + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("3")) + assertPosition(99, 0) + assertVisibleArea(99, 133) + } + + fun `test scroll page down moves cursor to top of screen`() { + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll page down in insert mode moves cursor`() { + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("i", "")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll page down moves cursor with scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("")) + assertPosition(43, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll page down in insert mode moves cursor with scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 20) + typeText(parseKeys("i", "")) + assertPosition(43, 0) + assertVisibleArea(33, 67) + } + + fun `test scroll page down ignores scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 0) + typeText(parseKeys("")) + assertPosition(33, 0) + assertVisibleArea(33, 67) + } + + @VimBehaviorDiffers(description = "IntelliJ does not have virtual space enabled by default") + fun `test scroll page down on last page moves cursor to end of file`() { + configureByPages(5) + setPositionAndScroll(145, 150) + typeText(parseKeys("")) + assertPosition(175, 0) + assertVisibleArea(146, 175) + } + + fun `test scroll page down on penultimate page`() { + configureByPages(5) + setPositionAndScroll(110, 130) + typeText(parseKeys("")) + assertPosition(143, 0) + assertVisibleArea(143, 175) + } + + fun `test scroll page down on last line causes beep`() { + configureByPages(5) + setPositionAndScroll(146, 175) + typeText(parseKeys("")) + assertTrue(VimPlugin.isError()) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt new file mode 100644 index 0000000000..a324c4f38d --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/action/scroll/ScrollPageUpActionTest.kt @@ -0,0 +1,168 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.action.scroll + +import com.maddyhome.idea.vim.VimPlugin +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager +import junit.framework.Assert +import org.jetbrains.plugins.ideavim.VimTestCase + +/* + or ** ** + or ** *CTRL-B* +CTRL-B Scroll window [count] pages Backwards (upwards) in the + buffer. See also 'startofline' option. + When there is only one window the 'window' option + might be used. + + move window one page up *i_* + move window one page up *i_* + */ +class ScrollPageUpActionTest : VimTestCase() { + fun `test scroll single page up with S-Up`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll single page up with PageUp`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll single page up with CTRL-B`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up in insert mode with S-Up`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("i", "")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up in insert mode with PageUp`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("i", "")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll count pages up with S-Up`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("3")) + assertPosition(64, 0) + assertVisibleArea(30, 64) + } + + fun `test scroll count pages up with PageUp`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("3")) + assertPosition(64, 0) + assertVisibleArea(30, 64) + } + + fun `test scroll count pages up with CTRL-B`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("3")) + assertPosition(64, 0) + assertVisibleArea(30, 64) + } + + fun `test scroll page up moves cursor to bottom of screen`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up in insert mode moves cursor`() { + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("i", "")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up moves cursor with scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("")) + assertPosition(120, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up in insert mode cursor with scrolloff`() { + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("i", "")) + assertPosition(120, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up ignores scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(129, 149) + typeText(parseKeys("")) + assertPosition(130, 0) + assertVisibleArea(96, 130) + } + + fun `test scroll page up on first page does not move`() { + configureByPages(5) + setPositionAndScroll(0, 25) + typeText(parseKeys("")) + assertPosition(25, 0) + assertVisibleArea(0, 34) + } + + fun `test scroll page up on first page causes beep`() { + configureByPages(5) + setPositionAndScroll(0, 25) + typeText(parseKeys("")) + Assert.assertTrue(VimPlugin.isError()) + } + + fun `test scroll page up on second page moves cursor to previous top`() { + configureByPages(5) + setPositionAndScroll(10, 35) + typeText(parseKeys("")) + assertPosition(11, 0) + assertVisibleArea(0, 34) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_ScrollCaretIntoViewHorizontally_Test.kt b/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_ScrollCaretIntoViewHorizontally_Test.kt new file mode 100644 index 0000000000..be84857ade --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_ScrollCaretIntoViewHorizontally_Test.kt @@ -0,0 +1,174 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.group.motion + +import com.intellij.openapi.editor.ex.util.EditorUtil +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +@Suppress("ClassName") +class MotionGroup_ScrollCaretIntoViewHorizontally_Test : VimTestCase() { + fun `test moving right scrolls half screen to right by default`() { + configureByColumns(200) + typeText(parseKeys("80|", "l")) // 1 based + assertPosition(0, 80) // 0 based + assertVisibleLineBounds(0, 40, 119) // 0 based + } + + fun `test moving right scrolls half screen to right by default 2`() { + configureByColumns(200) + setEditorVisibleSize(100, screenHeight) + typeText(parseKeys("100|", "l")) + assertVisibleLineBounds(0, 50, 149) + } + + fun `test moving right scrolls half screen if moving too far 1`() { + configureByColumns(400) + typeText(parseKeys("70|", "41l")) // Move more than half screen width, but scroll less + assertVisibleLineBounds(0, 70, 149) + } + + fun `test moving right scrolls half screen if moving too far 2`() { + configureByColumns(400) + typeText(parseKeys("50|", "200l")) // Move and scroll more than half screen width + assertVisibleLineBounds(0, 209, 288) + } + + fun `test moving right with sidescroll 1`() { + OptionsManager.sidescroll.set(1) + configureByColumns(200) + typeText(parseKeys("80|", "l")) + assertVisibleLineBounds(0, 1, 80) + } + + fun `test moving right with sidescroll 2`() { + OptionsManager.sidescroll.set(2) + configureByColumns(200) + typeText(parseKeys("80|", "l")) + assertVisibleLineBounds(0, 2, 81) + } + + fun `test moving right with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("70|", "l")) + assertVisibleLineBounds(0, 30, 109) + } + + fun `test moving right with sidescroll and sidescrolloff`() { + OptionsManager.sidescroll.set(1) + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("70|", "l")) + assertVisibleLineBounds(0, 1, 80) + } + + fun `test moving right with large sidescrolloff keeps cursor centred`() { + OptionsManager.sidescrolloff.set(999) + configureByColumns(200) + typeText(parseKeys("50|", "l")) + assertVisibleLineBounds(0, 10, 89) + } + + fun `test moving right with inline inlay`() { + OptionsManager.sidescroll.set(1) + configureByColumns(200) + val inlay = addInlay(110, true, 5) + typeText(parseKeys("100|", "20l")) + // These columns are hard to calculate, because the visible offset depends on the rendered width of the inlay + // Also, because we're scrolling right (adding columns to the right) we make the right most column line up + val textWidth = myFixture.editor.scrollingModel.visibleArea.width - inlay.widthInPixels + val availableColumns = textWidth / EditorUtil.getPlainSpaceWidth(myFixture.editor) + assertVisibleLineBounds(0, 119 - availableColumns + 1, 119) + } + + fun `test moving left scrolls half screen to left by default`() { + configureByColumns(200) + typeText(parseKeys("80|zs", "h")) + assertPosition(0, 78) + assertVisibleLineBounds(0, 38, 117) + } + + fun `test moving left scrolls half screen to left by default 2`() { + configureByColumns(200) + setEditorVisibleSize(100, screenHeight) + typeText(parseKeys("100|zs", "h")) + assertVisibleLineBounds(0, 48, 147) + } + + fun `test moving left scrolls half screen if moving too far 1`() { + configureByColumns(400) + typeText(parseKeys("170|zs", "41h")) // Move more than half screen width, but scroll less + assertVisibleLineBounds(0, 88, 167) + } + + fun `test moving left scrolls half screen if moving too far 2`() { + configureByColumns(400) + typeText(parseKeys("290|zs", "200h")) // Move more than half screen width, but scroll less + assertVisibleLineBounds(0, 49, 128) + } + + fun `test moving left with sidescroll 1`() { + OptionsManager.sidescroll.set(1) + configureByColumns(200) + typeText(parseKeys("100|zs", "h")) + assertVisibleLineBounds(0, 98, 177) + } + + fun `test moving left with sidescroll 2`() { + OptionsManager.sidescroll.set(2) + configureByColumns(200) + typeText(parseKeys("100|zs", "h")) + assertVisibleLineBounds(0, 97, 176) + } + + fun `test moving left with sidescrolloff`() { + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("120|zs", "h")) + assertVisibleLineBounds(0, 78, 157) + } + + fun `test moving left with sidescroll and sidescrolloff`() { + OptionsManager.sidescroll.set(1) + OptionsManager.sidescrolloff.set(10) + configureByColumns(200) + typeText(parseKeys("120|zs", "h")) + assertVisibleLineBounds(0, 108, 187) + } + + fun `test moving left with inline inlay`() { + OptionsManager.sidescroll.set(1) + configureByColumns(200) + val inlay = addInlay(110, true, 5) + typeText(parseKeys("120|zs", "20h")) + // These columns are hard to calculate, because the visible offset depends on the rendered width of the inlay + val textWidth = myFixture.editor.scrollingModel.visibleArea.width - inlay.widthInPixels + val availableColumns = textWidth / EditorUtil.getPlainSpaceWidth(myFixture.editor) + assertVisibleLineBounds(0, 99, 99 + availableColumns - 1) + } + + fun `test moving left with large sidescrolloff keeps cursor centred`() { + OptionsManager.sidescrolloff.set(999) + configureByColumns(200) + typeText(parseKeys("50|", "h")) + assertVisibleLineBounds(0, 8, 87) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_ScrollCaretIntoViewVertically_Test.kt b/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_ScrollCaretIntoViewVertically_Test.kt new file mode 100644 index 0000000000..c726bbd70c --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_ScrollCaretIntoViewVertically_Test.kt @@ -0,0 +1,235 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.group.motion + +import com.maddyhome.idea.vim.helper.EditorHelper +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.OptionsManager +import org.jetbrains.plugins.ideavim.VimTestCase + +@Suppress("ClassName") +class MotionGroup_ScrollCaretIntoViewVertically_Test : VimTestCase() { + fun `test moving up causes scrolling up`() { + configureByPages(5) + setPositionAndScroll(19, 24) + + typeText(parseKeys("12k")) + assertPosition(12, 0) + assertVisibleArea(12, 46) + } + + fun `test scroll up with scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(19, 24) + + typeText(parseKeys("12k")) + assertPosition(12, 0) + assertVisibleArea(3, 37) + } + + fun `test scroll up with scrolloff`() { + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(19, 29) + + typeText(parseKeys("12k")) + assertPosition(17, 0) + assertVisibleArea(12, 46) + } + + fun `test scroll up with scrolljump and scrolloff 1`() { + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + configureByPages(5) + + setPositionAndScroll(19, 29) + typeText(parseKeys("12k")) + assertPosition(17, 0) + assertVisibleArea(8, 42) + } + + fun `test scroll up with scrolljump and scrolloff 2`() { + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(29, 39) + + typeText(parseKeys("20k")) + assertPosition(19, 0) + assertVisibleArea(10, 44) + } + + fun `test scroll up with collapsed folds`() { + configureByPages(5) + // TODO: Implement zf + typeText(parseKeys("40G", "Vjjjj", ":'<,'>action CollapseSelection", "V")) + setPositionAndScroll(29, 49) + + typeText(parseKeys("30k")) + assertPosition(15, 0) + assertVisibleArea(15, 53) + } + + // TODO: Handle soft wraps +// fun `test scroll up with soft wraps`() { +// } + + fun `test scroll up more than half height moves caret to middle 1`() { + configureByPages(5) + setPositionAndScroll(115, 149) + + typeText(parseKeys("50k")) + assertPosition(99, 0) + assertVisualLineAtMiddleOfScreen(99) + } + + fun `test scroll up more than half height moves caret to middle with scrolloff`() { + configureByPages(5) + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + setPositionAndScroll(99, 109) + assertPosition(109, 0) + + typeText(parseKeys("21k")) + assertPosition(88, 0) + assertVisualLineAtMiddleOfScreen(88) + } + + fun `test scroll up with less than half height moves caret to top of screen`() { + configureByPages(5) + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + setPositionAndScroll(99, 109) + + typeText(parseKeys("20k")) + assertPosition(89, 0) + assertVisibleArea(80, 114) + } + + fun `test moving down causes scrolling down`() { + configureByPages(5) + setPositionAndScroll(0, 29) + + typeText(parseKeys("12j")) + assertPosition(41, 0) + assertVisibleArea(7, 41) + } + + fun `test scroll down with scrolljump`() { + OptionsManager.scrolljump.set(10) + configureByPages(5) + setPositionAndScroll(0, 29) + + typeText(parseKeys("12j")) + assertPosition(41, 0) + assertVisibleArea(11, 45) + } + + fun `test scroll down with scrolloff`() { + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 24) + + typeText(parseKeys("12j")) + assertPosition(36, 0) + assertVisibleArea(7, 41) + } + + fun `test scroll down with scrolljump and scrolloff 1`() { + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 24) + + typeText(parseKeys("12j")) + assertPosition(36, 0) + assertVisibleArea(10, 44) + } + + fun `test scroll down with scrolljump and scrolloff 2`() { + OptionsManager.scrolljump.set(15) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 24) + + typeText(parseKeys("20j")) + assertPosition(44, 0) + assertVisibleArea(17, 51) + } + + fun `test scroll down with scrolljump and scrolloff 3`() { + OptionsManager.scrolljump.set(20) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 24) + + typeText(parseKeys("25j")) + assertPosition(49, 0) + assertVisibleArea(24, 58) + } + + fun `test scroll down with scrolljump and scrolloff 4`() { + OptionsManager.scrolljump.set(11) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 24) + + typeText(parseKeys("12j")) + assertPosition(36, 0) + assertVisibleArea(11, 45) + } + + fun `test scroll down with scrolljump and scrolloff 5`() { + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 29) + + typeText(parseKeys("12j")) + assertPosition(41, 0) + assertVisibleArea(12, 46) + } + + fun `test scroll down with scrolljump and scrolloff 6`() { + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(5) + configureByPages(5) + setPositionAndScroll(0, 24) + + typeText(parseKeys("20j")) + assertPosition(44, 0) + assertVisibleArea(15, 49) + } + + fun `test scroll down too large cursor is centred`() { + OptionsManager.scrolljump.set(10) + OptionsManager.scrolloff.set(10) + configureByPages(5) + setPositionAndScroll(0, 19) + + typeText(parseKeys("35j")) + assertPosition(54, 0) + assertVisualLineAtMiddleOfScreen(54) + } + + private fun assertVisualLineAtMiddleOfScreen(expected: Int) { + assertEquals(expected, EditorHelper.getVisualLineAtMiddleOfScreen(myFixture.editor)) + } +} \ No newline at end of file diff --git a/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_scrolloff_scrolljump_Test.kt b/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_scrolloff_scrolljump_Test.kt new file mode 100644 index 0000000000..3db1124350 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/group/motion/MotionGroup_scrolloff_scrolljump_Test.kt @@ -0,0 +1,215 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +@file:Suppress("ClassName") + +package org.jetbrains.plugins.ideavim.group.motion + +import com.maddyhome.idea.vim.helper.StringHelper.parseKeys +import com.maddyhome.idea.vim.option.ScrollJumpData +import com.maddyhome.idea.vim.option.ScrollOffData +import org.jetbrains.plugins.ideavim.SkipNeovimReason +import org.jetbrains.plugins.ideavim.TestWithoutNeovim +import org.jetbrains.plugins.ideavim.VimOptionTestCase +import org.jetbrains.plugins.ideavim.VimOptionTestConfiguration +import org.jetbrains.plugins.ideavim.VimTestOption +import org.jetbrains.plugins.ideavim.VimTestOptionType + +// These tests are sanity tests for scrolloff and scrolljump, with actions that move the cursor. Other actions that are +// affected by scrolloff or scrolljump should include that in the action specific tests +class MotionGroup_scrolloff_Test : VimOptionTestCase(ScrollOffData.name) { + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["0"])) + fun `test move up shows no context with scrolloff=0`() { + configureByPages(5) + setPositionAndScroll(25, 25) + typeText(parseKeys("k")) + assertPosition(24, 0) + assertVisibleArea(24, 58) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["1"])) + fun `test move up shows context line with scrolloff=1`() { + configureByPages(5) + setPositionAndScroll(25, 26) + typeText(parseKeys("k")) + assertPosition(25, 0) + assertVisibleArea(24, 58) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["10"])) + fun `test move up shows context lines with scrolloff=10`() { + configureByPages(5) + setPositionAndScroll(25, 35) + typeText(parseKeys("k")) + assertPosition(34, 0) + assertVisibleArea(24, 58) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["0"])) + fun `test move down shows no context with scrolloff=0`() { + configureByPages(5) + setPositionAndScroll(25, 59) + typeText(parseKeys("j")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["1"])) + fun `test move down shows context line with scrolloff=1`() { + configureByPages(5) + setPositionAndScroll(25, 58) + typeText(parseKeys("j")) + assertPosition(59, 0) + assertVisibleArea(26, 60) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["10"])) + fun `test move down shows context lines with scrolloff=10`() { + configureByPages(5) + setPositionAndScroll(25, 49) + typeText(parseKeys("j")) + assertPosition(50, 0) + assertVisibleArea(26, 60) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["999"])) + fun `test scrolloff=999 keeps cursor in centre of screen`() { + configureByPages(5) + setPositionAndScroll(25, 42) + typeText(parseKeys("j")) + assertPosition(43, 0) + assertVisibleArea(26, 60) + } +} + +class MotionGroup_scrolljump_Test : VimOptionTestCase(ScrollJumpData.name) { + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["0"])) + fun `test move up scrolls single line with scrolljump=0`() { + configureByPages(5) + setPositionAndScroll(25, 25) + typeText(parseKeys("k")) + assertPosition(24, 0) + assertVisibleArea(24, 58) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["1"])) + fun `test move up scrolls single line with scrolljump=1`() { + configureByPages(5) + setPositionAndScroll(25, 25) + typeText(parseKeys("k")) + assertPosition(24, 0) + assertVisibleArea(24, 58) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["10"])) + fun `test move up scrolls multiple lines with scrolljump=10`() { + configureByPages(5) + setPositionAndScroll(25, 25) + typeText(parseKeys("k")) + assertPosition(24, 0) + assertVisibleArea(15, 49) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["0"])) + fun `test move down scrolls single line with scrolljump=0`() { + configureByPages(5) + setPositionAndScroll(25, 59) + typeText(parseKeys("j")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["1"])) + fun `test move down scrolls single line with scrolljump=1`() { + configureByPages(5) + setPositionAndScroll(25, 59) + typeText(parseKeys("j")) + assertPosition(60, 0) + assertVisibleArea(26, 60) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["10"])) + fun `test move down scrolls multiple lines with scrolljump=10`() { + configureByPages(5) + setPositionAndScroll(25, 59) + typeText(parseKeys("j")) + assertPosition(60, 0) + assertVisibleArea(35, 69) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["-50"])) + fun `test negative scrolljump treated as percentage 1`() { + configureByPages(5) + setPositionAndScroll(39, 39) + typeText(parseKeys("k")) + assertPosition(38, 0) + assertVisibleArea(22, 56) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration(VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["-10"])) + fun `test negative scrolljump treated as percentage 2`() { + configureByPages(5) + setPositionAndScroll(39, 39) + typeText(parseKeys("k")) + assertPosition(38, 0) + assertVisibleArea(36, 70) + } +} + +class MotionGroup_scrolloff_scrolljump_Test : VimOptionTestCase(ScrollJumpData.name, ScrollOffData.name) { + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration( + VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["10"]), + VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["5"]) + ) + fun `test scroll up with scrolloff and scrolljump set`() { + configureByPages(5) + setPositionAndScroll(50, 55) + typeText(parseKeys("k")) + assertPosition(54, 0) + assertVisibleArea(40, 74) + } + + @TestWithoutNeovim(SkipNeovimReason.OPTION) + @VimOptionTestConfiguration( + VimTestOption(ScrollJumpData.name, VimTestOptionType.NUMBER, ["10"]), + VimTestOption(ScrollOffData.name, VimTestOptionType.NUMBER, ["5"]) + ) + fun `test scroll down with scrolloff and scrolljump set`() { + configureByPages(5) + setPositionAndScroll(50, 79) + typeText(parseKeys("j")) + assertPosition(80, 0) + assertVisibleArea(60, 94) + } +} diff --git a/test/org/jetbrains/plugins/ideavim/helper/EditorHelperTest.kt b/test/org/jetbrains/plugins/ideavim/helper/EditorHelperTest.kt new file mode 100644 index 0000000000..867d3c8453 --- /dev/null +++ b/test/org/jetbrains/plugins/ideavim/helper/EditorHelperTest.kt @@ -0,0 +1,65 @@ +/* + * IdeaVim - Vim emulator for IDEs based on the IntelliJ platform + * Copyright (C) 2003-2020 The IdeaVim authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.jetbrains.plugins.ideavim.helper + +import com.maddyhome.idea.vim.helper.EditorHelper +import org.jetbrains.plugins.ideavim.VimTestCase +import org.junit.Assert + +class EditorHelperTest : VimTestCase() { + fun `test scroll column to left of screen`() { + configureByColumns(100) + EditorHelper.scrollColumnToLeftOfScreen(myFixture.editor, 0, 2) + val visibleArea = myFixture.editor.scrollingModel.visibleArea + val columnWidth = visibleArea.width / screenWidth + Assert.assertEquals(2 * columnWidth, visibleArea.x) + } + + fun `test scroll column to right of screen`() { + configureByColumns(100) + val column = screenWidth + 2 + EditorHelper.scrollColumnToRightOfScreen(myFixture.editor, 0, column) + val visibleArea = myFixture.editor.scrollingModel.visibleArea + val columnWidth = visibleArea.width / screenWidth + Assert.assertEquals((column - screenWidth + 1) * columnWidth, visibleArea.x) + } + + fun `test scroll column to middle of screen with even number of columns`() { + configureByColumns(200) + // For an 80 column screen, moving a column to the centre should position it in column 41 (1 based) - 40 columns on + // the left, mid point, 39 columns on the right + // Put column 100 into position 41 -> offset is 59 columns + EditorHelper.scrollColumnToMiddleOfScreen(myFixture.editor, 0, 99) + val visibleArea = myFixture.editor.scrollingModel.visibleArea + val columnWidth = visibleArea.width / screenWidth + Assert.assertEquals(59 * columnWidth, visibleArea.x) + } + + fun `test scroll column to middle of screen with odd number of columns`() { + configureByColumns(200) + setEditorVisibleSize(81, 25) + // For an 81 column screen, moving a column to the centre should position it in column 41 (1 based) - 40 columns on + // the left, mid point, 40 columns on the right + // Put column 100 into position 41 -> offset is 59 columns + EditorHelper.scrollColumnToMiddleOfScreen(myFixture.editor, 0, 99) + val visibleArea = myFixture.editor.scrollingModel.visibleArea + val columnWidth = visibleArea.width / screenWidth + Assert.assertEquals(59 * columnWidth, visibleArea.x) + } +} \ No newline at end of file