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